@ccmsg/cli 0.11.3 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +8 -12
  2. package/package.json +1 -1
  3. package/src/auth/auth.ts +23 -23
  4. package/src/auth/http.ts +3 -3
  5. package/src/auth/records.ts +2 -2
  6. package/src/auth/webauthn.ts +1 -1
  7. package/src/cli.ts +10 -10
  8. package/src/daemon/control.ts +1 -1
  9. package/src/daemon/registry.ts +11 -11
  10. package/src/daemon/snapshot.ts +1 -1
  11. package/src/daemon/supervise.ts +4 -4
  12. package/src/dispatch/caller.ts +1 -1
  13. package/src/dispatch/dispatch.ts +4 -4
  14. package/src/dispatch/handler.ts +4 -4
  15. package/src/dispatch/identity.ts +1 -1
  16. package/src/dispatch/result.ts +2 -2
  17. package/src/files/containment.ts +2 -2
  18. package/src/harness/index.ts +1 -1
  19. package/src/instance/client.ts +1 -1
  20. package/src/instance/config.ts +13 -13
  21. package/src/instance/instance.ts +42 -42
  22. package/src/instance/lock.ts +2 -2
  23. package/src/instance/log.ts +1 -1
  24. package/src/instance/paths.ts +6 -6
  25. package/src/instance/socket.ts +1 -1
  26. package/src/kv/store.ts +1 -1
  27. package/src/mesh/instances.ts +2 -2
  28. package/src/mesh/keys.ts +7 -7
  29. package/src/mesh/mesh.ts +67 -67
  30. package/src/mesh/relay.ts +8 -8
  31. package/src/mesh/wire.ts +2 -2
  32. package/src/messaging/delivery.ts +24 -24
  33. package/src/messaging/direct.ts +23 -23
  34. package/src/messaging/inbox.ts +6 -6
  35. package/src/messaging/notify.ts +5 -5
  36. package/src/plugin/codex.ts +1 -1
  37. package/src/sessions/classify.ts +6 -6
  38. package/src/sessions/dump.ts +1 -1
  39. package/src/sessions/handlers.ts +2 -2
  40. package/src/sessions/harness.ts +12 -12
  41. package/src/sessions/last-live.ts +4 -4
  42. package/src/sessions/registry.ts +32 -32
  43. package/src/sessions/status.ts +7 -7
  44. package/src/sessions/terminals.ts +1 -1
  45. package/src/transcript/files.ts +4 -4
  46. package/src/transcript/fold.ts +9 -9
  47. package/src/transcript/items/classify.ts +56 -8
  48. package/src/transcript/read.ts +1 -1
  49. package/src/transcript/tail.ts +3 -3
  50. package/src/transcript/transcripts.ts +7 -7
  51. package/src/translate/helper.ts +2 -2
  52. package/src/transport/conn.ts +3 -3
  53. package/src/transport/dial.ts +1 -1
  54. package/src/transport/driver.ts +2 -2
  55. package/src/transport/entry.ts +1 -1
  56. package/src/transport/framing.ts +1 -1
  57. package/src/transport/listener.ts +3 -3
  58. package/src/transport/uds.ts +2 -2
  59. package/src/transport/ws.ts +2 -2
  60. package/src/upstream/events.ts +3 -3
  61. package/src/upstream/gateway.ts +6 -6
  62. package/src/upstream/json.ts +1 -1
  63. package/src/upstream/requests.ts +6 -6
  64. package/src/upstream/stats.ts +1 -1
  65. package/src/upstream/status.ts +4 -4
  66. package/src/upstream/usage.ts +1 -1
  67. package/src/upstream/webhook.ts +1 -1
package/README.md CHANGED
@@ -2,19 +2,13 @@
2
2
 
3
3
  > 🇯🇵 [README-ja.md](./README-ja.md)
4
4
 
5
- The daemon that serves one **instance** (= one config home) as an endpoint of the contract,
6
- together with its CLI and the plugins it hands to agents.
5
+ The daemon that serves one **instance** (= one config home) as an endpoint of the contract, together with its CLI and the plugins it hands to agents.
7
6
 
8
7
  - `daemon` — holds sessions, delivery, topics and the mesh, and answers the contract's ops over UDS / WS
9
8
  - `cli` — the entry point a session calls its instance through (`ccmsg`)
10
- - `plugin` — the plugin an agent receives (`ccmsg plugin install claude`). Claude Code is the only
11
- agent that has one; a codex plugin is not implemented, and what it would take is tracked in
12
- [`docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md`](./docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md)
9
+ - `plugin` — the plugin an agent receives (`ccmsg plugin install claude`). Claude Code is the only agent that has one; a codex plugin is not implemented, and what it would take is tracked in [`docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md`](./docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md)
13
10
 
14
- The wire contract is [`@ccmsg/protocol`](https://github.com/kawaz/ccmsg-protocol), pinned to a
15
- version here. Who may call what, and what comes back, is decided by the contract's attribute
16
- table and schemas; the daemon reads them rather than carrying validation or authorization
17
- branches of its own.
11
+ The wire contract is [`@ccmsg/protocol`](https://github.com/kawaz/ccmsg-protocol), pinned to a version here. Who may call what, and what comes back, is decided by the contract's attribute table and schemas; the daemon reads them rather than carrying validation or authorization branches of its own.
18
12
 
19
13
  ## What it does not do
20
14
 
@@ -23,12 +17,14 @@ branches of its own.
23
17
  - Re-derive an upstream judgment (the gateway's severity, Claude Code's permission decisions) or observe another config home
24
18
  - Carry validation of its own, or serve v1 alongside — the contract's validator is called, and the new lineage stands as a separate instance
25
19
 
26
- Authenticating a person is not on that list: the daemon answers "who came" itself, with a
27
- passkey. [docs/DESIGN.md](./docs/DESIGN.md) §9 carries the reason each of these ties back to.
20
+ Authenticating a person is not on that list: the daemon answers "who came" itself, with a passkey. [docs/DESIGN.md](./docs/DESIGN.md) §8.6 carries the reason each of these ties back to.
28
21
 
29
22
  ## Documentation
30
23
 
31
- - [docs/DESIGN.md](./docs/DESIGN.md) — layers, delivery, the state model, the mesh, and how it is tested
24
+ - [docs/DESIGN.md](./docs/DESIGN.md) — **what it is now**: the purpose and what is not grown, the contract and the layers, authentication, the state model, transcripts and dumps, topics and delivery, the mesh, operation, and how it is tested
25
+ - [docs/decisions/](./docs/decisions/INDEX.md) — **the record of the judgments (DR)**: why something was decided that way, and what was set aside
26
+ - [docs/ROADMAP.md](./docs/ROADMAP.md) — the bundles of work in the order they are taken, pointing at the issues across the three repos
27
+ - [docs/design/](./docs/design/README.md) — the design details that stand on their own (authentication between instances, the item types of a dump)
32
28
 
33
29
  ## License
34
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/cli",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
package/src/auth/auth.ts CHANGED
@@ -44,7 +44,7 @@ import { CborError } from "./cbor.ts";
44
44
 
45
45
  /** How long an access token is accepted, and how long a refresh token is.
46
46
  *
47
- * Chosen rather than derived, within the DR's "hours" and "days" (§2.4). The
47
+ * Chosen rather than derived, within the DR's "hours" and "days" (DR-0001 §2.4). The
48
48
  * access token's life is also a connection's: a client renews on the
49
49
  * connection it already holds, so the period is what bounds a stolen token
50
50
  * rather than how often a person is interrupted. The refresh token's life is
@@ -84,7 +84,7 @@ export const CODE_ATTEMPTS = 5;
84
84
  /** How many `/auth/*` requests are answered per second, over all callers.
85
85
  *
86
86
  * The routes are reached before anything is proven, like the mesh's key
87
- * endpoint (§6), and the work behind them is a signature verification. The cap
87
+ * endpoint (mesh-peer-auth §6), and the work behind them is a signature verification. The cap
88
88
  * is far above what a person at a keyboard produces and far below what would
89
89
  * cost this instance anything. */
90
90
  export const AUTH_RATE_LIMIT = 30;
@@ -102,7 +102,7 @@ interface Pending {
102
102
  attempts: number;
103
103
  }
104
104
 
105
- /** One challenge this instance issued, good once (§2.6). */
105
+ /** One challenge this instance issued, good once (DR-0001 §2.6). */
106
106
  interface Issued {
107
107
  readonly expiresAt: Timestamp;
108
108
  }
@@ -194,7 +194,7 @@ export class Auth {
194
194
  return (this.deps.now ?? Date.now)();
195
195
  }
196
196
 
197
- // --- issuing a registration URL (§2.2) ---
197
+ // --- issuing a registration URL (DR-0001 §2.2) ---
198
198
 
199
199
  /** Make one registration URL and the code that goes with it.
200
200
  *
@@ -345,7 +345,7 @@ export class Auth {
345
345
  for (const sub of removed) this.disconnect(sub);
346
346
  }
347
347
 
348
- // --- challenges (§2.6) ---
348
+ // --- challenges (DR-0001 §2.6) ---
349
349
 
350
350
  challenge(): AuthChallengeResult {
351
351
  this.#forget();
@@ -365,7 +365,7 @@ export class Auth {
365
365
  }
366
366
 
367
367
  /** Spend a challenge wherever it was issued: here, or at the instance the
368
- * caller says issued it (§2.4, behind a load balancer either may be
368
+ * caller says issued it (DR-0001 §2.4, behind a load balancer either may be
369
369
  * reached). */
370
370
  async #spendAnywhere(challenge: AuthChallenge): Promise<void> {
371
371
  if (challenge.issuer === this.deps.self) {
@@ -402,13 +402,13 @@ export class Auth {
402
402
  return ask(iss, op, args);
403
403
  }
404
404
 
405
- // --- registration (§2.2) ---
405
+ // --- registration (DR-0001 §2.2) ---
406
406
 
407
407
  /** Verify a registration and write the credential down.
408
408
  *
409
409
  * The registration URL is checked where its secret is, which may be another
410
410
  * instance; everything else — the WebAuthn verification, the record — is done
411
- * here, by whoever the browser reached (§2.6). */
411
+ * here, by whoever the browser reached (DR-0001 §2.6). */
412
412
  async register(
413
413
  args: AuthRegisterArgs,
414
414
  from: { ip?: string; userAgent?: string; path?: string } = {},
@@ -493,7 +493,7 @@ export class Auth {
493
493
  *
494
494
  * The claims come back from the issuer having been checked and consumed, and
495
495
  * everything after this — the WebAuthn verification, the record — is done by
496
- * whichever instance the browser actually reached (§2.6). */
496
+ * whichever instance the browser actually reached (DR-0001 §2.6). */
497
497
  async #claimsOf(args: AuthRegisterArgs): Promise<RegisterClaims> {
498
498
  const stated = claimsOf(args.token);
499
499
  if (stated.iss === this.deps.self) return this.resolveRegistration(args.token, args.code);
@@ -530,7 +530,7 @@ export class Auth {
530
530
  if (code === undefined || !equalStrings(code, held.code)) {
531
531
  held.attempts += 1;
532
532
  // The URL itself is spent once the tries are gone, so guessing the code
533
- // costs the whole registration rather than one attempt (§2.2).
533
+ // costs the whole registration rather than one attempt (DR-0001 §2.2).
534
534
  if (held.attempts >= CODE_ATTEMPTS) {
535
535
  this.#pending.delete(stated.jti);
536
536
  throw new OpError(
@@ -544,7 +544,7 @@ export class Auth {
544
544
  return held.claims;
545
545
  }
546
546
 
547
- // --- assertion (§2.5) ---
547
+ // --- assertion (DR-0001 §2.5) ---
548
548
 
549
549
  async assert(
550
550
  args: AuthAssertArgs,
@@ -620,7 +620,7 @@ export class Auth {
620
620
  *
621
621
  * The one the credential was registered under, which the record carries: a
622
622
  * passkey only ever answers for the domain it was made under, and the
623
- * endpoint being reached says nothing about that (§2.3). Nothing is widened
623
+ * endpoint being reached says nothing about that (DR-0001 §2.3). Nothing is widened
624
624
  * to a suffix, and a record from before the field existed names the host of
625
625
  * the endpoint it was registered for. */
626
626
  #rpIdFor(record: CredentialRecord): string[] {
@@ -633,9 +633,9 @@ export class Auth {
633
633
  *
634
634
  * Its own is there because a browser may land here holding a URL another
635
635
  * instance issued — the page it runs the exchange from is then this
636
- * instance's, and the issuer is only asked to spend the URL (§2.6).
636
+ * instance's, and the issuer is only asked to spend the URL (DR-0001 §2.6).
637
637
  *
638
- * Read by the HTTP carrier, which compares them whole (§2.3). Not the relying
638
+ * Read by the HTTP carrier, which compares them whole (DR-0001 §2.3). Not the relying
639
639
  * party: an RP ID is a domain, so a page at any host under it would be let in
640
640
  * — and `/auth/refresh` answers a cookie the browser attaches by domain, so a
641
641
  * sibling subdomain admitted here would read a person's access token. What
@@ -651,7 +651,7 @@ export class Auth {
651
651
  return [...origins];
652
652
  }
653
653
 
654
- // --- tokens (§2.4) ---
654
+ // --- tokens (DR-0001 §2.4) ---
655
655
 
656
656
  /** Make a family for this person, minted by this instance. */
657
657
  mint(sub: Subject): MintedSession {
@@ -673,7 +673,7 @@ export class Auth {
673
673
  * A family is written by its `iss` alone, so a rotation that landed here for
674
674
  * a family minted elsewhere is carried there rather than done here — two
675
675
  * instances rotating one family in parallel would merge by last write and
676
- * read exactly like a stolen token being replayed (§2.4). */
676
+ * read exactly like a stolen token being replayed (DR-0001 §2.4). */
677
677
  async refreshToken(value: Base64Url, from: RefreshFrom = {}): Promise<MintedSession> {
678
678
  const held = this.deps.records.byRefresh(value);
679
679
  if (held === undefined) {
@@ -783,7 +783,7 @@ export class Auth {
783
783
 
784
784
  /** A value that is nobody's standing token but was somebody's: the family it
785
785
  * belonged to is failed, because a token in use twice is a token that was
786
- * taken (§2.4).
786
+ * taken (DR-0001 §2.4).
787
787
  *
788
788
  * Recognised three ways: the standing refresh token past its expiry, the one
789
789
  * before it past its grace, and any generation this instance rotated away
@@ -813,7 +813,7 @@ export class Auth {
813
813
  }
814
814
  }
815
815
 
816
- // --- connections (§2.5) ---
816
+ // --- connections (DR-0001 §2.5) ---
817
817
 
818
818
  /** Whether an access token opens a connection, and until when. */
819
819
  admits(access: Base64Url): { sub: Subject; expiresAt: Timestamp } | undefined {
@@ -861,7 +861,7 @@ export class Auth {
861
861
  return this.#authorized.get(conn)?.expiresAt;
862
862
  }
863
863
 
864
- /** Extend a live connection with a token got from `/auth/refresh` (§2.5). */
864
+ /** Extend a live connection with a token got from `/auth/refresh` (DR-0001 §2.5). */
865
865
  extend(conn: Requester, args: AuthExtendArgs): AuthExtendResult {
866
866
  const held = this.#authorized.get(conn);
867
867
  if (held === undefined) {
@@ -879,7 +879,7 @@ export class Auth {
879
879
  return { auth_expires_at: admitted.expiresAt };
880
880
  }
881
881
 
882
- // --- the rate limit the unauthenticated routes share (§2.4) ---
882
+ // --- the rate limit the unauthenticated routes share (DR-0001 §2.4) ---
883
883
 
884
884
  allowRequest(): boolean {
885
885
  const now = this.#now();
@@ -929,7 +929,7 @@ export function authHandlers(auth: Auth) {
929
929
  }
930
930
  // The digits arrive unjudged from wherever the browser landed, and are
931
931
  // checked here — this is the instance holding both the secret that signed
932
- // the URL and the count of tries against it (§2.2).
932
+ // the URL and the count of tries against it (DR-0001 §2.2).
933
933
  return { kind: "register", claims: auth.resolveRegistration(args.token, args.code) };
934
934
  },
935
935
  "auth.rotate": (input: HandlerInput): AuthRotateResult => {
@@ -1011,7 +1011,7 @@ function token(): Base64Url {
1011
1011
  * A JWS with HS256, because the value travels in a URL fragment and has to
1012
1012
  * survive being carried there: the shape is the conventional one, and the
1013
1013
  * verifier is the issuer itself, so nothing about it is a key anyone else
1014
- * needs (§2.2). */
1014
+ * needs (DR-0001 §2.2). */
1015
1015
  function sign(claims: RegisterClaims, secret: Buffer): string {
1016
1016
  const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
1017
1017
  const body = Buffer.from(JSON.stringify(claims)).toString("base64url");
@@ -1060,7 +1060,7 @@ function challengeIn(clientDataJson: Base64Url): Base64Url {
1060
1060
  return challenge;
1061
1061
  }
1062
1062
 
1063
- /** The host an endpoint names, which is the relying party by default (§2.3). */
1063
+ /** The host an endpoint names, which is the relying party by default (DR-0001 §2.3). */
1064
1064
  export function hostOf(endpoint: Endpoint): string {
1065
1065
  return new URL(endpoint).hostname;
1066
1066
  }
package/src/auth/http.ts CHANGED
@@ -60,7 +60,7 @@ export function endpointPath(pathname: string): string {
60
60
  /** The cookie path for a request: everything up to and including its `/auth/`.
61
61
  *
62
62
  * It narrows what the browser sends where, and nothing more — same-origin
63
- * script can fetch any path, so this is not an authorization boundary (§2.4).
63
+ * script can fetch any path, so this is not an authorization boundary (DR-0001 §2.4).
64
64
  * What it is for is that two endpoints behind one origin (`/` and `/personal`)
65
65
  * get cookies of their own. */
66
66
  export function cookiePath(pathname: string): string {
@@ -102,7 +102,7 @@ export interface AuthRoutesDeps {
102
102
  *
103
103
  * Everything unauthenticated shares one rate limit and one origin check: these
104
104
  * routes are reachable before anything is proven, and the work behind them is a
105
- * signature verification (§2.4). */
105
+ * signature verification (DR-0001 §2.4). */
106
106
  export async function handleAuth(
107
107
  request: Request,
108
108
  deps: AuthRoutesDeps,
@@ -116,7 +116,7 @@ export async function handleAuth(
116
116
  // anything else, including the preflight that would tell it to try. Compared
117
117
  // whole rather than by domain: `/auth/refresh` answers with a person's access
118
118
  // token, and a browser attaches the cookie it is asked for by domain, so a
119
- // sibling subdomain let in here could read that token (§2.3).
119
+ // sibling subdomain let in here could read that token (DR-0001 §2.3).
120
120
  if (origin !== null && !deps.auth.knownOrigins().includes(origin)) {
121
121
  return new Response("Forbidden", { status: 403 });
122
122
  }
@@ -57,7 +57,7 @@ function covers(tombstone: string, key: string): boolean {
57
57
  }
58
58
 
59
59
  export interface RecordsDeps {
60
- /** Where the set is written down (§3.6). */
60
+ /** Where the set is written down (DESIGN §2.5). */
61
61
  readonly dir: string;
62
62
  /** This instance's id, which is the one thing that makes a record arriving
63
63
  * from a peer refusable on sight: a family this instance minted is written by
@@ -76,7 +76,7 @@ export interface RecordsDeps {
76
76
  * write to the keys it covers, so a peer coming back from a partition cannot
77
77
  * carry a revoked credential in as news.
78
78
  *
79
- * Written down for the same reason the store is (§3.6): none of it is derived
79
+ * Written down for the same reason the store is (DESIGN §2.5): none of it is derived
80
80
  * from anything else this instance holds. A credential exists nowhere but here
81
81
  * and in the authenticator, and losing a family logs its person out. */
82
82
  export class AuthRecords {
@@ -245,7 +245,7 @@ export const SUPPORTED_ALGORITHMS: readonly number[] = [ES256, EdDSA, RS256];
245
245
  * Done at registration rather than at the first assertion: a key that cannot be
246
246
  * imported is a credential that can never be used, and finding that out when
247
247
  * the person tries to sign in leaves a record nobody can explain. The imported
248
- * key itself is thrown away — an assertion imports its own (§2.10). */
248
+ * key itself is thrown away — an assertion imports its own (DR-0001 §2.10). */
249
249
  export async function checkPublicKey(cose: Uint8Array): Promise<void> {
250
250
  const key = decodeCborWhole(cose);
251
251
  const alg = mapEntry(key, 3);
package/src/cli.ts CHANGED
@@ -76,7 +76,7 @@ import {
76
76
  import { type Run, runCommand, serviceFor } from "./service/index.ts";
77
77
  import { VERSION } from "./version.ts";
78
78
 
79
- /** The session this process runs inside, as its environment says (§3.8).
79
+ /** The session this process runs inside, as its environment says (DESIGN §4.1).
80
80
  *
81
81
  * One reading for every command that speaks as a session: which harness
82
82
  * claimed the process settles both who the sender is and which instance it
@@ -644,9 +644,9 @@ async function runInstance(given: string | undefined): Promise<unknown> {
644
644
  // The directory is handed over rather than put in the environment: the
645
645
  // instance would otherwise read it back through the question "which session
646
646
  // is this process inside", and a `daemon run` issued from a session of
647
- // another harness would answer for that session's config home (§3.8).
647
+ // another harness would answer for that session's config home (DESIGN §4.1).
648
648
  // With a supervisor up, it is the one that reads the files and writes down
649
- // what held (§8.2); this start reads what it applied. With none, this
649
+ // what held (DESIGN §8.2); this start reads what it applied. With none, this
650
650
  // process is the only one there is, so it does both.
651
651
  const outcome = await start({ configHome: home, settle: !(await reachable()) });
652
652
  if (!isRunning(outcome)) {
@@ -657,7 +657,7 @@ async function runInstance(given: string | undefined): Promise<unknown> {
657
657
  }
658
658
  const instance = outcome;
659
659
  // A signal is a request to leave, and leaving is the ordered shutdown of
660
- // §8.5 — the same one `instance.shutdown` runs, so a client sees the same
660
+ // DESIGN §8.5 — the same one `instance.shutdown` runs, so a client sees the same
661
661
  // departure either way. The listeners are removed once it has run, because a
662
662
  // signal listener keeps the event loop alive and the process would sit at an
663
663
  // empty loop instead of exiting.
@@ -689,7 +689,7 @@ async function supervise(): Promise<unknown> {
689
689
  * The file first and the supervisor second, because the file is what survives:
690
690
  * a host with no supervisor running still gets the config home added, and the
691
691
  * next supervisor starts it. Told rather than left to be discovered, because
692
- * the supervisor reads the list once (DV-Q8) and would otherwise not know
692
+ * the supervisor reads the list once (DR-0004) and would otherwise not know
693
693
  * until it is restarted. */
694
694
  async function added(args: readonly string[]): Promise<unknown> {
695
695
  const { named, rest } = options(args, ["harness", "port"]);
@@ -722,11 +722,11 @@ async function added(args: readonly string[]): Promise<unknown> {
722
722
  *
723
723
  * Its own command rather than one under `daemon`, because what it edits is not
724
724
  * one instance's anything: every instance of this host is in the same mesh
725
- * (§7.1), so the list is the host's. `peers` is what a session list is called,
725
+ * (DESIGN §7.1), so the list is the host's. `peers` is what a session list is called,
726
726
  * which is why this is called what the thing itself is called.
727
727
  *
728
728
  * An addition takes effect when the instances next start, for the reason
729
- * nothing else reloads either (DV-Q8). A removal is told to whoever is running
729
+ * nothing else reloads either (DR-0004). A removal is told to whoever is running
730
730
  * as well as written down: an endpoint taken off the list is one this host is
731
731
  * not to be talking to, and leaving a live link up until the next restart would
732
732
  * be leaving exactly the connection that was just revoked. */
@@ -829,7 +829,7 @@ async function configShow(args: readonly string[]): Promise<unknown> {
829
829
  }
830
830
  // A dry run: the files are read and the settings functions are called, and
831
831
  // nothing is written. They are expected to have no side effects for exactly
832
- // this reason (§8.2).
832
+ // this reason (DESIGN §8.2).
833
833
  const read = await evaluate(resolveConfigDir());
834
834
  if (read.satisfied === undefined) {
835
835
  throw new CommandError(
@@ -1437,7 +1437,7 @@ function notify(args: readonly string[]): Promise<unknown> {
1437
1437
 
1438
1438
  /** `ccmsg stopping`: this session is about to go.
1439
1439
  *
1440
- * What it buys is the difference between Paused and Disappeared (§5.2): the
1440
+ * What it buys is the difference between Paused and Disappeared (DESIGN §4.3): the
1441
1441
  * instance holds the declaration until the connection closes, and the entry it
1442
1442
  * then writes carries the instant it was told. The connection closing is the
1443
1443
  * second half of that, so the command says its piece and leaves — which is
@@ -1570,7 +1570,7 @@ async function plugin(
1570
1570
  // The config home is that agent's own, and not whichever variable happens to
1571
1571
  // be set: a Codex session started from a Claude Code session carries both,
1572
1572
  // and an install that read the wrong one would write Codex's hooks into
1573
- // Claude Code's config home (§3.8). The marker check is what says the
1573
+ // Claude Code's config home (DESIGN §4.1). The marker check is what says the
1574
1574
  // directory really is that agent's.
1575
1575
  const home = configHome(homeOf(which), which);
1576
1576
  const paths = resolvePathsFor(home);
@@ -64,7 +64,7 @@ export function greetAsUser(conn: Conn): Promise<Record<string, unknown>> {
64
64
  * and everything else.
65
65
  *
66
66
  * By `request_id` rather than by arrival order, because the two are not the
67
- * same stream: an instance pushes topic frames of its own accord (§6), and one
67
+ * same stream: an instance pushes topic frames of its own accord (DESIGN §6), and one
68
68
  * of those landing between a request and its reply would otherwise be read as
69
69
  * the reply. It is not a rare window — greeting an instance that has mesh peers
70
70
  * is enough, since a peer connecting moves a row on `instances`. */
@@ -32,7 +32,7 @@ import { CommandError } from "./link.ts";
32
32
  *
33
33
  * The harness's own settings file is what says the directory is a config home
34
34
  * rather than any directory somebody typed, so which file is looked for
35
- * follows which harness the directory runs (§3.8). Checked where a directory
35
+ * follows which harness the directory runs (DESIGN §4.1). Checked where a directory
36
36
  * is named — `add` and `run` — rather than at every use, so the mistake is
37
37
  * caught when it is made. */
38
38
  export function configHome(dir: string, harness: Harness = DEFAULT_HARNESS): string {
@@ -49,7 +49,7 @@ export function configHome(dir: string, harness: Harness = DEFAULT_HARNESS): str
49
49
 
50
50
  /** Which harness a registered config home runs, as its own file says.
51
51
  *
52
- * Read from the same file the instance itself will read (§8.2), so a command
52
+ * Read from the same file the instance itself will read (DESIGN §8.2), so a command
53
53
  * that has to know before anything is running — `run`, and the supervisor's
54
54
  * own start — reaches the same answer the instance does. A directory no file
55
55
  * names runs whatever the defaults say, which is what an unregistered
@@ -99,7 +99,7 @@ export interface InstanceRow {
99
99
  * settings for has none. */
100
100
  readonly name?: string;
101
101
  readonly dir: string;
102
- /** The address it binds, and the one its peers dial (§7.1). */
102
+ /** The address it binds, and the one its peers dial (DESIGN §7.1). */
103
103
  readonly port?: number;
104
104
  readonly endpoint?: string;
105
105
  readonly running: boolean;
@@ -113,7 +113,7 @@ export interface StatusRow extends InstanceRow {
113
113
  * what is written. */
114
114
  readonly config_problems?: readonly ConfigProblem[];
115
115
  /** What this config home's instance is configured with, after the shared
116
- * file's defaults and its own entry are merged (§8.2).
116
+ * file's defaults and its own entry are merged (DESIGN §8.2).
117
117
  *
118
118
  * Answered whether or not anything is running, and read from the file rather
119
119
  * than asked of the instance: this is what a restart would apply, which is
@@ -249,7 +249,7 @@ export function nameFor(dir: string): string {
249
249
  *
250
250
  * The marker file is the evidence: Claude Code keeps `settings.json` and Codex
251
251
  * keeps `config.toml`, so a directory that holds one of them is that harness's
252
- * (§3.8). A directory holding both, or neither, is not answered for — the
252
+ * (DESIGN §4.1). A directory holding both, or neither, is not answered for — the
253
253
  * first is two answers and the second is none, and guessing either way writes
254
254
  * down a setting the instance will act on for the whole of its life. */
255
255
  export function harnessOf(dir: string): Harness {
@@ -333,7 +333,7 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
333
333
  // is keyed by, and a fresh one gets its id here rather than at its first
334
334
  // start (DR-0001 §2.1).
335
335
  const id = instanceIdentity(targetFor(env, home).paths.instanceIdFile);
336
- // Every instance listens, because an instance is an entry of the mesh (§7.1)
336
+ // Every instance listens, because an instance is an entry of the mesh (DESIGN §7.1)
337
337
  // and a mesh is reached over the entry: what `--port` settles is which
338
338
  // address, not whether there is one.
339
339
  const port =
@@ -353,7 +353,7 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
353
353
  );
354
354
  // The loopback address, because that is the one this host is certainly
355
355
  // reached at. A proxy in front of it is a deployment fact nothing here can
356
- // see, so an operator who has one edits this row (§8.2).
356
+ // see, so an operator who has one edits this row (DESIGN §8.2).
357
357
  saveEndpoints(paths.configDir, [
358
358
  ...readEndpointRows(paths.configDir).filter((row) => row.id !== id),
359
359
  { id, endpoint: `http://127.0.0.1:${String(port)}/` as EndpointRow["endpoint"] },
@@ -563,7 +563,7 @@ export async function status(target: Target): Promise<StatusRow> {
563
563
  config: own?.config ?? DEFAULT_CONFIG,
564
564
  // What a person has to be told even though the instance is running: an
565
565
  // edit that did not check out is not applied, and the only sign of it
566
- // otherwise is a setting that did not take (§8.3).
566
+ // otherwise is a setting that did not take (DESIGN §8.3).
567
567
  ...(read.problems.length === 0 ? {} : { config_problems: read.problems }),
568
568
  };
569
569
  const conn = await connect(target.paths.socket);
@@ -596,7 +596,7 @@ export async function status(target: Target): Promise<StatusRow> {
596
596
  *
597
597
  * The contract's op rather than a signal, so the request goes through the same
598
598
  * authorization every other op does and the caller is told it was accepted
599
- * before the process goes down (§8.5). */
599
+ * before the process goes down (DESIGN §8.5). */
600
600
  export async function stop(target: Target): Promise<{ dir: string; stopped: boolean }> {
601
601
  const conn = await connect(target.paths.socket);
602
602
  if (conn === undefined) {
@@ -639,7 +639,7 @@ export const spawnInstance: SpawnInstance = (dir, env) => {
639
639
  // The directory is an argument and not an environment variable: `daemon run`
640
640
  // takes it from there and hands it to the instance by value, so which config
641
641
  // home the child answers for cannot depend on which session the supervisor
642
- // was started from (§3.8).
642
+ // was started from (DESIGN §4.1).
643
643
  const proc = Bun.spawn([process.execPath, ENTRY, "daemon", "run", dir], {
644
644
  stdio: ["ignore", "ignore", "ignore"],
645
645
  env: { ...env } as Record<string, string>,
@@ -679,7 +679,7 @@ export function awaitSocket(paths: InstancePaths, timeoutMs: number): Promise<vo
679
679
  }
680
680
 
681
681
  /** Wait for the lock to be released, which is the last thing a departing
682
- * instance does (§8.5). */
682
+ * instance does (DESIGN §8.5). */
683
683
  export function awaitGone(paths: InstancePaths, timeoutMs: number): Promise<void> {
684
684
  return awaitEntry(paths, timeoutMs, () => {
685
685
  const pid = lockHolder(paths.lockFile);
@@ -41,7 +41,7 @@ interface Greeted {
41
41
  * leave.
42
42
  *
43
43
  * A subscription opens with one `snapshot: true` frame per instance that has a
44
- * value (§6.2), so the current value of a topic is read by subscribing and
44
+ * value (DESIGN §6.2), so the current value of a topic is read by subscribing and
45
45
  * stopping at the frames rather than by an op of its own. The local instance's
46
46
  * frame is deferred behind the acknowledgement and is therefore already on the
47
47
  * wire; the relayed ones are what the budget is for.
@@ -30,7 +30,7 @@ import {
30
30
  * still holds the lock needs only the moment that takes, so the first retry
31
31
  * is short enough to be invisible.
32
32
  * - `maxMs` bounds a spin. The failure this is really for is a config the
33
- * instance refuses to start with (DV-Q9): it fails in milliseconds and will
33
+ * instance refuses to start with (DR-0004): it fails in milliseconds and will
34
34
  * fail again identically until somebody edits the file, so the cap is the
35
35
  * rate at which the supervisor is willing to say so in its log while waiting
36
36
  * for that edit.
@@ -93,7 +93,7 @@ class Supervised {
93
93
  /** The foreground supervisor: the instances the shared file lists, kept up, and
94
94
  * a socket over which they are asked about.
95
95
  *
96
- * The list is read once, when this starts (DV-Q8). What changes it afterwards
96
+ * The list is read once, when this starts (DR-0004). What changes it afterwards
97
97
  * is a request — `daemon add` and `daemon remove` tell the supervisor as well
98
98
  * as the file — rather than the file being re-read, so the supervisor's idea of
99
99
  * which instances there are and the file's cannot silently disagree about
@@ -122,7 +122,7 @@ export class Supervisor {
122
122
  }
123
123
 
124
124
  /** Read the edited files, check them, write down what held, and look after
125
- * what it names (§8.2).
125
+ * what it names (DESIGN §8.2).
126
126
  *
127
127
  * This is the one thing that writes the applied settings: the children read
128
128
  * them and write nothing, so nothing races over the file and there is one
@@ -316,7 +316,7 @@ export class Supervisor {
316
316
  /** Stop one child, and leave it stopped.
317
317
  *
318
318
  * Asked over its own socket rather than signalled, so what runs is the
319
- * ordered shutdown of §8.5 — the same departure a client sees from an
319
+ * ordered shutdown of DESIGN §8.5 — the same departure a client sees from an
320
320
  * `instance.shutdown`. */
321
321
  async stopOne(dir: string): Promise<{ dir: string; stopped: boolean }> {
322
322
  const unit = this.#units.get(dir);
@@ -10,7 +10,7 @@ export class CallerError extends Error {}
10
10
  * The role is read here because it decides which fields the value has to
11
11
  * carry — `sid` is present exactly when the role is `session` — and not
12
12
  * whether anything is allowed: what the identity may do is decided afterwards,
13
- * by dispatch, against the attribute table (§3.2). It is the same question
13
+ * by dispatch, against the attribute table (DESIGN §2.2). It is the same question
14
14
  * `hello` asks of a greeting, whose shape depends on its role in the same way
15
15
  * and for the same reason: one schema covers all three roles, so the schema
16
16
  * cannot state the rule and the instance does.
@@ -20,7 +20,7 @@ export interface DispatchDeps {
20
20
  readonly capabilities: ReadonlySet<Capability>;
21
21
  /** The instance that owns the subject of an `instance-local` op, or
22
22
  * `undefined` when no other instance owns it and we answer ourselves.
23
- * The routing table behind this is the `peers` topic (daemon-v2 §7.3). */
23
+ * The routing table behind this is the `peers` topic (DESIGN §7.3). */
24
24
  readonly resolveInstance: (op: OpName, frame: Record<string, unknown>) => InstanceId | undefined;
25
25
  readonly handlers: Handlers;
26
26
  }
@@ -31,7 +31,7 @@ function isOpName(op: string): op is OpName {
31
31
 
32
32
  /** Decide one frame.
33
33
  *
34
- * The six steps of daemon-v2 §3.2 are written once, here, and read the op
34
+ * The six steps of DESIGN §2.2 are written once, here, and read the op
35
35
  * attribute table for every op. Adding an op is a row in the table plus a
36
36
  * schema and an implementation — never a check in this function (M1). */
37
37
  export async function dispatch(
@@ -101,7 +101,7 @@ export async function dispatch(
101
101
  //
102
102
  // A request that has already been here is dropped before that: a cycle in
103
103
  // the routing would otherwise send it round the same instances until every
104
- // deadline expired (§7.3). It is answered rather than left unanswered,
104
+ // deadline expired (DESIGN §7.3). It is answered rather than left unanswered,
105
105
  // because the caller learns the same thing sooner and the code is the one
106
106
  // the contract gives a destination that could not be reached.
107
107
  const hops = fields["hops"];
@@ -123,7 +123,7 @@ export async function dispatch(
123
123
  args: fields,
124
124
  conn,
125
125
  identity: identity.state === "settled" ? identity : undefined,
126
- // The one route by which a role reaches an implementation (§3.2).
126
+ // The one route by which a role reaches an implementation (DESIGN §2.2).
127
127
  role: attrs.scope === "role" && identity.state === "settled" ? identity.role : undefined,
128
128
  });
129
129
  return reply(requestId, body);
@@ -5,7 +5,7 @@ import type { ConnIdentity, SettledIdentity } from "./identity.ts";
5
5
  *
6
6
  * It is the `Conn` transport accepted, narrowed to what an op may do with it:
7
7
  * read the identity, push frames, and learn that the connection is gone. A
8
- * subscription is held by a connection and ends with it (daemon-v2 §6.3), so
8
+ * subscription is held by a connection and ends with it (DESIGN §6.3), so
9
9
  * this is what the topic mechanism keys its subscribers on. Declared here
10
10
  * rather than imported from transport because dispatch sits below it. */
11
11
  export interface Requester {
@@ -25,12 +25,12 @@ export interface Requester {
25
25
  /** What an op implementation receives.
26
26
  *
27
27
  * The arguments are already validated and the caller is already allowed
28
- * (daemon-v2 §3.2): a handler starts from "this may be run", so it holds no
28
+ * (DESIGN §2.2): a handler starts from "this may be run", so it holds no
29
29
  * check of its own. */
30
30
  export interface HandlerInput {
31
31
  readonly op: OpName;
32
32
  /** The connection the request arrived on. Ops that hold something for the
33
- * length of a connection — the subscriptions of daemon-v2 §6.3 — need it;
33
+ * length of a connection — the subscriptions of DESIGN §6.3 — need it;
34
34
  * ops that only answer ignore it. */
35
35
  readonly conn: Requester;
36
36
  /** The whole request frame, validated against the op's request schema. */
@@ -40,7 +40,7 @@ export interface HandlerInput {
40
40
  /** Set only for ops the attribute table marks `scope: "role"`, where the role
41
41
  * changes what the reply may contain rather than whether the call is allowed.
42
42
  * This is the only route by which a role reaches an implementation
43
- * (daemon-v2 §3.2). */
43
+ * (DESIGN §2.2). */
44
44
  readonly role?: Role;
45
45
  }
46
46
 
@@ -2,7 +2,7 @@ import type { Role, Sid } from "@ccmsg/protocol";
2
2
 
3
3
  /** What dispatch knows about the connection a frame arrived on.
4
4
  *
5
- * transport settles this (daemon-v2 §3.1): a connection starts anonymous and
5
+ * transport settles this (DESIGN §2.1): a connection starts anonymous and
6
6
  * becomes `settled` when `hello` binds a role, and a session's `sid`, to it.
7
7
  * Nothing else about the connection reaches dispatch — the authorization steps
8
8
  * read the op attribute table, not the connection. */
@@ -3,7 +3,7 @@ import type { ErrorCode, ErrorResponse, InstanceId } from "@ccmsg/protocol";
3
3
  /** What dispatch decided about one frame.
4
4
  *
5
5
  * `forward` is the only outcome that is not an answer: the op belongs to
6
- * another instance and mesh has to carry it there (daemon-v2 §3.2 step 6).
6
+ * another instance and mesh has to carry it there (DESIGN §2.2 step 6).
7
7
  * There is no mesh yet, so dispatch names the destination and stops. */
8
8
  export type DispatchResult =
9
9
  /** The frame was not a request and has no answer. The mesh handshake's own
@@ -32,7 +32,7 @@ export class OpError extends Error {
32
32
  }
33
33
 
34
34
  /** The reply envelope, built here and nowhere else so the wire shape stays in
35
- * one place (daemon-v2 §11.1). */
35
+ * one place (DESIGN §9.1). */
36
36
  export function reply(
37
37
  requestId: string,
38
38
  body: unknown,