projectstore-claude 0.30.2 → 0.30.3
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/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/AGENTS.md +7 -5
- package/node_modules/projectstore/README.md +3 -3
- package/node_modules/projectstore/agents/clerk.md +2 -2
- package/node_modules/projectstore/commands/agents.md +38 -26
- package/node_modules/projectstore/commands/bind.md +1 -1
- package/node_modules/projectstore/commands/doctor.md +15 -3
- package/node_modules/projectstore/commands/reconcile.md +2 -2
- package/node_modules/projectstore/commands/review.md +1 -1
- package/node_modules/projectstore/commands/story.md +2 -1
- package/node_modules/projectstore/docs/getting-started.md +1 -1
- package/node_modules/projectstore/docs/harnesses.md +3 -2
- package/node_modules/projectstore/harnesses/claude-code.json +40 -1
- package/node_modules/projectstore/harnesses/codex.json +50 -14
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/agents-change.mjs +64 -10
- package/node_modules/projectstore/scripts/build-adapters.mjs +28 -13
- package/node_modules/projectstore/scripts/cli.mjs +32 -9
- package/node_modules/projectstore/scripts/configure-menu.mjs +133 -6
- package/node_modules/projectstore/scripts/configure.mjs +156 -19
- package/node_modules/projectstore/scripts/doctor.mjs +95 -31
- package/node_modules/projectstore/scripts/install-harness.mjs +29 -0
- package/node_modules/projectstore/scripts/lib.mjs +32 -9
- package/node_modules/projectstore/scripts/menu.mjs +9 -5
- package/node_modules/projectstore/scripts/models.mjs +150 -6
- package/node_modules/projectstore/templates/claude-md-block.md.tmpl +7 -5
- package/package.json +2 -2
|
@@ -102,7 +102,7 @@ function skill(name, description, body, { runtime = true } = {}) {
|
|
|
102
102
|
const COMMAND_OVERRIDES = {
|
|
103
103
|
agents: {
|
|
104
104
|
description:
|
|
105
|
-
"Register or remove ProjectStore's shared agents block, inspect its Codex model overlay, or configure per-role models. Arguments: <register | unregister | status | configure>.",
|
|
105
|
+
"Register or remove ProjectStore's shared agents block, inspect its Codex model overlay, or configure per-role models and efforts. Arguments: <register | unregister | status | configure>.",
|
|
106
106
|
body: `You are managing ProjectStore's Codex agent integration. Require a bound project.
|
|
107
107
|
|
|
108
108
|
## register / unregister
|
|
@@ -117,19 +117,22 @@ report the envelope's result. Never edit the managed block by hand.
|
|
|
117
117
|
|
|
118
118
|
Run \`plan --json --harness codex --surface agents_block --project "$PWD"\`,
|
|
119
119
|
then \`agents show --json --project "$PWD"\`. Report the block state and each
|
|
120
|
-
role's resolved model and source. The active overlay path
|
|
121
|
-
is authoritative.
|
|
120
|
+
role's resolved model and effort, each with its source. The active overlay path
|
|
121
|
+
returned by the core is authoritative.
|
|
122
122
|
|
|
123
123
|
## configure
|
|
124
124
|
|
|
125
|
-
Ask for a default model and optional per-role model ids
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
Ask for a default model and optional per-role model ids, then a default effort
|
|
126
|
+
and optional per-role efforts. Use actual Codex model ids and levels supplied by
|
|
127
|
+
the user or visible in the current host; do not translate model names from
|
|
128
|
+
another harness. Preview the exact argv, ask for approval, then run
|
|
129
|
+
\`configure --set agents.default=<model> --set agents.default.effort=<level>
|
|
130
|
+
[--set agents.<role>=<model>] [--set agents.<role>.effort=<level>] ... --harness
|
|
131
|
+
codex --project "$PWD"\`, after the same command with \`--dry-run\` showed its
|
|
132
|
+
plan. \`-\` as a value clears one. The core is the only writer; a level the
|
|
133
|
+
role's model does not list is written with a note in the plan. A configured
|
|
134
|
+
model and effort are consumed by the role-orchestration skills on their next
|
|
135
|
+
spawn; no restart is needed.`,
|
|
133
136
|
},
|
|
134
137
|
bind: {
|
|
135
138
|
description:
|
|
@@ -232,13 +235,25 @@ function renderPassive(file) {
|
|
|
232
235
|
return [join("skills", name, "SKILL.md"), skill(name, data.description || name, body)];
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
// The spawn's effort parameter is the target manifest's vocabulary: a
|
|
239
|
+
// manifest without it cannot render a role that passes one, and says so
|
|
240
|
+
// rather than guessing a name (as the clerk pin refuses a literal model).
|
|
241
|
+
function effortParam() {
|
|
242
|
+
const p = TARGET.agent_translation?.effort?.spawn_param;
|
|
243
|
+
if (!p)
|
|
244
|
+
throw new Error(
|
|
245
|
+
`${TARGET.id}: agent_translation.effort.spawn_param is missing; a role skill cannot name the effort`,
|
|
246
|
+
);
|
|
247
|
+
return p;
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
function renderRole(file) {
|
|
236
251
|
const { data, body } = frontmatter(read(file), file);
|
|
237
252
|
const role = file.split("/").pop().replace(/\.md$/, "");
|
|
238
253
|
const name = renderedName("agents", role);
|
|
239
|
-
const orchestration = `## Codex orchestration\n\nThis is a role-orchestration skill, not a native agent registration. Resolve the\nrole model by running:\n\n\`\`\`bash\nnode "\${PROJECTSTORE_CORE_ROOT}/bin/projectstore.mjs" agents model ${role} --json --project "$PWD"\n\`\`\`\n\nSpawn a collaboration agent for the bounded task. If the result names a model,\npass that model and use an empty or bounded context fork; otherwise inherit the\ncurrent model.
|
|
254
|
+
const orchestration = `## Codex orchestration\n\nThis is a role-orchestration skill, not a native agent registration. Resolve the\nrole's model and effort by running:\n\n\`\`\`bash\nnode "\${PROJECTSTORE_CORE_ROOT}/bin/projectstore.mjs" agents model ${role} --json --project "$PWD"\n\`\`\`\n\nSpawn a collaboration agent for the bounded task. If the result names a model,\npass that model and use an empty or bounded context fork; otherwise inherit the\ncurrent model. If the result names an effort, pass it as \`${effortParam()}\`,\nwith or without a model; otherwise pass none. Give the spawned agent the role\ncontract below and the exact artifact/diff it must inspect. Wait for its final\nresult.\n\n## Role contract\n\n`;
|
|
240
255
|
const description = (data.description || `Run the ProjectStore ${role} role in a fresh collaboration agent.`).replace(
|
|
241
|
-
/^(?:Opus|Sonnet) \(
|
|
256
|
+
/^(?:Opus|Sonnet) \([a-z]+-effort\)\s+/,
|
|
242
257
|
"",
|
|
243
258
|
);
|
|
244
259
|
return [join("skills", name, "SKILL.md"), skill(name, description, orchestration + body)];
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
readConfigAt,
|
|
72
72
|
readOverlayAt,
|
|
73
73
|
resolveAgentModel,
|
|
74
|
+
resolveAgentEffort,
|
|
74
75
|
overlayId,
|
|
75
76
|
layoutRoster,
|
|
76
77
|
commandForm,
|
|
@@ -1407,8 +1408,9 @@ async function runScaffold(ctx) {
|
|
|
1407
1408
|
return emit(true, scaffoldResult(plan, done), renderScaffoldPlan(plan, done, { env }), 0);
|
|
1408
1409
|
}
|
|
1409
1410
|
|
|
1410
|
-
// agents model <name> — the model a plugin surface passes for that
|
|
1411
|
-
// the active harness's overlay (ADR-008's two terms
|
|
1411
|
+
// agents model <name> — the model and effort a plugin surface passes for that
|
|
1412
|
+
// agent, from the active harness's overlay (ADR-008's two terms, once per
|
|
1413
|
+
// field; the effort ADR, decision 2); agents show — the overlay
|
|
1412
1414
|
// as read, with the keys the allowlist rejected; agents configure — the one
|
|
1413
1415
|
// writer, behind the same gate as install: naming --harness is the
|
|
1414
1416
|
// confirmation, a bare non-TTY call refuses (layout spec, contracts 2–4).
|
|
@@ -1439,11 +1441,15 @@ async function runAgents(ctx) {
|
|
|
1439
1441
|
const name = positionals[1];
|
|
1440
1442
|
if (!name) return usage("agents model takes the agent's bare name (critic, planner, …)");
|
|
1441
1443
|
const r = resolveAgentModel(project, name, { harness });
|
|
1444
|
+
const e = resolveAgentEffort(project, name, { harness });
|
|
1445
|
+
// The effort clause only when one resolves, so the line reads as it did
|
|
1446
|
+
// for an overlay of models alone.
|
|
1447
|
+
const effortText = e.effort ? `; effort ${e.effort} (${e.source})` : "";
|
|
1442
1448
|
return emit(
|
|
1443
1449
|
"agents model",
|
|
1444
1450
|
true,
|
|
1445
|
-
r,
|
|
1446
|
-
`${name}: ${r.model ? `${r.model} (${r.source}, ${r.overlay})` : "no model configured — the agent's frontmatter decides"}\n`,
|
|
1451
|
+
{ ...r, effort: e.effort, effort_source: e.source },
|
|
1452
|
+
`${name}: ${r.model ? `${r.model} (${r.source}, ${r.overlay})` : "no model configured — the agent's frontmatter decides"}${effortText}\n`,
|
|
1447
1453
|
);
|
|
1448
1454
|
}
|
|
1449
1455
|
const before = readOverlayAt(project, harness);
|
|
@@ -1451,22 +1457,28 @@ async function runAgents(ctx) {
|
|
|
1451
1457
|
if (sub === "show") {
|
|
1452
1458
|
// Resolved per roster agent (per configured name when no layout loads), so
|
|
1453
1459
|
// a reader reports what would run without restating the two-term rule.
|
|
1454
|
-
const
|
|
1460
|
+
const configured = [
|
|
1461
|
+
...new Set([...Object.keys(before.agents.per_agent), ...Object.keys(before.efforts.per_agent)]),
|
|
1462
|
+
];
|
|
1463
|
+
const names = [...new Set([...(roster || []), ...configured])].sort();
|
|
1455
1464
|
const resolved = Object.fromEntries(
|
|
1456
1465
|
names.map((n) => {
|
|
1457
1466
|
const r = resolveAgentModel(project, n, { harness });
|
|
1458
|
-
|
|
1467
|
+
const e = resolveAgentEffort(project, n, { harness });
|
|
1468
|
+
return [n, { model: r.model, source: r.source, effort: e.effort, effort_source: e.source }];
|
|
1459
1469
|
}),
|
|
1460
1470
|
);
|
|
1461
|
-
const unknown = roster ?
|
|
1471
|
+
const unknown = roster ? configured.filter((n) => !roster.includes(n)) : [];
|
|
1462
1472
|
const lines = [
|
|
1463
1473
|
`overlay: ${before.path || "(none: no harness detected and none named)"}${before.path && !before.present ? " (absent)" : ""}`,
|
|
1464
1474
|
];
|
|
1465
1475
|
if (before.unparseable) lines.push(" not valid JSON");
|
|
1466
1476
|
if (before.agents.default) lines.push(` default: ${before.agents.default}`);
|
|
1477
|
+
if (before.efforts.default) lines.push(` default effort: ${before.efforts.default}`);
|
|
1478
|
+
const effortOf = (n) => (resolved[n].effort ? `, effort ${resolved[n].effort} (${resolved[n].effort_source})` : "");
|
|
1467
1479
|
for (const n of names)
|
|
1468
1480
|
lines.push(
|
|
1469
|
-
` ${n}: ${resolved[n].model ? `${resolved[n].model} (${resolved[n].source})` : "— (the agent's frontmatter)"}${unknown.includes(n) ? " — not in the roster: nothing runs under this name" : ""}`,
|
|
1481
|
+
` ${n}: ${resolved[n].model ? `${resolved[n].model} (${resolved[n].source})` : "— (the agent's frontmatter)"}${effortOf(n)}${unknown.includes(n) ? " — not in the roster: nothing runs under this name" : ""}`,
|
|
1470
1482
|
);
|
|
1471
1483
|
if (before.rejected.length) lines.push(` ignored (not an overlay key): ${before.rejected.join(", ")}`);
|
|
1472
1484
|
if (inBinding) lines.push(" the binding still carries an agents block — a pre-0.28 leftover; run upgrade");
|
|
@@ -1479,6 +1491,7 @@ async function runAgents(ctx) {
|
|
|
1479
1491
|
present: before.present,
|
|
1480
1492
|
unparseable: before.unparseable,
|
|
1481
1493
|
agents: before.agents,
|
|
1494
|
+
efforts: before.efforts,
|
|
1482
1495
|
resolved,
|
|
1483
1496
|
roster,
|
|
1484
1497
|
unknown,
|
|
@@ -1535,7 +1548,16 @@ async function runAgents(ctx) {
|
|
|
1535
1548
|
{ harness, path: before.path, wrote: false, declined: true, agents: next, dropped },
|
|
1536
1549
|
preview.join("\n") + "\n nothing written.\n",
|
|
1537
1550
|
);
|
|
1538
|
-
const { path, after } = applyAgentsChange(project, harness, next);
|
|
1551
|
+
const { path, after } = applyAgentsChange(project, harness, next, change.nextEfforts);
|
|
1552
|
+
// Efforts join the envelope only where the overlay holds one, so a run over
|
|
1553
|
+
// models alone answers exactly as it did before efforts existed.
|
|
1554
|
+
const withEfforts =
|
|
1555
|
+
after.efforts.default ||
|
|
1556
|
+
Object.keys(after.efforts.per_agent).length ||
|
|
1557
|
+
before.efforts.default ||
|
|
1558
|
+
Object.keys(before.efforts.per_agent).length
|
|
1559
|
+
? { efforts: after.efforts, pinnedClerkEffort: change.pinnedClerkEffort }
|
|
1560
|
+
: {};
|
|
1539
1561
|
return emit(
|
|
1540
1562
|
"agents configure",
|
|
1541
1563
|
true,
|
|
@@ -1544,6 +1566,7 @@ async function runAgents(ctx) {
|
|
|
1544
1566
|
path,
|
|
1545
1567
|
wrote: true,
|
|
1546
1568
|
agents: after.agents,
|
|
1569
|
+
...withEfforts,
|
|
1547
1570
|
pinnedClerk,
|
|
1548
1571
|
clerkPinUnavailable,
|
|
1549
1572
|
dropped,
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
|
|
17
17
|
import * as term from "./term.mjs";
|
|
18
18
|
import { createMenu, renderMenu, seg, SMALLEST } from "./menu.mjs";
|
|
19
|
-
import { knownModels, readLive } from "./models.mjs";
|
|
19
|
+
import { knownModels, readLive, effortSpec, effortLevels } from "./models.mjs";
|
|
20
20
|
import { loadHarness } from "./harness.mjs";
|
|
21
21
|
import { readOverlayAt } from "./lib.mjs";
|
|
22
22
|
|
|
23
23
|
const CLEAR = "-";
|
|
24
24
|
|
|
25
|
+
// What an effort row sets: the settings panel's first sentence for it.
|
|
26
|
+
const EFFORT = "How long the agent thinks before it answers: a higher level is slower and costs more.";
|
|
27
|
+
|
|
25
28
|
// What each agent of the roster does: the settings panel's first sentence.
|
|
26
29
|
const ROLE = Object.freeze({
|
|
27
30
|
default: "The model for every agent that has none of its own.",
|
|
@@ -264,15 +267,17 @@ function agentsGroup(f, id, list, catalogs, { menu, now, caps: c }) {
|
|
|
264
267
|
return d ? (menu?.() ? menu().eff(d) : d.value) : null;
|
|
265
268
|
};
|
|
266
269
|
for (const s of list) {
|
|
267
|
-
const
|
|
270
|
+
const isEffort = s.rail === "effort";
|
|
271
|
+
const name = isEffort ? s.key.slice(7, -7) : s.key.slice(7);
|
|
268
272
|
const isDefault = name === "default";
|
|
269
273
|
if (s.source === "unparseable") {
|
|
270
274
|
items.push({
|
|
271
275
|
id: `${id}:${s.key}`,
|
|
272
276
|
key: s.key,
|
|
273
277
|
harness: id,
|
|
274
|
-
label: name,
|
|
275
|
-
|
|
278
|
+
label: isEffort ? "effort" : name,
|
|
279
|
+
...(isEffort ? { indent: 2, path: `${name} · effort` } : {}),
|
|
280
|
+
title: `${display} · ${name}${isEffort ? " · effort" : ""}`,
|
|
276
281
|
kind: "info",
|
|
277
282
|
value: null,
|
|
278
283
|
tag: SOURCE.unparseable,
|
|
@@ -280,6 +285,10 @@ function agentsGroup(f, id, list, catalogs, { menu, now, caps: c }) {
|
|
|
280
285
|
});
|
|
281
286
|
continue;
|
|
282
287
|
}
|
|
288
|
+
if (isEffort) {
|
|
289
|
+
items.push(effortItem(h, display, id, s, name, isDefault, o, items, catalogs, { defaultNow, menu, now }));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
283
292
|
const item = {
|
|
284
293
|
id: `${id}:${s.key}`,
|
|
285
294
|
key: s.key,
|
|
@@ -314,11 +323,127 @@ function agentsGroup(f, id, list, catalogs, { menu, now, caps: c }) {
|
|
|
314
323
|
return {
|
|
315
324
|
title: `Agents · ${display}`,
|
|
316
325
|
footer: file.split("/").pop(),
|
|
317
|
-
description: `Which model each projectstore agent runs on in ${display}${h?.agent_translation?.models?.spawn_takes === "id" ? `, in ${display}'s own model names` : ""}. A model set here goes to ${file}, which is committed, so it applies to everyone on this project. default covers every agent without
|
|
326
|
+
description: `Which model each projectstore agent runs on in ${display}${h?.agent_translation?.models?.spawn_takes === "id" ? `, in ${display}'s own model names` : ""}, and at what effort. A model or an effort set here goes to ${file}, which is committed, so it applies to everyone on this project. default covers every agent without one of its own.`,
|
|
318
327
|
items,
|
|
319
328
|
};
|
|
320
329
|
}
|
|
321
330
|
|
|
331
|
+
// An agent's effort row (the effort ADR, decision 4): under its model row,
|
|
332
|
+
// its value its own level, else the default's, else what the harness does
|
|
333
|
+
// with none; its values the levels of the model it will run on, a model
|
|
334
|
+
// change waiting in the menu counted.
|
|
335
|
+
function effortItem(h, display, id, s, name, isDefault, o, items, catalogs, { defaultNow, menu, now }) {
|
|
336
|
+
const es = effortSpec(h);
|
|
337
|
+
const own = isDefault ? o.efforts.default || null : o.efforts.per_agent[name] || null;
|
|
338
|
+
const eff = (it) => (it ? (menu?.() ? menu().eff(it) : it.value) : null);
|
|
339
|
+
const modelItem = items.find((x) => x.key === (isDefault ? "agents.default" : `agents.${name}`));
|
|
340
|
+
const defaultEffortItem = () => items.find((x) => x.key === "agents.default.effort");
|
|
341
|
+
// The model this agent will run on: its own, else the default's.
|
|
342
|
+
const runModel = () => eff(modelItem) || (isDefault ? null : defaultNow()) || null;
|
|
343
|
+
const defaultEffortNow = () => (isDefault ? null : eff(defaultEffortItem()));
|
|
344
|
+
const levelsNow = () => effortLevels(h, runModel(), catalogs[id]?.known || null);
|
|
345
|
+
// What runs when nothing is configured: the frontmatter's level, or, where
|
|
346
|
+
// a passed model's own default runs, that level when its list names it.
|
|
347
|
+
const unsetText = () => {
|
|
348
|
+
const m = runModel();
|
|
349
|
+
const d = es.unset.model_default && m ? levelsNow()?.default : null;
|
|
350
|
+
return d ? `${d} (${m}'s default)` : es.unset.value;
|
|
351
|
+
};
|
|
352
|
+
const unsetMeaning = es.unset.model_default
|
|
353
|
+
? "each agent runs at its model's own default level"
|
|
354
|
+
: "each agent runs at its frontmatter's effort";
|
|
355
|
+
const item = {
|
|
356
|
+
id: `${id}:${s.key}`,
|
|
357
|
+
key: s.key,
|
|
358
|
+
harness: id,
|
|
359
|
+
label: "effort",
|
|
360
|
+
indent: 2,
|
|
361
|
+
path: `${name} · effort`,
|
|
362
|
+
title: `${display} · ${name} · effort`,
|
|
363
|
+
kind: "choice",
|
|
364
|
+
value: own,
|
|
365
|
+
tag: own ? (isDefault ? SOURCE.overlay : SOURCE.per_agent) : isDefault ? SOURCE.default : "",
|
|
366
|
+
display: (v) => {
|
|
367
|
+
if (v) return v;
|
|
368
|
+
const d = defaultEffortNow();
|
|
369
|
+
return d ? `${d} (default)` : unsetText();
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
item.describe = (v, changed) => {
|
|
373
|
+
if (changed) return `${EFFORT} In the file: ${item.display(item.value)}. After the change: ${item.display(v)}.`;
|
|
374
|
+
if (isDefault)
|
|
375
|
+
return `${EFFORT} ${v ? `Now: ${v}, for every agent without its own.` : `Now: not set, so ${unsetMeaning}.`}`;
|
|
376
|
+
if (v) return `${EFFORT} Now: ${v}, its own.`;
|
|
377
|
+
const d = defaultEffortNow();
|
|
378
|
+
return `${EFFORT} ${d ? `Now: ${d}, from default.` : `Now: ${item.display(null)}.`}`;
|
|
379
|
+
};
|
|
380
|
+
item.options = () =>
|
|
381
|
+
effortOptions(es, display, item, name, isDefault, levelsNow(), runModel(), defaultEffortNow(), menu);
|
|
382
|
+
item.caption = () => {
|
|
383
|
+
const l = levelsNow();
|
|
384
|
+
if (!l) return null;
|
|
385
|
+
if (l.from === "catalog")
|
|
386
|
+
return {
|
|
387
|
+
text: `levels of ${l.model}${l.source ? ` · ${l.source}` : ""}${l.at ? ` · checked ${checkedWhen(l.at, now())}` : ""}`,
|
|
388
|
+
};
|
|
389
|
+
return { text: `${display}'s levels` };
|
|
390
|
+
};
|
|
391
|
+
return item;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// An effort row's values: the levels, the value in the file when the list
|
|
395
|
+
// lacks it, then the way back to the default. No free text and no refresh:
|
|
396
|
+
// the levels come with the model list, whose frame refreshes it.
|
|
397
|
+
function effortOptions(es, display, item, name, isDefault, levels, model, d, menu) {
|
|
398
|
+
const opts = (levels?.levels || []).map((l, i) => ({
|
|
399
|
+
value: l.id,
|
|
400
|
+
label: l.id,
|
|
401
|
+
kind: "value",
|
|
402
|
+
note: l.id === levels.default ? "default" : "",
|
|
403
|
+
section: i === 0 ? (levels.from === "catalog" ? `levels of ${model}` : `${display}'s levels`) : undefined,
|
|
404
|
+
description: [
|
|
405
|
+
l.description ? (/[.!?]$/.test(l.description) ? l.description : `${l.description}.`) : "",
|
|
406
|
+
l.id === levels.default ? `${model}'s own default.` : "",
|
|
407
|
+
]
|
|
408
|
+
.filter(Boolean)
|
|
409
|
+
.join(" "),
|
|
410
|
+
}));
|
|
411
|
+
const m = menu?.();
|
|
412
|
+
for (const v of [item.value, m ? m.eff(item) : null])
|
|
413
|
+
if (v && !opts.some((o) => o.value === v))
|
|
414
|
+
opts.push({
|
|
415
|
+
value: v,
|
|
416
|
+
label: v,
|
|
417
|
+
note: "not in the list",
|
|
418
|
+
kind: "value",
|
|
419
|
+
section: opts.length ? undefined : "",
|
|
420
|
+
description: model
|
|
421
|
+
? `Not a level ${model} lists. It is kept as it is.`
|
|
422
|
+
: `Not one of ${display}'s levels. It is kept as it is.`,
|
|
423
|
+
});
|
|
424
|
+
const meaning = es.unset.model_default ? "its model's own default level" : "its frontmatter's effort";
|
|
425
|
+
opts.push(
|
|
426
|
+
isDefault
|
|
427
|
+
? {
|
|
428
|
+
value: null,
|
|
429
|
+
label: es.unset.row,
|
|
430
|
+
kind: "value",
|
|
431
|
+
section: "",
|
|
432
|
+
description: `Drops the default effort: each agent without its own runs at ${meaning}.`,
|
|
433
|
+
}
|
|
434
|
+
: {
|
|
435
|
+
value: null,
|
|
436
|
+
label: "use the default",
|
|
437
|
+
kind: "value",
|
|
438
|
+
section: "",
|
|
439
|
+
description: d
|
|
440
|
+
? `Drops ${name}'s own effort: it runs at the default, ${d}.`
|
|
441
|
+
: `Drops ${name}'s own effort: it runs at ${meaning}.`,
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
return opts;
|
|
445
|
+
}
|
|
446
|
+
|
|
322
447
|
// The values frame's footer: the list's source and age, the host being
|
|
323
448
|
// asked, or why nothing can be asked (the ADR, decisions 3 and 6).
|
|
324
449
|
function caption(cat, now) {
|
|
@@ -502,8 +627,10 @@ export async function runMenu(ctx) {
|
|
|
502
627
|
menu = createMenu({
|
|
503
628
|
groups,
|
|
504
629
|
onRefresh: (it) => it.harness && read(it.harness),
|
|
630
|
+
// A list is read from a model's frame only: an effort frame shows what is
|
|
631
|
+
// known, and its levels follow when a model's list is refreshed.
|
|
505
632
|
onOpen: (it) => {
|
|
506
|
-
const cat = it.harness ? catalogs[it.harness] : null;
|
|
633
|
+
const cat = it.harness && !it.key?.endsWith(".effort") ? catalogs[it.harness] : null;
|
|
507
634
|
if (cat?.known?.stale) read(it.harness);
|
|
508
635
|
},
|
|
509
636
|
});
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
readOverlayAt,
|
|
24
24
|
resolveAgentModel,
|
|
25
|
+
resolveAgentEffort,
|
|
25
26
|
layoutRoster,
|
|
26
27
|
readVaultConfigStrict,
|
|
27
28
|
nextVaultConfig,
|
|
@@ -35,7 +36,15 @@ import { bandKeys, epicIds, planBand, applyBand } from "./band.mjs";
|
|
|
35
36
|
import { planAgentsChange, applyAgentsChange } from "./agents-change.mjs";
|
|
36
37
|
import { plan as planInstall, apply as applyInstall, planOptions, isInteractive } from "./install-harness.mjs";
|
|
37
38
|
import { analyseLayout } from "./surfaces.mjs";
|
|
38
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
knownModels,
|
|
41
|
+
offListNote,
|
|
42
|
+
effortSpec,
|
|
43
|
+
effortLevels,
|
|
44
|
+
offLevelNote,
|
|
45
|
+
standingLevelNote,
|
|
46
|
+
LEVEL,
|
|
47
|
+
} from "./models.mjs";
|
|
39
48
|
import { runMenu } from "./configure-menu.mjs";
|
|
40
49
|
import { SMALLEST } from "./menu.mjs";
|
|
41
50
|
|
|
@@ -76,6 +85,7 @@ export function readFacts(project, { env = process.env, root, home = homedir() }
|
|
|
76
85
|
return {
|
|
77
86
|
project,
|
|
78
87
|
home,
|
|
88
|
+
env,
|
|
79
89
|
path,
|
|
80
90
|
cfg,
|
|
81
91
|
used,
|
|
@@ -108,6 +118,29 @@ export function settingsOf(f) {
|
|
|
108
118
|
const o = readOverlayAt(f.project, h.id);
|
|
109
119
|
const file = where(f, o.path);
|
|
110
120
|
const base = { group: `agents:${h.id}`, harness: h.id, file, class: "changeable", team: true, writer: "overlay" };
|
|
121
|
+
// Each model setting is followed by its effort setting, where the harness
|
|
122
|
+
// passes one at spawn (the effort ADR, decisions 1 and 4). An effort that
|
|
123
|
+
// nothing configures reads as the harness says: the frontmatter's, or the
|
|
124
|
+
// model's own default level, named when its catalog entry is known.
|
|
125
|
+
const es = effortSpec(h);
|
|
126
|
+
const known = es?.unset?.model_default ? knownModels(h, { env: f.env, home: f.home }) : null;
|
|
127
|
+
// frontmatter: the agent's own; model_default: the passed model's own
|
|
128
|
+
// default, named; harness: the harness's own precedence (no model passed,
|
|
129
|
+
// or none whose default is known).
|
|
130
|
+
const unsetEffort = (model) => {
|
|
131
|
+
if (!es?.unset?.model_default) return { source: "frontmatter" };
|
|
132
|
+
const d = model ? effortLevels(h, model, known)?.default : null;
|
|
133
|
+
return d ? { source: "model_default", model_default: d } : { source: "harness" };
|
|
134
|
+
};
|
|
135
|
+
const effortRow = (key, effort, effortSource, model) =>
|
|
136
|
+
es &&
|
|
137
|
+
out.push({
|
|
138
|
+
...base,
|
|
139
|
+
key,
|
|
140
|
+
value: effort || null,
|
|
141
|
+
...(o.unparseable ? { source: "unparseable" } : effort ? { source: effortSource } : unsetEffort(model)),
|
|
142
|
+
rail: "effort",
|
|
143
|
+
});
|
|
111
144
|
out.push({
|
|
112
145
|
...base,
|
|
113
146
|
key: "agents.default",
|
|
@@ -115,7 +148,10 @@ export function settingsOf(f) {
|
|
|
115
148
|
source: o.unparseable ? "unparseable" : o.agents.default ? "overlay" : "frontmatter",
|
|
116
149
|
rail: "model",
|
|
117
150
|
});
|
|
118
|
-
|
|
151
|
+
effortRow("agents.default.effort", o.efforts.default, "overlay", o.agents.default);
|
|
152
|
+
const names = [
|
|
153
|
+
...new Set([...(f.roster || []), ...Object.keys(o.agents.per_agent), ...Object.keys(o.efforts.per_agent)]),
|
|
154
|
+
];
|
|
119
155
|
for (const n of names) {
|
|
120
156
|
const r = resolveAgentModel(f.project, n, { harness: h.id });
|
|
121
157
|
out.push({
|
|
@@ -125,6 +161,8 @@ export function settingsOf(f) {
|
|
|
125
161
|
source: o.unparseable ? "unparseable" : r.source || "frontmatter",
|
|
126
162
|
rail: "model",
|
|
127
163
|
});
|
|
164
|
+
const e = resolveAgentEffort(f.project, n, { harness: h.id });
|
|
165
|
+
effortRow(`agents.${n}.effort`, e.effort, e.source, r.model);
|
|
128
166
|
}
|
|
129
167
|
}
|
|
130
168
|
if (f.slot)
|
|
@@ -239,11 +277,13 @@ function setForm(s, f) {
|
|
|
239
277
|
? "on|off"
|
|
240
278
|
: s.key === "tags"
|
|
241
279
|
? "<a,b>"
|
|
242
|
-
: s.
|
|
243
|
-
? "<
|
|
244
|
-
: s.key
|
|
245
|
-
? "<
|
|
246
|
-
:
|
|
280
|
+
: s.rail === "effort"
|
|
281
|
+
? "<level>"
|
|
282
|
+
: s.key.startsWith("agents.")
|
|
283
|
+
? "<model>"
|
|
284
|
+
: s.key === "band.epic"
|
|
285
|
+
? "<epic>"
|
|
286
|
+
: `<${s.key}>`;
|
|
247
287
|
const h = s.harness && f.used.length > 1 ? ` --harness ${s.harness}` : "";
|
|
248
288
|
return `--set ${s.key}=${value}${h}`;
|
|
249
289
|
}
|
|
@@ -266,8 +306,10 @@ export function resolveSets(f, sets, { harness = null, nearest = null } = {}) {
|
|
|
266
306
|
const value = String(raw)
|
|
267
307
|
.slice(at + 1)
|
|
268
308
|
.trim();
|
|
269
|
-
|
|
270
|
-
|
|
309
|
+
// agents.<name> and agents.<name>.model are one key; agents.<name>.effort
|
|
310
|
+
// is a key of its own (the effort ADR, decision 4).
|
|
311
|
+
const m = /^agents\.([a-z][a-z0-9-]*)(?:\.(model|effort))?$/.exec(key);
|
|
312
|
+
if (m) key = m[2] === "effort" ? `agents.${m[1]}.effort` : `agents.${m[1]}`;
|
|
271
313
|
if (seen.has(key)) return { usage: `${key} is named twice` };
|
|
272
314
|
seen.add(key);
|
|
273
315
|
if (m) {
|
|
@@ -280,6 +322,19 @@ export function resolveSets(f, sets, { harness = null, nearest = null } = {}) {
|
|
|
280
322
|
else return { usage: `name the overlay: --harness <id> (this project uses ${harnessesNamed})` };
|
|
281
323
|
}
|
|
282
324
|
if (!harnessIds().includes(h)) return { usage: `unknown harness: ${h} — known: ${harnessIds().join(", ")}` };
|
|
325
|
+
if (m[2] === "effort") {
|
|
326
|
+
const manifest = loadHarness(h);
|
|
327
|
+
const es = effortSpec(manifest);
|
|
328
|
+
if (!es) return { usage: `${manifest?.display_name || h} takes no effort at spawn: ${key} has nothing to set` };
|
|
329
|
+
// A lowercase token, or - to clear; a level the harness does not list
|
|
330
|
+
// is written with a note, not refused (decision 5).
|
|
331
|
+
if (value !== CLEAR && !LEVEL.test(value))
|
|
332
|
+
return {
|
|
333
|
+
usage: `${key} takes a level (${es.levels.map((l) => l.id).join(", ")}), or ${CLEAR} to clear it, got ${value || "nothing"}`,
|
|
334
|
+
};
|
|
335
|
+
out.push({ key, writer: "overlay", harness: h, name, field: "effort", value: value === CLEAR ? null : value });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
283
338
|
if (!value) return { usage: `${key} takes a model, or ${CLEAR} to clear it` };
|
|
284
339
|
out.push({ key, writer: "overlay", harness: h, name, value: value === CLEAR ? null : value });
|
|
285
340
|
continue;
|
|
@@ -434,14 +489,19 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
434
489
|
for (const [h, list] of byHarness) {
|
|
435
490
|
const before = readOverlayAt(f.project, h);
|
|
436
491
|
if (before.unparseable) return refuse(`${before.path} is not valid JSON — fix or remove it first`);
|
|
437
|
-
const
|
|
492
|
+
const models = list.filter((c) => c.field !== "effort");
|
|
493
|
+
const efforts = list.filter((c) => c.field === "effort");
|
|
494
|
+
const def = models.find((c) => c.name === "default");
|
|
495
|
+
const defEffort = efforts.find((c) => c.name === "default");
|
|
438
496
|
const change = planAgentsChange({
|
|
439
497
|
before,
|
|
440
498
|
harness: h,
|
|
441
499
|
layout: f.cfg.layout,
|
|
442
500
|
roster: f.roster,
|
|
443
501
|
defaultModel: def ? def.value || "" : undefined,
|
|
444
|
-
agents:
|
|
502
|
+
agents: models.filter((c) => c.name !== "default").map((c) => `${c.name}=${c.value || ""}`),
|
|
503
|
+
defaultEffort: defEffort ? defEffort.value || "" : undefined,
|
|
504
|
+
efforts: efforts.filter((c) => c.name !== "default").map((c) => `${c.name}=${c.value || ""}`),
|
|
445
505
|
});
|
|
446
506
|
if (change.usage) return { usage: change.usage };
|
|
447
507
|
if (change.same) continue;
|
|
@@ -452,11 +512,47 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
452
512
|
// 8 as amended 2026-10-13): the model may be newer than the list.
|
|
453
513
|
// Only a model the run names gets the note: the clerk's pin does not.
|
|
454
514
|
const known = knownModels(manifest, { env, home });
|
|
455
|
-
const named = new Map(
|
|
515
|
+
const named = new Map(models.map((c) => [c.name, c.value]));
|
|
456
516
|
const offList = (name, model) => (model && named.get(name) === model ? offListNote(manifest, model, known) : null);
|
|
457
517
|
const notes = (...xs) => xs.filter(Boolean).join("; ") || null;
|
|
518
|
+
// Effort against the model each agent runs on after the run (the effort
|
|
519
|
+
// ADR, decision 5): a level the run names is checked on its own row; a
|
|
520
|
+
// model the run changes is checked against the level its agents keep, on
|
|
521
|
+
// the model's row; a default level the run names is checked against every
|
|
522
|
+
// agent that inherits it on a model of its own, on the default's row.
|
|
523
|
+
const ne = change.nextEfforts;
|
|
524
|
+
const namedEffort = new Map(efforts.map((c) => [c.name, c.value]));
|
|
525
|
+
const roster = [
|
|
526
|
+
...new Set([...(f.roster || []), ...Object.keys(change.next.per_agent), ...Object.keys(ne.per_agent)]),
|
|
527
|
+
];
|
|
528
|
+
const runModel = (n) => (n === "default" ? change.next.default : change.next.per_agent[n] || change.next.default);
|
|
529
|
+
const runEffort = (n) => (n === "default" ? ne.default : ne.per_agent[n] || ne.default) || null;
|
|
530
|
+
const levelNote = (n, level) =>
|
|
531
|
+
level && namedEffort.get(n) === level ? offLevelNote(manifest, level, runModel(n) || null, known) : null;
|
|
532
|
+
const inheritors = (level) =>
|
|
533
|
+
level && namedEffort.get("default") === level
|
|
534
|
+
? roster
|
|
535
|
+
.filter(
|
|
536
|
+
(n) => !ne.per_agent[n] && change.next.per_agent[n] && change.next.per_agent[n] !== change.next.default,
|
|
537
|
+
)
|
|
538
|
+
.map((n) => standingLevelNote(manifest, n, level, runModel(n), known))
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.join("; ") || null
|
|
541
|
+
: null;
|
|
542
|
+
// Agents a model change moves, and the level each keeps, when that level
|
|
543
|
+
// was not named by this run (a named one is checked on its own row).
|
|
544
|
+
const moved = (n) => {
|
|
545
|
+
const who = n === "default" ? ["default", ...roster.filter((r) => !change.next.per_agent[r])] : [n];
|
|
546
|
+
return (
|
|
547
|
+
who
|
|
548
|
+
.filter((a) => !namedEffort.has(a) && !(a !== "default" && !ne.per_agent[a] && namedEffort.has("default")))
|
|
549
|
+
.map((a) => standingLevelNote(manifest, a, runEffort(a), runModel(a) || null, known))
|
|
550
|
+
.filter(Boolean)
|
|
551
|
+
.join("; ") || null
|
|
552
|
+
);
|
|
553
|
+
};
|
|
458
554
|
if (before.agents.default !== change.next.default) {
|
|
459
|
-
const listNote = offList("default", change.next.default);
|
|
555
|
+
const listNote = notes(offList("default", change.next.default), moved("default"));
|
|
460
556
|
items.push({
|
|
461
557
|
key: "agents.default",
|
|
462
558
|
harness: h,
|
|
@@ -474,7 +570,7 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
474
570
|
const a = before.agents.per_agent[n] || null;
|
|
475
571
|
const b = change.next.per_agent[n] || null;
|
|
476
572
|
if (a === b) continue;
|
|
477
|
-
const listNote = offList(n, b);
|
|
573
|
+
const listNote = notes(offList(n, b), moved(n));
|
|
478
574
|
items.push({
|
|
479
575
|
key: `agents.${n}`,
|
|
480
576
|
harness: h,
|
|
@@ -501,7 +597,43 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
501
597
|
team: true,
|
|
502
598
|
note: `${display} declares no cheap model to pin the clerk to; set agents.clerk to name one`,
|
|
503
599
|
});
|
|
504
|
-
|
|
600
|
+
const beforeEfforts = before.efforts;
|
|
601
|
+
if (beforeEfforts.default !== ne.default) {
|
|
602
|
+
const listNote = notes(levelNote("default", ne.default), inheritors(ne.default));
|
|
603
|
+
items.push({
|
|
604
|
+
key: "agents.default.effort",
|
|
605
|
+
harness: h,
|
|
606
|
+
display,
|
|
607
|
+
file,
|
|
608
|
+
before: shown(beforeEfforts.default),
|
|
609
|
+
after: shown(ne.default),
|
|
610
|
+
team: true,
|
|
611
|
+
note: listNote,
|
|
612
|
+
listNote,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
const effortNames = new Set([...Object.keys(beforeEfforts.per_agent), ...Object.keys(ne.per_agent)]);
|
|
616
|
+
for (const n of [...effortNames].sort()) {
|
|
617
|
+
const a = beforeEfforts.per_agent[n] || null;
|
|
618
|
+
const b = ne.per_agent[n] || null;
|
|
619
|
+
if (a === b) continue;
|
|
620
|
+
const listNote = levelNote(n, b);
|
|
621
|
+
items.push({
|
|
622
|
+
key: `agents.${n}.effort`,
|
|
623
|
+
harness: h,
|
|
624
|
+
display,
|
|
625
|
+
file,
|
|
626
|
+
before: shown(a),
|
|
627
|
+
after: shown(b),
|
|
628
|
+
team: true,
|
|
629
|
+
note: notes(
|
|
630
|
+
change.pinnedClerkEffort && n === "clerk" ? "pinned: the clerk stays cheap under a strong default" : null,
|
|
631
|
+
listNote,
|
|
632
|
+
),
|
|
633
|
+
listNote,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
steps.push({ writer: "overlay", harness: h, next: change.next, nextEfforts: ne });
|
|
505
637
|
}
|
|
506
638
|
|
|
507
639
|
// The status line: install's own plan for the surface, with its flag item
|
|
@@ -602,7 +734,7 @@ export function applyChanges(f, steps, { env = process.env, home = homedir(), ro
|
|
|
602
734
|
writeVaultConfig(f.vault, nextVaultConfig(now.cfg, s.patch, new Date().toISOString()));
|
|
603
735
|
wrote.push(vaultConfigPath(f.vault));
|
|
604
736
|
} else if (s.writer === "overlay") {
|
|
605
|
-
wrote.push(applyAgentsChange(f.project, s.harness, s.next).path);
|
|
737
|
+
wrote.push(applyAgentsChange(f.project, s.harness, s.next, s.nextEfforts).path);
|
|
606
738
|
} else if (s.writer === "statusline") {
|
|
607
739
|
const finished = [];
|
|
608
740
|
try {
|
|
@@ -640,6 +772,8 @@ const SOURCE_TEXT = Object.freeze({
|
|
|
640
772
|
per_agent: "per agent",
|
|
641
773
|
default: "default",
|
|
642
774
|
frontmatter: "its frontmatter",
|
|
775
|
+
model_default: "its model's own default",
|
|
776
|
+
harness: "the harness decides",
|
|
643
777
|
binding: "binding",
|
|
644
778
|
vault_config: "vault config",
|
|
645
779
|
unparseable: "not valid JSON: fix the file to change it",
|
|
@@ -675,9 +809,11 @@ export function renderView(f, settings, { caps: c = term.PLAIN } = {}) {
|
|
|
675
809
|
note:
|
|
676
810
|
s.class === "other-verb"
|
|
677
811
|
? "changed by another command, below"
|
|
678
|
-
: s.
|
|
679
|
-
? `${SOURCE_TEXT
|
|
680
|
-
:
|
|
812
|
+
: s.source === "model_default" && s.model_default
|
|
813
|
+
? `${SOURCE_TEXT.model_default}, ${s.model_default}`
|
|
814
|
+
: s.class === "changeable" && !s.rail
|
|
815
|
+
? `${SOURCE_TEXT[s.source] || s.source}; change it with ${s.set}`
|
|
816
|
+
: SOURCE_TEXT[s.source] || s.source,
|
|
681
817
|
})),
|
|
682
818
|
),
|
|
683
819
|
);
|
|
@@ -764,6 +900,7 @@ export async function runConfigure(ctx) {
|
|
|
764
900
|
harness: s.harness || null,
|
|
765
901
|
team: s.team,
|
|
766
902
|
set: s.set,
|
|
903
|
+
...(s.model_default ? { model_default: s.model_default } : {}),
|
|
767
904
|
}));
|
|
768
905
|
const interactive =
|
|
769
906
|
!json && (ask ? true : isInteractive({ stdin, stdout, env, json })) && term.railAvailable(stdin, c);
|