agent-coord-mcp 0.26.23 → 0.26.24

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.
@@ -51,7 +51,8 @@ import { queueWriteSchema } from "./tools/queue-write.js";
51
51
  import { treeProvenance } from "./tools/tree-provenance.js";
52
52
  import { detectSpread } from "./server-spread.js";
53
53
  import { HerdrTransport, herdrKeyName } from "./transports/herdr.js";
54
- import { HERDR } from "./transports/types.js";
54
+ import { tickVerdict } from "./tools/tick.js";
55
+ import { HERDR, TICK_READS_AS, TICK_STORED_AS, type TickState } from "./transports/types.js";
55
56
 
56
57
  export type ProbeResult = {
57
58
  id: string;
@@ -237,6 +238,35 @@ const PROBES: Probe[] = [
237
238
  };
238
239
  },
239
240
  },
241
+ {
242
+ // ⟨q-b607005a⟩ — a WRITER must not impose this queue's id grammar on a
243
+ // document that does not use it. Measured on a consumer fleet 2026-09-16: filing ONE item
244
+ // stamped `⟨q-…⟩` onto ALL 168 rows of a queue keyed by `[Q-nnn]` and broke
245
+ // two green guards. The property is not "it stamps" — stamping a queue that
246
+ // ALREADY records ids is correct and must not regress. It is that the
247
+ // document's own prior art decides, and the row THIS CALL authored is the
248
+ // exception that keeps the absorption defect closed.
249
+ id: "stamp-respects-foreign-grammar",
250
+ since: "0.26.24",
251
+ run: () => {
252
+ const head = ["---", 'title: "Queue"', "---", "", "## Queue", ""];
253
+ const foreign = parseWorkDoc(head.concat(["- [ ] (P1) [Q-001] a consumer's own convention"]).join("\n"));
254
+ const native = parseWorkDoc(
255
+ head.concat(["- [ ] (P1) ⟨q-11111111⟩ records its id", "- [ ] (P2) appended by hand"]).join("\n"),
256
+ );
257
+ const foreignOwn = queueItemsOf(foreign)[0]?.id ?? "";
258
+ const left = stampQueueIds(foreign).stamped.length;
259
+ const owned = stampQueueIds(foreign, { own: [foreignOwn] }).stamped.length;
260
+ const absorbed = stampQueueIds(native).stamped.length;
261
+ const present = left === 0 && owned === 1 && absorbed === 1;
262
+ return {
263
+ present,
264
+ evidence:
265
+ `foreign doc, not ours -> stamped ${left} · foreign doc, our own row -> stamped ${owned} · ` +
266
+ `doc that already records ids -> absorbed ${absorbed} -> present=${present}`,
267
+ };
268
+ },
269
+ },
240
270
  {
241
271
  // #293 — `detectSpread` did not exist before 0.26.22. The property is not
242
272
  // "it answers": an UNREADABLE seat must poison the verdict rather than be
@@ -272,6 +302,28 @@ const PROBES: Probe[] = [
272
302
  return { present, evidence: `"⛔ Blocked — was 🚧 …" in flight: ${blocked} (0.26.21 said true) · "🔍 In Review": ${review} -> present=${present}` };
273
303
  },
274
304
  },
305
+ {
306
+ // ⟨q-1c95f7d4⟩ Phase 5.4 Task 5 — the external tick, probed by CALLING the code that
307
+ // decides what a reading MEANS (every probe here is synchronous, so the async read is
308
+ // exercised by its test suite and this asserts the decision layer plus the wiring):
309
+ // a `blocked` reading is a measured HIT, an `idle` one is coverage and NOT a hit, an
310
+ // unreadable one is neither, and herdr's measured write/read asymmetry is in place.
311
+ id: "external-tick-evidence",
312
+ since: "0.26.24",
313
+ run: () => {
314
+ const seat = (state: TickState) => ({ agentId: "probe", transport: HERDR, readable: true as const, state, source: "herdr pane w0:p0" });
315
+ const blocked = tickVerdict(seat("blocked"));
316
+ const idle = tickVerdict(seat("idle"));
317
+ const blind = tickVerdict({ agentId: "probe", transport: HERDR, readable: false as const, why: "no agent there" });
318
+ const wired = typeof new HerdrTransport().readTick === "function" && typeof new HerdrTransport().publishTick === "function";
319
+ const stored = TICK_STORED_AS.idle === "done" && TICK_READS_AS.done === "idle";
320
+ const present = blocked?.hit === true && idle?.hit === false && idle?.measured === true && blind?.measured === false && wired && stored;
321
+ return {
322
+ present,
323
+ evidence: `tickVerdict blocked -> hit=${blocked?.hit} · idle -> hit=${idle?.hit} measured=${idle?.measured} · unreadable -> measured=${blind?.measured} · readTick/publishTick wired=${wired} · TICK_STORED_AS.idle="${TICK_STORED_AS.idle}" -> present=${present}`,
324
+ };
325
+ },
326
+ },
275
327
  {
276
328
  // Phase 5.4 Task 4 (0.26.23) — the herdr transport exists and refuses by name: a scripted
277
329
  // dead pane reads dead from herdr's own reply, and a tmux key name is refused, not typed.
@@ -473,6 +525,7 @@ export function probeCapabilities(context: { module: string; versionLabel: strin
473
525
  import { resolveServerIdentity } from "./server-identity.js";
474
526
  import { seatBuildOf, installedFrom, psReader, type SeatBuild } from "./tools/seat-build.js";
475
527
  import { isInFlightStatus } from "./tools/stall.js";
528
+ import { parseWorkDoc, queueItemsOf, stampQueueIds } from "@davidbalzan/groundwork-seam";
476
529
 
477
530
  export const capabilitiesSchema = {} as const;
478
531
 
package/src/gated-head.ts CHANGED
@@ -207,13 +207,189 @@ export function gatedBy(verdicts: PassVerdict[], head: string, mergedAt: number
207
207
  }
208
208
 
209
209
  /**
210
- * The typed verdict line on a PR page (#313's grammar): `QA GATE — **PASS** @ \`sha\``
211
- * or the merge-time `**GATE: PASS** … @ \`sha\``. Uppercase PASS/FAIL as a whole
212
- * word on a line that opens with the gate marker and carries an `@ sha`. Prose
213
- * that says "passes", "verdict" or "FAILs" does not match — #299's shape, where
214
- * a PASS|FAIL substring scan would have counted an explicit non-verdict review.
210
+ * ⛔ #313's GRAMMAR, KEPT ONLY FOR BACKWARDS COMPATIBILITY — IT MATCHED NOTHING IN
211
+ * THE FIELD. `QA GATE — **PASS** @ \`sha\`` was ruled canonical and then had ZERO
212
+ * ADOPTION: measured against the 9 merges of 2026-09-15, it matched 0 verdict
213
+ * lines while all 9 merges carried one, so `unverdicted-merge` fired 9 times on 9
214
+ * correct merges — 100% false positive, which trains the room to ignore the one
215
+ * instrument protecting the convention.
216
+ *
217
+ * ⭐ THE LESSON IS WHY THIS CONSTANT IS NO LONGER THE PREDICATE: A REGEX OVER PROSE
218
+ * TESTS THE PHRASING, NOT THE CLAIM. The verdicts were never missing; only the
219
+ * agreed wording was. Nobody noticed because every verdict is read BY EYE at gate
220
+ * time and they all read fine — the instrument and the humans disagreed silently
221
+ * for a day.
222
+ *
223
+ * ⚠ SCOPE, CORRECTED: "zero adoption" IS TRUE OF A CONSUMER FLEET AND FALSE OF THIS REPO.
224
+ * Measured: 0 of 9 of that fleet's merges carry this grammar; 4 of 6 kit merges do, verbatim
225
+ * (`#342`, `#340`, `#337`, `#336`). The original write-up scoped a measurement to one
226
+ * consumer and stated it universally.
227
+ *
228
+ * ⛔⛔ AND DO NOT BELIEVE THIS CONSTANT IS WHAT KEEPS THAT KIT GRAMMAR WORKING — I
229
+ * CLAIMED THAT AND IT IS FALSE. `verdictBoundTo` → `verdictClaimsIn` NEVER references
230
+ * `VERDICT_COMMENT`; it survives only in the legacy `verdictShasIn`. Compatibility comes
231
+ * from `DECLARES` accepting the `(?:QA )?GATE\b` opener. ⇒ THE TRAP THAT MAKES THIS
232
+ * WORTH WRITING DOWN: deleting this constant as dead code leaves the kit grammar working,
233
+ * so the experiment CONFIRMS the false mechanism — whoever ran it would conclude they had
234
+ * removed something load-bearing and got away with it. If you are removing it, the thing
235
+ * to check is `DECLARES`, not this.
215
236
  */
216
237
  export const VERDICT_COMMENT = /^(?:QA GATE|\*\*GATE|GATE)\b[^\n]*?\b(PASS|FAIL)\b(?:\s*\([^)\n]*\))?[^\n]*?@\s*`?([0-9a-f]{7,40})`?/;
238
+
239
+ /**
240
+ * WHAT MAKES A VERDICT BINDING, expressed as a shape instead of a phrase: a
241
+ * DISPOSITION and a SHA, on ONE LINE, with the disposition ahead of the `@`. The
242
+ * caller then decides bindingness by comparing that sha to the head it cares
243
+ * about — the claim, not the header.
244
+ *
245
+ * Derived from the five spellings actually in the field on 2026-09-15, none of
246
+ * which the constant above matches:
247
+ * `## QA verdict — DONE: PASS \`#970\` @ \`sha\` · mode: full-local · base …`
248
+ * `## QA verdict — DONE: CONTENT PASS \`#964\` @ \`sha\``
249
+ * `## QA verdict — DONE: READINESS RELEASED \`#964\` @ \`sha\``
250
+ * `## QA-2 — FULL PASS (content + readiness) @ \`sha\``
251
+ * `## qa-2 — CONTENT PASS (re-bind) @ \`sha\` · READINESS NOT CLEAR`
252
+ *
253
+ * ⛔ THE HALVES ARE DISTINGUISHED, BECAUSE A CONTENT PASS IS NOT A CLEARANCE. It
254
+ * says so in its own body ("READINESS NOT ESTABLISHED — do not merge yet"), so
255
+ * counting it as a whole verdict would license exactly the merge it forbids. A
256
+ * caller wanting "was this clear to merge" needs a WHOLE verdict, or BOTH halves.
257
+ *
258
+ * ⛔ ORDER MATTERS IN THE CLASSIFIER: `CONTENT PASS` and `READINESS RELEASED` are
259
+ * tested before bare `PASS`, and `READINESS NOT CLEAR`/`NOT ESTABLISHED` must not
260
+ * read as a readiness half — the live `## qa-2 — CONTENT PASS (re-bind) @ sha ·
261
+ * READINESS NOT CLEAR` line carries both phrases and is a CONTENT half only.
262
+ */
263
+ export type VerdictClaim = { result: string; sha: string; half: "whole" | "content" | "readiness" };
264
+ const NEGATED_READINESS = /READINESS\s+(?:NOT|IS\s+NOT)\b/i;
265
+ /**
266
+ * ⛔ THE LINE MUST DECLARE, NOT REMARK — and this anchor is here because removing it
267
+ * BROKE THE AIDE'S #299 CONTROL, which caught it immediately. That control's sharpest
268
+ * fixture is `Looks like a PASS to me — see the run @ \`sha\` for the log; not a gate.`:
269
+ * uppercase PASS, an `@`, and the MERGED HEAD's own sha. Disposition-plus-bound-sha alone
270
+ * counts it, and a chat remark then silences the check for a genuinely unverdicted merge.
271
+ *
272
+ * So a claim is a HEADING (every live verdict on 2026-09-15 is `## …`) or the legacy gate
273
+ * opener. That is still structure rather than phrasing — what it requires is that someone
274
+ * DECLARED a disposition, not that they used a particular sentence — and it is the part of
275
+ * #313's grammar worth keeping: `QA GATE` was the wrong half to demand, `is a declaration`
276
+ * was the right one.
277
+ */
278
+ const DECLARES = /^\s*(?:#{1,6}\s+|\*\*(?:QA )?GATE|(?:QA )?GATE\b)/;
279
+ /** An explicit disclaimer on the same line is dispositive — the author says it is not one. */
280
+ const DISCLAIMED = /\bnot a (?:gate|verdict)\b/i;
281
+
282
+ /**
283
+ * ⛔⛔ A WITHDRAWAL IS NOT THE VERDICT IT WITHDRAWS, AND THE NAIVE READING IS THE DANGEROUS ONE.
284
+ * A consumer fleet's gate seat posted these on one of its PRs; both bound as a clean PASS before this line
285
+ * existed:
286
+ * `## ⛔ VOIDING my PASS-HOLD @ <sha> — that head is two moves dead`
287
+ * `## ⛔ VOIDING my PASS @ <sha>`
288
+ * A line that revokes a verdict names that verdict — so matching the disposition word inside it
289
+ * turns a RETRACTION into a CLEARANCE. That is a FALSE NEGATIVE in a watchdog, which is
290
+ * indistinguishable from a clean run: `unverdicted-merge` would stay silent on a PR merged after
291
+ * its verdict was voided, which is precisely the merge it exists to catch.
292
+ *
293
+ * ⛔⛔ AND POSITION IS THE DISCRIMINATOR, NOT PRESENCE — the first version tested this pattern
294
+ * ANYWHERE on the line and failed in the OPPOSITE direction. A verdict that REPLACES an earlier
295
+ * one routinely says so in its own header, which is the practice that keeps a superseded verdict
296
+ * VISIBLE instead of silently edited:
297
+ * `## QA GATE — **FAIL** @ <sha> — superseding my CONTENT PASS above` ← verbatim, a consumer fleet's PR
298
+ * Discarding that one makes `unverdicted-merge` FIRE ON A PROPERLY GATED MERGE — and it
299
+ * concentrates where gating was MOST active, because a PR whose verdict got corrected is one that
300
+ * received MORE scrutiny. The instrument would go loudest exactly where it is least needed.
301
+ *
302
+ * ⇒ IN A WITHDRAWAL THE VERB PRECEDES THE DISPOSITION; IN A SUPERSEDING VERDICT THE DISPOSITION
303
+ * COMES FIRST. qa-2's shape, and it is measured rather than reasoned: across ALL 124 verdict-shaped
304
+ * lines in both repos (101 of them declaration-shaped), exactly TWO carry a revocation verb, and
305
+ * they split correctly by position — 1 withdrawal, 1 superseding verdict, 99 plain verdicts.
306
+ *
307
+ * ⚠ THE RESIDUAL, STATED WITH ITS DENOMINATOR RATHER THAN LEFT IMPLIED: `PASS @ <sha> —
308
+ * SUPERSEDED, see below` puts the verb after the disposition and would be ACCEPTED. qa-2 raised it
309
+ * as its own invention and flagged it as not field-observed; the corpus agrees — ZERO of 124. It is
310
+ * also partly self-correcting, since whatever supersedes it binds the same sha later.
311
+ */
312
+ const REVOCATION_VERB = /\b(?:VOID(?:ING|ED)?|WITHDRAW(?:N|ING)?|RETRACT(?:ED|ING)?|SUPERSED(?:ED|ING|ES))\b/i;
313
+ /** True only when the revocation verb comes BEFORE the disposition it names. */
314
+ function isWithdrawal(line: string, dispositionIndex: number): boolean {
315
+ const verb = REVOCATION_VERB.exec(line);
316
+ return verb !== null && verb.index < dispositionIndex;
317
+ }
318
+
319
+ /**
320
+ * ⛔ `PASS-HOLD` IS A DOCUMENTED DO-NOT-MERGE VERDICT (`coord-qa/SKILL.md:71,74`) AND `\bPASS\b`
321
+ * MATCHED INSIDE IT, because `-` is a non-word character. So the one verdict type whose entire
322
+ * meaning is "do not merge" classified as a clearance and BOUND.
323
+ * ⚠ It had not yet fired, and that was LUCK: qa-2's own `#974` PASS-HOLD missed only because the
324
+ * head moved and the sha stopped agreeing. Nothing in the classifier prevented it.
325
+ */
326
+ const HOLD = /\bPASS-HOLD\b/i;
327
+
328
+ /**
329
+ * ⭐ FOUND BY THE CORPUS, NOT BY A GATE — the fourth non-clearance form, and the first one this
330
+ * suite caught itself rather than a reviewer catching it:
331
+ * `## QA — **READINESS REFUSED** @ <sha> · the CONTENT PASS above still stands` (a consumer fleet's PR)
332
+ * ⚠ IT ALREADY BEHAVED CORRECTLY, AND ONLY BY ACCIDENT: `READINESS REFUSED` is not a disposition,
333
+ * and the `CONTENT PASS` it mentions sits AFTER the `@ <sha>`, so no claim matched. Reorder that
334
+ * sentence — `the CONTENT PASS above still stands @ <sha>` — and the refusal would have BOUND as a
335
+ * content half. Naming it makes the behaviour a decision instead of a word-order coincidence.
336
+ */
337
+ const REFUSED = /\bREADINESS\s+REFUSED\b/i;
338
+ /** A disposition ahead of an `@ sha` on the same line. The sha's binding is the caller's test. */
339
+ const CLAIM_LINE = /\b(READINESS\s+RELEASED|CONTENT\s+PASS|FULL\s+PASS|PASS-HOLD|PASS|FAIL)\b[^\n]*?@\s*`?([0-9a-f]{7,40})`?/i;
340
+ export function verdictClaimsIn(comments: { body: string }[]): VerdictClaim[] {
341
+ const out: VerdictClaim[] = [];
342
+ for (const c of comments) {
343
+ for (const line of String(c.body ?? "").split("\n")) {
344
+ // ⛔ A HOLD IS A NON-CLEARANCE THAT NAMES ITS OWN DISPOSITION, so it is rejected outright.
345
+ if (!DECLARES.test(line) || DISCLAIMED.test(line) || HOLD.test(line) || REFUSED.test(line)) continue;
346
+ const m = CLAIM_LINE.exec(line);
347
+ if (!m) continue;
348
+ // ⛔ A WITHDRAWAL IS REJECTED BY POSITION, NOT BY PRESENCE — see isWithdrawal. Testing the
349
+ // verb anywhere on the line discards the superseding verdicts that carry it AFTER the
350
+ // disposition, which fires the watchdog on properly gated merges.
351
+ if (isWithdrawal(line, m.index)) continue;
352
+ const word = m[1]!.toUpperCase().replace(/\s+/g, " ");
353
+ const sha = m[2]!;
354
+ if (word === "READINESS RELEASED") {
355
+ out.push({ result: "PASS", sha, half: "readiness" });
356
+ } else if (word === "CONTENT PASS") {
357
+ out.push({ result: "PASS", sha, half: "content" });
358
+ } else if (word === "FAIL") {
359
+ out.push({ result: "FAIL", sha, half: "whole" });
360
+ } else {
361
+ // bare PASS / FULL PASS — a whole verdict unless the same line withholds readiness
362
+ out.push({ result: "PASS", sha, half: NEGATED_READINESS.test(line) ? "content" : "whole" });
363
+ }
364
+ }
365
+ }
366
+ return out;
367
+ }
368
+
369
+ /**
370
+ * Is there a verdict BOUND TO `head` — a whole one, or both halves? This is the
371
+ * question `unverdicted-merge` actually asks, and it is the seam #964 exposed:
372
+ * that PR published CONTENT PASS and READINESS RELEASED as two separate comments,
373
+ * so a rule scoped to a whole verdict misses a merge that was fully gated.
374
+ *
375
+ * ⭐ A SUPERSEDED VERDICT CANNOT SATISFY THIS, BY CONSTRUCTION RATHER THAN BY A
376
+ * SPECIAL CASE. #965's FAIL cited `ab58f9455` — an object that does not exist —
377
+ * and was corrected in a later comment. Requiring agreement with the head that
378
+ * MERGED means a sha naming any other tree (wrong, stale, or nonexistent) simply
379
+ * does not bind. No "is this superseded" heuristic is needed and none is used.
380
+ */
381
+ export function verdictBoundTo(comments: { body: string }[], head: string): { bound: boolean; why: string } {
382
+ const claims = verdictClaimsIn(comments).filter((v) => shaAgrees(v.sha, head));
383
+ const whole = claims.find((v) => v.half === "whole");
384
+ if (whole) return { bound: true, why: `${whole.result} bound to ${head.slice(0, 7)}` };
385
+ const content = claims.some((v) => v.half === "content");
386
+ const readiness = claims.some((v) => v.half === "readiness");
387
+ if (content && readiness) return { bound: true, why: `CONTENT PASS + READINESS RELEASED bound to ${head.slice(0, 7)} (split halves)` };
388
+ if (content) return { bound: false, why: `only a CONTENT half at ${head.slice(0, 7)} — readiness never released, which is the half that clears a merge` };
389
+ if (readiness) return { bound: false, why: `only a READINESS half at ${head.slice(0, 7)} — no content verdict` };
390
+ return { bound: false, why: `no disposition bound to ${head.slice(0, 7)}` };
391
+ }
392
+
217
393
  export function verdictShasIn(comments: { body: string }[]): { result: string; sha: string }[] {
218
394
  const out: { result: string; sha: string }[] = [];
219
395
  for (const c of comments) {
package/src/tools/away.ts CHANGED
@@ -92,6 +92,18 @@ export const COVERAGE_THRESHOLD = "EVERY in-flight lane the clock scores must be
92
92
  * carries the reading's age and labels it stale past this many minutes.
93
93
  */
94
94
  export const COVERAGE_STALE_MINUTES = 60;
95
+ /**
96
+ * ⟨q-1c95f7d4⟩ 5.4 — the tick's own sentence for the announcement and the refusal. A fleet
97
+ * with NO tick source says so (that is this fleet today: six tmux seats, zero herdr), and a
98
+ * fleet with a source that answered for nobody says BLIND rather than saying nothing.
99
+ */
100
+ export const tickSentence = (t?: AwayTick): string => {
101
+ if (!t || t.seats === 0) return " No external tick: no seat has an observer to ask, so every lane above was measured from its branch and the board alone.";
102
+ if (t.readable === 0) return ` External tick BLIND: ${t.seats} seat(s) have an observer and none answered — a source that cannot be read is not a quiet fleet.`;
103
+ const states = Object.entries(t.states).map(([k, v]) => `${v} ${k}`).join(", ");
104
+ return ` External tick: ${t.readable}/${t.seats} seat(s) answered${states ? ` (${states})` : ""} — evidence about the seats, beside the branch axes, never instead of them.`;
105
+ };
106
+
95
107
  export const fmtAge = (ms: number): string => (ms < 60_000 ? `${Math.max(0, Math.round(ms / 1000))}s` : ms < 3_600_000 ? `${Math.floor(ms / 60_000)}m` : ms < 86_400_000 ? `${Math.floor(ms / 3_600_000)}h` : `${Math.floor(ms / 86_400_000)}d`);
96
108
  export function coverageAgeOf(coverage: { at: string }, now = Date.now()): { ageMs: number | null; stale: boolean; label: string } {
97
109
  const at = Date.parse(coverage.at);
@@ -103,10 +115,28 @@ export function coverageAgeOf(coverage: { at: string }, now = Date.now()): { age
103
115
  /** Met when nothing scored is unseen — an empty board (0 of 0) is idle, not blind, and needs no special case. */
104
116
  export const meetsCoverageThreshold = (c: { checked: number; measurable: number }): boolean => c.measurable >= c.checked;
105
117
 
118
+ /**
119
+ * ⟨q-1c95f7d4⟩ Task 5.4 — WHAT THE EXTERNAL TICK CONTRIBUTES TO THE AWAY PRECONDITION.
120
+ *
121
+ * The precondition asks what the clock could actually SEE. A seat observed from OUTSIDE
122
+ * this process — herdr answering `idle | working | blocked` for its pane — is seen in a way
123
+ * a pid and a branch cannot manage: it is the only signal here that separates a thinking
124
+ * lane from a wedged one. So a readable tick counts as measured, through `stall_check`'s
125
+ * own coverage arithmetic, and nothing is special-cased here.
126
+ *
127
+ * ⛔ WHAT IS NOT DONE, DELIBERATELY: a tick NEVER raises coverage for a seat the branch
128
+ * axes could not measure into "observed enough to go away on". It credits the seat it read
129
+ * and nothing else, and a fleet with a tick source that answered for NOBODY is reported as
130
+ * blind — `0 of N readable` is the blind case this verb exists to refuse, not a quiet one.
131
+ */
132
+ export type AwayTick = { seats: number; readable: number; states: Record<string, number>; note: string };
133
+
106
134
  export type AwayCoverage = {
107
135
  checked: number;
108
136
  measurable: number;
109
137
  blind: string[];
138
+ /** ⟨q-1c95f7d4⟩ the external tick behind this coverage number, named so the announcement can say what watched the fleet. */
139
+ tick?: AwayTick;
110
140
  /** ⟨q-5d1c8e04⟩ — standing seats on the board: present, not scored, and therefore not blind. */
111
141
  roles?: number;
112
142
  /** Lanes that declare no per-agent branch: out of the population by their own statement. */
@@ -220,6 +250,7 @@ async function measureCoverage(repo?: string): Promise<AwayCoverage | null> {
220
250
  try {
221
251
  const r = (await stallCheckTool({ repo })) as unknown as {
222
252
  ok?: boolean; checked?: number; measurable?: number; blind?: string[]; roles?: unknown[]; deliberate?: unknown[];
253
+ tick?: { seats?: unknown[]; readable?: number; states?: Record<string, number>; note?: string };
223
254
  boardParse?: { readable: boolean; rowsPresent: number; rowsParsed: number; why: string };
224
255
  };
225
256
  // ⟨q-3d82f1a9⟩ — AN UNREADABLE BOARD IS A NAMED STATE, not "could not measure".
@@ -237,6 +268,9 @@ async function measureCoverage(repo?: string): Promise<AwayCoverage | null> {
237
268
  checked: r.checked ?? 0,
238
269
  measurable: r.measurable ?? 0,
239
270
  blind: r.blind ?? [],
271
+ tick: r.tick
272
+ ? { seats: r.tick.seats?.length ?? 0, readable: r.tick.readable ?? 0, states: (r.tick.states ?? {}) as Record<string, number>, note: r.tick.note ?? "" }
273
+ : undefined,
240
274
  roles: r.roles?.length ?? 0,
241
275
  deliberate: r.deliberate?.length ?? 0,
242
276
  at: new Date().toISOString(),
@@ -352,7 +386,8 @@ export async function coordAwayTool(args: {
352
386
  `"The clock ran" and "the fleet is observed" are different facts, and this verb must not treat the first as the second. ` +
353
387
  `The usual cause is a board 'Branch · Worktree' cell holding a PATH rather than a branch ref: a path resolves for ` +
354
388
  `git and measures the wrong thing, so the check reports it unmeasurable rather than guessing. Fix those cells and ` +
355
- `coverage returns. If you mean to go anyway, pass acknowledgeBlindFleet:true — it is recorded in the state and in the announcement.`,
389
+ `coverage returns. If you mean to go anyway, pass acknowledgeBlindFleet:true — it is recorded in the state and in the announcement.` +
390
+ tickSentence(coverage.tick),
356
391
  };
357
392
  }
358
393
 
@@ -382,7 +417,8 @@ export async function coordAwayTool(args: {
382
417
  `Decides: planning · priority · curation · roadmap · canon · releases under standing authorisation. ` +
383
418
  `Never: merges, gates, or takes a code lane. Parks for David: ${PARKED_CATEGORIES.join(" · ")}. ` +
384
419
  `Decisions logged to ${args.decisionLog}. ` +
385
- `Stall coverage ${coverage.measurable}/${coverage.checked}${!meetsCoverageThreshold(coverage) ? ` — ACKNOWLEDGED BLIND${coverage.measurable === 0 ? ": the clock runs and sees nothing" : ""}${coverage.blind.length ? ` on: ${coverage.blind.join(", ")}` : ""}` : ""}.`,
420
+ `Stall coverage ${coverage.measurable}/${coverage.checked}${!meetsCoverageThreshold(coverage) ? ` — ACKNOWLEDGED BLIND${coverage.measurable === 0 ? ": the clock runs and sees nothing" : ""}${coverage.blind.length ? ` on: ${coverage.blind.join(", ")}` : ""}` : ""}.` +
421
+ tickSentence(coverage.tick),
386
422
  };
387
423
  }
388
424
 
@@ -395,7 +395,10 @@ export async function queueWriteTool(args: {
395
395
  `item by hand once, or move the queue section last.`,
396
396
  );
397
397
  }
398
- const w0 = writeDoc(repo, QUEUE_DOC, reparsed, original);
398
+ // `own` names the row this call authored. It is already recorded above, so this
399
+ // changes nothing today — it is stated so the guarantee survives a refactor that
400
+ // stops pre-stamping, rather than depending on line 361 staying where it is.
401
+ const w0 = writeDoc(repo, QUEUE_DOC, reparsed, original, [], [target.id]);
399
402
  return {
400
403
  ok: true,
401
404
  op: args.op,
@@ -120,6 +120,48 @@ const norm = (s: string) => String(s).toLowerCase().replace(/[`*_]/g, "").replac
120
120
  * against them by the caller (`eventIsDerived`), so a diff that has since been
121
121
  * reverted cannot produce an event that outlives the record.
122
122
  */
123
+ /**
124
+ * ⟨q-…⟩ PHASE COMPLETENESS IS THE ABSENCE OF AN UNTICKED BOX, AND IT MUST NOT DEPEND ON WHAT
125
+ * A BOX IS CALLED.
126
+ *
127
+ * ⛔ MEASURED, NOT REASONED — a false "phase 5.4 complete" event was emitted and DELIVERED on
128
+ * 2026-09-16 at `579db9d`. The detector's box pattern required a label shaped `\d+\.\d+`, so
129
+ * against the real document it saw **31 of 36** checkboxes, every one of them ticked, and
130
+ * fired. The three it could not see were `3B.2`, `3B.3` and `3B.4` — labels with a LETTER
131
+ * inside the number — and `3B.3` is deliberately unticked and not recoverable.
132
+ *
133
+ * ⚠ SO THE CAUSE IS THE LABEL PATTERN, NOT THE POSITION OF THE LAST BOX. The event cited
134
+ * `- [x] 6.4`, the document's final line, which made it look like a last-line rule; the ref is
135
+ * only what it cites. Any doc whose remaining work carries an unanticipated label reported
136
+ * itself finished, wherever that work sat.
137
+ *
138
+ * ⭐ THE FIX IS A DIFFERENT QUESTION, not a wider regex for the same one: "is any checkbox in
139
+ * this document unticked" is asked over EVERY markdown checkbox, whatever follows it. A label
140
+ * shape nobody has invented yet cannot defeat it. The failure direction is deliberate — an
141
+ * unticked box anywhere blocks the event, so an unrecognised box costs a MISSED completion,
142
+ * never a FALSE one. This code's own comment already said which way to lean: *"A false
143
+ * completion is worse than a missing one: it closes a phase nobody finished."*
144
+ */
145
+ export const ANY_BOX = /^[ \t]*[-*+][ \t]*\[([ xX])\][ \t]*(.*)$/;
146
+ export const TICKED_BOX = /^[ \t]*[-*+][ \t]*\[[xX]\]/;
147
+
148
+ export type PhaseBox = { ticked: boolean; label: string | null; line: string };
149
+
150
+ /** Every markdown checkbox in a phase document, in file order, label-agnostic. */
151
+ export function checkboxesIn(text: string): PhaseBox[] {
152
+ const out: PhaseBox[] = [];
153
+ for (const raw of String(text).split("\n")) {
154
+ const m = ANY_BOX.exec(raw);
155
+ if (!m) continue;
156
+ const rest = m[2] ?? "";
157
+ out.push({ ticked: m[1]!.toLowerCase() === "x", label: (/^\*{0,2}([A-Za-z0-9][A-Za-z0-9.]*)\b/.exec(rest) ?? [])[1] ?? null, line: raw.trim() });
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /** The unticked boxes, for a caller that wants to say WHY it did not fire. */
163
+ export const untickedIn = (text: string): PhaseBox[] => checkboxesIn(text).filter((b) => !b.ticked);
164
+
123
165
  export function eventsFromCommittedChange(
124
166
  diff: string,
125
167
  after: { done?: string; phases?: Record<string, string> } = {},
@@ -257,14 +299,14 @@ export function eventsFromCommittedChange(
257
299
  // finished.
258
300
  for (const [rel, text] of Object.entries(after.phases ?? {})) {
259
301
  if (!isPhaseDoc(rel)) continue;
260
- const boxes = [...String(text).matchAll(/^\s*-\s*\[( |x)\]\s*\*{0,2}(\d+\.\d+[a-z]?)\b/gim)];
261
- if (!boxes.length || boxes.some((m) => m[1] !== "x")) continue;
302
+ const boxes = checkboxesIn(String(text));
303
+ if (!boxes.length || boxes.some((b) => !b.ticked)) continue;
262
304
  // This commit must have ticked a box IN THIS FILE.
263
- const closedHere = (byFile.get(rel)?.added ?? []).some((l) => /^\s*-\s*\[x\]\s*\*{0,2}\d+\.\d+/.test(l));
305
+ const closedHere = (byFile.get(rel)?.added ?? []).some((l) => TICKED_BOX.test(l));
264
306
  if (!closedHere) continue;
265
307
  const phase = /phase(\d+(?:\.\d+)?)/i.exec(rel)?.[1];
266
308
  if (!phase) continue;
267
- events.push({ kind: "phase", target: phase, ref: boxes[boxes.length - 1]![0].trim(), summary: `phase ${phase} — every task box ticked` });
309
+ events.push({ kind: "phase", target: phase, ref: boxes[boxes.length - 1]!.line, summary: `phase ${phase} — every task box ticked` });
268
310
  }
269
311
 
270
312
  lastUnattributedItems = unattributedItems;
@@ -149,8 +149,12 @@ export function writeDoc(
149
149
  doc: WorkDoc,
150
150
  original: string,
151
151
  alreadyWritten: string[] = [],
152
+ own: readonly string[] = [],
152
153
  ): { written: boolean; stamped: string[] } {
153
- const { text, stamped } = renderWorkDocForWrite(doc);
154
+ // `own` = the rows THIS CALL authored. Everything else is stamped only if the
155
+ // document already speaks the recorded-id grammar — see `stampQueueIds`, which
156
+ // holds the rule so both writers cannot drift apart on it.
157
+ const { text, stamped } = renderWorkDocForWrite(doc, { own });
154
158
  if (text === original) return { written: false, stamped: [] };
155
159
  const p = path.join(repo, rel);
156
160
  const current = existsSync(p) ? readFileSync(p, "utf8") : "";
@@ -1265,7 +1269,7 @@ export async function landTool(
1265
1269
  try {
1266
1270
  if (args.write) {
1267
1271
  if (queueChanged) {
1268
- const w = writeDoc(repo, QUEUE_DOC, q.doc, q.text, wrote);
1272
+ const w = writeDoc(repo, QUEUE_DOC, q.doc, q.text, wrote, args.queueItemId ? [args.queueItemId] : []);
1269
1273
  if (w.written) wrote.push(QUEUE_DOC);
1270
1274
  // Rows stamped that this call did not ask to touch. `queueItemId` is the
1271
1275
  // one item the caller supplied, so anything else here is a row that