@mmerterden/multi-agent-pipeline 20.28.0 → 20.30.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 (186) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +19 -6
  3. package/README.tr.md +5 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +7 -7
  7. package/install/catalog-history.json +1 -1
  8. package/install/claude.mjs +122 -1
  9. package/install/codex.mjs +2 -0
  10. package/install/copilot.mjs +13 -0
  11. package/install/templates/claude-hooks.json +30 -1
  12. package/manifest.json +189 -77
  13. package/package.json +2 -1
  14. package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -1
  15. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
  16. package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
  17. package/pipeline/commands/multi-agent/office-on/SKILL.md +50 -0
  18. package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
  19. package/pipeline/commands/multi-agent/resume/SKILL.md +2 -1
  20. package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
  21. package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
  22. package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
  23. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  24. package/pipeline/contract/CHANGELOG.md +62 -0
  25. package/pipeline/contract/README.md +52 -6
  26. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  27. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  28. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  29. package/pipeline/contract/manifest.json +367 -22
  30. package/pipeline/contract/types/index.d.ts +531 -4
  31. package/pipeline/lib/hook-registration.mjs +252 -0
  32. package/pipeline/lib/prefs-value.mjs +44 -0
  33. package/pipeline/lib/run-completion.mjs +148 -0
  34. package/pipeline/multi-agent-refs/analysis/render.md +4 -4
  35. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -4
  36. package/pipeline/multi-agent-refs/features/doctor.md +11 -0
  37. package/pipeline/multi-agent-refs/features/office-threat-model.md +91 -0
  38. package/pipeline/multi-agent-refs/help/en.md +3 -0
  39. package/pipeline/multi-agent-refs/help/tr.md +3 -0
  40. package/pipeline/multi-agent-refs/phases/operations.md +17 -0
  41. package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
  42. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
  43. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +1 -1
  44. package/pipeline/multi-agent-refs/phases/phase-3-review.md +2 -2
  45. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -6
  46. package/pipeline/multi-agent-refs/phases/phase-5-report.md +5 -4
  47. package/pipeline/office/NOTICE.md +42 -0
  48. package/pipeline/office/assets/BIN.png +0 -0
  49. package/pipeline/office/assets/BOOKSHELF.png +0 -0
  50. package/pipeline/office/assets/CACTUS.png +0 -0
  51. package/pipeline/office/assets/CLOCK.png +0 -0
  52. package/pipeline/office/assets/COFFEE.png +0 -0
  53. package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
  54. package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
  55. package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
  56. package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
  57. package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
  58. package/pipeline/office/assets/DESK_FRONT.png +0 -0
  59. package/pipeline/office/assets/DESK_SIDE.png +0 -0
  60. package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
  61. package/pipeline/office/assets/HANGING_PLANT.png +0 -0
  62. package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
  63. package/pipeline/office/assets/LARGE_PLANT.png +0 -0
  64. package/pipeline/office/assets/PC_BACK.png +0 -0
  65. package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
  66. package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
  67. package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
  68. package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
  69. package/pipeline/office/assets/PC_SIDE.png +0 -0
  70. package/pipeline/office/assets/PLANT.png +0 -0
  71. package/pipeline/office/assets/PLANT_2.png +0 -0
  72. package/pipeline/office/assets/POT.png +0 -0
  73. package/pipeline/office/assets/SMALL_PAINTING.png +0 -0
  74. package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
  75. package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
  76. package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
  77. package/pipeline/office/assets/SOFA_BACK.png +0 -0
  78. package/pipeline/office/assets/SOFA_FRONT.png +0 -0
  79. package/pipeline/office/assets/SOFA_SIDE.png +0 -0
  80. package/pipeline/office/assets/TABLE_FRONT.png +0 -0
  81. package/pipeline/office/assets/WHITEBOARD.png +0 -0
  82. package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
  83. package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
  84. package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
  85. package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
  86. package/pipeline/office/assets/char_0.png +0 -0
  87. package/pipeline/office/assets/char_1.png +0 -0
  88. package/pipeline/office/assets/char_2.png +0 -0
  89. package/pipeline/office/assets/char_3.png +0 -0
  90. package/pipeline/office/assets/char_4.png +0 -0
  91. package/pipeline/office/assets/char_5.png +0 -0
  92. package/pipeline/office/assets/floor_0.png +0 -0
  93. package/pipeline/office/assets/floor_1.png +0 -0
  94. package/pipeline/office/assets/floor_2.png +0 -0
  95. package/pipeline/office/assets/floor_3.png +0 -0
  96. package/pipeline/office/assets/floor_4.png +0 -0
  97. package/pipeline/office/assets/floor_5.png +0 -0
  98. package/pipeline/office/assets/floor_6.png +0 -0
  99. package/pipeline/office/assets/floor_7.png +0 -0
  100. package/pipeline/office/assets/floor_8.png +0 -0
  101. package/pipeline/office/assets/wall_0.png +0 -0
  102. package/pipeline/office/index.html +5319 -0
  103. package/pipeline/preferences-template.json +7 -0
  104. package/pipeline/rules/figma-pipeline.md +2 -0
  105. package/pipeline/schemas/activity-chat.schema.json +78 -0
  106. package/pipeline/schemas/activity-roles.json +48 -0
  107. package/pipeline/schemas/activity-roles.schema.json +49 -0
  108. package/pipeline/schemas/activity.schema.json +429 -0
  109. package/pipeline/schemas/agent-state.schema.json +20 -5
  110. package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
  111. package/pipeline/schemas/autopilot-on.schema.json +111 -0
  112. package/pipeline/schemas/contract-error.schema.json +33 -3
  113. package/pipeline/schemas/issue-maturity.schema.json +68 -0
  114. package/pipeline/schemas/ledger.schema.json +202 -0
  115. package/pipeline/schemas/permissions.schema.json +47 -0
  116. package/pipeline/schemas/phases.json +3 -2
  117. package/pipeline/schemas/phases.schema.json +5 -1
  118. package/pipeline/schemas/prefs.schema.json +56 -0
  119. package/pipeline/schemas/prs.schema.json +73 -0
  120. package/pipeline/schemas/review-verdict-request.schema.json +20 -0
  121. package/pipeline/schemas/review-verdict.schema.json +62 -0
  122. package/pipeline/schemas/routine-delete.schema.json +59 -0
  123. package/pipeline/schemas/routines.schema.json +38 -0
  124. package/pipeline/schemas/run-evidence.schema.json +29 -0
  125. package/pipeline/schemas/run-results.schema.json +218 -0
  126. package/pipeline/schemas/runs-index.schema.json +1 -1
  127. package/pipeline/schemas/service-read.schema.json +58 -0
  128. package/pipeline/schemas/stack-change.schema.json +82 -0
  129. package/pipeline/schemas/stack-request.schema.json +33 -0
  130. package/pipeline/schemas/stack.schema.json +59 -0
  131. package/pipeline/schemas/steer-request.schema.json +25 -0
  132. package/pipeline/schemas/steer.schema.json +59 -0
  133. package/pipeline/schemas/tool-call-request.schema.json +19 -0
  134. package/pipeline/schemas/tool-call.schema.json +47 -0
  135. package/pipeline/schemas/tools.schema.json +60 -0
  136. package/pipeline/scripts/_active-run.mjs +158 -0
  137. package/pipeline/scripts/_autopilot-outcomes.mjs +1 -0
  138. package/pipeline/scripts/_cost.mjs +66 -1
  139. package/pipeline/scripts/_guard_rules.py +605 -0
  140. package/pipeline/scripts/_office-smoke-lib.sh +115 -0
  141. package/pipeline/scripts/_spawn-async.mjs +73 -0
  142. package/pipeline/scripts/_transcript-tail.mjs +274 -0
  143. package/pipeline/scripts/activity-snapshot.mjs +1027 -0
  144. package/pipeline/scripts/agent-guard.py +118 -7
  145. package/pipeline/scripts/agent-guard.sh +50 -3
  146. package/pipeline/scripts/argv-secret-scan.mjs +114 -0
  147. package/pipeline/scripts/autopilot-control.mjs +214 -9
  148. package/pipeline/scripts/autopilot-runner.mjs +8 -2
  149. package/pipeline/scripts/confirm-command.mjs +70 -0
  150. package/pipeline/scripts/contract-server.mjs +1178 -48
  151. package/pipeline/scripts/cost-analyze.mjs +2 -53
  152. package/pipeline/scripts/cost-table.json +1 -0
  153. package/pipeline/scripts/doctor.mjs +54 -0
  154. package/pipeline/scripts/evidence-gate.mjs +8 -4
  155. package/pipeline/scripts/gate-ledger.mjs +163 -7
  156. package/pipeline/scripts/office-build.mjs +89 -0
  157. package/pipeline/scripts/office-control.mjs +383 -0
  158. package/pipeline/scripts/office-ledger.mjs +409 -0
  159. package/pipeline/scripts/plan-coverage-gate.mjs +9 -8
  160. package/pipeline/scripts/pre-commit-check.sh +12 -0
  161. package/pipeline/scripts/routine-registry.mjs +42 -6
  162. package/pipeline/scripts/run-results.mjs +373 -0
  163. package/pipeline/scripts/run-scope-guard.mjs +188 -0
  164. package/pipeline/scripts/run-scope-guard.sh +23 -0
  165. package/pipeline/scripts/run-stop-guard.mjs +246 -0
  166. package/pipeline/scripts/run-stop-guard.sh +25 -0
  167. package/pipeline/scripts/service-read.mjs +244 -0
  168. package/pipeline/scripts/stack-toggle.mjs +285 -0
  169. package/pipeline/scripts/steer-run.mjs +97 -0
  170. package/pipeline/scripts/toolkit-tools.mjs +599 -0
  171. package/pipeline/scripts/uninstall.mjs +4 -0
  172. package/pipeline/scripts/validate-state.mjs +26 -0
  173. package/pipeline/scripts/work-items.mjs +304 -0
  174. package/pipeline/scripts/write-state.mjs +114 -0
  175. package/pipeline/skills/.skill-manifest.json +19 -7
  176. package/pipeline/skills/.skills-index.json +34 -1
  177. package/pipeline/skills/shared/README.md +6 -3
  178. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
  179. package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
  180. package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +22 -0
  181. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
  182. package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
  183. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
  184. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
  185. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  186. package/pipeline/skills/skills-index.md +4 -1
@@ -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
+ }