agent-coord-mcp 0.26.13 → 0.26.15
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 +158 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/server-identity.js +82 -0
- package/dist/server-identity.js.map +1 -0
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/events.js +64 -7
- package/dist/tools/events.js.map +1 -1
- package/dist/tools/record-events.js +11 -6
- package/dist/tools/record-events.js.map +1 -1
- package/dist/tools/records.js +23 -2
- package/dist/tools/records.js.map +1 -1
- package/dist/tools/transport.js +26 -56
- package/dist/tools/transport.js.map +1 -1
- package/dist/tools/work.js +37 -1
- package/dist/tools/work.js.map +1 -1
- package/dist/tools/worktrees.js +46 -1
- package/dist/tools/worktrees.js.map +1 -1
- package/hooks/peek-coord.mjs +0 -0
- package/hooks/tmux-pusher.mjs +0 -0
- package/package.json +14 -11
- package/scripts/check-test-count.mjs +1 -1
- package/scripts/coord-attention-clock.mjs +0 -0
- package/scripts/coord-node.sh +0 -0
- package/scripts/coord-stall-clock.mjs +0 -0
- package/scripts/coord-token.mjs +0 -0
- package/scripts/probe-tmux-liveness.sh +0 -0
- package/scripts/spawn-agent.sh +0 -0
- package/scripts/stop-agent.sh +0 -0
- package/scripts/typed-record-stats.mjs +0 -0
- package/src/capabilities.ts +188 -0
- package/src/server-identity.ts +83 -0
- package/src/server.ts +10 -2
- package/src/tools/events.ts +89 -9
- package/src/tools/record-events.ts +11 -6
- package/src/tools/records.ts +23 -1
- package/src/tools/transport.ts +26 -57
- package/src/tools/work.ts +54 -2
- package/src/tools/worktrees.ts +45 -1
package/src/tools/transport.ts
CHANGED
|
@@ -5,6 +5,15 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { registerTool } from "./registry.js";
|
|
6
6
|
import { roleInputSchema, type RoleArg } from "../roles.js";
|
|
7
7
|
import { attributeWriter, isGitRepo, lastWriterOf, loadScopes, ownsDocument } from "./scopes.js";
|
|
8
|
+
// Moved to a LEAF so `status` can report capabilities without a cycle — see
|
|
9
|
+
// src/server-identity.ts. Re-exported so every existing importer is untouched.
|
|
10
|
+
export { resolveServerIdentity } from "../server-identity.js";
|
|
11
|
+
import { resolveServerIdentity } from "../server-identity.js";
|
|
12
|
+
// ONE-WAY EDGE, and the direction is the point: `status` CALLS the capabilities
|
|
13
|
+
// verb rather than re-deriving its probes. Two probes of the same thing is the
|
|
14
|
+
// four-matchers defect committed deliberately, which is why 17.4 was deferred
|
|
15
|
+
// behind 13.7 instead of built beside it.
|
|
16
|
+
import { capabilitiesTool } from "../capabilities.js";
|
|
8
17
|
import { sendMessageTool, readMessagesTool } from "./messaging.js";
|
|
9
18
|
import { randomUUID } from "node:crypto";
|
|
10
19
|
import { existsSync, openSync, readFileSync, watch } from "node:fs";
|
|
@@ -835,63 +844,6 @@ function resolvePusherPath(): string {
|
|
|
835
844
|
return path.resolve(here, "..", "..", "hooks", "tmux-pusher.mjs");
|
|
836
845
|
}
|
|
837
846
|
|
|
838
|
-
/** Running server package + optional git identity. `fromDir` is a test seam. */
|
|
839
|
-
export function resolveServerIdentity(fromDir?: string): {
|
|
840
|
-
path: string;
|
|
841
|
-
version: string;
|
|
842
|
-
branch?: string;
|
|
843
|
-
sha?: string;
|
|
844
|
-
} {
|
|
845
|
-
const start = fromDir ?? path.dirname(fileURLToPath(import.meta.url));
|
|
846
|
-
let dir = start;
|
|
847
|
-
let pkgFile: string | undefined;
|
|
848
|
-
for (let i = 0; i < 8; i++) {
|
|
849
|
-
const candidate = path.join(dir, "package.json");
|
|
850
|
-
if (existsSync(candidate)) {
|
|
851
|
-
pkgFile = candidate;
|
|
852
|
-
break;
|
|
853
|
-
}
|
|
854
|
-
const parent = path.dirname(dir);
|
|
855
|
-
if (parent === dir) break;
|
|
856
|
-
dir = parent;
|
|
857
|
-
}
|
|
858
|
-
const pkgPath = pkgFile ?? path.resolve(start, "..", "..", "package.json");
|
|
859
|
-
const pkgDir = path.dirname(pkgPath);
|
|
860
|
-
let version = "unknown";
|
|
861
|
-
try {
|
|
862
|
-
const raw = JSON.parse(readFileSync(pkgPath, "utf8")) as { version?: string };
|
|
863
|
-
if (typeof raw.version === "string" && raw.version) version = raw.version;
|
|
864
|
-
} catch {
|
|
865
|
-
/* leave unknown — never invent a version */
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const out: { path: string; version: string; branch?: string; sha?: string } = {
|
|
869
|
-
path: pkgDir,
|
|
870
|
-
version,
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
dir = pkgDir;
|
|
874
|
-
let gitRoot: string | undefined;
|
|
875
|
-
for (let i = 0; i < 10; i++) {
|
|
876
|
-
if (existsSync(path.join(dir, ".git"))) {
|
|
877
|
-
gitRoot = dir;
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
const parent = path.dirname(dir);
|
|
881
|
-
if (parent === dir) break;
|
|
882
|
-
dir = parent;
|
|
883
|
-
}
|
|
884
|
-
if (gitRoot && isGitRepo(gitRoot)) {
|
|
885
|
-
const branch = spawnSync("git", ["-C", gitRoot, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
886
|
-
const sha = spawnSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
887
|
-
const b = branch.status === 0 ? String(branch.stdout).trim() : "";
|
|
888
|
-
const s = sha.status === 0 ? String(sha.stdout).trim() : "";
|
|
889
|
-
if (b) out.branch = b;
|
|
890
|
-
if (s) out.sha = s;
|
|
891
|
-
}
|
|
892
|
-
return out;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
847
|
/** An epoch-ms stamp as ISO-8601 UTC, or an explicit absence — never a blank. */
|
|
896
848
|
function stamp(ms: number | undefined): string {
|
|
897
849
|
return ms === undefined ? "(no stamp)" : new Date(ms).toISOString();
|
|
@@ -925,6 +877,22 @@ export async function statusTool(args: { agentId: string }) {
|
|
|
925
877
|
const cursor = await readJson<Cursor>(cursorFile(args.agentId), {});
|
|
926
878
|
const inboxOffset = cursor.inboxOffset ?? 0;
|
|
927
879
|
const unread = Math.max(0, inbox.length - inboxOffset);
|
|
880
|
+
// WHAT THIS PROCESS CAN ACTUALLY DO, embedded WHOLE.
|
|
881
|
+
//
|
|
882
|
+
// A new tool otherwise lands silently: an agent reading an old listing
|
|
883
|
+
// asserts the tool does not exist, and it is right about its own listing and
|
|
884
|
+
// wrong about the world. Three wrong conclusions in two days came from
|
|
885
|
+
// exactly that, each about which build a pane was running.
|
|
886
|
+
//
|
|
887
|
+
// The full report travels, not a summary. Flattening it to a boolean would
|
|
888
|
+
// hand the caller a tidier answer and a worse one: `versionLabel` stays
|
|
889
|
+
// CONTEXT rather than evidence, a THROWN probe stays an absent capability
|
|
890
|
+
// rather than a broken check, and `answeredBy` keeps naming which pid
|
|
891
|
+
// answered — two servers disagreeing mid-rollout is the normal case, and it
|
|
892
|
+
// is the case every other instrument here reports as if the fleet were one
|
|
893
|
+
// thing.
|
|
894
|
+
const capabilities = await capabilitiesTool();
|
|
895
|
+
|
|
928
896
|
return {
|
|
929
897
|
agentId: args.agentId,
|
|
930
898
|
registered: !!entry,
|
|
@@ -935,6 +903,7 @@ export async function statusTool(args: { agentId: string }) {
|
|
|
935
903
|
inboxUnread: unread,
|
|
936
904
|
inTmux: !!process.env.TMUX_PANE,
|
|
937
905
|
tmuxPane: process.env.TMUX_PANE,
|
|
906
|
+
capabilities,
|
|
938
907
|
};
|
|
939
908
|
}
|
|
940
909
|
|
package/src/tools/work.ts
CHANGED
|
@@ -17,10 +17,11 @@ import {
|
|
|
17
17
|
workDocLegacyWriteIssues,
|
|
18
18
|
LANES_V0_WRITE_ISSUE,
|
|
19
19
|
} from "../work.js";
|
|
20
|
-
import { parseFactsDoc, type FactEntry } from "@davidbalzan/groundwork-seam";
|
|
20
|
+
import { parseFactsDoc, type FactEntry, type Priority } from "@davidbalzan/groundwork-seam";
|
|
21
21
|
import { loadScopes, ownsDocument } from "./scopes.js";
|
|
22
22
|
import { AGENTS_FILE } from "../store.js";
|
|
23
23
|
import type { AgentRegistry } from "./shared.js";
|
|
24
|
+
import { summarize, blockedBy } from "./records.js";
|
|
24
25
|
|
|
25
26
|
// ---------- work state as data (Phase 8 Task 5) ----------
|
|
26
27
|
//
|
|
@@ -189,8 +190,42 @@ export const listWorkSchema = {
|
|
|
189
190
|
// native (default): discriminated v1 | lanes-v0. lanes: lossy projection of v1.
|
|
190
191
|
view: z.enum(["native", "lanes"]).optional(),
|
|
191
192
|
repo: z.string().optional(),
|
|
193
|
+
// Fetch ONE full record by id — the second call Task 15.1 asks for.
|
|
194
|
+
// Identity rows (below) never carry the body; this is how a caller gets it,
|
|
195
|
+
// and only for the one row it actually needs rather than every row's.
|
|
196
|
+
id: z.string().optional(),
|
|
192
197
|
};
|
|
193
198
|
|
|
199
|
+
// A queue row without its body — id, priority, a bounded headline, and
|
|
200
|
+
// blocked-by, the fields the doc names. `owner` is NOT included: QueueItem
|
|
201
|
+
// carries no structured owner field, and the free-text `origin:` phrase some
|
|
202
|
+
// items end with ("origin: coordinator retracting its own broadcast canon…")
|
|
203
|
+
// is prose, not an id — extracting one heuristically would fabricate a field
|
|
204
|
+
// this data model does not have. Omitted rather than guessed, same
|
|
205
|
+
// discipline as `list_agents`' heartbeatSource (Task 13.5).
|
|
206
|
+
export type QueueIdentityRow = {
|
|
207
|
+
id: string;
|
|
208
|
+
priority: Priority | null;
|
|
209
|
+
headline: string;
|
|
210
|
+
truncated: boolean;
|
|
211
|
+
blockedBy: string | null;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
function identityRowOf(item: QueueItem): QueueIdentityRow {
|
|
215
|
+
const flat = String(item.text).replace(/\s+/g, " ").replace(/\*\*/g, "").trim();
|
|
216
|
+
return {
|
|
217
|
+
id: item.id,
|
|
218
|
+
priority: item.priority,
|
|
219
|
+
headline: summarize(item.text),
|
|
220
|
+
// `summarize` itself decides where to cut; re-deriving "did it cut" here
|
|
221
|
+
// from a second pass over the same text would be the two-matchers-one-
|
|
222
|
+
// question shape this phase keeps finding elsewhere (refsIn, COMMIT_REF).
|
|
223
|
+
// Comparing against its own bound is exact and free.
|
|
224
|
+
truncated: flat.length > 96,
|
|
225
|
+
blockedBy: blockedBy(item),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
194
229
|
export async function listWorkTool(args: {
|
|
195
230
|
project: string;
|
|
196
231
|
kind?: "queue" | "done" | "board" | "facts";
|
|
@@ -198,6 +233,7 @@ export async function listWorkTool(args: {
|
|
|
198
233
|
includeDone?: boolean;
|
|
199
234
|
view?: "native" | "lanes";
|
|
200
235
|
repo?: string;
|
|
236
|
+
id?: string;
|
|
201
237
|
}) {
|
|
202
238
|
// No import yet (or the store was deleted) → read the documents directly.
|
|
203
239
|
// This is the fallback that makes "the markdown stands alone" true in code
|
|
@@ -231,6 +267,17 @@ export async function listWorkTool(args: {
|
|
|
231
267
|
}
|
|
232
268
|
if (args.view === "lanes") board = board.map(projectV1ToLanes);
|
|
233
269
|
|
|
270
|
+
// THE SECOND CALL — one row, whole. Searches queue then done (a done entry
|
|
271
|
+
// shares the id space; an id is unique within a project's work state), so
|
|
272
|
+
// the caller does not have to know which kind its own id belongs to.
|
|
273
|
+
if (args.id !== undefined) {
|
|
274
|
+
const queueHit = queue.find((q) => q.id === args.id);
|
|
275
|
+
if (queueHit) return { ok: true as const, project: state.project, repo: state.repo, source, kind: "queue" as const, item: queueHit };
|
|
276
|
+
const doneHit = done.find((d) => d.id === args.id);
|
|
277
|
+
if (doneHit) return { ok: true as const, project: state.project, repo: state.repo, source, kind: "done" as const, item: doneHit };
|
|
278
|
+
return { ok: false as const, error: `no queue item or DONE entry with id '${args.id}' in project '${state.project}'` };
|
|
279
|
+
}
|
|
280
|
+
|
|
234
281
|
const openQueue = args.includeDone ? queue : queue.filter((q) => !q.done);
|
|
235
282
|
const filtered = args.priority ? openQueue.filter((q) => q.priority === args.priority) : openQueue;
|
|
236
283
|
|
|
@@ -239,7 +286,12 @@ export async function listWorkTool(args: {
|
|
|
239
286
|
project: state.project,
|
|
240
287
|
repo: state.repo,
|
|
241
288
|
source,
|
|
242
|
-
|
|
289
|
+
// IDENTITY ONLY (Task 15.1) — id, priority, a bounded headline, and
|
|
290
|
+
// blocked-by; never the full item text. Call again with `id` for one
|
|
291
|
+
// row's body. This is the change that makes the tool cheap: the same
|
|
292
|
+
// question that used to return every open item's ~1,500-char body now
|
|
293
|
+
// returns a fixed-width row per item.
|
|
294
|
+
...(args.kind === "queue" || args.kind === undefined ? { queue: filtered.map(identityRowOf) } : {}),
|
|
243
295
|
...(args.kind === "done" || args.kind === undefined ? { done } : {}),
|
|
244
296
|
...(args.kind === "board" || args.kind === undefined ? { board } : {}),
|
|
245
297
|
...(args.kind === "facts" || args.kind === undefined ? { facts } : {}),
|
package/src/tools/worktrees.ts
CHANGED
|
@@ -268,7 +268,51 @@ export async function refreshWorktreesTool(args: { repo: string; base: string; a
|
|
|
268
268
|
for (const w of listWorktrees(repo)) {
|
|
269
269
|
const at = w.path;
|
|
270
270
|
if (samePath(at, primaryOf(repo) ?? "")) {
|
|
271
|
-
|
|
271
|
+
// THE PRIMARY CHECKOUT IS REPORTED, ALWAYS — it used to be skipped with
|
|
272
|
+
// "not a slice tree", which is true and useless. It is the tree David
|
|
273
|
+
// reads, the one every hand-run measurement runs in, and a silent skip
|
|
274
|
+
// let it sit arbitrarily far behind while this verb reported a clean
|
|
275
|
+
// sweep. "Not ours to fast-forward" and "nothing to say about it" are
|
|
276
|
+
// different claims and only the first one was intended.
|
|
277
|
+
let behind = "unknown";
|
|
278
|
+
let ahead = "unknown";
|
|
279
|
+
try {
|
|
280
|
+
behind = git(at, ["rev-list", "--count", `HEAD..${tip}`]);
|
|
281
|
+
ahead = git(at, ["rev-list", "--count", `${tip}..HEAD`]);
|
|
282
|
+
} catch {
|
|
283
|
+
/* left as unknown — an unreadable count must not read as zero */
|
|
284
|
+
}
|
|
285
|
+
const dirty = isDirty(at);
|
|
286
|
+
// FAST-FORWARD ONLY, AND ONLY WHEN CLEAN. A ff-only merge cannot rewrite
|
|
287
|
+
// history or resolve a conflict, so the worst case is a refusal. Dirty is
|
|
288
|
+
// refused for the same reason a slice tree is: uncommitted work is what a
|
|
289
|
+
// diff cannot show and git cannot give back.
|
|
290
|
+
const canFf = !dirty && ahead === "0" && behind !== "0" && behind !== "unknown";
|
|
291
|
+
if (args.apply && canFf) {
|
|
292
|
+
try {
|
|
293
|
+
git(at, ["merge", "--ff-only", ref]);
|
|
294
|
+
results.push({ path: w.path, action: "fast-forwarded", why: `primary checkout advanced ${behind} commit(s) to ${ref}` });
|
|
295
|
+
continue;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
results.push({ path: w.path, action: "refused", why: `primary checkout: ff-only merge failed — ${(e as Error).message}` });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
results.push({
|
|
302
|
+
path: w.path,
|
|
303
|
+
action: behind === "0" ? "current" : "stale",
|
|
304
|
+
why:
|
|
305
|
+
behind === "0"
|
|
306
|
+
? `primary checkout is at ${ref}`
|
|
307
|
+
: `primary checkout is ${behind} commit(s) behind ${ref}` +
|
|
308
|
+
(dirty
|
|
309
|
+
? " and DIRTY — not fast-forwarded; uncommitted work is what a diff cannot show"
|
|
310
|
+
: ahead !== "0"
|
|
311
|
+
? ` and ${ahead} ahead — not fast-forwarded; it has commits ${ref} does not`
|
|
312
|
+
: args.apply
|
|
313
|
+
? ""
|
|
314
|
+
: " — pass apply:true to fast-forward it"),
|
|
315
|
+
});
|
|
272
316
|
continue;
|
|
273
317
|
}
|
|
274
318
|
if (isDirty(at)) {
|