@mmerterden/multi-agent-pipeline 20.29.0 → 20.33.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 (227) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +50 -5
  3. package/README.tr.md +64 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +9 -9
  7. package/install/_unattended-profile.mjs +61 -0
  8. package/install/catalog-history.json +1 -1
  9. package/install/claude.mjs +19 -3
  10. package/install/codex.mjs +2 -0
  11. package/install/copilot.mjs +13 -0
  12. package/install/index.mjs +10 -1
  13. package/install/templates/claude-hooks.json +12 -1
  14. package/manifest.json +230 -88
  15. package/package.json +3 -1
  16. package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
  17. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
  18. package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
  19. package/pipeline/commands/multi-agent/office-on/SKILL.md +51 -0
  20. package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
  21. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
  22. package/pipeline/commands/multi-agent/review/SKILL.md +4 -3
  23. package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
  24. package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
  25. package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
  26. package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
  27. package/pipeline/commands/multi-agent/sync/SKILL.md +15 -3
  28. package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
  29. package/pipeline/contract/CHANGELOG.md +108 -0
  30. package/pipeline/contract/README.md +72 -12
  31. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  32. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  33. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  34. package/pipeline/contract/fixtures/launch-plan.json +1 -1
  35. package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
  36. package/pipeline/contract/fixtures/runs-empty.json +1 -1
  37. package/pipeline/contract/fixtures/runs-failed.json +6 -1
  38. package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
  39. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
  40. package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
  41. package/pipeline/contract/fixtures/runs-running.json +6 -1
  42. package/pipeline/contract/manifest.json +506 -27
  43. package/pipeline/contract/types/index.d.ts +829 -4
  44. package/pipeline/lib/bg-launch.mjs +106 -0
  45. package/pipeline/lib/code-digest.mjs +49 -0
  46. package/pipeline/lib/credential-resolve.mjs +42 -2
  47. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -4
  48. package/pipeline/multi-agent-refs/features/office-threat-model.md +109 -0
  49. package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
  50. package/pipeline/multi-agent-refs/help/en.md +3 -0
  51. package/pipeline/multi-agent-refs/help/tr.md +3 -0
  52. package/pipeline/multi-agent-refs/phases/modes.md +7 -7
  53. package/pipeline/multi-agent-refs/phases/phase-5-report.md +16 -10
  54. package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
  55. package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
  56. package/pipeline/office/NOTICE.md +45 -0
  57. package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
  58. package/pipeline/office/assets/BIN.png +0 -0
  59. package/pipeline/office/assets/BOOKSHELF.png +0 -0
  60. package/pipeline/office/assets/CACTUS.png +0 -0
  61. package/pipeline/office/assets/CLOCK.png +0 -0
  62. package/pipeline/office/assets/COFFEE.png +0 -0
  63. package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
  64. package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
  65. package/pipeline/office/assets/COUNTER_SINK.png +0 -0
  66. package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
  67. package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
  68. package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
  69. package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
  70. package/pipeline/office/assets/DESK_FRONT.png +0 -0
  71. package/pipeline/office/assets/DESK_SIDE.png +0 -0
  72. package/pipeline/office/assets/DEVICE_STAND.png +0 -0
  73. package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
  74. package/pipeline/office/assets/FILING_CABINET.png +0 -0
  75. package/pipeline/office/assets/FRIDGE.png +0 -0
  76. package/pipeline/office/assets/HANGING_PLANT.png +0 -0
  77. package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
  78. package/pipeline/office/assets/LARGE_PLANT.png +0 -0
  79. package/pipeline/office/assets/MAILBOX.png +0 -0
  80. package/pipeline/office/assets/MOP_BUCKET.png +0 -0
  81. package/pipeline/office/assets/PC_BACK.png +0 -0
  82. package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
  83. package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
  84. package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
  85. package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
  86. package/pipeline/office/assets/PC_SIDE.png +0 -0
  87. package/pipeline/office/assets/PLANT.png +0 -0
  88. package/pipeline/office/assets/PLANT_2.png +0 -0
  89. package/pipeline/office/assets/POT.png +0 -0
  90. package/pipeline/office/assets/PRINTER.png +0 -0
  91. package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
  92. package/pipeline/office/assets/SERVER_RACK.png +0 -0
  93. package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
  94. package/pipeline/office/assets/SMALL_PAINTING.png +0 -0
  95. package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
  96. package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
  97. package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
  98. package/pipeline/office/assets/SOFA_BACK.png +0 -0
  99. package/pipeline/office/assets/SOFA_FRONT.png +0 -0
  100. package/pipeline/office/assets/SOFA_SIDE.png +0 -0
  101. package/pipeline/office/assets/TABLE_FRONT.png +0 -0
  102. package/pipeline/office/assets/TOILET_STALL.png +0 -0
  103. package/pipeline/office/assets/VENDING.png +0 -0
  104. package/pipeline/office/assets/WASHBASIN.png +0 -0
  105. package/pipeline/office/assets/WATER_COOLER.png +0 -0
  106. package/pipeline/office/assets/WHITEBOARD.png +0 -0
  107. package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
  108. package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
  109. package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
  110. package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
  111. package/pipeline/office/assets/char_0.png +0 -0
  112. package/pipeline/office/assets/char_1.png +0 -0
  113. package/pipeline/office/assets/char_2.png +0 -0
  114. package/pipeline/office/assets/char_3.png +0 -0
  115. package/pipeline/office/assets/char_4.png +0 -0
  116. package/pipeline/office/assets/char_5.png +0 -0
  117. package/pipeline/office/assets/floor_0.png +0 -0
  118. package/pipeline/office/assets/floor_1.png +0 -0
  119. package/pipeline/office/assets/floor_2.png +0 -0
  120. package/pipeline/office/assets/floor_3.png +0 -0
  121. package/pipeline/office/assets/floor_4.png +0 -0
  122. package/pipeline/office/assets/floor_5.png +0 -0
  123. package/pipeline/office/assets/floor_6.png +0 -0
  124. package/pipeline/office/assets/floor_7.png +0 -0
  125. package/pipeline/office/assets/floor_8.png +0 -0
  126. package/pipeline/office/assets/wall_0.png +0 -0
  127. package/pipeline/office/index.html +8431 -0
  128. package/pipeline/office/toolkit.json +3 -0
  129. package/pipeline/schemas/activity-chat.schema.json +78 -0
  130. package/pipeline/schemas/activity-roles.json +48 -0
  131. package/pipeline/schemas/activity-roles.schema.json +49 -0
  132. package/pipeline/schemas/activity.schema.json +613 -0
  133. package/pipeline/schemas/agent-state.schema.json +493 -115
  134. package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
  135. package/pipeline/schemas/autopilot-on.schema.json +111 -0
  136. package/pipeline/schemas/contract-error.schema.json +45 -4
  137. package/pipeline/schemas/issue-maturity.schema.json +68 -0
  138. package/pipeline/schemas/job-change.schema.json +72 -0
  139. package/pipeline/schemas/job-request.schema.json +38 -0
  140. package/pipeline/schemas/launch.json +4 -4
  141. package/pipeline/schemas/ledger.schema.json +202 -0
  142. package/pipeline/schemas/office-credentials.schema.json +71 -0
  143. package/pipeline/schemas/office-jobs.json +253 -0
  144. package/pipeline/schemas/office-jobs.schema.json +62 -0
  145. package/pipeline/schemas/office-memory.schema.json +105 -0
  146. package/pipeline/schemas/office-settings-request.schema.json +30 -0
  147. package/pipeline/schemas/office-settings.schema.json +67 -0
  148. package/pipeline/schemas/permissions.schema.json +47 -0
  149. package/pipeline/schemas/phases.json +10 -2
  150. package/pipeline/schemas/phases.schema.json +86 -19
  151. package/pipeline/schemas/prefs.schema.json +19 -0
  152. package/pipeline/schemas/prs.schema.json +78 -0
  153. package/pipeline/schemas/prune-logs-request.schema.json +26 -0
  154. package/pipeline/schemas/prune-logs.schema.json +52 -0
  155. package/pipeline/schemas/repo-change.schema.json +54 -0
  156. package/pipeline/schemas/repo-request.schema.json +29 -0
  157. package/pipeline/schemas/repos.schema.json +49 -1
  158. package/pipeline/schemas/review-verdict-request.schema.json +20 -0
  159. package/pipeline/schemas/review-verdict.schema.json +62 -0
  160. package/pipeline/schemas/routine-delete.schema.json +59 -0
  161. package/pipeline/schemas/routines.schema.json +38 -0
  162. package/pipeline/schemas/run-evidence.schema.json +29 -0
  163. package/pipeline/schemas/run-results.schema.json +218 -0
  164. package/pipeline/schemas/runs-index.schema.json +257 -46
  165. package/pipeline/schemas/service-read.schema.json +58 -0
  166. package/pipeline/schemas/stack-change.schema.json +82 -0
  167. package/pipeline/schemas/stack-request.schema.json +33 -0
  168. package/pipeline/schemas/stack.schema.json +59 -0
  169. package/pipeline/schemas/steer-request.schema.json +25 -0
  170. package/pipeline/schemas/steer.schema.json +59 -0
  171. package/pipeline/schemas/system-status.schema.json +111 -0
  172. package/pipeline/schemas/tool-call-request.schema.json +19 -0
  173. package/pipeline/schemas/tool-call.schema.json +47 -0
  174. package/pipeline/schemas/tools.schema.json +65 -0
  175. package/pipeline/schemas/unattended-profile.json +7 -2
  176. package/pipeline/scripts/_active-run.mjs +2 -1
  177. package/pipeline/scripts/_cost.mjs +66 -1
  178. package/pipeline/scripts/_office-smoke-lib.sh +115 -0
  179. package/pipeline/scripts/_spawn-async.mjs +77 -0
  180. package/pipeline/scripts/_transcript-tail.mjs +274 -0
  181. package/pipeline/scripts/activity-snapshot.mjs +1223 -0
  182. package/pipeline/scripts/autopilot-control.mjs +214 -9
  183. package/pipeline/scripts/autopilot-runner.mjs +7 -5
  184. package/pipeline/scripts/contract-server.mjs +1853 -197
  185. package/pipeline/scripts/cost-analyze.mjs +2 -53
  186. package/pipeline/scripts/cost-table.json +1 -0
  187. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
  188. package/pipeline/scripts/job-launch.mjs +477 -0
  189. package/pipeline/scripts/launch-request.mjs +94 -9
  190. package/pipeline/scripts/office-build.mjs +226 -0
  191. package/pipeline/scripts/office-control.mjs +410 -0
  192. package/pipeline/scripts/office-credentials.mjs +106 -0
  193. package/pipeline/scripts/office-ledger.mjs +409 -0
  194. package/pipeline/scripts/office-memory.mjs +200 -0
  195. package/pipeline/scripts/office-prune.mjs +125 -0
  196. package/pipeline/scripts/office-settings.mjs +145 -0
  197. package/pipeline/scripts/phase-tracker.sh +12 -5
  198. package/pipeline/scripts/prune-logs.sh +53 -2
  199. package/pipeline/scripts/routine-registry.mjs +42 -6
  200. package/pipeline/scripts/run-results.mjs +373 -0
  201. package/pipeline/scripts/runs-index.mjs +62 -2
  202. package/pipeline/scripts/service-read.mjs +244 -0
  203. package/pipeline/scripts/stack-toggle.mjs +285 -0
  204. package/pipeline/scripts/steer-run.mjs +97 -0
  205. package/pipeline/scripts/system-status.mjs +196 -0
  206. package/pipeline/scripts/task-mirror.mjs +150 -0
  207. package/pipeline/scripts/task-mirror.sh +20 -0
  208. package/pipeline/scripts/toolkit-tools.mjs +625 -0
  209. package/pipeline/scripts/transcript-index.mjs +4 -1
  210. package/pipeline/scripts/uninstall.mjs +3 -0
  211. package/pipeline/scripts/work-items.mjs +473 -0
  212. package/pipeline/skills/.skill-manifest.json +23 -11
  213. package/pipeline/skills/.skills-index.json +34 -1
  214. package/pipeline/skills/shared/README.md +6 -3
  215. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
  216. package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
  217. package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +21 -0
  218. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
  219. package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
  220. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -0
  221. package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
  222. package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
  223. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
  224. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
  225. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  226. package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
  227. package/pipeline/skills/skills-index.md +4 -1
@@ -0,0 +1,625 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file toolkit-tools.mjs - the multi-agent-toolkit MCP server's tools, for
4
+ * a client that is not an MCP host: list them, and call one with arguments its
5
+ * own inputSchema accepts.
6
+ *
7
+ * The server is the one the user registered with Claude Code: the
8
+ * `mcpServers` entry whose name contains "toolkit" in ~/.claude.json, else in
9
+ * ~/.claude/settings.json (the lookup doctor.mjs makes). It is started as a
10
+ * stdio child with that entry's command, arguments and environment on top of
11
+ * this process's, so every guard the toolkit honours from the environment
12
+ * (MULTI_AGENT_UNATTENDED, its URL policy) applies unchanged, and a tool's own
13
+ * confirm or preview behaviour stays the tool's. One child per client, started
14
+ * on first use and closed with it.
15
+ *
16
+ * Each tool carries `risk` from its own annotations: readOnlyHint is read,
17
+ * destructiveHint is high, anything else is low.
18
+ *
19
+ * Calls are denied by default. A tool may be called only when it declares
20
+ * readOnlyHint and its inputSchema takes nothing that names a place: no
21
+ * property whose name speaks of a path, URL, file, directory, output, project
22
+ * or root, and no uri format, at any depth. Everything else is listed with
23
+ * `allowed: false` and refused with `tool-not-allowed`, because a read-only
24
+ * tool that is handed a location reads whatever the location points at (a
25
+ * local file, a loopback service). Arguments may not carry `confirm`, which
26
+ * would skip a tool's own preview.
27
+ *
28
+ * The child always runs with MULTI_AGENT_UNATTENDED=1 and
29
+ * MCP_TOOLKIT_URL_POLICY=strict, set after the registration's own environment
30
+ * so neither can be overridden. A registration whose package spec has no exact
31
+ * version (bare, a dist-tag such as `@latest`, or a range) is pinned to
32
+ * MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion, else the
33
+ * version the release ships beside the office page (office/toolkit.json, the
34
+ * one it was checked against), and refused when none is set.
35
+ *
36
+ * No tool that is not read-only is allowed today. Opening one up means routing
37
+ * it through the server's two-step confirm, never a direct call.
38
+ *
39
+ * Arguments are checked against the tool's inputSchema before the call (the
40
+ * subset the toolkit's schemas use: type, enum, const, required, properties,
41
+ * additionalProperties, items, min/max, minLength/maxLength, pattern). A key
42
+ * the schema does not declare is refused at any depth, whatever
43
+ * additionalProperties says, and so is a `confirm` key. An unknown tool or
44
+ * invalid arguments never reach the server. Results come back as text
45
+ * (redacted), JSON (redacted, as text) and images (base64, image types only,
46
+ * size-capped); a tool's error message is redacted too.
47
+ *
48
+ * Usage:
49
+ * toolkit-tools.mjs list
50
+ * toolkit-tools.mjs call <name> '<json arguments>'
51
+ *
52
+ * MA_TOOLKIT_MCP sets the server as JSON {command, args, env} (tests).
53
+ *
54
+ * Exit: 0 printed, 1 refused or failed, 2 usage.
55
+ *
56
+ * @module pipeline/scripts/toolkit-tools
57
+ */
58
+
59
+ import { spawn } from "node:child_process";
60
+ import { readFileSync } from "node:fs";
61
+ import { homedir } from "node:os";
62
+ import { basename, dirname, join } from "node:path";
63
+ import { clearTimeout, setTimeout } from "node:timers";
64
+ import { createRedactor } from "./transcript-index.mjs";
65
+ import { runMain } from "../lib/fatal.mjs";
66
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
67
+
68
+ export const CONTRACT_VERSION = "1.0.0";
69
+ export const TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
70
+ const START_TIMEOUT_MS = 60_000;
71
+ const CALL_TIMEOUT_MS = 180_000;
72
+ const MAX_LINE_BYTES = 16 * 1024 * 1024;
73
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
74
+ const IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
75
+
76
+ export class ToolError extends Error {
77
+ constructor(code, message, extra = {}) {
78
+ super(message);
79
+ this.code = code;
80
+ this.extra = extra;
81
+ }
82
+ }
83
+
84
+ const readJson = (p) => {
85
+ try {
86
+ return JSON.parse(readFileSync(p, "utf8"));
87
+ } catch {
88
+ return null;
89
+ }
90
+ };
91
+
92
+ /** The registered toolkit server, or null. */
93
+ export function toolkitEntry(env = process.env) {
94
+ if (env.MA_TOOLKIT_MCP) {
95
+ try {
96
+ const e = JSON.parse(env.MA_TOOLKIT_MCP);
97
+ if (e && typeof e.command === "string") return e;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ const home = env.HOME || homedir();
103
+ for (const p of [join(home, ".claude.json"), join(home, ".claude", "settings.json")]) {
104
+ const j = readJson(p);
105
+ const key = j?.mcpServers && Object.keys(j.mcpServers).find((k) => /toolkit/i.test(k));
106
+ if (key && typeof j.mcpServers[key]?.command === "string") return j.mcpServers[key];
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Families refused whatever their schema says: `web` tools share a browser
113
+ * whose page may already point at a local file or service, and `context`
114
+ * tools search an index built from whatever files were indexed.
115
+ */
116
+ const DENIED_CATEGORIES = new Set(["web", "context"]);
117
+
118
+ /** Property names that hand a tool a location to read or write. */
119
+ const LOCATION_NAME =
120
+ /(path|url|uri|href|link|file|dir|folder|output|out_?dir|project|root|cwd|workspace|location|endpoint|host|address|socket|device|bundle|archive|repo)/i;
121
+ const LOCATION_FORMAT = /^(uri|uri-reference|iri|iri-reference|url|hostname|idn-hostname)$/i;
122
+
123
+ /** The location-shaped properties anywhere in a schema, by name or format. */
124
+ export function locationProperties(schema, prefix = "") {
125
+ const out = [];
126
+ if (!schema || typeof schema !== "object") return out;
127
+ for (const [k, v] of Object.entries(schema.properties ?? {})) {
128
+ const at = prefix ? `${prefix}.${k}` : k;
129
+ if (LOCATION_NAME.test(k) || LOCATION_FORMAT.test(String(v?.format ?? ""))) out.push(at);
130
+ out.push(...locationProperties(v, at));
131
+ if (v?.items) out.push(...locationProperties(v.items, `${at}[]`));
132
+ }
133
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
134
+ for (const alt of Array.isArray(schema[key]) ? schema[key] : []) {
135
+ out.push(...locationProperties(alt, prefix));
136
+ }
137
+ }
138
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
139
+ out.push(...locationProperties(schema.additionalProperties, `${prefix}*`));
140
+ }
141
+ return out;
142
+ }
143
+
144
+ /**
145
+ * Whether a tool may be called from this client, and why not. Only a
146
+ * read-only tool that takes no location is allowed.
147
+ */
148
+ export function callPolicy(tool) {
149
+ if (DENIED_CATEGORIES.has(categoryOf(String(tool?.name ?? "")))) {
150
+ return {
151
+ allowed: false,
152
+ reason:
153
+ "this family reads state shared with other sessions (a browser, an index of files); call it from a terminal session",
154
+ };
155
+ }
156
+ if (tool?.annotations?.readOnlyHint !== true || riskOf(tool) !== "read") {
157
+ return {
158
+ allowed: false,
159
+ reason: "only tools that declare readOnlyHint, and nothing destructive, can be called here",
160
+ };
161
+ }
162
+ const where = locationProperties(tool.inputSchema);
163
+ if (where.length) {
164
+ return {
165
+ allowed: false,
166
+ reason: `the tool takes a location (${where.slice(0, 3).join(", ")}); call it from a terminal session`,
167
+ };
168
+ }
169
+ return { allowed: true, reason: null };
170
+ }
171
+
172
+ /** Environment the toolkit child cannot override: unattended guards, strict URL policy. */
173
+ export const FORCED_ENV = Object.freeze({
174
+ MULTI_AGENT_UNATTENDED: "1",
175
+ MCP_TOOLKIT_URL_POLICY: "strict",
176
+ });
177
+
178
+ const SEMVER = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
179
+ const RUNNERS = new Set(["npx", "bunx", "pnpx"]);
180
+
181
+ /**
182
+ * The package specs a registration hands to a package runner (npx, bunx,
183
+ * pnpx, `npm exec`, `pnpm dlx`): the first positional argument, or each
184
+ * `-p` / `--package` value. A registration that runs a local file has none.
185
+ */
186
+ export function packageSpecs(entry) {
187
+ const cmd = basename(String(entry?.command ?? ""));
188
+ const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
189
+ let rest;
190
+ if (RUNNERS.has(cmd)) rest = args;
191
+ else if (cmd === "npm" && (args[0] === "exec" || args[0] === "x")) rest = args.slice(1);
192
+ else if (cmd === "pnpm" && args[0] === "dlx") rest = args.slice(1);
193
+ else return [];
194
+ const specs = [];
195
+ let named = false;
196
+ for (let i = 0; i < rest.length; i++) {
197
+ const a = rest[i];
198
+ if (a === "-p" || a === "--package") {
199
+ if (rest[i + 1] !== undefined) specs.push(rest[++i]);
200
+ named = true;
201
+ } else if (a.startsWith("--package=")) {
202
+ specs.push(a.slice("--package=".length));
203
+ named = true;
204
+ } else if (a === "--") {
205
+ if (!named && rest[i + 1] !== undefined) specs.push(rest[i + 1]);
206
+ break;
207
+ } else if (!a.startsWith("-")) {
208
+ if (!named) specs.push(a);
209
+ break;
210
+ }
211
+ }
212
+ return specs;
213
+ }
214
+
215
+ /** The name and version of a spec: `@scope/name@1.2.3`, `name@^1`, `name`. */
216
+ function splitSpec(spec) {
217
+ const at = spec.lastIndexOf("@");
218
+ return at > 0 ? [spec.slice(0, at), spec.slice(at + 1)] : [spec, null];
219
+ }
220
+
221
+ /**
222
+ * Whether a registration would resolve its package anew on every start: a
223
+ * spec without an exact version (bare, a dist-tag such as latest or next, or
224
+ * a range) is unpinned.
225
+ */
226
+ export function unpinned(entry) {
227
+ return packageSpecs(entry).some((s) => !SEMVER.test(splitSpec(s)[1] ?? ""));
228
+ }
229
+
230
+ /**
231
+ * The registration with every unpinned spec set to `<name>@<version>`, or
232
+ * null when it is unpinned and no exact version is known. The version comes
233
+ * from MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion, else
234
+ * office/toolkit.json beside the page (MA_OFFICE_PAGE's directory, or the
235
+ * office directory next to this script's).
236
+ */
237
+ export function pinnedEntry(entry, env = process.env) {
238
+ if (!unpinned(entry)) return entry;
239
+ const prefs = readJson(join(env.HOME || homedir(), ".claude", "multi-agent-preferences.json"));
240
+ const version =
241
+ env.MA_TOOLKIT_VERSION || prefs?.global?.office?.toolkitVersion || shippedToolkitVersion(env);
242
+ if (typeof version !== "string" || !SEMVER.test(version)) return null;
243
+ const loose = new Set(packageSpecs(entry).filter((s) => !SEMVER.test(splitSpec(s)[1] ?? "")));
244
+ const pin = (a) => {
245
+ if (typeof a !== "string") return a;
246
+ if (loose.has(a)) return `${splitSpec(a)[0]}@${version}`;
247
+ if (a.startsWith("--package=") && loose.has(a.slice(10))) {
248
+ return `--package=${splitSpec(a.slice(10))[0]}@${version}`;
249
+ }
250
+ return a;
251
+ };
252
+ return { ...entry, args: Array.isArray(entry.args) ? entry.args.map(pin) : entry.args };
253
+ }
254
+
255
+ /** The toolkit version the release ships beside the office page, or undefined. */
256
+ export function shippedToolkitVersion(env = process.env) {
257
+ const dir = env.MA_OFFICE_PAGE
258
+ ? dirname(env.MA_OFFICE_PAGE)
259
+ : join(import.meta.dirname, "..", "office");
260
+ const v = readJson(join(dir, "toolkit.json"))?.toolkitVersion;
261
+ return typeof v === "string" && SEMVER.test(v) ? v : undefined;
262
+ }
263
+
264
+ /** The exact toolkit version the Tools window starts, or null when none is known. */
265
+ export function activeToolkitVersion(env = process.env) {
266
+ const registered = toolkitEntry(env);
267
+ const entry = registered ? pinnedEntry(registered, env) : null;
268
+ const spec = entry ? packageSpecs(entry)[0] : null;
269
+ const v = spec ? splitSpec(spec)[1] : null;
270
+ return typeof v === "string" && SEMVER.test(v) ? v : null;
271
+ }
272
+
273
+ /** read, low or high, from the tool's own annotations. */
274
+ export function riskOf(tool) {
275
+ const a = tool?.annotations ?? {};
276
+ if (a.destructiveHint === true) return "high";
277
+ if (a.readOnlyHint === true) return "read";
278
+ return "low";
279
+ }
280
+
281
+ /** The tool's category: the prefix before its first underscore. */
282
+ export const categoryOf = (name) => (name.includes("_") ? name.split("_")[0] : "other");
283
+
284
+ /**
285
+ * Every reason `value` does not satisfy `schema`, for the subset of JSON
286
+ * Schema the toolkit's input schemas use. Unknown keywords are ignored; the
287
+ * server validates again.
288
+ */
289
+ export function schemaErrors(schema, value, path = "", opts = {}) {
290
+ if (!schema || typeof schema !== "object") return [];
291
+ const at = path || "/";
292
+ const errors = [];
293
+ const types = schema.type === undefined ? null : [].concat(schema.type);
294
+ const typeOf = (v) =>
295
+ v === null ? "null" : Array.isArray(v) ? "array" : Number.isInteger(v) ? "integer" : typeof v;
296
+ if (types) {
297
+ const t = typeOf(value);
298
+ const ok = types.some((x) => x === t || (x === "number" && t === "integer"));
299
+ if (!ok) return [`${at} must be ${types.join(" or ")}`];
300
+ }
301
+ if (
302
+ Array.isArray(schema.enum) &&
303
+ !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))
304
+ ) {
305
+ errors.push(`${at} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(", ")}`);
306
+ }
307
+ if ("const" in schema && JSON.stringify(schema.const) !== JSON.stringify(value)) {
308
+ errors.push(`${at} must be ${JSON.stringify(schema.const)}`);
309
+ }
310
+ if (typeof value === "string") {
311
+ if (Number.isInteger(schema.minLength) && value.length < schema.minLength)
312
+ errors.push(`${at} is too short`);
313
+ if (Number.isInteger(schema.maxLength) && value.length > schema.maxLength)
314
+ errors.push(`${at} is too long`);
315
+ if (typeof schema.pattern === "string") {
316
+ try {
317
+ if (!new RegExp(schema.pattern, "u").test(value))
318
+ errors.push(`${at} does not match its pattern`);
319
+ } catch {
320
+ // A pattern this engine cannot compile is left to the server.
321
+ }
322
+ }
323
+ }
324
+ if (typeof value === "number") {
325
+ if (typeof schema.minimum === "number" && value < schema.minimum)
326
+ errors.push(`${at} is below ${schema.minimum}`);
327
+ if (typeof schema.maximum === "number" && value > schema.maximum)
328
+ errors.push(`${at} is above ${schema.maximum}`);
329
+ }
330
+ if (Array.isArray(value)) {
331
+ if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems)
332
+ errors.push(`${at} has too many items`);
333
+ if (Number.isInteger(schema.minItems) && value.length < schema.minItems)
334
+ errors.push(`${at} has too few items`);
335
+ if (schema.items && typeof schema.items === "object") {
336
+ value.forEach((v, i) => errors.push(...schemaErrors(schema.items, v, `${path}/${i}`, opts)));
337
+ }
338
+ }
339
+ if (value && typeof value === "object" && !Array.isArray(value)) {
340
+ const props = schema.properties ?? {};
341
+ for (const k of Array.isArray(schema.required) ? schema.required : []) {
342
+ if (!(k in value)) errors.push(`${path}/${k} is required`);
343
+ }
344
+ for (const [k, v] of Object.entries(value)) {
345
+ if (Object.hasOwn(props, k)) errors.push(...schemaErrors(props[k], v, `${path}/${k}`, opts));
346
+ else if (opts.strict) errors.push(`${path}/${k} is not declared by the tool's inputSchema`);
347
+ else if (schema.additionalProperties === false) errors.push(`${path}/${k} is not allowed`);
348
+ else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
349
+ errors.push(...schemaErrors(schema.additionalProperties, v, `${path}/${k}`, opts));
350
+ }
351
+ }
352
+ }
353
+ return errors;
354
+ }
355
+
356
+ /** Every `confirm` key in an argument value, at any depth, as JSON pointers. */
357
+ export function confirmPaths(value, path = "") {
358
+ if (Array.isArray(value)) return value.flatMap((v, i) => confirmPaths(v, `${path}/${i}`));
359
+ if (!value || typeof value !== "object") return [];
360
+ return Object.entries(value).flatMap(([k, v]) => [
361
+ ...(k === "confirm" ? [`${path}/confirm`] : []),
362
+ ...confirmPaths(v, `${path}/${k}`),
363
+ ]);
364
+ }
365
+
366
+ /**
367
+ * A stdio MCP client for the toolkit, started lazily, one per caller.
368
+ *
369
+ * @param {{env?: object}} [opts]
370
+ */
371
+ export function createToolkitClient({ env = process.env } = {}) {
372
+ let child = null;
373
+ let ready = null;
374
+ let nextId = 1;
375
+ let buf = "";
376
+ let tools = null;
377
+ const pending = new Map();
378
+
379
+ const failAll = (err) => {
380
+ for (const p of pending.values()) {
381
+ clearTimeout(p.timer);
382
+ p.reject(err);
383
+ }
384
+ pending.clear();
385
+ };
386
+
387
+ const rpc = (method, params, timeoutMs) =>
388
+ new Promise((resolve, reject) => {
389
+ const id = nextId++;
390
+ const timer = setTimeout(() => {
391
+ pending.delete(id);
392
+ reject(new ToolError("timeout", `the toolkit did not answer ${method} in time`));
393
+ }, timeoutMs);
394
+ pending.set(id, { resolve, reject, timer });
395
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
396
+ });
397
+
398
+ const start = () => {
399
+ if (ready) return ready;
400
+ const registered = toolkitEntry(env);
401
+ if (!registered) {
402
+ return Promise.reject(
403
+ new ToolError("not-registered", "multi-agent-toolkit is not registered as an MCP server"),
404
+ );
405
+ }
406
+ const entry = pinnedEntry(registered, env);
407
+ if (!entry) {
408
+ return Promise.reject(
409
+ new ToolError(
410
+ "unpinned",
411
+ "no exact toolkit version is known: the release ships it in office/toolkit.json beside the page, which reinstalling (node install.js) restores; MA_TOOLKIT_VERSION or prefs.global.office.toolkitVersion override it",
412
+ ),
413
+ );
414
+ }
415
+ ready = new Promise((resolve, reject) => {
416
+ child = spawn(entry.command, Array.isArray(entry.args) ? entry.args : [], {
417
+ env: { ...env, ...(entry.env ?? {}), ...FORCED_ENV },
418
+ stdio: ["pipe", "pipe", "ignore"],
419
+ });
420
+ child.on("error", (e) => {
421
+ ready = null;
422
+ reject(
423
+ new ToolError("not-started", `the toolkit could not start (${e.code ?? e.message})`),
424
+ );
425
+ });
426
+ child.on("exit", () => {
427
+ ready = null;
428
+ tools = null;
429
+ failAll(new ToolError("exited", "the toolkit exited"));
430
+ });
431
+ child.stdout.setEncoding("utf8");
432
+ child.stdout.on("data", (d) => {
433
+ buf += d;
434
+ if (buf.length > MAX_LINE_BYTES) {
435
+ buf = "";
436
+ return;
437
+ }
438
+ let nl;
439
+ while ((nl = buf.indexOf("\n")) !== -1) {
440
+ const line = buf.slice(0, nl);
441
+ buf = buf.slice(nl + 1);
442
+ let msg;
443
+ try {
444
+ msg = JSON.parse(line);
445
+ } catch {
446
+ continue;
447
+ }
448
+ const p = msg && pending.get(msg.id);
449
+ if (!p) continue;
450
+ pending.delete(msg.id);
451
+ clearTimeout(p.timer);
452
+ if (msg.error) {
453
+ const why = createRedactor().clean(String(msg.error.message ?? "the tool failed"));
454
+ p.reject(new ToolError("tool-error", why.slice(0, 2000)));
455
+ } else p.resolve(msg.result);
456
+ }
457
+ });
458
+ rpc(
459
+ "initialize",
460
+ {
461
+ protocolVersion: "2025-06-18",
462
+ capabilities: {},
463
+ clientInfo: { name: "multi-agent-contract-server", version: CONTRACT_VERSION },
464
+ },
465
+ START_TIMEOUT_MS,
466
+ )
467
+ .then(() => {
468
+ child.stdin.write(
469
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n",
470
+ );
471
+ resolve();
472
+ })
473
+ .catch((e) => {
474
+ ready = null;
475
+ reject(e);
476
+ });
477
+ });
478
+ return ready;
479
+ };
480
+
481
+ /** Every tool with its category, risk, description and inputSchema. */
482
+ async function listTools() {
483
+ if (tools) return tools;
484
+ await start();
485
+ const all = [];
486
+ let cursor;
487
+ for (let page = 0; page < 20; page++) {
488
+ const r = await rpc("tools/list", cursor ? { cursor } : {}, START_TIMEOUT_MS);
489
+ for (const t of Array.isArray(r?.tools) ? r.tools : []) {
490
+ if (typeof t?.name !== "string" || !TOOL_NAME.test(t.name)) continue;
491
+ all.push({
492
+ name: t.name,
493
+ title: typeof t.title === "string" ? t.title : (t.annotations?.title ?? null),
494
+ category: categoryOf(t.name),
495
+ description: typeof t.description === "string" ? t.description : "",
496
+ risk: riskOf(t),
497
+ ...callPolicy(t),
498
+ inputSchema:
499
+ t.inputSchema && typeof t.inputSchema === "object" ? t.inputSchema : { type: "object" },
500
+ });
501
+ }
502
+ cursor = r?.nextCursor;
503
+ if (!cursor) break;
504
+ }
505
+ tools = all.sort((a, b) => (a.name < b.name ? -1 : 1));
506
+ return tools;
507
+ }
508
+
509
+ /** Call one tool after checking its arguments; the result redacted. */
510
+ async function callTool(name, args) {
511
+ if (typeof name !== "string" || !TOOL_NAME.test(name))
512
+ throw new ToolError("unknown-tool", "no such tool");
513
+ const tool = (await listTools()).find((t) => t.name === name);
514
+ if (!tool) throw new ToolError("unknown-tool", "no such tool");
515
+ if (!tool.allowed) throw new ToolError("tool-not-allowed", tool.reason);
516
+ const input = args ?? {};
517
+ const confirms = confirmPaths(input);
518
+ if (confirms.length) {
519
+ throw new ToolError("invalid-arguments", "confirm is not accepted here", {
520
+ errors: confirms.map(
521
+ (p) => `${p} is not allowed: a tool's own preview is never skipped from here`,
522
+ ),
523
+ });
524
+ }
525
+ const errors = schemaErrors(tool.inputSchema, input, "", { strict: true });
526
+ if (errors.length)
527
+ throw new ToolError(
528
+ "invalid-arguments",
529
+ "the arguments do not match the tool's inputSchema",
530
+ { errors },
531
+ );
532
+ const r = await rpc("tools/call", { name, arguments: input }, CALL_TIMEOUT_MS);
533
+ return renderResult(tool, r);
534
+ }
535
+
536
+ function close() {
537
+ failAll(new ToolError("closed", "the client was closed"));
538
+ if (child && child.exitCode === null) child.kill("SIGTERM");
539
+ child = null;
540
+ ready = null;
541
+ tools = null;
542
+ }
543
+
544
+ return {
545
+ listTools,
546
+ callTool,
547
+ close,
548
+ get started() {
549
+ return Boolean(child);
550
+ },
551
+ };
552
+ }
553
+
554
+ /** A tools/call result as text, JSON text and images, every text redacted. */
555
+ export function renderResult(tool, result) {
556
+ const R = createRedactor();
557
+ const content = [];
558
+ for (const c of Array.isArray(result?.content) ? result.content : []) {
559
+ if (c?.type === "text") content.push({ kind: "text", text: R.clean(String(c.text ?? "")) });
560
+ else if (c?.type === "image" && IMAGE_TYPES.has(c.mimeType) && typeof c.data === "string") {
561
+ if (c.data.length * 0.75 <= MAX_IMAGE_BYTES)
562
+ content.push({ kind: "image", mime: c.mimeType, data: c.data });
563
+ else content.push({ kind: "text", text: "[image too large to show]" });
564
+ } else if (c?.type === "resource" && typeof c.resource?.text === "string") {
565
+ content.push({ kind: "text", text: R.clean(c.resource.text) });
566
+ } else if (c?.type)
567
+ content.push({ kind: "text", text: `[${String(c.type)} content not shown]` });
568
+ }
569
+ if (result?.structuredContent && typeof result.structuredContent === "object") {
570
+ content.push({
571
+ kind: "json",
572
+ text: R.clean(JSON.stringify(result.structuredContent, null, 2)),
573
+ });
574
+ }
575
+ return {
576
+ contractVersion: CONTRACT_VERSION,
577
+ tool: tool.name,
578
+ risk: tool.risk,
579
+ isError: result?.isError === true,
580
+ content,
581
+ };
582
+ }
583
+
584
+ async function main(argv) {
585
+ const [cmd, name, raw] = argv;
586
+ const client = createToolkitClient();
587
+ try {
588
+ let out;
589
+ if (cmd === "list" && !name) {
590
+ out = {
591
+ contractVersion: CONTRACT_VERSION,
592
+ version: activeToolkitVersion(),
593
+ tools: await client.listTools(),
594
+ };
595
+ } else if (cmd === "call" && name) {
596
+ let args = {};
597
+ if (raw !== undefined) {
598
+ try {
599
+ args = JSON.parse(raw);
600
+ } catch {
601
+ throw new ToolError("invalid-arguments", "the arguments are not JSON");
602
+ }
603
+ }
604
+ out = await client.callTool(name, args);
605
+ } else {
606
+ process.stderr.write("usage: toolkit-tools.mjs list | call <name> '<json arguments>'\n");
607
+ return 2;
608
+ }
609
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
610
+ return 0;
611
+ } catch (err) {
612
+ if (!(err instanceof ToolError)) throw err;
613
+ process.stderr.write(`toolkit-tools: ${err.message}\n`);
614
+ for (const e of err.extra?.errors ?? []) process.stderr.write(` ${e}\n`);
615
+ return 1;
616
+ } finally {
617
+ client.close();
618
+ }
619
+ }
620
+
621
+ if (invokedDirectly(import.meta.url)) {
622
+ runMain("toolkit-tools", async () => {
623
+ process.exitCode = await main(process.argv.slice(2));
624
+ });
625
+ }
@@ -139,9 +139,12 @@ const TOKEN_CHAR = /[A-Za-z0-9_\-./+=:@~%]/;
139
139
  * `text` cut to `limit` characters, backed off to the start of a token that
140
140
  * the cut would split, so no half-credential survives on the boundary.
141
141
  */
142
+ // The " ..." ending counts toward the limit, so a cut text is never longer
143
+ // than the field that holds it.
142
144
  function cut(text, limit) {
143
145
  if (text.length <= limit) return text;
144
- let end = limit;
146
+ if (limit < 5) return text.slice(0, limit);
147
+ let end = limit - 4;
145
148
  while (end > 0 && TOKEN_CHAR.test(text[end]) && TOKEN_CHAR.test(text[end - 1])) end--;
146
149
  return `${text.slice(0, end).trimEnd()} ...`;
147
150
  }
@@ -997,6 +997,7 @@ export async function main() {
997
997
  rmIfExists(join(CLAUDE, "multi-agent-refs"));
998
998
  rmIfExists(join(CLAUDE, ".pipeline-swap"));
999
999
  rmIfExists(join(CLAUDE, "schemas"));
1000
+ rmIfExists(join(CLAUDE, "office"));
1000
1001
  if (!rmManifestFiles(CLAUDE, KEEP_IN_LIB)) {
1001
1002
  // An install that predates the manifest wiped lib/ and templates/ on
1002
1003
  // every run, so their shipped names are the pipeline's. The top-level
@@ -1053,6 +1054,7 @@ export async function main() {
1053
1054
  rmIfExists(join(COP, "scripts"));
1054
1055
  // Pipeline-managed trees the installer lays down alongside scripts/.
1055
1056
  rmIfExists(join(COP, "schemas"));
1057
+ rmIfExists(join(COP, "office"));
1056
1058
  rmDirExcept(join(COP, "lib"), KEEP_IN_LIB);
1057
1059
  rmIfExists(join(COP, ".pipeline-version"));
1058
1060
  const skills = join(COP, "skills");
@@ -1164,6 +1166,7 @@ export async function main() {
1164
1166
  rmIfExists(join(CODEX, "scripts"));
1165
1167
  rmDirExcept(join(CODEX, "lib"), KEEP_IN_LIB);
1166
1168
  rmIfExists(join(CODEX, "schemas"));
1169
+ rmIfExists(join(CODEX, "office"));
1167
1170
  rmIfExists(join(CODEX, "rules"));
1168
1171
  rmIfExists(join(CODEX, ".pipeline-version"));
1169
1172