@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,244 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file service-read.mjs - read one thing from Confluence, Crashlytics or
4
+ * Figma through the clients the pipeline already ships, for a client that
5
+ * shows it next to a run:
6
+ *
7
+ * confluence <page> lib/fetch-confluence.sh: a page by id or by https
8
+ * URL, both only on the configured
9
+ * prefs.global.hosts.confluence; a URL on any other
10
+ * host, or any URL when no host is configured, is
11
+ * refused before the script runs; title, markdown
12
+ * body, API contracts count
13
+ * crashlytics <issue-url> lib/fetch-crashlytics.sh: one Crashlytics issue,
14
+ * title, occurrences, impacted users, top frames
15
+ * figma <file-key> <node> lib/figma-screenshot.sh (REST with the Figma
16
+ * token, tier 2): one PNG render of a frame. The
17
+ * Figma MCP is never called here; the pipeline
18
+ * keeps MCP access to the analysis phase.
19
+ *
20
+ * Search across Confluence, a per-version Crashlytics trend and a Figma file
21
+ * browser have no client in the pipeline, so none is offered; a client asks
22
+ * an agent instead.
23
+ *
24
+ * The scripts run as child processes without blocking the caller's event
25
+ * loop (contract-server.mjs serves other routes meanwhile).
26
+ *
27
+ * Credentials are read only by those scripts, through credential-store.sh.
28
+ * Every text that comes back is scrubbed and redacted; an image comes back as
29
+ * base64.
30
+ *
31
+ * Usage:
32
+ * service-read.mjs confluence <page-id|https-url>
33
+ * service-read.mjs crashlytics <https-issue-url>
34
+ * service-read.mjs figma <file-key> <node-id>
35
+ *
36
+ * MA_FETCH_CONFLUENCE, MA_FETCH_CRASHLYTICS and MA_FIGMA_SCREENSHOT stand in
37
+ * for the scripts (tests).
38
+ *
39
+ * Exit: 0 printed, 1 refused or failed, 2 usage.
40
+ *
41
+ * @module pipeline/scripts/service-read
42
+ */
43
+
44
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
45
+ import { homedir, tmpdir } from "node:os";
46
+ import { dirname, join } from "node:path";
47
+ import { fileURLToPath } from "node:url";
48
+ import { createRedactor } from "./transcript-index.mjs";
49
+ import { spawnAsync } from "./_spawn-async.mjs";
50
+ import { runMain } from "../lib/fatal.mjs";
51
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
52
+
53
+ export const CONTRACT_VERSION = "1.0.0";
54
+ const HERE = dirname(fileURLToPath(import.meta.url));
55
+ const TIMEOUT_MS = 90_000;
56
+ const MAX_TEXT = 100_000;
57
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
58
+ export const PAGE_ID = /^[0-9]{1,20}$/;
59
+ export const FIGMA_KEY = /^[A-Za-z0-9]{10,64}$/;
60
+ export const FIGMA_NODE = /^[0-9]{1,10}[:-][0-9]{1,10}$/;
61
+
62
+ export class ServiceError extends Error {
63
+ constructor(code, message) {
64
+ super(message);
65
+ this.code = code;
66
+ }
67
+ }
68
+
69
+ const script = (env, name, override) =>
70
+ env[override] ||
71
+ [join(HERE, "..", "lib", name), join(HERE, "..", "..", "lib", name)].find((p) => existsSync(p));
72
+
73
+ function httpsUrl(v, host = null) {
74
+ if (typeof v !== "string" || v.length > 2048) return null;
75
+ let u;
76
+ try {
77
+ u = new URL(v);
78
+ } catch {
79
+ return null;
80
+ }
81
+ if (u.protocol !== "https:" || u.username || u.password) return null;
82
+ if (host && u.hostname !== host) return null;
83
+ return u.toString();
84
+ }
85
+
86
+ async function run(env, file, args, failCodes) {
87
+ if (!file) throw new ServiceError("not-installed", "the service client is not installed");
88
+ const r = await spawnAsync("bash", [file, ...args], { env, timeoutMs: TIMEOUT_MS });
89
+ if (r.status !== 0) {
90
+ const why = failCodes[r.status] ?? "the service could not be read";
91
+ throw new ServiceError("service-failed", why);
92
+ }
93
+ try {
94
+ return JSON.parse(r.stdout);
95
+ } catch {
96
+ throw new ServiceError(
97
+ "service-failed",
98
+ "the service answered with something that is not JSON",
99
+ );
100
+ }
101
+ }
102
+
103
+ function confluenceHost(env) {
104
+ try {
105
+ const prefs = JSON.parse(
106
+ readFileSync(
107
+ env.MULTI_AGENT_PREFS ||
108
+ join(env.HOME || homedir(), ".claude", "multi-agent-preferences.json"),
109
+ "utf8",
110
+ ),
111
+ );
112
+ const h = prefs?.global?.hosts?.confluence;
113
+ return typeof h === "string" && h ? h.replace(/^https?:\/\//, "").replace(/\/.*$/, "") : null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /** One Confluence page, by id or by a URL on the configured host only. */
120
+ export async function confluencePage(page, env = process.env) {
121
+ let args;
122
+ const host = confluenceHost(env);
123
+ if (PAGE_ID.test(String(page))) {
124
+ if (!host) throw new ServiceError("not-configured", "no Confluence host is configured");
125
+ args = ["--host", host, "--page-id", String(page)];
126
+ } else {
127
+ const url = httpsUrl(page);
128
+ if (!url) throw new ServiceError("bad-input", "a page id or an https URL is required");
129
+ if (!host) {
130
+ throw new ServiceError(
131
+ "host-not-allowed",
132
+ "no Confluence host is configured, so only a page id is accepted",
133
+ );
134
+ }
135
+ if (new URL(url).hostname !== host) {
136
+ throw new ServiceError("host-not-allowed", "that host is not the configured Confluence host");
137
+ }
138
+ args = [url];
139
+ }
140
+ const d = await run(env, script(env, "fetch-confluence.sh", "MA_FETCH_CONFLUENCE"), args, {
141
+ 2: "the Confluence credential is missing or was refused",
142
+ 3: "Confluence could not be reached",
143
+ 6: "that host is not the configured Confluence host",
144
+ });
145
+ const R = createRedactor();
146
+ return {
147
+ contractVersion: CONTRACT_VERSION,
148
+ service: "confluence",
149
+ pageId: d?.source?.pageId ? String(d.source.pageId) : null,
150
+ title: R.excerpt(d?.title ?? "", 300),
151
+ body: R.clean(String(d?.bodyMarkdown ?? "").slice(0, MAX_TEXT)),
152
+ apiContracts: Array.isArray(d?.apiContracts) ? d.apiContracts.length : 0,
153
+ };
154
+ }
155
+
156
+ /** One Crashlytics issue. */
157
+ export async function crashlyticsIssue(issueUrl, env = process.env) {
158
+ const url = httpsUrl(issueUrl, "console.firebase.google.com");
159
+ if (!url) throw new ServiceError("bad-input", "a Firebase console issue URL is required");
160
+ const d = await run(env, script(env, "fetch-crashlytics.sh", "MA_FETCH_CRASHLYTICS"), [url], {
161
+ 2: "the Firebase credential is missing or was refused",
162
+ 3: "Crashlytics could not be reached",
163
+ });
164
+ const R = createRedactor();
165
+ const i = d?.issue ?? {};
166
+ return {
167
+ contractVersion: CONTRACT_VERSION,
168
+ service: "crashlytics",
169
+ title: R.excerpt(i.title ?? "", 300),
170
+ type: i.type ? String(i.type) : null,
171
+ occurrences: Number.isFinite(i.occurrences) ? i.occurrences : null,
172
+ impactedUsers: Number.isFinite(i.impactedUsers) ? i.impactedUsers : null,
173
+ stackTrace: (Array.isArray(i.stackTrace) ? i.stackTrace : [])
174
+ .slice(0, 40)
175
+ .map((f) => R.excerpt(typeof f === "string" ? f : JSON.stringify(f), 300)),
176
+ };
177
+ }
178
+
179
+ /** One frame rendered as PNG. */
180
+ export async function figmaFrame(fileKey, nodeId, env = process.env) {
181
+ if (!FIGMA_KEY.test(String(fileKey)) || !FIGMA_NODE.test(String(nodeId))) {
182
+ throw new ServiceError("bad-input", "a Figma file key and a node id (1:2) are required");
183
+ }
184
+ const out = mkdtempSync(join(tmpdir(), "ma-figma-"));
185
+ try {
186
+ const file = script(env, "figma-screenshot.sh", "MA_FIGMA_SCREENSHOT");
187
+ if (!file) throw new ServiceError("not-installed", "the service client is not installed");
188
+ const r = await spawnAsync(
189
+ "bash",
190
+ [file, "--file-key", fileKey, "--node-id", nodeId.replace("-", ":"), "--output-dir", out],
191
+ { env, timeoutMs: TIMEOUT_MS, maxBytes: 1024 * 1024 },
192
+ );
193
+ const why = {
194
+ 2: "the Figma token is missing or was refused",
195
+ 4: "no such frame",
196
+ 5: "Figma is rate limiting; try again later",
197
+ 6: "Figma could not be reached",
198
+ };
199
+ if (r.status !== 0)
200
+ throw new ServiceError("service-failed", why[r.status] ?? "the frame could not be rendered");
201
+ const png = readdirSync(out).find((f) => f.endsWith(".png"));
202
+ if (!png || statSync(join(out, png)).size > MAX_IMAGE_BYTES) {
203
+ throw new ServiceError("service-failed", "the frame could not be rendered");
204
+ }
205
+ return {
206
+ contractVersion: CONTRACT_VERSION,
207
+ service: "figma",
208
+ fileKey,
209
+ nodeId: nodeId.replace("-", ":"),
210
+ mime: "image/png",
211
+ data: readFileSync(join(out, png)).toString("base64"),
212
+ };
213
+ } finally {
214
+ rmSync(out, { recursive: true, force: true });
215
+ }
216
+ }
217
+
218
+ async function main(argv) {
219
+ const [svc, a, b, ...rest] = argv;
220
+ try {
221
+ let out;
222
+ if (svc === "confluence" && a && !b) out = await confluencePage(a);
223
+ else if (svc === "crashlytics" && a && !b) out = await crashlyticsIssue(a);
224
+ else if (svc === "figma" && a && b && !rest.length) out = await figmaFrame(a, b);
225
+ else {
226
+ process.stderr.write(
227
+ "usage: service-read.mjs confluence <page> | crashlytics <issue-url> | figma <file-key> <node-id>\n",
228
+ );
229
+ return 2;
230
+ }
231
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
232
+ return 0;
233
+ } catch (err) {
234
+ if (!(err instanceof ServiceError)) throw err;
235
+ process.stderr.write(`service-read: ${err.message}\n`);
236
+ return 1;
237
+ }
238
+ }
239
+
240
+ if (invokedDirectly(import.meta.url)) {
241
+ runMain("service-read", async () => {
242
+ process.exitCode = await main(process.argv.slice(2));
243
+ });
244
+ }
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file stack-toggle.mjs - read and set which stack plugins a repository
4
+ * enables, the one writer of `.claude/settings.json` `enabledPlugins` for
5
+ * `/multi-agent:stack` and the office panel (POST /v1/stack).
6
+ *
7
+ * The write rules, applied to the repository's settings file and nothing else:
8
+ *
9
+ * - every plugin `pluginsForStacks()` (scripts/_stack-routing.mjs) derives for
10
+ * the selection is set to true;
11
+ * - every other stack toolkit of the marketplace is set to false;
12
+ * - ai-common-toolkit and ai-analyst-toolkit are always true;
13
+ * - retired `ai-*-engineering-toolkit` keys are deleted;
14
+ * - `xcode-integration@xcode-apple` follows `ios`, only when the installer
15
+ * linked Apple's marketplace (`~/.claude/xcode-apple-marketplace`);
16
+ * - every key outside the marketplace and every other setting is kept as it
17
+ * was.
18
+ *
19
+ * An empty selection is refused rather than written: it would turn every stack
20
+ * toolkit off, which nobody asks for by selecting nothing. The file is written
21
+ * atomically (a temporary file renamed over it), keeping its mode.
22
+ *
23
+ * Usage:
24
+ * stack-toggle.mjs status --repo <path> [--json]
25
+ * stack-toggle.mjs set --repo <path> <stack>... [--json] [--dry-run]
26
+ *
27
+ * Exit: 0 done, 1 refused (unknown stack, empty selection, unreadable
28
+ * settings), 2 usage.
29
+ *
30
+ * @module pipeline/scripts/stack-toggle
31
+ */
32
+
33
+ import {
34
+ chmodSync,
35
+ existsSync,
36
+ mkdirSync,
37
+ readFileSync,
38
+ readdirSync,
39
+ renameSync,
40
+ statSync,
41
+ writeFileSync,
42
+ } from "node:fs";
43
+ import { homedir } from "node:os";
44
+ import { isAbsolute, join } from "node:path";
45
+ import {
46
+ ALWAYS_ON,
47
+ KNOWN_STACKS,
48
+ STACK_PLUGIN_ALIAS,
49
+ expandStackArg,
50
+ pluginsForStacks,
51
+ } from "./_stack-routing.mjs";
52
+ import { PLUGIN_MARKETPLACE, pluginSkillRootCandidates } from "../lib/plugin-skill-roots.mjs";
53
+ import { runMain } from "../lib/fatal.mjs";
54
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
55
+
56
+ export const CONTRACT_VERSION = "1.0.0";
57
+ const SUFFIX = `@${PLUGIN_MARKETPLACE}`;
58
+ const XCODE_KEY = "xcode-integration@xcode-apple";
59
+ const RETIRED = /^ai-[a-z]+-engineering-toolkit@multi-agent-plugins$/;
60
+
61
+ export class StackError extends Error {
62
+ constructor(code, message) {
63
+ super(message);
64
+ this.code = code;
65
+ }
66
+ }
67
+
68
+ const home = (env) => env.HOME || homedir();
69
+ const stackPlugin = (s) => `ai-${STACK_PLUGIN_ALIAS[s] ?? s}-toolkit`;
70
+
71
+ /** The repository's settings file. */
72
+ export function settingsPath(repo) {
73
+ return join(repo, ".claude", "settings.json");
74
+ }
75
+
76
+ function readSettings(repo) {
77
+ const p = settingsPath(repo);
78
+ if (!existsSync(p)) return {};
79
+ try {
80
+ const doc = JSON.parse(readFileSync(p, "utf8"));
81
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw new Error("not an object");
82
+ return doc;
83
+ } catch (err) {
84
+ throw new StackError(
85
+ "unreadable-settings",
86
+ `the repository's .claude/settings.json is not valid JSON (${err.message})`,
87
+ );
88
+ }
89
+ }
90
+
91
+ function marketplaceNames(env) {
92
+ const path = join(
93
+ home(env),
94
+ ".claude",
95
+ "plugins",
96
+ "marketplaces",
97
+ PLUGIN_MARKETPLACE,
98
+ ".claude-plugin",
99
+ "marketplace.json",
100
+ );
101
+ try {
102
+ return (JSON.parse(readFileSync(path, "utf8")).plugins || [])
103
+ .map((p) => p.name)
104
+ .filter(Boolean);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /** The skill names a plugin ships, from its best local root. */
111
+ function skillsOf(plugin, env) {
112
+ // Marketplace checkout first, then the plugin cache, newest version first.
113
+ const marketplaceCopy = join(
114
+ home(env),
115
+ ".claude",
116
+ "plugins",
117
+ "marketplaces",
118
+ PLUGIN_MARKETPLACE,
119
+ "plugins",
120
+ plugin,
121
+ "skills",
122
+ );
123
+ for (const root of [marketplaceCopy, ...pluginSkillRootCandidates(home(env), [plugin])]) {
124
+ try {
125
+ return readdirSync(root)
126
+ .filter((d) => existsSync(join(root, d, "SKILL.md")))
127
+ .sort();
128
+ } catch {
129
+ // The next root may be readable.
130
+ }
131
+ }
132
+ return [];
133
+ }
134
+
135
+ /**
136
+ * The repository's current state: which stacks are on, every marketplace
137
+ * plugin with its enablement and skills.
138
+ *
139
+ * @param {string} repo absolute path
140
+ */
141
+ export function stackStatus(repo, env = process.env) {
142
+ if (!isAbsolute(repo)) throw new StackError("bad-repo", "the repository path must be absolute");
143
+ const enabled = readSettings(repo).enabledPlugins ?? {};
144
+ const names = marketplaceNames(env) ?? [...ALWAYS_ON, ...KNOWN_STACKS.map(stackPlugin)];
145
+ const plugins = names.map((name) => ({
146
+ name,
147
+ enabled: enabled[`${name}${SUFFIX}`] === true,
148
+ alwaysOn: ALWAYS_ON.includes(name),
149
+ stack: KNOWN_STACKS.find((s) => stackPlugin(s) === name) ?? null,
150
+ skills: skillsOf(name, env),
151
+ }));
152
+ return {
153
+ contractVersion: CONTRACT_VERSION,
154
+ stacks: KNOWN_STACKS.filter((s) => enabled[`${stackPlugin(s)}${SUFFIX}`] === true),
155
+ knownStacks: KNOWN_STACKS,
156
+ plugins,
157
+ manifestVerified: marketplaceNames(env) !== null,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * The enabledPlugins block the write rules give for a selection.
163
+ *
164
+ * @param {object} current the current enabledPlugins
165
+ * @param {string[]} args stack arguments as a user types them (aliases allowed)
166
+ */
167
+ export function plannedPlugins(current, args, env = process.env) {
168
+ const stacks = [...new Set(args.flatMap((a) => expandStackArg(a)))];
169
+ const unknown = args.filter((a) => !expandStackArg(a).length);
170
+ if (unknown.length) {
171
+ throw new StackError(
172
+ "unknown-stack",
173
+ `unknown stack ${unknown.join(", ")}; one of ${KNOWN_STACKS.join(" ")} mobile fullstack all`,
174
+ );
175
+ }
176
+ if (!stacks.length) {
177
+ throw new StackError("empty-selection", "no stack selected; nothing is changed");
178
+ }
179
+ const { plugins, dropped } = pluginsForStacks(stacks, {
180
+ manifestPath: join(
181
+ home(env),
182
+ ".claude",
183
+ "plugins",
184
+ "marketplaces",
185
+ PLUGIN_MARKETPLACE,
186
+ ".claude-plugin",
187
+ "marketplace.json",
188
+ ),
189
+ });
190
+ const next = {};
191
+ for (const [k, v] of Object.entries(current ?? {})) if (!RETIRED.test(k)) next[k] = v;
192
+ for (const s of KNOWN_STACKS) next[`${stackPlugin(s)}${SUFFIX}`] = false;
193
+ for (const p of plugins) next[`${p}${SUFFIX}`] = true;
194
+ for (const p of ALWAYS_ON) next[`${p}${SUFFIX}`] = true;
195
+ if (existsSync(join(home(env), ".claude", "xcode-apple-marketplace"))) {
196
+ next[XCODE_KEY] = stacks.includes("ios");
197
+ }
198
+ return { stacks, enabledPlugins: next, dropped };
199
+ }
200
+
201
+ /**
202
+ * Apply a selection to the repository's settings file.
203
+ *
204
+ * @returns {{contractVersion: string, stacks: string[], enabledPlugins: object,
205
+ * changed: string[], dropped: string[], written: boolean}}
206
+ */
207
+ export function stackSet(repo, args, { env = process.env, dryRun = false } = {}) {
208
+ if (!isAbsolute(repo)) throw new StackError("bad-repo", "the repository path must be absolute");
209
+ const doc = readSettings(repo);
210
+ const before = doc.enabledPlugins ?? {};
211
+ const plan = plannedPlugins(before, args, env);
212
+ const keys = new Set([...Object.keys(before), ...Object.keys(plan.enabledPlugins)]);
213
+ const changed = [...keys].filter((k) => before[k] !== plan.enabledPlugins[k]).sort();
214
+ if (!dryRun && changed.length) {
215
+ const p = settingsPath(repo);
216
+ mkdirSync(join(repo, ".claude"), { recursive: true });
217
+ const mode = existsSync(p) ? statSync(p).mode & 0o777 : 0o644;
218
+ const tmp = `${p}.${process.pid}.tmp`;
219
+ writeFileSync(
220
+ tmp,
221
+ JSON.stringify({ ...doc, enabledPlugins: plan.enabledPlugins }, null, 2) + "\n",
222
+ );
223
+ chmodSync(tmp, mode);
224
+ renameSync(tmp, p);
225
+ }
226
+ return {
227
+ contractVersion: CONTRACT_VERSION,
228
+ stacks: plan.stacks,
229
+ enabledPlugins: plan.enabledPlugins,
230
+ changed,
231
+ dropped: plan.dropped,
232
+ written: !dryRun && changed.length > 0,
233
+ };
234
+ }
235
+
236
+ function main(argv) {
237
+ const [mode, ...rest] = argv;
238
+ let repo = null;
239
+ let json = false;
240
+ let dryRun = false;
241
+ const stacks = [];
242
+ for (let i = 0; i < rest.length; i++) {
243
+ if (rest[i] === "--repo") repo = rest[++i];
244
+ else if (rest[i] === "--json") json = true;
245
+ else if (rest[i] === "--dry-run") dryRun = true;
246
+ else if (rest[i].startsWith("--")) return usage(`unknown argument ${rest[i]}`);
247
+ else stacks.push(rest[i]);
248
+ }
249
+ repo ??= process.cwd();
250
+ if (mode !== "status" && mode !== "set") return usage();
251
+ if (mode === "status" && stacks.length) return usage("status takes no stacks");
252
+ try {
253
+ const out = mode === "status" ? stackStatus(repo) : stackSet(repo, stacks, { dryRun });
254
+ if (json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
255
+ else if (mode === "status") {
256
+ process.stdout.write(`stacks: ${out.stacks.join(" ") || "(none)"}\n`);
257
+ for (const p of out.plugins)
258
+ process.stdout.write(` ${p.enabled ? "on " : "off"} ${p.name}\n`);
259
+ } else {
260
+ for (const d of out.dropped) process.stderr.write(`not in the marketplace, dropped: ${d}\n`);
261
+ process.stdout.write(JSON.stringify({ enabledPlugins: out.enabledPlugins }, null, 2) + "\n");
262
+ }
263
+ return 0;
264
+ } catch (err) {
265
+ if (err instanceof StackError) {
266
+ process.stderr.write(`stack-toggle: ${err.message}\n`);
267
+ return 1;
268
+ }
269
+ throw err;
270
+ }
271
+ }
272
+
273
+ function usage(why) {
274
+ if (why) process.stderr.write(`stack-toggle: ${why}\n`);
275
+ process.stderr.write(
276
+ "usage: stack-toggle.mjs status --repo <path> [--json] | set --repo <path> <stack>... [--json] [--dry-run]\n",
277
+ );
278
+ return 2;
279
+ }
280
+
281
+ if (invokedDirectly(import.meta.url)) {
282
+ runMain("stack-toggle", () => {
283
+ process.exitCode = main(process.argv.slice(2));
284
+ });
285
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file steer-run.mjs - queue one instruction for a running task, the writer
4
+ * behind `/multi-agent:steer` step 5 and POST /v1/runs/{id}/steer.
5
+ *
6
+ * The instruction is stored as `pendingSteer` in the run's agent-state.json
7
+ * through the state lock (gate-ledger.mjs updateState), never by editing the
8
+ * file, because the running task writes it too. The next phase reads it at its
9
+ * entry, applies it and sets `appliedAt` (multi-agent-refs/phases.md, "Phase
10
+ * entry - pending steer"). One instruction is queued at a time: a new one
11
+ * replaces an unconsumed one, and the replaced text is returned so the caller
12
+ * can show what was dropped.
13
+ *
14
+ * A finished run is refused: nothing would read the instruction.
15
+ *
16
+ * Usage:
17
+ * steer-run.mjs <agent-state.json> --text <instruction>
18
+ * steer-run.mjs <agent-state.json> --text-file <path>
19
+ *
20
+ * Stdout: one JSON line {contractVersion, queuedAt, replaced, status, currentPhase}.
21
+ * Exit: 0 queued, 1 refused, 2 usage.
22
+ *
23
+ * @module pipeline/scripts/steer-run
24
+ */
25
+
26
+ import { readFileSync } from "node:fs";
27
+ import { updateState } from "./gate-ledger.mjs";
28
+ import { runMain } from "../lib/fatal.mjs";
29
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
30
+
31
+ export const CONTRACT_VERSION = "1.0.0";
32
+ export const MAX_STEER_CHARS = 4000;
33
+ const FINISHED = new Set(["complete", "completed", "done"]);
34
+
35
+ export class SteerError extends Error {
36
+ constructor(code, message) {
37
+ super(message);
38
+ this.code = code;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Queue `text` on the run whose state file is `stateFile`.
44
+ *
45
+ * @returns {Promise<{contractVersion: string, queuedAt: string, replaced: string|null,
46
+ * status: string|null, currentPhase: number|null}>}
47
+ */
48
+ export async function queueSteer(stateFile, text) {
49
+ if (typeof text !== "string" || !text.trim()) {
50
+ throw new SteerError("empty", "the instruction is empty");
51
+ }
52
+ if (text.length > MAX_STEER_CHARS) {
53
+ throw new SteerError("too-long", `the instruction is over ${MAX_STEER_CHARS} characters`);
54
+ }
55
+ const queuedAt = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
56
+ let replaced = null;
57
+ let status = null;
58
+ let currentPhase = null;
59
+ await updateState(stateFile, (s) => {
60
+ status = typeof s.status === "string" ? s.status : null;
61
+ currentPhase = Number.isFinite(Number(s.currentPhase)) ? Number(s.currentPhase) : null;
62
+ if (FINISHED.has(status)) {
63
+ throw new SteerError("finished", "the run is finished; nothing would read the instruction");
64
+ }
65
+ const prev = s.pendingSteer;
66
+ if (prev && typeof prev === "object" && !prev.appliedAt && typeof prev.text === "string") {
67
+ replaced = prev.text;
68
+ }
69
+ return { ...s, pendingSteer: { text, at: queuedAt, appliedAt: null, appliedPhase: null } };
70
+ });
71
+ return { contractVersion: CONTRACT_VERSION, queuedAt, replaced, status, currentPhase };
72
+ }
73
+
74
+ async function main(argv) {
75
+ const [file, flag, value, ...rest] = argv;
76
+ if (!file || rest.length || !value || (flag !== "--text" && flag !== "--text-file")) {
77
+ process.stderr.write(
78
+ "usage: steer-run.mjs <agent-state.json> --text <instruction> | --text-file <path>\n",
79
+ );
80
+ return 2;
81
+ }
82
+ const text = flag === "--text" ? value : readFileSync(value, "utf8");
83
+ try {
84
+ process.stdout.write(`${JSON.stringify(await queueSteer(file, text))}\n`);
85
+ return 0;
86
+ } catch (err) {
87
+ if (!(err instanceof SteerError) && err?.code !== "no-state") throw err;
88
+ process.stderr.write(`steer-run: ${err.message}\n`);
89
+ return 1;
90
+ }
91
+ }
92
+
93
+ if (invokedDirectly(import.meta.url)) {
94
+ runMain("steer-run", async () => {
95
+ process.exitCode = await main(process.argv.slice(2));
96
+ });
97
+ }