agent-coord-mcp 0.19.1 → 0.23.0

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/work.js CHANGED
@@ -1,292 +1,2 @@
1
- // Work state as data (Phase 8 Task 5): queue items, done entries and board
2
- // rows as typed records, with markdown as a first-class EXPORT rather than the
3
- // datastore.
4
- //
5
- // WHY THIS SHAPE. `docs/QUEUE.md` / `docs/DONE.md` were parsed under a contract
6
- // requiring exact `—` (U+2014) and ` · ` (U+00B7). A parser once returned ZERO
7
- // items on the real file while passing every synthetic test, and the consuming
8
- // UI rendered an empty panel — no error, no signal. The files themselves are
9
- // good: David edits them, git diffs them, they grep. So markdown stays the
10
- // INTERFACE and stops being the DATASTORE.
11
- //
12
- // MARKDOWN REMAINS AUTHORITATIVE. Delete the store and the documents still
13
- // stand alone: every field here is recoverable from the file it came from, the
14
- // exporter is byte-exact, and nothing downstream is required to read records.
15
- // The store is a derived index until a consumer chooses otherwise.
16
- //
17
- // The round-trip guarantee is structural, not incidental: everything this
18
- // module does NOT model — prose, the write-rule paragraphs, the Cross-project
19
- // section, fenced examples — is captured verbatim as a `text` block and
20
- // replayed in place. Only lines it genuinely understands are re-rendered, so
21
- // an unmodelled construct can never be silently dropped.
22
- import { createHash } from "node:crypto";
23
- // The one arity BoardRow can hold. boardCells() and the parser's arity guard
24
- // both reference this constant so the two sides cannot drift; a test pins
25
- // boardCells().length === BOARD_ARITY.
26
- export const BOARD_ARITY = 5;
27
- export function isMalformedRow(r) {
28
- return r.malformed === true;
29
- }
30
- // ---------- the glyph contract ----------
31
- //
32
- // Pinned in docs/DONE.md and consumed by external parsers: the ref splits on
33
- // the LAST ` — ` (space, U+2014, space) and the date is a trailing ` · `
34
- // (space, U+00B7, space) + ISO date. Written as explicit escapes so a source
35
- // file normalization or a copy-paste through an ASCII-mangling tool can never
36
- // change them silently — the constants are the contract.
37
- export const EM_DASH_SEP = " — ";
38
- export const MIDDOT_SEP = " · ";
39
- const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
40
- // Deterministic ids: same content in → same id out, so import→export→import is
41
- // stable and no id has to be written into the markdown (which would change the
42
- // bytes the round trip is measured on).
43
- function idFor(kind, seed) {
44
- return `${kind}-${createHash("sha1").update(seed).digest("hex").slice(0, 8)}`;
45
- }
46
- // ---------- line-level recognizers ----------
47
- const QUEUE_LINE = /^- \[( |x)\] \((P1|P2|P3)\) (.*)$/;
48
- const DONE_LINE = /^- \[( |x)\] (.*)$/;
49
- export function parseQueueLine(line, section) {
50
- const m = QUEUE_LINE.exec(line);
51
- if (!m)
52
- return null;
53
- const [, mark, priority, text] = m;
54
- return {
55
- id: idFor("q", text ?? ""),
56
- priority: priority,
57
- text: text ?? "",
58
- done: mark === "x",
59
- ...(section ? { section } : {}),
60
- };
61
- }
62
- // Split on the LAST ` — `, then peel a trailing ` · YYYY-MM-DD`. Both parts are
63
- // optional: an entry with neither is still a done entry, it just carries no
64
- // verifiable ref — which is a fact about the entry, not a parse failure. The
65
- // old parser treated the same line as nothing at all.
66
- export function parseDoneLine(line, section) {
67
- const m = DONE_LINE.exec(line);
68
- if (!m)
69
- return null;
70
- let body = m[2] ?? "";
71
- let ref;
72
- let date;
73
- const cut = body.lastIndexOf(EM_DASH_SEP);
74
- if (cut !== -1) {
75
- const tail = body.slice(cut + EM_DASH_SEP.length);
76
- const dot = tail.lastIndexOf(MIDDOT_SEP);
77
- if (dot !== -1) {
78
- const maybeDate = tail.slice(dot + MIDDOT_SEP.length);
79
- if (ISO_DATE.test(maybeDate)) {
80
- date = maybeDate;
81
- ref = tail.slice(0, dot);
82
- }
83
- }
84
- // A tail with no date is still a ref, as long as it isn't prose with
85
- // spaces — refs are `owner/repo#N` or `owner/repo@sha`.
86
- if (date === undefined && /^\S+$/.test(tail))
87
- ref = tail;
88
- if (ref !== undefined)
89
- body = body.slice(0, cut);
90
- }
91
- return {
92
- id: idFor("d", `${body}|${ref ?? ""}|${date ?? ""}`),
93
- text: body,
94
- ...(ref !== undefined ? { ref } : {}),
95
- ...(date !== undefined ? { date } : {}),
96
- ...(section ? { section } : {}),
97
- };
98
- }
99
- export function renderQueueLine(item) {
100
- return `- [${item.done ? "x" : " "}] (${item.priority}) ${item.text}`;
101
- }
102
- export function renderDoneLine(entry) {
103
- let line = `- [x] ${entry.text}`;
104
- if (entry.ref !== undefined)
105
- line += `${EM_DASH_SEP}${entry.ref}`;
106
- if (entry.date !== undefined)
107
- line += `${MIDDOT_SEP}${entry.date}`;
108
- return line;
109
- }
110
- // ---------- board table ----------
111
- const TABLE_ROW = /^\|(.*)\|\s*$/;
112
- const TABLE_ALIGN = /^\|[\s:|-]+\|\s*$/;
113
- function splitRow(line) {
114
- const m = TABLE_ROW.exec(line);
115
- if (!m)
116
- return [];
117
- return (m[1] ?? "").split("|").map((c) => c.trim());
118
- }
119
- function renderRow(cells) {
120
- return `| ${cells.join(" | ")} |`;
121
- }
122
- function boardCells(r) {
123
- return [r.lane, r.owner, r.state, r.currentSlice, r.nextGo];
124
- }
125
- // Replay the original line when the fields still say what it said; re-render
126
- // once anything actually changed. A malformed row has no fields to have
127
- // changed — it replays its bytes, always.
128
- export function renderBoardRow(r) {
129
- if (isMalformedRow(r))
130
- return r.verbatim;
131
- if (r.raw !== undefined) {
132
- const cells = splitRow(r.raw);
133
- if (cells.length === BOARD_ARITY && cells.every((c, i) => c === boardCells(r)[i]))
134
- return r.raw;
135
- }
136
- return renderRow(boardCells(r));
137
- }
138
- // Which record kind a `## Heading` opens. `## Queue` → queue items,
139
- // `## Done` → done entries; anything else closes the record region, so the
140
- // Cross-project section's bullets stay verbatim prose.
141
- function sectionKindOf(heading) {
142
- const h = heading.replace(/^#+\s*/, "").trim().toLowerCase();
143
- if (h === "queue")
144
- return "queue";
145
- if (h === "done")
146
- return "done";
147
- return "none";
148
- }
149
- export function parseWorkDoc(source) {
150
- const trailingNewline = source.endsWith("\n");
151
- const lines = source.split("\n");
152
- if (trailingNewline)
153
- lines.pop(); // the split's empty tail, restored on render
154
- const blocks = [];
155
- let text = [];
156
- let kind = "none";
157
- let subsection;
158
- let inFence = false;
159
- const flushText = () => {
160
- if (text.length)
161
- blocks.push({ kind: "text", lines: text });
162
- text = [];
163
- };
164
- // Consecutive item lines coalesce into one block; anything in between (a
165
- // blank line, a subsection heading) flushes as text first and therefore
166
- // starts a new one, which is what keeps rendering order faithful.
167
- const pushItem = (item) => {
168
- flushText();
169
- const last = blocks[blocks.length - 1];
170
- if (last?.kind === "queue")
171
- last.items.push(item);
172
- else
173
- blocks.push({ kind: "queue", items: [item] });
174
- };
175
- const pushEntry = (entry) => {
176
- flushText();
177
- const last = blocks[blocks.length - 1];
178
- if (last?.kind === "done")
179
- last.entries.push(entry);
180
- else
181
- blocks.push({ kind: "done", entries: [entry] });
182
- };
183
- for (let i = 0; i < lines.length; i++) {
184
- const line = lines[i] ?? "";
185
- // Fenced blocks are never interpreted — docs/DONE.md pins the done-line
186
- // FORMAT inside a fence, and parsing that example as an entry would
187
- // invent a record out of documentation.
188
- if (/^\s*```/.test(line)) {
189
- inFence = !inFence;
190
- text.push(line);
191
- continue;
192
- }
193
- if (inFence) {
194
- text.push(line);
195
- continue;
196
- }
197
- if (/^#{1,6}\s/.test(line)) {
198
- const level = (line.match(/^#+/) ?? [""])[0].length;
199
- if (level <= 2) {
200
- kind = sectionKindOf(line);
201
- subsection = undefined;
202
- }
203
- else if (kind !== "none") {
204
- subsection = line.replace(/^#+\s*/, "").trim();
205
- }
206
- text.push(line);
207
- continue;
208
- }
209
- if (kind === "queue") {
210
- const item = parseQueueLine(line, subsection);
211
- if (item) {
212
- pushItem(item);
213
- continue;
214
- }
215
- }
216
- else if (kind === "done") {
217
- const entry = parseDoneLine(line, subsection);
218
- if (entry) {
219
- pushEntry(entry);
220
- continue;
221
- }
222
- }
223
- // Lanes table: a header row followed by an alignment row.
224
- if (TABLE_ROW.test(line) && TABLE_ALIGN.test(lines[i + 1] ?? "")) {
225
- const rows = [];
226
- let j = i + 2;
227
- for (; j < lines.length && TABLE_ROW.test(lines[j] ?? ""); j++) {
228
- const raw = lines[j] ?? "";
229
- const cells = splitRow(raw);
230
- // Arity guard: a row BoardRow cannot hold is refused a record, kept
231
- // verbatim, and named loudly — destructuring it into the 5-tuple is
232
- // exactly the silent narrow/widen this exists to prevent. An empty
233
- // trailing cell (`| a | b | c | d | |`) is still five columns and
234
- // still a legitimate row.
235
- if (cells.length !== BOARD_ARITY) {
236
- rows.push({
237
- malformed: true,
238
- verbatim: raw,
239
- issue: `board row at line ${j + 1} has ${cells.length} column(s), expected ${BOARD_ARITY} — ` +
240
- `refused (kept verbatim, excluded from board records). Fix the row, or widen BoardRow ` +
241
- `deliberately (BOARD_ARITY + boardCells + this guard together). Row: ${raw}`,
242
- });
243
- continue;
244
- }
245
- const [lane = "", owner = "", state = "", currentSlice = "", nextGo = ""] = cells;
246
- rows.push({ id: idFor("b", lane), lane, owner, state, currentSlice, nextGo, raw });
247
- }
248
- flushText();
249
- blocks.push({ kind: "board", header: line, align: lines[i + 1] ?? "", rows });
250
- i = j - 1;
251
- continue;
252
- }
253
- text.push(line);
254
- }
255
- flushText();
256
- return { trailingNewline, blocks };
257
- }
258
- export function renderWorkDoc(doc) {
259
- const out = [];
260
- for (const block of doc.blocks) {
261
- if (block.kind === "text")
262
- out.push(...block.lines);
263
- else if (block.kind === "queue")
264
- out.push(...block.items.map(renderQueueLine));
265
- else if (block.kind === "done")
266
- out.push(...block.entries.map(renderDoneLine));
267
- else {
268
- out.push(block.header);
269
- out.push(block.align);
270
- for (const r of block.rows)
271
- out.push(renderBoardRow(r));
272
- }
273
- }
274
- return out.join("\n") + (doc.trailingNewline ? "\n" : "");
275
- }
276
- // ---------- record extraction ----------
277
- export function queueItemsOf(doc) {
278
- return doc.blocks.flatMap((b) => (b.kind === "queue" ? b.items : []));
279
- }
280
- export function doneEntriesOf(doc) {
281
- return doc.blocks.flatMap((b) => (b.kind === "done" ? b.entries : []));
282
- }
283
- export function boardRowsOf(doc) {
284
- return doc.blocks.flatMap((b) => b.kind === "board" ? b.rows.filter((r) => !isMalformedRow(r)) : []);
285
- }
286
- // Every refused row's issue, in document order — what makes the parse-time
287
- // rejection LOUD at the tool layer (import_work/list_work/export_work all
288
- // surface it) instead of a filtered-out record nobody notices.
289
- export function workDocIssues(doc) {
290
- return doc.blocks.flatMap((b) => b.kind === "board" ? b.rows.filter(isMalformedRow).map((r) => r.issue) : []);
291
- }
1
+ export { BOARD_ARITY, isMalformedRow, EM_DASH_SEP, MIDDOT_SEP, parseQueueLine, parseDoneLine, renderQueueLine, renderDoneLine, renderBoardRow, parseWorkDoc, renderWorkDoc, queueItemsOf, doneEntriesOf, boardRowsOf, workstreamsV1RowsOf, listWorkBoardOf, projectV1ToLanes, workDocIssues, workDocLegacyWriteIssues, LANES_V0_WRITE_ISSUE, } from "@davidbalzan/groundwork-seam/docs";
292
2
  //# sourceMappingURL=work.js.map
package/dist/work.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"work.js","sourceRoot":"","sources":["../src/work.ts"],"names":[],"mappings":"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"}
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,YAAY,EACZ,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACxB,oBAAoB,GAGrB,MAAM,mCAAmC,CAAC"}
File without changes
package/hooks/roles.mjs CHANGED
@@ -25,6 +25,10 @@ export const GATE_RUNNER_ROLE_IDS = new Set(["qa", "quality", "coordinator", "ga
25
25
  // Coordinators countersign work orders (`go`) and in-flight contract
26
26
  // amendments (`scope`).
27
27
  export const COORDINATOR_ROLE_IDS = new Set(["coordinator"]);
28
+ // CI receives express-lane DONE: wake. Not a gate-runner — must not grant verdict.
29
+ export const CI_ROLE_IDS = new Set(["ci"]);
30
+ // Console / watchers. May send prefixed messages. Not a gate-runner.
31
+ export const AUTOMATION_ROLE_IDS = new Set(["automation"]);
28
32
 
29
33
  // Normalize any role shape into {roleId, displayName, explicit}.
30
34
  //
@@ -77,3 +81,11 @@ export function isGateRunner(role) {
77
81
  export function isCoordinator(role) {
78
82
  return roleMatches(role, COORDINATOR_ROLE_IDS);
79
83
  }
84
+
85
+ export function isCi(role) {
86
+ return roleMatches(role, CI_ROLE_IDS);
87
+ }
88
+
89
+ export function isAutomation(role) {
90
+ return roleMatches(role, AUTOMATION_ROLE_IDS);
91
+ }
package/hooks/tier.mjs CHANGED
@@ -35,7 +35,7 @@ function tierFromRecord(rec, m, opts) {
35
35
  case "scope":
36
36
  return opts.trustedSenders?.has?.(m.from) ? "urgent" : "routine";
37
37
  case "done":
38
- return opts.gateRunner ? "urgent" : "routine";
38
+ return wakesOnDone(opts) ? "urgent" : "routine";
39
39
  // risk / fyi / action / verdict: real semantics, but not a reason to
40
40
  // interrupt someone else's turn. A risk that genuinely needs an immediate
41
41
  // turn is a blocker — that judgement stays with the sender.
@@ -100,12 +100,17 @@ export function classifyTier(m, opts = {}) {
100
100
  if (/^SCOPE(?: CHANGE)?:/.test(text)) {
101
101
  return opts.trustedSenders?.has?.(m.from) ? "urgent" : "routine";
102
102
  }
103
- // DONE: is for whoever runs the merge gate (QA/coordinator) — nobody else,
104
- // DM or room alike.
105
- if (text.startsWith("DONE:")) return opts.gateRunner ? "urgent" : "routine";
103
+ // DONE: wakes the merge gate (QA/coordinator) OR ci (express-lane).
104
+ // Classification lives on the GO (`gate: express`), not on the DONE line.
105
+ // Do not invent `DONE: express`. ci is not a gate-runner (no verdict).
106
+ if (text.startsWith("DONE:")) return wakesOnDone(opts) ? "urgent" : "routine";
106
107
  return "routine";
107
108
  }
108
109
 
110
+ function wakesOnDone(opts) {
111
+ return Boolean(opts.gateRunner || opts.ci);
112
+ }
113
+
109
114
  // Tier with the enable switch applied — AGENT_COORD_TIERS=0 restores legacy
110
115
  // push-everything.
111
116
  export function effectiveTier(m, opts = {}) {
@@ -75,6 +75,7 @@ import { fileURLToPath } from "node:url";
75
75
  import path from "node:path";
76
76
  import { spawn, spawnSync } from "node:child_process";
77
77
  import { effectiveTier, isGateRunnerRole, TierQueue, formatBatch } from "./tier.mjs";
78
+ import { isCi } from "./roles.mjs";
78
79
  import { mergeTransportMarker } from "./marker.mjs";
79
80
  import { pasteAndSubmit as sharedPasteAndSubmit, submitControl as sharedSubmitControl } from "./submit.mjs";
80
81
 
@@ -113,9 +114,10 @@ const MAX_QUEUE_MS = parseInt(process.env.AGENT_COORD_MAX_QUEUE_MS || "15000", 1
113
114
  // SCOPE: changes are honored only from these trusted ids. Re-resolved from
114
115
  // the registry every 30s so a role change doesn't strand a stale pusher;
115
116
  // AGENT_COORD_GATE_RUNNER=1|0 overrides in both directions.
116
- let tierCtx = { enabled: TIERS_ENABLED, gateRunner: false, trustedSenders: new Set() };
117
+ let tierCtx = { enabled: TIERS_ENABLED, gateRunner: false, ci: false, trustedSenders: new Set() };
117
118
  function refreshTierCtx() {
118
119
  let gateRunner = false;
120
+ let ci = false;
119
121
  const trusted = new Set();
120
122
  try {
121
123
  const reg = JSON.parse(readFileSync(path.join(ROOT, "agents.json"), "utf8"));
@@ -123,18 +125,21 @@ function refreshTierCtx() {
123
125
  // Pass the whole entry: the frozen roleId lives beside `role`, and
124
126
  // reading only the display string would throw the identity away.
125
127
  if (isGateRunnerRole(entry)) trusted.add(id);
128
+ if (id === AGENT_ID) ci = isCi(entry);
126
129
  }
127
130
  gateRunner = trusted.has(AGENT_ID);
128
131
  } catch {
129
- // No registry yet — conservative default (not a gate runner).
132
+ // No registry yet — conservative default (not a gate runner, not ci).
130
133
  }
131
134
  const env = process.env.AGENT_COORD_GATE_RUNNER;
132
135
  if (env === "1" || env === "true") gateRunner = true;
133
136
  if (env === "0" || env === "false") gateRunner = false;
134
- if (gateRunner !== tierCtx.gateRunner) {
135
- process.stderr.write(`[tmux-pusher] gate-runner resolved: ${gateRunner}\n`);
137
+ // AGENT_COORD_GATE_RUNNER must not flip ci — that would look like a
138
+ // verdict grant. Express-lane wake is roleId === ci only.
139
+ if (gateRunner !== tierCtx.gateRunner || ci !== tierCtx.ci) {
140
+ process.stderr.write(`[tmux-pusher] gate-runner resolved: ${gateRunner} ci: ${ci}\n`);
136
141
  }
137
- tierCtx = { enabled: TIERS_ENABLED, gateRunner, trustedSenders: trusted };
142
+ tierCtx = { enabled: TIERS_ENABLED, gateRunner, ci, trustedSenders: trusted };
138
143
  }
139
144
  refreshTierCtx();
140
145
  setInterval(refreshTierCtx, 30_000).unref();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.19.1",
3
+ "version": "0.23.0",
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,15 +8,6 @@
8
8
  "coord-chat": "scripts/coord-chat.mjs",
9
9
  "coord-pusher": "scripts/coord-pusher.mjs"
10
10
  },
11
- "scripts": {
12
- "build": "tsc",
13
- "prepare": "node scripts/check-self-dependency.mjs && tsc",
14
- "start": "node dist/server.js",
15
- "dev": "tsx src/server.ts",
16
- "pretest": "node scripts/check-self-dependency.mjs && tsc",
17
- "test": "node scripts/check-test-count.mjs",
18
- "test:raw": "node --test \"test/*.test.mjs\""
19
- },
20
11
  "files": [
21
12
  "dist",
22
13
  "src",
@@ -52,14 +43,23 @@
52
43
  "node": ">=18"
53
44
  },
54
45
  "dependencies": {
55
- "@modelcontextprotocol/sdk": "^1.0.4",
46
+ "@modelcontextprotocol/sdk": "^1.30.0",
56
47
  "proper-lockfile": "^4.1.2",
57
- "zod": "^4.4.3"
48
+ "zod": "^4.4.3",
49
+ "@davidbalzan/groundwork-seam": "0.1.0"
58
50
  },
59
51
  "devDependencies": {
60
- "@types/node": "^25.9.2",
52
+ "@types/node": "^26.2.0",
61
53
  "@types/proper-lockfile": "^4.1.4",
62
- "tsx": "^4.19.2",
63
- "typescript": "^6.0.3"
54
+ "tsx": "^4.23.12",
55
+ "typescript": "^7.0.2"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc",
59
+ "start": "node dist/server.js",
60
+ "dev": "tsx src/server.ts",
61
+ "pretest": "node scripts/check-self-dependency.mjs && tsc",
62
+ "test": "node scripts/check-test-count.mjs",
63
+ "test:raw": "node --test \"test/*.test.mjs\""
64
64
  }
65
- }
65
+ }
@@ -17,10 +17,13 @@
17
17
  // was untouched; a reviewer reads the claim, not the diff. This guard is
18
18
  // cheap insurance against that class, not evidence of a recurring bug.
19
19
  //
20
- // Runs on `prepare` (every install), `pretest`, and `prepublishOnly`.
21
- import { readFileSync } from "node:fs";
20
+ // Runs on `prepare` (every install) and `pretest`.
21
+ import { existsSync, readFileSync } from "node:fs";
22
+ import { dirname, join, relative } from "node:path";
23
+ import { fileURLToPath } from "node:url";
22
24
 
23
- const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
25
+ const pkgDir = fileURLToPath(new URL("..", import.meta.url));
26
+ const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8"));
24
27
  const DEP_FIELDS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
25
28
 
26
29
  function selfDepFields(manifest) {
@@ -29,26 +32,71 @@ function selfDepFields(manifest) {
29
32
  );
30
33
  }
31
34
 
32
- const offenders = selfDepFields(pkg);
35
+ function walkFor(name, start) {
36
+ let dir = start;
37
+ for (let i = 0; i < 6; i++) {
38
+ const candidate = join(dir, name);
39
+ if (existsSync(candidate)) return { file: candidate, root: dir };
40
+ const parent = dirname(dir);
41
+ if (parent === dir) break;
42
+ dir = parent;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function npmLockSelfDep() {
48
+ const sibling = join(pkgDir, "package-lock.json");
49
+ if (!existsSync(sibling)) return false;
50
+ try {
51
+ const lock = JSON.parse(readFileSync(sibling, "utf8"));
52
+ const rootPkg = lock.packages?.[""];
53
+ return Boolean(rootPkg && selfDepFields(rootPkg).length);
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
33
58
 
34
- let lockOffender = false;
35
- try {
36
- const lock = JSON.parse(readFileSync(new URL("../package-lock.json", import.meta.url), "utf8"));
37
- const rootPkg = lock.packages?.[""];
38
- if (rootPkg && selfDepFields(rootPkg).length) lockOffender = true;
39
- } catch {
40
- /* no lockfile yet (fresh checkout pre-install) — nothing to check */
59
+ // pnpm-lock.yaml has no JSON root package. Check this package's importer
60
+ // block for a key named after the package (a self-dep).
61
+ function pnpmLockSelfDep() {
62
+ const found = walkFor("pnpm-lock.yaml", pkgDir);
63
+ if (!found) return false;
64
+ let text;
65
+ try {
66
+ text = readFileSync(found.file, "utf8");
67
+ } catch {
68
+ return false;
69
+ }
70
+ const rel = relative(found.root, pkgDir).split("\\").join("/") || ".";
71
+ const key = rel === "." ? "." : rel;
72
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73
+ const header = new RegExp(`(?<=^|\\n) ${escaped}:\\n`);
74
+ const start = text.search(header);
75
+ if (start === -1) return false;
76
+ const from = text.indexOf("\n", start) + 1;
77
+ const next = text.slice(from).search(/\n [^ \n]/);
78
+ const block = next === -1 ? text.slice(from) : text.slice(from, from + next);
79
+ const selfKey = new RegExp(`^[ \\t]+${pkg.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*$`, "m");
80
+ return selfKey.test(block);
41
81
  }
42
82
 
43
- if (offenders.length || lockOffender) {
83
+ const offenders = selfDepFields(pkg);
84
+ const npmLockOffender = npmLockSelfDep();
85
+ const pnpmLockOffender = pnpmLockSelfDep();
86
+
87
+ if (offenders.length || npmLockOffender || pnpmLockOffender) {
88
+ const lockBits = [
89
+ npmLockOffender && "package-lock.json's root package",
90
+ pnpmLockOffender && "pnpm-lock.yaml's importer for this package",
91
+ ].filter(Boolean);
44
92
  console.error(
45
93
  `[check-self-dependency] "${pkg.name}" depends on itself` +
46
94
  (offenders.length ? ` in package.json's ${offenders.join(", ")}` : "") +
47
- (lockOffender ? `${offenders.length ? " and" : " in"} package-lock.json's root package` : "") +
95
+ (lockBits.length ? `${offenders.length ? " and" : " in"} ${lockBits.join(" and ")}` : "") +
48
96
  `.\nThis happened once before, added by an "npm audit fix" whose commit message said it was ` +
49
97
  `lockfile-only (de4e1ee, removed in 3bce3ce — see docs/DONE.md). If you just ran an audit or ` +
50
98
  `dependency update, check what else it changed in package.json. ` +
51
- `Remove the self-reference from both files and re-run "npm install".`,
99
+ `Remove the self-reference from the manifest and lockfile.`,
52
100
  );
53
101
  process.exit(1);
54
102
  }
@@ -22,7 +22,7 @@
22
22
 
23
23
  import { spawn } from "node:child_process";
24
24
 
25
- const EXPECTED_TESTS = 262;
25
+ const EXPECTED_TESTS = 272;
26
26
 
27
27
  const expected = Number(process.env.AGENT_COORD_EXPECTED_TESTS ?? EXPECTED_TESTS);
28
28
  // Same glob the suite always used — `--test test/` would recurse differently
@@ -41,9 +41,9 @@ child.stdout.on("data", (chunk) => {
41
41
 
42
42
  child.on("exit", (code, signal) => {
43
43
  if (signal) process.exit(1);
44
- // The TAP summary lines the runner prints once, at the end.
44
+ // TAP (`# pass N`) on Node 20–22; spec reporter (`ℹ pass N`) on newer Node.
45
45
  const num = (label) => {
46
- const m = out.match(new RegExp(`^# ${label} (\\d+)$`, "m"));
46
+ const m = out.match(new RegExp(`^(?:#|ℹ) ${label} (\\d+)\\s*$`, "m"));
47
47
  return m ? Number(m[1]) : null;
48
48
  };
49
49
  const tests = num("tests");
File without changes
File without changes
File without changes
File without changes
package/src/roles.ts CHANGED
@@ -33,6 +33,10 @@ export function slugifyRole(role: unknown): string {
33
33
 
34
34
  export const GATE_RUNNER_ROLE_IDS = new Set(["qa", "quality", "coordinator", "gate"]);
35
35
  export const COORDINATOR_ROLE_IDS = new Set(["coordinator"]);
36
+ // CI receives express-lane DONE: wake. Not a gate-runner — must not grant verdict.
37
+ export const CI_ROLE_IDS = new Set(["ci"]);
38
+ // Console / watchers. May send prefixed messages. Not a gate-runner.
39
+ export const AUTOMATION_ROLE_IDS = new Set(["automation"]);
36
40
 
37
41
  export function resolveRole(role: RoleInput): ResolvedRole | undefined {
38
42
  if (role === null || role === undefined) return undefined;
@@ -70,6 +74,14 @@ export function isCoordinator(role: RoleInput): boolean {
70
74
  return roleMatches(role, COORDINATOR_ROLE_IDS);
71
75
  }
72
76
 
77
+ export function isCi(role: RoleInput): boolean {
78
+ return roleMatches(role, CI_ROLE_IDS);
79
+ }
80
+
81
+ export function isAutomation(role: RoleInput): boolean {
82
+ return roleMatches(role, AUTOMATION_ROLE_IDS);
83
+ }
84
+
73
85
  // Which roles may emit which record.type at the send path. Enforcement lives
74
86
  // in messaging.ts (checkRecordAuthority); the table lives here so register/join
75
87
  // can ECHO the consequence back at onboarding — a role that cannot emit `go`