projectstore-claude 0.30.2 → 0.30.4
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 +43 -28
- package/node_modules/projectstore/commands/bind.md +1 -1
- package/node_modules/projectstore/commands/doctor.md +19 -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/convergence.mjs +56 -1
- package/node_modules/projectstore/scripts/doctor.mjs +134 -35
- package/node_modules/projectstore/scripts/harness.mjs +14 -9
- package/node_modules/projectstore/scripts/install-harness.mjs +182 -14
- package/node_modules/projectstore/scripts/lib.mjs +83 -24
- package/node_modules/projectstore/scripts/menu.mjs +9 -5
- package/node_modules/projectstore/scripts/models.mjs +150 -6
- package/node_modules/projectstore/scripts/setup.mjs +7 -5
- package/node_modules/projectstore/scripts/surfaces.mjs +36 -9
- package/node_modules/projectstore/templates/claude-md-block.md.tmpl +7 -5
- package/package.json +2 -2
|
@@ -7,9 +7,20 @@ import { harnessForOverlay } from "./harness.mjs";
|
|
|
7
7
|
|
|
8
8
|
// What a change would write. `before` is readOverlayAt's read; `roster` the
|
|
9
9
|
// layout's agent names (null when no layout loads); `agents` the
|
|
10
|
-
// `<name>=<model>` specs
|
|
11
|
-
//
|
|
12
|
-
|
|
10
|
+
// `<name>=<model>` specs; `defaultEffort` and `efforts` (`<name>=<level>`)
|
|
11
|
+
// the same for effort (the effort ADR, decision 8). A malformed spec or a name
|
|
12
|
+
// outside the roster comes back as `usage`, with nothing planned.
|
|
13
|
+
export function planAgentsChange({
|
|
14
|
+
before,
|
|
15
|
+
harness,
|
|
16
|
+
layout,
|
|
17
|
+
roster,
|
|
18
|
+
defaultModel,
|
|
19
|
+
agents = [],
|
|
20
|
+
defaultEffort,
|
|
21
|
+
efforts = [],
|
|
22
|
+
reset = false,
|
|
23
|
+
}) {
|
|
13
24
|
// --reset empties the block first; --default and --agent then apply on top of
|
|
14
25
|
// the emptied block — "reset, then set" is one call, and nothing given is
|
|
15
26
|
// silently dropped.
|
|
@@ -27,6 +38,22 @@ export function planAgentsChange({ before, harness, layout, roster, defaultModel
|
|
|
27
38
|
if (m[2]) next.per_agent[m[1]] = m[2];
|
|
28
39
|
else delete next.per_agent[m[1]];
|
|
29
40
|
}
|
|
41
|
+
// Efforts, field by field beside the models: a run that names none keeps
|
|
42
|
+
// every effort the overlay holds, and --reset empties them with the models.
|
|
43
|
+
const beforeEfforts = before.efforts || { default: null, per_agent: {} };
|
|
44
|
+
const baseEfforts = reset ? { default: null, per_agent: {} } : beforeEfforts;
|
|
45
|
+
const nextEfforts = {
|
|
46
|
+
default: defaultEffort !== undefined ? defaultEffort || null : baseEfforts.default,
|
|
47
|
+
per_agent: { ...baseEfforts.per_agent },
|
|
48
|
+
};
|
|
49
|
+
for (const spec of efforts) {
|
|
50
|
+
const m = /^([a-z][a-z0-9-]*)=(.*)$/.exec(spec);
|
|
51
|
+
if (!m) return { usage: `an effort takes <name>=<level>, got ${spec}` };
|
|
52
|
+
if (roster && !roster.includes(m[1]))
|
|
53
|
+
return { usage: `no agent named ${m[1]} in the ${layout} roster — known: ${roster.join(", ")}` };
|
|
54
|
+
if (m[2]) nextEfforts.per_agent[m[1]] = m[2];
|
|
55
|
+
else delete nextEfforts.per_agent[m[1]];
|
|
56
|
+
}
|
|
30
57
|
// The clerk transcribes; a strong default must not lift it (commands/agents.md,
|
|
31
58
|
// ADR-008). WHICH model it is pinned to is the harness's vocabulary, so the
|
|
32
59
|
// manifest names it and this file does not: pinning every harness to
|
|
@@ -49,12 +76,29 @@ export function planAgentsChange({ before, harness, layout, roster, defaultModel
|
|
|
49
76
|
pinnedClerk = true;
|
|
50
77
|
} else clerkPinUnavailable = true;
|
|
51
78
|
}
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
79
|
+
// The clerk's effort, pinned the same way while a default effort stands
|
|
80
|
+
// (the effort ADR, decision 6), where the harness names a cheap level; a
|
|
81
|
+
// harness that names none pins nothing and says nothing, because its clerk
|
|
82
|
+
// already runs at its frontmatter's level.
|
|
83
|
+
let pinnedClerkEffort = false;
|
|
84
|
+
const clerkEffortAsked = efforts.some((s) => s.startsWith("clerk="));
|
|
85
|
+
if (nextEfforts.default && !nextEfforts.per_agent.clerk && !clerkEffortAsked) {
|
|
86
|
+
const cheap = harnessForOverlay(harness)?.agent_translation?.effort?.cheap || null;
|
|
87
|
+
if (cheap) {
|
|
88
|
+
nextEfforts.per_agent.clerk = cheap;
|
|
89
|
+
pinnedClerkEffort = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Keys inside the agents block that the allowlist rejected (a stray shape)
|
|
93
|
+
// are rewritten out by the write and announced here; keys outside the block
|
|
94
|
+
// are kept, and stay doctor's to name.
|
|
55
95
|
const dropped = before.rejected.filter((k) => k.startsWith("agents."));
|
|
96
|
+
const sameEfforts =
|
|
97
|
+
JSON.stringify({ d: beforeEfforts.default, p: beforeEfforts.per_agent }) ===
|
|
98
|
+
JSON.stringify({ d: nextEfforts.default, p: nextEfforts.per_agent });
|
|
56
99
|
const same =
|
|
57
100
|
!dropped.length &&
|
|
101
|
+
sameEfforts &&
|
|
58
102
|
JSON.stringify({ d: before.agents.default, p: before.agents.per_agent }) ===
|
|
59
103
|
JSON.stringify({ d: next.default, p: next.per_agent });
|
|
60
104
|
const preview = [
|
|
@@ -72,13 +116,23 @@ export function planAgentsChange({ before, harness, layout, roster, defaultModel
|
|
|
72
116
|
preview.push(
|
|
73
117
|
` clerk: takes the default — ${harness} declares no cheap model to pin it to, so name one with --agent clerk=<model>`,
|
|
74
118
|
);
|
|
119
|
+
// Effort lines only where an effort is held or asked for, so a run over an
|
|
120
|
+
// overlay of models alone previews exactly as it did before efforts.
|
|
121
|
+
if (beforeEfforts.default || nextEfforts.default)
|
|
122
|
+
preview.push(` default effort: ${beforeEfforts.default || "—"} → ${nextEfforts.default || "—"}`);
|
|
123
|
+
const effortNames = new Set([...Object.keys(beforeEfforts.per_agent), ...Object.keys(nextEfforts.per_agent)]);
|
|
124
|
+
for (const n of [...effortNames].sort())
|
|
125
|
+
preview.push(
|
|
126
|
+
` ${n} effort: ${beforeEfforts.per_agent[n] || "—"} → ${nextEfforts.per_agent[n] || "—"}${pinnedClerkEffort && n === "clerk" ? " (pinned: the clerk stays cheap under a strong default)" : ""}`,
|
|
127
|
+
);
|
|
75
128
|
if (dropped.length)
|
|
76
|
-
preview.push(` drops from the agents block (an overlay carries only models): ${dropped.join(", ")}`);
|
|
77
|
-
return { next, pinnedClerk, clerkPinUnavailable, dropped, same, preview };
|
|
129
|
+
preview.push(` drops from the agents block (an overlay carries only models and efforts): ${dropped.join(", ")}`);
|
|
130
|
+
return { next, nextEfforts, pinnedClerk, pinnedClerkEffort, clerkPinUnavailable, dropped, same, preview };
|
|
78
131
|
}
|
|
79
132
|
|
|
80
133
|
// Writes a planned change through the overlay's one writer and reads it back.
|
|
81
|
-
|
|
82
|
-
|
|
134
|
+
// `nextEfforts` left out keeps the efforts the file holds.
|
|
135
|
+
export function applyAgentsChange(projectDir, harness, next, nextEfforts) {
|
|
136
|
+
const path = writeOverlayAt(projectDir, harness, next, nextEfforts);
|
|
83
137
|
return { path, after: readOverlayAt(projectDir, harness) };
|
|
84
138
|
}
|
|
@@ -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
|
});
|