@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.
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -1
- package/dist/mcp/guide.d.ts +5 -0
- package/dist/mcp/guide.d.ts.map +1 -0
- package/dist/mcp/guide.js +69 -0
- package/dist/mcp/guide.js.map +1 -0
- package/dist/mcp/land.d.ts +34 -0
- package/dist/mcp/land.d.ts.map +1 -0
- package/dist/mcp/land.js +153 -0
- package/dist/mcp/land.js.map +1 -0
- package/dist/mcp/respond.d.ts +32 -0
- package/dist/mcp/respond.d.ts.map +1 -0
- package/dist/mcp/respond.js +75 -0
- package/dist/mcp/respond.js.map +1 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +275 -33
- package/dist/mcp/server.js.map +1 -1
- package/dist/merge/merge3.d.ts +13 -0
- package/dist/merge/merge3.d.ts.map +1 -1
- package/dist/merge/merge3.js +1 -1
- package/dist/merge/merge3.js.map +1 -1
- package/dist/query/diff.d.ts +13 -0
- package/dist/query/diff.d.ts.map +1 -1
- package/dist/query/diff.js +61 -0
- package/dist/query/diff.js.map +1 -1
- package/package.json +1 -1
|
@@ -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"}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
|
138
|
-
inputSchema: {
|
|
139
|
-
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
578
|
+
description: "Submit a checkpoint to the integration queue. Verdict is advanced, conflict, needs_evidence or queued — never '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
|
|
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: "
|
|
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
|
|
533
|
-
inputSchema: {
|
|
534
|
-
|
|
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
|
|
539
|
-
inputSchema: {
|
|
540
|
-
|
|
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
|
|
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
|
|
570
|
-
inputSchema: {
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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`
|
|
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
|
-
|
|
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--;
|