agent-coord-mcp 0.26.18 → 0.26.19

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 (73) hide show
  1. package/dist/build.js +113 -0
  2. package/dist/build.js.map +1 -0
  3. package/dist/capabilities.js +158 -0
  4. package/dist/capabilities.js.map +1 -0
  5. package/dist/prefix.js +64 -0
  6. package/dist/prefix.js.map +1 -0
  7. package/dist/roles.js +132 -0
  8. package/dist/roles.js.map +1 -0
  9. package/dist/server-identity.js +82 -0
  10. package/dist/server-identity.js.map +1 -0
  11. package/dist/server.js +647 -0
  12. package/dist/server.js.map +1 -0
  13. package/dist/store.js +553 -0
  14. package/dist/store.js.map +1 -0
  15. package/dist/tools/admin.js +323 -0
  16. package/dist/tools/admin.js.map +1 -0
  17. package/dist/tools/attention.js +73 -0
  18. package/dist/tools/attention.js.map +1 -0
  19. package/dist/tools/away.js +285 -0
  20. package/dist/tools/away.js.map +1 -0
  21. package/dist/tools/board-ref.js +208 -0
  22. package/dist/tools/board-ref.js.map +1 -0
  23. package/dist/tools/event-kinds.js +39 -0
  24. package/dist/tools/event-kinds.js.map +1 -0
  25. package/dist/tools/events.js +234 -0
  26. package/dist/tools/events.js.map +1 -0
  27. package/dist/tools/index.js +14 -0
  28. package/dist/tools/index.js.map +1 -0
  29. package/dist/tools/logwatch.js +85 -0
  30. package/dist/tools/logwatch.js.map +1 -0
  31. package/dist/tools/messaging.js +706 -0
  32. package/dist/tools/messaging.js.map +1 -0
  33. package/dist/tools/record-events.js +380 -0
  34. package/dist/tools/record-events.js.map +1 -0
  35. package/dist/tools/records.js +1027 -0
  36. package/dist/tools/records.js.map +1 -0
  37. package/dist/tools/registry.js +500 -0
  38. package/dist/tools/registry.js.map +1 -0
  39. package/dist/tools/render.js +2 -0
  40. package/dist/tools/render.js.map +1 -0
  41. package/dist/tools/rooms.js +210 -0
  42. package/dist/tools/rooms.js.map +1 -0
  43. package/dist/tools/rotate.js +143 -0
  44. package/dist/tools/rotate.js.map +1 -0
  45. package/dist/tools/scopes.js +126 -0
  46. package/dist/tools/scopes.js.map +1 -0
  47. package/dist/tools/shared.js +104 -0
  48. package/dist/tools/shared.js.map +1 -0
  49. package/dist/tools/stall.js +414 -0
  50. package/dist/tools/stall.js.map +1 -0
  51. package/dist/tools/transport.js +1878 -0
  52. package/dist/tools/transport.js.map +1 -0
  53. package/dist/tools/work.js +373 -0
  54. package/dist/tools/work.js.map +1 -0
  55. package/dist/tools/worktrees.js +581 -0
  56. package/dist/tools/worktrees.js.map +1 -0
  57. package/dist/typed-records.js +174 -0
  58. package/dist/typed-records.js.map +1 -0
  59. package/dist/work.js +2 -0
  60. package/dist/work.js.map +1 -0
  61. package/hooks/peek-coord.mjs +0 -0
  62. package/hooks/tmux-pusher.mjs +0 -0
  63. package/package.json +11 -14
  64. package/scripts/coord-attention-clock.mjs +0 -0
  65. package/scripts/coord-node.sh +0 -0
  66. package/scripts/coord-stall-clock.mjs +0 -0
  67. package/scripts/coord-token.mjs +0 -0
  68. package/scripts/probe-tmux-liveness.sh +0 -0
  69. package/scripts/spawn-agent.sh +0 -0
  70. package/scripts/stop-agent.sh +0 -0
  71. package/scripts/typed-record-stats.mjs +0 -0
  72. package/src/server.ts +9 -1
  73. package/src/tools/worktrees.ts +234 -32
@@ -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"}
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.26.18",
3
+ "version": "0.26.19",
4
4
  "description": "File-backed MCP server for coordinating multiple AI coding agents (Claude Code, Cursor, Cline, etc.). Local stdio or networked over Streamable HTTP.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,18 +8,6 @@
8
8
  "coord-chat": "scripts/coord-chat.mjs",
9
9
  "coord-pusher": "scripts/coord-pusher.mjs"
10
10
  },
11
- "scripts": {
12
- "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
13
- "build": "pnpm run clean && tsc",
14
- "prepare": "node scripts/check-self-dependency.mjs && pnpm run clean && tsc",
15
- "prepack": "node scripts/check-self-dependency.mjs && pnpm run build",
16
- "prepublishOnly": "node scripts/check-publish-tool.mjs",
17
- "start": "node dist/server.js",
18
- "dev": "tsx src/server.ts",
19
- "pretest": "node scripts/check-self-dependency.mjs && node scripts/check-optional-call.mjs && tsc",
20
- "test": "node scripts/check-test-count.mjs",
21
- "test:raw": "node --test \"test/*.test.mjs\""
22
- },
23
11
  "files": [
24
12
  "dist",
25
13
  "src",
@@ -65,5 +53,14 @@
65
53
  "@types/proper-lockfile": "^4.1.4",
66
54
  "tsx": "^4.23.12",
67
55
  "typescript": "^7.0.2"
56
+ },
57
+ "scripts": {
58
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
59
+ "build": "pnpm run clean && tsc",
60
+ "start": "node dist/server.js",
61
+ "dev": "tsx src/server.ts",
62
+ "pretest": "node scripts/check-self-dependency.mjs && node scripts/check-optional-call.mjs && tsc",
63
+ "test": "node scripts/check-test-count.mjs",
64
+ "test:raw": "node --test \"test/*.test.mjs\""
68
65
  }
69
- }
66
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/server.ts CHANGED
@@ -96,6 +96,8 @@ import {
96
96
  ensureWorktreeTool,
97
97
  refreshWorktreesSchema,
98
98
  refreshWorktreesTool,
99
+ releaseWorktreeSchema,
100
+ releaseWorktreeTool,
99
101
  claimSchema,
100
102
  claimTool,
101
103
  landSchema,
@@ -659,6 +661,12 @@ function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {
659
661
  refreshWorktreesSchema,
660
662
  gate(null, refreshWorktreesTool as (a: Record<string, unknown>) => Promise<unknown>),
661
663
  );
664
+ addTool(
665
+ "release_worktree",
666
+ "GIVE A WORKTREE BACK — the counterpart to ensure_worktree, which created trees while nothing released them, leaving `git worktree remove`/`prune` as a hand step in five role cards. Identify the tree by `agentId` or `path`. REFUSES the primary checkout (the path everyone already has); REFUSES a DIRTY tree with no override, because uncommitted work is what a diff cannot show and git cannot give back; and REFUSES a tree whose commits cannot be shown to have landed. Landedness is decided by PATCH ID, never ancestry — under squash merge a landed branch's shas never enter the base, so ancestry calls every finished tree mid-slice forever and would refuse exactly the trees that are safe to release. A patch-id negative is INCONCLUSIVE rather than a no, so it refuses and says so; `force:true` releases anyway and is recorded in the result. Removing and pruning are ONE act here, not two lines of prose the caller remembers. Reports by default; pass apply:true to remove.",
667
+ releaseWorktreeSchema,
668
+ gate(null, releaseWorktreeTool as (a: Record<string, unknown>) => Promise<unknown>),
669
+ );
662
670
 
663
671
  addTool(
664
672
  "next_unblocked",
@@ -669,7 +677,7 @@ function buildServer(initialBound?: string, opts: { trackSession?: boolean } = {
669
677
 
670
678
  addTool(
671
679
  "claim",
672
- "Bind a queue item to an agent and produce the 🚧 board row. With no itemId it takes next_unblocked. WARNS LOUDLY while `ensure_worktree` (Phase 5 Task 1) does not exist: the claim binds the item and the row only, and creating an isolated worktree at origin/<base> is still yours. The warning is conditional on that verb's absence, so it stops once Task 1 lands.",
680
+ "Bind a queue item to an agent, ENSURE THE AGENT HAS AN ISOLATED WORKTREE, and produce the \ud83d\udea7 board row. With no itemId it takes next_unblocked. `claim` CALLS `ensure_worktree` rather than telling you to — a worktree that cannot be ensured is a REFUSAL, not a warning, because binding an item to an agent with nowhere isolated to work is the shared-checkout failure this pair exists to prevent. A REUSED tree that is not at origin/<base> is also refused, naming how far behind it is and pointing at `refresh_worktrees`: `claim` means START SOMETHING NEW, and a tree left on last week's base produces confidently-wrong work with nothing about the result looking stale. A freshly created tree is cut from origin/<base> and needs no such check. Returns `worktreeEnsured` with the tree's path and branch, and writes the board cell as the REMOTE-TRACKING ref so the same row reads identically from every checkout.",
673
681
  claimSchema,
674
682
  gate(null, claimTool as (a: Record<string, unknown>) => Promise<unknown>),
675
683
  );
@@ -227,42 +227,18 @@ async function create(a: {
227
227
  atBase: true,
228
228
  behindBy: 0,
229
229
  ...(a.ephemeral
230
- ? { ephemeral: true, removeWith: `git -C ${repo} worktree remove --force ${target} && git -C ${repo} branch -D ${branch}` }
230
+ ? {
231
+ ephemeral: true,
232
+ // A VERB NAME, NOT A COMMAND LINE. This used to hand back the shell to
233
+ // paste — the tool knew the repo, path and branch and spent the agent's
234
+ // tokens on work it could do itself.
235
+ releaseWith: { verb: "release_worktree", args: { repo, path: target, apply: true } },
236
+ }
231
237
  : {}),
232
238
  };
233
239
  }
234
240
 
235
- // ---------- 1.2 / 1.3: refresh idle trees, never mid-slice ----------
236
-
237
- export const refreshWorktreesSchema = {
238
- repo: z.string().min(1),
239
- base: z.string().min(1),
240
- apply: z.boolean().optional(),
241
- };
242
-
243
- /**
244
- * Fast-forward IDLE trees onto `origin/<base>`. Never `--force`, never a
245
- * mid-slice tree.
246
- *
247
- * A tree is MID-SLICE when it is dirty or holds commits the base does not.
248
- * Pulling under someone's feet is worse than staleness: staleness is visible in
249
- * a diff, a clobbered work-in-progress is not. So dirty or diverged is REFUSED
250
- * per tree and named, and the run continues for the others.
251
- */
252
- export async function refreshWorktreesTool(args: { repo: string; base: string; apply?: boolean }) {
253
- const { repo, base } = args;
254
- const ref = `origin/${base}`;
255
- try {
256
- git(repo, ["fetch", "--quiet", "origin", base]);
257
- } catch {
258
- /* reported per tree below */
259
- }
260
- let tip: string;
261
- try {
262
- tip = git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]);
263
- } catch {
264
- return { ok: false as const, error: `${ref} does not resolve — nothing to fast-forward onto` };
265
- }
241
+ // ---------- shared: is this tree's work already in the base? ----------
266
242
 
267
243
  /**
268
244
  * Are the commits HEAD has beyond `tip` ALREADY LANDED there?
@@ -303,6 +279,39 @@ function aheadIsLanded(at: string, tip: string): boolean {
303
279
  .every((l) => l.trim().startsWith("-"));
304
280
  }
305
281
 
282
+ // ---------- 1.2 / 1.3: refresh idle trees, never mid-slice ----------
283
+
284
+ export const refreshWorktreesSchema = {
285
+ repo: z.string().min(1),
286
+ base: z.string().min(1),
287
+ apply: z.boolean().optional(),
288
+ };
289
+
290
+ /**
291
+ * Fast-forward IDLE trees onto `origin/<base>`. Never `--force`, never a
292
+ * mid-slice tree.
293
+ *
294
+ * A tree is MID-SLICE when it is dirty or holds commits the base does not.
295
+ * Pulling under someone's feet is worse than staleness: staleness is visible in
296
+ * a diff, a clobbered work-in-progress is not. So dirty or diverged is REFUSED
297
+ * per tree and named, and the run continues for the others.
298
+ */
299
+ export async function refreshWorktreesTool(args: { repo: string; base: string; apply?: boolean }) {
300
+ const { repo, base } = args;
301
+ const ref = `origin/${base}`;
302
+ try {
303
+ git(repo, ["fetch", "--quiet", "origin", base]);
304
+ } catch {
305
+ /* reported per tree below */
306
+ }
307
+ let tip: string;
308
+ try {
309
+ tip = git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]);
310
+ } catch {
311
+ return { ok: false as const, error: `${ref} does not resolve — nothing to fast-forward onto` };
312
+ }
313
+
314
+
306
315
  const results = [];
307
316
  for (const w of listWorktrees(repo)) {
308
317
  const at = w.path;
@@ -415,3 +424,196 @@ function aheadIsLanded(at: string, tip: string): boolean {
415
424
  }
416
425
  return { ok: true as const, base: ref, tip: tip.slice(0, 8), applied: args.apply === true, trees: results };
417
426
  }
427
+
428
+ // ---------- the counterpart to ensure_worktree: give the tree back ----------
429
+
430
+ /*
431
+ * `ensure_worktree` CREATED trees and nothing RELEASED them, so teardown stayed a
432
+ * hand step in five role cards — `git worktree prune` in coord-audit, coord-ci,
433
+ * coord-qa and coord-worker, `git worktree remove` twice in coordinator, which
434
+ * says so itself: "it never removes, so `git worktree remove`/`prune` remain hand
435
+ * steps". Seven commands for one act the bus already had the inputs for.
436
+ *
437
+ * WORSE THAN ABSENT: `ensure_worktree` COMPUTED the teardown command and handed it
438
+ * back as a string for the agent to paste (`removeWith`). The tool knew the repo,
439
+ * the path and the branch, and spent the agent's tokens on shell it could have run.
440
+ * That is the shape David ruled against: "where possible we should hide the
441
+ * plumbing from the agents".
442
+ *
443
+ * THE REFUSALS ARE THE POINT, and they inherit `refresh_worktrees`' direction —
444
+ * report by default, refuse per tree with the reason named, never clobber:
445
+ *
446
+ * - THE PRIMARY CHECKOUT IS NEVER RELEASED. It is the path everyone already has.
447
+ * - A DIRTY TREE IS REFUSED. Uncommitted work is what a diff cannot show and git
448
+ * cannot give back, so this is the one refusal with no override.
449
+ * - A TREE HOLDING UNLANDED COMMITS IS REFUSED — and "unlanded" is decided by
450
+ * `aheadIsLanded` (patch ids), NOT by ancestry. Under squash merge a landed
451
+ * branch's shas never enter the base, so ancestry calls every finished tree
452
+ * mid-slice forever (q-5c40db91). Using ancestry here would refuse to release
453
+ * exactly the trees that are safe to release, which is how the hand step
454
+ * survived: the guard would have been wrong and everyone would have forced it.
455
+ *
456
+ * A NEGATIVE FROM `aheadIsLanded` IS INCONCLUSIVE, NOT A NO. Several commits squashed
457
+ * into one carry a different combined patch id, so it cannot prove landedness for
458
+ * every shape. It therefore refuses and says the check was inconclusive rather than
459
+ * claiming the work is unlanded — the caller lands it, or passes `force:true`, which
460
+ * is recorded in the result so a forced release is findable afterwards.
461
+ */
462
+ export const releaseWorktreeSchema = {
463
+ repo: z.string().min(1),
464
+ agentId: z.string().min(1).optional(),
465
+ path: z.string().min(1).optional(),
466
+ base: z.string().min(1).optional(),
467
+ apply: z.boolean().optional(),
468
+ force: z.boolean().optional(),
469
+ };
470
+
471
+ export async function releaseWorktreeTool(args: {
472
+ repo: string;
473
+ agentId?: string;
474
+ path?: string;
475
+ base?: string;
476
+ apply?: boolean;
477
+ force?: boolean;
478
+ }) {
479
+ const { repo } = args;
480
+ if (!args.agentId && !args.path) {
481
+ return { ok: false as const, error: "name the tree to release: pass `agentId` or `path`" };
482
+ }
483
+
484
+ const trees = listWorktrees(repo);
485
+ const primary = primaryOf(repo) ?? "";
486
+ const target = args.path
487
+ ? trees.find((w) => samePath(w.path, args.path as string))
488
+ : trees.find((w) => (w.branch ?? "").includes(args.agentId as string) || w.path.includes(args.agentId as string));
489
+
490
+ if (!target) {
491
+ return {
492
+ ok: false as const,
493
+ error: args.path
494
+ ? `no worktree at '${args.path}' — nothing to release`
495
+ : `no worktree found for '${args.agentId}' — nothing to release`,
496
+ // An ABSENCE is reported with what WAS found, so a misaimed lookup is
497
+ // distinguishable from an empty world (the positive-control rule).
498
+ known: trees.map((w) => ({ path: w.path, branch: w.branch })),
499
+ };
500
+ }
501
+
502
+ const at = target.path;
503
+ const branch = target.branch;
504
+
505
+ if (samePath(at, primary)) {
506
+ return {
507
+ ok: false as const,
508
+ error: `refusing to release ${at}: that is the PRIMARY checkout, the path everyone already has`,
509
+ path: at,
510
+ };
511
+ }
512
+
513
+ if (isDirty(at)) {
514
+ return {
515
+ ok: false as const,
516
+ error:
517
+ `refusing to release ${at}: the tree is DIRTY. Uncommitted work is what a diff cannot show and ` +
518
+ `git cannot give back, so this refusal has no override — commit it, stash it, or delete it by hand.`,
519
+ path: at,
520
+ branch,
521
+ dirty: true,
522
+ };
523
+ }
524
+
525
+ const base = args.base ?? "main";
526
+ const ref = `origin/${base}`;
527
+ let tip: string | null = null;
528
+ try {
529
+ git(repo, ["fetch", "--quiet", "origin", base]);
530
+ } catch {
531
+ /* a stale tip is reported below rather than guessed at */
532
+ }
533
+ try {
534
+ tip = git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]);
535
+ } catch {
536
+ tip = null;
537
+ }
538
+
539
+ let ahead = "unknown";
540
+ if (tip) {
541
+ try {
542
+ ahead = git(at, ["rev-list", "--count", `${tip}..HEAD`]);
543
+ } catch {
544
+ /* left unknown — an unreadable count must not read as zero */
545
+ }
546
+ }
547
+
548
+ const holdsWork = ahead !== "0";
549
+ const landed = tip && holdsWork ? aheadIsLanded(at, tip) : true;
550
+
551
+ if (!tip) {
552
+ return {
553
+ ok: false as const,
554
+ error: `refusing to release ${at}: ${ref} does not resolve, so whether this tree's work landed cannot be measured`,
555
+ path: at,
556
+ branch,
557
+ };
558
+ }
559
+
560
+ if (holdsWork && !landed && !args.force) {
561
+ return {
562
+ ok: false as const,
563
+ error:
564
+ `refusing to release ${at}: it is ${ahead} commit(s) ahead of ${ref} and the patch-id check was ` +
565
+ `INCONCLUSIVE — not proof the work is unlanded, but not proof it landed either (several commits ` +
566
+ `squashed into one carry a different combined patch id). Land it, or pass force:true, which is recorded.`,
567
+ path: at,
568
+ branch,
569
+ ahead,
570
+ landedCheck: "inconclusive",
571
+ };
572
+ }
573
+
574
+ const plan = {
575
+ path: at,
576
+ branch,
577
+ ahead,
578
+ landedCheck: holdsWork ? (landed ? "landed" : "inconclusive") : "nothing-ahead",
579
+ ...(args.force && holdsWork && !landed ? { forced: true } : {}),
580
+ };
581
+
582
+ if (!args.apply) {
583
+ return {
584
+ ok: true as const,
585
+ applied: false,
586
+ would: "remove the worktree and delete its branch",
587
+ ...plan,
588
+ note: "reports by default; pass apply:true to remove",
589
+ };
590
+ }
591
+
592
+ try {
593
+ git(repo, ["worktree", "remove", "--force", at]);
594
+ } catch (e) {
595
+ return {
596
+ ok: false as const,
597
+ error: `worktree remove failed: ${String((e as Error).message).split("\n")[0]}`,
598
+ ...plan,
599
+ };
600
+ }
601
+ // PRUNE IS PART OF THE ACT, not a step the caller remembers. Every card that
602
+ // said `git worktree remove` also said `git worktree prune`, which is the
603
+ // clearest evidence they were one operation split across two lines of prose.
604
+ try {
605
+ git(repo, ["worktree", "prune"]);
606
+ } catch {
607
+ /* removal already succeeded; a failed prune is not worth failing the call */
608
+ }
609
+ let branchDeleted = false;
610
+ if (branch) {
611
+ try {
612
+ git(repo, ["branch", "-D", branch.replace(/^refs\/heads\//, "")]);
613
+ branchDeleted = true;
614
+ } catch {
615
+ /* a branch that will not delete is reported, not fatal */
616
+ }
617
+ }
618
+ return { ok: true as const, applied: true, removed: true, branchDeleted, ...plan };
619
+ }