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.
- package/CHANGELOG.md +97 -0
- package/README.md +45 -50
- package/catalog/discovered.json +29156 -26656
- package/dist/src/commands/catalog-workflows.js +227 -9
- package/dist/src/commands/coordinate.js +148 -4
- package/dist/src/commands/coordination-discussions.js +71 -9
- package/dist/src/core/catalog/safety.js +46 -2
- package/dist/src/core/coordination/adapters/claude-code.js +15 -7
- package/dist/src/core/coordination/adapters/codex.js +29 -2
- package/dist/src/core/coordination/auto-contract.js +457 -0
- package/dist/src/core/coordination/coordinator.js +5 -4
- package/dist/src/core/coordination/daemon.js +6 -3
- package/dist/src/core/coordination/discussion-pipeline.js +313 -0
- package/dist/src/core/coordination/discussion.js +22 -2
- package/dist/src/core/coordination/git-ownership.js +217 -0
- package/dist/src/core/coordination/lock.js +34 -4
- package/dist/src/core/coordination/quick-start.js +200 -0
- package/dist/src/core/coordination/retention.js +67 -5
- package/dist/src/core/delegation/handoff-bundle.js +253 -0
- package/dist/src/core/delegation/handoff-templates.js +222 -0
- package/dist/src/core/delegation/handoff-verification.js +117 -0
- package/dist/src/core/delegation/handoff.js +218 -26
- package/dist/src/core/install/catalog-install.js +8 -2
- package/dist/src/core/install/snapshot.js +49 -6
- package/dist/src/core/install/source.js +8 -6
- package/dist/src/core/install/update.js +55 -1
- package/docs/DISCOVERED.md +249 -251
- package/docs/FEATURE_TEST_MATRIX.md +26 -11
- package/docs/LIVE_COLLABORATION.md +49 -0
- package/docs/REFERENCE.md +100 -0
- package/docs/USER_TEST_GUIDE.md +75 -2
- package/docs/evidence/coordination-provider-check-2026-09-05.md +33 -0
- package/docs/specs/HANDOFF_CONTEXT_BUNDLES.md +139 -0
- package/docs/specs/HANDOFF_VERIFICATION.md +83 -0
- package/docs/superpowers/plans/2026-09-04-handoff-context-bundles.md +109 -0
- package/docs/superpowers/plans/2026-09-04-handoff-verification.md +56 -0
- package/docs/superpowers/plans/2026-09-05-pre-release-hardening.md +175 -0
- package/docs/superpowers/plans/2026-09-05-public-readiness.md +20 -0
- package/package.json +3 -2
- package/skills/loadout-handoff/SKILL.md +68 -15
- 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,
|
|
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
|
|
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(
|
|
86
|
-
:
|
|
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
|
-
:
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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",
|
|
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",
|
|
439
|
-
.option("--max-age <days>", "max age in days",
|
|
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",
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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]
|
|
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) {
|