@nikovirtala/adr 0.2.0 → 0.4.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.
Files changed (45) hide show
  1. package/README.md +77 -13
  2. package/dist/cli.js +348 -46
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +5 -3
  5. package/dist/init-YgW7g9Fx.js +588 -0
  6. package/dist/init-YgW7g9Fx.js.map +1 -0
  7. package/dist/{kind-BcHjCHvi.js → kind-DjvA3dyA.js} +2 -6
  8. package/dist/{kind-BcHjCHvi.js.map → kind-DjvA3dyA.js.map} +1 -1
  9. package/dist/kind.js +1 -1
  10. package/dist/list-Dt4KGxvj.js +58 -0
  11. package/dist/list-Dt4KGxvj.js.map +1 -0
  12. package/dist/{write-9IaqHRrY.js → search-CwIPlNGd.js} +1339 -560
  13. package/dist/search-CwIPlNGd.js.map +1 -0
  14. package/dist/server-CXCO5zfp.js +938 -0
  15. package/dist/server-CXCO5zfp.js.map +1 -0
  16. package/dist/types/cli/hooks.d.ts +6 -0
  17. package/dist/types/cli/index-command.d.ts +3 -0
  18. package/dist/types/cli/init.d.ts +5 -2
  19. package/dist/types/cli/list.d.ts +1 -1
  20. package/dist/types/cli/mcp.d.ts +6 -0
  21. package/dist/types/cli/open.d.ts +2 -0
  22. package/dist/types/cli/search.d.ts +3 -0
  23. package/dist/types/cli/text.d.ts +12 -0
  24. package/dist/types/commit-actions.d.ts +74 -0
  25. package/dist/types/config.d.ts +7 -1
  26. package/dist/types/hooks.d.ts +59 -0
  27. package/dist/types/index.d.ts +7 -2
  28. package/dist/types/init.d.ts +23 -2
  29. package/dist/types/log-index.d.ts +117 -0
  30. package/dist/types/log.d.ts +44 -1
  31. package/dist/types/mcp/policy.d.ts +33 -0
  32. package/dist/types/mcp/prompt.d.ts +7 -0
  33. package/dist/types/mcp/resources.d.ts +16 -0
  34. package/dist/types/mcp/server.d.ts +16 -0
  35. package/dist/types/mcp/text.d.ts +42 -0
  36. package/dist/types/mcp/tools.d.ts +27 -0
  37. package/dist/types/scope.d.ts +3 -1
  38. package/dist/types/search.d.ts +61 -0
  39. package/dist/types/skill.d.ts +5 -0
  40. package/dist/types/templates/list.d.ts +31 -0
  41. package/dist/types/verify.d.ts +6 -4
  42. package/dist/types/views.d.ts +9 -3
  43. package/dist/types/write.d.ts +65 -9
  44. package/package.json +5 -2
  45. package/dist/write-9IaqHRrY.js.map +0 -1
@@ -0,0 +1,938 @@
1
+ import { I as resolveDecision, N as openDecisionLog, P as readDecisions, _ as withdrawDecision, a as stateHeader, d as proposeDecision, f as rejectDecision, g as updateDraft, h as supersedeDecision, i as showDecision, l as linkDecision, n as decisionsFor, o as acceptDecision, p as scopeDecision, r as listDecisions, s as deprecateDecision, t as searchDecisions, u as noteDecision, y as DECISION_INDEX } from "./search-CwIPlNGd.js";
2
+ import { n as DECISION_STATUSES, t as adrKind } from "./kind-DjvA3dyA.js";
3
+ import { t as listTemplates } from "./list-Dt4KGxvj.js";
4
+ import { MAX_GLOB_LENGTH, UsageError, eventDir, parseOnBehalfOf, renderIndex, renderStateHeader, resolveSubjectInput } from "@nikovirtala/ledger";
5
+ import { mcpAttribution, serveMcp } from "@nikovirtala/ledger/mcp";
6
+ // src/mcp/policy.ts
7
+ /** The tools that decide; listed only with `[policy] mcp_can_decide = true`. */
8
+ const DECISION_TOOLS = [
9
+ "adr_accept",
10
+ "adr_reject",
11
+ "adr_deprecate",
12
+ "adr_supersede"
13
+ ];
14
+ /** Whether `adr mcp` offers the decision tools (design §7.5, Table A). */
15
+ function decisionToolsAvailable(adr) {
16
+ return adr.mcpCanDecide;
17
+ }
18
+ /**
19
+ * Who a proposal is for (design D5): its `proposed` event's actor, `on_behalf_of` for an agent
20
+ * and `email` for a human, lower-cased; `null` when the event or the address is missing.
21
+ */
22
+ function proposalOwner(read, id) {
23
+ const actor = (read.folded.subjects.get(id)?.applied.find((a) => a.event.type === adrKind.machine.initial.type))?.event.actor;
24
+ if (actor === void 0) return null;
25
+ const owner = actor.kind === "agent" ? actor.on_behalf_of : actor.email;
26
+ return typeof owner === "string" && owner.trim() !== "" ? owner.trim().toLowerCase() : null;
27
+ }
28
+ /**
29
+ * The MCP policy (design §7.5, Table B): `null` when the write may go ahead, else the
30
+ * `decision/mcp-cannot-decide` finding. Without `mcp_can_decide` an agent may scope, edit and
31
+ * withdraw only the proposals made for the person it acts for, and rescope no accepted or
32
+ * deprecated decision.
33
+ * Writes the state machine forbids anyway get `null`, so the library refuses them with the real
34
+ * reason.
35
+ */
36
+ function mcpWriteRefusal(o) {
37
+ const { view } = o;
38
+ if (o.tool === "adr_scope" && !adrKind.machine.allowed(view.status, "scoped")) return null;
39
+ if (o.tool === "adr_update_draft" && view.status !== "proposed") return null;
40
+ if (o.tool === "adr_withdraw" && !adrKind.machine.allowed(view.status, "withdrawn")) return null;
41
+ if (o.mcpCanDecide) return null;
42
+ const me = o.actor.on_behalf_of?.trim().toLowerCase() ?? null;
43
+ if (view.status === "proposed" && o.owner !== null && o.owner === me) return null;
44
+ const shown = o.display(view.id);
45
+ const named = shown === view.id ? view.id : `${shown} (${view.id})`;
46
+ const verb = o.tool === "adr_withdraw" ? "withdrawing" : "changing";
47
+ return {
48
+ severity: "error",
49
+ rule: "decision/mcp-cannot-decide",
50
+ message: view.status === "proposed" ? `${named} was proposed for ${o.owner ?? "someone else"}, and you act for ${o.actor.on_behalf_of ?? "nobody"}; ${verb} another person's proposal needs [policy] mcp_can_decide = true. Add a note with adr_note instead.` : `${named} is ${view.status}; rescoping it needs [policy] mcp_can_decide = true, because narrowing a scope can switch a decision off. Ask a human to run adr scope ${shown} …, or propose a superseding decision.`,
51
+ file: view.body_path ?? o.eventsDir,
52
+ line: null,
53
+ subject: view.id,
54
+ kind: adrKind.name
55
+ };
56
+ }
57
+ // src/mcp/resources.ts
58
+ const MAX_ID = 64;
59
+ /**
60
+ * Where an MCP client reads a decision: `adr://decision/ADR-0012` when numbered, else
61
+ * `adr://decision/<id>`. The `refer` of every header the MCP texts show.
62
+ */
63
+ function decisionUri(read) {
64
+ return (id) => `adr://decision/${read.projection.views.get(id)?.display ?? id}`;
65
+ }
66
+ /** `adr://index`: the index table, numbered like every other read (design §7.7). */
67
+ function indexResource(e) {
68
+ return {
69
+ uri: "adr://index",
70
+ name: "index",
71
+ title: "Decision index",
72
+ description: "All decisions, grouped by status, with numbers and dates",
73
+ mimeType: "text/markdown",
74
+ async read() {
75
+ const read = await readDecisions(await openLog(e));
76
+ const views = read.projection.views;
77
+ return {
78
+ header: null,
79
+ text: renderIndex(DECISION_INDEX(read.root, views), [...views.values()], read.numbering)
80
+ };
81
+ }
82
+ };
83
+ }
84
+ /**
85
+ * `adr://decision/{id}`: the decision's state header, then its body. `{id}` is the id, the number
86
+ * (`12`, `0012`) or `ADR-0012`; an unknown one is not found, a malformed or qualified one is the
87
+ * caller's error.
88
+ */
89
+ function decisionResource(e) {
90
+ return {
91
+ uriTemplate: "adr://decision/{id}",
92
+ name: "decision",
93
+ title: "Decision",
94
+ description: "One decision: its state header, then its body. {id} is the id (k3f9q8a1), the number (12) or ADR-0012",
95
+ mimeType: "text/markdown",
96
+ async read({ id = "" }) {
97
+ if (id.length > MAX_ID) throw new UsageError(`invalid decision ${JSON.stringify(`${id.slice(0, MAX_ID)}…`)}: an id or number is at most ${MAX_ID} characters`);
98
+ const read = await readDecisions(await openLog(e));
99
+ const r = resolveSubjectInput(id, {
100
+ subjects: new Set(read.folded.subjects.keys()),
101
+ kind: adrKind,
102
+ numbering: read.numbering
103
+ });
104
+ if (!r.ok && r.reason === "not-found") return null;
105
+ const doc = await showDecision(read, resolveDecision(read, id));
106
+ return {
107
+ header: mcpHeader(doc.decision, read),
108
+ text: doc.body === null ? "(no body file)\n" : doc.body
109
+ };
110
+ }
111
+ };
112
+ }
113
+ // src/mcp/text.ts
114
+ /**
115
+ * The readable text of each MCP tool result (design §7.4, last column). The documents are the
116
+ * CLI's; these texts are MCP's own, and their state headers point at `adr://decision/…`.
117
+ */
118
+ const display = (read) => (id) => read.projection.views.get(id)?.display ?? id;
119
+ function named(read, id) {
120
+ const shown = display(read)(id);
121
+ return shown === id ? id : `${shown} (${id})`;
122
+ }
123
+ /** The decision's state header with MCP's `adr://` references. */
124
+ function mcpHeader(view, read) {
125
+ return stateHeader(view, read, { refer: decisionUri(read) });
126
+ }
127
+ /** `adr_search`: each result's header, title and excerpt, then the counts. */
128
+ function searchText(doc, read) {
129
+ if (doc.results.length === 0) return {
130
+ headers: [],
131
+ text: ["No decisions match.", ...doc.inactive_omitted > 0 ? [`${inactiveLine(doc.inactive_omitted)}; pass include_inactive`] : []].join("\n")
132
+ };
133
+ const lines = [];
134
+ for (const r of doc.results) {
135
+ lines.push(renderStateHeader(mcpHeader(r.decision, read)).trimEnd());
136
+ lines.push(` ${r.decision.title}`);
137
+ const where = r.matched.filter((f) => f === "id" || f === "title").join(" and ");
138
+ lines.push(r.excerpt === null ? ` (${where} match)` : ` ${r.excerpt}`);
139
+ }
140
+ lines.push(`${doc.results.length} of ${doc.total} shown`);
141
+ if (doc.inactive_omitted > 0) lines.push(`${inactiveLine(doc.inactive_omitted)}; pass include_inactive`);
142
+ return {
143
+ headers: [],
144
+ text: lines.join("\n")
145
+ };
146
+ }
147
+ const inactiveLine = (k) => k === 1 ? "1 inactive decision also matches" : `${k} inactive decisions also match`;
148
+ function listOf(read, ids) {
149
+ return ids.length === 0 ? "none" : ids.map((id) => named(read, id)).join(", ");
150
+ }
151
+ function historyLines(events) {
152
+ return events.map((e) => {
153
+ const a = e.actor;
154
+ const who = a.kind === "agent" ? `${a.name} (agent for ${a.on_behalf_of})` : a.name;
155
+ const why = typeof e.reason === "string" ? ` — ${e.reason}` : "";
156
+ return ` ${e.at} ${e.type} ${who}${why}`;
157
+ });
158
+ }
159
+ /** `adr_get`: the header, the decision's fields as `adr show` lists them, then the body. */
160
+ function getText(doc, read) {
161
+ const v = doc.decision;
162
+ const lines = [
163
+ `Title: ${v.title}`,
164
+ `Status: ${v.status} (${v.status_at.slice(0, 10)})`,
165
+ `Effective: ${v.effective === null ? "none (supersession cycle)" : named(read, v.effective)}`,
166
+ `Chain: ${v.chain.map(display(read)).join(" → ")}`,
167
+ `Supersedes: ${listOf(read, v.supersedes)}`,
168
+ `Applies to: ${v.applies_to.length === 0 ? "no globs recorded" : v.applies_to.join(", ")}`
169
+ ];
170
+ if (doc.events !== null) lines.push("History:", ...historyLines(doc.events));
171
+ lines.push("", doc.body === null ? "(no body file)" : doc.body.trimEnd());
172
+ return {
173
+ headers: [mcpHeader(v, read)],
174
+ text: lines.join("\n")
175
+ };
176
+ }
177
+ /** `adr_list`: one line per decision. */
178
+ function listText(doc, read) {
179
+ if (doc.decisions.length === 0) return {
180
+ headers: [],
181
+ text: "No decisions."
182
+ };
183
+ return {
184
+ headers: [],
185
+ text: doc.decisions.map((v) => {
186
+ const head = v.number === null ? v.id : `${v.display} (${v.id})`;
187
+ const to = v.effective !== null && v.effective !== v.id ? ` → ${display(read)(v.effective)}` : "";
188
+ return `${head} · ${v.status} · ${v.title}${to}`;
189
+ }).join("\n")
190
+ };
191
+ }
192
+ /** `adr_templates`: each template, then its sections. */
193
+ function templatesText(doc) {
194
+ const lines = doc.templates.flatMap((t) => [`${t.name} v${t.version} (${t.source})${t.name === doc.default ? " default" : ""}`, ...t.sections.map((s) => ` ${s.id}: ${s.heading} (${s.required ? "required" : "optional"})${s.guidance === null ? "" : ` — ${s.guidance}`}`)]);
195
+ return {
196
+ headers: [],
197
+ text: lines.length === 0 ? "No templates." : lines.join("\n")
198
+ };
199
+ }
200
+ /** `adr_for_path`: per path, each decision's header and how it matched. */
201
+ function forText(doc, read) {
202
+ const lines = [];
203
+ for (const p of doc.paths) {
204
+ lines.push(p.tracked ? p.path : `${p.path} (not tracked)`);
205
+ if (p.decisions.length === 0 && p.unresolved.length === 0) lines.push(" no decisions");
206
+ for (const d of p.decisions) {
207
+ const view = read.projection.views.get(d.id);
208
+ const h = view === void 0 ? {
209
+ line: `${named(read, d.id)} · ${d.status}`,
210
+ warning: d.warning
211
+ } : mcpHeader(view, read);
212
+ lines.push(` ${h.line}`);
213
+ if (h.warning !== null) lines.push(` ${h.warning}`);
214
+ for (const via of d.via) lines.push(via.type === "glob" ? ` via glob ${via.glob}` : ` via anchor line ${via.line}`);
215
+ }
216
+ for (const u of p.unresolved) {
217
+ const why = u.reason === "other-log" ? "in another log" : "not in this log";
218
+ lines.push(` unresolved: ${u.ref} (line ${u.line}, ${why})`);
219
+ }
220
+ }
221
+ return {
222
+ headers: [],
223
+ text: lines.join("\n")
224
+ };
225
+ }
226
+ const SECTION_FINDING = /^(?:required )?section (.+?) (is missing|is empty|still contains its guidance text)$/;
227
+ const REASONS = {
228
+ "is missing": "missing",
229
+ "is empty": "empty",
230
+ "still contains its guidance text": "guidance"
231
+ };
232
+ /** Lint findings as `Fill in: <heading> (<reason>)`, others as `Draft: <message> [<rule>]`. */
233
+ function lintLines(findings) {
234
+ return findings.map((f) => {
235
+ const m = SECTION_FINDING.exec(f.message);
236
+ if (m === null) return `Draft: ${f.message} [${f.rule}]`;
237
+ const [, heading = "", what = ""] = m;
238
+ return `Fill in: ${heading} (${REASONS[what] ?? what})`;
239
+ });
240
+ }
241
+ const noteLines = (doc) => doc.notes.map((n) => `Note: ${n}`);
242
+ /** A write's text: `Refused:` and the findings, or what was written; then the notes. */
243
+ function writeText(doc, o) {
244
+ const { read } = o;
245
+ const view = doc.decision === null ? null : read.projection.views.get(doc.decision.id) ?? doc.decision;
246
+ const headers = view === null ? [] : [mcpHeader(view, read), ...affectedHeaders(doc, read)];
247
+ if (!doc.ok) {
248
+ const again = doc.findings.length > 0 && doc.findings.every((f) => f.rule.startsWith("template/")) && (o.tool === "adr_propose" || o.tool === "adr_update_draft") ? [`Fill in every required section and call ${o.tool} again.`] : [];
249
+ return {
250
+ headers,
251
+ text: [
252
+ "Refused:",
253
+ ...doc.findings.map((f) => ` ${f.file === null ? "" : `${f.file}: `}${f.message} [${f.rule}]`),
254
+ ...again,
255
+ ...noteLines(doc)
256
+ ].join("\n")
257
+ };
258
+ }
259
+ if (view === null) return {
260
+ headers,
261
+ text: noteLines(doc).join("\n")
262
+ };
263
+ return {
264
+ headers,
265
+ text: [...bodyLines(doc, view, o), ...noteLines(doc)].join("\n")
266
+ };
267
+ }
268
+ function bodyLines(doc, v, o) {
269
+ const { read } = o;
270
+ const who = named(read, v.id);
271
+ const recorded = doc.commit === null ? [] : [`Committed ${doc.commit.slice(0, 7)}.`];
272
+ switch (doc.command) {
273
+ case "new": return [
274
+ `Proposed ${v.id}: ${v.title}`,
275
+ ...v.body_path === null ? [] : [` ${v.body_path}`],
276
+ ...lintLines(doc.findings),
277
+ ...recorded,
278
+ o.acceptRequiresHuman === true ? `Next: ask a human to review and accept it (adr accept ${v.id})` : `Next: adr_accept or adr accept ${v.id}`
279
+ ];
280
+ case "update": return doc.written.length === 0 ? ["Unchanged"] : [
281
+ `Updated the draft of ${who}: ${(o.sections ?? []).join(", ")}`,
282
+ ...lintLines(doc.findings),
283
+ ...recorded
284
+ ];
285
+ case "note": return [`Noted on ${who}`, ...recorded];
286
+ case "link": {
287
+ const link = doc.written.length === 0 ? void 0 : v.links.at(-1);
288
+ if (link === void 0) return [`Links of ${who} unchanged`];
289
+ return [`Linked ${who} ${link.rel} ${named(read, link.to)}`, ...recorded];
290
+ }
291
+ case "scope": {
292
+ const before = new Set(o.scopeBefore ?? []);
293
+ const after = new Set(v.applies_to);
294
+ if (doc.written.length === 0) return ["unchanged"];
295
+ return [
296
+ ...v.applies_to.filter((g) => !before.has(g)).map((g) => `+ ${g}`),
297
+ ...[...before].filter((g) => !after.has(g)).map((g) => `- ${g}`),
298
+ `= ${v.applies_to.length === 0 ? "no globs" : v.applies_to.join(", ")}`,
299
+ ...recorded
300
+ ];
301
+ }
302
+ case "accept": return [
303
+ `Accepted ${who}: ${v.title}`,
304
+ ...doc.affected.map((a) => `Superseded ${named(read, a.id)}: ${a.title}`),
305
+ ...recorded
306
+ ];
307
+ case "reject": return [`Rejected ${who}`, ...recorded];
308
+ case "withdraw": return [`Withdrawn ${who}`, ...recorded];
309
+ case "deprecate": return [`Deprecated ${who}`, ...recorded];
310
+ case "supersede": return [`Superseded ${who} by ${named(read, v.superseded_by ?? "")}`, ...recorded];
311
+ default: return [`${doc.command} ${who}`, ...recorded];
312
+ }
313
+ }
314
+ /** The headers of the decisions a write affected (superseded), other than its own. */
315
+ function affectedHeaders(doc, read) {
316
+ return doc.affected.filter((a) => a.id !== doc.decision?.id).map((a) => mcpHeader(read.projection.views.get(a.id) ?? a, read));
317
+ }
318
+ // src/mcp/tools.ts
319
+ const ID = {
320
+ type: "string",
321
+ minLength: 1,
322
+ maxLength: 64
323
+ };
324
+ const TEXT = {
325
+ type: "string",
326
+ minLength: 1,
327
+ maxLength: 65536
328
+ };
329
+ const SECTIONS = {
330
+ type: "object",
331
+ propertyNames: {
332
+ type: "string",
333
+ pattern: "^[a-z][a-z0-9-]*$",
334
+ maxLength: 64
335
+ },
336
+ additionalProperties: {
337
+ type: "string",
338
+ maxLength: 65536
339
+ },
340
+ maxProperties: 50
341
+ };
342
+ const GLOBS = {
343
+ type: "array",
344
+ items: {
345
+ type: "string",
346
+ minLength: 1,
347
+ maxLength: MAX_GLOB_LENGTH
348
+ },
349
+ maxItems: 200
350
+ };
351
+ const object = (properties, required = []) => ({
352
+ type: "object",
353
+ properties,
354
+ ...required.length === 0 ? {} : { required },
355
+ additionalProperties: false
356
+ });
357
+ const READ = {
358
+ readOnlyHint: true,
359
+ openWorldHint: false
360
+ };
361
+ const WRITE = {
362
+ readOnlyHint: false,
363
+ destructiveHint: false,
364
+ openWorldHint: false
365
+ };
366
+ /** The log for one call, opened again so outside edits and `[policy]` changes are seen. */
367
+ const openLog = (e, ref = null) => openDecisionLog({
368
+ cwd: e.cwd,
369
+ root: e.root,
370
+ ref,
371
+ gitOptions: { env: e.env }
372
+ });
373
+ const str = (v) => typeof v === "string" ? v : void 0;
374
+ const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
375
+ const sectionsOf = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).flatMap(([k, x]) => typeof x === "string" ? [[k, x]] : [])) : {};
376
+ const doc = (d) => d;
377
+ /**
378
+ * A write tool's result: the library's document, and its text rendered from a read of the log
379
+ * after the write.
380
+ */
381
+ async function writeResult(log, d, o) {
382
+ const read = await readDecisions(log);
383
+ return {
384
+ document: doc(d),
385
+ ...writeText(d, {
386
+ ...o,
387
+ read
388
+ })
389
+ };
390
+ }
391
+ /** The `adr-write/v1` command of each policy tool, as the CLI names it. */
392
+ const COMMAND = {
393
+ adr_scope: "scope",
394
+ adr_update_draft: "update",
395
+ adr_withdraw: "withdraw"
396
+ };
397
+ /**
398
+ * The MCP policy step of `adr_scope`, `adr_update_draft` and `adr_withdraw` (design §7.5): the refusal document
399
+ * when the agent may not make this write, else `null`.
400
+ */
401
+ function policyRefusal(log, read, tool, id, attribution) {
402
+ const view = read.projection.views.get(id);
403
+ if (view === void 0) return null;
404
+ const finding = mcpWriteRefusal({
405
+ tool,
406
+ view,
407
+ owner: proposalOwner(read, id),
408
+ actor: attribution.actor,
409
+ mcpCanDecide: log.adr.mcpCanDecide,
410
+ display: (x) => read.projection.views.get(x)?.display ?? x,
411
+ eventsDir: eventDir(read.layout, id)
412
+ });
413
+ if (finding === null) return null;
414
+ return {
415
+ format: "adr-write/v1",
416
+ command: COMMAND[tool],
417
+ ok: false,
418
+ decision: view,
419
+ affected: [],
420
+ written: [],
421
+ commit: null,
422
+ findings: [finding],
423
+ notes: [...log.notes, ...attribution.notes]
424
+ };
425
+ }
426
+ /** The eleven tools `adr mcp` always lists (design §7.4). */
427
+ function alwaysOnTools(e) {
428
+ return [
429
+ {
430
+ name: "adr_search",
431
+ title: "Search decisions",
432
+ description: "Full-text search of the decision log: every word must appear in a decision's id, title, body or notes. Returns active (proposed or accepted) decisions unless include_inactive is true, each with its state header and an excerpt.",
433
+ inputSchema: object({
434
+ query: {
435
+ type: "string",
436
+ minLength: 1,
437
+ maxLength: 200
438
+ },
439
+ include_inactive: { type: "boolean" },
440
+ limit: {
441
+ type: "integer",
442
+ minimum: 1,
443
+ maximum: 100
444
+ }
445
+ }, ["query"]),
446
+ annotations: READ,
447
+ async run(input) {
448
+ const read = await readDecisions(await openLog(e));
449
+ const d = searchDecisions(read, {
450
+ query: str(input.query) ?? "",
451
+ includeInactive: input.include_inactive === true,
452
+ limit: typeof input.limit === "number" ? input.limit : 10
453
+ });
454
+ return {
455
+ document: doc(d),
456
+ ...searchText(d, read)
457
+ };
458
+ }
459
+ },
460
+ {
461
+ name: "adr_get",
462
+ title: "Get a decision",
463
+ description: "One decision with its state header, computed status, supersession chain, effective decision and body. Use this or adr://decision/{id} instead of reading files under docs/adr. A SUPERSEDED or DEPRECATED header means: don't apply it, follow the warning.",
464
+ inputSchema: object({
465
+ id: ID,
466
+ include_history: { type: "boolean" },
467
+ ref: {
468
+ type: "string",
469
+ minLength: 1,
470
+ maxLength: 256
471
+ }
472
+ }, ["id"]),
473
+ annotations: READ,
474
+ async run(input) {
475
+ const read = await readDecisions(await openLog(e, str(input.ref) ?? null));
476
+ const id = resolveDecision(read, str(input.id) ?? "");
477
+ const d = await showDecision(read, id, { history: input.include_history === true });
478
+ return {
479
+ document: doc(d),
480
+ ...getText(d, read)
481
+ };
482
+ }
483
+ },
484
+ {
485
+ name: "adr_list",
486
+ title: "List decisions",
487
+ description: "All decisions with their computed status, for orientation. Filter by status; limit truncates.",
488
+ inputSchema: object({
489
+ status: {
490
+ type: "array",
491
+ items: {
492
+ type: "string",
493
+ enum: [...DECISION_STATUSES]
494
+ },
495
+ uniqueItems: true,
496
+ minItems: 1,
497
+ maxItems: DECISION_STATUSES.length
498
+ },
499
+ limit: {
500
+ type: "integer",
501
+ minimum: 1,
502
+ maximum: 1e3
503
+ }
504
+ }),
505
+ annotations: READ,
506
+ async run(input) {
507
+ const read = await readDecisions(await openLog(e));
508
+ const status = strings(input.status);
509
+ const d = listDecisions(read, {
510
+ ...status === void 0 ? {} : { status },
511
+ ...typeof input.limit === "number" ? { limit: input.limit } : {}
512
+ });
513
+ return {
514
+ document: doc(d),
515
+ ...listText(d, read)
516
+ };
517
+ }
518
+ },
519
+ {
520
+ name: "adr_templates",
521
+ title: "List templates",
522
+ description: "The templates adr_propose can use, with each section's id, heading, whether it is required, and guidance for filling it in.",
523
+ inputSchema: object({}),
524
+ annotations: READ,
525
+ async run() {
526
+ const d = await listTemplates(await readDecisions(await openLog(e)));
527
+ return {
528
+ document: doc(d),
529
+ ...templatesText(d)
530
+ };
531
+ }
532
+ },
533
+ {
534
+ name: "adr_propose",
535
+ title: "Propose a decision",
536
+ description: "Propose a decision: give the title and a sections map (section id → markdown) for the template; the tool renders and stages the draft. Lint findings come back in findings: fix them with adr_update_draft. With auto_commit on, an incomplete draft is refused; send every required section. You can't accept your own proposal; tell the user its id.",
537
+ inputSchema: object({
538
+ title: {
539
+ type: "string",
540
+ minLength: 1,
541
+ maxLength: 200
542
+ },
543
+ template: {
544
+ type: "string",
545
+ pattern: "^[a-z][a-z0-9-]*$",
546
+ maxLength: 64
547
+ },
548
+ sections: SECTIONS,
549
+ supersedes: {
550
+ type: "array",
551
+ items: ID,
552
+ maxItems: 20
553
+ }
554
+ }, ["title"]),
555
+ annotations: WRITE,
556
+ async run(input, ctx) {
557
+ const log = await openLog(e);
558
+ const { actor, notes } = await mcpAttribution(log.git, ctx);
559
+ const template = str(input.template);
560
+ const supersedes = strings(input.supersedes);
561
+ return writeResult(log, await proposeDecision(log, {
562
+ title: str(input.title) ?? "",
563
+ sections: sectionsOf(input.sections),
564
+ actor,
565
+ ...template === void 0 ? {} : { template },
566
+ ...supersedes === void 0 ? {} : { supersedes }
567
+ }, { notes }), {
568
+ tool: "adr_propose",
569
+ acceptRequiresHuman: log.adr.acceptRequiresHuman
570
+ });
571
+ }
572
+ },
573
+ {
574
+ name: "adr_update_draft",
575
+ title: "Update a draft",
576
+ description: "Replace sections of a proposed decision's draft (section id → markdown). Only your own proposals unless mcp_can_decide is on. Returns the lint result. With auto_commit on, an incomplete draft is refused; send every required section.",
577
+ inputSchema: object({
578
+ id: ID,
579
+ sections: {
580
+ ...SECTIONS,
581
+ minProperties: 1
582
+ }
583
+ }, ["id", "sections"]),
584
+ annotations: WRITE,
585
+ async run(input, ctx) {
586
+ const log = await openLog(e);
587
+ const read = await readDecisions(log);
588
+ const id = resolveDecision(read, str(input.id) ?? "");
589
+ const attribution = await mcpAttribution(log.git, ctx);
590
+ const sections = sectionsOf(input.sections);
591
+ const o = {
592
+ tool: "adr_update_draft",
593
+ sections: Object.keys(sections)
594
+ };
595
+ const refused = policyRefusal(log, read, "adr_update_draft", id, attribution);
596
+ if (refused !== null) return writeResult(log, refused, o);
597
+ return writeResult(log, await updateDraft(log, {
598
+ id,
599
+ sections
600
+ }, { notes: attribution.notes }), o);
601
+ }
602
+ },
603
+ {
604
+ name: "adr_note",
605
+ title: "Note on a decision",
606
+ description: "Add a note to a decision in any state. Notes clarify; a changed decision needs a superseding proposal.",
607
+ inputSchema: object({
608
+ id: ID,
609
+ text: TEXT
610
+ }, ["id", "text"]),
611
+ annotations: WRITE,
612
+ async run(input, ctx) {
613
+ const log = await openLog(e);
614
+ const { actor, notes } = await mcpAttribution(log.git, ctx);
615
+ return writeResult(log, await noteDecision(log, {
616
+ id: str(input.id) ?? "",
617
+ text: str(input.text) ?? "",
618
+ actor
619
+ }, { notes }), { tool: "adr_note" });
620
+ }
621
+ },
622
+ {
623
+ name: "adr_link",
624
+ title: "Link decisions",
625
+ description: "Record a relation (relates-to, refines, depends-on) from one decision to another.",
626
+ inputSchema: object({
627
+ id: ID,
628
+ to: {
629
+ type: "string",
630
+ minLength: 1,
631
+ maxLength: 100
632
+ },
633
+ rel: {
634
+ type: "string",
635
+ enum: [
636
+ "relates-to",
637
+ "refines",
638
+ "depends-on"
639
+ ]
640
+ }
641
+ }, ["id", "to"]),
642
+ annotations: WRITE,
643
+ async run(input, ctx) {
644
+ const log = await openLog(e);
645
+ const { actor, notes } = await mcpAttribution(log.git, ctx);
646
+ const rel = str(input.rel);
647
+ return writeResult(log, await linkDecision(log, {
648
+ id: str(input.id) ?? "",
649
+ to: str(input.to) ?? "",
650
+ actor,
651
+ ...rel === void 0 ? {} : { rel }
652
+ }, { notes }), { tool: "adr_link" });
653
+ }
654
+ },
655
+ {
656
+ name: "adr_for_path",
657
+ title: "Decisions for paths",
658
+ description: "The decisions that govern each file, from scope globs and code anchors. Inactive ones carry a warning.",
659
+ inputSchema: object({ paths: {
660
+ type: "array",
661
+ items: {
662
+ type: "string",
663
+ minLength: 1,
664
+ maxLength: 4096
665
+ },
666
+ minItems: 1,
667
+ maxItems: 100
668
+ } }, ["paths"]),
669
+ annotations: READ,
670
+ async run(input) {
671
+ const log = await openLog(e);
672
+ const read = await readDecisions(log);
673
+ const d = await decisionsFor(read, log, { paths: strings(input.paths) ?? [] });
674
+ return {
675
+ document: doc(d),
676
+ ...forText(d, read)
677
+ };
678
+ }
679
+ },
680
+ {
681
+ name: "adr_scope",
682
+ title: "Scope a decision",
683
+ description: "Set, add or remove the globs that map a decision to code. Allowed on your own proposals; rescoping someone else's proposal or an accepted or deprecated decision needs mcp_can_decide. If the decision's Scope section would no longer read true, propose a superseding decision instead.",
684
+ inputSchema: {
685
+ ...object({
686
+ id: ID,
687
+ set: GLOBS,
688
+ add: GLOBS,
689
+ remove: GLOBS
690
+ }, ["id"]),
691
+ minProperties: 2
692
+ },
693
+ inputHint: "adr_scope needs id and at least one of set, add or remove",
694
+ annotations: WRITE,
695
+ async run(input, ctx) {
696
+ const log = await openLog(e);
697
+ const read = await readDecisions(log);
698
+ const id = resolveDecision(read, str(input.id) ?? "");
699
+ const attribution = await mcpAttribution(log.git, ctx);
700
+ const o = {
701
+ tool: "adr_scope",
702
+ scopeBefore: read.projection.views.get(id)?.applies_to ?? []
703
+ };
704
+ const refused = policyRefusal(log, read, "adr_scope", id, attribution);
705
+ if (refused !== null) return writeResult(log, refused, o);
706
+ const set = strings(input.set);
707
+ const op = {
708
+ ...set === void 0 ? {} : { set },
709
+ add: strings(input.add) ?? [],
710
+ remove: strings(input.remove) ?? []
711
+ };
712
+ return writeResult(log, await scopeDecision(log, {
713
+ id,
714
+ op,
715
+ actor: attribution.actor
716
+ }, { notes: attribution.notes }), o);
717
+ }
718
+ },
719
+ {
720
+ name: "adr_withdraw",
721
+ title: "Withdraw a proposal",
722
+ description: "Withdraw a proposed decision, with an optional reason, for example your own proposal when the user decides against it. Withdrawing someone else's proposal needs mcp_can_decide. Only proposals can be withdrawn; an accepted decision needs a superseding proposal.",
723
+ inputSchema: object({
724
+ id: ID,
725
+ reason: TEXT
726
+ }, ["id"]),
727
+ annotations: WRITE,
728
+ async run(input, ctx) {
729
+ const log = await openLog(e);
730
+ const read = await readDecisions(log);
731
+ const id = resolveDecision(read, str(input.id) ?? "");
732
+ const attribution = await mcpAttribution(log.git, ctx);
733
+ const o = { tool: "adr_withdraw" };
734
+ const refused = policyRefusal(log, read, "adr_withdraw", id, attribution);
735
+ if (refused !== null) return writeResult(log, refused, o);
736
+ return writeResult(log, await withdrawDecision(log, {
737
+ id,
738
+ reason: str(input.reason) ?? null,
739
+ actor: attribution.actor
740
+ }, { notes: attribution.notes }), o);
741
+ }
742
+ }
743
+ ];
744
+ }
745
+ /**
746
+ * The log for a decision tool's call. The tool list was fixed at startup, so a call after
747
+ * `mcp_can_decide` was turned off is a usage error telling the user to restart (design §7.3).
748
+ */
749
+ async function decidingLog(e) {
750
+ const log = await openLog(e);
751
+ if (!log.adr.mcpCanDecide) throw new UsageError(`[policy] mcp_can_decide is now false in ${log.configPath ?? "adr.toml (not present)"}; restart adr mcp to update its tool list`);
752
+ return log;
753
+ }
754
+ /** The four tools `adr mcp` lists only with `[policy] mcp_can_decide = true` (design §7.4). */
755
+ function decisionTools(e) {
756
+ return [
757
+ {
758
+ name: "adr_accept",
759
+ title: "Accept a decision",
760
+ description: "Accept a proposed decision (lints it, freezes the body, supersedes what it declares). Refused for agents while accept_requires_human is on.",
761
+ inputSchema: object({
762
+ id: ID,
763
+ copy_scope: { type: "boolean" }
764
+ }, ["id"]),
765
+ annotations: WRITE,
766
+ async run(input, ctx) {
767
+ const log = await decidingLog(e);
768
+ const { actor, notes } = await mcpAttribution(log.git, ctx);
769
+ const copyScope = input.copy_scope === true ? "copy" : input.copy_scope === false ? "skip" : "suggest";
770
+ return writeResult(log, await acceptDecision(log, {
771
+ id: str(input.id) ?? "",
772
+ actor
773
+ }, {
774
+ notes,
775
+ copyScope
776
+ }), { tool: "adr_accept" });
777
+ }
778
+ },
779
+ {
780
+ name: "adr_reject",
781
+ title: "Reject a decision",
782
+ description: "Reject a proposed decision, with a reason.",
783
+ inputSchema: object({
784
+ id: ID,
785
+ reason: TEXT
786
+ }, ["id", "reason"]),
787
+ annotations: WRITE,
788
+ async run(input, ctx) {
789
+ const log = await decidingLog(e);
790
+ const { actor, notes } = await mcpAttribution(log.git, ctx);
791
+ return writeResult(log, await rejectDecision(log, {
792
+ id: str(input.id) ?? "",
793
+ reason: str(input.reason) ?? "",
794
+ actor
795
+ }, { notes }), { tool: "adr_reject" });
796
+ }
797
+ },
798
+ {
799
+ name: "adr_deprecate",
800
+ title: "Deprecate a decision",
801
+ description: "Deprecate an accepted decision that nothing replaces, with a reason.",
802
+ inputSchema: object({
803
+ id: ID,
804
+ reason: TEXT
805
+ }, ["id", "reason"]),
806
+ annotations: WRITE,
807
+ async run(input, ctx) {
808
+ const log = await decidingLog(e);
809
+ const { actor, notes } = await mcpAttribution(log.git, ctx);
810
+ return writeResult(log, await deprecateDecision(log, {
811
+ id: str(input.id) ?? "",
812
+ reason: str(input.reason) ?? "",
813
+ actor
814
+ }, { notes }), { tool: "adr_deprecate" });
815
+ }
816
+ },
817
+ {
818
+ name: "adr_supersede",
819
+ title: "Supersede a decision",
820
+ description: "Mark an accepted or deprecated decision superseded by an accepted one.",
821
+ inputSchema: object({
822
+ id: ID,
823
+ by: ID,
824
+ reason: TEXT
825
+ }, ["id", "by"]),
826
+ annotations: WRITE,
827
+ async run(input, ctx) {
828
+ const log = await decidingLog(e);
829
+ const { actor, notes } = await mcpAttribution(log.git, ctx);
830
+ const reason = str(input.reason);
831
+ return writeResult(log, await supersedeDecision(log, {
832
+ id: str(input.id) ?? "",
833
+ by: str(input.by) ?? "",
834
+ actor,
835
+ ...reason === void 0 ? {} : { reason }
836
+ }, { notes }), { tool: "adr_supersede" });
837
+ }
838
+ }
839
+ ];
840
+ }
841
+ // src/mcp/prompt.ts
842
+ /**
843
+ * `adr_draft_from_discussion` (design §7.8): the steps from a discussion to a proposal, with the
844
+ * template's sections spelled out, ending with a human's `adr accept`.
845
+ */
846
+ function draftPrompt(e) {
847
+ return {
848
+ name: "adr_draft_from_discussion",
849
+ title: "Draft a decision from the discussion",
850
+ description: "Turn the current discussion into a proposed architecture decision, checking the existing ones first.",
851
+ arguments: [
852
+ {
853
+ name: "title",
854
+ description: "Working title of the decision",
855
+ required: false
856
+ },
857
+ {
858
+ name: "template",
859
+ description: "Template name; default [log] default_template",
860
+ required: false
861
+ },
862
+ {
863
+ name: "supersedes",
864
+ description: "A decision this proposal replaces: id or number",
865
+ required: false
866
+ }
867
+ ],
868
+ async get(args) {
869
+ const read = await readDecisions(await openLog(e));
870
+ const templates = await listTemplates(read);
871
+ const name = args.template ?? templates.default;
872
+ const t = templates.templates.find((x) => x.name === name);
873
+ if (t === void 0) throw new UsageError(`unknown template ${name}; adr_templates lists them`);
874
+ const replaced = args.supersedes === void 0 ? null : resolveDecision(read, args.supersedes);
875
+ const shown = (id) => {
876
+ const display = read.projection.views.get(id)?.display ?? id;
877
+ return display === id ? id : `${display} (${id})`;
878
+ };
879
+ const title = args.title === void 0 ? "a short title that states the decision, such as \"Use PostgreSQL for orders\"" : JSON.stringify(args.title);
880
+ return [
881
+ "Turn the current discussion into an architecture decision proposal with adr.",
882
+ "",
883
+ "1. Call adr_search with the main subject of the discussion, and adr_for_path for the files it concerns. If an accepted decision already covers this, tell the user which one, and ask whether the proposal should supersede it.",
884
+ ...replaced === null ? [] : [`1a. The user named ${shown(replaced)} as the decision this replaces; read it with adr_get first.`],
885
+ `2. Use the template ${t.name} (version ${t.version}). Its sections:`,
886
+ ...t.sections.map((s) => ` - ${s.id}: ${s.heading}, ${s.required ? "required" : "optional"}.${s.guidance === null ? "" : ` ${s.guidance}`}`),
887
+ "3. Fill every required section with facts from the discussion and the code only. Where something wasn't settled, write it as an open question in that section instead of inventing an answer. If the template has an options section, list at least two options with their trade-offs, including those the discussion rejected.",
888
+ `4. Call adr_propose with the title ${title}, template "${t.name}", the sections map${replaced === null ? "" : `, and supersedes ["${replaced}"]`}.`,
889
+ "5. If the result has ok: true with findings, fix those sections with adr_update_draft and the new decision's id. If it was refused (ok: false), fill in every section the findings name and call adr_propose again.",
890
+ "6. Don't accept the proposal. Tell the user its id, and that they can review it and run: adr accept <id>"
891
+ ].join("\n");
892
+ }
893
+ };
894
+ }
895
+ // src/mcp/server.ts
896
+ const INSTRUCTIONS = "Architecture decisions in this repository are managed by adr. Decision files don't show their status: read decisions with adr_get or adr://decision/{id}, check adr_for_path and adr_search before changing architecture, and propose changes with adr_propose; a human accepts them.";
897
+ /**
898
+ * `adr mcp` (design §7.2): checks `ARK_ON_BEHALF_OF` and opens the log first, so a bad
899
+ * environment exits 2 before serving, then serves adr's tools over stdio until the client goes.
900
+ */
901
+ async function serveAdrMcp(o) {
902
+ parseOnBehalfOf(o.env.ARK_ON_BEHALF_OF);
903
+ const log = await openDecisionLog({
904
+ cwd: o.cwd,
905
+ root: o.root,
906
+ gitOptions: { env: o.env }
907
+ });
908
+ const e = {
909
+ cwd: o.cwd,
910
+ root: o.root,
911
+ env: o.env
912
+ };
913
+ const decide = decisionToolsAvailable(log.adr);
914
+ const tools = [...alwaysOnTools(e), ...decide ? decisionTools(e) : []];
915
+ const unavailable = {};
916
+ if (decide && log.adr.acceptRequiresHuman) o.stderr.write("adr: warning: adr_accept refuses agents while [policy] accept_requires_human is true\n");
917
+ if (!decide) {
918
+ const where = log.configPath ?? "adr.toml (not present)";
919
+ for (const name of DECISION_TOOLS) unavailable[name] = `[policy] mcp_can_decide is false in ${where}; a human decides with the adr CLI, or set it to true and restart adr mcp`;
920
+ }
921
+ await serveMcp({
922
+ name: "adr",
923
+ version: o.version,
924
+ instructions: INSTRUCTIONS,
925
+ tools,
926
+ unavailable,
927
+ resources: [indexResource(e)],
928
+ resourceTemplates: [decisionResource(e)],
929
+ prompts: [draftPrompt(e)]
930
+ }, {
931
+ env: o.env,
932
+ stderr: o.stderr,
933
+ ...o.transport === void 0 ? {} : { transport: o.transport }
934
+ });
935
+ }
936
+ export { serveAdrMcp };
937
+
938
+ //# sourceMappingURL=server-CXCO5zfp.js.map