loadout-ai 0.9.0 → 0.9.2

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 (41) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +45 -50
  3. package/catalog/discovered.json +29156 -26656
  4. package/dist/src/commands/catalog-workflows.js +227 -9
  5. package/dist/src/commands/coordinate.js +148 -4
  6. package/dist/src/commands/coordination-discussions.js +71 -9
  7. package/dist/src/core/catalog/safety.js +46 -2
  8. package/dist/src/core/coordination/adapters/claude-code.js +15 -7
  9. package/dist/src/core/coordination/adapters/codex.js +29 -2
  10. package/dist/src/core/coordination/auto-contract.js +457 -0
  11. package/dist/src/core/coordination/coordinator.js +5 -4
  12. package/dist/src/core/coordination/daemon.js +6 -3
  13. package/dist/src/core/coordination/discussion-pipeline.js +313 -0
  14. package/dist/src/core/coordination/discussion.js +22 -2
  15. package/dist/src/core/coordination/git-ownership.js +217 -0
  16. package/dist/src/core/coordination/lock.js +34 -4
  17. package/dist/src/core/coordination/quick-start.js +200 -0
  18. package/dist/src/core/coordination/retention.js +67 -5
  19. package/dist/src/core/delegation/handoff-bundle.js +253 -0
  20. package/dist/src/core/delegation/handoff-templates.js +222 -0
  21. package/dist/src/core/delegation/handoff-verification.js +117 -0
  22. package/dist/src/core/delegation/handoff.js +218 -26
  23. package/dist/src/core/install/catalog-install.js +8 -2
  24. package/dist/src/core/install/snapshot.js +49 -6
  25. package/dist/src/core/install/source.js +8 -6
  26. package/dist/src/core/install/update.js +55 -1
  27. package/docs/DISCOVERED.md +249 -251
  28. package/docs/FEATURE_TEST_MATRIX.md +26 -11
  29. package/docs/LIVE_COLLABORATION.md +49 -0
  30. package/docs/REFERENCE.md +100 -0
  31. package/docs/USER_TEST_GUIDE.md +75 -2
  32. package/docs/evidence/coordination-provider-check-2026-09-05.md +33 -0
  33. package/docs/specs/HANDOFF_CONTEXT_BUNDLES.md +139 -0
  34. package/docs/specs/HANDOFF_VERIFICATION.md +83 -0
  35. package/docs/superpowers/plans/2026-09-04-handoff-context-bundles.md +109 -0
  36. package/docs/superpowers/plans/2026-09-04-handoff-verification.md +56 -0
  37. package/docs/superpowers/plans/2026-09-05-pre-release-hardening.md +175 -0
  38. package/docs/superpowers/plans/2026-09-05-public-readiness.md +20 -0
  39. package/package.json +3 -2
  40. package/skills/loadout-handoff/SKILL.md +68 -15
  41. package/docs/DEMO_SCRIPT.md +0 -152
@@ -1,6 +1,9 @@
1
1
  import { parseAgentSelection } from "../core/agents/paths.js";
2
2
  import { applyFirstPartySkill, FIRST_PARTY_SKILLS, formatFirstPartySkillList, formatFirstPartySkillPlan, installedFirstPartySkills, planFirstPartySkill, removeFirstPartySkill, } from "../core/delegation/first-party-skills.js";
3
- import { applyPickup, formatHandoffStatus, formatInbox, getHandoffState, initHandoff, isHandoffInitialized, isPickupTarget, markDone, planPickup, readInbox, sendHandoff, } from "../core/delegation/handoff.js";
3
+ import { applyPickup, formatHandoffStatus, formatInboxWithBundles, getHandoffState, initHandoff, isHandoffInitialized, isPickupTarget, planPickup, readInbox, sendHandoff, } from "../core/delegation/handoff.js";
4
+ import { createHandoffBundle, removeHandoffBundle, } from "../core/delegation/handoff-bundle.js";
5
+ import { completeHandoff } from "../core/delegation/handoff-verification.js";
6
+ import { applyTemplate, deleteTemplate, formatTemplateDetail, formatTemplateList, listTemplates, loadTemplate, saveTemplate, } from "../core/delegation/handoff-templates.js";
4
7
  import { completionCommandPaths, parseCompletionShell, renderShellCompletion, } from "../core/reporting/completion.js";
5
8
  export function registerWorkflowCommands(program) {
6
9
  const skills = program
@@ -74,16 +77,94 @@ export function registerWorkflowCommands(program) {
74
77
  .argument("[agent]", "who should do it, for example codex")
75
78
  .argument("[task...]", "what they should do")
76
79
  .option("--context <text>", "anything they need that is not in the task")
80
+ .option("--bundle <paths...>", "attach bounded snapshots of project-relative text files")
77
81
  .option("--from <agent>", "who is sending", "user")
82
+ .option("--verify <criteria>", "acceptance criteria for this task")
83
+ .option("--verify-command <executable>", "executable to run on --done (never uses a shell)")
84
+ .option("--verify-args <json>", "JSON array of literal arguments for --verify-command")
85
+ .option("--verify-timeout <seconds>", "verification timeout in seconds (1-900; default 120)")
78
86
  .option("--done <id>", "mark a task finished")
87
+ .option("--run-verification", "explicitly approve the stored no-shell verification command")
88
+ .option("--evidence <text>", "manual evidence for human-only criteria")
89
+ .option("--template <name>", "use a handoff template for defaults")
79
90
  .option("--json", "emit machine-readable JSON")
80
91
  .action(async (agent, taskWords, options) => {
81
92
  const cwd = process.cwd();
93
+ // Apply template defaults if specified
94
+ if (options.template) {
95
+ const tmpl = await loadTemplate(cwd, options.template);
96
+ if (!tmpl) {
97
+ throw new Error(`Unknown template '${options.template}'. List with: loadout template list`);
98
+ }
99
+ const applied = applyTemplate(tmpl, {
100
+ input: taskWords.join(" ").trim() || undefined,
101
+ agent: agent || undefined,
102
+ });
103
+ if (applied.agent && !agent)
104
+ agent = applied.agent;
105
+ if (applied.description)
106
+ taskWords = [applied.description];
107
+ if (applied.context && !options.context)
108
+ options.context = applied.context;
109
+ if (applied.bundleGlobs?.length) {
110
+ options.bundle = [
111
+ ...new Set([...(options.bundle ?? []), ...applied.bundleGlobs]),
112
+ ];
113
+ }
114
+ if (applied.verifyCriteria && !options.verify)
115
+ options.verify = applied.verifyCriteria;
116
+ if (applied.verifyCommand && !options.verifyCommand) {
117
+ options.verifyCommand = applied.verifyCommand.executable;
118
+ options.verifyArgs = JSON.stringify(applied.verifyCommand.args);
119
+ options.verifyTimeout = String(applied.verifyCommand.timeoutMs / 1000);
120
+ }
121
+ }
122
+ if (options.verifyCommand && !options.verify)
123
+ throw new Error("--verify-command requires --verify");
124
+ if (options.verifyArgs && !options.verifyCommand)
125
+ throw new Error("--verify-args requires --verify-command");
126
+ if (options.verifyTimeout && !options.verifyCommand)
127
+ throw new Error("--verify-timeout requires --verify-command");
128
+ if (options.evidence && !options.done)
129
+ throw new Error("--evidence requires --done");
130
+ if (options.runVerification && !options.done)
131
+ throw new Error("--run-verification requires --done");
132
+ let verifyArgs = [];
133
+ if (options.verifyArgs) {
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(options.verifyArgs);
137
+ }
138
+ catch {
139
+ throw new Error("--verify-args must be a JSON array of strings");
140
+ }
141
+ if (!Array.isArray(parsed) ||
142
+ parsed.some((value) => typeof value !== "string"))
143
+ throw new Error("--verify-args must be a JSON array of strings");
144
+ verifyArgs = parsed;
145
+ }
146
+ const timeoutSeconds = options.verifyTimeout
147
+ ? Number(options.verifyTimeout)
148
+ : 120;
149
+ if (options.verifyCommand &&
150
+ (!Number.isInteger(timeoutSeconds) ||
151
+ timeoutSeconds < 1 ||
152
+ timeoutSeconds > 900))
153
+ throw new Error("--verify-timeout must be an integer from 1 to 900");
82
154
  if (options.done) {
83
- const message = await markDone(cwd, options.done);
155
+ const outcome = await completeHandoff(cwd, options.done, {
156
+ ...(options.runVerification ? { approveCommand: true } : {}),
157
+ ...(options.evidence ? { manualEvidence: options.evidence } : {}),
158
+ });
84
159
  console.log(options.json
85
- ? JSON.stringify(message, null, 2)
86
- : `Marked ${options.done} done.`);
160
+ ? JSON.stringify(outcome, null, 2)
161
+ : outcome.completed
162
+ ? outcome.message.evidence
163
+ ? `Marked ${options.done} done with verification evidence.`
164
+ : `Marked ${options.done} done.`
165
+ : `Verification failed for ${options.done}; the task remains pending.\n${outcome.message.evidence?.stderr || outcome.message.evidence?.stdout || "No command output."}`);
166
+ if (!outcome.completed)
167
+ process.exitCode = 1;
87
168
  return;
88
169
  }
89
170
  // No agent named: show what is waiting, for everyone.
@@ -100,7 +181,7 @@ export function registerWorkflowCommands(program) {
100
181
  const messages = await readInbox(cwd, agent);
101
182
  console.log(options.json
102
183
  ? JSON.stringify(messages, null, 2)
103
- : formatInbox(agent, messages));
184
+ : await formatInboxWithBundles(cwd, agent, messages));
104
185
  return;
105
186
  }
106
187
  // Sending is the common case, so it sets itself up rather than failing
@@ -119,10 +200,38 @@ export function registerWorkflowCommands(program) {
119
200
  setup.push(`told ${target} to check its inbox`);
120
201
  }
121
202
  }
122
- const message = await sendHandoff(cwd, agent, task, {
123
- from: options.from,
124
- ...(options.context ? { context: options.context } : {}),
125
- });
203
+ const bundle = options.bundle
204
+ ? await createHandoffBundle(cwd, options.bundle)
205
+ : undefined;
206
+ let message;
207
+ try {
208
+ message = await sendHandoff(cwd, agent, task, {
209
+ from: options.from,
210
+ ...(options.context ? { context: options.context } : {}),
211
+ ...(bundle ? { bundle } : {}),
212
+ ...(options.verify
213
+ ? {
214
+ verification: {
215
+ criteria: options.verify,
216
+ ...(options.verifyCommand
217
+ ? {
218
+ command: {
219
+ executable: options.verifyCommand,
220
+ args: verifyArgs,
221
+ timeoutMs: timeoutSeconds * 1_000,
222
+ },
223
+ }
224
+ : {}),
225
+ },
226
+ }
227
+ : {}),
228
+ });
229
+ }
230
+ catch (error) {
231
+ if (bundle)
232
+ await removeHandoffBundle(cwd, bundle);
233
+ throw error;
234
+ }
126
235
  if (options.json) {
127
236
  console.log(JSON.stringify({ message, setup }, null, 2));
128
237
  return;
@@ -130,8 +239,117 @@ export function registerWorkflowCommands(program) {
130
239
  for (const line of setup)
131
240
  console.log(` ${line}`);
132
241
  console.log(`Sent to ${agent}: ${task}`);
242
+ if (bundle)
243
+ console.log(`Bundled ${bundle.fileCount} file(s) at ${bundle.path}${bundle.isTruncated ? " (truncated to safety limits)" : ""}.`);
244
+ if (options.verify)
245
+ console.log(options.verifyCommand
246
+ ? `Verification runs only with: loadout handoff --done ${message.id} --run-verification`
247
+ : `Completion requires evidence: loadout handoff --done ${message.id} --evidence "what you checked"`);
133
248
  console.log(`It will pick this up next session, or now with: loadout handoff ${agent}`);
134
249
  });
250
+ // `loadout template` — manage handoff templates
251
+ const template = program
252
+ .command("template")
253
+ .description("Manage reusable handoff task templates");
254
+ template
255
+ .command("list")
256
+ .description("List available templates (built-in and custom)")
257
+ .option("--json", "machine-readable output")
258
+ .action(async (opts) => {
259
+ const templates = await listTemplates(process.cwd());
260
+ if (opts.json) {
261
+ console.log(JSON.stringify({
262
+ custom: templates.custom,
263
+ builtin: templates.builtin,
264
+ }, null, 2));
265
+ }
266
+ else {
267
+ console.log(formatTemplateList(templates));
268
+ }
269
+ });
270
+ template
271
+ .command("show")
272
+ .description("Show details of a template")
273
+ .argument("<name>", "template name")
274
+ .option("--json", "machine-readable output")
275
+ .action(async (name, opts) => {
276
+ const tmpl = await loadTemplate(process.cwd(), name);
277
+ if (!tmpl) {
278
+ console.error(`No template named '${name}'.`);
279
+ process.exitCode = 1;
280
+ return;
281
+ }
282
+ if (opts.json) {
283
+ console.log(JSON.stringify(tmpl, null, 2));
284
+ }
285
+ else {
286
+ console.log(formatTemplateDetail(tmpl));
287
+ }
288
+ });
289
+ template
290
+ .command("create")
291
+ .description("Create a custom handoff template")
292
+ .argument("<name>", "template name (kebab-case)")
293
+ .requiredOption("--description <text>", "one-line description")
294
+ .option("--agent <agent>", "default receiver agent")
295
+ .option("--task <text>", "task template (use {{placeholders}})")
296
+ .option("--context <text>", "default context")
297
+ .option("--bundle <paths...>", "default project-relative bundle paths")
298
+ .option("--verify <criteria>", "default verification criteria")
299
+ .option("--verify-command <executable>", "verification command")
300
+ .option("--verify-args <json>", "JSON array of args for verify command")
301
+ .option("--verify-timeout <seconds>", "verification timeout in seconds")
302
+ .option("--json", "machine-readable output")
303
+ .action(async (name, opts) => {
304
+ const tmpl = {
305
+ name,
306
+ description: opts.description,
307
+ ...(opts.agent ? { defaultAgent: opts.agent } : {}),
308
+ ...(opts.task ? { taskTemplate: opts.task } : {}),
309
+ ...(opts.context ? { context: opts.context } : {}),
310
+ ...(opts.bundle ? { bundleGlobs: opts.bundle } : {}),
311
+ ...(opts.verify ? { verifyCriteria: opts.verify } : {}),
312
+ };
313
+ if (opts.verifyCommand) {
314
+ let args = [];
315
+ if (opts.verifyArgs) {
316
+ const parsed = JSON.parse(opts.verifyArgs);
317
+ if (!Array.isArray(parsed) ||
318
+ parsed.some((v) => typeof v !== "string"))
319
+ throw new Error("--verify-args must be a JSON array of strings");
320
+ args = parsed;
321
+ }
322
+ const timeoutMs = opts.verifyTimeout
323
+ ? Number(opts.verifyTimeout) * 1000
324
+ : 120_000;
325
+ tmpl.verifyCommand = {
326
+ executable: opts.verifyCommand,
327
+ args,
328
+ timeoutMs,
329
+ };
330
+ }
331
+ await saveTemplate(process.cwd(), tmpl);
332
+ if (opts.json) {
333
+ console.log(JSON.stringify(tmpl, null, 2));
334
+ }
335
+ else {
336
+ console.log(`\x1b[32m✓\x1b[0m Template '${name}' saved.`);
337
+ }
338
+ });
339
+ template
340
+ .command("delete")
341
+ .description("Delete a custom template")
342
+ .argument("<name>", "template name to delete")
343
+ .action(async (name) => {
344
+ const deleted = await deleteTemplate(process.cwd(), name);
345
+ if (deleted) {
346
+ console.log(`Deleted template '${name}'.`);
347
+ }
348
+ else {
349
+ console.error(`No custom template named '${name}'.`);
350
+ process.exitCode = 1;
351
+ }
352
+ });
135
353
  program
136
354
  .command("completion")
137
355
  .description("Print a shell-completion script; redirect it to your shell profile")
@@ -7,6 +7,9 @@ import { getDaemonStatus, stopDaemon, activateKillSwitch, deactivateKillSwitch,
7
7
  import { previewConflicts, formatConflictPreview, } from "../core/coordination/conflict-preview.js";
8
8
  import { getContractHistory, diffLatestContract, diffContracts, formatContractDelta, } from "../core/coordination/contract-diff.js";
9
9
  import { buildReplay, formatReplay } from "../core/coordination/replay.js";
10
+ import { quickStart, formatQuickStart, } from "../core/coordination/quick-start.js";
11
+ import { detectContracts, formatDetectionResult, } from "../core/coordination/auto-contract.js";
12
+ import { applyGitOwnership, formatGitOwnership, } from "../core/coordination/git-ownership.js";
10
13
  import { registerCoordinationSessions } from "./coordination-sessions.js";
11
14
  import { registerCoordinationDiscussions } from "./coordination-discussions.js";
12
15
  export function registerCoordinate(program) {
@@ -16,6 +19,147 @@ export function registerCoordinate(program) {
16
19
  .description("Live coordination between agents — contracts, ownership, updates, and snapshots");
17
20
  registerCoordinationSessions(coord);
18
21
  registerCoordinationDiscussions(coord);
22
+ coord
23
+ .command("start")
24
+ .description("Set up coordination in one command — detect project structure, assign ownership, ready to go")
25
+ .requiredOption("--agents <agents>", "two agents, comma-separated (e.g. claude-code,codex)")
26
+ .option("--split <pattern>", "split strategy: backend/frontend or core/tests")
27
+ .option("--yes", "apply ownership (default is dry run)")
28
+ .option("--json", "machine-readable output")
29
+ .action(async (opts) => {
30
+ const agents = opts.agents.split(",").map((a) => a.trim());
31
+ if (agents.length !== 2) {
32
+ console.error("Error: --agents requires exactly two agents.");
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ try {
37
+ const result = await quickStart(process.cwd(), agents, {
38
+ split: opts.split,
39
+ dryRun: !opts.yes,
40
+ });
41
+ if (opts.json) {
42
+ const jsonResult = {
43
+ strategy: result.split.strategy,
44
+ assignments: Object.fromEntries(result.split.assignments),
45
+ unassigned: result.split.unassigned,
46
+ ownershipClaimed: result.ownershipClaimed,
47
+ existingOwnership: result.existingOwnership,
48
+ };
49
+ console.log(JSON.stringify(jsonResult, null, 2));
50
+ }
51
+ else {
52
+ console.log(formatQuickStart(result));
53
+ }
54
+ }
55
+ catch (error) {
56
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
57
+ process.exitCode = 1;
58
+ }
59
+ });
60
+ coord
61
+ .command("detect")
62
+ .description("Auto-detect cross-boundary exports that should be contracts")
63
+ .option("--scope <dirs...>", "only scan these directories (default: whole project)")
64
+ .option("--publish", "prepare publication of detected contracts")
65
+ .option("--yes", "publish exact candidates after previewing")
66
+ .option("--json", "machine-readable output")
67
+ .action(async (opts) => {
68
+ const cwd = process.cwd();
69
+ try {
70
+ const result = await detectContracts(cwd, { scope: opts.scope });
71
+ if (opts.json) {
72
+ console.log(JSON.stringify({
73
+ filesScanned: result.filesScanned,
74
+ crossBoundaryImports: result.crossBoundaryImports.length,
75
+ candidates: result.candidates.map((c) => ({
76
+ name: c.name,
77
+ sourceFile: c.sourceFile,
78
+ sourceAgent: c.sourceAgent,
79
+ consumers: c.consumers,
80
+ symbols: c.sharedSymbols.map((s) => s.name),
81
+ coverageState: c.coverageState,
82
+ publishable: c.publishable,
83
+ sourceHash: c.sourceHash,
84
+ })),
85
+ }, null, 2));
86
+ }
87
+ else {
88
+ console.log(formatDetectionResult(result));
89
+ }
90
+ if (opts.publish) {
91
+ const uncovered = result.candidates.filter((c) => c.coverageState !== "current");
92
+ if (uncovered.length === 0) {
93
+ if (!opts.json)
94
+ console.log("\nAll boundaries already covered by contracts.");
95
+ return;
96
+ }
97
+ const manual = uncovered.filter((c) => !c.publishable);
98
+ if (manual.length > 0) {
99
+ throw new Error(`${manual.length} candidate(s) require manual declarations: ${manual.map((c) => c.name).join(", ")}`);
100
+ }
101
+ if (!opts.yes) {
102
+ if (!opts.json)
103
+ console.log(`\nPreview only — ${uncovered.length} exact contract(s) ready. Re-run with --publish --yes.`);
104
+ return;
105
+ }
106
+ for (const c of uncovered) {
107
+ const event = await publishContract(cwd, {
108
+ from: c.sourceAgent,
109
+ name: c.name,
110
+ body: c.suggestedBody,
111
+ format: "typescript",
112
+ });
113
+ const rev = event.payload.revision;
114
+ if (!opts.json) {
115
+ console.log(`\x1b[32m✓\x1b[0m Published '${c.name}' rev${rev}`);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ catch (error) {
121
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
122
+ process.exitCode = 1;
123
+ }
124
+ });
125
+ coord
126
+ .command("git-ownership")
127
+ .description("Infer directory ownership from git commit history and optionally apply it")
128
+ .requiredOption("--agents <agents>", "agent=Git Author mappings, comma-separated (exact names also work)")
129
+ .option("--depth <n>", "directory grouping depth (default 1)", (_v) => Number(_v), 1)
130
+ .option("--threshold <n>", "minimum % of commits to suggest ownership (default 60)", (_v) => Number(_v), 60)
131
+ .option("--max-commits <n>", "max commits to scan (default 200)", parseInt)
132
+ .option("--yes", "apply suggested ownership (default is dry run)")
133
+ .option("--json", "machine-readable output")
134
+ .action(async (opts) => {
135
+ const agents = opts.agents.split(",").map((a) => a.trim());
136
+ try {
137
+ const result = await applyGitOwnership(process.cwd(), agents, {
138
+ depth: opts.depth,
139
+ threshold: opts.threshold,
140
+ maxCommits: opts.maxCommits,
141
+ dryRun: !opts.yes,
142
+ });
143
+ if (opts.json) {
144
+ console.log(JSON.stringify({
145
+ suggestions: result.suggestions,
146
+ ownershipApplied: result.ownershipApplied,
147
+ authorStats: result.authorStats.map((s) => ({
148
+ author: s.author,
149
+ totalCommits: s.totalCommits,
150
+ directories: Object.fromEntries(s.directories),
151
+ })),
152
+ }, null, 2));
153
+ }
154
+ else {
155
+ console.log(formatGitOwnership(result, !opts.yes));
156
+ }
157
+ }
158
+ catch (error) {
159
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
160
+ process.exitCode = 1;
161
+ }
162
+ });
19
163
  coord
20
164
  .command("snapshot")
21
165
  .description("Current coordination state for an agent")
@@ -196,7 +340,7 @@ export function registerCoordinate(program) {
196
340
  .command("subscribe")
197
341
  .description("Read events after a cursor")
198
342
  .argument("<agent>", "subscribing agent")
199
- .option("--cursor <n>", "sequence number to read after", parseInt, -1)
343
+ .option("--cursor <n>", "sequence number to read after", (_v) => Number(_v), -1)
200
344
  .option("--json", "machine-readable JSON output")
201
345
  .action(async (agent, options) => {
202
346
  const { events, highSeq } = await readAfterCursor(process.cwd(), options.cursor);
@@ -435,8 +579,8 @@ export function registerCoordinate(program) {
435
579
  coord
436
580
  .command("compact")
437
581
  .description("Compact the coordination log — archive old events")
438
- .option("--max-events <n>", "max events to keep", parseInt, 10000)
439
- .option("--max-age <days>", "max age in days", parseInt, 30)
582
+ .option("--max-events <n>", "max events to keep", (v) => Number(v), 10000)
583
+ .option("--max-age <days>", "max age in days", (v) => Number(v), 30)
440
584
  .option("--json", "machine-readable JSON output")
441
585
  .action(async (options) => {
442
586
  const result = await compact(process.cwd(), {
@@ -461,7 +605,7 @@ export function registerCoordinate(program) {
461
605
  daemon
462
606
  .command("start")
463
607
  .description("Start the coordination daemon — HTTP server with live dashboard")
464
- .option("--port <n>", "port to listen on", parseInt, 4510)
608
+ .option("--port <n>", "port to listen on", (_v) => Number(_v), 4510)
465
609
  .action(async (options) => {
466
610
  const cwd = process.cwd();
467
611
  console.log(`Starting coordination daemon on port ${options.port}...`);
@@ -1,8 +1,32 @@
1
1
  import { formatDiscussion, getDiscussion, listDiscussions, requiredDiscussionTurns, runDiscussion, validateDiscussionOptions, } from "../core/coordination/discussion.js";
2
+ import { runPipeline, formatPlan, } from "../core/coordination/discussion-pipeline.js";
2
3
  import { SessionManager } from "../core/coordination/session-manager.js";
3
4
  import { createProviderAdapters } from "../core/coordination/runtime.js";
4
5
  import { acquireBridgeLease } from "../core/coordination/bridge-lease.js";
5
6
  import { parseProviderSessionRef, } from "./coordination-sessions.js";
7
+ const QUOTA_PATTERNS = [
8
+ /rate.?limit/i,
9
+ /quota.?exceed/i,
10
+ /too many requests/i,
11
+ /429/,
12
+ /usage.?limit/i,
13
+ /capacity/i,
14
+ /try again later/i,
15
+ /billing/i,
16
+ /insufficient.?credits/i,
17
+ ];
18
+ export function wrapProviderError(provider, error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ const isQuota = QUOTA_PATTERNS.some((pattern) => pattern.test(message));
21
+ if (isQuota) {
22
+ return new Error(`${provider} appears to be out of quota or rate-limited. ` +
23
+ `The discussion cannot continue until usage resets. ` +
24
+ `Original error: ${message}`);
25
+ }
26
+ return new Error(`${provider} provider error: ${message}`, {
27
+ ...(error instanceof Error ? { cause: error } : {}),
28
+ });
29
+ }
6
30
  function assertBothProviders(values) {
7
31
  if (values.length !== 2 ||
8
32
  new Set(values).size !== 2 ||
@@ -67,17 +91,22 @@ export function createSessionParticipant(sessions, selection, role, cwd, timeout
67
91
  agent: selection.provider,
68
92
  role,
69
93
  async respond(prompt) {
70
- if (!sessionId) {
71
- const started = await sessions.startSession(selection.provider, cwd, prompt, timeoutMs);
72
- sessionId = started.sessionId;
73
- }
74
- else {
75
- const accepted = await sessions.submitTurn(sessionId, prompt, timeoutMs);
76
- if (!accepted) {
77
- throw new Error(`${selection.provider}:${sessionId} rejected the discussion turn`);
94
+ try {
95
+ if (!sessionId) {
96
+ const started = await sessions.startSession(selection.provider, cwd, prompt, timeoutMs);
97
+ sessionId = started.sessionId;
78
98
  }
99
+ else {
100
+ const accepted = await sessions.submitTurn(sessionId, prompt, timeoutMs);
101
+ if (!accepted) {
102
+ throw new Error(`${selection.provider}:${sessionId} rejected the discussion turn`);
103
+ }
104
+ }
105
+ return sessions.getLastResponse(sessionId) ?? "";
106
+ }
107
+ catch (error) {
108
+ throw wrapProviderError(selection.provider, error);
79
109
  }
80
- return sessions.getLastResponse(sessionId) ?? "";
81
110
  },
82
111
  };
83
112
  }
@@ -125,6 +154,29 @@ export function registerCoordinationDiscussions(coord) {
125
154
  ? JSON.stringify(discussion, null, 2)
126
155
  : formatDiscussion(discussion));
127
156
  });
157
+ discuss
158
+ .command("implement")
159
+ .description("Create implementation tasks from a closed discussion's decision")
160
+ .argument("<thread-id>", "discussion thread ID")
161
+ .option("--yes", "send handoff tasks (default is dry run)")
162
+ .option("--json", "machine-readable output")
163
+ .action(async (threadId, options) => {
164
+ try {
165
+ const result = await runPipeline(process.cwd(), threadId, {
166
+ dryRun: !options.yes,
167
+ });
168
+ if (options.json) {
169
+ console.log(JSON.stringify(result, null, 2));
170
+ }
171
+ else {
172
+ console.log(formatPlan(result.plan, !options.yes, result.handoffsSent, result.reused));
173
+ }
174
+ }
175
+ catch (error) {
176
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
177
+ process.exitCode = 1;
178
+ }
179
+ });
128
180
  discuss
129
181
  .command("start")
130
182
  .description("Run a bounded design discussion (spends paid provider turns)")
@@ -166,6 +218,16 @@ export function registerCoordinationDiscussions(coord) {
166
218
  const sessions = createManager(projectRoot);
167
219
  try {
168
220
  await sessions.start();
221
+ // Pre-flight: verify both provider CLIs are reachable before
222
+ // spending any paid turns.
223
+ const detected = await sessions.detectProviders();
224
+ const needed = selection.participants.map((p) => p.provider);
225
+ const missing = needed.filter((name) => !detected.some((d) => d.provider === name));
226
+ if (missing.length) {
227
+ throw new Error(`Cannot start discussion: ${missing.join(", ")} not detected. ` +
228
+ `Install the CLI or check that it is on PATH. ` +
229
+ `Detected providers: ${detected.map((d) => `${d.provider} ${d.version}`).join(", ") || "none"}.`);
230
+ }
169
231
  if (selection.mode === "existing") {
170
232
  for (const item of selection.participants) {
171
233
  await sessions.attachSession(item.provider, item.sessionId, projectRoot);
@@ -22,17 +22,61 @@ const SECRET_PATTERNS = [
22
22
  },
23
23
  ];
24
24
  const SUSPICIOUS_INSTRUCTIONS = [
25
+ // --- Prompt injection / instruction override ---
25
26
  {
26
27
  name: "instruction override",
27
28
  pattern: /\bignore (?:all |any )?(?:previous|prior|system|developer) instructions?\b/i,
28
29
  },
30
+ {
31
+ name: "instruction override (disregard)",
32
+ pattern: /\b(?:disregard|forget|override|bypass)\b.{0,40}\b(?:previous|prior|system|above|rules?|instructions?|guidelines?)\b/i,
33
+ },
34
+ {
35
+ name: "role hijack",
36
+ pattern: /\byou are (?:now |actually )?(?:a|an|the)\b.{0,60}\b(?:assistant|system|admin|root)\b/i,
37
+ },
38
+ {
39
+ name: "hidden system prompt",
40
+ pattern: /\[(?:SYSTEM|INST)\]|<\|(?:system|im_start)\|?>/i,
41
+ },
42
+ {
43
+ name: "encoding evasion",
44
+ pattern: /\b(?:base64|rot13|hex)[- ]?(?:decode|encoded?)\b.{0,60}\b(?:execute|run|eval)\b/i,
45
+ },
46
+ // --- Credential / data exfiltration ---
29
47
  {
30
48
  name: "credential extraction",
31
- pattern: /\b(?:read|print|send|upload|exfiltrate)\b.{0,80}\b(?:credentials?|tokens?|secrets?|\.ssh|\.aws)\b/i,
49
+ pattern: /\b(?:read|print|send|upload|exfiltrate|output|display|echo|cat)\b.{0,80}\b(?:credentials?|tokens?|secrets?|\.ssh|\.aws|\.env|api[_-]?keys?|passwords?)\b/i,
50
+ },
51
+ {
52
+ name: "exfiltration via URL",
53
+ pattern: /\b(?:curl|wget|fetch|http)\b.{0,120}\b(?:credentials?|tokens?|secrets?|api[_-]?key|password|\.env)\b/i,
32
54
  },
55
+ {
56
+ name: "environment variable leak",
57
+ pattern: /\b(?:print|echo|log|send|post)\b.{0,40}\bprocess\.env\b/i,
58
+ },
59
+ // --- Destructive operations ---
33
60
  {
34
61
  name: "hidden destructive command",
35
- pattern: /\b(?:rm\s+-rf|del\s+\/s|format\s+[a-z]:)\b/i,
62
+ pattern: /\b(?:rm\s+-rf|del\s+\/s|format\s+[a-z]:|mkfs\b|dd\s+if=)/i,
63
+ },
64
+ {
65
+ name: "git force push",
66
+ pattern: /\bgit\s+push\s+(?:--force|-f)\b/i,
67
+ },
68
+ // --- Stealth / persistence ---
69
+ {
70
+ name: "hidden file creation",
71
+ pattern: /\b(?:create|write|add|mkdir)\b.{0,60}(?:\/\.[a-z]|\\\.|\bhidden\b)/i,
72
+ },
73
+ {
74
+ name: "cron/scheduler injection",
75
+ pattern: /\b(?:crontab|at\s+now|systemctl\s+enable|launchctl\s+load)\b/i,
76
+ },
77
+ {
78
+ name: "network listener",
79
+ pattern: /\b(?:listen|bind|nc\s+-l|socat|ncat)\b.{0,40}\b(?:port|0\.0\.0\.0|\d{4,5})\b/i,
36
80
  },
37
81
  ];
38
82
  export function detectSecretKinds(content) {