projectstore-claude 0.30.1 → 0.30.2
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/README.md +3 -3
- package/node_modules/projectstore/docs/getting-started.md +1 -1
- package/node_modules/projectstore/harnesses/claude-code.json +52 -1
- package/node_modules/projectstore/harnesses/codex.json +32 -1
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/configure-menu.mjs +589 -0
- package/node_modules/projectstore/scripts/configure.mjs +87 -76
- package/node_modules/projectstore/scripts/harness.mjs +13 -3
- package/node_modules/projectstore/scripts/menu.mjs +742 -0
- package/node_modules/projectstore/scripts/models.mjs +505 -0
- package/node_modules/projectstore/scripts/term.mjs +167 -0
- package/package.json +2 -2
|
@@ -35,10 +35,10 @@ import { bandKeys, epicIds, planBand, applyBand } from "./band.mjs";
|
|
|
35
35
|
import { planAgentsChange, applyAgentsChange } from "./agents-change.mjs";
|
|
36
36
|
import { plan as planInstall, apply as applyInstall, planOptions, isInteractive } from "./install-harness.mjs";
|
|
37
37
|
import { analyseLayout } from "./surfaces.mjs";
|
|
38
|
+
import { knownModels, offListNote } from "./models.mjs";
|
|
39
|
+
import { runMenu } from "./configure-menu.mjs";
|
|
40
|
+
import { SMALLEST } from "./menu.mjs";
|
|
38
41
|
|
|
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
42
|
const CLEAR = "-";
|
|
43
43
|
|
|
44
44
|
const LANGUAGE_NOTE =
|
|
@@ -446,8 +446,17 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
446
446
|
if (change.usage) return { usage: change.usage };
|
|
447
447
|
if (change.same) continue;
|
|
448
448
|
const file = where(f, before.path);
|
|
449
|
-
const
|
|
450
|
-
|
|
449
|
+
const manifest = loadHarness(h);
|
|
450
|
+
const display = manifest.display_name || h;
|
|
451
|
+
// A model no list of the harness holds is written, with a note (contract
|
|
452
|
+
// 8 as amended 2026-10-13): the model may be newer than the list.
|
|
453
|
+
// Only a model the run names gets the note: the clerk's pin does not.
|
|
454
|
+
const known = knownModels(manifest, { env, home });
|
|
455
|
+
const named = new Map(list.map((c) => [c.name, c.value]));
|
|
456
|
+
const offList = (name, model) => (model && named.get(name) === model ? offListNote(manifest, model, known) : null);
|
|
457
|
+
const notes = (...xs) => xs.filter(Boolean).join("; ") || null;
|
|
458
|
+
if (before.agents.default !== change.next.default) {
|
|
459
|
+
const listNote = offList("default", change.next.default);
|
|
451
460
|
items.push({
|
|
452
461
|
key: "agents.default",
|
|
453
462
|
harness: h,
|
|
@@ -456,12 +465,16 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
456
465
|
before: shown(before.agents.default),
|
|
457
466
|
after: shown(change.next.default),
|
|
458
467
|
team: true,
|
|
468
|
+
note: listNote,
|
|
469
|
+
listNote,
|
|
459
470
|
});
|
|
471
|
+
}
|
|
460
472
|
const names = new Set([...Object.keys(before.agents.per_agent), ...Object.keys(change.next.per_agent)]);
|
|
461
473
|
for (const n of [...names].sort()) {
|
|
462
474
|
const a = before.agents.per_agent[n] || null;
|
|
463
475
|
const b = change.next.per_agent[n] || null;
|
|
464
476
|
if (a === b) continue;
|
|
477
|
+
const listNote = offList(n, b);
|
|
465
478
|
items.push({
|
|
466
479
|
key: `agents.${n}`,
|
|
467
480
|
harness: h,
|
|
@@ -470,7 +483,11 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
470
483
|
before: shown(a),
|
|
471
484
|
after: shown(b),
|
|
472
485
|
team: true,
|
|
473
|
-
note:
|
|
486
|
+
note: notes(
|
|
487
|
+
change.pinnedClerk && n === "clerk" ? "pinned: the clerk stays cheap under a strong default" : null,
|
|
488
|
+
listNote,
|
|
489
|
+
),
|
|
490
|
+
listNote,
|
|
474
491
|
});
|
|
475
492
|
}
|
|
476
493
|
if (change.clerkPinUnavailable)
|
|
@@ -711,51 +728,6 @@ export function renderPlan(f, items, { caps: c = term.PLAIN } = {}) {
|
|
|
711
728
|
|
|
712
729
|
// ─── The run ───────────────────────────────────────────────────────────
|
|
713
730
|
|
|
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
731
|
// ctx: { values, project, env, stdin, stdout, ask, root, home, ownEnv,
|
|
760
732
|
// nearest, proc, escapeMs, drainTicks, now }. Returns { code, ok, result }
|
|
761
733
|
// or { code: 2, usage } or { code: 3, unbound: true }; the bin writes the
|
|
@@ -795,46 +767,83 @@ export async function runConfigure(ctx) {
|
|
|
795
767
|
}));
|
|
796
768
|
const interactive =
|
|
797
769
|
!json && (ask ? true : isInteractive({ stdin, stdout, env, json })) && term.railAvailable(stdin, c);
|
|
770
|
+
// The menu needs a screen of its own size (contract 4 as amended
|
|
771
|
+
// 2026-10-13): smaller, the run is as without a terminal.
|
|
772
|
+
const fits =
|
|
773
|
+
(Number(stdout?.columns) || c.columns || 100) >= SMALLEST.columns && (Number(stdout?.rows) || 24) >= SMALLEST.rows;
|
|
798
774
|
|
|
799
775
|
// Nothing named and no terminal: the view, and nothing written.
|
|
800
|
-
if (!sets.length && !interactive) {
|
|
776
|
+
if (!sets.length && (!interactive || !fits)) {
|
|
801
777
|
write(renderView(f, settings, { caps: c }));
|
|
802
778
|
return { code: 0, ok: true, result: { settings: publicSettings, plan: [], wrote: [], removed: [] } };
|
|
803
779
|
}
|
|
804
780
|
|
|
805
781
|
let resolved;
|
|
782
|
+
// The menu's review screen confirms; a --set run at a terminal asks once.
|
|
783
|
+
let confirmed = false,
|
|
784
|
+
reviewed = null;
|
|
806
785
|
if (sets.length) {
|
|
807
786
|
resolved = resolveSets(f, sets, { harness: named[0] || null, nearest: ctx.nearest });
|
|
808
787
|
if (resolved.usage) return { code: 2, usage: resolved.usage };
|
|
809
788
|
write(`${term.badge(c, "projectstore configure")}\n ${tildeOf(f.project, f.home)}\n`);
|
|
810
789
|
} else {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
790
|
+
// The settings menu (contract 4 as amended 2026-10-13). Its changes come
|
|
791
|
+
// back as --set strings and go through the same resolve, plan and apply;
|
|
792
|
+
// its review screen's second Enter is the confirmation.
|
|
793
|
+
const resolveAll = (list) => {
|
|
794
|
+
const changes = [];
|
|
795
|
+
for (const x of list) {
|
|
796
|
+
const r = resolveSets(f, [x.set], { harness: x.harness, nearest: ctx.nearest });
|
|
797
|
+
if (r.usage) return { usage: r.usage };
|
|
798
|
+
changes.push(...r.changes);
|
|
799
|
+
}
|
|
800
|
+
return { changes };
|
|
801
|
+
};
|
|
802
|
+
const a = await runMenu({
|
|
803
|
+
f,
|
|
804
|
+
settings,
|
|
805
|
+
stdin,
|
|
806
|
+
stdout,
|
|
807
|
+
caps: c,
|
|
808
|
+
proc: ctx.proc || process,
|
|
809
|
+
escapeMs: ctx.escapeMs,
|
|
810
|
+
env,
|
|
811
|
+
home,
|
|
812
|
+
now,
|
|
813
|
+
path: tildeOf(f.project, f.home),
|
|
814
|
+
dryRun,
|
|
815
|
+
fetch: ctx.fetch,
|
|
816
|
+
spawn: ctx.spawn,
|
|
817
|
+
timers: ctx.timers,
|
|
818
|
+
review: (list) => {
|
|
819
|
+
const r = resolveAll(list);
|
|
820
|
+
if (r.usage) return { ok: false, message: r.usage };
|
|
821
|
+
const p = planChanges(f, r.changes, { env, home, root, ownEnv: ctx.ownEnv || (() => env) });
|
|
822
|
+
if (p.usage) return { ok: false, message: p.usage };
|
|
823
|
+
if (p.refusal !== undefined) return { ok: false, message: p.refusal };
|
|
824
|
+
return { ok: true, items: p.items, plan: p };
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
const head = `${term.badge(c, "projectstore configure")}\n ${tildeOf(f.project, f.home)}\n`;
|
|
828
|
+
if (a.cancelled) {
|
|
829
|
+
write(`${head}\n Cancelled ${term.icon(c, "dash")} nothing was written\n`);
|
|
830
|
+
return { code: 130, ok: false, result: null };
|
|
826
831
|
}
|
|
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);
|
|
832
|
+
if (a.nothing) {
|
|
833
|
+
write(`${head}\n No change ${term.icon(c, "dash")} nothing was written\n`);
|
|
834
|
+
return { code: 0, ok: true, result: { settings: publicSettings, plan: [], wrote: [], removed: [] } };
|
|
833
835
|
}
|
|
834
|
-
|
|
836
|
+
write(head);
|
|
837
|
+
const r = resolveAll(a.sets);
|
|
838
|
+
if (r.usage) return { code: 2, usage: r.usage };
|
|
839
|
+
resolved = r;
|
|
840
|
+
confirmed = !a.dryRun;
|
|
841
|
+
// What the review showed is what APPLY runs: the plan is not made again
|
|
842
|
+
// after its confirmation.
|
|
843
|
+
reviewed = a.planned || null;
|
|
835
844
|
}
|
|
836
845
|
|
|
837
|
-
const planned = planChanges(f, resolved.changes, { env, home, root, ownEnv: ctx.ownEnv || (() => env) });
|
|
846
|
+
const planned = reviewed || planChanges(f, resolved.changes, { env, home, root, ownEnv: ctx.ownEnv || (() => env) });
|
|
838
847
|
if (planned.usage) return { code: 2, usage: planned.usage };
|
|
839
848
|
const planOut = (items) =>
|
|
840
849
|
items.map((i) => ({
|
|
@@ -844,6 +853,8 @@ export async function runConfigure(ctx) {
|
|
|
844
853
|
before: i.before,
|
|
845
854
|
after: i.after,
|
|
846
855
|
team: i.team,
|
|
856
|
+
// Contract 3 as amended 2026-10-13: the off-list note only.
|
|
857
|
+
...(i.listNote ? { note: i.listNote } : {}),
|
|
847
858
|
}));
|
|
848
859
|
if (planned.refusal !== undefined)
|
|
849
860
|
return {
|
|
@@ -862,7 +873,7 @@ export async function runConfigure(ctx) {
|
|
|
862
873
|
write("\n Nothing written: this was a dry run.\n");
|
|
863
874
|
return { code: 0, ok: true, result: { ...result, dry_run: true } };
|
|
864
875
|
}
|
|
865
|
-
if (interactive) {
|
|
876
|
+
if (interactive && !confirmed) {
|
|
866
877
|
if (stdin && !ask) await term.drainInput(stdin, { ticks: ctx.drainTicks ?? 2 });
|
|
867
878
|
write("\n");
|
|
868
879
|
const yes = await term.askApply(planned.items.length, { stdin, stdout, ask, caps: c });
|
|
@@ -302,9 +302,7 @@ export function hostPresence(manifest, { env = process.env, home = homedir() } =
|
|
|
302
302
|
x.cli.bin,
|
|
303
303
|
)?.[1] || null;
|
|
304
304
|
const bin = s ? s.cli.bin : null;
|
|
305
|
-
const
|
|
306
|
-
const fromEnv = r.home_env && env ? env[r.home_env] : null;
|
|
307
|
-
const homeDir = fromEnv || (home && r.home_default ? join(home, r.home_default) : null);
|
|
305
|
+
const homeDir = harnessHome(manifest, { env, home });
|
|
308
306
|
let present = false;
|
|
309
307
|
if (homeDir) {
|
|
310
308
|
try {
|
|
@@ -314,6 +312,15 @@ export function hostPresence(manifest, { env = process.env, home = homedir() } =
|
|
|
314
312
|
return { cli: bin ? whichOnPath(bin, env || {}) : null, home: present ? homeDir : null, bin, homeDir };
|
|
315
313
|
}
|
|
316
314
|
|
|
315
|
+
// A harness's own home directory, as the harness resolves it: its home
|
|
316
|
+
// variable (runtime.home_env) when set, else home_default under the home
|
|
317
|
+
// directory, else null. Whether it exists is the caller's question.
|
|
318
|
+
export function harnessHome(manifest, { env = process.env, home = homedir() } = {}) {
|
|
319
|
+
const r = manifest?.runtime || {};
|
|
320
|
+
const fromEnv = r.home_env && env ? env[r.home_env] : null;
|
|
321
|
+
return fromEnv || (home && r.home_default ? join(home, r.home_default) : null);
|
|
322
|
+
}
|
|
323
|
+
|
|
317
324
|
// The agents block of a RUN (the story "One run plans the agents block once,
|
|
318
325
|
// and a bare uninstall selects every harness the project uses", rules 1–2):
|
|
319
326
|
// the files every manifest of the run names for it, in the FIRST manifest's
|
|
@@ -703,6 +710,9 @@ export function cachePaths({ env = process.env, home = homedir() } = {}) {
|
|
|
703
710
|
fetch: root ? join(root, "fetch") : null,
|
|
704
711
|
fetchRun: (id) => (root ? join(root, "fetch", id) : null),
|
|
705
712
|
registry: root ? join(root, "registry.json") : null,
|
|
713
|
+
// A harness's model list, as its last live read answered (the ADR "Agent
|
|
714
|
+
// model lists are read from each harness's own catalog…", decision 4).
|
|
715
|
+
models: (id) => (root && id ? join(root, "models", `${id}.json`) : null),
|
|
706
716
|
from: fromEnv ? variable : "the home directory",
|
|
707
717
|
variable,
|
|
708
718
|
};
|