@@ -78,7 +78,7 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u
7878 the changed keys), ` dead-fields.js ` (retired fields stripped on every write).
7979- ` src/db/ ` — ` index.js ` (the one lazy ` node:sqlite ` connection: WAL, ` busy_timeout ` ,
8080 ` foreign_keys ` , migrations on open, the one-time legacy JSON import behind ` _meta ` flags),
81- ` migrations.js ` (versioned on ` PRAGMA user_version ` , currently 26 — append, never edit),
81+ ` migrations.js ` (versioned on ` PRAGMA user_version ` , currently 29 — append, never edit),
8282 ` import-legacy.js ` , ` fts.js ` (the optional FTS5 ` channel_memory_fts ` index; without FTS5 memory
8383 search degrades to a scan).
8484- ` src/gateway/run.js ` — the run orchestrator: engine adapter selection and precedence (per-run
@@ -166,6 +166,16 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u
166166 ` channel_instructions ` ) that never expire and re-post after a restart, and signed single-use
167167 browser links (` /approve/<token> ` : GET decides nothing, POST decides exactly once and re-checks
168168 authority) for surfaces without Block Kit and for automation.
169+ - ` src/gateway/ssh-access.js ` + ` ssh-broker.js ` + ` ssh-session.js ` + ` src/mcp/tools/ssh-access.js ` — SSH access to
170+ channel containers (` docs/SSH-ACCESS.md ` ): the per-person key registry (` ssh_keys ` ), the
171+ per-channel ` sshUsers ` grant list, the host ` authorized_keys ` export (every line
172+ ` restrict,command= ` ), the in-container sshd files, the session records (` ssh_sessions ` ), and the
173+ daemon's attach socket under ` /var/lib/channelgate-ssh ` (` CHANNELGATE_SSH_DIR ` ) — bound only
174+ once ` scripts/install-ssh-access.sh ` has set the host up. A session authorizes key → user →
175+ channel → grant → not operator-home-mounted, holds a container lease for its whole life, and
176+ pipes the developer's SSH stream into ` exec -i <container> cg-sshd ` (an unprivileged inetd-mode
177+ sshd in the image). ` scripts/cg-ssh-attach.mjs ` is the host wrapper (self-contained, copied
178+ root-owned by the installer). Nothing listens on a port anywhere.
169179- ` src/gateway/{updater,update-state,update-smoke,restart}.js ` + ` scripts/update-* ` — the
170180 transactional self-update (the automatic path is an Enterprise entitlement; ` scripts/update.sh `
171181 stays available to every operator): one durable transaction and lock, a detached built-ins-only
@@ -273,7 +283,8 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u
273283 (process-group kills), ` process-outcome.js ` (human descriptions of exits), ` logger.js ` (the
274284 ` events ` table), ` timezone.js ` (daemon-local time vs UTC containers), ` cron.js ` , ` keyed-lock.js ` ,
275285 ` singleton.js ` , ` drops.js ` , ` tail.js ` .
276- - ` scripts/ ` — install and service (` install.sh ` , ` install-systemd.sh ` , ` install-whisper.mjs ` ),
286+ - ` scripts/ ` — install and service (` install.sh ` , ` install-systemd.sh ` , ` install-ssh-access.sh ` +
287+ ` cg-ssh-attach.mjs ` + ` cg-ssh-authorized-keys ` , ` install-whisper.mjs ` ),
277288 updates (` update.sh ` → ` update-runner.mjs ` ), backup and restore (` backup-config.sh ` ,
278289 ` restore-config.sh ` , ` restore-drill.sh ` , ` runtime-maintenance.mjs ` ), the image
279290 (` build-image.mjs ` ), the checks (` run-tests.mjs ` , ` static-check.mjs ` , ` secret-scan.mjs ` ,
@@ -311,7 +322,8 @@ through the control MCP.
311322 ` thread_overrides ` , ` conversation_reply_sessions ` , ` active_runs ` , ` stopped_turns ` ,
312323 ` inbound_events ` , ` teams_graph_subscriptions ` ); automation (` schedules ` , ` acks ` ,
313324 ` followup_threads ` , ` followup_done ` , ` followup_digest_messages ` , ` bg_jobs ` , ` api_jobs ` );
314- approvals and questions (` approval_requests ` , ` approval_link_tokens ` , ` question_requests ` ); skills (` skills ` , ` skill_revisions ` ,
325+ approvals and questions (` approval_requests ` , ` approval_link_tokens ` , ` question_requests ` ); SSH
326+ access (` ssh_keys ` , ` ssh_sessions ` ); skills (` skills ` , ` skill_revisions ` ,
315327 ` skill_revision_files ` , ` skill_sources ` , ` skill_templates ` , ` skill_usage ` , ` skill_proposals ` ,
316328 ` skill_access_tokens ` ); Composio SDK (` composio_sessions ` ); licensing (` license_usage ` );
317329 dashboard data (` usage ` , ` usage_components ` , ` usage_requests ` , ` usage_repair_batches ` ,
@@ -449,7 +461,14 @@ Config that stays as **files** (read wholesale / bootstrap, hand-editable):
449461 in. A run receives a RELAY of the resolved login's ACCESS token instead, gateway-owned and
450462 applied last in the child env so a channel secret cannot displace it. A turn with no resolvable
451463 login fails closed with the remedy named; it never runs on a guessed credential. A new consumer
452- asks the resolver; it never adds a second notion of "the login".
464+ asks the resolver; it never adds a second notion of "the login". The ONE login file the gateway
465+ writes is an interactive SSH session's (` src/gateway/ssh-session.js ` ): an ACCESS-ONLY
466+ ` .credentials.json ` in the channel's config dir — the relayed token, its expiry and plan facts,
467+ never a refresh token, so it can rotate nothing — plus the operator's account record, both
468+ written through the relay, refreshed with it, and removed when the channel's last session ends.
469+ Claude Code labels a token in the environment "Claude API" and hides the plan, the usage windows
470+ and the plan's default model; only a file login shows them, which is what a developer in a
471+ terminal needs to see.
453472- ** Only admins get ` --dangerously-skip-permissions ` ** , and only in an Admin-mode channel.
454473 Everyone else runs with the folder's ` permissions.allow ` allowlist and answers tool requests
455474 through the ` permission_prompt ` approval card (or Auto mode); headless can't answer interactive
@@ -489,6 +508,13 @@ Config that stays as **files** (read wholesale / bootstrap, hand-editable):
489508 including the ` licenseAdmission() ` call site in ` src/gateway/run.js ` and the ` license_usage `
490509 schema — is a license violation and is sent back in review. Patches to that directory, new
491510 tiers or limits, and any licensing/CLA/trademark text change need a prior discussion.
511+ - ** One worktree per task; never edit a shared checkout's working tree.** Several threads,
512+ schedules and background jobs run in the same folder, and two of them editing one working
513+ tree silently overwrite each other. Before the first edit of any task that touches a tracked
514+ file, open ` .worktrees/<slug> ` on its own branch from ` origin/beta ` and work only there; the
515+ shared checkout stays on ` beta ` , clean, and never changes branch. Uncommitted changes you did
516+ not write belong to another task — leave them and say so. Commands, landing and cleanup are
517+ under ** One worktree per task** in Contributor workflow.
492518- ** Project records:** shipped behavior lives in ` FEATURES.md ` ; the cumulative regression lives in
493519 ` TEST-PLAN.md ` . Keep both aligned with shipped code. Do not create, consult, or update
494520 ` TASKS.md ` , and do not make a brainstorming or standalone plan-writing phase a prerequisite for
@@ -503,6 +529,49 @@ Config that stays as **files** (read wholesale / bootstrap, hand-editable):
503529
504530## Contributor workflow
505531
532+ ### One worktree per task — before your first edit
533+
534+ ** Never edit tracked files in a shared checkout's working tree.** Several threads, schedules and
535+ background jobs can run in the same folder at once, and two of them editing one working tree
536+ overwrite each other's uncommitted changes with no error and no trace. This is not a style
537+ preference and is not waived by a task being small, urgent, one file, or already started. The
538+ failure it prevents is real and has happened here: six unrelated features were built directly in
539+ the served checkout and left uncommitted, interleaved at hunk level across the same shared files,
540+ and could no longer be separated into the six commits they should have been.
541+
542+ Run this before the first edit of any task that creates, modifies or deletes a tracked file
543+ (read-only questions, status checks and analysis need none of it):
544+
545+ ``` sh
546+ git rev-parse --is-inside-work-tree # confirm this is the repo
547+ git fetch origin
548+ grep -qx ' .worktrees/' .git/info/exclude 2> /dev/null || echo ' .worktrees/' >> .git/info/exclude
549+ git worktree add -b < type> /< slug> .worktrees/< slug> origin/beta
550+ ```
551+
552+ - Branch from ` origin/beta ` — never from ` main ` , and never from the shared checkout's current HEAD.
553+ - ` <type> ` is ` feat ` , ` fix ` , ` docs ` , ` chore ` or ` test ` ; ` <slug> ` names the work. If the branch
554+ already exists, another task owns it: pick a different slug.
555+ - Keep worktrees in ` .worktrees/ ` ** inside** the repo folder, so they stay within the channel's
556+ mounted working folder. The ` info/exclude ` line keeps them out of ` git status ` without touching
557+ the tracked ` .gitignore ` .
558+ - Do every edit, test run and commit under ` .worktrees/<slug>/… ` , using paths inside the worktree
559+ rather than the matching path at the repo root. Commit early: only committed work survives a
560+ concurrent thread. Stage only your own files.
561+ - The shared checkout stays on ` beta ` and clean. Never switch its branch, and never edit its files
562+ while a task worktree is open.
563+ - One task, one worktree. Two features are two branches, even when they touch the same file — that
564+ is what keeps them separable later.
565+
566+ Land in the shared checkout (under the deployment's landing lock where one applies): bring ` beta `
567+ current, ` git merge --no-ff <type>/<slug> ` , re-run a proportionate check after any conflict
568+ resolution, then push. Clean up only after ` git merge-base --is-ancestor <type>/<slug> beta `
569+ succeeds, with ` git worktree remove ` followed by ` git branch -d ` — never ` rm -rf ` the directory,
570+ which leaves git's entry ` prunable ` and then blocks the branch delete.
571+
572+ If you find uncommitted changes in the shared checkout that you did not write, they are another
573+ task's work in progress. Leave them alone, say so in your reply, and open your own worktree.
574+
506575### Beta development and stable promotion
507576
508577- ` beta ` is the integration branch for all new development, fixes, documentation and tests.
@@ -533,8 +602,8 @@ Config that stays as **files** (read wholesale / bootstrap, hand-editable):
533602 defaults that say to merge completed tasks into ` main ` . Use the deployment's serialized landing
534603 lock for integration; never switch a shared served checkout to ` main ` merely to publish a release.
535604
536- Use a dedicated branch and worktree from the latest upstream ` beta ` . Target development pull
537- requests at ` beta ` ; completed development work is merged and pushed to ` beta ` . Keep shared
605+ Open the task's own worktree first (see ** One worktree per task ** above; it is not optional).
606+ Target development pull requests at ` beta ` ; completed development work is merged and pushed to ` beta ` . Keep shared
538607integration checkouts clean, stage only your changes (never ` git add -A ` / ` git commit -a ` ), write
539608conventional imperative subjects, sign off every commit under ` CLA.md ` (` git commit -s ` ; the
540609trailer is the CLA acceptance), and open a pull request with the design note and the actual check
0 commit comments