agent-coord-mcp 0.26.21 → 0.26.22
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.
- package/dist/capabilities.js +250 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/closing-line.js +83 -0
- package/dist/closing-line.js.map +1 -0
- package/dist/commit-cite.js +55 -0
- package/dist/commit-cite.js.map +1 -0
- package/dist/gated-head.js +67 -20
- package/dist/gated-head.js.map +1 -1
- package/dist/server-spread.js +195 -0
- package/dist/server-spread.js.map +1 -0
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/store.js +32 -0
- package/dist/store.js.map +1 -1
- package/dist/tools/away.js +67 -7
- package/dist/tools/away.js.map +1 -1
- package/dist/tools/board-ref.js +44 -4
- package/dist/tools/board-ref.js.map +1 -1
- package/dist/tools/event-kinds.js +5 -1
- package/dist/tools/event-kinds.js.map +1 -1
- package/dist/tools/events.js +31 -2
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/messaging.js +64 -6
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/record-events.js +85 -5
- package/dist/tools/record-events.js.map +1 -1
- package/dist/tools/records.js +231 -38
- package/dist/tools/records.js.map +1 -1
- package/dist/tools/registry.js +52 -2
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/seat-build.js +173 -0
- package/dist/tools/seat-build.js.map +1 -0
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/stall.js +1095 -18
- package/dist/tools/stall.js.map +1 -1
- package/dist/tools/transport.js +21 -2
- package/dist/tools/transport.js.map +1 -1
- package/dist/tools/worktrees.js +14 -0
- package/dist/tools/worktrees.js.map +1 -1
- package/package.json +1 -1
- package/scripts/coord-attention-clock.mjs +2 -0
- package/scripts/coord-stall-clock.mjs +52 -11
- package/src/capabilities.ts +264 -2
- package/src/closing-line.ts +85 -0
- package/src/commit-cite.ts +58 -0
- package/src/gated-head.ts +128 -26
- package/src/server-spread.ts +233 -0
- package/src/server.ts +2 -2
- package/src/store.ts +32 -0
- package/src/tools/away.ts +82 -9
- package/src/tools/board-ref.ts +70 -3
- package/src/tools/event-kinds.ts +17 -1
- package/src/tools/events.ts +33 -2
- package/src/tools/messaging.ts +63 -6
- package/src/tools/record-events.ts +78 -5
- package/src/tools/records.ts +248 -38
- package/src/tools/registry.ts +54 -3
- package/src/tools/seat-build.ts +194 -0
- package/src/tools/shared.ts +22 -0
- package/src/tools/stall.ts +1266 -23
- package/src/tools/transport.ts +21 -2
- package/src/tools/worktrees.ts +13 -0
package/dist/tools/records.js
CHANGED
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
* that drifts.
|
|
13
13
|
*/
|
|
14
14
|
import { execFileSync } from "node:child_process";
|
|
15
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { treeProvenance } from "./tree-provenance.js";
|
|
19
|
-
import { parseWorkDoc, renderWorkDocForWrite, queueItemsOf, doneEntriesOf, phaseCitationsDetailed, newlyTickedInDiff, sweepTagOf, awaitingOf, refsIn, refsMatch, workstreamsV1RowsOf, } from "@davidbalzan/groundwork-seam";
|
|
19
|
+
import { parseWorkDoc, renderWorkDocForWrite, queueItemsOf, doneEntriesOf, phaseCitationsDetailed, newlyTickedInDiff, sweepTagOf, awaitingOf, refsIn, closingCitation, refsMatch, workstreamsV1RowsOf, workStateOf, declarationOf, } from "@davidbalzan/groundwork-seam";
|
|
20
20
|
import { ensureWorktreeTool } from "./worktrees.js";
|
|
21
21
|
import { ROOT } from "../store.js";
|
|
22
|
-
import { verdictsFor,
|
|
22
|
+
import { verdictsFor, gatedBy, prVerdictsIn } from "../gated-head.js";
|
|
23
23
|
import { boardRefFor, classifyBoardRef } from "./board-ref.js";
|
|
24
|
-
import { haltState
|
|
24
|
+
import { haltState } from "./stall.js";
|
|
25
25
|
import { readSubs, evaluate, commitEvaluation, eventIsDerived } from "./events.js";
|
|
26
26
|
const QUEUE_DOC = "docs/QUEUE.md";
|
|
27
27
|
const DONE_DOC = "docs/DONE.md";
|
|
@@ -252,18 +252,60 @@ export function noDownstream(items) {
|
|
|
252
252
|
function rowSubjectIds(row) {
|
|
253
253
|
return [...String(row.stream).matchAll(/\b(q-[0-9a-f]{8})\b/g)].map((m) => m[1]);
|
|
254
254
|
}
|
|
255
|
+
/*
|
|
256
|
+
* ⭐ THE NUANCE THE SEAM DOES NOT SETTLE, SETTLED HERE (⟨q-a42503cb⟩): `⏸
|
|
257
|
+
* MERGE-HELD` and `⏸ Parked — awaits David` are BOTH `parked` to
|
|
258
|
+
* `workStateOf` and both `open` to `coarseOf`, yet one has BUILT WORK gated
|
|
259
|
+
* green in an open PR and the other has nothing behind it. Reporting them under
|
|
260
|
+
* one word re-creates the defect with a wider net: a reader told "parked" for
|
|
261
|
+
* `#292`'s row would go and build it.
|
|
262
|
+
*
|
|
263
|
+
* Two INDEPENDENT facts, deliberately not one: the WORD comes from the
|
|
264
|
+
* declaration (`held` · `queued` · `parked`), and `builtWork` comes from the
|
|
265
|
+
* Branch · Worktree cell. A row can say MERGE-HELD with an empty branch cell,
|
|
266
|
+
* and that disagreement is worth seeing rather than resolving.
|
|
267
|
+
*/
|
|
268
|
+
const STATUS_DECORATION = /^[\s*⭐]+/u;
|
|
269
|
+
function holdReasonOf(state, status) {
|
|
270
|
+
if (state !== "parked")
|
|
271
|
+
return state;
|
|
272
|
+
const decl = declarationOf(String(status).replace(STATUS_DECORATION, ""));
|
|
273
|
+
if (/\bheld\b/i.test(decl))
|
|
274
|
+
return "held";
|
|
275
|
+
if (/\bqueued\b/i.test(decl))
|
|
276
|
+
return "queued";
|
|
277
|
+
return "parked";
|
|
278
|
+
}
|
|
255
279
|
/**
|
|
256
|
-
*
|
|
280
|
+
* What does the board say about this item, if it is the SUBJECT of any row?
|
|
281
|
+
*
|
|
282
|
+
* ⛔⛆ ANY ROW WHOSE SUBJECT IS THE ITEM EXCLUDES IT FROM ROUTING, AND SAYS WHY
|
|
283
|
+
* (⟨q-a42503cb⟩). This was a boolean gated on `isInFlightStatus` — 🚧 and 🔍
|
|
284
|
+
* only — so a `⏸ MERGE-HELD` row with its work gated green in `#292` left
|
|
285
|
+
* its item at the TOP of the pool: a seat taking it would have rebuilt the PR.
|
|
286
|
+
* Measured on the live board 2026-09-14: seven `⏸` rows, every one the
|
|
287
|
+
* coordinator's most careful bookkeeping, every one invisible to this join.
|
|
257
288
|
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
289
|
+
* ⚠ THIS SUPERSEDES HALF OF ⟨q-2ba7f0c5⟩'S RULING, AND KEEPS ITS REASON. That
|
|
290
|
+
* fix made a `⏸ Parked` row NOT silence its item, because the alternative
|
|
291
|
+
* then was a SILENT drop with no axis. The drop is no longer silent: the item
|
|
292
|
+
* lands on `delivered` with the row's state as its `why`, and an `AWAITS`
|
|
293
|
+
* item still reaches `awaitingDecision`, which is gathered over every open
|
|
294
|
+
* item before this join. What ⟨q-2ba7f0c5⟩ actually defended — a warning row
|
|
295
|
+
* must not make its item VANISH — still holds; what changes is that a warning
|
|
296
|
+
* row now also stops the item being HANDED OUT, which is what a warning is for.
|
|
297
|
+
*
|
|
298
|
+
* Conditions:
|
|
263
299
|
* SUBJECT the row's Stream cell names the item — by recorded id, or by the
|
|
264
300
|
* text prefix `claim` has always written there. A mention in a Last
|
|
265
301
|
* note is one lane REFERRING to another item — the aide's live row
|
|
266
302
|
* does exactly this — and a status-only rule would still hide it.
|
|
303
|
+
* STATE whatever the seam reads: in-progress, in-review, parked (split
|
|
304
|
+
* into held · queued · parked, see `holdReasonOf`), blocked,
|
|
305
|
+
* orphaned, done, … An UNRECOGNISED glyph is `unknown` and STILL
|
|
306
|
+
* excludes: the row is on the board with this item as its subject,
|
|
307
|
+
* and offering it anyway is the silent path this exists to close.
|
|
308
|
+
* A `✅ Done` row excludes too — that item is offerable to nobody.
|
|
267
309
|
*
|
|
268
310
|
* THE PREFIX ARM IS FOR THE ROWS ALREADY ON THE BOARD, and without it this fix
|
|
269
311
|
* would have shipped a transitional hole in its own negative control: every row
|
|
@@ -274,16 +316,24 @@ function rowSubjectIds(row) {
|
|
|
274
316
|
* here carry the id (see `claim`), so the prefix arm is the compatibility half
|
|
275
317
|
* rather than the mechanism, and it is deliberately still SUBJECT-only.
|
|
276
318
|
*/
|
|
277
|
-
function
|
|
319
|
+
function boardHoldOf(rows, id, text) {
|
|
278
320
|
const key = keyOf({ text }).replace(/\s+/g, " ").trim();
|
|
279
|
-
|
|
280
|
-
if (!isInFlightStatus(r.status))
|
|
281
|
-
return false;
|
|
282
|
-
if (rowSubjectIds(r).includes(id))
|
|
283
|
-
return true;
|
|
321
|
+
for (const r of rows) {
|
|
284
322
|
const stream = String(r.stream).replace(/\s+/g, " ").trim();
|
|
285
|
-
|
|
286
|
-
|
|
323
|
+
const subject = rowSubjectIds(r).includes(id) || (key.length >= 20 && stream.startsWith(key.slice(0, Math.min(key.length, 60))));
|
|
324
|
+
if (!subject)
|
|
325
|
+
continue;
|
|
326
|
+
const state = workStateOf(r.status);
|
|
327
|
+
const branch = String(r.branchWorktree).replace(/[`*]/g, "").trim();
|
|
328
|
+
return {
|
|
329
|
+
state,
|
|
330
|
+
why: holdReasonOf(state, r.status),
|
|
331
|
+
status: declarationOf(String(r.status).replace(STATUS_DECORATION, "")),
|
|
332
|
+
builtWork: branch.length > 0 && branch !== "—" && branch !== "-",
|
|
333
|
+
branch,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
287
337
|
}
|
|
288
338
|
/**
|
|
289
339
|
* Is this DONE entry the RECORD OF THIS ITEM CLOSING, rather than an entry that
|
|
@@ -311,7 +361,7 @@ function boardHoldsItem(rows, id, text) {
|
|
|
311
361
|
* false positives, because open rows cite refs as EVIDENCE.
|
|
312
362
|
*
|
|
313
363
|
* ✅ WHAT SURVIVES IS THE COMPOSED-SUMMARY ARM, and it is a DIFFERENT EVIDENCE
|
|
314
|
-
* CLASS — the same distinction that keeps `
|
|
364
|
+
* CLASS — the same distinction that keeps `boardHoldOf`. It does not infer
|
|
315
365
|
* delivery from prose: it requires the entry to carry the item's own
|
|
316
366
|
* deterministically composed text, `summarize(item.text)`, which a VERB writes.
|
|
317
367
|
* A citation cannot accidentally satisfy it.
|
|
@@ -407,20 +457,34 @@ export async function nextUnblockedTool(args) {
|
|
|
407
457
|
if (i.done)
|
|
408
458
|
continue; // already off `open` by the checkbox; not an exclusion this axis owns
|
|
409
459
|
// Board first, and the order is load-bearing for the REPORT rather than the
|
|
410
|
-
// routing: both causes exclude, but a
|
|
411
|
-
// (wait
|
|
412
|
-
|
|
413
|
-
|
|
460
|
+
// routing: both causes exclude, but a board row is a different remedy
|
|
461
|
+
// (wait for the merge · ask its owner · ask the human it awaits, by its
|
|
462
|
+
// `why`) from a landed delivery (close the row).
|
|
463
|
+
const hold = boardHoldOf(boardRows, i.id, String(i.text));
|
|
464
|
+
if (hold)
|
|
465
|
+
deliveredBy.set(i.id, { reason: "board", hold });
|
|
414
466
|
else if (doneRecordsDelivery(doneEntries, i.id, String(i.text)))
|
|
415
|
-
deliveredBy.set(i.id, "done");
|
|
467
|
+
deliveredBy.set(i.id, { reason: "done" });
|
|
416
468
|
}
|
|
417
469
|
const open = items.filter((i) => !i.done && !deliveredBy.has(i.id));
|
|
418
470
|
// THE AXIS THE SUBTRACTION USED TO SKIP. Every row absent from `open` for this
|
|
419
471
|
// reason is named here, INCLUDING correctly-delivered ones: a correct exclusion
|
|
420
472
|
// reported silently is the same defect as an incorrect one.
|
|
473
|
+
//
|
|
474
|
+
// ⛔ THE REASON NAMES THE STATE, NOT JUST THE DOCUMENT (⟨q-a42503cb⟩). "on
|
|
475
|
+
// the board" covered a live 🚧 lane and a `⏸ MERGE-HELD` PR with one word,
|
|
476
|
+
// and the two want opposite things from the reader — leave the first alone,
|
|
477
|
+
// MERGE the second. `why` is the seam's state (parked split into held ·
|
|
478
|
+
// queued · parked) and `builtWork` says whether the row names a branch at
|
|
479
|
+
// all, so "held with nothing behind it" is visible rather than resolved.
|
|
421
480
|
const delivered = items
|
|
422
481
|
.filter((i) => !i.done && deliveredBy.has(i.id))
|
|
423
|
-
.map((i) =>
|
|
482
|
+
.map((i) => {
|
|
483
|
+
const d = deliveredBy.get(i.id);
|
|
484
|
+
return d.reason === "board"
|
|
485
|
+
? { item: keyOf(i), id: i.id, reason: "board", why: d.hold.why, state: d.hold.state, status: d.hold.status, builtWork: d.hold.builtWork, branch: d.hold.branch }
|
|
486
|
+
: { item: keyOf(i), id: i.id, reason: "done", why: "recorded in DONE.md" };
|
|
487
|
+
});
|
|
424
488
|
// ⛔⛆ A DUPLICATED ID IS A SILENT DOUBLE-EXCLUSION, AND IT IS THIS ROW'S OWN
|
|
425
489
|
// DEFECT ONE LEVEL DOWN. Queue ids are STABLE, derived from the row's text, so
|
|
426
490
|
// two rows with identical text carry the SAME id — verified: `- [ ] (P1) an
|
|
@@ -555,7 +619,7 @@ export async function nextUnblockedTool(args) {
|
|
|
555
619
|
...awaitingDecision.map((a) => `⏸ skipped — awaiting decision from ${a.who} (${a.id})`),
|
|
556
620
|
...duplicateIds.map((d) => `⚠ ${d.rows} open rows share the id ${d.id} — one record excludes all of them`),
|
|
557
621
|
...delivered.map((d) => d.reason === "board"
|
|
558
|
-
? `⏭ not offered —
|
|
622
|
+
? `⏭ not offered — on the board as ${d.why}${d.builtWork ? ` with built work on ${d.branch}` : " with no branch on the row"} (${d.id})`
|
|
559
623
|
: `⏭ not offered — delivery recorded in DONE.md (${d.id})`),
|
|
560
624
|
],
|
|
561
625
|
// A SEPARATE AXIS, deliberately. See noDownstream().
|
|
@@ -622,6 +686,13 @@ export const claimSchema = {
|
|
|
622
686
|
base: z.string().optional(),
|
|
623
687
|
task: z.string().optional(),
|
|
624
688
|
write: z.boolean().optional(),
|
|
689
|
+
/**
|
|
690
|
+
* ⟨q-5d1c8e04⟩ — A SLICE WITH NO CODE DELIVERABLE, said as a first-class value.
|
|
691
|
+
* What the slice delivers instead (a ruling, a canon edit, a measurement).
|
|
692
|
+
* No worktree is cut; the board cell carries the statement in words, which
|
|
693
|
+
* the grammar and `stall_check` both read as DELIBERATE rather than missing.
|
|
694
|
+
*/
|
|
695
|
+
noCodeDeliverable: z.string().min(1).optional(),
|
|
625
696
|
};
|
|
626
697
|
export async function claimTool(args) {
|
|
627
698
|
const halt = haltState();
|
|
@@ -660,6 +731,25 @@ export async function claimTool(args) {
|
|
|
660
731
|
// A worktree that cannot be ensured is a REFUSAL, not a warning. Binding an
|
|
661
732
|
// item to an agent with nowhere isolated to work is the shared-checkout failure
|
|
662
733
|
// this pair exists to prevent.
|
|
734
|
+
// ⟨q-5d1c8e04⟩ — NO CODE DELIVERABLE: no tree, and the cell SAYS so. Five
|
|
735
|
+
// seats wrote this by hand in five spellings because the verb had no way to
|
|
736
|
+
// say it; `stall_check` reads the prose cell as deliberately branchless and
|
|
737
|
+
// keeps the row out of its population rather than calling it unmeasurable.
|
|
738
|
+
if (args.noCodeDeliverable) {
|
|
739
|
+
const cell = `no code deliverable · ${args.noCodeDeliverable.replace(/[|\n]/g, " ").trim()}`;
|
|
740
|
+
const boardHunk = `| ⟨${item.id}⟩ ${keyOf(item)} | ${args.agentId} | ${cell} | 🚧 In Progress | — | claimed |`;
|
|
741
|
+
let board = { action: "reported" };
|
|
742
|
+
const b = readDoc(repo, BOARD_DOC);
|
|
743
|
+
if (!b)
|
|
744
|
+
board = { action: `no ${BOARD_DOC} under '${repo}' — row NOT written` };
|
|
745
|
+
else if (args.write) {
|
|
746
|
+
const next = upsertBoardRow(b.text, args.agentId, boardHunk);
|
|
747
|
+
if (next.action !== "unchanged")
|
|
748
|
+
writeFileSync(path.join(repo, BOARD_DOC), next.text);
|
|
749
|
+
board = { action: next.action, path: BOARD_DOC };
|
|
750
|
+
}
|
|
751
|
+
return { ok: true, project: args.project, agentId: args.agentId, item: { id: item.id, priority: item.priority, text: item.text }, boardHunk, board, worktreeEnsured: false, noCodeDeliverable: args.noCodeDeliverable };
|
|
752
|
+
}
|
|
663
753
|
const wt = await ensureWorktreeTool({
|
|
664
754
|
agentId: args.agentId,
|
|
665
755
|
repo,
|
|
@@ -792,20 +882,39 @@ export const landSchema = {
|
|
|
792
882
|
};
|
|
793
883
|
const ghMergeFacts = (repo, n) => {
|
|
794
884
|
try {
|
|
795
|
-
const out = execFileSync("gh", ["pr", "view", n, "--json", "headRefOid,mergedAt"], {
|
|
885
|
+
const out = execFileSync("gh", ["pr", "view", n, "--json", "headRefOid,mergedAt,comments"], {
|
|
796
886
|
cwd: repo,
|
|
797
887
|
encoding: "utf8",
|
|
798
888
|
stdio: ["ignore", "pipe", "ignore"],
|
|
889
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
799
890
|
});
|
|
800
891
|
const j = JSON.parse(out);
|
|
801
892
|
if (!j.headRefOid || !j.mergedAt)
|
|
802
893
|
return null;
|
|
803
|
-
return {
|
|
894
|
+
return {
|
|
895
|
+
headRefOid: String(j.headRefOid),
|
|
896
|
+
mergedAt: String(j.mergedAt),
|
|
897
|
+
comments: (j.comments ?? []).map((c) => ({ body: String(c.body ?? ""), createdAt: c.createdAt, author: c.author?.login })),
|
|
898
|
+
};
|
|
804
899
|
}
|
|
805
900
|
catch {
|
|
806
901
|
return null;
|
|
807
902
|
}
|
|
808
903
|
};
|
|
904
|
+
/** ⟨q-6b3af019⟩ — the row ids the landing commit of #n cites, read off its SUBJECT on the base (the `(#n)` squash marker). */
|
|
905
|
+
export function landingCitations(repo, n) {
|
|
906
|
+
let subjects = "";
|
|
907
|
+
for (const base of ["origin/main", "main"]) {
|
|
908
|
+
try {
|
|
909
|
+
subjects = git(repo, ["log", base, "--format=%s", "--fixed-strings", `--grep=(#${n})`, "-n", "20"]);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
catch { /* next */ }
|
|
913
|
+
}
|
|
914
|
+
const marker = `(#${n})`;
|
|
915
|
+
const landing = subjects.split("\n").find((s) => s.trimEnd().endsWith(marker));
|
|
916
|
+
return landing ? [...new Set(landing.match(/q-[0-9a-f]{8}/g) ?? [])] : [];
|
|
917
|
+
}
|
|
809
918
|
export async function landTool(args, readMergeFacts = ghMergeFacts, readVerdictLog = roomLog) {
|
|
810
919
|
const repo = args.repo ?? process.cwd();
|
|
811
920
|
const base = args.base ?? "main";
|
|
@@ -861,6 +970,17 @@ export async function landTool(args, readMergeFacts = ghMergeFacts, readVerdictL
|
|
|
861
970
|
// `queueItemId` it closes nothing and reports the candidates for the caller to
|
|
862
971
|
// pick, saying so.
|
|
863
972
|
const candidates = items.filter((i) => !i.done && new RegExp(`#${n}\\b`).test(String(i.text)));
|
|
973
|
+
// ⟨q-6b3af019⟩ — SIBLINGS: open rows that NAME an id this PR cites. q-b4e7c209
|
|
974
|
+
// shipped under #250/#251 (both citing q-0b8e5c47) and q-0c5e73a1 under #261
|
|
975
|
+
// (cited as "slice A" of q-6ce4a8d0): the delivered row named the cited one.
|
|
976
|
+
// The ids are read off the LANDING COMMIT'S SUBJECT on the base — the record
|
|
977
|
+
// every check reads, local, no network — and listed beside the #N candidates
|
|
978
|
+
// at the one moment both facts are in hand. Closing nothing: a sibling is a
|
|
979
|
+
// question for the coordinator, not a claim.
|
|
980
|
+
const citedIds = landingCitations(repo, n);
|
|
981
|
+
const siblings = items
|
|
982
|
+
.filter((i) => !i.done && !citedIds.includes(i.id) && i.id !== args.queueItemId && citedIds.some((c) => String(i.text).includes(c)))
|
|
983
|
+
.map((i) => ({ id: i.id, priority: i.priority, names: citedIds.filter((c) => String(i.text).includes(c)) }));
|
|
864
984
|
const target = args.queueItemId ? items.find((i) => i.id === args.queueItemId) : null;
|
|
865
985
|
if (args.queueItemId && !target)
|
|
866
986
|
return { ok: false, error: `no queue item with id '${args.queueItemId}'` };
|
|
@@ -899,7 +1019,35 @@ export async function landTool(args, readMergeFacts = ghMergeFacts, readVerdictL
|
|
|
899
1019
|
queueChanged = true;
|
|
900
1020
|
const prRef = refsIn(args.pr)[0] ?? { raw: args.pr, repo: null, number: n };
|
|
901
1021
|
const contextRepo = originRepoOf(repo);
|
|
902
|
-
|
|
1022
|
+
/*
|
|
1023
|
+
* ⛔⛆ "ALREADY CITED" IS A QUESTION ABOUT THE CLOSING POSITION, NOT THE ROW (⟨q-217cc151⟩).
|
|
1024
|
+
*
|
|
1025
|
+
* This asked whether the PR number appears ANYWHERE in the row, so a row that
|
|
1026
|
+
* DISCUSSES its own closer read as already cited and `land` wrote no token.
|
|
1027
|
+
* The natural experiment that isolates it — three rows, one variable:
|
|
1028
|
+
*
|
|
1029
|
+
* ⟨q-8a7b04f2⟩ mentions #269 ×4, its closer #271 ZERO times -> TOKEN WRITTEN
|
|
1030
|
+
* ⟨q-5f27b1ae⟩ mentions its own closer #269 ×1 -> NO TOKEN
|
|
1031
|
+
* ⟨q-8db146d0⟩ mentions its own closer #272 ×2 -> NO TOKEN
|
|
1032
|
+
*
|
|
1033
|
+
* ⭐ THE VARIABLE IS SELF-REFERENCE, NOT REF DENSITY. A row naming four of
|
|
1034
|
+
* somebody else's PRs is cited normally; a row naming its own once is not. So the
|
|
1035
|
+
* better a row documents what closed it, the more certainly the closure goes
|
|
1036
|
+
* unwritten — and the ARTEFACT is what loses, silently.
|
|
1037
|
+
*
|
|
1038
|
+
* ⚠ RETIRED ONCE AS ⟨q-2b91c188⟩, AND THAT RETIREMENT WAS CORRECT FOR THE QUESTION
|
|
1039
|
+
* ASKED: the behaviour is harmless to every CONSUMER, because both readers accept
|
|
1040
|
+
* a line-1 prose mention and `doctor` falls back to the row. That was measured and
|
|
1041
|
+
* a patch was reverted rather than shipped. What nobody asked was what it does to
|
|
1042
|
+
* the ARTEFACT — a closure whose citation sits in no fixed place. Position is what
|
|
1043
|
+
* makes a ref a citation, and that rule applies to the verb that WRITES one.
|
|
1044
|
+
*
|
|
1045
|
+
* ⛔ THE READER IS NOT TOUCHED HERE. Fixing the writer forward does nothing for
|
|
1046
|
+
* rows already written, and narrowing the reader is what ⟨q-bf162723⟩ established
|
|
1047
|
+
* must never happen.
|
|
1048
|
+
*/
|
|
1049
|
+
const closingHere = closingCitation(String(target.text))?.refs ?? [];
|
|
1050
|
+
const alreadyCited = closingHere.some((r) => refsMatch(r, prRef, { contextRepo }));
|
|
903
1051
|
if (!alreadyCited) {
|
|
904
1052
|
citationAppended = ` · **closed by ${args.pr}${args.result ? ` — ${args.result.trim()}` : ""}**`;
|
|
905
1053
|
target.text = `${target.text}${citationAppended}`;
|
|
@@ -1066,10 +1214,32 @@ export async function landTool(args, readMergeFacts = ghMergeFacts, readVerdictL
|
|
|
1066
1214
|
const at = Date.parse(mf.mergedAt);
|
|
1067
1215
|
if (!Number.isFinite(at))
|
|
1068
1216
|
return { checked: false, note: `#${n} reports an unparseable mergedAt ('${mf.mergedAt}') — cannot place the merge in time.` };
|
|
1069
|
-
const { verdicts, unparsed } = verdictsFor(log, n);
|
|
1070
|
-
|
|
1217
|
+
const { verdicts: bus, unparsed } = verdictsFor(log, n);
|
|
1218
|
+
// ⟨q-5a93c2d7⟩ — both channels, one predicate: the bus's typed records and
|
|
1219
|
+
// the PR page's typed lines. Gatedness is (head sha, typed verdict); the
|
|
1220
|
+
// merge time only DISCLOSES lateness, in words, beside the answer.
|
|
1221
|
+
const verdicts = [...bus.map((v) => ({ ...v, channel: "bus" })), ...prVerdictsIn(mf.comments ?? [])];
|
|
1222
|
+
const a = gatedBy(verdicts, mf.headRefOid, at);
|
|
1071
1223
|
if (a.gated) {
|
|
1072
|
-
|
|
1224
|
+
const late = (a.lateByMs ?? 0) > 0;
|
|
1225
|
+
return {
|
|
1226
|
+
checked: true,
|
|
1227
|
+
gated: true,
|
|
1228
|
+
head: mf.headRefOid.slice(0, 8),
|
|
1229
|
+
// ⟨q-dcbaf544⟩ — `from` here is the GATER (`gatedBy ?? from`), never the
|
|
1230
|
+
// sender alone; `attribution` says whether that is a seat or the account.
|
|
1231
|
+
by: {
|
|
1232
|
+
from: a.gater,
|
|
1233
|
+
sha: a.by.head.slice(0, 8),
|
|
1234
|
+
channel: a.by.channel ?? "bus",
|
|
1235
|
+
attribution: a.attribution,
|
|
1236
|
+
...(a.by.scribe ?? a.seatRecord?.scribe ? { scribe: a.seatRecord?.scribe ?? a.by.scribe } : {}),
|
|
1237
|
+
...(a.seatRecord ? { firstSeenOnPr: new Date(a.by.ts).toISOString(), seatRecordedAt: new Date(a.seatRecord.ts).toISOString() } : {}),
|
|
1238
|
+
},
|
|
1239
|
+
recordedAfterMerge: late,
|
|
1240
|
+
verified: a.verified,
|
|
1241
|
+
...(late ? { note: `GATED, LATE RECORD: #${n} — ${a.verified}.` } : {}),
|
|
1242
|
+
};
|
|
1073
1243
|
}
|
|
1074
1244
|
return {
|
|
1075
1245
|
checked: true,
|
|
@@ -1078,7 +1248,7 @@ export async function landTool(args, readMergeFacts = ghMergeFacts, readVerdictL
|
|
|
1078
1248
|
reason: a.reason,
|
|
1079
1249
|
gatedInstead: (a.crossed ?? []).map((c) => c.gatedSha.slice(0, 8)),
|
|
1080
1250
|
note: `UNGATED MERGE RECORDED: #${n} merged ${mf.headRefOid.slice(0, 8)} and ${a.reason}. ` +
|
|
1081
|
-
`The record is written — this is a report, not a refusal — but
|
|
1251
|
+
`The record is written — this is a report, not a refusal — but no typed verdict bound to the merged head exists in any channel.` +
|
|
1082
1252
|
(unparsed ? ` (${unparsed} log line(s) unreadable and skipped.)` : ""),
|
|
1083
1253
|
};
|
|
1084
1254
|
})();
|
|
@@ -1117,6 +1287,11 @@ export async function landTool(args, readMergeFacts = ghMergeFacts, readVerdictL
|
|
|
1117
1287
|
candidates: target
|
|
1118
1288
|
? undefined
|
|
1119
1289
|
: candidates.map((i) => ({ id: i.id, priority: i.priority, key: keyOf(i) })),
|
|
1290
|
+
// ⟨q-6b3af019⟩ — open rows naming an id this PR's landing commit cites; none closed.
|
|
1291
|
+
siblings,
|
|
1292
|
+
...(siblings.length
|
|
1293
|
+
? { note_siblings: `${siblings.length} open row(s) NAME an id #${n} cites (${citedIds.join(", ")}) and are not cited themselves — work shipped under another row's citation is how q-b4e7c209 and q-0c5e73a1 sat open; judge each, close none from here.` }
|
|
1294
|
+
: {}),
|
|
1120
1295
|
...(target || !candidates.length
|
|
1121
1296
|
? {}
|
|
1122
1297
|
: {
|
|
@@ -1186,7 +1361,7 @@ const ghFacts = (repo, n) => {
|
|
|
1186
1361
|
// history: the PR title (which becomes the squash subject) and the commit
|
|
1187
1362
|
// subjects. The body is included because a reviewer reads it, but a claim
|
|
1188
1363
|
// that lives only in a comment thread is not a record.
|
|
1189
|
-
const meta = JSON.parse(gh(["pr", "view", n, "--json", "title,body,commits"]) || "{}");
|
|
1364
|
+
const meta = JSON.parse(gh(["pr", "view", n, "--json", "title,body,commits,comments"]) || "{}");
|
|
1190
1365
|
const subjects = (meta.commits ?? [])
|
|
1191
1366
|
.map((c) => c.messageHeadline ?? "")
|
|
1192
1367
|
.join("\n");
|
|
@@ -1194,6 +1369,7 @@ const ghFacts = (repo, n) => {
|
|
|
1194
1369
|
state: String(j.state ?? ""),
|
|
1195
1370
|
mergeable: String(j.mergeable ?? ""),
|
|
1196
1371
|
headRefOid: String(j.headRefOid ?? ""),
|
|
1372
|
+
comments: (meta.comments ?? []).map((c) => ({ body: String(c.body ?? ""), createdAt: c.createdAt, author: c.author?.login })),
|
|
1197
1373
|
checks: j.statusCheckRollup ?? [],
|
|
1198
1374
|
diff: gh(["pr", "diff", n]),
|
|
1199
1375
|
citationText: [String(meta.title ?? ""), subjects, String(meta.body ?? "")].join("\n"),
|
|
@@ -1206,9 +1382,25 @@ const ghMerge = (repo, n, method) => {
|
|
|
1206
1382
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1207
1383
|
});
|
|
1208
1384
|
};
|
|
1385
|
+
/**
|
|
1386
|
+
* ⟨q-5a93c2d7⟩ — EVERY room, not the project's alone: "any channel" means a
|
|
1387
|
+
* verdict recorded in another room still counts for this head. The project
|
|
1388
|
+
* room is read first so a missing bus is still a null (unknown), never an
|
|
1389
|
+
* empty string (nothing).
|
|
1390
|
+
*/
|
|
1209
1391
|
const roomLog = (project) => {
|
|
1210
1392
|
try {
|
|
1211
|
-
|
|
1393
|
+
const own = readFileSync(path.join(ROOT, "rooms", `${project}.jsonl`), "utf8");
|
|
1394
|
+
let others = "";
|
|
1395
|
+
try {
|
|
1396
|
+
const dir = path.join(ROOT, "rooms");
|
|
1397
|
+
others = readdirSync(dir)
|
|
1398
|
+
.filter((f) => f.endsWith(".jsonl") && f !== `${project}.jsonl`)
|
|
1399
|
+
.map((f) => readFileSync(path.join(dir, f), "utf8"))
|
|
1400
|
+
.join("\n");
|
|
1401
|
+
}
|
|
1402
|
+
catch { /* other rooms are optional */ }
|
|
1403
|
+
return others ? `${own}\n${others}` : own;
|
|
1212
1404
|
}
|
|
1213
1405
|
catch {
|
|
1214
1406
|
return null;
|
|
@@ -1341,8 +1533,9 @@ export async function mergeTool(args, facts = ghFacts, doMerge = ghMerge, readVe
|
|
|
1341
1533
|
verdict,
|
|
1342
1534
|
};
|
|
1343
1535
|
}
|
|
1344
|
-
const { verdicts, unparsed } = verdictsFor(log, n);
|
|
1345
|
-
|
|
1536
|
+
const { verdicts: busVerdicts, unparsed } = verdictsFor(log, n);
|
|
1537
|
+
// ⟨q-5a93c2d7⟩ — the pre-merge question, same predicate, both channels.
|
|
1538
|
+
const gate = gatedBy([...busVerdicts.map((v) => ({ ...v, channel: "bus" })), ...prVerdictsIn(f.comments ?? [])], head, null);
|
|
1346
1539
|
if (!gate.gated) {
|
|
1347
1540
|
const named = (gate.crossed ?? []).map((c) => c.gatedSha.slice(0, 7)).join(", ");
|
|
1348
1541
|
return {
|
|
@@ -1356,7 +1549,7 @@ export async function mergeTool(args, facts = ghFacts, doMerge = ghMerge, readVe
|
|
|
1356
1549
|
};
|
|
1357
1550
|
}
|
|
1358
1551
|
if (!args.write)
|
|
1359
|
-
return { ok: true, merged: false, verdict, note: `#${n} would merge: all ${checks.length} check(s) pass, and head ${head.slice(0, 7)} is gated by a PASS from ${gate.
|
|
1552
|
+
return { ok: true, merged: false, verdict, note: `#${n} would merge: all ${checks.length} check(s) pass, and head ${head.slice(0, 7)} is gated by a PASS from ${gate.gater}${gate.attribution === "account" ? " (the PR's shared account — no bus verdict names a seat)" : ""}. Pass write:true to apply.` };
|
|
1360
1553
|
doMerge(repo, n, args.method ?? "squash");
|
|
1361
1554
|
return { ok: true, merged: true, verdict };
|
|
1362
1555
|
}
|