agent-coord-mcp 0.26.18 → 0.26.20

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 (84) hide show
  1. package/README.md +82 -0
  2. package/dist/build.js +113 -0
  3. package/dist/build.js.map +1 -0
  4. package/dist/capabilities.js +214 -0
  5. package/dist/capabilities.js.map +1 -0
  6. package/dist/prefix.js +64 -0
  7. package/dist/prefix.js.map +1 -0
  8. package/dist/roles.js +132 -0
  9. package/dist/roles.js.map +1 -0
  10. package/dist/server-identity.js +82 -0
  11. package/dist/server-identity.js.map +1 -0
  12. package/dist/server.js +668 -0
  13. package/dist/server.js.map +1 -0
  14. package/dist/store.js +553 -0
  15. package/dist/store.js.map +1 -0
  16. package/dist/tools/admin.js +323 -0
  17. package/dist/tools/admin.js.map +1 -0
  18. package/dist/tools/attention.js +73 -0
  19. package/dist/tools/attention.js.map +1 -0
  20. package/dist/tools/away.js +285 -0
  21. package/dist/tools/away.js.map +1 -0
  22. package/dist/tools/board-ref.js +208 -0
  23. package/dist/tools/board-ref.js.map +1 -0
  24. package/dist/tools/event-kinds.js +39 -0
  25. package/dist/tools/event-kinds.js.map +1 -0
  26. package/dist/tools/events.js +234 -0
  27. package/dist/tools/events.js.map +1 -0
  28. package/dist/tools/index.js +14 -0
  29. package/dist/tools/index.js.map +1 -0
  30. package/dist/tools/logwatch.js +85 -0
  31. package/dist/tools/logwatch.js.map +1 -0
  32. package/dist/tools/messaging.js +706 -0
  33. package/dist/tools/messaging.js.map +1 -0
  34. package/dist/tools/record-events.js +380 -0
  35. package/dist/tools/record-events.js.map +1 -0
  36. package/dist/tools/records.js +1205 -0
  37. package/dist/tools/records.js.map +1 -0
  38. package/dist/tools/registry.js +529 -0
  39. package/dist/tools/registry.js.map +1 -0
  40. package/dist/tools/render.js +2 -0
  41. package/dist/tools/render.js.map +1 -0
  42. package/dist/tools/rooms.js +210 -0
  43. package/dist/tools/rooms.js.map +1 -0
  44. package/dist/tools/rotate.js +143 -0
  45. package/dist/tools/rotate.js.map +1 -0
  46. package/dist/tools/scopes.js +126 -0
  47. package/dist/tools/scopes.js.map +1 -0
  48. package/dist/tools/shared.js +104 -0
  49. package/dist/tools/shared.js.map +1 -0
  50. package/dist/tools/stall.js +415 -0
  51. package/dist/tools/stall.js.map +1 -0
  52. package/dist/tools/transport.js +1918 -0
  53. package/dist/tools/transport.js.map +1 -0
  54. package/dist/tools/work.js +465 -0
  55. package/dist/tools/work.js.map +1 -0
  56. package/dist/tools/worktrees.js +581 -0
  57. package/dist/tools/worktrees.js.map +1 -0
  58. package/dist/transports/config.js +82 -0
  59. package/dist/transports/config.js.map +1 -0
  60. package/dist/transports/index.js +113 -0
  61. package/dist/transports/index.js.map +1 -0
  62. package/dist/transports/tmux.js +140 -0
  63. package/dist/transports/tmux.js.map +1 -0
  64. package/dist/transports/types.js +86 -0
  65. package/dist/transports/types.js.map +1 -0
  66. package/dist/typed-records.js +174 -0
  67. package/dist/typed-records.js.map +1 -0
  68. package/dist/work.js +2 -0
  69. package/dist/work.js.map +1 -0
  70. package/hooks/tmux-pusher.mjs +33 -3
  71. package/package.json +1 -1
  72. package/src/capabilities.ts +104 -1
  73. package/src/server.ts +30 -1
  74. package/src/tools/records.ts +221 -34
  75. package/src/tools/registry.ts +36 -5
  76. package/src/tools/shared.ts +12 -36
  77. package/src/tools/stall.ts +2 -1
  78. package/src/tools/transport.ts +96 -43
  79. package/src/tools/work.ts +95 -3
  80. package/src/tools/worktrees.ts +234 -32
  81. package/src/transports/config.ts +110 -0
  82. package/src/transports/index.ts +126 -0
  83. package/src/transports/tmux.ts +177 -0
  84. package/src/transports/types.ts +201 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WHICH TRANSPORT THIS FLEET IS CONFIGURED TO USE (Phase 5.4 Task 3.1–3.2).
3
+ *
4
+ * Whole-fleet, read ONCE at startup, per David 2026-09-11 — no per-seat
5
+ * branching in `send_command` or `attach`. Memoised for that reason and not for
6
+ * speed: a value that can be re-read mid-process is a value that can change
7
+ * mid-process, and then two calls in one session disagree about what the fleet
8
+ * is doing.
9
+ *
10
+ * ⛔ AN UNKNOWN VALUE REFUSES AT STARTUP. It does not fall back to `tmux-push`.
11
+ *
12
+ * The reason is not tidiness. A SILENT FALLBACK AND A CORRECT DEFAULT PRODUCE
13
+ * IDENTICAL EVIDENCE: both give you a fleet on tmux with nothing in any log, so
14
+ * a typo in the config reads exactly like a deliberate default, and the person
15
+ * who typed `heardr` spends the afternoon asking why their transport change did
16
+ * nothing. Refusing is louder than the bug it prevents.
17
+ */
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import path from "node:path";
20
+ import { ROOT } from "../store.js";
21
+ import { TMUX_PUSH, TRANSPORT_KINDS } from "./types.js";
22
+ /** `$AGENT_COORD_DIR/config.json`, the fleet-wide file. */
23
+ export const TRANSPORT_CONFIG_FILE = path.join(ROOT, "config.json");
24
+ export const TRANSPORT_ENV_VAR = "AGENT_COORD_TRANSPORT";
25
+ function refuse(value, origin) {
26
+ throw new Error(`[agent-coord-mcp] unknown transport ${JSON.stringify(value)} from ${origin}. ` +
27
+ `Valid: ${TRANSPORT_KINDS.join(", ")}. ` +
28
+ `REFUSING AT STARTUP rather than falling back to "${TMUX_PUSH}" — a silent fallback and a correct ` +
29
+ `default leave identical evidence, so a typo here would look exactly like a working default and the ` +
30
+ `transport change would appear to do nothing. Fix the value or remove it to get the default.`);
31
+ }
32
+ function asKind(value, origin) {
33
+ if (typeof value !== "string" || value.length === 0)
34
+ refuse(String(value), origin);
35
+ const match = TRANSPORT_KINDS.find((k) => k === value);
36
+ if (!match)
37
+ refuse(value, origin);
38
+ return match;
39
+ }
40
+ let cached;
41
+ /**
42
+ * Resolve the fleet's transport. Throws on an unknown value — call it once at
43
+ * startup so the refusal lands before any agent attaches.
44
+ */
45
+ export function configuredTransport() {
46
+ if (cached)
47
+ return cached;
48
+ if (existsSync(TRANSPORT_CONFIG_FILE)) {
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(readFileSync(TRANSPORT_CONFIG_FILE, "utf8"));
52
+ }
53
+ catch (e) {
54
+ // A CONFIG FILE THAT CANNOT BE PARSED IS NOT AN ABSENT ONE. Treating it as
55
+ // absent would silently use the default while a file sits there stating
56
+ // otherwise — the same two-states-one-evidence defect as the fallback.
57
+ throw new Error(`[agent-coord-mcp] ${TRANSPORT_CONFIG_FILE} is unreadable (${e.message}). ` +
58
+ `REFUSING rather than treating it as absent: a file that exists and cannot be read is not the ` +
59
+ `same as no file, and defaulting here would hide a stated intent behind a working fleet.`);
60
+ }
61
+ const raw = parsed?.transport;
62
+ if (raw !== undefined) {
63
+ cached = { kind: asKind(raw, `${TRANSPORT_CONFIG_FILE} ("transport")`), source: "config", origin: TRANSPORT_CONFIG_FILE };
64
+ return cached;
65
+ }
66
+ }
67
+ const env = process.env[TRANSPORT_ENV_VAR];
68
+ if (env !== undefined && env !== "") {
69
+ cached = { kind: asKind(env, `$${TRANSPORT_ENV_VAR}`), source: "env", origin: `$${TRANSPORT_ENV_VAR}` };
70
+ return cached;
71
+ }
72
+ cached = { kind: TMUX_PUSH, source: "default", origin: `built-in default (${TMUX_PUSH})` };
73
+ return cached;
74
+ }
75
+ /**
76
+ * Drop the memo. FOR TESTS ONLY — the whole point of reading once is that
77
+ * production code cannot do this.
78
+ */
79
+ export function resetConfiguredTransportForTests() {
80
+ cached = undefined;
81
+ }
82
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/transports/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * THE REGISTRY — the one place a transport kind is turned into an implementation.
3
+ *
4
+ * `resolveTransport` is deliberately total over `TransportKind`: adding a kind to
5
+ * the union without registering it is a compile error here rather than a silent
6
+ * fall-through at a call site. Until Task 3, tmux is the only registered
7
+ * implementation and that is the rollback plan — the seam lands behind no config.
8
+ */
9
+ import { HERDR, TMUX_PUSH, TMUX_PUSH_REMOTE } from "./types.js";
10
+ import { TmuxTransport } from "./tmux.js";
11
+ import { configuredTransport } from "./config.js";
12
+ export * from "./types.js";
13
+ export * from "./config.js";
14
+ export { TmuxTransport, tmuxAvailable, paneExists, probePane, tmuxVersion } from "./tmux.js";
15
+ let host;
16
+ /** Wire the process-layer implementation in once, at module init. */
17
+ export function registerTmuxHost(h) {
18
+ host = h;
19
+ }
20
+ export function resolveTransport(kind) {
21
+ if (!host) {
22
+ throw new Error("transport host not registered — call registerTmuxHost() before resolveTransport(); " +
23
+ "this is a wiring error, not a runtime condition");
24
+ }
25
+ switch (kind) {
26
+ case TMUX_PUSH:
27
+ case TMUX_PUSH_REMOTE:
28
+ return new TmuxTransport(host, kind);
29
+ case HERDR:
30
+ // Task 4. Named so the union stays total and the gap is a stated absence
31
+ // rather than a default that silently behaves like tmux.
32
+ throw new Error('transport "herdr" is not implemented yet (Phase 5.4 Task 4)');
33
+ }
34
+ }
35
+ /* ── the ACTIVE transport, and why `running` is not read from config ────────── */
36
+ /**
37
+ * The instance this process would actually use to deliver.
38
+ *
39
+ * Kept as an OBJECT rather than re-derived from the config value on each call,
40
+ * and that is the whole design. If `running` were computed by reading the config
41
+ * and resolving it, then `configured` and `running` would be two names for one
42
+ * fact and `agrees` could never be false — a check that cannot fail. The active
43
+ * instance is what startup actually installed, so the two can genuinely differ:
44
+ * a startup that refused, a process that never wired one, a test that installed
45
+ * another, a future path that falls back.
46
+ */
47
+ let active;
48
+ export function setActiveTransport(t) {
49
+ active = t;
50
+ }
51
+ /** FOR TESTS ONLY — production wires the active transport once, at startup. */
52
+ export function clearActiveTransportForTests() {
53
+ active = undefined;
54
+ }
55
+ export function activeTransport() {
56
+ return active;
57
+ }
58
+ /**
59
+ * Wire the transport this fleet is configured for. Called once at startup, and
60
+ * it is where an unknown config value turns into a refusal.
61
+ */
62
+ export function initTransportFromConfig() {
63
+ const conf = configuredTransport();
64
+ setActiveTransport(resolveTransport(conf.kind));
65
+ return { kind: conf.kind, source: conf.source };
66
+ }
67
+ /**
68
+ * WHAT THIS PROCESS IS ACTUALLY RUNNING, answered by CALLING the transport.
69
+ *
70
+ * *A config value is a label someone typed.* This asks the live object instead:
71
+ * it calls `available()` and puts a synthetic marker through `probe()`, and
72
+ * reports what came back as the evidence beside the answer. The probe is chosen
73
+ * to be discriminating rather than decorative — a tmux transport answers a
74
+ * bogus pane id with a reason that names the pane, and an implementation that
75
+ * does not talk to panes cannot produce that.
76
+ *
77
+ * Returns `undefined` for `kind` when nothing is wired, which is NOT the same as
78
+ * "tmux by default": a process with no transport delivers nothing, and reporting
79
+ * a default here would be the exact substitution this function exists to refuse.
80
+ */
81
+ export async function runningTransport() {
82
+ const t = active;
83
+ if (!t) {
84
+ return {
85
+ kind: undefined,
86
+ evidence: "no transport is wired in this process — nothing was called, and no default is assumed",
87
+ };
88
+ }
89
+ const availability = (() => {
90
+ try {
91
+ return `available()=${t.available()}`;
92
+ }
93
+ catch (e) {
94
+ return `available() threw: ${e.message}`;
95
+ }
96
+ })();
97
+ let probeEvidence;
98
+ try {
99
+ const live = await t.probe({
100
+ agentId: "__capability_probe__",
101
+ transport: t.kind,
102
+ pid: process.pid,
103
+ target: "__no_such_target__",
104
+ since: Date.now(),
105
+ });
106
+ probeEvidence = `probe(bogus target)=${live.state}${live.state === "live" ? "" : `: ${live.reason}`}`;
107
+ }
108
+ catch (e) {
109
+ probeEvidence = `probe threw: ${e.message}`;
110
+ }
111
+ return { kind: t.kind, evidence: `${availability}, ${probeEvidence}` };
112
+ }
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/transports/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * THE TMUX TRANSPORT — every `tmux` shell-out in this package, in one file.
3
+ *
4
+ * Before this, `spawnSync("tmux", …)` appeared at 7 sites in `tools/transport.ts`
5
+ * and the `has-session` caveat below was written out TWICE, verbatim, at two of
6
+ * them. A rule duplicated in two comments is a rule that will be re-derived
7
+ * wrongly at the third site somebody adds.
8
+ *
9
+ * WHAT THIS FILE DOES NOT DO, on purpose: spawn or reap the pusher process,
10
+ * read receipts, or schedule reminders. Delivery is a second process for THIS
11
+ * transport and will not be for a socket one, so that machinery stays with the
12
+ * tools that own it and reaches this file through `TmuxHost` below. An interface
13
+ * that baked in a pusher could not host an implementation that has none.
14
+ */
15
+ import { spawnSync } from "node:child_process";
16
+ import { TMUX_PUSH, isLocallyProbeable, isTmuxKind, targetOf } from "./types.js";
17
+ /** `tmux -V` — is tmux on this host at all? */
18
+ export function tmuxAvailable() {
19
+ return spawnSync("tmux", ["-V"]).status === 0;
20
+ }
21
+ /**
22
+ * DOES THIS TARGET EXIST? The only tmux probe with discriminating power.
23
+ *
24
+ * `has-session` VALIDATES THE TARGET; `display-message -p -t <target> "ok"`
25
+ * DOES NOT — tmux exits 0 for any target, including a pane killed a moment ago,
26
+ * so that probe had ZERO discriminating power and reported every dead pane
27
+ * alive. Pinned to the BEHAVIOUR, not a version: measured identical on tmux
28
+ * 3.6b, 3.7b. A version-pinned claim rots on the next upgrade.
29
+ *
30
+ * Positive control, both directions, re-run on tmux 3.7b while extracting this:
31
+ * bogus target `%99999` -> has-session exit 1, display-message exit 0; live pane
32
+ * `%8` -> both exit 0. The wrong probe is wrong in only one direction, which is
33
+ * why it survived: it never reports a live pane dead.
34
+ */
35
+ export function paneExists(target) {
36
+ return spawnSync("tmux", ["has-session", "-t", target]).status === 0;
37
+ }
38
+ /**
39
+ * `has-session` with the failure text, for the one caller that reports it.
40
+ *
41
+ * `paneExists` is the boolean most sites want; attach quotes tmux's own stderr
42
+ * back to the user, and losing that text would make a refusal less useful while
43
+ * still typechecking — the exact class of silent regression this task guards.
44
+ */
45
+ export function probePane(target) {
46
+ const probe = spawnSync("tmux", ["has-session", "-t", target]);
47
+ return { exists: probe.status === 0, stderr: (probe.stderr ?? "").toString().trim() };
48
+ }
49
+ /** `tmux -V` output, or undefined when tmux is absent. */
50
+ export function tmuxVersion() {
51
+ const probe = spawnSync("tmux", ["-V"]);
52
+ if (probe.status !== 0)
53
+ return undefined;
54
+ return (probe.stdout ?? "").toString().trim() || undefined;
55
+ }
56
+ export class TmuxTransport {
57
+ kind;
58
+ #host;
59
+ constructor(host, kind = TMUX_PUSH) {
60
+ this.#host = host;
61
+ this.kind = kind;
62
+ }
63
+ available() {
64
+ return tmuxAvailable();
65
+ }
66
+ attach(args) {
67
+ return this.#host.attach(args);
68
+ }
69
+ detach(agentId) {
70
+ return this.#host.detach(agentId);
71
+ }
72
+ push(marker, text) {
73
+ return this.#host.push(marker, text);
74
+ }
75
+ sendControl(marker, cmd) {
76
+ return this.#host.sendControl(marker, cmd);
77
+ }
78
+ /**
79
+ * THREE ANSWERS, AND THE THIRD IS LOAD-BEARING.
80
+ *
81
+ * Each `unknown` below was previously a `continue` with a comment. The states
82
+ * are preserved exactly, because "we could not look" is not evidence of death
83
+ * and a reaper that cannot tell them apart kills live sessions:
84
+ *
85
+ * · not a tmux marker -> unknown (this transport cannot speak for it)
86
+ * · remote (foreign host) -> unknown (no local pane; heartbeat decides)
87
+ * · no target recorded -> unknown (nothing to probe)
88
+ * · tmux missing on host -> unknown (the instrument is absent, not the pane)
89
+ * · target absent -> dead
90
+ * · pusher gone, pane alive -> dead, and it says which half failed
91
+ */
92
+ async probe(marker) {
93
+ if (!isTmuxKind(marker.transport)) {
94
+ return { state: "unknown", reason: `transport "${marker.transport}" is not tmux` };
95
+ }
96
+ if (!isLocallyProbeable(marker.transport)) {
97
+ return {
98
+ state: "unknown",
99
+ reason: `${marker.transport} runs on another host${marker.host ? ` (${marker.host})` : ""} — no local pane to probe; liveness is heartbeat-based`,
100
+ };
101
+ }
102
+ const target = targetOf(marker);
103
+ if (!target)
104
+ return { state: "unknown", reason: "no target recorded on the marker" };
105
+ if (!tmuxAvailable())
106
+ return { state: "unknown", reason: "tmux is not available on this host" };
107
+ if (!paneExists(target))
108
+ return { state: "dead", reason: `pane ${target} does not exist` };
109
+ if (!this.#host.pusherAlive(marker)) {
110
+ return { state: "dead", reason: `pane ${target} is alive but its pusher (pid ${marker.pid}) is gone` };
111
+ }
112
+ return { state: "live" };
113
+ }
114
+ /**
115
+ * Reap pushers whose pane has gone, and say which ones could not be judged.
116
+ *
117
+ * `unprobeable` is NOT a residual list nobody reads — it is the distinction
118
+ * PRODUCTION_ROADMAP Phase 5.3 requires between "dead" and "cannot probe".
119
+ * An agent whose liveness is unknown is left alone and reported, never reaped.
120
+ */
121
+ async reapWedged(markers) {
122
+ const reaped = [];
123
+ const unprobeable = [];
124
+ for (const marker of markers) {
125
+ const live = await this.probe(marker);
126
+ if (live.state === "unknown") {
127
+ unprobeable.push(marker.agentId);
128
+ continue;
129
+ }
130
+ if (live.state === "live")
131
+ continue;
132
+ if (this.#host.killPusher(marker))
133
+ reaped.push(marker.agentId);
134
+ else
135
+ unprobeable.push(marker.agentId);
136
+ }
137
+ return { reaped, unprobeable };
138
+ }
139
+ }
140
+ //# sourceMappingURL=tmux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux.js","sourceRoot":"","sources":["../../src/transports/tmux.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * THE TRANSPORT SEAM (Phase 5.4 Task 2).
3
+ *
4
+ * This interface is DERIVED from the 12 `"tmux-push"` literals and the 7
5
+ * `spawnSync("tmux", …)` shell-outs that were spread across `tools/`, not
6
+ * designed top-down. Where the phase doc's sketch and the code disagreed, the
7
+ * code won — three times, each recorded below, because the gate for this task is
8
+ * that a running fleet cannot tell the difference.
9
+ *
10
+ * 1. `tmux-push-remote` IS A LIVE KIND. The doc's sketch had two kinds; the code
11
+ * has three. `registry.ts` gives remote markers heartbeat-based liveness
12
+ * because their pid is 0 on a foreign host, and `server.ts` documents the
13
+ * value as the wire contract for `scripts/coord-pusher.mjs`. A union without
14
+ * it silently reclassifies every remote agent as not-tmux.
15
+ * 2. THE MARKER HAS NINE FIELDS, NOT SIX. `scriptMtime`, `serverBuildMtime` and
16
+ * `rooms` are absent from the sketch and each one closes a defect that
17
+ * actually shipped. They are kept verbatim, including the rule they share:
18
+ * ABSENT MEANS UNKNOWN, NEVER "ON" — the same principle as `Liveness.unknown`
19
+ * below, one field deeper.
20
+ * 3. `target` IS ADDITIVE, NOT A RENAME. See `targetOf`.
21
+ */
22
+ /**
23
+ * Every transport value that can appear in a marker on disk.
24
+ *
25
+ * Not `string`: the whole defect being fixed is code branching on a literal
26
+ * instead of reading the field, and a `string` discriminant cannot tell the
27
+ * compiler that a branch was missed. Not a two-member union either — see (1).
28
+ */
29
+ export const TMUX_PUSH = "tmux-push";
30
+ export const TMUX_PUSH_REMOTE = "tmux-push-remote";
31
+ export const HERDR = "herdr";
32
+ export const TRANSPORT_KINDS = [TMUX_PUSH, TMUX_PUSH_REMOTE, HERDR];
33
+ /**
34
+ * The tmux family. Both members are delivered by a pusher process typing into a
35
+ * pane; they differ in WHERE that pane is, which is why liveness splits below.
36
+ *
37
+ * THIS IS THE ONE PLACE THE TMUX LITERALS LIVE. Twelve call sites used to spell
38
+ * them; they now ask.
39
+ */
40
+ const TMUX_KINDS = new Set([TMUX_PUSH, TMUX_PUSH_REMOTE]);
41
+ /** Is this marker carried by the tmux family (local OR remote)? */
42
+ export function isTmuxKind(transport) {
43
+ return transport !== undefined && TMUX_KINDS.has(transport);
44
+ }
45
+ /**
46
+ * Is this marker's pane on THIS host, so that a local probe means anything?
47
+ *
48
+ * The distinction every `marker.transport !== "tmux-push"` site was making by
49
+ * hand, with a comment explaining "remote = can't verify". Naming it stops the
50
+ * next person re-deriving it — and re-deriving it wrongly is how a remote agent
51
+ * gets reported dead because a pane that was never local did not answer.
52
+ */
53
+ export function isLocallyProbeable(transport) {
54
+ return transport === TMUX_PUSH;
55
+ }
56
+ /**
57
+ * Is liveness for this marker decided by the registry heartbeat rather than by a
58
+ * local pid? True only for the remote kind, whose pid is 0 on a foreign host.
59
+ */
60
+ export function isRemoteTmuxKind(transport) {
61
+ return transport === TMUX_PUSH_REMOTE;
62
+ }
63
+ /**
64
+ * Read a marker's address, preferring the generic field and falling back to the
65
+ * tmux-specific one.
66
+ *
67
+ * This is the read half of the dual-write. Call it rather than touching either
68
+ * field: a consumer that reads only `tmuxTarget` goes blind the day a herdr
69
+ * marker appears, and one that reads only `target` is blind to every marker on
70
+ * disk today.
71
+ */
72
+ export function targetOf(marker) {
73
+ return marker.target ?? marker.tmuxTarget;
74
+ }
75
+ /**
76
+ * Stamp an address into BOTH fields, so the marker is legible to the current
77
+ * code and to whatever a revert restores.
78
+ */
79
+ export function withTarget(marker, target) {
80
+ if (target === undefined)
81
+ return marker;
82
+ return { ...marker, target, tmuxTarget: target };
83
+ }
84
+ /** Keystroke-shaped commands a transport must deliver to an interactive pane. */
85
+ export const CONTROL_COMMANDS = ["clear", "compact", "reload-skills"];
86
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/transports/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAoB,CAAC;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAA2B,CAAC;AAC5D,MAAM,CAAC,MAAM,KAAK,GAAG,OAAgB,CAAC;AAItC,MAAM,CAAC,MAAM,eAAe,GAA6B,CAAC,SAAS,EAAE,gBAAgB,EAAE,KAAK,CAAU,CAAC;AAEvG;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAS,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAElE,mEAAmE;AACnE,MAAM,UAAU,UAAU,CAAC,SAA6B;IACtD,OAAO,SAAS,KAAK,SAAS,IAAI,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAA6B;IAC9D,OAAO,SAAS,KAAK,SAAS,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAA6B;IAC5D,OAAO,SAAS,KAAK,gBAAgB,CAAC;AACxC,CAAC;AAoDD;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAsD;IAC7E,OAAO,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAmB,MAAS,EAAE,MAA0B;IAChF,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AACnD,CAAC;AAeD,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,eAAe,CAAU,CAAC"}
@@ -0,0 +1,174 @@
1
+ // Typed records become obligatory on agent→agent traffic (Phase 5.1 Task 12).
2
+ //
3
+ // THE MECHANISM ALREADY SHIPPED AND ADOPTION WAS THE WHOLE GAP.
4
+ // `hooks/tier.mjs:injectLine` slims a multi-line message to *first line +
5
+ // `[+N lines · record:<type> · retrieve_message id=<uuid>]`* — but ONLY when
6
+ // `record.type` is a string. Measured on the groundwork room 2026-08-30: 206
7
+ // messages, 175 multi-line, 33 typed, 33 slimmed; the coordinator had sent 101
8
+ // and typed 0. The renderer was never the problem.
9
+ //
10
+ // SO THE RULE IS ENFORCED AT THE SEND, NOT THE RENDER. An untyped agent→agent
11
+ // message must not be able to EXIST, or the next reader re-derives the type
12
+ // from prose and we are back to parsing English. Prose is a second source of
13
+ // truth about what a message meant.
14
+ //
15
+ // Three things this module is careful about, each one a stated acceptance
16
+ // property rather than an implementation taste:
17
+ //
18
+ // 1. STAGED, WITH A NAMED DATE. Five fleets share this bus and 7 of 13 server
19
+ // processes predate 0.26.7. A refusal shipped before a verified restart
20
+ // rejects messages from agents that CANNOT comply — the sender gets an
21
+ // error it has no code path to satisfy. So: WARN until the cutover, REFUSE
22
+ // after it, and the warning names the date in every single message so the
23
+ // deadline arrives having been announced ~every send rather than once.
24
+ //
25
+ // 2. THE REFUSAL NAMES A TYPE THE SENDER MAY ACTUALLY USE. `go` and `scope`
26
+ // are coordinator-restricted and `verdict` is gate-runner-restricted
27
+ // (RECORD_AUTHORITY): the aide hit exactly this, being told to use `scope`
28
+ // by one check and refused it by another. Naming a forbidden type is
29
+ // unusable guidance, so every suggestion is filtered through what this
30
+ // sender's role may emit before it is offered.
31
+ //
32
+ // 3. `fyi` IS AN HONEST CATCH-ALL. If the nine types do not cover a legitimate
33
+ // send, agents mistype into whatever passes and the type becomes noise —
34
+ // which costs more than the untyped message did. Anything unrecognised
35
+ // suggests `fyi` and says so plainly, never a forced `decision`/`risk`.
36
+ // The named cutover. Until this instant an untyped agent→agent send WARNS and
37
+ // is stored; from it, the same send is refused.
38
+ //
39
+ // A DATE, NOT A VERSION, because the constraint being waited on is a fleet-wide
40
+ // RESTART, and no version string on the wire can answer "has every server
41
+ // restarted" — 0.26.7 is published while servers predating it still run. Time
42
+ // is the only clock every one of those processes shares.
43
+ export const TYPED_RECORD_CUTOVER_ISO = "2026-09-15T00:00:00.000Z";
44
+ export const TYPED_RECORD_CUTOVER_MS = Date.parse(TYPED_RECORD_CUTOVER_ISO);
45
+ // `AGENT_COORD_TYPED_RECORDS=warn|refuse` overrides the date. It exists for two
46
+ // callers and no others: tests (which must exercise both sides of a date that
47
+ // has not arrived) and an operator who needs to pull the refusal back for a
48
+ // fleet mid-incident. It is NOT the exemption — an exemption is per-agent and
49
+ // visible (see `proseOnly`); this switch is fleet-wide and invisible, which is
50
+ // exactly why it is not how a less capable model gets on the bus.
51
+ export function typedRecordMode(now = Date.now()) {
52
+ const env = process.env.AGENT_COORD_TYPED_RECORDS;
53
+ if (env === "warn" || env === "refuse")
54
+ return env;
55
+ return now >= TYPED_RECORD_CUTOVER_MS ? "refuse" : "warn";
56
+ }
57
+ // ---------- suggesting the type the sender should have used ----------
58
+ // A rejection that says only "record required" costs a round trip; one that
59
+ // says "looks like a `done` — it cites a PR" costs none. A FAILURE PATH MUST BE
60
+ // MORE INFORMATIVE THAN THE SUCCESS PATH HERE, because it fires during a
61
+ // migration, at agents that are mid-slice and did nothing wrong yesterday.
62
+ //
63
+ // Read from the text the fleet already writes: the six canonical prefixes are
64
+ // in every card and on ~every message, so the type is usually already declared
65
+ // in English one token in.
66
+ const PREFIX_TYPES = [
67
+ [/^\s*DONE\b/i, "done", "the message opens with the DONE: prefix"],
68
+ [/^\s*BLOCKER\b/i, "blocker", "the message opens with the BLOCKER: prefix"],
69
+ [/^\s*RISK\b/i, "risk", "the message opens with the RISK: prefix"],
70
+ [/^\s*FYI\b/i, "fyi", "the message opens with the FYI: prefix"],
71
+ [/^\s*AGENT_ACTION\b/i, "action", "the message opens with the AGENT_ACTION: prefix"],
72
+ [/^\s*DAVID_DECISION\b/i, "decision", "the message opens with the DAVID_DECISION: prefix"],
73
+ [/^\s*GO\b/i, "go", "the message opens with the GO: prefix"],
74
+ [/^\s*SCOPE(?:\s+CHANGE)?\b/i, "scope", "the message opens with the SCOPE: prefix"],
75
+ ];
76
+ // Weaker than a prefix and only consulted when there is no prefix at all.
77
+ const SHAPE_TYPES = [
78
+ [/\b(PASS|FAIL)\b.*\b[0-9a-f]{7,40}\b/, "verdict", "it reads as a gate verdict over a commit sha"],
79
+ [/^\s*(PASS|FAIL)\b/, "verdict", "it opens with a PASS/FAIL gate result"],
80
+ [/(^|\s)(#\d+|[\w.-]+\/[\w.-]+#\d+|https:\/\/github\.com\/\S+\/pull\/\d+)/, "done", "it cites a PR"],
81
+ ];
82
+ /**
83
+ * The record type this message most likely should have carried, CONSTRAINED to
84
+ * what this sender is allowed to emit.
85
+ *
86
+ * `mayNotEmit` is the restricted set this role is refused (recordAuthorityFor).
87
+ * A suggestion landing in it is downgraded to `fyi` and the downgrade is
88
+ * reported rather than hidden — being quietly steered off `go` reads as the
89
+ * heuristic being bad, when in fact the role simply does not own that type.
90
+ */
91
+ export function suggestRecordType(text, mayNotEmit = []) {
92
+ const forbidden = new Set(mayNotEmit);
93
+ const firstLine = (text ?? "").split("\n", 1)[0] ?? "";
94
+ const hit = PREFIX_TYPES.find(([re]) => re.test(firstLine)) ??
95
+ SHAPE_TYPES.find(([re]) => re.test(firstLine) || re.test(text ?? ""));
96
+ if (!hit) {
97
+ return {
98
+ type: "fyi",
99
+ why: "nothing in the text names a kind — 'fyi' is the honest catch-all and is never wrong on purpose",
100
+ };
101
+ }
102
+ const [, type, why] = hit;
103
+ if (forbidden.has(type)) {
104
+ return {
105
+ type: "fyi",
106
+ downgradedFrom: type,
107
+ why: `${why}, but '${type}' is restricted to roles this sender does not hold — 'fyi' carries the same slimming`,
108
+ };
109
+ }
110
+ return { type, why };
111
+ }
112
+ /**
113
+ * The one line of guidance appended to both the warning and the refusal. Same
114
+ * words either side of the cutover ON PURPOSE: an agent that reads it during
115
+ * the warn window has already been told the exact call that will keep working.
116
+ */
117
+ export function typedRecordGuidance(s) {
118
+ const cite = s.type === "done"
119
+ ? `, cites: [{kind:'pr', ref:'<owner/repo#N>'}]` // a `done` is refused without one anyway
120
+ : "";
121
+ return (`looks like a '${s.type}' — ${s.why}. ` +
122
+ `Add record: {type:'${s.type}', payload:{summary:'<one line>'}${cite}} to this same call; ` +
123
+ `'text' is untouched by the record and your wording always wins.`);
124
+ }
125
+ // ---------- measuring adoption, and the inverse failure ----------
126
+ // SUCCESS IS THE TRAFFIC TABLE RE-MEASURED, NOT THIS TASK MERGED. The baseline
127
+ // captured the day the rule was made: 212 room messages, 37 typed (17%),
128
+ // 425,903 bytes sitting below line 1 — bytes every reader pays for and no
129
+ // reader asked for.
130
+ //
131
+ // AND THE INVERSE FAILURE LOOKS EXACTLY LIKE SUCCESS ON A COVERAGE NUMBER. If
132
+ // `fyi` becomes nearly everything, coverage reads ~100% while agents are
133
+ // mistyping to get past the gate and the type has stopped carrying
134
+ // information — that is the catch-all rule (12.5) failing, not holding. So the
135
+ // distribution is reported alongside the percentage and is what the verdict
136
+ // keys on. A coverage-only check would certify the failure it exists to catch.
137
+ //
138
+ // The threshold is deliberately generous: `fyi` IS the honest answer for a
139
+ // large share of real traffic, so this fires on domination, not on presence.
140
+ export const FYI_DOMINANCE_THRESHOLD = 0.75;
141
+ /** `messages` is any array of stored messages ({text, record?}). */
142
+ export function typedRecordStats(messages) {
143
+ const distribution = {};
144
+ let typed = 0;
145
+ let unslimmedBytes = 0;
146
+ for (const m of messages) {
147
+ const type = typeof m.record?.type === "string" ? m.record.type : undefined;
148
+ distribution[type ?? "untyped"] = (distribution[type ?? "untyped"] ?? 0) + 1;
149
+ if (type) {
150
+ typed++;
151
+ continue;
152
+ }
153
+ const text = m.text ?? "";
154
+ const nl = text.indexOf("\n");
155
+ if (nl !== -1)
156
+ unslimmedBytes += Buffer.byteLength(text.slice(nl + 1), "utf8");
157
+ }
158
+ const total = messages.length;
159
+ const coverage = total ? typed / total : 0;
160
+ const fyiShareOfTyped = typed ? (distribution.fyi ?? 0) / typed : 0;
161
+ // Order matters: degeneracy is checked BEFORE coverage, or a room that is
162
+ // 100% `fyi` reports "healthy" — the exact misreading this guards.
163
+ const degenerate = typed >= 10 && fyiShareOfTyped > FYI_DOMINANCE_THRESHOLD;
164
+ const verdict = degenerate ? "degenerate" : coverage >= 0.95 ? "healthy" : "adopting";
165
+ const note = degenerate
166
+ ? `'fyi' is ${(fyiShareOfTyped * 100).toFixed(0)}% of typed messages — coverage looks solved while the type has ` +
167
+ `stopped carrying information. Agents are typing to pass the gate, which is 12.5 FAILING, not holding.`
168
+ : verdict === "healthy"
169
+ ? `${(coverage * 100).toFixed(0)}% typed with a spread distribution — the reader-side cost is paid down and the type still discriminates.`
170
+ : `${(coverage * 100).toFixed(0)}% typed; ${unslimmedBytes.toLocaleString("en-US")} bytes still sit below line 1 in untyped messages, ` +
171
+ `carried by every reader.`;
172
+ return { total, typed, coverage: Number(coverage.toFixed(3)), unslimmedBytes, distribution, fyiShareOfTyped: Number(fyiShareOfTyped.toFixed(3)), verdict, note };
173
+ }
174
+ //# sourceMappingURL=typed-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed-records.js","sourceRoot":"","sources":["../src/typed-records.ts"],"names":[],"mappings":"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"}
package/dist/work.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BOARD_ARITY, isMalformedRow, EM_DASH_SEP, MIDDOT_SEP, parseQueueLine, parseDoneLine, renderQueueLine, renderDoneLine, renderBoardRow, parseWorkDoc, renderWorkDoc, renderWorkDocForWrite, stampQueueIds, zeroIsUnparsed, substantiveLines, queueItemsOf, doneEntriesOf, boardRowsOf, workstreamsV1RowsOf, listWorkBoardOf, projectV1ToLanes, workDocIssues, workDocLegacyWriteIssues, LANES_V0_WRITE_ISSUE, } from "@davidbalzan/groundwork-seam/docs";
2
+ //# sourceMappingURL=work.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"work.js","sourceRoot":"","sources":["../src/work.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,WAAW,EAEX,cAAc,EAGd,WAAW,EACX,UAAU,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACZ,aAAa,EACb,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACxB,oBAAoB,GAGrB,MAAM,mCAAmC,CAAC"}