@bridge_gpt/mcp-server 0.2.1 → 0.2.3

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 (113) hide show
  1. package/README.md +97 -15
  2. package/build/agent-config-credential-migration.js +272 -0
  3. package/build/agents.generated.js +1 -1
  4. package/build/chain-orchestrator.js +16 -1
  5. package/build/commands.generated.js +9 -7
  6. package/build/conductor/bridge-api-client.js +625 -0
  7. package/build/conductor/claude-hook.js +251 -0
  8. package/build/conductor/cli.js +1048 -0
  9. package/build/conductor/data-normalization.js +114 -0
  10. package/build/conductor/doctor.js +164 -0
  11. package/build/conductor/done-gate.js +325 -0
  12. package/build/conductor/epic-reconcile.js +139 -0
  13. package/build/conductor/epic-runtime.js +611 -0
  14. package/build/conductor/epic-state.js +125 -0
  15. package/build/conductor/errors.js +85 -0
  16. package/build/conductor/git-ci-types.js +129 -0
  17. package/build/conductor/git-hooks.js +218 -0
  18. package/build/conductor/git-inspection.js +185 -0
  19. package/build/conductor/git-producer.js +137 -0
  20. package/build/conductor/merge-ledger.js +198 -0
  21. package/build/conductor/paths.js +224 -0
  22. package/build/conductor/plan.js +77 -0
  23. package/build/conductor/pr-ci-producer.js +427 -0
  24. package/build/conductor/pr-discovery.js +135 -0
  25. package/build/conductor/producer-ledger.js +125 -0
  26. package/build/conductor/redaction.js +112 -0
  27. package/build/conductor/store.js +1156 -0
  28. package/build/conductor/supervisor-config.js +150 -0
  29. package/build/conductor/supervisor-escalation.js +244 -0
  30. package/build/conductor/supervisor-judgment-python.js +141 -0
  31. package/build/conductor/supervisor-judgment.js +215 -0
  32. package/build/conductor/supervisor-ledger.js +119 -0
  33. package/build/conductor/supervisor-merge.js +127 -0
  34. package/build/conductor/supervisor-message-relay.js +61 -0
  35. package/build/conductor/supervisor-notification.js +39 -0
  36. package/build/conductor/supervisor-runtime.js +351 -0
  37. package/build/conductor/supervisor-state.js +572 -0
  38. package/build/conductor/supervisor-types.js +16 -0
  39. package/build/conductor/taxonomy.js +58 -0
  40. package/build/conductor/tools.js +367 -0
  41. package/build/conductor/types.js +9 -0
  42. package/build/conductor-bin.js +21 -0
  43. package/build/conductor-claude-hook-bin.js +21 -0
  44. package/build/credential-store.js +175 -4
  45. package/build/credentials-cli.js +223 -0
  46. package/build/decision-page-schema.js +60 -0
  47. package/build/decision-page-template.js +262 -10
  48. package/build/doctor.js +5 -1
  49. package/build/index.js +558 -63
  50. package/build/pipeline-orchestrator.js +5 -1
  51. package/build/pipeline-utils.js +45 -5
  52. package/build/pipelines.generated.js +37 -9
  53. package/build/readme.generated.js +3 -0
  54. package/build/review-tickets.js +596 -0
  55. package/build/scheduled-prompt.js +16 -10
  56. package/build/start-tickets-conductor.js +496 -0
  57. package/build/start-tickets-prereqs.js +32 -23
  58. package/build/start-tickets-repo.js +49 -0
  59. package/build/start-tickets.js +683 -82
  60. package/build/version.generated.js +1 -1
  61. package/design-assets/favicon/android-chrome-192x192.png +0 -0
  62. package/design-assets/favicon/android-chrome-512x512.png +0 -0
  63. package/design-assets/favicon/apple-touch-icon.png +0 -0
  64. package/design-assets/favicon/favicon-16x16.png +0 -0
  65. package/design-assets/favicon/favicon-32x32.png +0 -0
  66. package/design-assets/favicon/favicon.ico +0 -0
  67. package/design-assets/favicon/site.webmanifest +1 -0
  68. package/design-assets/just-logo-rough-draft.png +0 -0
  69. package/package.json +18 -6
  70. package/pipelines/idea-to-ticket.json +5 -0
  71. package/pipelines/plan-epic.json +16 -1
  72. package/pipelines/review-ticket.json +2 -1
  73. package/public/css/main.min.css +2 -0
  74. package/public/css/main.min.css.map +1 -0
  75. package/public/fonts/OFL.txt +93 -0
  76. package/public/fonts/SourceSansPro-Black.ttf +0 -0
  77. package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
  78. package/public/fonts/SourceSansPro-Bold.ttf +0 -0
  79. package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
  80. package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
  81. package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
  82. package/public/fonts/SourceSansPro-Italic.ttf +0 -0
  83. package/public/fonts/SourceSansPro-Light.ttf +0 -0
  84. package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
  85. package/public/fonts/SourceSansPro-Regular.ttf +0 -0
  86. package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
  87. package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
  88. package/public/img/bridge-logo-160x51.webp +0 -0
  89. package/public/img/bridge-logo-300x92.webp +0 -0
  90. package/public/img/favicon/android-chrome-192x192.png +0 -0
  91. package/public/img/favicon/android-chrome-512x512.png +0 -0
  92. package/public/img/favicon/apple-touch-icon.png +0 -0
  93. package/public/img/favicon/favicon-16x16.png +0 -0
  94. package/public/img/favicon/favicon-32x32.png +0 -0
  95. package/public/img/favicon/favicon.ico +0 -0
  96. package/public/img/favicon/site.webmanifest +1 -0
  97. package/public/img/installation/bitbucket/app-password-1.png +0 -0
  98. package/public/img/installation/bitbucket/app-password-2.png +0 -0
  99. package/public/img/installation/bitbucket/create-token-1.png +0 -0
  100. package/public/img/installation/bitbucket/create-token-2.png +0 -0
  101. package/public/img/installation/bitbucket/webhook-1.png +0 -0
  102. package/public/img/installation/github/github-review-webhook.png +0 -0
  103. package/public/img/installation/jira/credentials/api-key.png +0 -0
  104. package/public/img/installation/jira/webhook/create-rule.png +0 -0
  105. package/public/img/installation/jira/webhook/project-settings.png +0 -0
  106. package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
  107. package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
  108. package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
  109. package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
  110. package/public/img/installation/pinecone/pinecone-index.png +0 -0
  111. package/public/js/main.min.js +2 -0
  112. package/public/js/main.min.js.map +1 -0
  113. package/smoke-test/SMOKE-TEST.md +16 -8
@@ -0,0 +1,1048 @@
1
+ /**
2
+ * Local-only conductor CLI.
3
+ *
4
+ * Provides a dependency-light command skeleton for fast git/agent hooks and
5
+ * operator diagnostics that must NOT depend on the Bridge API backend:
6
+ *
7
+ * conductor emit-event --type <t> --source <s> [...]
8
+ * conductor doctor [--json]
9
+ * conductor purge [--json]
10
+ *
11
+ * Every command talks only to the local SQLite ledger. Errors are surfaced as
12
+ * sanitized messages (no stack traces, no secrets, no raw payloads) and
13
+ * {@link runConductorCli} returns a process exit code rather than calling
14
+ * `process.exit` itself (the bin wrapper owns that).
15
+ */
16
+ import { readFileSync, unlinkSync } from "node:fs";
17
+ import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
18
+ import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
19
+ import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
20
+ import { installConductorGitHooks } from "./git-hooks.js";
21
+ import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
22
+ import { buildConductorDoctorReport, formatConductorDoctorReport } from "./doctor.js";
23
+ import { resolveSupervisorConfig } from "./supervisor-config.js";
24
+ // `runSupervisor` is imported LAZILY inside runSuperviseCommand: it transitively
25
+ // pulls in the SQLite store, and a static import here would force every conductor
26
+ // CLI invocation (and every test that mocks ./store.js for the other commands) to
27
+ // resolve the full supervisor/store graph eagerly.
28
+ /** Human-readable usage text for the conductor CLI. */
29
+ export function getConductorUsage() {
30
+ return [
31
+ "Usage: conductor <command> [options]",
32
+ "",
33
+ "Local append-only event ledger for multi-agent coordination.",
34
+ "Talks ONLY to the local SQLite store (~/.config/bridge/events.db); no Bridge API calls.",
35
+ "",
36
+ "Commands:",
37
+ " emit-event Append one semantic event to the ledger",
38
+ " supervise --run-id <id> Run the foreground, run-scoped supervisor loop",
39
+ " epic-tick Run one stateless reconciliation pass for an Epic",
40
+ " epic-status Print read-only health summary of an Epic Run",
41
+ " send-message Enqueue ONE typed supervisor->worker relay message (idempotent)",
42
+ " check-messages Read + ACK pending relay messages for a worker (no redelivery)",
43
+ " doctor Read-only health/diagnostics report (ledger + git hooks)",
44
+ " purge Delete ALL ledger rows (events, messages, supervisor_projection)",
45
+ " install-git-hooks Install local, opportunistic, non-blocking git hooks",
46
+ " git-hook post-commit Run the post-commit producer (invoked by the installed hook)",
47
+ " git-hook reference-transaction --phase <p> --stdin-file <f>",
48
+ " Run the reference-transaction producer (invoked by the hook)",
49
+ "",
50
+ "supervise options:",
51
+ " --run-id <id> Run/session identifier to supervise (required)",
52
+ " --wake-interval-ms <n> Deterministic event-poll cadence (clamped 30000..60000)",
53
+ " --global-timeout-ms <n> Total wall-clock ceiling for the run",
54
+ " --llm-budget-calls <n> Max LLM judgment calls per run before degraded-only mode",
55
+ " --escalation-cooldown-ms <n> Min gap between escalations for the same worker+reason",
56
+ " --no-llm Deterministic-only mode (never call the LLM judgment boundary)",
57
+ "",
58
+ "install-git-hooks notes:",
59
+ " Hooks are LOCAL, unversioned, opportunistic, and bypassable. Missing hooks are a",
60
+ " degraded optional capability and never prevent PR/CI gate evaluation.",
61
+ "",
62
+ "emit-event options:",
63
+ " --type <t> Semantic event type (required). One of:",
64
+ ` ${SEMANTIC_EVENT_TYPES.join(", ")}`,
65
+ " --source <s> Logical producer (required)",
66
+ " --subject <s> Subject the event is about (e.g. ticket key)",
67
+ " --run-id <s> Run/session identifier",
68
+ " --worker-id <s> Worker/agent identifier",
69
+ " --producer <s> Finer-grained producer identity",
70
+ " --schema-version <n> Event schema version (default 1)",
71
+ " --time <iso> ISO-8601 event time (default now)",
72
+ " --confidence <0..1> Confidence score",
73
+ " --observed-via <s> Channel the event was observed through",
74
+ " --data-json <json> Normalized data object (allowlisted top-level keys)",
75
+ " --data-json-stdin Read the complete normalized data object as JSON from stdin",
76
+ " (mutually exclusive with --data-json; keeps raw payloads and",
77
+ " secrets out of the process argument list)",
78
+ " --raw-json <json> Tool-native object; nested under data.raw",
79
+ " --payload-ref <ref> Reference for large external payloads (-> data.payload_ref)",
80
+ " --json Print compact JSON result",
81
+ "",
82
+ "send-message options:",
83
+ " --run-id <s> Run/session identifier (required)",
84
+ " --worker-id <s> Target worker identifier (required)",
85
+ " --type <s> Typed message kind, e.g. supervisor.worker_stalled (required)",
86
+ " --cause-seq <n> Idempotency cause sequence, non-negative integer (required)",
87
+ " --payload-json <json> Compact payload object (allowlisted top-level keys)",
88
+ " --payload-json-stdin Read the payload object as JSON from stdin",
89
+ " (mutually exclusive with --payload-json)",
90
+ " --available-at <iso> ISO-8601 time the message becomes available (default now)",
91
+ " --cooldown-ms <n> Per-call cooldown override in ms",
92
+ " --json Print compact JSON result",
93
+ " Note: a duplicate idempotency key or a same-type message inside the cooldown",
94
+ " window does NOT enqueue a second message.",
95
+ "",
96
+ "check-messages options:",
97
+ " --run-id <s> Run/session identifier (required)",
98
+ " --worker-id <s> Worker identifier (required)",
99
+ " --limit <n> Max messages to deliver/ack (default 10, max 100)",
100
+ " --json Print compact JSON result",
101
+ " Note: returned messages are ACKNOWLEDGED by this call and are not redelivered.",
102
+ "",
103
+ "doctor / purge options:",
104
+ " --json Print machine-readable JSON",
105
+ "",
106
+ "Examples:",
107
+ " conductor emit-event --type run.started --source git-hook --run-id BAPI-393 \\",
108
+ " --data-json '{\"summary\":\"run started\"}'",
109
+ " conductor emit-event --type git.commit_created --source git-hook \\",
110
+ " --raw-json '{\"branch\":\"feature/x\",\"sha\":\"abc123\"}'",
111
+ " conductor emit-event --type ci.failed --source ci \\",
112
+ " --payload-ref 'file:///tmp/ci-log.txt'",
113
+ " conductor emit-event --type merge.succeeded --source conductor-merge \\",
114
+ " --worker-id w1 --data-json '{\"summary\":\"auto-merged\",\"status\":\"succeeded\"}'",
115
+ " conductor doctor --json",
116
+ " conductor purge",
117
+ "",
118
+ "epic-tick options:",
119
+ " --epic-key <KEY> Epic key to supervise (required, non-empty)",
120
+ " --scheduled-at <epoch> Epoch-seconds timestamp when this tick was scheduled (optional)",
121
+ " --lease-ttl-seconds <n> Lease TTL in seconds (optional, default 120)",
122
+ "",
123
+ "approve-plan options:",
124
+ " approve-plan <epic_key> --plan-version N [--json]",
125
+ " <epic_key> Epic key (e.g. EPIC-405) (required positional)",
126
+ " --plan-version <n> Strictly positive plan version to approve (required)",
127
+ " --json Print compact JSON result",
128
+ " --help Print this usage message",
129
+ "",
130
+ "epic-status options:",
131
+ " --epic-key <KEY> Epic key to fetch the snapshot for (required)",
132
+ " --json Print compact JSON result",
133
+ " --help Print this usage message",
134
+ "",
135
+ "Examples:",
136
+ " conductor approve-plan EPIC-405 --plan-version 2",
137
+ " conductor approve-plan EPIC-405 --plan-version 2 --json",
138
+ " conductor epic-status --epic-key EPIC-405",
139
+ " conductor epic-status --epic-key EPIC-405 --json",
140
+ ].join("\n");
141
+ }
142
+ const VALID_COMMANDS = new Set([
143
+ "emit-event",
144
+ "supervise",
145
+ "epic-tick",
146
+ "approve-plan",
147
+ "epic-status",
148
+ "send-message",
149
+ "check-messages",
150
+ "doctor",
151
+ "purge",
152
+ "install-git-hooks",
153
+ "git-hook",
154
+ ]);
155
+ /**
156
+ * Parse the top-level conductor argv into a subcommand (without a CLI
157
+ * framework). `-h`/`--help` and no-args yield help; an unknown first token is a
158
+ * sanitized error.
159
+ */
160
+ export function parseConductorArgs(argv) {
161
+ if (argv.length === 0)
162
+ return { kind: "help" };
163
+ const first = argv[0];
164
+ if (first === "-h" || first === "--help")
165
+ return { kind: "help" };
166
+ if (first.startsWith("-")) {
167
+ return { kind: "error", message: `Unknown option "${first}". Run "conductor --help" for usage.` };
168
+ }
169
+ if (!VALID_COMMANDS.has(first)) {
170
+ return { kind: "error", message: `Unknown command "${first}". Run "conductor --help" for usage.` };
171
+ }
172
+ return { kind: "command", command: first, argv: argv.slice(1) };
173
+ }
174
+ /**
175
+ * Tokenize `--flag value` / `--flag=value` / boolean flags. Unknown flags and
176
+ * stray positionals raise a {@link ConductorValidationError} with actionable text.
177
+ */
178
+ function tokenizeFlags(argv, valueFlags, boolFlags) {
179
+ const values = new Map();
180
+ const bools = new Set();
181
+ for (let i = 0; i < argv.length; i += 1) {
182
+ const token = argv[i];
183
+ if (token === "-h") {
184
+ bools.add("--help");
185
+ continue;
186
+ }
187
+ if (!token.startsWith("--")) {
188
+ throw new ConductorValidationError(`Unexpected argument "${token}".`);
189
+ }
190
+ const eq = token.indexOf("=");
191
+ const name = eq >= 0 ? token.slice(0, eq) : token;
192
+ if (boolFlags.has(name)) {
193
+ bools.add(name);
194
+ continue;
195
+ }
196
+ if (!valueFlags.has(name)) {
197
+ throw new ConductorValidationError(`Unknown flag "${name}".`);
198
+ }
199
+ let value;
200
+ if (eq >= 0) {
201
+ value = token.slice(eq + 1);
202
+ }
203
+ else {
204
+ const next = argv[i + 1];
205
+ if (next === undefined) {
206
+ throw new ConductorValidationError(`Flag "${name}" requires a value.`);
207
+ }
208
+ value = next;
209
+ i += 1;
210
+ }
211
+ values.set(name, value);
212
+ }
213
+ return { values, bools };
214
+ }
215
+ const EMIT_VALUE_FLAGS = new Set([
216
+ "--type",
217
+ "--source",
218
+ "--subject",
219
+ "--run-id",
220
+ "--worker-id",
221
+ "--producer",
222
+ "--schema-version",
223
+ "--time",
224
+ "--confidence",
225
+ "--observed-via",
226
+ "--data-json",
227
+ "--raw-json",
228
+ "--payload-ref",
229
+ ]);
230
+ const EMIT_BOOL_FLAGS = new Set(["--json", "--help", "--data-json-stdin"]);
231
+ function defaultReadStdin() {
232
+ return readFileSync(0, "utf-8");
233
+ }
234
+ function parseJsonFlag(raw, flag) {
235
+ try {
236
+ return JSON.parse(raw);
237
+ }
238
+ catch {
239
+ throw new ConductorValidationError(`Flag "${flag}" must be valid JSON.`);
240
+ }
241
+ }
242
+ function isPlainObject(value) {
243
+ return value !== null && typeof value === "object" && !Array.isArray(value);
244
+ }
245
+ /**
246
+ * Parse `emit-event` flags into a {@link ConductorEventInput}. `--raw-json` is
247
+ * wrapped under `data.raw` (preserving the normalization boundary for hook
248
+ * producers) and `--payload-ref` is merged into `data.payload_ref`.
249
+ *
250
+ * The normalized `data` object can be supplied either inline via `--data-json`
251
+ * or, for callers (such as the Claude hook writer) that must keep raw payloads
252
+ * and secrets out of the process argument list, through stdin via the boolean
253
+ * `--data-json-stdin` flag. The two are mutually exclusive. `deps.readStdin`
254
+ * defaults to a blocking read of fd 0 and is only invoked when
255
+ * `--data-json-stdin` is present, so the synchronous contract is preserved.
256
+ */
257
+ export function parseEmitEventArgs(argv, deps = {}) {
258
+ const { values, bools } = tokenizeFlags(argv, EMIT_VALUE_FLAGS, EMIT_BOOL_FLAGS);
259
+ if (bools.has("--help")) {
260
+ return { input: { source: "", type: "run.started" }, json: bools.has("--json"), help: true };
261
+ }
262
+ const type = values.get("--type");
263
+ const source = values.get("--source");
264
+ if (!type)
265
+ throw new ConductorValidationError('Flag "--type" is required for emit-event.');
266
+ if (!source)
267
+ throw new ConductorValidationError('Flag "--source" is required for emit-event.');
268
+ // Build the data object: start from --data-json OR --data-json-stdin (never
269
+ // both), then layer raw + payload-ref.
270
+ let data = {};
271
+ const dataJsonRaw = values.get("--data-json");
272
+ const dataJsonStdin = bools.has("--data-json-stdin");
273
+ // Reject the ambiguous combination BEFORE reading stdin so a misconfigured
274
+ // caller never blocks on an fd-0 read it did not intend.
275
+ if (dataJsonRaw !== undefined && dataJsonStdin) {
276
+ throw new ConductorValidationError('Flags "--data-json" and "--data-json-stdin" are mutually exclusive; pass the normalized data object exactly one way.');
277
+ }
278
+ if (dataJsonStdin) {
279
+ const readStdin = deps.readStdin ?? defaultReadStdin;
280
+ const stdinRaw = readStdin();
281
+ // Reuse the same JSON parse + object-validation path as --data-json. The
282
+ // flag name in any error is the flag itself, never the (possibly secret)
283
+ // stdin content.
284
+ const parsed = parseJsonFlag(stdinRaw, "--data-json-stdin");
285
+ if (!isPlainObject(parsed)) {
286
+ throw new ConductorValidationError('Flag "--data-json-stdin" must be a JSON object.');
287
+ }
288
+ data = { ...parsed };
289
+ }
290
+ else if (dataJsonRaw !== undefined) {
291
+ const parsed = parseJsonFlag(dataJsonRaw, "--data-json");
292
+ if (!isPlainObject(parsed)) {
293
+ throw new ConductorValidationError('Flag "--data-json" must be a JSON object.');
294
+ }
295
+ data = { ...parsed };
296
+ }
297
+ const rawJsonRaw = values.get("--raw-json");
298
+ if (rawJsonRaw !== undefined) {
299
+ const parsedRaw = parseJsonFlag(rawJsonRaw, "--raw-json");
300
+ if (!isPlainObject(parsedRaw)) {
301
+ throw new ConductorValidationError('Flag "--raw-json" must be a JSON object.');
302
+ }
303
+ // Merge with any raw already present in --data-json so neither is dropped.
304
+ const existingRaw = isPlainObject(data.raw) ? data.raw : {};
305
+ data.raw = { ...existingRaw, ...parsedRaw };
306
+ }
307
+ const payloadRef = values.get("--payload-ref");
308
+ if (payloadRef !== undefined) {
309
+ data.payload_ref = payloadRef;
310
+ }
311
+ const schemaVersionRaw = values.get("--schema-version");
312
+ const confidenceRaw = values.get("--confidence");
313
+ const input = {
314
+ source,
315
+ type: type,
316
+ subject: values.get("--subject"),
317
+ run_id: values.get("--run-id"),
318
+ worker_id: values.get("--worker-id"),
319
+ producer: values.get("--producer"),
320
+ time: values.get("--time"),
321
+ observed_via: values.get("--observed-via"),
322
+ data,
323
+ };
324
+ if (schemaVersionRaw !== undefined) {
325
+ const n = Number.parseInt(schemaVersionRaw, 10);
326
+ if (!Number.isFinite(n))
327
+ throw new ConductorValidationError('Flag "--schema-version" must be an integer.');
328
+ input.schema_version = n;
329
+ }
330
+ if (confidenceRaw !== undefined) {
331
+ const n = Number.parseFloat(confidenceRaw);
332
+ if (!Number.isFinite(n))
333
+ throw new ConductorValidationError('Flag "--confidence" must be a number.');
334
+ input.confidence = n;
335
+ }
336
+ return { input, json: bools.has("--json"), help: false };
337
+ }
338
+ /** Run the `emit-event` command. Prints the inserted event summary. */
339
+ export function runEmitEventCommand(argv, deps = {}) {
340
+ const parsed = parseEmitEventArgs(argv, deps);
341
+ if (parsed.help) {
342
+ console.log(getConductorUsage());
343
+ return 0;
344
+ }
345
+ const result = emitConductorEvent(parsed.input);
346
+ if (parsed.json) {
347
+ console.log(JSON.stringify(result));
348
+ }
349
+ else {
350
+ console.log(JSON.stringify(result, null, 2));
351
+ }
352
+ return 0;
353
+ }
354
+ // ---------------------------------------------------------------------------
355
+ // send-message / check-messages (BAPI-397 cooperative message relay)
356
+ // ---------------------------------------------------------------------------
357
+ const SEND_MESSAGE_VALUE_FLAGS = new Set([
358
+ "--run-id",
359
+ "--worker-id",
360
+ "--type",
361
+ "--cause-seq",
362
+ "--payload-json",
363
+ "--available-at",
364
+ "--cooldown-ms",
365
+ ]);
366
+ const SEND_MESSAGE_BOOL_FLAGS = new Set(["--payload-json-stdin", "--json", "--help"]);
367
+ /**
368
+ * Parse `send-message` flags into a {@link SendWorkerMessageInput}. The payload
369
+ * object can be supplied inline via `--payload-json` OR from stdin via
370
+ * `--payload-json-stdin` (mutually exclusive, mirroring `emit-event`). The
371
+ * ambiguous combination is rejected BEFORE any stdin read so a misconfigured
372
+ * caller never blocks on fd 0. `--cause-seq` and `--cooldown-ms` are validated as
373
+ * integers; deep identity/type validation is left to the store boundary.
374
+ */
375
+ export function parseSendMessageArgs(argv, deps = {}) {
376
+ const { values, bools } = tokenizeFlags(argv, SEND_MESSAGE_VALUE_FLAGS, SEND_MESSAGE_BOOL_FLAGS);
377
+ if (bools.has("--help")) {
378
+ return {
379
+ input: { run_id: "", worker_id: "", type: "", cause_seq: 0 },
380
+ json: bools.has("--json"),
381
+ help: true,
382
+ };
383
+ }
384
+ const runId = values.get("--run-id");
385
+ const workerId = values.get("--worker-id");
386
+ const type = values.get("--type");
387
+ const causeSeqRaw = values.get("--cause-seq");
388
+ if (!runId)
389
+ throw new ConductorValidationError('Flag "--run-id" is required for send-message.');
390
+ if (!workerId)
391
+ throw new ConductorValidationError('Flag "--worker-id" is required for send-message.');
392
+ if (!type)
393
+ throw new ConductorValidationError('Flag "--type" is required for send-message.');
394
+ if (causeSeqRaw === undefined) {
395
+ throw new ConductorValidationError('Flag "--cause-seq" is required for send-message.');
396
+ }
397
+ if (!/^\d+$/.test(causeSeqRaw.trim())) {
398
+ throw new ConductorValidationError('Flag "--cause-seq" must be a non-negative integer.');
399
+ }
400
+ const causeSeq = Number.parseInt(causeSeqRaw.trim(), 10);
401
+ let payload = {};
402
+ const payloadInline = values.get("--payload-json");
403
+ const payloadStdin = bools.has("--payload-json-stdin");
404
+ if (payloadInline !== undefined && payloadStdin) {
405
+ throw new ConductorValidationError('Flags "--payload-json" and "--payload-json-stdin" are mutually exclusive; pass the payload object exactly one way.');
406
+ }
407
+ if (payloadStdin) {
408
+ const readStdin = deps.readStdin ?? defaultReadStdin;
409
+ const parsed = parseJsonFlag(readStdin(), "--payload-json-stdin");
410
+ if (!isPlainObject(parsed)) {
411
+ throw new ConductorValidationError('Flag "--payload-json-stdin" must be a JSON object.');
412
+ }
413
+ payload = { ...parsed };
414
+ }
415
+ else if (payloadInline !== undefined) {
416
+ const parsed = parseJsonFlag(payloadInline, "--payload-json");
417
+ if (!isPlainObject(parsed)) {
418
+ throw new ConductorValidationError('Flag "--payload-json" must be a JSON object.');
419
+ }
420
+ payload = { ...parsed };
421
+ }
422
+ const input = {
423
+ run_id: runId,
424
+ worker_id: workerId,
425
+ type,
426
+ cause_seq: causeSeq,
427
+ payload,
428
+ };
429
+ const availableAt = values.get("--available-at");
430
+ if (availableAt !== undefined)
431
+ input.available_at = availableAt;
432
+ const cooldownRaw = values.get("--cooldown-ms");
433
+ if (cooldownRaw !== undefined) {
434
+ if (!/^\d+$/.test(cooldownRaw.trim())) {
435
+ throw new ConductorValidationError('Flag "--cooldown-ms" must be a non-negative integer.');
436
+ }
437
+ input.cooldown_ms = Number.parseInt(cooldownRaw.trim(), 10);
438
+ }
439
+ return { input, json: bools.has("--json"), help: false };
440
+ }
441
+ /**
442
+ * Run `send-message`. Prints compact JSON when `--json` is set; otherwise a
443
+ * sanitized human summary (message id / status / type only — NEVER the payload).
444
+ */
445
+ export function runSendMessageCommand(argv, deps = {}) {
446
+ const parsed = parseSendMessageArgs(argv, deps);
447
+ if (parsed.help) {
448
+ console.log(getConductorUsage());
449
+ return 0;
450
+ }
451
+ const result = sendWorkerMessage(parsed.input);
452
+ if (parsed.json) {
453
+ console.log(JSON.stringify(result));
454
+ }
455
+ else {
456
+ console.log([
457
+ `Message ${result.status}.`,
458
+ ` id: ${result.message.id}`,
459
+ ` type: ${result.message.type}`,
460
+ ` state: ${result.message.state}`,
461
+ ].join("\n"));
462
+ }
463
+ return 0;
464
+ }
465
+ const CHECK_MESSAGES_VALUE_FLAGS = new Set(["--run-id", "--worker-id", "--limit"]);
466
+ const CHECK_MESSAGES_BOOL_FLAGS = new Set(["--json", "--help"]);
467
+ /**
468
+ * Parse `check-messages` flags into a {@link CheckWorkerMessagesInput}. CLI usage
469
+ * requires EXPLICIT `--run-id` and `--worker-id` — unlike the MCP tool, it never
470
+ * silently falls back to conductor environment identity (an operator must say
471
+ * exactly which worker's queue to drain).
472
+ */
473
+ export function parseCheckMessagesArgs(argv) {
474
+ const { values, bools } = tokenizeFlags(argv, CHECK_MESSAGES_VALUE_FLAGS, CHECK_MESSAGES_BOOL_FLAGS);
475
+ if (bools.has("--help")) {
476
+ return { input: { run_id: "", worker_id: "" }, json: bools.has("--json"), help: true };
477
+ }
478
+ const runId = values.get("--run-id");
479
+ const workerId = values.get("--worker-id");
480
+ if (!runId)
481
+ throw new ConductorValidationError('Flag "--run-id" is required for check-messages.');
482
+ if (!workerId)
483
+ throw new ConductorValidationError('Flag "--worker-id" is required for check-messages.');
484
+ const input = { run_id: runId, worker_id: workerId };
485
+ const limitRaw = values.get("--limit");
486
+ if (limitRaw !== undefined) {
487
+ if (!/^\d+$/.test(limitRaw.trim())) {
488
+ throw new ConductorValidationError('Flag "--limit" must be a positive integer.');
489
+ }
490
+ input.limit = Number.parseInt(limitRaw.trim(), 10);
491
+ }
492
+ return { input, json: bools.has("--json"), help: false };
493
+ }
494
+ /**
495
+ * Run `check-messages`. Prints compact JSON when `--json` is set; otherwise a
496
+ * sanitized human summary (counts + per-message id/type only — NEVER payloads).
497
+ */
498
+ export function runCheckMessagesCommand(argv) {
499
+ const parsed = parseCheckMessagesArgs(argv);
500
+ if (parsed.help) {
501
+ console.log(getConductorUsage());
502
+ return 0;
503
+ }
504
+ const result = checkWorkerMessages(parsed.input);
505
+ if (parsed.json) {
506
+ console.log(JSON.stringify(result));
507
+ return 0;
508
+ }
509
+ const lines = [`Acknowledged ${result.acked_count} of ${result.count} message(s).`];
510
+ for (const message of result.messages) {
511
+ lines.push(` ${message.id} [${message.type}] -> ${message.state}`);
512
+ }
513
+ console.log(lines.join("\n"));
514
+ return 0;
515
+ }
516
+ const DIAGNOSTIC_BOOL_FLAGS = new Set(["--json", "--help"]);
517
+ /**
518
+ * Run the strictly read-only `doctor` command. Combines ledger health, git hook
519
+ * health, and epic-tick schedule enablement status. `--json` emits the full
520
+ * report with `epic_tick` alongside `git_hooks` at the top level.
521
+ */
522
+ export async function runDoctorCommand(argv) {
523
+ const { bools } = tokenizeFlags(argv, new Set(), DIAGNOSTIC_BOOL_FLAGS);
524
+ if (bools.has("--help")) {
525
+ console.log(getConductorUsage());
526
+ return 0;
527
+ }
528
+ // scheduleDeps omitted: buildConductorDoctorReport lazily loads schedule-run.
529
+ const report = await buildConductorDoctorReport({});
530
+ if (bools.has("--json")) {
531
+ console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, epic_tick: report.epic_tick }));
532
+ return 0;
533
+ }
534
+ console.log(formatConductorDoctorReport(report));
535
+ return 0;
536
+ }
537
+ /**
538
+ * Run `install-git-hooks`: install/update the local managed `post-commit` and
539
+ * `reference-transaction` hooks. Returns 0 even when the directory is not a git
540
+ * worktree (degraded optional capability) — never a fatal failure.
541
+ */
542
+ export function runInstallGitHooksCommand(argv) {
543
+ const { bools } = tokenizeFlags(argv, new Set(), DIAGNOSTIC_BOOL_FLAGS);
544
+ if (bools.has("--help")) {
545
+ console.log(getConductorUsage());
546
+ return 0;
547
+ }
548
+ const result = installConductorGitHooks();
549
+ if (bools.has("--json")) {
550
+ console.log(JSON.stringify(result));
551
+ return 0;
552
+ }
553
+ const lines = [
554
+ "Conductor git hooks install",
555
+ "───────────────────────────",
556
+ `is git worktree: ${result.is_worktree}`,
557
+ `hooks dir: ${result.hooks_dir ?? "n/a"}`,
558
+ ];
559
+ for (const hook of result.installed) {
560
+ lines.push(` ${hook.name}: ${hook.action}${hook.warning ? ` (${hook.warning})` : ""}`);
561
+ }
562
+ if (result.warnings.length > 0) {
563
+ lines.push("warnings:");
564
+ for (const w of result.warnings)
565
+ lines.push(` - ${w}`);
566
+ }
567
+ console.log(lines.join("\n"));
568
+ return 0;
569
+ }
570
+ const GIT_HOOK_VALUE_FLAGS = new Set(["--phase", "--stdin-file"]);
571
+ const GIT_HOOK_BOOL_FLAGS = new Set(["--help"]);
572
+ /**
573
+ * Parse `git-hook <subcommand> [--phase <p>] [--stdin-file <f>]`. Validates the
574
+ * subcommand and, for `reference-transaction`, surfaces the phase / stdin-file.
575
+ */
576
+ export function parseGitHookArgs(argv) {
577
+ const subcommand = argv[0];
578
+ if (subcommand !== "post-commit" && subcommand !== "reference-transaction") {
579
+ throw new ConductorValidationError(`Unknown git-hook subcommand "${subcommand ?? ""}". Expected "post-commit" or "reference-transaction".`);
580
+ }
581
+ const { values } = tokenizeFlags(argv.slice(1), GIT_HOOK_VALUE_FLAGS, GIT_HOOK_BOOL_FLAGS);
582
+ return {
583
+ subcommand,
584
+ phase: values.get("--phase"),
585
+ stdinFile: values.get("--stdin-file"),
586
+ };
587
+ }
588
+ /**
589
+ * Run a `git-hook` subcommand. ALWAYS returns exit code 0 (after at most a generic
590
+ * warning) so a conductor producer failure never blocks the git commit/ref update
591
+ * the hook is attached to.
592
+ */
593
+ export function runGitHookCommand(argv) {
594
+ let parsed;
595
+ try {
596
+ parsed = parseGitHookArgs(argv);
597
+ }
598
+ catch {
599
+ // A malformed hook invocation must not block git; warn generically and exit 0.
600
+ process.stderr.write("Warning: conductor git-hook invocation was invalid.\n");
601
+ return 0;
602
+ }
603
+ try {
604
+ if (parsed.subcommand === "post-commit") {
605
+ runPostCommitHookProducer();
606
+ return 0;
607
+ }
608
+ // reference-transaction: read the captured updates from the stdin file.
609
+ let stdin = "";
610
+ if (parsed.stdinFile) {
611
+ try {
612
+ stdin = readFileSync(parsed.stdinFile, "utf-8");
613
+ }
614
+ catch {
615
+ stdin = "";
616
+ }
617
+ finally {
618
+ // The hook writes ref updates to a per-transaction mktemp file; remove it
619
+ // so these never accumulate in /tmp on long-lived checkouts / CI runners.
620
+ try {
621
+ unlinkSync(parsed.stdinFile);
622
+ }
623
+ catch {
624
+ /* best-effort cleanup */
625
+ }
626
+ }
627
+ }
628
+ runReferenceTransactionHookProducer({ phase: parsed.phase ?? "", stdin });
629
+ return 0;
630
+ }
631
+ catch {
632
+ process.stderr.write("Warning: conductor git-hook producer failed.\n");
633
+ return 0;
634
+ }
635
+ }
636
+ // ---------------------------------------------------------------------------
637
+ // epic-tick
638
+ // ---------------------------------------------------------------------------
639
+ const EPIC_TICK_VALUE_FLAGS = new Set([
640
+ "--epic-key",
641
+ "--scheduled-at",
642
+ "--lease-ttl-seconds",
643
+ ]);
644
+ const EPIC_TICK_BOOL_FLAGS = new Set(["--help"]);
645
+ /**
646
+ * Parse `epic-tick` flags. `--epic-key` is required and must be non-empty /
647
+ * non-whitespace. Numeric overrides (`--scheduled-at`, `--lease-ttl-seconds`)
648
+ * are validated as non-negative integers using the shared
649
+ * {@link parsePositiveIntFlag} convention. Malformed input raises a sanitized
650
+ * {@link ConductorValidationError}.
651
+ */
652
+ export function parseEpicTickArgs(argv) {
653
+ const { values, bools } = tokenizeFlags(argv, EPIC_TICK_VALUE_FLAGS, EPIC_TICK_BOOL_FLAGS);
654
+ if (bools.has("--help")) {
655
+ return { epicKey: "", help: true };
656
+ }
657
+ const epicKeyRaw = values.get("--epic-key");
658
+ if (epicKeyRaw === undefined || epicKeyRaw.trim().length === 0) {
659
+ throw new ConductorValidationError('Flag "--epic-key" is required for epic-tick and must be non-empty.');
660
+ }
661
+ const scheduledAt = parsePositiveIntFlag(values, "--scheduled-at");
662
+ const leaseTtlSeconds = parsePositiveIntFlag(values, "--lease-ttl-seconds");
663
+ return { epicKey: epicKeyRaw.trim(), scheduledAt, leaseTtlSeconds, help: false };
664
+ }
665
+ /**
666
+ * Run the `epic-tick` command. Lazily imports the epic runtime so a plain
667
+ * `conductor doctor` / `emit-event` invocation never eagerly resolves the
668
+ * epic/store graph.
669
+ */
670
+ export async function runEpicTickCommand(argv) {
671
+ const parsed = parseEpicTickArgs(argv);
672
+ if (parsed.help) {
673
+ console.log(getConductorUsage());
674
+ return 0;
675
+ }
676
+ // `runEpicTick` is imported LAZILY so the store/supervisor graph is not
677
+ // evaluated for other conductor CLI commands that never need it.
678
+ const { runEpicTick, buildProductionEpicRuntimeDeps } = await import("./epic-runtime.js");
679
+ const prodDeps = await buildProductionEpicRuntimeDeps(parsed.epicKey);
680
+ const result = await runEpicTick({
681
+ epic_key: parsed.epicKey,
682
+ scheduled_at: parsed.scheduledAt !== undefined ? parsed.scheduledAt * 1000 : undefined,
683
+ lease_ttl_seconds: parsed.leaseTtlSeconds,
684
+ }, prodDeps);
685
+ return result.exit_code;
686
+ }
687
+ // ---------------------------------------------------------------------------
688
+ // approve-plan
689
+ // ---------------------------------------------------------------------------
690
+ const APPROVE_PLAN_VALUE_FLAGS = new Set(["--plan-version"]);
691
+ const APPROVE_PLAN_BOOL_FLAGS = new Set(["--json", "--help"]);
692
+ /**
693
+ * Parse `approve-plan` argv. `<epic_key>` is the required first positional
694
+ * argument (a Jira key, e.g. EPIC-405); `--plan-version` is a required
695
+ * strictly-positive integer flag.
696
+ * Malformed input raises a sanitized {@link ConductorValidationError}.
697
+ */
698
+ export function parseApprovePlanArgs(argv) {
699
+ // Short-circuit for bare --help / -h before positional extraction.
700
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
701
+ return { epicKey: "", planVersion: 0, json: false, help: true };
702
+ }
703
+ // Extract the first positional argument (epic_key) before flag tokenization.
704
+ let epicKey;
705
+ let restArgv;
706
+ if (!argv[0].startsWith("-")) {
707
+ epicKey = argv[0];
708
+ restArgv = argv.slice(1);
709
+ }
710
+ else {
711
+ restArgv = argv;
712
+ }
713
+ const { values, bools } = tokenizeFlags(restArgv, APPROVE_PLAN_VALUE_FLAGS, APPROVE_PLAN_BOOL_FLAGS);
714
+ if (bools.has("--help")) {
715
+ return { epicKey: "", planVersion: 0, json: false, help: true };
716
+ }
717
+ if (!epicKey || epicKey.trim().length === 0) {
718
+ throw new ConductorValidationError('Argument "<epic_key>" is required for approve-plan and must be non-empty.');
719
+ }
720
+ const planVersion = parsePositiveIntFlag(values, "--plan-version");
721
+ if (planVersion === undefined) {
722
+ throw new ConductorValidationError('Flag "--plan-version" is required for approve-plan and must be a strictly positive integer.');
723
+ }
724
+ if (planVersion === 0) {
725
+ throw new ConductorValidationError('Flag "--plan-version" must be a strictly positive integer (≥ 1).');
726
+ }
727
+ return { epicKey: epicKey.trim(), planVersion, json: bools.has("--json"), help: false };
728
+ }
729
+ /**
730
+ * Run the `approve-plan` command. Resolves Bridge API credentials, calls the
731
+ * atomic approve endpoint, and prints a human-readable or JSON summary.
732
+ * Returns a process exit code; never calls `process.exit` directly. All
733
+ * errors are wrapped in sanitized {@link toConductorErrorEnvelope} output —
734
+ * no stack traces, tokens, or response bodies are printed.
735
+ */
736
+ export async function runApprovePlanCommand(argv) {
737
+ let parsed;
738
+ try {
739
+ parsed = parseApprovePlanArgs(argv);
740
+ }
741
+ catch (error) {
742
+ const envelope = toConductorErrorEnvelope(error);
743
+ console.error(`Error: ${envelope.message}`);
744
+ return 1;
745
+ }
746
+ if (parsed.help) {
747
+ console.log(getConductorUsage());
748
+ return 0;
749
+ }
750
+ // Lazy import keeps the credential/HTTP graph out of commands that never
751
+ // need Bridge API access (doctor, emit-event, etc.).
752
+ const { resolveConductorBridgeApiAccess, approveEpicPlan } = await import("./bridge-api-client.js");
753
+ const accessResult = await resolveConductorBridgeApiAccess();
754
+ if (!accessResult.ok) {
755
+ const envelope = { error: "CREDENTIALS_UNAVAILABLE", status: 401, message: accessResult.error };
756
+ if (parsed.json) {
757
+ console.log(JSON.stringify({ ok: false, kind: "unauthorized", error: envelope.message }));
758
+ }
759
+ else {
760
+ console.error(`Error: ${accessResult.error}`);
761
+ }
762
+ return 1;
763
+ }
764
+ try {
765
+ const result = await approveEpicPlan(accessResult.access, {
766
+ epicKey: parsed.epicKey,
767
+ planVersion: parsed.planVersion,
768
+ });
769
+ if (!result.ok) {
770
+ throw new ConductorValidationError(`approve-plan rejected: version drift or monotonic constraint violation ` +
771
+ `(kind: ${result.kind}). Ensure --plan-version is strictly greater than ` +
772
+ `the current approved version and that the blob has been stored first.`);
773
+ }
774
+ if (parsed.json) {
775
+ console.log(JSON.stringify({ ok: true, plan_hash: result.plan_hash }));
776
+ }
777
+ else {
778
+ console.log(`Plan approved atomically. ` +
779
+ `Epic Key: ${parsed.epicKey}, ` +
780
+ `Plan Version: ${parsed.planVersion}, ` +
781
+ `Approved Hash: ${result.plan_hash}`);
782
+ }
783
+ return 0;
784
+ }
785
+ catch (error) {
786
+ const envelope = toConductorErrorEnvelope(error);
787
+ if (parsed.json) {
788
+ console.log(JSON.stringify(envelope));
789
+ }
790
+ else {
791
+ console.error(`Error: ${envelope.message}`);
792
+ }
793
+ return envelope.status >= 500 ? 2 : 1;
794
+ }
795
+ }
796
+ // ---------------------------------------------------------------------------
797
+ // epic-status
798
+ // ---------------------------------------------------------------------------
799
+ const EPIC_STATUS_VALUE_FLAGS = new Set(["--epic-key"]);
800
+ const EPIC_STATUS_BOOL_FLAGS = new Set(["--json", "--help"]);
801
+ /**
802
+ * Parse `epic-status` flags. `--epic-key` is required and must be non-empty.
803
+ * Malformed input raises a sanitized {@link ConductorValidationError}.
804
+ */
805
+ export function parseEpicStatusArgs(argv) {
806
+ const { values, bools } = tokenizeFlags(argv, EPIC_STATUS_VALUE_FLAGS, EPIC_STATUS_BOOL_FLAGS);
807
+ if (bools.has("--help")) {
808
+ return { epicKey: "", json: false, help: true };
809
+ }
810
+ const epicKeyRaw = values.get("--epic-key");
811
+ if (epicKeyRaw === undefined || epicKeyRaw.trim().length === 0) {
812
+ throw new ConductorValidationError('Flag "--epic-key" is required for epic-status and must be non-empty.');
813
+ }
814
+ return { epicKey: epicKeyRaw.trim(), json: bools.has("--json"), help: false };
815
+ }
816
+ /**
817
+ * Run the `epic-status` command. Resolves Bridge API credentials, calls
818
+ * `fetchEpicRunState`, and prints either compact JSON or a sanitized
819
+ * human-readable summary (structured status fields only — never raw payloads).
820
+ * Returns a process exit code; never calls `process.exit` directly.
821
+ */
822
+ export async function runEpicStatusCommand(argv) {
823
+ let parsed;
824
+ try {
825
+ parsed = parseEpicStatusArgs(argv);
826
+ }
827
+ catch (error) {
828
+ const envelope = toConductorErrorEnvelope(error);
829
+ console.error(`Error: ${envelope.message}`);
830
+ return 1;
831
+ }
832
+ if (parsed.help) {
833
+ console.log(getConductorUsage());
834
+ return 0;
835
+ }
836
+ // Lazy import keeps the credential/HTTP graph out of commands that never
837
+ // need Bridge API access (doctor, emit-event, etc.).
838
+ const { resolveConductorBridgeApiAccess, fetchEpicRunState, ConductorBridgeApiError: BridgeApiError } = await import("./bridge-api-client.js");
839
+ const accessResult = await resolveConductorBridgeApiAccess();
840
+ if (!accessResult.ok) {
841
+ if (parsed.json) {
842
+ console.log(JSON.stringify({ ok: false, kind: "unauthorized", error: accessResult.error }));
843
+ }
844
+ else {
845
+ console.error(`Error: ${accessResult.error}`);
846
+ }
847
+ return 1;
848
+ }
849
+ try {
850
+ const state = await fetchEpicRunState(accessResult.access, parsed.epicKey);
851
+ if (parsed.json) {
852
+ console.log(JSON.stringify(state));
853
+ return 0;
854
+ }
855
+ // Human-readable summary: structured status fields only, no raw payloads.
856
+ const lines = [];
857
+ const run = state.epic_run;
858
+ lines.push(`Epic Run: ${run.epic_key}`);
859
+ lines.push(` status: ${run.status}`);
860
+ lines.push(` plan_version: ${run.current_plan_version}`);
861
+ const leaseState = run.lease_owner
862
+ ? `${run.lease_owner} (expires ${run.lease_expires_at ?? "unknown"})`
863
+ : "none";
864
+ lines.push(` lease: ${leaseState}`);
865
+ lines.push(` budget: ${run.budget_wall_clock_seconds ?? "unlimited"}s / ${run.budget_cost_cents ?? "unlimited"} cents`);
866
+ lines.push(` consumed: ${run.consumed_wall_clock_seconds}s / ${run.consumed_cost_cents} cents`);
867
+ lines.push(`\nTickets (${state.ticket_statuses.length}):`);
868
+ for (const t of state.ticket_statuses) {
869
+ const dispatchRef = t.dispatch_run_id ? `dispatch_run_id=${t.dispatch_run_id}` : "-";
870
+ lines.push(` ${t.ticket_key} [${t.status}] ${dispatchRef} remediation=${t.remediation_attempts}/${t.remediation_no_progress_attempts}`);
871
+ }
872
+ lines.push(`\nDispatches (${state.dispatches.length}):`);
873
+ for (const d of state.dispatches) {
874
+ const runRef = d.run_id ? `run_id=${d.run_id}` : "-";
875
+ lines.push(` ${d.dispatch_key} [${d.status}] ${runRef}`);
876
+ }
877
+ console.log(lines.join("\n"));
878
+ return 0;
879
+ }
880
+ catch (error) {
881
+ if (error instanceof BridgeApiError && error.status === 404) {
882
+ if (parsed.json) {
883
+ console.log(JSON.stringify({ epic_key: parsed.epicKey, status: "unknown", state: null }));
884
+ }
885
+ else {
886
+ console.log("No such epic found.");
887
+ }
888
+ return 0;
889
+ }
890
+ const envelope = toConductorErrorEnvelope(error);
891
+ if (parsed.json) {
892
+ console.log(JSON.stringify(envelope));
893
+ }
894
+ else {
895
+ console.error(`Error: ${envelope.message}`);
896
+ }
897
+ return envelope.status >= 500 ? 2 : 1;
898
+ }
899
+ }
900
+ // ---------------------------------------------------------------------------
901
+ // supervise
902
+ // ---------------------------------------------------------------------------
903
+ const SUPERVISE_VALUE_FLAGS = new Set([
904
+ "--run-id",
905
+ "--wake-interval-ms",
906
+ "--global-timeout-ms",
907
+ "--llm-budget-calls",
908
+ "--escalation-cooldown-ms",
909
+ ]);
910
+ const SUPERVISE_BOOL_FLAGS = new Set(["--no-llm", "--help"]);
911
+ /** Parse a required positive-integer flag, raising a sanitized error otherwise. */
912
+ function parsePositiveIntFlag(values, flag) {
913
+ const raw = values.get(flag);
914
+ if (raw === undefined)
915
+ return undefined;
916
+ if (!/^\d+$/.test(raw.trim())) {
917
+ throw new ConductorValidationError(`Flag "${flag}" must be a non-negative integer.`);
918
+ }
919
+ const n = Number.parseInt(raw.trim(), 10);
920
+ if (!Number.isFinite(n)) {
921
+ throw new ConductorValidationError(`Flag "${flag}" must be a non-negative integer.`);
922
+ }
923
+ return n;
924
+ }
925
+ /**
926
+ * Parse `supervise` flags. `--run-id` is required and must be non-empty /
927
+ * non-whitespace. Numeric overrides are validated as non-negative integers (the
928
+ * config resolver clamps them to safe bounds); `--no-llm` selects
929
+ * deterministic-only mode. Malformed input raises a sanitized
930
+ * {@link ConductorValidationError}.
931
+ */
932
+ export function parseSuperviseArgs(argv) {
933
+ const { values, bools } = tokenizeFlags(argv, SUPERVISE_VALUE_FLAGS, SUPERVISE_BOOL_FLAGS);
934
+ if (bools.has("--help")) {
935
+ return { runId: "", overrides: {}, help: true };
936
+ }
937
+ const runIdRaw = values.get("--run-id");
938
+ if (runIdRaw === undefined || runIdRaw.trim().length === 0) {
939
+ throw new ConductorValidationError('Flag "--run-id" is required for supervise and must be non-empty.');
940
+ }
941
+ const overrides = {};
942
+ const wake = parsePositiveIntFlag(values, "--wake-interval-ms");
943
+ if (wake !== undefined)
944
+ overrides.wake_interval_ms = wake;
945
+ const globalTimeout = parsePositiveIntFlag(values, "--global-timeout-ms");
946
+ if (globalTimeout !== undefined)
947
+ overrides.global_timeout_ms = globalTimeout;
948
+ const llmCalls = parsePositiveIntFlag(values, "--llm-budget-calls");
949
+ if (llmCalls !== undefined)
950
+ overrides.llm_max_calls = llmCalls;
951
+ const cooldown = parsePositiveIntFlag(values, "--escalation-cooldown-ms");
952
+ if (cooldown !== undefined)
953
+ overrides.escalation_cooldown_ms = cooldown;
954
+ if (bools.has("--no-llm"))
955
+ overrides.llm_enabled = false;
956
+ return { runId: runIdRaw.trim(), overrides, help: false };
957
+ }
958
+ /**
959
+ * Run the foreground `supervise` command. Resolves config (for a startup
960
+ * banner), runs the supervisor loop, and returns the supervisor's process exit
961
+ * code. Normal foreground output is written through the runtime's injected log
962
+ * functions.
963
+ */
964
+ export async function runSuperviseCommand(argv) {
965
+ const parsed = parseSuperviseArgs(argv);
966
+ if (parsed.help) {
967
+ console.log(getConductorUsage());
968
+ return 0;
969
+ }
970
+ const config = resolveSupervisorConfig(parsed.overrides);
971
+ console.log(`[supervisor] starting run=${parsed.runId} wake=${config.wake_interval_ms}ms ` +
972
+ `global_timeout=${config.global_timeout_ms}ms llm=${config.llm_enabled ? `on(${config.llm_max_calls})` : "off"}`);
973
+ const { runSupervisor } = await import("./supervisor-runtime.js");
974
+ const result = await runSupervisor({ run_id: parsed.runId, config });
975
+ return result.exit_code;
976
+ }
977
+ /** Run the explicit `purge` command. Prints deleted row counts. */
978
+ export function runPurgeCommand(argv) {
979
+ const { bools } = tokenizeFlags(argv, new Set(), DIAGNOSTIC_BOOL_FLAGS);
980
+ if (bools.has("--help")) {
981
+ console.log(getConductorUsage());
982
+ return 0;
983
+ }
984
+ const result = purgeConductorLedger();
985
+ if (bools.has("--json")) {
986
+ console.log(JSON.stringify(result));
987
+ return 0;
988
+ }
989
+ console.log([
990
+ `Conductor ledger purged (existed: ${result.existed}).`,
991
+ ` events: ${result.deleted.events}`,
992
+ ` messages: ${result.deleted.messages}`,
993
+ ` supervisor_projection: ${result.deleted.supervisor_projection}`,
994
+ ].join("\n"));
995
+ return 0;
996
+ }
997
+ /**
998
+ * Dispatch the conductor CLI. Returns (asynchronously) a process exit code
999
+ * (non-zero for parser, validation, and storage failures). The dispatcher is
1000
+ * async so the foreground `supervise` command can be awaited; the synchronous
1001
+ * commands resolve immediately. Only sanitized error messages are printed; stack
1002
+ * traces are never shown.
1003
+ */
1004
+ export async function runConductorCli(argv) {
1005
+ const parsed = parseConductorArgs(argv);
1006
+ if (parsed.kind === "help") {
1007
+ console.log(getConductorUsage());
1008
+ return 0;
1009
+ }
1010
+ if (parsed.kind === "error") {
1011
+ console.error(`Error: ${parsed.message}`);
1012
+ return 1;
1013
+ }
1014
+ try {
1015
+ switch (parsed.command) {
1016
+ case "emit-event":
1017
+ return runEmitEventCommand(parsed.argv);
1018
+ case "supervise":
1019
+ return await runSuperviseCommand(parsed.argv);
1020
+ case "epic-tick":
1021
+ return await runEpicTickCommand(parsed.argv);
1022
+ case "approve-plan":
1023
+ return await runApprovePlanCommand(parsed.argv);
1024
+ case "epic-status":
1025
+ return await runEpicStatusCommand(parsed.argv);
1026
+ case "send-message":
1027
+ return runSendMessageCommand(parsed.argv);
1028
+ case "check-messages":
1029
+ return runCheckMessagesCommand(parsed.argv);
1030
+ case "doctor":
1031
+ return await runDoctorCommand(parsed.argv);
1032
+ case "purge":
1033
+ return runPurgeCommand(parsed.argv);
1034
+ case "install-git-hooks":
1035
+ return runInstallGitHooksCommand(parsed.argv);
1036
+ case "git-hook":
1037
+ return runGitHookCommand(parsed.argv);
1038
+ default:
1039
+ console.error('Error: Unknown command. Run "conductor --help" for usage.');
1040
+ return 1;
1041
+ }
1042
+ }
1043
+ catch (error) {
1044
+ const envelope = toConductorErrorEnvelope(error);
1045
+ console.error(`Error: ${envelope.message}`);
1046
+ return envelope.status >= 500 ? 2 : 1;
1047
+ }
1048
+ }