loadout-ai 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +149 -304
  3. package/SECURITY.md +21 -1
  4. package/catalog/discovered.json +27897 -25327
  5. package/catalog/packages.json +4 -4
  6. package/dist/src/cli.js +12 -4
  7. package/dist/src/commands/agents.js +3 -1
  8. package/dist/src/commands/catalog-candidate.js +150 -0
  9. package/dist/src/commands/catalog-workflows.js +147 -0
  10. package/dist/src/commands/catalog.js +9 -354
  11. package/dist/src/commands/coordinate.js +586 -0
  12. package/dist/src/commands/coordination-discussions.js +194 -0
  13. package/dist/src/commands/coordination-sessions.js +197 -0
  14. package/dist/src/commands/inventory.js +4 -2
  15. package/dist/src/core/{routing → agents}/model-config.js +7 -2
  16. package/dist/src/core/catalog/catalog.js +1 -0
  17. package/dist/src/core/catalog/registry.js +5 -19
  18. package/dist/src/core/coordination/adapters/claude-code.js +154 -0
  19. package/dist/src/core/coordination/adapters/codex.js +137 -0
  20. package/dist/src/core/coordination/adapters/types.js +64 -0
  21. package/dist/src/core/coordination/auth.js +59 -0
  22. package/dist/src/core/coordination/bridge-lease.js +79 -0
  23. package/dist/src/core/coordination/conflict-preview.js +167 -0
  24. package/dist/src/core/coordination/contract-diff.js +106 -0
  25. package/dist/src/core/coordination/coordinator.js +552 -0
  26. package/dist/src/core/coordination/crash-recovery.js +169 -0
  27. package/dist/src/core/coordination/daemon.js +667 -0
  28. package/dist/src/core/coordination/discussion.js +340 -0
  29. package/dist/src/core/coordination/events.js +161 -0
  30. package/dist/src/core/coordination/http-api.js +118 -0
  31. package/dist/src/core/coordination/interrupt-policy.js +89 -0
  32. package/dist/src/core/coordination/lock.js +110 -0
  33. package/dist/src/core/coordination/mcp-server.js +424 -0
  34. package/dist/src/core/coordination/redaction.js +85 -0
  35. package/dist/src/core/coordination/replay.js +188 -0
  36. package/dist/src/core/coordination/retention.js +128 -0
  37. package/dist/src/core/coordination/runtime.js +21 -0
  38. package/dist/src/core/coordination/session-manager.js +366 -0
  39. package/dist/src/core/coordination/watcher.js +128 -0
  40. package/dist/src/core/{routing → delegation}/first-party-skills.js +25 -13
  41. package/dist/src/core/{routing → delegation}/handoff.js +40 -9
  42. package/dist/src/core/discovery/candidate-intelligence-evidence.js +108 -0
  43. package/dist/src/core/discovery/candidate-intelligence-types.js +1 -0
  44. package/dist/src/core/discovery/candidate-intelligence-validation.js +108 -0
  45. package/dist/src/core/discovery/candidate-intelligence.js +3 -214
  46. package/dist/src/core/discovery/community.js +12 -4
  47. package/dist/src/core/discovery/github-discovery.js +6 -2
  48. package/dist/src/core/discovery/private-discovery.js +6 -2
  49. package/dist/src/core/install/reconcile.js +1 -1
  50. package/dist/src/core/install/uninstall.js +39 -3
  51. package/dist/src/core/reporting/cli-guide.js +9 -5
  52. package/dist/src/core/reporting/completion.js +43 -38
  53. package/dist/src/core/reporting/doctor.js +1 -1
  54. package/dist/src/core/runtime/bounded-json.js +65 -0
  55. package/dist/src/core/runtime/github.js +30 -16
  56. package/dist/src/core/runtime/mcp-recipes.js +1 -1
  57. package/dist/src/core/workspace/active-policy.js +1 -1
  58. package/docs/DEMO_SCRIPT.md +19 -23
  59. package/docs/DISCOVERED.md +251 -251
  60. package/docs/FEATURE_TEST_MATRIX.md +3 -3
  61. package/docs/LIVE_COLLABORATION.md +234 -0
  62. package/docs/REFERENCE.md +163 -0
  63. package/docs/RELEASE_REVIEW.md +1 -1
  64. package/docs/TESTING.md +17 -1
  65. package/docs/USER_TEST_GUIDE.md +138 -3
  66. package/docs/assets/loadout-discover-activate.webp +0 -0
  67. package/docs/assets/loadout-handoff-coordinate.webp +0 -0
  68. package/docs/assets/loadout-social-preview.png +0 -0
  69. package/docs/decisions/001-coordination-jsonl-locking.md +35 -0
  70. package/docs/decisions/002-local-daemon-authentication.md +32 -0
  71. package/docs/decisions/003-bounded-agent-discussions.md +91 -0
  72. package/docs/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -0
  73. package/docs/superpowers/plans/2026-09-03-coordination-hardening.md +231 -0
  74. package/docs/superpowers/plans/2026-09-03-release-readiness.md +232 -0
  75. package/docs/superpowers/plans/2026-09-04-bounded-agent-discussions.md +121 -0
  76. package/package.json +13 -4
  77. package/skills/loadout-handoff/SKILL.md +238 -0
  78. package/dist/src/core/routing/policy.js +0 -147
  79. package/dist/src/core/routing/route.js +0 -411
  80. package/docs/assets/loadout-workflow.png +0 -0
  81. package/skills/loadout-router/SKILL.md +0 -85
  82. /package/dist/src/core/{routing → agents}/credentials.js +0 -0
@@ -1,26 +1,18 @@
1
- import { explainCatalogScore, loadEffectiveCatalog, loadCatalog, promoteCatalogCandidate, rankCatalog, refreshCatalog, validateCatalog, } from "../core/catalog/catalog.js";
2
- import { detectAgents, parseAgentSelection } from "../core/agents/paths.js";
3
- import { resolve } from "node:path";
4
- import { readFile } from "node:fs/promises";
5
- import { formatModelCatalog, MODEL_CATALOG } from "../core/routing/route.js";
6
- import { BUCKETS, formatAnswer, formatPolicy, guessBucket, policyPath, readPolicy, resolveRoute, setRule, writePolicy, } from "../core/routing/policy.js";
7
- import { rm } from "node:fs/promises";
8
- import { applyFirstPartySkill, FIRST_PARTY_SKILLS, formatFirstPartySkillList, formatFirstPartySkillPlan, installedFirstPartySkills, planFirstPartySkill, removeFirstPartySkill, } from "../core/routing/first-party-skills.js";
9
- import { applyPickup, formatHandoffStatus, formatInbox, getHandoffState, initHandoff, isHandoffInitialized, isPickupTarget, markDone, planPickup, readInbox, sendHandoff, } from "../core/routing/handoff.js";
1
+ import { explainCatalogScore, loadEffectiveCatalog, loadCatalog, rankCatalog, refreshCatalog, } from "../core/catalog/catalog.js";
2
+ import { parseAgentSelection } from "../core/agents/paths.js";
10
3
  import { catalogTrustStage, formatCatalogTrustStage, } from "../core/catalog/profiles.js";
11
4
  import { discoverHackerNewsRepositories } from "../core/discovery/community.js";
12
5
  import { discoverPrivateRepositories } from "../core/discovery/private-discovery.js";
13
6
  import { defaultGitHubDiscoveryQueries, discoverGitHubRepositories, } from "../core/discovery/github-discovery.js";
14
7
  import { formatStarHistory, readCatalogObservations, } from "../core/discovery/observations.js";
15
- import { writeFileAtomically } from "../core/install/atomic-file.js";
16
- import { formatReviewQueue, markPromoted, mergeReviewQueue, readReviewQueue, } from "../core/discovery/review-queue.js";
17
- import { applyProviderModelSelection, defaultModelConfigurationPath, formatProviderModelConfiguration, planProviderModelSelection, readProviderModelConfiguration, requestOpenRouter, } from "../core/routing/model-config.js";
18
- import { parseCompletionShell, renderShellCompletion, } from "../core/reporting/completion.js";
8
+ import { formatReviewQueue, mergeReviewQueue, readReviewQueue, } from "../core/discovery/review-queue.js";
9
+ import { applyProviderModelSelection, defaultModelConfigurationPath, formatProviderModelConfiguration, planProviderModelSelection, readProviderModelConfiguration, requestOpenRouter, } from "../core/agents/model-config.js";
19
10
  import { buildCatalogCoverage, formatCatalogCoverage, } from "../core/catalog/catalog-coverage.js";
20
- import { createCredentialResolver, createOsCredentialStore, } from "../core/routing/credentials.js";
21
- import { buildCandidateDossier, buildCatalogProposal, formatCandidateDossier, formatCandidateSummaries, listDiscoveryCandidates, readCandidateDossier, verifyCandidateDossierSource, writeCandidateDossier, } from "../core/discovery/candidate-intelligence.js";
11
+ import { createCredentialResolver, createOsCredentialStore, } from "../core/agents/credentials.js";
22
12
  import { discoverSkillsSh } from "../core/discovery/skills-sh-discovery.js";
23
13
  import { discoverOfficialMcpRegistry } from "../core/discovery/mcp-registry-discovery.js";
14
+ import { registerCandidateCommands } from "./catalog-candidate.js";
15
+ import { registerWorkflowCommands } from "./catalog-workflows.js";
24
16
  import { readCredentialFromStdin } from "./support.js";
25
17
  export function registerCatalog(program) {
26
18
  program
@@ -92,148 +84,7 @@ export function registerCatalog(program) {
92
84
  for (const failure of result.observationFailures)
93
85
  console.error(`Warning: could not record release observation for ${failure.repository}: ${failure.error}`);
94
86
  });
95
- const candidate = program
96
- .command("candidate")
97
- .description("Triage and statically inspect daily discovery candidates; never auto-promotes");
98
- candidate
99
- .command("list")
100
- .allowExcessArguments(false)
101
- .description("Rank discovery leads for human triage, not as universal quality")
102
- .option("--limit <count>", "maximum candidates", "20")
103
- .option("--query <words>", "require all search words")
104
- .option("--include-reviewed", "include repositories already in the catalog")
105
- .option("--feed <path>", "alternate discovered.json evidence feed")
106
- .option("--json", "emit machine-readable JSON")
107
- .action(async (options) => {
108
- const limit = Number(options.limit);
109
- if (!Number.isInteger(limit) || limit < 1 || limit > 500)
110
- throw new Error("--limit must be an integer from 1 to 500");
111
- const result = await listDiscoveryCandidates({
112
- limit,
113
- query: options.query,
114
- includeReviewed: options.includeReviewed,
115
- path: options.feed,
116
- });
117
- console.log(options.json
118
- ? JSON.stringify(result, null, 2)
119
- : formatCandidateSummaries(result));
120
- });
121
- candidate
122
- .command("inspect")
123
- .allowExcessArguments(false)
124
- .description("Clone one lead at an immutable commit and build a static evidence dossier")
125
- .argument("<repository>", "owner/repository present in the discovery feed")
126
- .option("--feed <path>", "alternate discovered.json evidence feed")
127
- .option("--write", "persist the dossier in private Loadout state")
128
- .option("--output <path>", "persist at an explicit path (implies --write)")
129
- .option("--json", "emit the complete dossier as JSON")
130
- .action(async (repository, options) => {
131
- const dossier = await buildCandidateDossier(repository, {
132
- discoveryPath: options.feed,
133
- });
134
- const path = options.write || options.output
135
- ? await writeCandidateDossier(dossier, options.output)
136
- : undefined;
137
- if (options.json)
138
- return console.log(JSON.stringify({
139
- dossier,
140
- persisted: Boolean(path),
141
- ...(path ? { path } : {}),
142
- }, null, 2));
143
- console.log(formatCandidateDossier(dossier));
144
- if (path)
145
- console.log(`Dossier: ${path}`);
146
- else {
147
- console.log("Preview only. Re-run with --write to persist this dossier.");
148
- }
149
- });
150
- candidate
151
- .command("propose")
152
- .allowExcessArguments(false)
153
- .description("Convert a reviewed dossier into a catalog-record proposal; never edits the catalog")
154
- .argument("<dossier>", "persisted candidate dossier JSON")
155
- .requiredOption("--id <id>", "lowercase kebab-case catalog id")
156
- .requiredOption("--category <category>", "inspected catalog category")
157
- .requiredOption("--platforms <ids>", "explicitly reviewed comma-separated platforms: windows,macos,linux")
158
- .option("--display-name <name>", "reviewed display name")
159
- .option("--description <text>", "reviewed description")
160
- .option("--license <spdx>", "human-reviewed license override")
161
- .option("--tier <tier>", "official, stable, trending, or community", "community")
162
- .option("--approve", "confirm human review and write the proposal")
163
- .option("--output <path>", "proposal JSON output path; required with --approve")
164
- .option("--json", "emit machine-readable JSON")
165
- .action(async (dossierPath, options) => {
166
- const platforms = options.platforms
167
- .split(",")
168
- .map((item) => item.trim())
169
- .filter(Boolean);
170
- const knownPlatforms = new Set(["windows", "macos", "linux"]);
171
- if (platforms.some((item) => !knownPlatforms.has(item)))
172
- throw new Error("--platforms supports only windows, macos, and linux");
173
- const knownTiers = new Set([
174
- "official",
175
- "stable",
176
- "trending",
177
- "community",
178
- ]);
179
- if (!knownTiers.has(options.tier))
180
- throw new Error("--tier is invalid");
181
- if (options.approve && !options.output)
182
- throw new Error("--approve requires --output so catalog mutation stays separate");
183
- const proposal = buildCatalogProposal(await verifyCandidateDossierSource(await readCandidateDossier(dossierPath)), {
184
- id: options.id,
185
- category: options.category,
186
- operatingSystems: platforms,
187
- tier: options.tier,
188
- displayName: options.displayName,
189
- description: options.description,
190
- license: options.license,
191
- }, await loadEffectiveCatalog());
192
- const output = options.approve ? resolve(options.output) : undefined;
193
- if (output)
194
- await writeFileAtomically(output, `${JSON.stringify(proposal, null, 2)}\n`);
195
- if (options.json)
196
- return console.log(JSON.stringify({
197
- proposal,
198
- approved: Boolean(output),
199
- catalogMutated: false,
200
- ...(output ? { output } : {}),
201
- }, null, 2));
202
- console.log(JSON.stringify(proposal, null, 2));
203
- if (!output)
204
- console.log("Proposal preview only. Human review is still required; use --approve --output <path> to persist it.");
205
- else
206
- console.log(`Approved proposal written to ${output}.`);
207
- });
208
- candidate
209
- .command("promote")
210
- .allowExcessArguments(false)
211
- .description("Merge a reviewed proposal into the local catalog; requires --approve")
212
- .argument("<proposal>", "proposal JSON file from `candidate propose --approve`")
213
- .option("--approve", "confirm human review and write to catalog")
214
- .option("--json", "emit machine-readable JSON")
215
- .action(async (proposalPath, options) => {
216
- const proposal = JSON.parse(await readFile(resolve(proposalPath), "utf8"));
217
- validateCatalog([proposal], { requireEvidence: true });
218
- if (!options.approve) {
219
- if (options.json)
220
- return console.log(JSON.stringify({ proposal, promoted: false, catalogMutated: false }, null, 2));
221
- console.log(JSON.stringify(proposal, null, 2));
222
- console.log("Preview only. Re-run with --approve to merge into the catalog.");
223
- return;
224
- }
225
- const result = await promoteCatalogCandidate(proposal);
226
- const item = await markPromoted(proposal.repository);
227
- if (options.json)
228
- return console.log(JSON.stringify({
229
- proposal,
230
- promoted: true,
231
- catalogMutated: true,
232
- ...result,
233
- reviewQueue: item,
234
- }, null, 2));
235
- console.log(`Promoted '${proposal.id}' into ${result.catalogPath} (${result.totalRecords} records). Review queue: ${item.repository} marked promoted.`);
236
- });
87
+ registerCandidateCommands(program);
237
88
  program
238
89
  .command("discover")
239
90
  .description("Find public community leads; discovery never installs anything")
@@ -592,201 +443,5 @@ export function registerCatalog(program) {
592
443
  });
593
444
  console.log(`Verified model selection '${id}'. No credential value was stored or printed.`);
594
445
  });
595
- const skills = program
596
- .command("skills")
597
- .description("Install the skills Loadout ships so your agents can use Loadout from inside a conversation");
598
- skills
599
- .command("list", { isDefault: true })
600
- .description("Show the skills Loadout ships and whether they are installed")
601
- .option("--json", "emit machine-readable JSON")
602
- .action(async (options) => {
603
- const installed = await installedFirstPartySkills();
604
- console.log(options.json
605
- ? JSON.stringify(FIRST_PARTY_SKILLS.map((skill) => ({
606
- ...skill,
607
- installed: installed.has(skill.id),
608
- })), null, 2)
609
- : formatFirstPartySkillList(installed));
610
- });
611
- skills
612
- .command("install")
613
- .description("Preview or install one Loadout skill into detected agents")
614
- .argument("<id>", "skill id, for example loadout-router")
615
- .option("--agents <ids>", "comma-separated agent ids; defaults to all detected")
616
- .option("--yes", "apply after previewing")
617
- .action(async (id, options) => {
618
- const plan = await planFirstPartySkill(id, {
619
- ...(options.agents
620
- ? { agents: parseAgentSelection(options.agents) }
621
- : {}),
622
- });
623
- console.log(formatFirstPartySkillPlan(plan));
624
- if (!plan.targets.length)
625
- return;
626
- if (!options.yes) {
627
- console.log("\nPreview only. Re-run with --yes to install.");
628
- return;
629
- }
630
- await applyFirstPartySkill(plan);
631
- console.log(`\nInstalled '${plan.skill.id}' for ${plan.targets.length} agent(s). Start a new agent session to pick it up.`);
632
- });
633
- skills
634
- .command("remove")
635
- .description("Preview or remove one Loadout skill from detected agents")
636
- .argument("<id>", "skill id")
637
- .option("--agents <ids>", "comma-separated agent ids; defaults to all detected")
638
- .option("--yes", "apply after previewing")
639
- .action(async (id, options) => {
640
- const plan = await planFirstPartySkill(id, {
641
- ...(options.agents
642
- ? { agents: parseAgentSelection(options.agents) }
643
- : {}),
644
- });
645
- const present = plan.targets.filter((target) => target.replacing);
646
- if (!present.length) {
647
- console.log(`'${id}' is not installed for any detected agent.`);
648
- return;
649
- }
650
- for (const target of present)
651
- console.log(` remove → ${target.destination} [${target.displayName}]`);
652
- if (!options.yes) {
653
- console.log("\nPreview only. Re-run with --yes to remove.");
654
- return;
655
- }
656
- await removeFirstPartySkill(plan);
657
- console.log(`\nRemoved '${id}' from ${present.length} agent(s).`);
658
- });
659
- const handoff = program
660
- .command("handoff")
661
- .description("Hand a task to your other agent, or see what has been handed to you");
662
- handoff
663
- .argument("[agent]", "who should do it, for example codex")
664
- .argument("[task...]", "what they should do")
665
- .option("--context <text>", "anything they need that is not in the task")
666
- .option("--from <agent>", "who is sending", "user")
667
- .option("--done <id>", "mark a task finished")
668
- .option("--json", "emit machine-readable JSON")
669
- .action(async (agent, taskWords, options) => {
670
- const cwd = process.cwd();
671
- if (options.done) {
672
- const message = await markDone(cwd, options.done);
673
- console.log(options.json
674
- ? JSON.stringify(message, null, 2)
675
- : `Marked ${options.done} done.`);
676
- return;
677
- }
678
- // No agent named: show what is waiting, for everyone.
679
- if (!agent) {
680
- const state = await getHandoffState(cwd);
681
- console.log(options.json
682
- ? JSON.stringify(state, null, 2)
683
- : formatHandoffStatus(state));
684
- return;
685
- }
686
- // An agent with no task means "show me my inbox".
687
- const task = taskWords.join(" ").trim();
688
- if (!task) {
689
- const messages = await readInbox(cwd, agent);
690
- console.log(options.json
691
- ? JSON.stringify(messages, null, 2)
692
- : formatInbox(agent, messages));
693
- return;
694
- }
695
- // Sending is the common case, so it sets itself up rather than failing
696
- // with instructions to run two other commands first.
697
- const setup = [];
698
- if (!(await isHandoffInitialized(cwd))) {
699
- await initHandoff(cwd);
700
- setup.push("created .handoff/");
701
- }
702
- for (const target of [options.from, agent]) {
703
- if (!isPickupTarget(target))
704
- continue;
705
- const plan = await planPickup(cwd, target);
706
- if (!plan.replacing) {
707
- await applyPickup(plan);
708
- setup.push(`told ${target} to check its inbox`);
709
- }
710
- }
711
- const message = await sendHandoff(cwd, agent, task, {
712
- from: options.from,
713
- ...(options.context ? { context: options.context } : {}),
714
- });
715
- if (options.json) {
716
- console.log(JSON.stringify({ message, setup }, null, 2));
717
- return;
718
- }
719
- for (const line of setup)
720
- console.log(` ${line}`);
721
- console.log(`Sent to ${agent}: ${task}`);
722
- console.log(`It will pick this up next session, or now with: loadout handoff ${agent}`);
723
- });
724
- const route = program
725
- .command("route")
726
- .description("Which model to use for a task, according to a routing policy you own");
727
- route
728
- .argument("[description...]", "what you are about to do")
729
- .option("--bucket <bucket>", "state the bucket yourself: hard, normal, or cheap")
730
- .option("--set <bucket=model>", "change the model for one bucket and save")
731
- .option("--save", "write the current defaults to disk so you can edit them")
732
- .option("--reset", "delete your policy and go back to the defaults")
733
- .option("--models", "list the model ids you can name in a policy")
734
- .option("--json", "emit machine-readable JSON")
735
- .action(async (descriptionWords, options) => {
736
- if (options.models) {
737
- console.log(options.json
738
- ? JSON.stringify(MODEL_CATALOG, null, 2)
739
- : formatModelCatalog());
740
- return;
741
- }
742
- const detected = await detectAgents();
743
- const installed = detected
744
- .filter((agent) => agent.installed)
745
- .map((agent) => agent.id);
746
- if (options.reset) {
747
- await rm(policyPath(), { force: true });
748
- console.log("Removed your routing policy; defaults apply again.");
749
- return;
750
- }
751
- if (options.set) {
752
- const separator = options.set.indexOf("=");
753
- if (separator <= 0)
754
- throw new Error("--set expects <bucket>=<model>, for example normal=gpt-5.6-terra");
755
- const bucket = options.set.slice(0, separator).trim();
756
- if (!BUCKETS.includes(bucket))
757
- throw new Error(`Unknown bucket '${bucket}'. Valid: ${BUCKETS.join(", ")}`);
758
- const updated = await setRule(bucket, options.set.slice(separator + 1).trim(), installed);
759
- console.log(formatPolicy(updated, "file"));
760
- return;
761
- }
762
- const { policy, source } = await readPolicy(installed);
763
- if (options.save) {
764
- const path = await writePolicy(policy);
765
- console.log(`Saved your routing policy to ${path}\n`);
766
- console.log(formatPolicy(policy, "file"));
767
- return;
768
- }
769
- const description = descriptionWords.join(" ").trim();
770
- if (!description && !options.bucket) {
771
- console.log(options.json
772
- ? JSON.stringify({ policy, source }, null, 2)
773
- : formatPolicy(policy, source));
774
- return;
775
- }
776
- const stated = options.bucket;
777
- if (stated && !BUCKETS.includes(stated))
778
- throw new Error(`Unknown bucket '${stated}'. Valid: ${BUCKETS.join(", ")}`);
779
- const bucket = stated ?? guessBucket(description);
780
- const answer = resolveRoute(policy, bucket, installed, !stated);
781
- console.log(options.json
782
- ? JSON.stringify(answer, null, 2)
783
- : formatAnswer(answer, description || undefined));
784
- });
785
- program
786
- .command("completion")
787
- .description("Print a shell-completion script; redirect it to your shell profile")
788
- .argument("<shell>", "bash, zsh, fish, or powershell")
789
- .action((shell) => {
790
- process.stdout.write(renderShellCompletion(parseCompletionShell(shell)));
791
- });
446
+ registerWorkflowCommands(program);
792
447
  }