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.
- package/CHANGELOG.md +76 -1
- package/README.md +149 -304
- package/SECURITY.md +21 -1
- package/catalog/discovered.json +27897 -25327
- 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 +147 -0
- package/dist/src/commands/catalog.js +9 -354
- package/dist/src/commands/coordinate.js +586 -0
- package/dist/src/commands/coordination-discussions.js +194 -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 +137 -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/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 +667 -0
- package/dist/src/core/coordination/discussion.js +340 -0
- package/dist/src/core/coordination/events.js +161 -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 +110 -0
- package/dist/src/core/coordination/mcp-server.js +424 -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 +128 -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/{routing → delegation}/handoff.js +40 -9
- 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/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 +251 -251
- package/docs/FEATURE_TEST_MATRIX.md +3 -3
- package/docs/LIVE_COLLABORATION.md +234 -0
- package/docs/REFERENCE.md +163 -0
- package/docs/RELEASE_REVIEW.md +1 -1
- package/docs/TESTING.md +17 -1
- package/docs/USER_TEST_GUIDE.md +138 -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/specs/BOUNDED_AGENT_DISCUSSIONS.md +169 -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/package.json +13 -4
- package/skills/loadout-handoff/SKILL.md +238 -0
- 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
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { writeFileAtomically } from "../install/atomic-file.js";
|
|
4
|
-
import { ensureDirectory, loadoutHome } from "../agents/paths.js";
|
|
5
|
-
import { MODEL_CATALOG } from "./route.js";
|
|
6
|
-
export const BUCKETS = ["hard", "normal", "cheap"];
|
|
7
|
-
export const BUCKET_MEANING = {
|
|
8
|
-
hard: "architecture, security, migrations, tricky debugging, risky review",
|
|
9
|
-
normal: "most implementation, ordinary debugging, refactors",
|
|
10
|
-
cheap: "tests, docs, boilerplate, renames, mechanical edits",
|
|
11
|
-
};
|
|
12
|
-
export const policyPath = () => join(loadoutHome(), "routing.json");
|
|
13
|
-
/**
|
|
14
|
-
* Defaults reflect a deliberate stance: on Claude the real choice is Opus or
|
|
15
|
-
* Sonnet, and the cheapest useful tier lives on Codex. Anyone who disagrees
|
|
16
|
-
* edits the file rather than arguing with a hardcoded table.
|
|
17
|
-
*/
|
|
18
|
-
export function defaultPolicy(installed = []) {
|
|
19
|
-
const hasCodex = installed.length === 0 || installed.includes("codex");
|
|
20
|
-
return {
|
|
21
|
-
version: 1,
|
|
22
|
-
rules: {
|
|
23
|
-
hard: "claude-opus-5",
|
|
24
|
-
normal: "claude-sonnet-5",
|
|
25
|
-
// Without Codex there is no fast tier to fall back to.
|
|
26
|
-
cheap: hasCodex ? "gpt-5.6-luna" : "claude-sonnet-5",
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export function findModel(id) {
|
|
31
|
-
return MODEL_CATALOG.find((model) => model.id === id);
|
|
32
|
-
}
|
|
33
|
-
export function validatePolicy(value) {
|
|
34
|
-
const candidate = value;
|
|
35
|
-
if (!candidate || candidate.version !== 1)
|
|
36
|
-
throw new Error("Routing policy must have version 1");
|
|
37
|
-
const rules = candidate.rules;
|
|
38
|
-
if (!rules)
|
|
39
|
-
throw new Error("Routing policy has no rules");
|
|
40
|
-
for (const bucket of BUCKETS) {
|
|
41
|
-
const id = rules[bucket];
|
|
42
|
-
if (typeof id !== "string" || !id)
|
|
43
|
-
throw new Error(`Routing policy is missing a model for '${bucket}'`);
|
|
44
|
-
if (!findModel(id))
|
|
45
|
-
throw new Error(`Routing policy names unknown model '${id}' for '${bucket}'. Run 'loadout route --models' to list valid ids.`);
|
|
46
|
-
}
|
|
47
|
-
return { version: 1, rules: { ...rules } };
|
|
48
|
-
}
|
|
49
|
-
export async function readPolicy(installed = []) {
|
|
50
|
-
try {
|
|
51
|
-
const raw = await readFile(policyPath(), "utf8");
|
|
52
|
-
return { policy: validatePolicy(JSON.parse(raw)), source: "file" };
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
if (error &&
|
|
56
|
-
typeof error === "object" &&
|
|
57
|
-
error.code === "ENOENT")
|
|
58
|
-
return { policy: defaultPolicy(installed), source: "default" };
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export async function writePolicy(policy) {
|
|
63
|
-
const path = policyPath();
|
|
64
|
-
await ensureDirectory(dirname(path));
|
|
65
|
-
await writeFileAtomically(path, `${JSON.stringify(policy, null, 2)}\n`);
|
|
66
|
-
return path;
|
|
67
|
-
}
|
|
68
|
-
export async function setRule(bucket, modelId, installed = []) {
|
|
69
|
-
if (!findModel(modelId))
|
|
70
|
-
throw new Error(`Unknown model '${modelId}'. Run 'loadout route --models' to list valid ids.`);
|
|
71
|
-
const { policy } = await readPolicy(installed);
|
|
72
|
-
const next = validatePolicy({
|
|
73
|
-
version: 1,
|
|
74
|
-
rules: { ...policy.rules, [bucket]: modelId },
|
|
75
|
-
});
|
|
76
|
-
await writePolicy(next);
|
|
77
|
-
return next;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* A deliberately small classifier. It exists so the CLI can answer without an
|
|
81
|
-
* agent present, and it says plainly that it is guessing — the skill, which has
|
|
82
|
-
* the conversation and the code, is expected to do better.
|
|
83
|
-
*/
|
|
84
|
-
export function guessBucket(description) {
|
|
85
|
-
const text = description.toLowerCase();
|
|
86
|
-
const hard = /\b(architect|design|migrat|security|secure|auth|login|session|crypto|signature|signing|token|secret|credential|permission|payment|billing|checkout|stripe|webhook|invoice|concurren|race condition|deadlock|schema|rollout|rollback|encrypt|sanitiz|inject)\w*/;
|
|
87
|
-
const cheap = /\b(test|tests|spec|doc|docs|docstring|comment|readme|changelog|rename|typo|format|lint|boilerplate|scaffold)\w*/;
|
|
88
|
-
if (hard.test(text))
|
|
89
|
-
return "hard";
|
|
90
|
-
if (cheap.test(text))
|
|
91
|
-
return "cheap";
|
|
92
|
-
return "normal";
|
|
93
|
-
}
|
|
94
|
-
export function resolveRoute(policy, bucket, installed, guessed) {
|
|
95
|
-
const model = findModel(policy.rules[bucket]);
|
|
96
|
-
const answer = { bucket, model, guessed };
|
|
97
|
-
if (!installed.length)
|
|
98
|
-
return answer;
|
|
99
|
-
const runnable = model.nativeAgents.some((id) => installed.includes(id));
|
|
100
|
-
if (runnable)
|
|
101
|
-
return answer;
|
|
102
|
-
// Prefer a model of the same tier the user can actually run; otherwise the
|
|
103
|
-
// closest one by price, so the advice stays actionable.
|
|
104
|
-
const reachable = MODEL_CATALOG.filter((candidate) => candidate.nativeAgents.some((id) => installed.includes(id)));
|
|
105
|
-
const fallback = reachable.find((candidate) => candidate.tier === model.tier) ??
|
|
106
|
-
reachable.sort((a, b) => Math.abs(a.inputCostPer1M - model.inputCostPer1M) -
|
|
107
|
-
Math.abs(b.inputCostPer1M - model.inputCostPer1M))[0];
|
|
108
|
-
answer.unavailable = {
|
|
109
|
-
reason: `${model.name} needs ${model.nativeAgents.join(" or ")}, which is not installed`,
|
|
110
|
-
...(fallback ? { fallback } : {}),
|
|
111
|
-
};
|
|
112
|
-
return answer;
|
|
113
|
-
}
|
|
114
|
-
function price(model) {
|
|
115
|
-
return `$${model.inputCostPer1M}/$${model.outputCostPer1M} per M`;
|
|
116
|
-
}
|
|
117
|
-
export function formatPolicy(policy, source) {
|
|
118
|
-
const lines = [
|
|
119
|
-
source === "file"
|
|
120
|
-
? `Your routing policy — ${policyPath()}`
|
|
121
|
-
: "Default routing policy (not saved yet)",
|
|
122
|
-
"",
|
|
123
|
-
];
|
|
124
|
-
for (const bucket of BUCKETS) {
|
|
125
|
-
const model = findModel(policy.rules[bucket]);
|
|
126
|
-
lines.push(` ${bucket.padEnd(7)} ${model.name.padEnd(16)} ${price(model)}`);
|
|
127
|
-
lines.push(` ${"".padEnd(7)} ${BUCKET_MEANING[bucket]}`);
|
|
128
|
-
}
|
|
129
|
-
lines.push("", "Change it: loadout route --set normal=gpt-5.6-terra", source === "default"
|
|
130
|
-
? "Save it: loadout route --save"
|
|
131
|
-
: "Reset it: loadout route --reset");
|
|
132
|
-
return lines.join("\n");
|
|
133
|
-
}
|
|
134
|
-
export function formatAnswer(answer, task) {
|
|
135
|
-
const lines = [];
|
|
136
|
-
if (task)
|
|
137
|
-
lines.push(`Task: ${task}`, "");
|
|
138
|
-
lines.push(`Bucket: ${answer.bucket} — ${BUCKET_MEANING[answer.bucket]}`, `Use: ${answer.model.name} (${price(answer.model)})`);
|
|
139
|
-
if (answer.unavailable) {
|
|
140
|
-
lines.push("", `Note: ${answer.unavailable.reason}.`);
|
|
141
|
-
if (answer.unavailable.fallback)
|
|
142
|
-
lines.push(` Reachable alternative: ${answer.unavailable.fallback.name} (${price(answer.unavailable.fallback)}).`);
|
|
143
|
-
}
|
|
144
|
-
if (answer.guessed)
|
|
145
|
-
lines.push("", "This bucket was guessed from wording alone. If the work is riskier than it", "reads — payments, auth, migrations — treat it as hard and move up.");
|
|
146
|
-
return lines.join("\n");
|
|
147
|
-
}
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
// Sources:
|
|
2
|
-
// Anthropic: https://allaboutclaude.com/models
|
|
3
|
-
// OpenAI: https://benchlm.ai/openai/api-pricing
|
|
4
|
-
// Prices as of August 2026.
|
|
5
|
-
export const MODEL_CATALOG = [
|
|
6
|
-
// --- Anthropic (Claude Code) ---
|
|
7
|
-
// Haiku and the 4.x line are deliberately absent: for real coding work the
|
|
8
|
-
// choice is Opus or Sonnet, and offering a weaker tier invites picking it.
|
|
9
|
-
{
|
|
10
|
-
id: "claude-opus-5",
|
|
11
|
-
provider: "anthropic",
|
|
12
|
-
name: "Claude Opus 5",
|
|
13
|
-
tier: "frontier",
|
|
14
|
-
inputCostPer1M: 5,
|
|
15
|
-
outputCostPer1M: 25,
|
|
16
|
-
nativeAgents: ["claude-code"],
|
|
17
|
-
current: true,
|
|
18
|
-
generation: "5",
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
id: "claude-sonnet-5",
|
|
22
|
-
provider: "anthropic",
|
|
23
|
-
name: "Claude Sonnet 5",
|
|
24
|
-
tier: "standard",
|
|
25
|
-
inputCostPer1M: 3,
|
|
26
|
-
outputCostPer1M: 15,
|
|
27
|
-
nativeAgents: ["claude-code"],
|
|
28
|
-
current: true,
|
|
29
|
-
generation: "5",
|
|
30
|
-
},
|
|
31
|
-
// --- OpenAI (Codex) — the current GPT-5.6 tiers only ---
|
|
32
|
-
{
|
|
33
|
-
id: "gpt-5.6-sol",
|
|
34
|
-
provider: "openai",
|
|
35
|
-
name: "GPT-5.6 Sol",
|
|
36
|
-
tier: "frontier",
|
|
37
|
-
inputCostPer1M: 5,
|
|
38
|
-
outputCostPer1M: 30,
|
|
39
|
-
nativeAgents: ["codex"],
|
|
40
|
-
current: true,
|
|
41
|
-
generation: "5.6",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "gpt-5.6-terra",
|
|
45
|
-
provider: "openai",
|
|
46
|
-
name: "GPT-5.6 Terra",
|
|
47
|
-
tier: "standard",
|
|
48
|
-
inputCostPer1M: 2,
|
|
49
|
-
outputCostPer1M: 12,
|
|
50
|
-
nativeAgents: ["codex"],
|
|
51
|
-
current: true,
|
|
52
|
-
generation: "5.6",
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: "gpt-5.6-luna",
|
|
56
|
-
provider: "openai",
|
|
57
|
-
name: "GPT-5.6 Luna",
|
|
58
|
-
tier: "fast",
|
|
59
|
-
inputCostPer1M: 0.2,
|
|
60
|
-
outputCostPer1M: 1.2,
|
|
61
|
-
nativeAgents: ["codex"],
|
|
62
|
-
current: true,
|
|
63
|
-
generation: "5.6",
|
|
64
|
-
},
|
|
65
|
-
];
|
|
66
|
-
const TIER_LABELS = {
|
|
67
|
-
frontier: "Frontier (deep reasoning)",
|
|
68
|
-
standard: "Standard (balanced)",
|
|
69
|
-
fast: "Fast (high throughput)",
|
|
70
|
-
};
|
|
71
|
-
const PHASE_CONFIG = {
|
|
72
|
-
plan: {
|
|
73
|
-
tier: "frontier",
|
|
74
|
-
reason: "Architecture and decomposition need deep reasoning to avoid costly rework",
|
|
75
|
-
agents: ["claude-code"],
|
|
76
|
-
conserveTier: "standard",
|
|
77
|
-
conserveTradeoff: "Slightly shallower architectural reasoning; review the plan more carefully",
|
|
78
|
-
},
|
|
79
|
-
implement: {
|
|
80
|
-
tier: "standard",
|
|
81
|
-
reason: "Implementation is high-volume; standard models score within 5% of frontier on SWE-bench",
|
|
82
|
-
agents: ["claude-code", "codex"],
|
|
83
|
-
conserveTier: "fast",
|
|
84
|
-
conserveTradeoff: "May need more iterations on complex logic; fine for CRUD and boilerplate",
|
|
85
|
-
},
|
|
86
|
-
review: {
|
|
87
|
-
tier: "frontier",
|
|
88
|
-
reason: "Code review catches subtle bugs that cheaper models miss; worth the cost per-review",
|
|
89
|
-
agents: ["claude-code", "codex"],
|
|
90
|
-
conserveTier: "standard",
|
|
91
|
-
conserveTradeoff: "May miss edge-case bugs; pair with a linter",
|
|
92
|
-
},
|
|
93
|
-
test: {
|
|
94
|
-
tier: "fast",
|
|
95
|
-
reason: "Test generation is pattern-heavy; fast models produce equivalent coverage",
|
|
96
|
-
agents: ["claude-code", "codex"],
|
|
97
|
-
},
|
|
98
|
-
debug: {
|
|
99
|
-
tier: "standard",
|
|
100
|
-
reason: "Targeted debugging needs good reasoning but not frontier-level; standard balances cost and quality",
|
|
101
|
-
agents: ["claude-code", "codex"],
|
|
102
|
-
conserveTier: "fast",
|
|
103
|
-
conserveTradeoff: "Works for simple bugs; complex root-cause analysis may suffer",
|
|
104
|
-
},
|
|
105
|
-
document: {
|
|
106
|
-
tier: "fast",
|
|
107
|
-
reason: "Documentation and comments are well-suited to fast models; any decent model writes docs",
|
|
108
|
-
agents: ["claude-code", "codex"],
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
const PHASE_KEYWORDS = {
|
|
112
|
-
plan: [
|
|
113
|
-
"plan",
|
|
114
|
-
"design",
|
|
115
|
-
"architect",
|
|
116
|
-
"architecture",
|
|
117
|
-
"rfc",
|
|
118
|
-
"spec",
|
|
119
|
-
"decompose",
|
|
120
|
-
"strategy",
|
|
121
|
-
"system design",
|
|
122
|
-
],
|
|
123
|
-
implement: [
|
|
124
|
-
"implement",
|
|
125
|
-
"build",
|
|
126
|
-
"code",
|
|
127
|
-
"create",
|
|
128
|
-
"add",
|
|
129
|
-
"feature",
|
|
130
|
-
"write",
|
|
131
|
-
"develop",
|
|
132
|
-
"make",
|
|
133
|
-
],
|
|
134
|
-
review: [
|
|
135
|
-
"review",
|
|
136
|
-
"audit",
|
|
137
|
-
"check",
|
|
138
|
-
"inspect",
|
|
139
|
-
"critique",
|
|
140
|
-
"pr",
|
|
141
|
-
"pull request",
|
|
142
|
-
"diff",
|
|
143
|
-
],
|
|
144
|
-
test: [
|
|
145
|
-
"test",
|
|
146
|
-
"spec",
|
|
147
|
-
"coverage",
|
|
148
|
-
"unit",
|
|
149
|
-
"integration",
|
|
150
|
-
"e2e",
|
|
151
|
-
"assert",
|
|
152
|
-
"vitest",
|
|
153
|
-
"jest",
|
|
154
|
-
],
|
|
155
|
-
debug: [
|
|
156
|
-
"debug",
|
|
157
|
-
"fix",
|
|
158
|
-
"bug",
|
|
159
|
-
"error",
|
|
160
|
-
"crash",
|
|
161
|
-
"broken",
|
|
162
|
-
"investigate",
|
|
163
|
-
"troubleshoot",
|
|
164
|
-
"failing",
|
|
165
|
-
],
|
|
166
|
-
document: [
|
|
167
|
-
"document",
|
|
168
|
-
"docs",
|
|
169
|
-
"readme",
|
|
170
|
-
"comment",
|
|
171
|
-
"jsdoc",
|
|
172
|
-
"changelog",
|
|
173
|
-
"explain",
|
|
174
|
-
"docstring",
|
|
175
|
-
],
|
|
176
|
-
};
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// Classification
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
export function classifyTask(description) {
|
|
181
|
-
const lower = description.toLowerCase();
|
|
182
|
-
let best = "implement";
|
|
183
|
-
let bestScore = 0;
|
|
184
|
-
for (const [phase, keywords] of Object.entries(PHASE_KEYWORDS)) {
|
|
185
|
-
let score = 0;
|
|
186
|
-
for (const kw of keywords) {
|
|
187
|
-
if (lower.includes(kw))
|
|
188
|
-
score++;
|
|
189
|
-
}
|
|
190
|
-
if (score > bestScore) {
|
|
191
|
-
bestScore = score;
|
|
192
|
-
best = phase;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return best;
|
|
196
|
-
}
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
// Model queries
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
200
|
-
export function modelsForTier(tier, currentOnly = true) {
|
|
201
|
-
return MODEL_CATALOG.filter((m) => m.tier === tier && (!currentOnly || m.current));
|
|
202
|
-
}
|
|
203
|
-
export function modelsByProvider(provider) {
|
|
204
|
-
return MODEL_CATALOG.filter((m) => m.provider === provider && m.current);
|
|
205
|
-
}
|
|
206
|
-
export function cheapestInTier(tier) {
|
|
207
|
-
const models = modelsForTier(tier);
|
|
208
|
-
return models.sort((a, b) => a.inputCostPer1M - b.inputCostPer1M)[0];
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Narrow a recommendation to the agents actually present on this machine. A
|
|
212
|
-
* recommendation that names an agent the user has not installed is noise, so
|
|
213
|
-
* the resolved list drives both the printed advice and the handoff command.
|
|
214
|
-
*/
|
|
215
|
-
export function resolveAvailableAgents(suggested, installed) {
|
|
216
|
-
const present = new Set(installed);
|
|
217
|
-
return {
|
|
218
|
-
available: suggested.filter((id) => present.has(id)),
|
|
219
|
-
missing: suggested.filter((id) => !present.has(id)),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
/** Models in the recommended tier that the given agents can actually run. */
|
|
223
|
-
export function modelsRunnableBy(models, agents) {
|
|
224
|
-
if (!agents.length)
|
|
225
|
-
return models;
|
|
226
|
-
const wanted = new Set(agents);
|
|
227
|
-
return models.filter((m) => m.nativeAgents.some((id) => wanted.has(id)));
|
|
228
|
-
}
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// Routing
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
export function routeTask(description, conserve = false) {
|
|
233
|
-
return routePhase(classifyTask(description), conserve);
|
|
234
|
-
}
|
|
235
|
-
export function routePhase(phase, conserve = false) {
|
|
236
|
-
const config = PHASE_CONFIG[phase];
|
|
237
|
-
if (!config)
|
|
238
|
-
throw new Error(`Unknown phase '${phase}'. Valid: ${Object.keys(PHASE_CONFIG).join(", ")}`);
|
|
239
|
-
const effectiveTier = conserve && config.conserveTier ? config.conserveTier : config.tier;
|
|
240
|
-
const models = modelsForTier(effectiveTier);
|
|
241
|
-
const rec = {
|
|
242
|
-
phase: phase,
|
|
243
|
-
tier: effectiveTier,
|
|
244
|
-
tierLabel: TIER_LABELS[effectiveTier],
|
|
245
|
-
models,
|
|
246
|
-
reason: conserve && config.conserveTier
|
|
247
|
-
? `Conserve mode: ${config.conserveTradeoff}`
|
|
248
|
-
: config.reason,
|
|
249
|
-
suggestedAgents: config.agents,
|
|
250
|
-
};
|
|
251
|
-
if (!conserve && config.conserveTier) {
|
|
252
|
-
rec.conserveAlternative = {
|
|
253
|
-
tier: config.conserveTier,
|
|
254
|
-
models: modelsForTier(config.conserveTier),
|
|
255
|
-
tradeoff: config.conserveTradeoff,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
return rec;
|
|
259
|
-
}
|
|
260
|
-
export function allPhaseRoutes(conserve = false) {
|
|
261
|
-
return Object.keys(PHASE_CONFIG).map((p) => routePhase(p, conserve));
|
|
262
|
-
}
|
|
263
|
-
export function estimateCostSavings() {
|
|
264
|
-
const normal = allPhaseRoutes(false);
|
|
265
|
-
const conserved = allPhaseRoutes(true);
|
|
266
|
-
const estimates = normal.map((r) => {
|
|
267
|
-
const cheapest = cheapestInTier(r.tier);
|
|
268
|
-
return {
|
|
269
|
-
phase: r.phase,
|
|
270
|
-
tier: r.tier,
|
|
271
|
-
cheapestModel: cheapest?.name ?? "—",
|
|
272
|
-
inputCost: cheapest?.inputCostPer1M ?? 0,
|
|
273
|
-
outputCost: cheapest?.outputCostPer1M ?? 0,
|
|
274
|
-
};
|
|
275
|
-
});
|
|
276
|
-
const normalTotal = estimates.reduce((s, e) => s + e.inputCost, 0);
|
|
277
|
-
const conserveEstimates = conserved.map((r) => cheapestInTier(r.tier)?.inputCostPer1M ?? 0);
|
|
278
|
-
const conserveTotal = conserveEstimates.reduce((s, c) => s + c, 0);
|
|
279
|
-
return { estimates, normalTotal, conserveTotal };
|
|
280
|
-
}
|
|
281
|
-
/** Shell-quote a task description for the suggested handoff command. */
|
|
282
|
-
function shellQuote(value) {
|
|
283
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
284
|
-
}
|
|
285
|
-
export function formatRouteRecommendation(rec, context = {}) {
|
|
286
|
-
// `undefined` means detection never ran, so every suggested agent stands. An
|
|
287
|
-
// empty array means detection ran and found nothing, which must not be
|
|
288
|
-
// silently upgraded back to the full suggestion list.
|
|
289
|
-
const checked = context.installedAgents !== undefined;
|
|
290
|
-
const { available, missing } = checked
|
|
291
|
-
? resolveAvailableAgents(rec.suggestedAgents, context.installedAgents)
|
|
292
|
-
: { available: rec.suggestedAgents, missing: [] };
|
|
293
|
-
// Only recommend models an available agent can actually run. Not every
|
|
294
|
-
// provider offers every tier — Claude Code has no fast tier — so when the
|
|
295
|
-
// recommended tier holds nothing the user can reach, step up to the nearest
|
|
296
|
-
// tier that does rather than naming a model they cannot run.
|
|
297
|
-
const runnable = checked
|
|
298
|
-
? modelsRunnableBy(rec.models, available)
|
|
299
|
-
: rec.models;
|
|
300
|
-
const stepUp = {
|
|
301
|
-
fast: "standard",
|
|
302
|
-
standard: "frontier",
|
|
303
|
-
};
|
|
304
|
-
let effective = runnable;
|
|
305
|
-
let tier = rec.tier;
|
|
306
|
-
let substituted;
|
|
307
|
-
while (checked && available.length && !effective.length && stepUp[tier]) {
|
|
308
|
-
const next = stepUp[tier];
|
|
309
|
-
effective = modelsRunnableBy(modelsForTier(next), available);
|
|
310
|
-
if (effective.length)
|
|
311
|
-
substituted = { from: rec.tier, to: next };
|
|
312
|
-
tier = next;
|
|
313
|
-
}
|
|
314
|
-
const shown = (effective.length ? effective : rec.models).slice(0, 4);
|
|
315
|
-
const modelNames = shown.map((m) => `${m.name} ($${m.inputCostPer1M}/$${m.outputCostPer1M})`);
|
|
316
|
-
const lines = [
|
|
317
|
-
`Phase: ${rec.phase}`,
|
|
318
|
-
`Tier: ${rec.tierLabel}`,
|
|
319
|
-
`Models: ${modelNames.join("\n ")}`,
|
|
320
|
-
`Agents: ${available.length ? available.join(", ") : "none detected"}${missing.length ? ` (not installed: ${missing.join(", ")})` : ""}`,
|
|
321
|
-
`Why: ${rec.reason}`,
|
|
322
|
-
];
|
|
323
|
-
if (substituted)
|
|
324
|
-
lines.push(`Note: your agents have no ${substituted.from}-tier model, so this shows`, ` the ${substituted.to} tier instead.`);
|
|
325
|
-
if (rec.conserveAlternative) {
|
|
326
|
-
const alt = rec.conserveAlternative;
|
|
327
|
-
const altCheapest = cheapestInTier(alt.tier);
|
|
328
|
-
lines.push(``, `Conserve: drop to ${alt.tier} tier (${altCheapest?.name ?? "—"} at $${altCheapest?.inputCostPer1M ?? "?"}/$${altCheapest?.outputCostPer1M ?? "?"})`, ` ${alt.tradeoff}`);
|
|
329
|
-
}
|
|
330
|
-
// Actionable next step: hand this task to a detected agent.
|
|
331
|
-
if (context.description && available.length) {
|
|
332
|
-
const target = available[0];
|
|
333
|
-
lines.push(``, `Hand off:`, ` loadout handoff ${target} ${shellQuote(context.description)}`);
|
|
334
|
-
}
|
|
335
|
-
return lines.join("\n");
|
|
336
|
-
}
|
|
337
|
-
export function formatRoutingTable(conserve = false) {
|
|
338
|
-
const routes = allPhaseRoutes(conserve);
|
|
339
|
-
const header = `Phase Tier Cheapest model $/M in $/M out Agents`;
|
|
340
|
-
const divider = "─".repeat(header.length);
|
|
341
|
-
const rows = routes.map((r) => {
|
|
342
|
-
const cheapest = cheapestInTier(r.tier);
|
|
343
|
-
const phase = r.phase.padEnd(12);
|
|
344
|
-
const tier = r.tier.padEnd(11);
|
|
345
|
-
const model = (cheapest?.name ?? "—").padEnd(27);
|
|
346
|
-
const inCost = `$${(cheapest?.inputCostPer1M ?? 0).toFixed(2)}`.padEnd(8);
|
|
347
|
-
const outCost = `$${(cheapest?.outputCostPer1M ?? 0).toFixed(2)}`.padEnd(8);
|
|
348
|
-
const agents = r.suggestedAgents.slice(0, 3).join(", ");
|
|
349
|
-
return `${phase} ${tier} ${model} ${inCost} ${outCost} ${agents}`;
|
|
350
|
-
});
|
|
351
|
-
return [
|
|
352
|
-
conserve
|
|
353
|
-
? "ROUTING TABLE (conserve mode — usage-saving tier for each phase)"
|
|
354
|
-
: "ROUTING TABLE",
|
|
355
|
-
"",
|
|
356
|
-
header,
|
|
357
|
-
divider,
|
|
358
|
-
...rows,
|
|
359
|
-
].join("\n");
|
|
360
|
-
}
|
|
361
|
-
export function formatCostTable() {
|
|
362
|
-
const { estimates, normalTotal, conserveTotal } = estimateCostSavings();
|
|
363
|
-
const header = "Phase Tier Cheapest model $/M input";
|
|
364
|
-
const divider = "─".repeat(header.length);
|
|
365
|
-
const rows = estimates.map((e) => {
|
|
366
|
-
const phase = e.phase.padEnd(12);
|
|
367
|
-
const tier = e.tier.padEnd(11);
|
|
368
|
-
const model = e.cheapestModel.padEnd(27);
|
|
369
|
-
return `${phase} ${tier} ${model} $${e.inputCost.toFixed(2)}`;
|
|
370
|
-
});
|
|
371
|
-
const savings = Math.round((1 - conserveTotal / normalTotal) * 100);
|
|
372
|
-
return [
|
|
373
|
-
header,
|
|
374
|
-
divider,
|
|
375
|
-
...rows,
|
|
376
|
-
divider,
|
|
377
|
-
`Normal total: $${normalTotal.toFixed(2)}/M input across 6 phases`,
|
|
378
|
-
`Conserve total: $${conserveTotal.toFixed(2)}/M input (${savings}% cheaper)`,
|
|
379
|
-
"",
|
|
380
|
-
"Prices are per-million-token list rates. Actual cost depends on prompt length.",
|
|
381
|
-
"Use --conserve when you want to stretch remaining session quota.",
|
|
382
|
-
].join("\n");
|
|
383
|
-
}
|
|
384
|
-
export function formatModelCatalog(filter) {
|
|
385
|
-
let models = MODEL_CATALOG;
|
|
386
|
-
if (filter?.provider)
|
|
387
|
-
models = models.filter((m) => m.provider === filter.provider);
|
|
388
|
-
if (filter?.tier)
|
|
389
|
-
models = models.filter((m) => m.tier === filter.tier);
|
|
390
|
-
if (filter?.current !== undefined)
|
|
391
|
-
models = models.filter((m) => m.current === filter.current);
|
|
392
|
-
const header = "Model Provider Tier $/M in $/M out Gen Agents";
|
|
393
|
-
const divider = "─".repeat(header.length);
|
|
394
|
-
const rows = models.map((m) => {
|
|
395
|
-
const name = m.name.padEnd(28);
|
|
396
|
-
const provider = m.provider.padEnd(11);
|
|
397
|
-
const tier = m.tier.padEnd(11);
|
|
398
|
-
const inCost = `$${m.inputCostPer1M.toFixed(2)}`.padEnd(8);
|
|
399
|
-
const outCost = `$${m.outputCostPer1M.toFixed(2)}`.padEnd(8);
|
|
400
|
-
const gen = m.generation.padEnd(6);
|
|
401
|
-
const agents = m.nativeAgents.slice(0, 3).join(", ");
|
|
402
|
-
return `${name} ${provider} ${tier} ${inCost} ${outCost} ${gen} ${agents}`;
|
|
403
|
-
});
|
|
404
|
-
return [
|
|
405
|
-
`${models.length} models${filter?.current === false ? " (including legacy)" : ""}`,
|
|
406
|
-
"",
|
|
407
|
-
header,
|
|
408
|
-
divider,
|
|
409
|
-
...rows,
|
|
410
|
-
].join("\n");
|
|
411
|
-
}
|
|
Binary file
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: loadout-router
|
|
3
|
-
description: Decide which model to use for a coding task and hand work to another agent. Use when the user asks which model to use, mentions running low on usage or cost, asks whether to switch to Opus or Sonnet or a GPT tier, or wants to delegate a task to Codex or Claude Code.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Loadout Router
|
|
7
|
-
|
|
8
|
-
Pick the model that fits the work, using the user's own routing policy rather
|
|
9
|
-
than your guess or mine.
|
|
10
|
-
|
|
11
|
-
## The policy is the user's, not yours
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
loadout route
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
That prints three buckets and the model the user has chosen for each:
|
|
18
|
-
|
|
19
|
-
- **hard** — architecture, security, migrations, tricky debugging, risky review
|
|
20
|
-
- **normal** — most implementation, ordinary debugging, refactors
|
|
21
|
-
- **cheap** — tests, docs, boilerplate, renames, mechanical edits
|
|
22
|
-
|
|
23
|
-
Read the policy before advising. If the user disagrees with a recommendation,
|
|
24
|
-
the fix is to change the policy, not to argue:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
loadout route --set cheap=claude-sonnet-5
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Your job is the bucket, not the model
|
|
31
|
-
|
|
32
|
-
The CLI can guess a bucket from wording, and it says so when it does. **You
|
|
33
|
-
should do better**, because you have the conversation, the code, and the stakes.
|
|
34
|
-
Decide the bucket yourself and state it:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
loadout route --bucket hard
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Judge by consequence, not vocabulary:
|
|
41
|
-
|
|
42
|
-
- Anything touching auth, payments, migrations, or data deletion is **hard**,
|
|
43
|
-
however small the diff.
|
|
44
|
-
- Unfamiliar code is harder than familiar code doing the same thing.
|
|
45
|
-
- A one-line change in a hot path is not cheap.
|
|
46
|
-
- Genuinely mechanical work — a rename, a docstring, a test for code you just
|
|
47
|
-
wrote — is **cheap**, and paying frontier prices for it is waste.
|
|
48
|
-
|
|
49
|
-
Report the model and why that bucket. One or two sentences.
|
|
50
|
-
|
|
51
|
-
## When cost matters
|
|
52
|
-
|
|
53
|
-
If the user mentions running low, being rate limited, or wanting to spend less,
|
|
54
|
-
say what a cheaper bucket would cost them in quality rather than presenting it
|
|
55
|
-
as free. `loadout route` shows real per-million prices for the comparison.
|
|
56
|
-
|
|
57
|
-
Neither Claude Code nor Codex exposes remaining quota programmatically, so never
|
|
58
|
-
claim to know how much the user has left.
|
|
59
|
-
|
|
60
|
-
## Handing work to the other agent
|
|
61
|
-
|
|
62
|
-
One command sends a task; it sets up the shared log on first use:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
loadout handoff codex "write vitest coverage for src/auth.ts" --context "zod schemas already exist"
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Put everything the receiver needs into `--context` — file paths, decisions you
|
|
69
|
-
already made, what you deliberately left out. It has none of this conversation.
|
|
70
|
-
|
|
71
|
-
Sending writes to a shared file in the user's repository, so confirm first
|
|
72
|
-
unless they asked for the handoff themselves.
|
|
73
|
-
|
|
74
|
-
## Reading your own inbox
|
|
75
|
-
|
|
76
|
-
At session start, and after finishing a task:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
loadout handoff claude-code
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Work anything listed in order, then run the `loadout handoff --done <id>`
|
|
83
|
-
command it prints. If nothing is pending, say nothing and carry on.
|
|
84
|
-
|
|
85
|
-
`loadout handoff` with no arguments shows every pending task, both directions.
|
|
File without changes
|