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
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
readOverlayAt,
|
|
24
24
|
resolveAgentModel,
|
|
25
|
+
resolveAgentEffort,
|
|
25
26
|
layoutRoster,
|
|
26
27
|
readVaultConfigStrict,
|
|
27
28
|
nextVaultConfig,
|
|
@@ -35,7 +36,15 @@ import { bandKeys, epicIds, planBand, applyBand } from "./band.mjs";
|
|
|
35
36
|
import { planAgentsChange, applyAgentsChange } from "./agents-change.mjs";
|
|
36
37
|
import { plan as planInstall, apply as applyInstall, planOptions, isInteractive } from "./install-harness.mjs";
|
|
37
38
|
import { analyseLayout } from "./surfaces.mjs";
|
|
38
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
knownModels,
|
|
41
|
+
offListNote,
|
|
42
|
+
effortSpec,
|
|
43
|
+
effortLevels,
|
|
44
|
+
offLevelNote,
|
|
45
|
+
standingLevelNote,
|
|
46
|
+
LEVEL,
|
|
47
|
+
} from "./models.mjs";
|
|
39
48
|
import { runMenu } from "./configure-menu.mjs";
|
|
40
49
|
import { SMALLEST } from "./menu.mjs";
|
|
41
50
|
|
|
@@ -76,6 +85,7 @@ export function readFacts(project, { env = process.env, root, home = homedir() }
|
|
|
76
85
|
return {
|
|
77
86
|
project,
|
|
78
87
|
home,
|
|
88
|
+
env,
|
|
79
89
|
path,
|
|
80
90
|
cfg,
|
|
81
91
|
used,
|
|
@@ -108,6 +118,29 @@ export function settingsOf(f) {
|
|
|
108
118
|
const o = readOverlayAt(f.project, h.id);
|
|
109
119
|
const file = where(f, o.path);
|
|
110
120
|
const base = { group: `agents:${h.id}`, harness: h.id, file, class: "changeable", team: true, writer: "overlay" };
|
|
121
|
+
// Each model setting is followed by its effort setting, where the harness
|
|
122
|
+
// passes one at spawn (the effort ADR, decisions 1 and 4). An effort that
|
|
123
|
+
// nothing configures reads as the harness says: the frontmatter's, or the
|
|
124
|
+
// model's own default level, named when its catalog entry is known.
|
|
125
|
+
const es = effortSpec(h);
|
|
126
|
+
const known = es?.unset?.model_default ? knownModels(h, { env: f.env, home: f.home }) : null;
|
|
127
|
+
// frontmatter: the agent's own; model_default: the passed model's own
|
|
128
|
+
// default, named; harness: the harness's own precedence (no model passed,
|
|
129
|
+
// or none whose default is known).
|
|
130
|
+
const unsetEffort = (model) => {
|
|
131
|
+
if (!es?.unset?.model_default) return { source: "frontmatter" };
|
|
132
|
+
const d = model ? effortLevels(h, model, known)?.default : null;
|
|
133
|
+
return d ? { source: "model_default", model_default: d } : { source: "harness" };
|
|
134
|
+
};
|
|
135
|
+
const effortRow = (key, effort, effortSource, model) =>
|
|
136
|
+
es &&
|
|
137
|
+
out.push({
|
|
138
|
+
...base,
|
|
139
|
+
key,
|
|
140
|
+
value: effort || null,
|
|
141
|
+
...(o.unparseable ? { source: "unparseable" } : effort ? { source: effortSource } : unsetEffort(model)),
|
|
142
|
+
rail: "effort",
|
|
143
|
+
});
|
|
111
144
|
out.push({
|
|
112
145
|
...base,
|
|
113
146
|
key: "agents.default",
|
|
@@ -115,7 +148,10 @@ export function settingsOf(f) {
|
|
|
115
148
|
source: o.unparseable ? "unparseable" : o.agents.default ? "overlay" : "frontmatter",
|
|
116
149
|
rail: "model",
|
|
117
150
|
});
|
|
118
|
-
|
|
151
|
+
effortRow("agents.default.effort", o.efforts.default, "overlay", o.agents.default);
|
|
152
|
+
const names = [
|
|
153
|
+
...new Set([...(f.roster || []), ...Object.keys(o.agents.per_agent), ...Object.keys(o.efforts.per_agent)]),
|
|
154
|
+
];
|
|
119
155
|
for (const n of names) {
|
|
120
156
|
const r = resolveAgentModel(f.project, n, { harness: h.id });
|
|
121
157
|
out.push({
|
|
@@ -125,6 +161,8 @@ export function settingsOf(f) {
|
|
|
125
161
|
source: o.unparseable ? "unparseable" : r.source || "frontmatter",
|
|
126
162
|
rail: "model",
|
|
127
163
|
});
|
|
164
|
+
const e = resolveAgentEffort(f.project, n, { harness: h.id });
|
|
165
|
+
effortRow(`agents.${n}.effort`, e.effort, e.source, r.model);
|
|
128
166
|
}
|
|
129
167
|
}
|
|
130
168
|
if (f.slot)
|
|
@@ -239,11 +277,13 @@ function setForm(s, f) {
|
|
|
239
277
|
? "on|off"
|
|
240
278
|
: s.key === "tags"
|
|
241
279
|
? "<a,b>"
|
|
242
|
-
: s.
|
|
243
|
-
? "<
|
|
244
|
-
: s.key
|
|
245
|
-
? "<
|
|
246
|
-
:
|
|
280
|
+
: s.rail === "effort"
|
|
281
|
+
? "<level>"
|
|
282
|
+
: s.key.startsWith("agents.")
|
|
283
|
+
? "<model>"
|
|
284
|
+
: s.key === "band.epic"
|
|
285
|
+
? "<epic>"
|
|
286
|
+
: `<${s.key}>`;
|
|
247
287
|
const h = s.harness && f.used.length > 1 ? ` --harness ${s.harness}` : "";
|
|
248
288
|
return `--set ${s.key}=${value}${h}`;
|
|
249
289
|
}
|
|
@@ -266,8 +306,10 @@ export function resolveSets(f, sets, { harness = null, nearest = null } = {}) {
|
|
|
266
306
|
const value = String(raw)
|
|
267
307
|
.slice(at + 1)
|
|
268
308
|
.trim();
|
|
269
|
-
|
|
270
|
-
|
|
309
|
+
// agents.<name> and agents.<name>.model are one key; agents.<name>.effort
|
|
310
|
+
// is a key of its own (the effort ADR, decision 4).
|
|
311
|
+
const m = /^agents\.([a-z][a-z0-9-]*)(?:\.(model|effort))?$/.exec(key);
|
|
312
|
+
if (m) key = m[2] === "effort" ? `agents.${m[1]}.effort` : `agents.${m[1]}`;
|
|
271
313
|
if (seen.has(key)) return { usage: `${key} is named twice` };
|
|
272
314
|
seen.add(key);
|
|
273
315
|
if (m) {
|
|
@@ -280,6 +322,19 @@ export function resolveSets(f, sets, { harness = null, nearest = null } = {}) {
|
|
|
280
322
|
else return { usage: `name the overlay: --harness <id> (this project uses ${harnessesNamed})` };
|
|
281
323
|
}
|
|
282
324
|
if (!harnessIds().includes(h)) return { usage: `unknown harness: ${h} — known: ${harnessIds().join(", ")}` };
|
|
325
|
+
if (m[2] === "effort") {
|
|
326
|
+
const manifest = loadHarness(h);
|
|
327
|
+
const es = effortSpec(manifest);
|
|
328
|
+
if (!es) return { usage: `${manifest?.display_name || h} takes no effort at spawn: ${key} has nothing to set` };
|
|
329
|
+
// A lowercase token, or - to clear; a level the harness does not list
|
|
330
|
+
// is written with a note, not refused (decision 5).
|
|
331
|
+
if (value !== CLEAR && !LEVEL.test(value))
|
|
332
|
+
return {
|
|
333
|
+
usage: `${key} takes a level (${es.levels.map((l) => l.id).join(", ")}), or ${CLEAR} to clear it, got ${value || "nothing"}`,
|
|
334
|
+
};
|
|
335
|
+
out.push({ key, writer: "overlay", harness: h, name, field: "effort", value: value === CLEAR ? null : value });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
283
338
|
if (!value) return { usage: `${key} takes a model, or ${CLEAR} to clear it` };
|
|
284
339
|
out.push({ key, writer: "overlay", harness: h, name, value: value === CLEAR ? null : value });
|
|
285
340
|
continue;
|
|
@@ -434,14 +489,19 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
434
489
|
for (const [h, list] of byHarness) {
|
|
435
490
|
const before = readOverlayAt(f.project, h);
|
|
436
491
|
if (before.unparseable) return refuse(`${before.path} is not valid JSON — fix or remove it first`);
|
|
437
|
-
const
|
|
492
|
+
const models = list.filter((c) => c.field !== "effort");
|
|
493
|
+
const efforts = list.filter((c) => c.field === "effort");
|
|
494
|
+
const def = models.find((c) => c.name === "default");
|
|
495
|
+
const defEffort = efforts.find((c) => c.name === "default");
|
|
438
496
|
const change = planAgentsChange({
|
|
439
497
|
before,
|
|
440
498
|
harness: h,
|
|
441
499
|
layout: f.cfg.layout,
|
|
442
500
|
roster: f.roster,
|
|
443
501
|
defaultModel: def ? def.value || "" : undefined,
|
|
444
|
-
agents:
|
|
502
|
+
agents: models.filter((c) => c.name !== "default").map((c) => `${c.name}=${c.value || ""}`),
|
|
503
|
+
defaultEffort: defEffort ? defEffort.value || "" : undefined,
|
|
504
|
+
efforts: efforts.filter((c) => c.name !== "default").map((c) => `${c.name}=${c.value || ""}`),
|
|
445
505
|
});
|
|
446
506
|
if (change.usage) return { usage: change.usage };
|
|
447
507
|
if (change.same) continue;
|
|
@@ -452,11 +512,47 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
452
512
|
// 8 as amended 2026-10-13): the model may be newer than the list.
|
|
453
513
|
// Only a model the run names gets the note: the clerk's pin does not.
|
|
454
514
|
const known = knownModels(manifest, { env, home });
|
|
455
|
-
const named = new Map(
|
|
515
|
+
const named = new Map(models.map((c) => [c.name, c.value]));
|
|
456
516
|
const offList = (name, model) => (model && named.get(name) === model ? offListNote(manifest, model, known) : null);
|
|
457
517
|
const notes = (...xs) => xs.filter(Boolean).join("; ") || null;
|
|
518
|
+
// Effort against the model each agent runs on after the run (the effort
|
|
519
|
+
// ADR, decision 5): a level the run names is checked on its own row; a
|
|
520
|
+
// model the run changes is checked against the level its agents keep, on
|
|
521
|
+
// the model's row; a default level the run names is checked against every
|
|
522
|
+
// agent that inherits it on a model of its own, on the default's row.
|
|
523
|
+
const ne = change.nextEfforts;
|
|
524
|
+
const namedEffort = new Map(efforts.map((c) => [c.name, c.value]));
|
|
525
|
+
const roster = [
|
|
526
|
+
...new Set([...(f.roster || []), ...Object.keys(change.next.per_agent), ...Object.keys(ne.per_agent)]),
|
|
527
|
+
];
|
|
528
|
+
const runModel = (n) => (n === "default" ? change.next.default : change.next.per_agent[n] || change.next.default);
|
|
529
|
+
const runEffort = (n) => (n === "default" ? ne.default : ne.per_agent[n] || ne.default) || null;
|
|
530
|
+
const levelNote = (n, level) =>
|
|
531
|
+
level && namedEffort.get(n) === level ? offLevelNote(manifest, level, runModel(n) || null, known) : null;
|
|
532
|
+
const inheritors = (level) =>
|
|
533
|
+
level && namedEffort.get("default") === level
|
|
534
|
+
? roster
|
|
535
|
+
.filter(
|
|
536
|
+
(n) => !ne.per_agent[n] && change.next.per_agent[n] && change.next.per_agent[n] !== change.next.default,
|
|
537
|
+
)
|
|
538
|
+
.map((n) => standingLevelNote(manifest, n, level, runModel(n), known))
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.join("; ") || null
|
|
541
|
+
: null;
|
|
542
|
+
// Agents a model change moves, and the level each keeps, when that level
|
|
543
|
+
// was not named by this run (a named one is checked on its own row).
|
|
544
|
+
const moved = (n) => {
|
|
545
|
+
const who = n === "default" ? ["default", ...roster.filter((r) => !change.next.per_agent[r])] : [n];
|
|
546
|
+
return (
|
|
547
|
+
who
|
|
548
|
+
.filter((a) => !namedEffort.has(a) && !(a !== "default" && !ne.per_agent[a] && namedEffort.has("default")))
|
|
549
|
+
.map((a) => standingLevelNote(manifest, a, runEffort(a), runModel(a) || null, known))
|
|
550
|
+
.filter(Boolean)
|
|
551
|
+
.join("; ") || null
|
|
552
|
+
);
|
|
553
|
+
};
|
|
458
554
|
if (before.agents.default !== change.next.default) {
|
|
459
|
-
const listNote = offList("default", change.next.default);
|
|
555
|
+
const listNote = notes(offList("default", change.next.default), moved("default"));
|
|
460
556
|
items.push({
|
|
461
557
|
key: "agents.default",
|
|
462
558
|
harness: h,
|
|
@@ -474,7 +570,7 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
474
570
|
const a = before.agents.per_agent[n] || null;
|
|
475
571
|
const b = change.next.per_agent[n] || null;
|
|
476
572
|
if (a === b) continue;
|
|
477
|
-
const listNote = offList(n, b);
|
|
573
|
+
const listNote = notes(offList(n, b), moved(n));
|
|
478
574
|
items.push({
|
|
479
575
|
key: `agents.${n}`,
|
|
480
576
|
harness: h,
|
|
@@ -501,7 +597,43 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
501
597
|
team: true,
|
|
502
598
|
note: `${display} declares no cheap model to pin the clerk to; set agents.clerk to name one`,
|
|
503
599
|
});
|
|
504
|
-
|
|
600
|
+
const beforeEfforts = before.efforts;
|
|
601
|
+
if (beforeEfforts.default !== ne.default) {
|
|
602
|
+
const listNote = notes(levelNote("default", ne.default), inheritors(ne.default));
|
|
603
|
+
items.push({
|
|
604
|
+
key: "agents.default.effort",
|
|
605
|
+
harness: h,
|
|
606
|
+
display,
|
|
607
|
+
file,
|
|
608
|
+
before: shown(beforeEfforts.default),
|
|
609
|
+
after: shown(ne.default),
|
|
610
|
+
team: true,
|
|
611
|
+
note: listNote,
|
|
612
|
+
listNote,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
const effortNames = new Set([...Object.keys(beforeEfforts.per_agent), ...Object.keys(ne.per_agent)]);
|
|
616
|
+
for (const n of [...effortNames].sort()) {
|
|
617
|
+
const a = beforeEfforts.per_agent[n] || null;
|
|
618
|
+
const b = ne.per_agent[n] || null;
|
|
619
|
+
if (a === b) continue;
|
|
620
|
+
const listNote = levelNote(n, b);
|
|
621
|
+
items.push({
|
|
622
|
+
key: `agents.${n}.effort`,
|
|
623
|
+
harness: h,
|
|
624
|
+
display,
|
|
625
|
+
file,
|
|
626
|
+
before: shown(a),
|
|
627
|
+
after: shown(b),
|
|
628
|
+
team: true,
|
|
629
|
+
note: notes(
|
|
630
|
+
change.pinnedClerkEffort && n === "clerk" ? "pinned: the clerk stays cheap under a strong default" : null,
|
|
631
|
+
listNote,
|
|
632
|
+
),
|
|
633
|
+
listNote,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
steps.push({ writer: "overlay", harness: h, next: change.next, nextEfforts: ne });
|
|
505
637
|
}
|
|
506
638
|
|
|
507
639
|
// The status line: install's own plan for the surface, with its flag item
|
|
@@ -602,7 +734,7 @@ export function applyChanges(f, steps, { env = process.env, home = homedir(), ro
|
|
|
602
734
|
writeVaultConfig(f.vault, nextVaultConfig(now.cfg, s.patch, new Date().toISOString()));
|
|
603
735
|
wrote.push(vaultConfigPath(f.vault));
|
|
604
736
|
} else if (s.writer === "overlay") {
|
|
605
|
-
wrote.push(applyAgentsChange(f.project, s.harness, s.next).path);
|
|
737
|
+
wrote.push(applyAgentsChange(f.project, s.harness, s.next, s.nextEfforts).path);
|
|
606
738
|
} else if (s.writer === "statusline") {
|
|
607
739
|
const finished = [];
|
|
608
740
|
try {
|
|
@@ -640,6 +772,8 @@ const SOURCE_TEXT = Object.freeze({
|
|
|
640
772
|
per_agent: "per agent",
|
|
641
773
|
default: "default",
|
|
642
774
|
frontmatter: "its frontmatter",
|
|
775
|
+
model_default: "its model's own default",
|
|
776
|
+
harness: "the harness decides",
|
|
643
777
|
binding: "binding",
|
|
644
778
|
vault_config: "vault config",
|
|
645
779
|
unparseable: "not valid JSON: fix the file to change it",
|
|
@@ -675,9 +809,11 @@ export function renderView(f, settings, { caps: c = term.PLAIN } = {}) {
|
|
|
675
809
|
note:
|
|
676
810
|
s.class === "other-verb"
|
|
677
811
|
? "changed by another command, below"
|
|
678
|
-
: s.
|
|
679
|
-
? `${SOURCE_TEXT
|
|
680
|
-
:
|
|
812
|
+
: s.source === "model_default" && s.model_default
|
|
813
|
+
? `${SOURCE_TEXT.model_default}, ${s.model_default}`
|
|
814
|
+
: s.class === "changeable" && !s.rail
|
|
815
|
+
? `${SOURCE_TEXT[s.source] || s.source}; change it with ${s.set}`
|
|
816
|
+
: SOURCE_TEXT[s.source] || s.source,
|
|
681
817
|
})),
|
|
682
818
|
),
|
|
683
819
|
);
|
|
@@ -764,6 +900,7 @@ export async function runConfigure(ctx) {
|
|
|
764
900
|
harness: s.harness || null,
|
|
765
901
|
team: s.team,
|
|
766
902
|
set: s.set,
|
|
903
|
+
...(s.model_default ? { model_default: s.model_default } : {}),
|
|
767
904
|
}));
|
|
768
905
|
const interactive =
|
|
769
906
|
!json && (ask ? true : isInteractive({ stdin, stdout, env, json })) && term.railAvailable(stdin, c);
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
writeFileAtomic,
|
|
57
57
|
envFlag,
|
|
58
58
|
isEphemeralRoot,
|
|
59
|
+
COPY_UPDATE_STEPS,
|
|
59
60
|
} from "./lib.mjs";
|
|
60
61
|
import { loadHarness, loadHarnesses } from "./harness.mjs";
|
|
61
62
|
import { analysePortableRegistration, portableRegistrationPaths } from "./portable-registration.mjs";
|
|
@@ -348,6 +349,34 @@ function runArgs({ harnesses = [], surfaces = null, register = true } = {}) {
|
|
|
348
349
|
].join(" ");
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
// The run a deferral sends to a terminal (the install spec, contract 6, as
|
|
353
|
+
// amended 2026-10-14): the run's own verb for one harness, its --surface
|
|
354
|
+
// selection, --global for a global removal, and setup's --channel npm when
|
|
355
|
+
// that was the answer for the harness whose channel it is (setup's rule 13 —
|
|
356
|
+
// a keep is never repeated). `plan` writes nothing, so it names install. One
|
|
357
|
+
// builder for a deferred row's reason and setup's DONE, so the two agree.
|
|
358
|
+
export function terminalRun({
|
|
359
|
+
verb = "install",
|
|
360
|
+
harness = null,
|
|
361
|
+
version = null,
|
|
362
|
+
surfaces = null,
|
|
363
|
+
globalRemoval = false,
|
|
364
|
+
channel = null,
|
|
365
|
+
} = {}) {
|
|
366
|
+
const run = verb === "plan" ? "install" : verb;
|
|
367
|
+
const hostRegistration = Object.values(harness?.surfaces || {}).some(
|
|
368
|
+
(s) => s && typeof s === "object" && s.format === "host-plugin-registration",
|
|
369
|
+
);
|
|
370
|
+
const args = [
|
|
371
|
+
runArgs({ harnesses: harness ? [harness.id] : [], surfaces }),
|
|
372
|
+
globalRemoval ? "--global" : "",
|
|
373
|
+
run === "setup" && channel === "npm" && hostRegistration ? "--channel npm" : "",
|
|
374
|
+
]
|
|
375
|
+
.filter(Boolean)
|
|
376
|
+
.join(" ");
|
|
377
|
+
return coreCommand(run, { version, args });
|
|
378
|
+
}
|
|
379
|
+
|
|
351
380
|
// Stop when the registry's latest is at least a newer copy this project runs
|
|
352
381
|
// — the newest such copy is named. A copy the latest does not reach, or no
|
|
353
382
|
// answer, proceeds: the rows say which case it was (proceedCase). `read` is
|
|
@@ -577,9 +606,15 @@ export async function readKeepPlan(copy, { harness, projectDir, env, run, budget
|
|
|
577
606
|
...(Array.isArray(result.refusals) ? result.refusals : []),
|
|
578
607
|
...items.filter((i) => i.action === "refuse").map((i) => i.reason),
|
|
579
608
|
].filter((x) => typeof x === "string" && x);
|
|
609
|
+
// A deferred row's reason may carry its run on the lines after its prose
|
|
610
|
+
// (the install spec, contract 6, as amended 2026-10-14): only the prose is
|
|
611
|
+
// quoted inside our own sentence, never another copy's command.
|
|
580
612
|
const deferred = items
|
|
581
613
|
.filter((i) => i.deferred === true)
|
|
582
|
-
.map((i) => ({
|
|
614
|
+
.map((i) => ({
|
|
615
|
+
surface: String(i.surface ?? ""),
|
|
616
|
+
reason: typeof i.reason === "string" ? i.reason.split("\n")[0] : null,
|
|
617
|
+
}));
|
|
583
618
|
return {
|
|
584
619
|
argv,
|
|
585
620
|
envelope: true,
|
|
@@ -603,6 +638,26 @@ export function ownUpgrade(copy, harness) {
|
|
|
603
638
|
};
|
|
604
639
|
}
|
|
605
640
|
|
|
641
|
+
// Whether a kept copy is behind the running package (the install spec,
|
|
642
|
+
// contract 21, rule 9, as amended 2026-10-14): semver precedence, rc
|
|
643
|
+
// included. Never for a copy or a run with no semver version recorded — a
|
|
644
|
+
// recorded "unknown" or a sha must not rank.
|
|
645
|
+
export function copyBehind(copy, version) {
|
|
646
|
+
return isSemver(copy?.version) && isSemver(version) && cmpPrecedence(copy.version, version) < 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// What the keep says when its copy is behind: the project's surfaces are at
|
|
650
|
+
// the running version until the copy updates, and the update in the copy's
|
|
651
|
+
// channel. With a step, the copy's own upgrade runs after this run's agents
|
|
652
|
+
// block, and a copy shipped before this rule plans the block back.
|
|
653
|
+
export function behindAdvice(copy, version, { step = false } = {}) {
|
|
654
|
+
const head = `${copy.key} ${copy.version} is older than this package (${version})`;
|
|
655
|
+
const middle = step
|
|
656
|
+
? `its own upgrade runs after this run's agents block and may plan the block back to its own version; until it updates, its doctor may call the block stale, and the next run may plan the block again`
|
|
657
|
+
: `until it updates, the agents block and anything else this run writes for the project are at ${version}, and its own doctor may call the block stale`;
|
|
658
|
+
return `${head}: ${middle}. Update it (${COPY_UPDATE_STEPS}).`;
|
|
659
|
+
}
|
|
660
|
+
|
|
606
661
|
// This checkout's npm registration enabled beside the kept copy: the keep
|
|
607
662
|
// changes no registration, so it stays; its uninstall (rule 4) drops it.
|
|
608
663
|
export function npmRowBeside({ harness, id, surface }) {
|