loadout-ai 0.8.0 → 0.9.1
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 +136 -0
- package/README.md +149 -309
- package/SECURITY.md +21 -1
- package/catalog/discovered.json +29122 -25325
- package/catalog/packages.json +4 -4
- package/dist/src/cli.js +12 -4
- package/dist/src/commands/agents.js +3 -1
- package/dist/src/commands/catalog-candidate.js +150 -0
- package/dist/src/commands/catalog-workflows.js +365 -0
- package/dist/src/commands/catalog.js +9 -354
- package/dist/src/commands/coordinate.js +730 -0
- package/dist/src/commands/coordination-discussions.js +218 -0
- package/dist/src/commands/coordination-sessions.js +197 -0
- package/dist/src/commands/inventory.js +4 -2
- package/dist/src/core/{routing → agents}/model-config.js +7 -2
- package/dist/src/core/catalog/catalog.js +1 -0
- package/dist/src/core/catalog/registry.js +5 -19
- package/dist/src/core/coordination/adapters/claude-code.js +154 -0
- package/dist/src/core/coordination/adapters/codex.js +160 -0
- package/dist/src/core/coordination/adapters/types.js +64 -0
- package/dist/src/core/coordination/auth.js +59 -0
- package/dist/src/core/coordination/auto-contract.js +457 -0
- package/dist/src/core/coordination/bridge-lease.js +79 -0
- package/dist/src/core/coordination/conflict-preview.js +167 -0
- package/dist/src/core/coordination/contract-diff.js +106 -0
- package/dist/src/core/coordination/coordinator.js +552 -0
- package/dist/src/core/coordination/crash-recovery.js +169 -0
- package/dist/src/core/coordination/daemon.js +670 -0
- package/dist/src/core/coordination/discussion-pipeline.js +313 -0
- package/dist/src/core/coordination/discussion.js +340 -0
- package/dist/src/core/coordination/events.js +161 -0
- package/dist/src/core/coordination/git-ownership.js +217 -0
- package/dist/src/core/coordination/http-api.js +118 -0
- package/dist/src/core/coordination/interrupt-policy.js +89 -0
- package/dist/src/core/coordination/lock.js +140 -0
- package/dist/src/core/coordination/mcp-server.js +424 -0
- package/dist/src/core/coordination/quick-start.js +200 -0
- package/dist/src/core/coordination/redaction.js +85 -0
- package/dist/src/core/coordination/replay.js +188 -0
- package/dist/src/core/coordination/retention.js +180 -0
- package/dist/src/core/coordination/runtime.js +21 -0
- package/dist/src/core/coordination/session-manager.js +366 -0
- package/dist/src/core/coordination/watcher.js +128 -0
- package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
- 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 +537 -0
- package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
- package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
- package/dist/src/core/discovery/candidate-intelligence.js +3 -214
- package/dist/src/core/discovery/community.js +12 -4
- package/dist/src/core/discovery/github-discovery.js +6 -2
- package/dist/src/core/discovery/private-discovery.js +6 -2
- package/dist/src/core/install/reconcile.js +1 -1
- package/dist/src/core/install/snapshot.js +24 -5
- package/dist/src/core/install/uninstall.js +39 -3
- package/dist/src/core/reporting/cli-guide.js +9 -5
- package/dist/src/core/reporting/completion.js +43 -38
- package/dist/src/core/reporting/doctor.js +1 -1
- package/dist/src/core/runtime/bounded-json.js +65 -0
- package/dist/src/core/runtime/github.js +30 -16
- package/dist/src/core/runtime/mcp-recipes.js +1 -1
- package/dist/src/core/workspace/active-policy.js +1 -1
- package/docs/DEMO_SCRIPT.md +19 -23
- package/docs/DISCOVERED.md +250 -251
- package/docs/FEATURE_TEST_MATRIX.md +30 -15
- package/docs/LIVE_COLLABORATION.md +283 -0
- package/docs/REFERENCE.md +263 -0
- package/docs/RELEASE_REVIEW.md +1 -1
- package/docs/TESTING.md +17 -1
- package/docs/USER_TEST_GUIDE.md +211 -3
- package/docs/assets/loadout-discover-activate.webp +0 -0
- package/docs/assets/loadout-handoff-coordinate.webp +0 -0
- package/docs/assets/loadout-social-preview.png +0 -0
- package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
- package/docs/decisions/002-local-daemon-authentication.md +32 -0
- package/docs/decisions/003-bounded-agent-discussions.md +91 -0
- package/docs/evidence/coordination-provider-check-2026-09-05.md +33 -0
- package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
- package/docs/specs/HANDOFF_CONTEXT_BUNDLES.md +139 -0
- package/docs/specs/HANDOFF_VERIFICATION.md +83 -0
- package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
- package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
- package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -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 +15 -5
- package/skills/loadout-handoff/SKILL.md +291 -0
- package/dist/src/core/routing/handoff.js +0 -314
- package/dist/src/core/routing/policy.js +0 -147
- package/dist/src/core/routing/route.js +0 -411
- package/docs/assets/loadout-workflow.png +0 -0
- package/skills/loadout-router/SKILL.md +0 -85
- /package/dist/src/core/{routing → agents}/credentials.js +0 -0
package/catalog/packages.json
CHANGED
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"description": "Skills Catalog for Codex.",
|
|
117
117
|
"category": "agent-skills",
|
|
118
118
|
"tier": "official",
|
|
119
|
-
"license": "
|
|
119
|
+
"license": "MIT",
|
|
120
120
|
"components": ["skill"],
|
|
121
121
|
"operatingSystems": ["windows", "macos", "linux"],
|
|
122
122
|
"stars": 23702,
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"description": "Public repository for Agent Skills.",
|
|
140
140
|
"category": "agent-skills",
|
|
141
141
|
"tier": "official",
|
|
142
|
-
"license": "
|
|
142
|
+
"license": "MIT",
|
|
143
143
|
"components": ["skill"],
|
|
144
144
|
"operatingSystems": ["windows", "macos", "linux"],
|
|
145
145
|
"stars": 161108,
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"description": "Vercel's official collection of agent skills.",
|
|
186
186
|
"category": "frontend-development",
|
|
187
187
|
"tier": "official",
|
|
188
|
-
"license": "
|
|
188
|
+
"license": "Apache-2.0",
|
|
189
189
|
"components": ["skill"],
|
|
190
190
|
"operatingSystems": ["windows", "macos", "linux"],
|
|
191
191
|
"stars": 29060,
|
|
@@ -810,7 +810,7 @@
|
|
|
810
810
|
"description": "Official reference MCP servers for filesystem, Git, memory, fetching, time, and protocol testing.",
|
|
811
811
|
"category": "mcp-reference",
|
|
812
812
|
"tier": "official",
|
|
813
|
-
"license": "
|
|
813
|
+
"license": "MIT",
|
|
814
814
|
"components": ["mcp"],
|
|
815
815
|
"operatingSystems": ["windows", "macos", "linux"],
|
|
816
816
|
"stars": 88530,
|
package/dist/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { registerInventory } from "./commands/inventory.js";
|
|
|
12
12
|
import { registerCatalog } from "./commands/catalog.js";
|
|
13
13
|
import { registerMcp } from "./commands/mcp.js";
|
|
14
14
|
import { registerLifecycle } from "./commands/lifecycle.js";
|
|
15
|
+
import { registerCoordinate } from "./commands/coordinate.js";
|
|
15
16
|
const program = new Command();
|
|
16
17
|
program
|
|
17
18
|
.name("loadout")
|
|
@@ -35,10 +36,17 @@ registerInventory(program);
|
|
|
35
36
|
registerCatalog(program);
|
|
36
37
|
registerMcp(program);
|
|
37
38
|
registerLifecycle(program);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
subcommands
|
|
41
|
-
|
|
39
|
+
registerCoordinate(program);
|
|
40
|
+
registerCompletionCommands(program.commands.flatMap((command) => {
|
|
41
|
+
const subcommands = command.commands.flatMap((subcommand) => [
|
|
42
|
+
subcommand.name(),
|
|
43
|
+
...subcommand.aliases(),
|
|
44
|
+
]);
|
|
45
|
+
return [command.name(), ...command.aliases()].map((name) => ({
|
|
46
|
+
name,
|
|
47
|
+
subcommands,
|
|
48
|
+
}));
|
|
49
|
+
}));
|
|
42
50
|
for (const command of program.commands)
|
|
43
51
|
// Commander supports `hidden` when a command is created. These commands are
|
|
44
52
|
// registered by separate feature blocks, so use the same runtime flag here
|
|
@@ -37,7 +37,9 @@ export function registerAgents(program) {
|
|
|
37
37
|
const snapshotId = await applyProjectActivation(plan);
|
|
38
38
|
console.log(options.json
|
|
39
39
|
? JSON.stringify({ plan, snapshotId }, null, 2)
|
|
40
|
-
:
|
|
40
|
+
: snapshotId
|
|
41
|
+
? `${formatProjectActivation(plan)}\nApplied and verified. Snapshot: ${snapshotId}\nRollback: loadout rollback --snapshot ${snapshotId}`
|
|
42
|
+
: `${formatProjectActivation(plan)}\nAlready within the requested active-set limit. No changes or snapshot were needed.`);
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
for (const action of [
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { loadEffectiveCatalog, promoteCatalogCandidate, validateCatalog, } from "../core/catalog/catalog.js";
|
|
4
|
+
import { buildCandidateDossier, buildCatalogProposal, formatCandidateDossier, formatCandidateSummaries, listDiscoveryCandidates, readCandidateDossier, verifyCandidateDossierSource, writeCandidateDossier, } from "../core/discovery/candidate-intelligence.js";
|
|
5
|
+
import { markPromoted } from "../core/discovery/review-queue.js";
|
|
6
|
+
import { writeFileAtomically } from "../core/install/atomic-file.js";
|
|
7
|
+
export function registerCandidateCommands(program) {
|
|
8
|
+
const candidate = program
|
|
9
|
+
.command("candidate")
|
|
10
|
+
.description("Triage and statically inspect daily discovery candidates; never auto-promotes");
|
|
11
|
+
candidate
|
|
12
|
+
.command("list")
|
|
13
|
+
.allowExcessArguments(false)
|
|
14
|
+
.description("Rank discovery leads for human triage, not as universal quality")
|
|
15
|
+
.option("--limit <count>", "maximum candidates", "20")
|
|
16
|
+
.option("--query <words>", "require all search words")
|
|
17
|
+
.option("--include-reviewed", "include repositories already in the catalog")
|
|
18
|
+
.option("--feed <path>", "alternate discovered.json evidence feed")
|
|
19
|
+
.option("--json", "emit machine-readable JSON")
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
const limit = Number(options.limit);
|
|
22
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500)
|
|
23
|
+
throw new Error("--limit must be an integer from 1 to 500");
|
|
24
|
+
const result = await listDiscoveryCandidates({
|
|
25
|
+
limit,
|
|
26
|
+
query: options.query,
|
|
27
|
+
includeReviewed: options.includeReviewed,
|
|
28
|
+
path: options.feed,
|
|
29
|
+
});
|
|
30
|
+
console.log(options.json
|
|
31
|
+
? JSON.stringify(result, null, 2)
|
|
32
|
+
: formatCandidateSummaries(result));
|
|
33
|
+
});
|
|
34
|
+
candidate
|
|
35
|
+
.command("inspect")
|
|
36
|
+
.allowExcessArguments(false)
|
|
37
|
+
.description("Clone one lead at an immutable commit and build a static evidence dossier")
|
|
38
|
+
.argument("<repository>", "owner/repository present in the discovery feed")
|
|
39
|
+
.option("--feed <path>", "alternate discovered.json evidence feed")
|
|
40
|
+
.option("--write", "persist the dossier in private Loadout state")
|
|
41
|
+
.option("--output <path>", "persist at an explicit path (implies --write)")
|
|
42
|
+
.option("--json", "emit the complete dossier as JSON")
|
|
43
|
+
.action(async (repository, options) => {
|
|
44
|
+
const dossier = await buildCandidateDossier(repository, {
|
|
45
|
+
discoveryPath: options.feed,
|
|
46
|
+
});
|
|
47
|
+
const path = options.write || options.output
|
|
48
|
+
? await writeCandidateDossier(dossier, options.output)
|
|
49
|
+
: undefined;
|
|
50
|
+
if (options.json)
|
|
51
|
+
return console.log(JSON.stringify({
|
|
52
|
+
dossier,
|
|
53
|
+
persisted: Boolean(path),
|
|
54
|
+
...(path ? { path } : {}),
|
|
55
|
+
}, null, 2));
|
|
56
|
+
console.log(formatCandidateDossier(dossier));
|
|
57
|
+
if (path)
|
|
58
|
+
console.log(`Dossier: ${path}`);
|
|
59
|
+
else {
|
|
60
|
+
console.log("Preview only. Re-run with --write to persist this dossier.");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
candidate
|
|
64
|
+
.command("propose")
|
|
65
|
+
.allowExcessArguments(false)
|
|
66
|
+
.description("Convert a reviewed dossier into a catalog-record proposal; never edits the catalog")
|
|
67
|
+
.argument("<dossier>", "persisted candidate dossier JSON")
|
|
68
|
+
.requiredOption("--id <id>", "lowercase kebab-case catalog id")
|
|
69
|
+
.requiredOption("--category <category>", "inspected catalog category")
|
|
70
|
+
.requiredOption("--platforms <ids>", "explicitly reviewed comma-separated platforms: windows,macos,linux")
|
|
71
|
+
.option("--display-name <name>", "reviewed display name")
|
|
72
|
+
.option("--description <text>", "reviewed description")
|
|
73
|
+
.option("--license <spdx>", "human-reviewed license override")
|
|
74
|
+
.option("--tier <tier>", "official, stable, trending, or community", "community")
|
|
75
|
+
.option("--approve", "confirm human review and write the proposal")
|
|
76
|
+
.option("--output <path>", "proposal JSON output path; required with --approve")
|
|
77
|
+
.option("--json", "emit machine-readable JSON")
|
|
78
|
+
.action(async (dossierPath, options) => {
|
|
79
|
+
const platforms = options.platforms
|
|
80
|
+
.split(",")
|
|
81
|
+
.map((item) => item.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
const knownPlatforms = new Set(["windows", "macos", "linux"]);
|
|
84
|
+
if (platforms.some((item) => !knownPlatforms.has(item)))
|
|
85
|
+
throw new Error("--platforms supports only windows, macos, and linux");
|
|
86
|
+
const knownTiers = new Set([
|
|
87
|
+
"official",
|
|
88
|
+
"stable",
|
|
89
|
+
"trending",
|
|
90
|
+
"community",
|
|
91
|
+
]);
|
|
92
|
+
if (!knownTiers.has(options.tier))
|
|
93
|
+
throw new Error("--tier is invalid");
|
|
94
|
+
if (options.approve && !options.output)
|
|
95
|
+
throw new Error("--approve requires --output so catalog mutation stays separate");
|
|
96
|
+
const proposal = buildCatalogProposal(await verifyCandidateDossierSource(await readCandidateDossier(dossierPath)), {
|
|
97
|
+
id: options.id,
|
|
98
|
+
category: options.category,
|
|
99
|
+
operatingSystems: platforms,
|
|
100
|
+
tier: options.tier,
|
|
101
|
+
displayName: options.displayName,
|
|
102
|
+
description: options.description,
|
|
103
|
+
license: options.license,
|
|
104
|
+
}, await loadEffectiveCatalog());
|
|
105
|
+
const output = options.approve ? resolve(options.output) : undefined;
|
|
106
|
+
if (output)
|
|
107
|
+
await writeFileAtomically(output, `${JSON.stringify(proposal, null, 2)}\n`);
|
|
108
|
+
if (options.json)
|
|
109
|
+
return console.log(JSON.stringify({
|
|
110
|
+
proposal,
|
|
111
|
+
approved: Boolean(output),
|
|
112
|
+
catalogMutated: false,
|
|
113
|
+
...(output ? { output } : {}),
|
|
114
|
+
}, null, 2));
|
|
115
|
+
console.log(JSON.stringify(proposal, null, 2));
|
|
116
|
+
if (!output)
|
|
117
|
+
console.log("Proposal preview only. Human review is still required; use --approve --output <path> to persist it.");
|
|
118
|
+
else
|
|
119
|
+
console.log(`Approved proposal written to ${output}.`);
|
|
120
|
+
});
|
|
121
|
+
candidate
|
|
122
|
+
.command("promote")
|
|
123
|
+
.allowExcessArguments(false)
|
|
124
|
+
.description("Merge a reviewed proposal into the local catalog; requires --approve")
|
|
125
|
+
.argument("<proposal>", "proposal JSON file from `candidate propose --approve`")
|
|
126
|
+
.option("--approve", "confirm human review and write to catalog")
|
|
127
|
+
.option("--json", "emit machine-readable JSON")
|
|
128
|
+
.action(async (proposalPath, options) => {
|
|
129
|
+
const proposal = JSON.parse(await readFile(resolve(proposalPath), "utf8"));
|
|
130
|
+
validateCatalog([proposal], { requireEvidence: true });
|
|
131
|
+
if (!options.approve) {
|
|
132
|
+
if (options.json)
|
|
133
|
+
return console.log(JSON.stringify({ proposal, promoted: false, catalogMutated: false }, null, 2));
|
|
134
|
+
console.log(JSON.stringify(proposal, null, 2));
|
|
135
|
+
console.log("Preview only. Re-run with --approve to merge into the catalog.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const result = await promoteCatalogCandidate(proposal);
|
|
139
|
+
const item = await markPromoted(proposal.repository);
|
|
140
|
+
if (options.json)
|
|
141
|
+
return console.log(JSON.stringify({
|
|
142
|
+
proposal,
|
|
143
|
+
promoted: true,
|
|
144
|
+
catalogMutated: true,
|
|
145
|
+
...result,
|
|
146
|
+
reviewQueue: item,
|
|
147
|
+
}, null, 2));
|
|
148
|
+
console.log(`Promoted '${proposal.id}' into ${result.catalogPath} (${result.totalRecords} records). Review queue: ${item.repository} marked promoted.`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { parseAgentSelection } from "../core/agents/paths.js";
|
|
2
|
+
import { applyFirstPartySkill, FIRST_PARTY_SKILLS, formatFirstPartySkillList, formatFirstPartySkillPlan, installedFirstPartySkills, planFirstPartySkill, removeFirstPartySkill, } from "../core/delegation/first-party-skills.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";
|
|
7
|
+
import { completionCommandPaths, parseCompletionShell, renderShellCompletion, } from "../core/reporting/completion.js";
|
|
8
|
+
export function registerWorkflowCommands(program) {
|
|
9
|
+
const skills = program
|
|
10
|
+
.command("skills")
|
|
11
|
+
.description("Install the skills Loadout ships so your agents can use Loadout from inside a conversation");
|
|
12
|
+
skills
|
|
13
|
+
.command("list", { isDefault: true })
|
|
14
|
+
.description("Show the skills Loadout ships and whether they are installed")
|
|
15
|
+
.option("--json", "emit machine-readable JSON")
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
const installed = await installedFirstPartySkills();
|
|
18
|
+
console.log(options.json
|
|
19
|
+
? JSON.stringify(FIRST_PARTY_SKILLS.map((skill) => ({
|
|
20
|
+
...skill,
|
|
21
|
+
installed: installed.has(skill.id),
|
|
22
|
+
})), null, 2)
|
|
23
|
+
: formatFirstPartySkillList(installed));
|
|
24
|
+
});
|
|
25
|
+
skills
|
|
26
|
+
.command("install")
|
|
27
|
+
.description("Preview or install one Loadout skill into detected agents")
|
|
28
|
+
.argument("<id>", "skill id, for example loadout-handoff")
|
|
29
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to all detected")
|
|
30
|
+
.option("--yes", "apply after previewing")
|
|
31
|
+
.action(async (id, options) => {
|
|
32
|
+
const plan = await planFirstPartySkill(id, {
|
|
33
|
+
...(options.agents
|
|
34
|
+
? { agents: parseAgentSelection(options.agents) }
|
|
35
|
+
: {}),
|
|
36
|
+
});
|
|
37
|
+
console.log(formatFirstPartySkillPlan(plan));
|
|
38
|
+
if (!plan.targets.length)
|
|
39
|
+
return;
|
|
40
|
+
if (!options.yes) {
|
|
41
|
+
console.log("\nPreview only. Re-run with --yes to install.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const snapshotId = await applyFirstPartySkill(plan);
|
|
45
|
+
console.log(`\nInstalled '${plan.skill.id}' for ${plan.targets.length} agent(s). Snapshot: ${snapshotId}\nStart a new agent session to pick it up.`);
|
|
46
|
+
});
|
|
47
|
+
skills
|
|
48
|
+
.command("remove")
|
|
49
|
+
.description("Preview or remove one Loadout skill from detected agents")
|
|
50
|
+
.argument("<id>", "skill id")
|
|
51
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to all detected")
|
|
52
|
+
.option("--yes", "apply after previewing")
|
|
53
|
+
.action(async (id, options) => {
|
|
54
|
+
const plan = await planFirstPartySkill(id, {
|
|
55
|
+
...(options.agents
|
|
56
|
+
? { agents: parseAgentSelection(options.agents) }
|
|
57
|
+
: {}),
|
|
58
|
+
});
|
|
59
|
+
const present = plan.targets.filter((target) => target.replacing);
|
|
60
|
+
if (!present.length) {
|
|
61
|
+
console.log(`'${id}' is not installed for any detected agent.`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const target of present)
|
|
65
|
+
console.log(` remove → ${target.destination} [${target.displayName}]`);
|
|
66
|
+
if (!options.yes) {
|
|
67
|
+
console.log("\nPreview only. Re-run with --yes to remove.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const snapshotId = await removeFirstPartySkill(plan);
|
|
71
|
+
console.log(`\nRemoved '${id}' from ${present.length} agent(s). Snapshot: ${snapshotId}`);
|
|
72
|
+
});
|
|
73
|
+
const handoff = program
|
|
74
|
+
.command("handoff")
|
|
75
|
+
.description("Hand a task to your other agent, or see what has been handed to you");
|
|
76
|
+
handoff
|
|
77
|
+
.argument("[agent]", "who should do it, for example codex")
|
|
78
|
+
.argument("[task...]", "what they should do")
|
|
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")
|
|
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)")
|
|
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")
|
|
90
|
+
.option("--json", "emit machine-readable JSON")
|
|
91
|
+
.action(async (agent, taskWords, options) => {
|
|
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");
|
|
154
|
+
if (options.done) {
|
|
155
|
+
const outcome = await completeHandoff(cwd, options.done, {
|
|
156
|
+
...(options.runVerification ? { approveCommand: true } : {}),
|
|
157
|
+
...(options.evidence ? { manualEvidence: options.evidence } : {}),
|
|
158
|
+
});
|
|
159
|
+
console.log(options.json
|
|
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;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// No agent named: show what is waiting, for everyone.
|
|
171
|
+
if (!agent) {
|
|
172
|
+
const state = await getHandoffState(cwd);
|
|
173
|
+
console.log(options.json
|
|
174
|
+
? JSON.stringify(state, null, 2)
|
|
175
|
+
: formatHandoffStatus(state));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// An agent with no task means "show me my inbox".
|
|
179
|
+
const task = taskWords.join(" ").trim();
|
|
180
|
+
if (!task) {
|
|
181
|
+
const messages = await readInbox(cwd, agent);
|
|
182
|
+
console.log(options.json
|
|
183
|
+
? JSON.stringify(messages, null, 2)
|
|
184
|
+
: await formatInboxWithBundles(cwd, agent, messages));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Sending is the common case, so it sets itself up rather than failing
|
|
188
|
+
// with instructions to run two other commands first.
|
|
189
|
+
const setup = [];
|
|
190
|
+
if (!(await isHandoffInitialized(cwd))) {
|
|
191
|
+
await initHandoff(cwd);
|
|
192
|
+
setup.push("created .handoff/");
|
|
193
|
+
}
|
|
194
|
+
for (const target of [options.from, agent]) {
|
|
195
|
+
if (!isPickupTarget(target))
|
|
196
|
+
continue;
|
|
197
|
+
const plan = await planPickup(cwd, target);
|
|
198
|
+
if (!plan.replacing) {
|
|
199
|
+
await applyPickup(plan);
|
|
200
|
+
setup.push(`told ${target} to check its inbox`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
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
|
+
}
|
|
235
|
+
if (options.json) {
|
|
236
|
+
console.log(JSON.stringify({ message, setup }, null, 2));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
for (const line of setup)
|
|
240
|
+
console.log(` ${line}`);
|
|
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"`);
|
|
248
|
+
console.log(`It will pick this up next session, or now with: loadout handoff ${agent}`);
|
|
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
|
+
});
|
|
353
|
+
program
|
|
354
|
+
.command("completion")
|
|
355
|
+
.description("Print a shell-completion script; redirect it to your shell profile")
|
|
356
|
+
.argument("[shell]", "bash, zsh, fish, or powershell")
|
|
357
|
+
.option("--commands-json", "print registered command paths as JSON for tooling")
|
|
358
|
+
.action((shell, options) => {
|
|
359
|
+
if (options.commandsJson) {
|
|
360
|
+
console.log(JSON.stringify(completionCommandPaths(), null, 2));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
process.stdout.write(renderShellCompletion(parseCompletionShell(shell ?? "")));
|
|
364
|
+
});
|
|
365
|
+
}
|