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.
Files changed (32) hide show
  1. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  2. package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
  3. package/node_modules/projectstore/AGENTS.md +7 -5
  4. package/node_modules/projectstore/README.md +3 -3
  5. package/node_modules/projectstore/agents/clerk.md +2 -2
  6. package/node_modules/projectstore/commands/agents.md +43 -28
  7. package/node_modules/projectstore/commands/bind.md +1 -1
  8. package/node_modules/projectstore/commands/doctor.md +19 -3
  9. package/node_modules/projectstore/commands/reconcile.md +2 -2
  10. package/node_modules/projectstore/commands/review.md +1 -1
  11. package/node_modules/projectstore/commands/story.md +2 -1
  12. package/node_modules/projectstore/docs/getting-started.md +1 -1
  13. package/node_modules/projectstore/docs/harnesses.md +3 -2
  14. package/node_modules/projectstore/harnesses/claude-code.json +40 -1
  15. package/node_modules/projectstore/harnesses/codex.json +50 -14
  16. package/node_modules/projectstore/package.json +1 -1
  17. package/node_modules/projectstore/scripts/agents-change.mjs +64 -10
  18. package/node_modules/projectstore/scripts/build-adapters.mjs +28 -13
  19. package/node_modules/projectstore/scripts/cli.mjs +32 -9
  20. package/node_modules/projectstore/scripts/configure-menu.mjs +133 -6
  21. package/node_modules/projectstore/scripts/configure.mjs +156 -19
  22. package/node_modules/projectstore/scripts/convergence.mjs +56 -1
  23. package/node_modules/projectstore/scripts/doctor.mjs +134 -35
  24. package/node_modules/projectstore/scripts/harness.mjs +14 -9
  25. package/node_modules/projectstore/scripts/install-harness.mjs +182 -14
  26. package/node_modules/projectstore/scripts/lib.mjs +83 -24
  27. package/node_modules/projectstore/scripts/menu.mjs +9 -5
  28. package/node_modules/projectstore/scripts/models.mjs +150 -6
  29. package/node_modules/projectstore/scripts/setup.mjs +7 -5
  30. package/node_modules/projectstore/scripts/surfaces.mjs +36 -9
  31. package/node_modules/projectstore/templates/claude-md-block.md.tmpl +7 -5
  32. 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. A malformed spec or a name outside the roster comes
11
- // back as `usage`, with nothing planned.
12
- export function planAgentsChange({ before, harness, layout, roster, defaultModel, agents = [], reset = false }) {
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
- // Keys inside the agents block that the allowlist rejected (an `effort`, a
53
- // stray shape) are rewritten out by the write and announced here; keys
54
- // outside the block are kept, and stay doctor's to name.
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
- export function applyAgentsChange(projectDir, harness, next) {
82
- const path = writeOverlayAt(projectDir, harness, next);
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 returned by the core
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. Use actual Codex model
126
- ids supplied by the user or visible in the current host; do not translate model
127
- names from another harness. Preview the exact argv, ask for approval, then run
128
- \`agents configure --harness codex [--default <model>] [--agent
129
- <role>=<model> ...] --project "$PWD"\`. The core is the only writer. Do not pass
130
- an effort override: per-role effort is outside this integration's current
131
- contract. A configured model is consumed by the role-orchestration skills on
132
- their next spawn; no restart is needed.`,
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. Do not pass a reasoning-effort override: per-role effort belongs\nto a separate accepted story. Give the spawned agent the role contract below\nand the exact artifact/diff it must inspect. Wait for its final result.\n\n## Role contract\n\n`;
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) \(max-effort\)\s+/,
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 agent, from
1411
- // the active harness's overlay (ADR-008's two terms); agents show — the overlay
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 names = [...new Set([...(roster || []), ...Object.keys(before.agents.per_agent)])].sort();
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
- return [n, { model: r.model, source: r.source }];
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 ? Object.keys(before.agents.per_agent).filter((n) => !roster.includes(n)) : [];
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 name = s.key.slice(7);
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
- title: `${display} · ${name}`,
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 a model of its own.`,
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
  });