@izagood/avcs 0.18.0 → 0.20.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.
@@ -0,0 +1,75 @@
1
+ // Phase 16 M1.1 (docs/18 §1.1) — the MCP response layer.
2
+ //
3
+ // Tokens are a budget (docs/18 §2 principle 2). Two consequences live here:
4
+ //
5
+ // - Serialization is COMPACT by default. Pretty-printing costs indentation tokens on
6
+ // every call an agent ever makes, for a reader that is not human. `verbose` restores
7
+ // it for the times a person is actually looking.
8
+ // - Every failure becomes a machine-readable envelope carrying `nextActions`, so an
9
+ // agent recovers by following a list instead of parsing prose and flailing (§1 gap 6).
10
+ //
11
+ // What this layer deliberately does NOT do: wrap success shapes. Existing consumers and
12
+ // tests parse the raw shape, so compatibility is absolute — additive fields only
13
+ // (§2 principle 1, and the second recorded risk in §5).
14
+ /**
15
+ * Known failure classes → what to do about them. Every dotted `avcs.*` name here must be a
16
+ * REGISTERED tool — a hint pointing at a tool that does not exist is worse than prose,
17
+ * because the agent follows it and fails. A test pins this against the live tool list.
18
+ */
19
+ export const RECOVERY = [
20
+ {
21
+ // The one error an agent used to flail on becomes a single call: land re-pushes,
22
+ // re-checks the merge, re-checkpoints and re-integrates, and the queue behind it
23
+ // re-reduces the frontier union rather than bouncing the submission back.
24
+ re: /head moved|not up to date|stale (parent|head)/i,
25
+ hint: "the view's head advanced while you worked; landing absorbs that for you",
26
+ nextActions: ["avcs.sync.land", "avcs.integration.status"],
27
+ },
28
+ {
29
+ re: /no local signing key|signing key|keystore/i,
30
+ hint: "this action must be signed by an actor key held locally",
31
+ nextActions: ["avcs key provision <actor-id>", "avcs key ls"],
32
+ },
33
+ {
34
+ re: /not an AVCS repo|no \.avcs/i,
35
+ hint: "the resolved directory has no .avcs/ at or above it",
36
+ nextActions: ["pass cwd: <repo dir> with the call", "avcs init <dir>"],
37
+ },
38
+ {
39
+ re: /open conflict|conflicts? remain|unresolved conflict/i,
40
+ hint: "a human decision is required; do not retry through it",
41
+ nextActions: ["avcs.conflict.list", "avcs.decision.record"],
42
+ },
43
+ {
44
+ re: /lease|held by/i,
45
+ hint: "another actor holds a write lease overlapping your scope",
46
+ nextActions: ["avcs.contention.check", "avcs.lease.request"],
47
+ },
48
+ {
49
+ re: /validation failed|checks? failed|evidence/i,
50
+ hint: "the gate wants passing evidence bound to this tree",
51
+ nextActions: ["avcs.validate.run", "avcs.evidence.attach", "avcs.repair.context"],
52
+ },
53
+ ];
54
+ /**
55
+ * Normalize a caller-supplied limit against a default (Phase 16 M1.2). A missing, negative,
56
+ * zero, or non-finite value falls back to the default rather than returning nothing or
57
+ * everything — an unbounded read is the failure mode this layer exists to prevent.
58
+ */
59
+ export function boundedLimit(raw, fallback) {
60
+ const n = typeof raw === "number" ? raw : Number(raw);
61
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
62
+ }
63
+ /** Serialize a successful tool result. Compact unless a human asked for readability. */
64
+ export function serializeResult(result, opts) {
65
+ return opts?.verbose ? JSON.stringify(result, null, 2) : JSON.stringify(result);
66
+ }
67
+ /** Translate a thrown value into the failure envelope the transport sends. */
68
+ export function errorEnvelope(e) {
69
+ const error = e instanceof Error ? e.message : String(e);
70
+ const rule = RECOVERY.find((r) => r.re.test(error));
71
+ if (!rule)
72
+ return { error };
73
+ return { error, hint: rule.hint, nextActions: rule.nextActions };
74
+ }
75
+ //# sourceMappingURL=respond.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"respond.js","sourceRoot":"","sources":["../../src/mcp/respond.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,4EAA4E;AAC5E,EAAE;AACF,sFAAsF;AACtF,wFAAwF;AACxF,oDAAoD;AACpD,qFAAqF;AACrF,0FAA0F;AAC1F,EAAE;AACF,wFAAwF;AACxF,iFAAiF;AACjF,wDAAwD;AAiBxD;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAmB;IACtC;QACE,iFAAiF;QACjF,iFAAiF;QACjF,0EAA0E;QAC1E,EAAE,EAAE,gDAAgD;QACpD,IAAI,EAAE,yEAAyE;QAC/E,WAAW,EAAE,CAAC,gBAAgB,EAAE,yBAAyB,CAAC;KAC3D;IACD;QACE,EAAE,EAAE,4CAA4C;QAChD,IAAI,EAAE,yDAAyD;QAC/D,WAAW,EAAE,CAAC,+BAA+B,EAAE,aAAa,CAAC;KAC9D;IACD;QACE,EAAE,EAAE,6BAA6B;QACjC,IAAI,EAAE,qDAAqD;QAC3D,WAAW,EAAE,CAAC,oCAAoC,EAAE,iBAAiB,CAAC;KACvE;IACD;QACE,EAAE,EAAE,sDAAsD;QAC1D,IAAI,EAAE,uDAAuD;QAC7D,WAAW,EAAE,CAAC,oBAAoB,EAAE,sBAAsB,CAAC;KAC5D;IACD;QACE,EAAE,EAAE,gBAAgB;QACpB,IAAI,EAAE,0DAA0D;QAChE,WAAW,EAAE,CAAC,uBAAuB,EAAE,oBAAoB,CAAC;KAC7D;IACD;QACE,EAAE,EAAE,4CAA4C;QAChD,IAAI,EAAE,oDAAoD;QAC1D,WAAW,EAAE,CAAC,mBAAmB,EAAE,sBAAsB,EAAE,qBAAqB,CAAC;KAClF;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY,EAAE,QAAgB;IACzD,MAAM,CAAC,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AAChE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,eAAe,CAAC,MAAe,EAAE,IAA4B;IAC3E,OAAO,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAClF,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,CAAU;IACtC,MAAM,KAAK,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;AACnE,CAAC"}
@@ -17,6 +17,25 @@ export interface ToolDef {
17
17
  handler: (repo: Repo, input: Record<string, unknown>, ctx?: ToolCtx) => Promise<unknown>;
18
18
  }
19
19
  export declare function actorOf(input: Record<string, unknown>): Actor;
20
+ /** The schema advertised to clients: the tool's own inputs plus the universal `cwd` and
21
+ * `verbose`. Returns a fresh object — the ToolDef's own schema is never mutated. */
22
+ export declare function advertisedSchema(t: ToolDef): Record<string, unknown>;
23
+ /**
24
+ * Run one tool call and render it for the transport (Phase 16 M1.1, docs/18 §1.1).
25
+ * Exported so the layer is testable without booting the SDK, the same way the handlers are.
26
+ *
27
+ * Success keeps its raw shape — only the serialization changes (§2 principle 1). Failure
28
+ * becomes `{ error, hint?, nextActions? }` so the agent recovers from a list instead of
29
+ * parsing prose; it is returned with `isError`, not thrown, because a thrown error reaches
30
+ * the agent as an opaque transport failure and loses the recovery hints entirely.
31
+ */
32
+ export declare function runTool(tool: ToolDef, repo: Repo, args: Record<string, unknown>, ctx?: ToolCtx): Promise<{
33
+ content: {
34
+ type: "text";
35
+ text: string;
36
+ }[];
37
+ isError?: boolean;
38
+ }>;
20
39
  /**
21
40
  * Resolve which AVCS repo a tool call targets, returning its `.avcs` root dir.
22
41
  *
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAGtC,OAAO,KAAK,EAAE,KAAK,EAAmB,MAAM,qBAAqB,CAAC;AAuBlE,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wFAAwF;AACxF,MAAM,WAAW,OAAO;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1F;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAG7D;AAuBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,GACjC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,eAAO,MAAM,KAAK,EAAE,OAAO,EAke1B,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAiIpD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAmBtC,OAAO,KAAK,EAAE,KAAK,EAAmB,MAAM,qBAAqB,CAAC;AAuBlE,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wFAAwF;AACxF,MAAM,WAAW,OAAO;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1F;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAG7D;AA+BD;qFACqF;AACrF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASpE;AAED;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,CAAC,EAAE,OAAO,GACZ,OAAO,CAAC;IAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAQ3E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,GACjC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,eAAO,MAAM,KAAK,EAAE,OAAO,EA+pB1B,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CA6HpD"}
@@ -19,6 +19,23 @@ import { dirname, join } from "node:path";
19
19
  import { Repo } from "../api/repo.js";
20
20
  import { isBinary } from "../core/bytes.js";
21
21
  import { ObjectStore } from "../store/objectStore.js";
22
+ import { serializeResult, errorEnvelope, boundedLimit } from "./respond.js";
23
+ import { unifiedDiff } from "../query/diff.js";
24
+ import { buildGuide } from "./guide.js";
25
+ import { land } from "./land.js";
26
+ /** The hub's governance refs (`head:<view>`, policy, …). Empty when unreachable — a head
27
+ * comparison is informational, so a dead hub degrades the answer instead of failing it. */
28
+ async function fetchHubRefs(url) {
29
+ try {
30
+ const res = await fetch(`${url.replace(/\/$/, "")}/refs`);
31
+ if (!res.ok)
32
+ return {};
33
+ return (await res.json()).refs ?? {};
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ }
22
39
  /**
23
40
  * Read the installed package version off disk. Works for both the type-stripped
24
41
  * source layout (src/mcp/server.ts) and the built layout (dist/mcp/server.js):
@@ -61,6 +78,44 @@ const cwdSchema = {
61
78
  "owning AVCS repo (.avcs). Optional — defaults to the client's workspace root, then the " +
62
79
  "server's own cwd. Ignored when the server is pinned via AVCS_REPO.",
63
80
  };
81
+ /** Universal formatting switch (Phase 16 M1.1). Responses are compact by default because
82
+ * indentation is whitespace the agent pays for on every call; this restores pretty-print
83
+ * for the times a human is reading. Consumed by the dispatch layer, never by a handler. */
84
+ const verboseSchema = {
85
+ type: "boolean",
86
+ description: "Pretty-print the response for human reading. Default false (compact, fewer tokens).",
87
+ };
88
+ /** The schema advertised to clients: the tool's own inputs plus the universal `cwd` and
89
+ * `verbose`. Returns a fresh object — the ToolDef's own schema is never mutated. */
90
+ export function advertisedSchema(t) {
91
+ return {
92
+ ...t.inputSchema,
93
+ properties: {
94
+ ...(t.inputSchema.properties ?? {}),
95
+ cwd: cwdSchema,
96
+ verbose: verboseSchema,
97
+ },
98
+ };
99
+ }
100
+ /**
101
+ * Run one tool call and render it for the transport (Phase 16 M1.1, docs/18 §1.1).
102
+ * Exported so the layer is testable without booting the SDK, the same way the handlers are.
103
+ *
104
+ * Success keeps its raw shape — only the serialization changes (§2 principle 1). Failure
105
+ * becomes `{ error, hint?, nextActions? }` so the agent recovers from a list instead of
106
+ * parsing prose; it is returned with `isError`, not thrown, because a thrown error reaches
107
+ * the agent as an opaque transport failure and loses the recovery hints entirely.
108
+ */
109
+ export async function runTool(tool, repo, args, ctx) {
110
+ const verbose = args.verbose === true;
111
+ try {
112
+ const result = await tool.handler(repo, args, ctx);
113
+ return { content: [{ type: "text", text: serializeResult(result, { verbose }) }] };
114
+ }
115
+ catch (e) {
116
+ return { content: [{ type: "text", text: serializeResult(errorEnvelope(e), { verbose }) }], isError: true };
117
+ }
118
+ }
64
119
  /**
65
120
  * Resolve which AVCS repo a tool call targets, returning its `.avcs` root dir.
66
121
  *
@@ -108,6 +163,113 @@ export async function resolveRepoDir(callCwd, listRoots) {
108
163
  "or run `avcs init`.");
109
164
  }
110
165
  export const TOOLS = [
166
+ {
167
+ // First in the list on purpose: it is the tool that explains the rest.
168
+ name: "avcs.guide",
169
+ description: "How to use AVCS: the canonical loop, agent rules, tool index, error recovery. Call this first.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ topic: {
174
+ type: "string",
175
+ enum: ["workflow", "tools", "sync", "rules", "errors"],
176
+ description: "omit for the canonical loop",
177
+ },
178
+ },
179
+ },
180
+ handler: async (_repo, i) => buildGuide(TOOLS, typeof i.topic === "string" ? i.topic : undefined),
181
+ },
182
+ {
183
+ // ── M2 sync surface (docs/18 §M2): an agent never pulls by hand ──
184
+ name: "avcs.sync.pull",
185
+ description: "Pull objects from the hub (conflict-free gossip). dryRun reports what would arrive without importing.",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: {
189
+ hub: { type: "string", description: "remote name or hub URL; default the persisted 'origin'" },
190
+ dryRun: { type: "boolean", description: "report the would-pull count and head comparison, import nothing" },
191
+ },
192
+ },
193
+ handler: async (repo, i) => {
194
+ const remote = typeof i.hub === "string" ? i.hub : "origin";
195
+ const url = await repo.remoteUrl(remote);
196
+ const hubRefs = await fetchHubRefs(url);
197
+ const view = "main";
198
+ const hubHead = hubRefs[`head:${view}`] ?? null;
199
+ if (i.dryRun === true) {
200
+ // Count what the hub holds and we lack, without importing any of it.
201
+ const have = (await (await fetch(`${url}/have`)).json());
202
+ let missing = 0;
203
+ for (const oid of have)
204
+ if (!(await repo.store.has(oid)))
205
+ missing++;
206
+ const local = await repo.protectedHead(view);
207
+ return { pulled: missing, dryRun: true, head: { local, hub: hubHead }, converged: local === hubHead };
208
+ }
209
+ const { pulled } = await repo.pullHub(url);
210
+ const local = await repo.protectedHead(view);
211
+ return { pulled, head: { local, hub: hubHead }, converged: local === hubHead };
212
+ },
213
+ },
214
+ {
215
+ name: "avcs.sync.push",
216
+ description: "Push local objects to the hub. Returns how many were accepted and how many the gate rejected.",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ hub: { type: "string", description: "remote name or hub URL; default the persisted 'origin'" },
221
+ as: { type: "string", description: "actor id whose key signs the writes" },
222
+ },
223
+ },
224
+ handler: async (repo, i) => {
225
+ const url = await repo.remoteUrl(typeof i.hub === "string" ? i.hub : "origin");
226
+ return repo.pushHub(url, typeof i.as === "string" ? { as: i.as } : undefined);
227
+ },
228
+ },
229
+ {
230
+ name: "avcs.sync.land",
231
+ description: "Land work on a protected head in one call: push, merge-check, checkpoint, integrate. Result is landed or a conflict packet.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: {
235
+ view: { type: "string", description: "default 'main'" },
236
+ summary: { type: "string", description: "checkpoint summary" },
237
+ by: { type: "string", description: "actor id landing the work" },
238
+ hub: { type: "string", description: "remote name or hub URL; omit for the persisted 'origin', or a local-only repo" },
239
+ maxAttempts: { type: "number", description: "bounded contention retries; default 5. Conflicts are never retried." },
240
+ workspace: { type: "string", description: "land this workspace onto the base line first" },
241
+ },
242
+ required: ["by"],
243
+ },
244
+ handler: (repo, i) => land(repo, {
245
+ view: i.view,
246
+ summary: i.summary,
247
+ by: String(i.by),
248
+ hub: i.hub,
249
+ maxAttempts: i.maxAttempts,
250
+ workspace: i.workspace,
251
+ }),
252
+ },
253
+ {
254
+ name: "avcs.workspace.project",
255
+ description: "Write a view to a directory on disk, so build/test loops outside validate.run need no CLI.",
256
+ inputSchema: {
257
+ type: "object",
258
+ properties: {
259
+ out: { type: "string", description: "absolute target directory" },
260
+ view: { type: "string", description: "default 'main'" },
261
+ name: { type: "string", description: "project this workspace's isolated ops too" },
262
+ },
263
+ required: ["out"],
264
+ },
265
+ handler: async (repo, i) => {
266
+ const view = i.view ?? "main";
267
+ const workspace = typeof i.name === "string" ? i.name : undefined;
268
+ const files = await repo.checkoutInto(String(i.out), view, workspace ? { workspace } : undefined);
269
+ const res = await repo.materialize(view, workspace ? { workspace } : undefined);
270
+ return { dir: String(i.out), fileCount: files.length, treeHash: res.treeHash };
271
+ },
272
+ },
111
273
  {
112
274
  name: "avcs.intent.create",
113
275
  description: "Open an intent: the goal + constraints + allowed scopes for a unit of work. Agents must work within an intent.",
@@ -134,9 +296,12 @@ export const TOOLS = [
134
296
  },
135
297
  {
136
298
  name: "avcs.intent.list",
137
- description: "List all intents in the repo.",
138
- inputSchema: { type: "object", properties: {} },
139
- handler: (repo) => repo.listIntents(),
299
+ description: "List intents in the repo. Bounded: limit (default 50).",
300
+ inputSchema: {
301
+ type: "object",
302
+ properties: { limit: { type: "number", description: "max intents to return; default 50" } },
303
+ },
304
+ handler: async (repo, i) => (await repo.listIntents()).slice(0, boundedLimit(i.limit, 50)),
140
305
  },
141
306
  {
142
307
  name: "avcs.session.start",
@@ -155,7 +320,7 @@ export const TOOLS = [
155
320
  },
156
321
  {
157
322
  name: "avcs.operation.propose",
158
- description: "Propose a semantic change. MVP supports file writes. DECLARE EFFECTS honestly (changesBehavior, breaksPublicApi) — policy gates on them. Pass baseText or baseBlobOid to author a base-relative edit_file (3-way-mergeable with concurrent edits); omit both for a whole-file put_file.",
323
+ description: "Propose a semantic change (MVP: file writes). Declare effects honestly — policy gates on them. baseText/baseBlobOid authors a 3-way-mergeable edit_file.",
159
324
  inputSchema: {
160
325
  type: "object",
161
326
  properties: {
@@ -215,7 +380,7 @@ export const TOOLS = [
215
380
  },
216
381
  {
217
382
  name: "avcs.contention.check",
218
- description: "Early conflict warning (Phase 15.3): for each entity key, other actors' LIVE concurrent operations (outside your causal closure, not rejected, not superseded) and active lease holders whose scope overlaps. Check BEFORE/WHILE editing so overlap is discovered at authoring time, not at finalize. Perspective: pass sessionOid (its actor + ops), or actor (all their keys), or explicit keys.",
383
+ description: "Other actors' live concurrent ops and overlapping lease holders for given keys. Run while editing, so overlap surfaces before finalize.",
219
384
  inputSchema: {
220
385
  type: "object",
221
386
  properties: {
@@ -234,7 +399,7 @@ export const TOOLS = [
234
399
  },
235
400
  {
236
401
  name: "avcs.workspace.land",
237
- description: "Land a workspace onto its base line (docs/16): its isolated ops join the base view and merge there via the normal 3-way reduce — disjoint edits auto-merge, overlaps surface as conflicts. Idempotent. Returns the current landed set.",
402
+ description: "Land a workspace onto its base line: its isolated ops join the base view and merge there. Idempotent.",
238
403
  inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
239
404
  handler: async (repo, i) => {
240
405
  await repo.landWorkspace(String(i.name));
@@ -249,7 +414,7 @@ export const TOOLS = [
249
414
  },
250
415
  {
251
416
  name: "avcs.line.create",
252
- description: "Fork a long-lived line (e.g. 'v1.x') from another line at its current state. The new line inherits history up to the fork and then diverges — same entity can hold different content per line with no conflict (Phase 8).",
417
+ description: "Fork a long-lived line (e.g. 'v1.x') from another at its current state. It inherits history to the fork, then diverges.",
253
418
  inputSchema: {
254
419
  type: "object",
255
420
  properties: { name: { type: "string" }, fromLine: { type: "string" }, atCheckpointOid: { type: "string" } },
@@ -265,7 +430,7 @@ export const TOOLS = [
265
430
  },
266
431
  {
267
432
  name: "avcs.operation.backport",
268
- description: "Port (cherry-pick / backport) an operation onto another line: mints a new op on the target line carrying the source's change, with derivedFrom provenance. Does not affect the source line.",
433
+ description: "Cherry-pick an operation onto another line: mints a new op carrying the change, with derivedFrom provenance. Source line untouched.",
269
434
  inputSchema: {
270
435
  type: "object",
271
436
  properties: { sourceOpOid: { type: "string" }, targetLine: { type: "string" }, actor: actorSchema },
@@ -304,7 +469,7 @@ export const TOOLS = [
304
469
  },
305
470
  {
306
471
  name: "avcs.view.materialize",
307
- description: "Reduce the operation graph for a view into a code tree + per-op status + open conflicts. This is how an agent checks whether its work merges. Pass includeStatuses (e.g. ['accepted','needs_decision']) to ALSO project gated/pending ops so their computed 3-way merge can be inspected before acceptance; `dropped` lists ops NOT in the tree (status outside includeStatuses) so omissions are never silent (issue #13).",
472
+ description: "Reduce a view's operation graph into a tree, per-op status and open conflicts how an agent checks that its work merges.",
308
473
  inputSchema: {
309
474
  type: "object",
310
475
  properties: {
@@ -314,6 +479,8 @@ export const TOOLS = [
314
479
  items: { type: "string", enum: ["proposed", "validating", "accepted", "rejected", "superseded", "needs_decision", "quarantined"] },
315
480
  description: "op statuses to project into the tree; default ['accepted']",
316
481
  },
482
+ filesLimit: { type: "number", description: "max paths to list; default 500. treeHash/status/conflicts/dropped are never bounded." },
483
+ pathsOnlyUnder: { type: "string", description: "list only paths under this prefix (e.g. 'src/')" },
317
484
  },
318
485
  },
319
486
  handler: async (repo, i) => {
@@ -328,9 +495,16 @@ export const TOOLS = [
328
495
  const dropped = Object.entries(status)
329
496
  .filter(([, s]) => !projected.has(s))
330
497
  .map(([oid, s]) => ({ oid, status: s }));
498
+ // Only the FILE LISTING is bounded (Phase 16 M1.2). treeHash, status, conflicts and
499
+ // dropped are correctness data — bounding them would make a wrong answer look right.
500
+ const under = typeof i.pathsOnlyUnder === "string" ? i.pathsOnlyUnder : undefined;
501
+ const all = [...res.tree.keys()].sort().filter((p) => (under ? p.startsWith(under) : true));
502
+ const filesLimit = boundedLimit(i.filesLimit, 500);
331
503
  return {
332
504
  treeHash: res.treeHash,
333
- files: [...res.tree.keys()].sort(),
505
+ files: all.slice(0, filesLimit),
506
+ filesTotal: all.length,
507
+ filesTruncated: all.length > filesLimit,
334
508
  status,
335
509
  conflicts: res.conflicts,
336
510
  dropped,
@@ -345,7 +519,7 @@ export const TOOLS = [
345
519
  },
346
520
  {
347
521
  name: "avcs.decision.record",
348
- description: "Record a HUMAN/owner resolution of a conflict. Chosen ops are accepted, rejected ops dropped — and the rationale becomes reusable history. The agent CANNOT forge this: the owner is asked to confirm via MCP elicitation, and the decision is then signed with the owner's LOCAL private key (which the agent never holds). Unsigned/forged decisions are dropped by the reducer's trust gate (issue #15). Requires actor.kind 'human', an elicitation-capable client, and a provisioned local owner key.",
522
+ description: "Record a human owner's conflict resolution. Owner-confirmed and signed with their local key; an agent cannot forge it.",
349
523
  inputSchema: {
350
524
  type: "object",
351
525
  properties: {
@@ -401,7 +575,7 @@ export const TOOLS = [
401
575
  },
402
576
  {
403
577
  name: "avcs.integration.submit",
404
- description: "Submit a draft checkpoint to a remote hub's integration queue (Phase 14, docs/17). NEVER pull-and-redo: the result is always a verdict to act on — `advanced` (done: the hub re-reduced the frontier union and moved the head), `conflict` (a minimal repair packet with counterpart ops + prior decisions: decide via avcs.decision.record, then resubmit), `needs_evidence` (run avcs.validate.run ONCE against the returned integrated tree the needed delta objects were already pulled — attach evidence, resubmit the SAME ticketId), `queued` (retry after retryAfterMs), or `rejected` (hard gate). Omit `checkpoint` to checkpoint the view automatically.",
578
+ description: "Submit a checkpoint to the integration queue. Verdict is advanced, conflict, needs_evidence or queuednever 'pull and redo'.",
405
579
  inputSchema: {
406
580
  type: "object",
407
581
  properties: {
@@ -464,7 +638,7 @@ export const TOOLS = [
464
638
  },
465
639
  {
466
640
  name: "avcs.validate.run",
467
- description: "Run validation commands against a view and attach treeHash-bound Evidence (docs/16 §8). By default materializes into a fresh temp dir. Pass `dir` (e.g. your working tree, which already has the build env like node_modules) to run there without avcs owning install — the fix for Node/pnpm (issue #11); with `dir` it runs in place unless `project:true`. Use `workspace` to validate a workspace view. Evidence trusts author≠signer, so pass a ciActor distinct from the op authors.",
641
+ description: "Run validation commands against a view and attach treeHash-bound evidence. Pass dir to reuse an existing build environment.",
468
642
  inputSchema: {
469
643
  type: "object",
470
644
  properties: {
@@ -511,7 +685,7 @@ export const TOOLS = [
511
685
  },
512
686
  {
513
687
  name: "avcs.repair.context",
514
- description: "Get a MINIMAL repair packet for ops whose validation failed (the failing output + related prior decisions + a fix instruction). Use instead of re-reading the whole repo.",
688
+ description: "Minimal repair packet for ops whose validation failed: failing output, related decisions, a fix instruction. Cheaper than re-reading the repo.",
515
689
  inputSchema: { type: "object", properties: { ops: { type: "array", items: { type: "string" } } }, required: ["ops"] },
516
690
  handler: (repo, i) => repo.repairContext(i.ops),
517
691
  },
@@ -529,19 +703,56 @@ export const TOOLS = [
529
703
  },
530
704
  {
531
705
  name: "avcs.history",
532
- description: "History of one entity in causal order (the ops that touched a file/symbol, each with actor/intent/purpose). O(ops-on-entity) via the entity index.",
533
- inputSchema: { type: "object", properties: { entityKey: { type: "string" } }, required: ["entityKey"] },
534
- handler: async (repo, i) => (await repo.historyOf(String(i.entityKey))).map((o) => ({ op: o.oid, actor: o.actor.id, purpose: o.declaredPurpose, at: o.createdAt, line: o.line ?? "main" })),
706
+ description: "History of one entity in causal order. Paged: limit (default 20) + cursor.",
707
+ inputSchema: {
708
+ type: "object",
709
+ properties: {
710
+ entityKey: { type: "string" },
711
+ limit: { type: "number", description: "max ops to return; default 20. A full page means there may be more." },
712
+ cursor: { type: "string", description: "opaque: the last op oid of the previous page; returns what follows it." },
713
+ },
714
+ required: ["entityKey"],
715
+ },
716
+ // Stays an ARRAY: success shapes are never wrapped (docs/18 §2 principle 1, §5), so the
717
+ // page-size signal is the standard "short page = end" rather than an added total field.
718
+ handler: async (repo, i) => {
719
+ const all = await repo.historyOf(String(i.entityKey));
720
+ const from = i.cursor ? all.findIndex((o) => o.oid === i.cursor) + 1 : 0;
721
+ const limit = boundedLimit(i.limit, 20);
722
+ return all.slice(from, from + limit).map((o) => ({ op: o.oid, actor: o.actor.id, purpose: o.declaredPurpose, at: o.createdAt, line: o.line ?? "main" }));
723
+ },
535
724
  },
536
725
  {
537
726
  name: "avcs.diff",
538
- description: "Diff two views/lines: added/removed/modified paths.",
539
- inputSchema: { type: "object", properties: { viewA: { type: "string" }, viewB: { type: "string" } }, required: ["viewA", "viewB"] },
540
- handler: (repo, i) => repo.diff(String(i.viewA), String(i.viewB)),
727
+ description: "Diff two views/lines. format 'paths' (default) or 'patch' (unified diff).",
728
+ inputSchema: {
729
+ type: "object",
730
+ properties: {
731
+ viewA: { type: "string" },
732
+ viewB: { type: "string" },
733
+ format: { type: "string", enum: ["paths", "patch"], description: "'paths' (default, unchanged shape) or 'patch' for unified diffs" },
734
+ path: { type: "string", description: "restrict a patch to this single path" },
735
+ },
736
+ required: ["viewA", "viewB"],
737
+ },
738
+ handler: async (repo, i) => {
739
+ const paths = await repo.diff(String(i.viewA), String(i.viewB));
740
+ if (i.format !== "patch")
741
+ return paths; // default shape is unchanged (compatibility)
742
+ const [a, b] = await Promise.all([repo.materialize(String(i.viewA)), repo.materialize(String(i.viewB))]);
743
+ const only = typeof i.path === "string" ? i.path : undefined;
744
+ const changed = [...paths.added, ...paths.removed, ...paths.modified].sort().filter((p) => (only ? p === only : true));
745
+ // Go through materializedFiles, NOT readBlob: a merged path's tree entry can be a
746
+ // synth oid derived from the merge result, which was never stored as a blob.
747
+ const [fa, fb] = await Promise.all([repo.materializedFiles(a), repo.materializedFiles(b)]);
748
+ const textOf = (files, p) => files.find((f) => f.path === p)?.content ?? "";
749
+ const patches = changed.map((p) => ({ path: p, patch: unifiedDiff(textOf(fa, p), textOf(fb, p)) }));
750
+ return { ...paths, patches };
751
+ },
541
752
  },
542
753
  {
543
754
  name: "avcs.release.cut",
544
- description: "Cut a Release: a verified (conflict-free) checkpoint + its evidence + an SBOM of what shipped + artifact references. Refuses if the view has open or semantic conflicts.",
755
+ description: "Cut a release: a conflict-free checkpoint plus evidence, SBOM and artifact references. Refuses if the view has open conflicts.",
545
756
  inputSchema: {
546
757
  type: "object",
547
758
  properties: {
@@ -566,16 +777,51 @@ export const TOOLS = [
566
777
  },
567
778
  {
568
779
  name: "avcs.object.show",
569
- description: "Read an object by oid the MCP equivalent of CLI `show`. For a blob, returns the decoded content (utf8 `text`, or `base64` if binary); for an operation/intent/etc., the structured object. Lets a pure-MCP agent inspect another agent's authored content, or fetch a base blob to declare as baseBlobOid on an edit_file propose (issue #12).",
570
- inputSchema: { type: "object", properties: { oid: { type: "string" } }, required: ["oid"] },
780
+ description: "Read an object by oid. Blobs return decoded content (utf8 text or base64); other types return the structured object.",
781
+ inputSchema: {
782
+ type: "object",
783
+ properties: {
784
+ oid: { type: "string" },
785
+ maxBytes: { type: "number", description: "cap on returned blob content; default 65536. `bytes` always reports the FULL size." },
786
+ lines: {
787
+ type: "object",
788
+ properties: { start: { type: "number" }, end: { type: "number" } },
789
+ description: "1-based inclusive line range of a text blob to return instead of the whole thing",
790
+ },
791
+ },
792
+ required: ["oid"],
793
+ },
571
794
  handler: async (repo, i) => {
572
795
  const oid = String(i.oid);
573
796
  const obj = await repo.store.get(oid);
574
797
  if (obj.type === "blob") {
575
798
  const buf = await repo.readBlob(oid);
576
- return isBinary(buf)
577
- ? { oid, kind: "blob", encoding: "base64", binary: true, bytes: buf.length, data: buf.toString("base64") }
578
- : { oid, kind: "blob", encoding: "utf8", binary: false, bytes: buf.length, text: buf.toString("utf8") };
799
+ const bytes = buf.length; // ALWAYS the full size, even when the payload is a slice
800
+ if (isBinary(buf)) {
801
+ const maxBytes = boundedLimit(i.maxBytes, 65_536);
802
+ const slice = buf.subarray(0, maxBytes);
803
+ return { oid, kind: "blob", encoding: "base64", binary: true, bytes, truncated: bytes > maxBytes, data: slice.toString("base64") };
804
+ }
805
+ let text = buf.toString("utf8");
806
+ let truncated = false;
807
+ // A line range is the cheaper ask: slice first, then still honour maxBytes.
808
+ const range = i.lines;
809
+ if (range && (range.start !== undefined || range.end !== undefined)) {
810
+ const all = text.split("\n");
811
+ const trailing = text.endsWith("\n");
812
+ const body = trailing ? all.slice(0, -1) : all;
813
+ const start = Math.max(1, Math.floor(range.start ?? 1));
814
+ const end = Math.min(body.length, Math.floor(range.end ?? body.length));
815
+ const picked = body.slice(start - 1, end);
816
+ truncated = picked.length < body.length;
817
+ text = picked.length ? picked.join("\n") + "\n" : "";
818
+ }
819
+ const maxBytes = boundedLimit(i.maxBytes, 65_536);
820
+ if (Buffer.byteLength(text, "utf8") > maxBytes) {
821
+ text = Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
822
+ truncated = true;
823
+ }
824
+ return { oid, kind: "blob", encoding: "utf8", binary: false, bytes, truncated, text };
579
825
  }
580
826
  return { oid, kind: obj.type ?? "object", object: obj };
581
827
  },
@@ -609,11 +855,8 @@ export async function startMcpServer() {
609
855
  tools: TOOLS.map((t) => ({
610
856
  name: t.name,
611
857
  description: t.description,
612
- // Advertise the universal optional `cwd` so per-call repo targeting is discoverable.
613
- inputSchema: {
614
- ...t.inputSchema,
615
- properties: { ...(t.inputSchema.properties ?? {}), cwd: cwdSchema },
616
- },
858
+ // Advertise the universal optional `cwd` and `verbose` so both are discoverable.
859
+ inputSchema: advertisedSchema(t),
617
860
  })),
618
861
  }));
619
862
  // Ask the MCP client for its workspace roots (the protocol-blessed way to learn where the
@@ -677,8 +920,7 @@ export async function startMcpServer() {
677
920
  },
678
921
  };
679
922
  try {
680
- const result = await tool.handler(repo, argsIn, ctx);
681
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
923
+ return await runTool(tool, repo, argsIn, ctx);
682
924
  }
683
925
  finally {
684
926
  inFlight--;