projectstore-claude 0.30.1 → 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 +5 -5
- 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 +92 -2
- package/node_modules/projectstore/harnesses/codex.json +81 -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 +716 -0
- package/node_modules/projectstore/scripts/configure.mjs +239 -91
- package/node_modules/projectstore/scripts/doctor.mjs +95 -31
- package/node_modules/projectstore/scripts/harness.mjs +13 -3
- 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 +746 -0
- package/node_modules/projectstore/scripts/models.mjs +649 -0
- package/node_modules/projectstore/scripts/term.mjs +167 -0
- package/node_modules/projectstore/templates/claude-md-block.md.tmpl +7 -5
- package/package.json +2 -2
|
@@ -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,10 +36,18 @@ 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";
|
|
39
|
+
import {
|
|
40
|
+
knownModels,
|
|
41
|
+
offListNote,
|
|
42
|
+
effortSpec,
|
|
43
|
+
effortLevels,
|
|
44
|
+
offLevelNote,
|
|
45
|
+
standingLevelNote,
|
|
46
|
+
LEVEL,
|
|
47
|
+
} from "./models.mjs";
|
|
48
|
+
import { runMenu } from "./configure-menu.mjs";
|
|
49
|
+
import { SMALLEST } from "./menu.mjs";
|
|
38
50
|
|
|
39
|
-
// The models the rail offers: the in-session `agents configure` presets
|
|
40
|
-
// (commands/agents.md). Any other model is set with --set (contract 2).
|
|
41
|
-
const MODEL_PRESETS = Object.freeze(["opus", "fable", "sonnet"]);
|
|
42
51
|
const CLEAR = "-";
|
|
43
52
|
|
|
44
53
|
const LANGUAGE_NOTE =
|
|
@@ -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,20 +489,70 @@ 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;
|
|
448
508
|
const file = where(f, before.path);
|
|
449
|
-
const
|
|
450
|
-
|
|
509
|
+
const manifest = loadHarness(h);
|
|
510
|
+
const display = manifest.display_name || h;
|
|
511
|
+
// A model no list of the harness holds is written, with a note (contract
|
|
512
|
+
// 8 as amended 2026-10-13): the model may be newer than the list.
|
|
513
|
+
// Only a model the run names gets the note: the clerk's pin does not.
|
|
514
|
+
const known = knownModels(manifest, { env, home });
|
|
515
|
+
const named = new Map(models.map((c) => [c.name, c.value]));
|
|
516
|
+
const offList = (name, model) => (model && named.get(name) === model ? offListNote(manifest, model, known) : null);
|
|
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
|
+
};
|
|
554
|
+
if (before.agents.default !== change.next.default) {
|
|
555
|
+
const listNote = notes(offList("default", change.next.default), moved("default"));
|
|
451
556
|
items.push({
|
|
452
557
|
key: "agents.default",
|
|
453
558
|
harness: h,
|
|
@@ -456,12 +561,16 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
456
561
|
before: shown(before.agents.default),
|
|
457
562
|
after: shown(change.next.default),
|
|
458
563
|
team: true,
|
|
564
|
+
note: listNote,
|
|
565
|
+
listNote,
|
|
459
566
|
});
|
|
567
|
+
}
|
|
460
568
|
const names = new Set([...Object.keys(before.agents.per_agent), ...Object.keys(change.next.per_agent)]);
|
|
461
569
|
for (const n of [...names].sort()) {
|
|
462
570
|
const a = before.agents.per_agent[n] || null;
|
|
463
571
|
const b = change.next.per_agent[n] || null;
|
|
464
572
|
if (a === b) continue;
|
|
573
|
+
const listNote = notes(offList(n, b), moved(n));
|
|
465
574
|
items.push({
|
|
466
575
|
key: `agents.${n}`,
|
|
467
576
|
harness: h,
|
|
@@ -470,7 +579,11 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
470
579
|
before: shown(a),
|
|
471
580
|
after: shown(b),
|
|
472
581
|
team: true,
|
|
473
|
-
note:
|
|
582
|
+
note: notes(
|
|
583
|
+
change.pinnedClerk && n === "clerk" ? "pinned: the clerk stays cheap under a strong default" : null,
|
|
584
|
+
listNote,
|
|
585
|
+
),
|
|
586
|
+
listNote,
|
|
474
587
|
});
|
|
475
588
|
}
|
|
476
589
|
if (change.clerkPinUnavailable)
|
|
@@ -484,7 +597,43 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
484
597
|
team: true,
|
|
485
598
|
note: `${display} declares no cheap model to pin the clerk to; set agents.clerk to name one`,
|
|
486
599
|
});
|
|
487
|
-
|
|
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 });
|
|
488
637
|
}
|
|
489
638
|
|
|
490
639
|
// The status line: install's own plan for the surface, with its flag item
|
|
@@ -585,7 +734,7 @@ export function applyChanges(f, steps, { env = process.env, home = homedir(), ro
|
|
|
585
734
|
writeVaultConfig(f.vault, nextVaultConfig(now.cfg, s.patch, new Date().toISOString()));
|
|
586
735
|
wrote.push(vaultConfigPath(f.vault));
|
|
587
736
|
} else if (s.writer === "overlay") {
|
|
588
|
-
wrote.push(applyAgentsChange(f.project, s.harness, s.next).path);
|
|
737
|
+
wrote.push(applyAgentsChange(f.project, s.harness, s.next, s.nextEfforts).path);
|
|
589
738
|
} else if (s.writer === "statusline") {
|
|
590
739
|
const finished = [];
|
|
591
740
|
try {
|
|
@@ -623,6 +772,8 @@ const SOURCE_TEXT = Object.freeze({
|
|
|
623
772
|
per_agent: "per agent",
|
|
624
773
|
default: "default",
|
|
625
774
|
frontmatter: "its frontmatter",
|
|
775
|
+
model_default: "its model's own default",
|
|
776
|
+
harness: "the harness decides",
|
|
626
777
|
binding: "binding",
|
|
627
778
|
vault_config: "vault config",
|
|
628
779
|
unparseable: "not valid JSON: fix the file to change it",
|
|
@@ -658,9 +809,11 @@ export function renderView(f, settings, { caps: c = term.PLAIN } = {}) {
|
|
|
658
809
|
note:
|
|
659
810
|
s.class === "other-verb"
|
|
660
811
|
? "changed by another command, below"
|
|
661
|
-
: s.
|
|
662
|
-
? `${SOURCE_TEXT
|
|
663
|
-
:
|
|
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,
|
|
664
817
|
})),
|
|
665
818
|
),
|
|
666
819
|
);
|
|
@@ -711,51 +864,6 @@ export function renderPlan(f, items, { caps: c = term.PLAIN } = {}) {
|
|
|
711
864
|
|
|
712
865
|
// ─── The run ───────────────────────────────────────────────────────────
|
|
713
866
|
|
|
714
|
-
const railChoices = (s, f) => {
|
|
715
|
-
if (s.rail === "bool") return ["on", "off"];
|
|
716
|
-
if (s.rail === "language") return f.languages;
|
|
717
|
-
if (s.rail === "policy") return ["optional", "required"];
|
|
718
|
-
if (s.rail === "epic") return [...f.epics, { value: CLEAR, label: "release the pin" }];
|
|
719
|
-
if (s.rail === "model") {
|
|
720
|
-
// The presets are Claude Code's model names: offered only for a harness
|
|
721
|
-
// whose agents carry a model (agent_translation.carries_model). Any other
|
|
722
|
-
// harness's model is named with --set, in its own vocabulary.
|
|
723
|
-
const list = loadHarness(s.harness)?.agent_translation?.carries_model === true ? [...MODEL_PRESETS] : [];
|
|
724
|
-
if (s.value && !list.includes(s.value)) list.unshift(s.value);
|
|
725
|
-
return [...list, { value: CLEAR, label: s.key === "agents.default" ? "clear" : "clear (the default decides)" }];
|
|
726
|
-
}
|
|
727
|
-
return [];
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
// The rail's answers as --set strings, so a terminal run and a named one
|
|
731
|
-
// resolve through the same code.
|
|
732
|
-
async function askRail(rail, f, settings) {
|
|
733
|
-
const answerable = settings.filter((s) => s.class === "changeable" && s.rail && railChoices(s, f).length);
|
|
734
|
-
const twoHarnesses = f.used.length > 1;
|
|
735
|
-
const label = (s) =>
|
|
736
|
-
twoHarnesses && s.harness
|
|
737
|
-
? `${loadHarness(s.harness).display_name || s.harness} · ${s.key.replace(/^agents\./, "")}`
|
|
738
|
-
: s.key;
|
|
739
|
-
const ticked = await rail.multi(
|
|
740
|
-
"What do you want to change?",
|
|
741
|
-
answerable.map((s, k) => ({ value: k, label: label(s), reason: shown(s.value) })),
|
|
742
|
-
{ min: 0, note: "space ticks a row; Enter with none ticked changes nothing" },
|
|
743
|
-
);
|
|
744
|
-
if (ticked === term.CANCELLED) return { cancelled: true };
|
|
745
|
-
const sets = [];
|
|
746
|
-
for (const k of ticked) {
|
|
747
|
-
const s = answerable[k];
|
|
748
|
-
const choices = railChoices(s, f);
|
|
749
|
-
const current = s.rail === "bool" ? s.value : s.value || undefined;
|
|
750
|
-
const v = await rail.select(label(s), choices, {
|
|
751
|
-
default: choices.some((x) => (typeof x === "object" ? x.value : x) === current) ? current : undefined,
|
|
752
|
-
});
|
|
753
|
-
if (v === term.CANCELLED) return { cancelled: true };
|
|
754
|
-
sets.push({ set: `${s.key}=${v}`, harness: s.harness || null });
|
|
755
|
-
}
|
|
756
|
-
return { sets };
|
|
757
|
-
}
|
|
758
|
-
|
|
759
867
|
// ctx: { values, project, env, stdin, stdout, ask, root, home, ownEnv,
|
|
760
868
|
// nearest, proc, escapeMs, drainTicks, now }. Returns { code, ok, result }
|
|
761
869
|
// or { code: 2, usage } or { code: 3, unbound: true }; the bin writes the
|
|
@@ -792,49 +900,87 @@ export async function runConfigure(ctx) {
|
|
|
792
900
|
harness: s.harness || null,
|
|
793
901
|
team: s.team,
|
|
794
902
|
set: s.set,
|
|
903
|
+
...(s.model_default ? { model_default: s.model_default } : {}),
|
|
795
904
|
}));
|
|
796
905
|
const interactive =
|
|
797
906
|
!json && (ask ? true : isInteractive({ stdin, stdout, env, json })) && term.railAvailable(stdin, c);
|
|
907
|
+
// The menu needs a screen of its own size (contract 4 as amended
|
|
908
|
+
// 2026-10-13): smaller, the run is as without a terminal.
|
|
909
|
+
const fits =
|
|
910
|
+
(Number(stdout?.columns) || c.columns || 100) >= SMALLEST.columns && (Number(stdout?.rows) || 24) >= SMALLEST.rows;
|
|
798
911
|
|
|
799
912
|
// Nothing named and no terminal: the view, and nothing written.
|
|
800
|
-
if (!sets.length && !interactive) {
|
|
913
|
+
if (!sets.length && (!interactive || !fits)) {
|
|
801
914
|
write(renderView(f, settings, { caps: c }));
|
|
802
915
|
return { code: 0, ok: true, result: { settings: publicSettings, plan: [], wrote: [], removed: [] } };
|
|
803
916
|
}
|
|
804
917
|
|
|
805
918
|
let resolved;
|
|
919
|
+
// The menu's review screen confirms; a --set run at a terminal asks once.
|
|
920
|
+
let confirmed = false,
|
|
921
|
+
reviewed = null;
|
|
806
922
|
if (sets.length) {
|
|
807
923
|
resolved = resolveSets(f, sets, { harness: named[0] || null, nearest: ctx.nearest });
|
|
808
924
|
if (resolved.usage) return { code: 2, usage: resolved.usage };
|
|
809
925
|
write(`${term.badge(c, "projectstore configure")}\n ${tildeOf(f.project, f.home)}\n`);
|
|
810
926
|
} else {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
927
|
+
// The settings menu (contract 4 as amended 2026-10-13). Its changes come
|
|
928
|
+
// back as --set strings and go through the same resolve, plan and apply;
|
|
929
|
+
// its review screen's second Enter is the confirmation.
|
|
930
|
+
const resolveAll = (list) => {
|
|
931
|
+
const changes = [];
|
|
932
|
+
for (const x of list) {
|
|
933
|
+
const r = resolveSets(f, [x.set], { harness: x.harness, nearest: ctx.nearest });
|
|
934
|
+
if (r.usage) return { usage: r.usage };
|
|
935
|
+
changes.push(...r.changes);
|
|
936
|
+
}
|
|
937
|
+
return { changes };
|
|
938
|
+
};
|
|
939
|
+
const a = await runMenu({
|
|
940
|
+
f,
|
|
941
|
+
settings,
|
|
942
|
+
stdin,
|
|
943
|
+
stdout,
|
|
944
|
+
caps: c,
|
|
945
|
+
proc: ctx.proc || process,
|
|
946
|
+
escapeMs: ctx.escapeMs,
|
|
947
|
+
env,
|
|
948
|
+
home,
|
|
949
|
+
now,
|
|
950
|
+
path: tildeOf(f.project, f.home),
|
|
951
|
+
dryRun,
|
|
952
|
+
fetch: ctx.fetch,
|
|
953
|
+
spawn: ctx.spawn,
|
|
954
|
+
timers: ctx.timers,
|
|
955
|
+
review: (list) => {
|
|
956
|
+
const r = resolveAll(list);
|
|
957
|
+
if (r.usage) return { ok: false, message: r.usage };
|
|
958
|
+
const p = planChanges(f, r.changes, { env, home, root, ownEnv: ctx.ownEnv || (() => env) });
|
|
959
|
+
if (p.usage) return { ok: false, message: p.usage };
|
|
960
|
+
if (p.refusal !== undefined) return { ok: false, message: p.refusal };
|
|
961
|
+
return { ok: true, items: p.items, plan: p };
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
const head = `${term.badge(c, "projectstore configure")}\n ${tildeOf(f.project, f.home)}\n`;
|
|
965
|
+
if (a.cancelled) {
|
|
966
|
+
write(`${head}\n Cancelled ${term.icon(c, "dash")} nothing was written\n`);
|
|
967
|
+
return { code: 130, ok: false, result: null };
|
|
826
968
|
}
|
|
827
|
-
if (a.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const r = resolveSets(f, [x.set], { harness: x.harness, nearest: ctx.nearest });
|
|
831
|
-
if (r.usage) return { code: 2, usage: r.usage };
|
|
832
|
-
changes.push(...r.changes);
|
|
969
|
+
if (a.nothing) {
|
|
970
|
+
write(`${head}\n No change ${term.icon(c, "dash")} nothing was written\n`);
|
|
971
|
+
return { code: 0, ok: true, result: { settings: publicSettings, plan: [], wrote: [], removed: [] } };
|
|
833
972
|
}
|
|
834
|
-
|
|
973
|
+
write(head);
|
|
974
|
+
const r = resolveAll(a.sets);
|
|
975
|
+
if (r.usage) return { code: 2, usage: r.usage };
|
|
976
|
+
resolved = r;
|
|
977
|
+
confirmed = !a.dryRun;
|
|
978
|
+
// What the review showed is what APPLY runs: the plan is not made again
|
|
979
|
+
// after its confirmation.
|
|
980
|
+
reviewed = a.planned || null;
|
|
835
981
|
}
|
|
836
982
|
|
|
837
|
-
const planned = planChanges(f, resolved.changes, { env, home, root, ownEnv: ctx.ownEnv || (() => env) });
|
|
983
|
+
const planned = reviewed || planChanges(f, resolved.changes, { env, home, root, ownEnv: ctx.ownEnv || (() => env) });
|
|
838
984
|
if (planned.usage) return { code: 2, usage: planned.usage };
|
|
839
985
|
const planOut = (items) =>
|
|
840
986
|
items.map((i) => ({
|
|
@@ -844,6 +990,8 @@ export async function runConfigure(ctx) {
|
|
|
844
990
|
before: i.before,
|
|
845
991
|
after: i.after,
|
|
846
992
|
team: i.team,
|
|
993
|
+
// Contract 3 as amended 2026-10-13: the off-list note only.
|
|
994
|
+
...(i.listNote ? { note: i.listNote } : {}),
|
|
847
995
|
}));
|
|
848
996
|
if (planned.refusal !== undefined)
|
|
849
997
|
return {
|
|
@@ -862,7 +1010,7 @@ export async function runConfigure(ctx) {
|
|
|
862
1010
|
write("\n Nothing written: this was a dry run.\n");
|
|
863
1011
|
return { code: 0, ok: true, result: { ...result, dry_run: true } };
|
|
864
1012
|
}
|
|
865
|
-
if (interactive) {
|
|
1013
|
+
if (interactive && !confirmed) {
|
|
866
1014
|
if (stdin && !ask) await term.drainInput(stdin, { ticks: ctx.drainTicks ?? 2 });
|
|
867
1015
|
write("\n");
|
|
868
1016
|
const yes = await term.askApply(planned.items.length, { stdin, stdout, ask, caps: c });
|