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
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
// modelsProblem(manifest) → what is wrong with the block, or null:
|
|
19
19
|
// the manifest schema test's check
|
|
20
20
|
//
|
|
21
|
+
// Effort (the ADR "Agent effort is passed at spawn beside the model, from the
|
|
22
|
+
// overlay, in each harness's own levels") reads the same entries: the
|
|
23
|
+
// manifest's agent_translation.effort names the levels its spawn takes, and,
|
|
24
|
+
// where a catalog lists a model's own levels, their field paths.
|
|
25
|
+
//
|
|
26
|
+
// effortSpec(manifest) → the effort block, or null
|
|
27
|
+
// effortLevels(manifest, model, known) → the levels a model takes, from its
|
|
28
|
+
// catalog entry or the manifest's floor
|
|
29
|
+
// offLevelNote(manifest, level, model, known) → the PLAN's note for a level
|
|
30
|
+
// that list lacks, or null (decision 5)
|
|
31
|
+
// effortProblem(manifest) → what is wrong with the block, or null
|
|
32
|
+
//
|
|
21
33
|
// Local reads are synchronous and cheap: the manifest, a `file` catalog, and
|
|
22
34
|
// projectstore's own cache. A `command` or `http` catalog is read only by
|
|
23
35
|
// readLive, which a menu starts in the background. env, home, now, fetch,
|
|
@@ -41,6 +53,15 @@ export function modelSpec(manifest) {
|
|
|
41
53
|
return m && typeof m === "object" ? m : null;
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
export function effortSpec(manifest) {
|
|
57
|
+
const e = manifest?.agent_translation?.effort;
|
|
58
|
+
return e && typeof e === "object" ? e : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A level is a lowercase token: the shape --set accepts and a catalog's
|
|
62
|
+
// level must have to be kept.
|
|
63
|
+
export const LEVEL = /^[a-z][a-z0-9-]*$/;
|
|
64
|
+
|
|
44
65
|
// One field path, dotted: "data", "models", "fetched_at".
|
|
45
66
|
const at = (obj, path) =>
|
|
46
67
|
String(path || "")
|
|
@@ -67,9 +88,10 @@ const clean = (s) =>
|
|
|
67
88
|
.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
68
89
|
|
|
69
90
|
// A catalog's answer as rows: { id, label, description, family, hidden },
|
|
70
|
-
// in the catalog's order
|
|
71
|
-
//
|
|
72
|
-
|
|
91
|
+
// in the catalog's order, and `levels` and `default_level` when `levelFields`
|
|
92
|
+
// (the effort block's catalog paths) finds them in an entry. Entries without
|
|
93
|
+
// an id, and entries `keep_when` rejects, are dropped; `shown_when` only hides.
|
|
94
|
+
export function entriesOf(body, fields, levelFields = null) {
|
|
73
95
|
const list = at(body, fields.list);
|
|
74
96
|
if (!Array.isArray(list)) return null;
|
|
75
97
|
const rows = [];
|
|
@@ -89,6 +111,7 @@ export function entriesOf(body, fields) {
|
|
|
89
111
|
description: text(fields.description),
|
|
90
112
|
family: text(fields.family),
|
|
91
113
|
hidden: fields.shown_when ? !matches(e, fields.shown_when) : false,
|
|
114
|
+
...levelsOf(e, levelFields),
|
|
92
115
|
order: fields.order ? at(e, fields.order) : k,
|
|
93
116
|
k,
|
|
94
117
|
});
|
|
@@ -100,7 +123,37 @@ export function entriesOf(body, fields) {
|
|
|
100
123
|
if (x === y || x === undefined || y === undefined) return a.k - b.k;
|
|
101
124
|
return (x < y ? -1 : 1) * (desc ? -1 : 1);
|
|
102
125
|
});
|
|
103
|
-
return rows.map(({
|
|
126
|
+
return rows.map(({ order: _order, k: _k, ...row }) => row);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// One entry's own levels, by the effort block's catalog paths: { levels:
|
|
130
|
+
// [{ id, description }], default_level } or nothing at all, so an entry
|
|
131
|
+
// without them reads exactly as it did before levels were read.
|
|
132
|
+
function levelsOf(e, lf) {
|
|
133
|
+
if (!lf || !lf.list) return {};
|
|
134
|
+
const list = at(e, lf.list);
|
|
135
|
+
if (!Array.isArray(list)) return {};
|
|
136
|
+
const levels = [];
|
|
137
|
+
for (const x of list) {
|
|
138
|
+
const raw = x && typeof x === "object" ? at(x, lf.id || "id") : x;
|
|
139
|
+
const id = typeof raw === "string" ? clean(raw) : "";
|
|
140
|
+
if (!LEVEL.test(id) || levels.some((l) => l.id === id)) continue;
|
|
141
|
+
const d = x && typeof x === "object" && lf.description ? at(x, lf.description) : undefined;
|
|
142
|
+
levels.push({ id, description: typeof d === "string" ? clean(d) : "" });
|
|
143
|
+
}
|
|
144
|
+
if (!levels.length) return {};
|
|
145
|
+
const dRaw = lf.default ? at(e, lf.default) : undefined;
|
|
146
|
+
const d = typeof dRaw === "string" ? clean(dRaw) : "";
|
|
147
|
+
return { levels, ...(levels.some((l) => l.id === d) ? { default_level: d } : {}) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A cached row's levels, cleaned again: the cache is a file anyone can edit.
|
|
151
|
+
function cachedLevels(x) {
|
|
152
|
+
if (!Array.isArray(x.levels)) return {};
|
|
153
|
+
return levelsOf(
|
|
154
|
+
{ l: x.levels, d: x.default_level },
|
|
155
|
+
{ list: "l", id: "id", description: "description", default: "d" },
|
|
156
|
+
);
|
|
104
157
|
}
|
|
105
158
|
|
|
106
159
|
const timeOf = (v) => {
|
|
@@ -129,6 +182,7 @@ function readCache(manifest, opts) {
|
|
|
129
182
|
description: clean(x.description ?? ""),
|
|
130
183
|
family: clean(x.family ?? ""),
|
|
131
184
|
hidden: Boolean(x.hidden),
|
|
185
|
+
...cachedLevels(x),
|
|
132
186
|
}));
|
|
133
187
|
const source = typeof c.source === "string" ? c.source : "";
|
|
134
188
|
const from = (modelSpec(manifest)?.catalogs || []).find((x) => x.source === source);
|
|
@@ -147,7 +201,7 @@ function readFileCatalog(manifest, spec, catalog, opts) {
|
|
|
147
201
|
const raw = readFileSync(p, "utf8");
|
|
148
202
|
const fields = fieldsOf(spec, catalog);
|
|
149
203
|
const body = JSON.parse(raw);
|
|
150
|
-
const entries = entriesOf(body, fields);
|
|
204
|
+
const entries = entriesOf(body, fields, effortSpec(manifest)?.catalog);
|
|
151
205
|
if (!entries) return null;
|
|
152
206
|
const t = timeOf(fields.at ? at(body, fields.at) : undefined) ?? statSync(p).mtimeMs;
|
|
153
207
|
return { at: t, source: catalog.source || "", caption: catalog.caption || catalog.source || "", entries };
|
|
@@ -239,6 +293,14 @@ export function knownModels(manifest, { env = process.env, home = homedir(), now
|
|
|
239
293
|
if (!offered.some((o) => o.id === s.id))
|
|
240
294
|
offered.push({ id: s.id, label: s.label || "", description: s.description || "" });
|
|
241
295
|
}
|
|
296
|
+
// A model's own levels, from the newest list whose entry for it names
|
|
297
|
+
// them: a cache written before levels were kept falls through to the
|
|
298
|
+
// harness's own file, so a refresh never loses what that file says.
|
|
299
|
+
const levels = new Map();
|
|
300
|
+
for (const c of [...(past.length ? past : candidates)].sort((a, b) => b.at - a.at))
|
|
301
|
+
for (const e of c.entries)
|
|
302
|
+
if (e.levels && !levels.has(e.id))
|
|
303
|
+
levels.set(e.id, { levels: e.levels, default: e.default_level || null, source: c.source, at: c.at });
|
|
242
304
|
const t = t0;
|
|
243
305
|
const live = liveCatalog(spec, env);
|
|
244
306
|
return {
|
|
@@ -247,6 +309,7 @@ export function knownModels(manifest, { env = process.env, home = homedir(), now
|
|
|
247
309
|
offered,
|
|
248
310
|
known,
|
|
249
311
|
hidden: new Set(entries.filter((e) => e.hidden).map((e) => e.id)),
|
|
312
|
+
levels,
|
|
250
313
|
source: newest ? newest.source : "",
|
|
251
314
|
caption: newest ? newest.caption || newest.source : "",
|
|
252
315
|
at: newest ? newest.at : null,
|
|
@@ -288,6 +351,53 @@ const localDate = (t) => {
|
|
|
288
351
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
289
352
|
};
|
|
290
353
|
|
|
354
|
+
// The levels a model takes (the effort ADR, decisions 3 and 4): its own, when
|
|
355
|
+
// a list names them, else the manifest's floor. `known` is knownModels'
|
|
356
|
+
// answer, or null. Null when the manifest declares no effort block.
|
|
357
|
+
// { levels: [{ id, description }], default, from: "catalog" | "floor",
|
|
358
|
+
// model, source, at }
|
|
359
|
+
export function effortLevels(manifest, model, known) {
|
|
360
|
+
const spec = effortSpec(manifest);
|
|
361
|
+
if (!spec) return null;
|
|
362
|
+
const own = model ? known?.levels?.get(model) : null;
|
|
363
|
+
if (own) return { levels: own.levels, default: own.default, from: "catalog", model, source: own.source, at: own.at };
|
|
364
|
+
const levels = (spec.levels || []).filter((l) => l && typeof l.id === "string" && l.id);
|
|
365
|
+
return { levels, default: null, from: "floor", model: model || null, source: "", at: null };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// The PLAN's note for a level the list lacks (the effort ADR, decision 5), or
|
|
369
|
+
// null when it is listed, or when the harness declares no levels at all.
|
|
370
|
+
// the model's own list: "<model> does not list <level> in <source>'s list
|
|
371
|
+
// as of <date>; written as typed"
|
|
372
|
+
// the floor: "<level> is not one of <harness>'s effort levels
|
|
373
|
+
// (<levels>); written as typed", and, where the
|
|
374
|
+
// spawn takes those levels only, that it refuses it
|
|
375
|
+
export function offLevelNote(manifest, level, model, known) {
|
|
376
|
+
if (!level) return null;
|
|
377
|
+
const l = effortLevels(manifest, model, known);
|
|
378
|
+
if (!l || l.levels.some((x) => x.id === level)) return null;
|
|
379
|
+
const display = manifest?.display_name || manifest?.id || "this harness";
|
|
380
|
+
if (l.from === "catalog")
|
|
381
|
+
return `${l.model} does not list ${level} in ${l.source || display}'s list${l.at ? ` as of ${localDate(l.at)}` : ""}; written as typed`;
|
|
382
|
+
const list = l.levels.map((x) => x.id).join(", ");
|
|
383
|
+
return effortSpec(manifest).strict
|
|
384
|
+
? `${level} is not one of ${display}'s effort levels (${list}); written as typed, and ${display} refuses it at spawn`
|
|
385
|
+
: `${level} is not one of ${display}'s effort levels (${list}); written as typed`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// The note for a level an agent keeps when the run moves it to a model that
|
|
389
|
+
// does not take it (the effort ADR, decision 5), or null: "<agent> runs at
|
|
390
|
+
// <level>, which <model> does not list in <source>'s list as of <date>".
|
|
391
|
+
export function standingLevelNote(manifest, agent, level, model, known) {
|
|
392
|
+
if (!level) return null;
|
|
393
|
+
const l = effortLevels(manifest, model, known);
|
|
394
|
+
if (!l || l.levels.some((x) => x.id === level)) return null;
|
|
395
|
+
const display = manifest?.display_name || manifest?.id || "this harness";
|
|
396
|
+
return l.from === "catalog"
|
|
397
|
+
? `${agent} runs at ${level}, which ${l.model} does not list in ${l.source || display}'s list${l.at ? ` as of ${localDate(l.at)}` : ""}`
|
|
398
|
+
: `${agent} runs at ${level}, which is not one of ${display}'s effort levels`;
|
|
399
|
+
}
|
|
400
|
+
|
|
291
401
|
// What is wrong with a manifest's models block, or null (the manifest schema
|
|
292
402
|
// test, generation contract 1). A manifest without the block is fine: its
|
|
293
403
|
// agents' models are typed by name.
|
|
@@ -321,6 +431,40 @@ export function modelsProblem(manifest) {
|
|
|
321
431
|
return null;
|
|
322
432
|
}
|
|
323
433
|
|
|
434
|
+
// What is wrong with a manifest's effort block, or null (the manifest schema
|
|
435
|
+
// test). A manifest without the block passes no effort, and is fine.
|
|
436
|
+
export function effortProblem(manifest) {
|
|
437
|
+
const spec = effortSpec(manifest);
|
|
438
|
+
if (!spec) return manifest?.agent_translation?.effort === undefined ? null : "effort is not an object";
|
|
439
|
+
const str = (v) => typeof v === "string" && v.length > 0;
|
|
440
|
+
const EVIDENCE = ["measured", "documented", "chosen"];
|
|
441
|
+
if (!str(spec.spawn_param) || !str(spec.spawn_param_reason)) return "spawn_param carries no name or no reason";
|
|
442
|
+
if (!EVIDENCE.includes(spec.spawn_param_evidence))
|
|
443
|
+
return `spawn_param_evidence is ${spec.spawn_param_evidence}, not ${EVIDENCE.join(", ")}`;
|
|
444
|
+
if (!Array.isArray(spec.levels) || !spec.levels.length) return "levels is not a non-empty list";
|
|
445
|
+
for (const l of spec.levels)
|
|
446
|
+
if (!l || !LEVEL.test(String(l.id)) || !str(l.description))
|
|
447
|
+
return `a level is not { id, description }: ${JSON.stringify(l)}`;
|
|
448
|
+
if (!str(spec.levels_reason)) return "levels carries no levels_reason";
|
|
449
|
+
if (spec.strict !== undefined && (typeof spec.strict !== "boolean" || !str(spec.strict_reason)))
|
|
450
|
+
return "strict is not a boolean with a strict_reason";
|
|
451
|
+
if (spec.catalog !== undefined) {
|
|
452
|
+
const c = spec.catalog;
|
|
453
|
+
if (!c || !str(c.list) || !str(c.id) || !str(c.reason)) return "the effort catalog names no list, id or reason";
|
|
454
|
+
if (!modelSpec(manifest)) return "an effort catalog without a models block has no entries to read";
|
|
455
|
+
}
|
|
456
|
+
const u = spec.unset;
|
|
457
|
+
if (!u || !str(u.value) || !str(u.row) || typeof u.model_default !== "boolean" || !str(u.reason))
|
|
458
|
+
return "unset is not { value, row, model_default, reason }";
|
|
459
|
+
if (spec.cheap !== undefined) {
|
|
460
|
+
if (!spec.levels.some((l) => l.id === spec.cheap)) return `cheap is ${spec.cheap}, not one of the levels`;
|
|
461
|
+
if (!str(spec.cheap_reason)) return "cheap carries no cheap_reason";
|
|
462
|
+
if (!EVIDENCE.includes(spec.cheap_evidence))
|
|
463
|
+
return `cheap_evidence is ${spec.cheap_evidence}, not ${EVIDENCE.join(", ")}`;
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
324
468
|
// One bounded fetch, settled by a timer whatever the request does after: the
|
|
325
469
|
// pattern of convergence.mjs's request(). The key goes in its header and
|
|
326
470
|
// nowhere else; nothing of the request is kept.
|
|
@@ -487,7 +631,7 @@ export async function readLive(
|
|
|
487
631
|
}
|
|
488
632
|
if (r.error) return { error: r.error, source: catalog.source || "" };
|
|
489
633
|
if (signal?.aborted) return { error: "stopped", source: catalog.source || "" };
|
|
490
|
-
const entries = entriesOf(r.body, fieldsOf(spec, catalog));
|
|
634
|
+
const entries = entriesOf(r.body, fieldsOf(spec, catalog), effortSpec(manifest)?.catalog);
|
|
491
635
|
if (!entries) return { error: "the answer holds no list", source: catalog.source || "" };
|
|
492
636
|
const t = now();
|
|
493
637
|
const p = modelCachePath(manifest, { env, home });
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
languageNames,
|
|
65
65
|
} from "./binding.mjs";
|
|
66
66
|
import { analyseRegistration, analyseJsonEntry } from "./surfaces.mjs";
|
|
67
|
-
import { runningCopies, coreCommand, channelChoices, pinFor } from "./convergence.mjs";
|
|
67
|
+
import { runningCopies, coreCommand, channelChoices, pinFor, terminalRun } from "./convergence.mjs";
|
|
68
68
|
import { runVerb, isInteractive, gateText, channelText, publicItem, planOptions } from "./install-harness.mjs";
|
|
69
69
|
import { resolveBinding } from "./worktree.mjs";
|
|
70
70
|
import * as term from "./term.mjs";
|
|
@@ -328,7 +328,9 @@ function factsOf(project, { env, home, root, version, interactive }) {
|
|
|
328
328
|
sessionCopy = selectCopy(home, project, { harness: channelHarness });
|
|
329
329
|
}
|
|
330
330
|
const slotHarness = manifests.find((m) => statuslineOf(m)) || null;
|
|
331
|
-
const slot = slotHarness
|
|
331
|
+
const slot = slotHarness
|
|
332
|
+
? analyseJsonEntry(project, statuslineOf(slotHarness), { root, home, harness: slotHarness }).state
|
|
333
|
+
: null;
|
|
332
334
|
// A worktree's main checkout (worktree.mjs): only where it can be offered —
|
|
333
335
|
// at a terminal, unbound, in a linked worktree. Read for the project
|
|
334
336
|
// answered, never from the git top, which --project leaves unread.
|
|
@@ -692,12 +694,12 @@ function doneLines(r, { project, runEnv, home, version, channel, vaultPlan, unde
|
|
|
692
694
|
// Rule 13: `--channel npm` when that was the answer, and only for the
|
|
693
695
|
// harness whose channel it is — setup refuses --channel in a run without
|
|
694
696
|
// that harness (exit 2). A keep is not repeated: once recorded, it is the
|
|
695
|
-
// default already (rule 6).
|
|
696
|
-
|
|
697
|
+
// default already (rule 6). The deferred row's reason names the same
|
|
698
|
+
// command, from the same builder.
|
|
697
699
|
if (left.length)
|
|
698
700
|
blocks.push({
|
|
699
701
|
lead: `Left for a terminal outside this ${m.display_name} session: ${left.join(", ")}. From a terminal in this project, run:`,
|
|
700
|
-
commands: [
|
|
702
|
+
commands: [terminalRun({ verb: "setup", harness: m, version, channel })],
|
|
701
703
|
after: `then restart ${m.display_name}.`,
|
|
702
704
|
});
|
|
703
705
|
}
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Normative: the install spec — contract 0 (exclusive vs shared), 3–4 (the
|
|
21
21
|
// four states and the ladder, rungs 1′ and 1″ included), 6 (per-entry states
|
|
22
|
-
// for shared files: ours-current, ours-stale, ours-absent,
|
|
22
|
+
// for shared files: ours-current, ours-stale, ours-newer — 2026-10-14 —, ours-absent,
|
|
23
|
+
// unparseable),
|
|
23
24
|
// 12 (current, last written by), 14 (host-managed surfaces have no state to
|
|
24
25
|
// derive), 16 (a harness is in use when detected or when it has files of
|
|
25
26
|
// ours). Pure node, no external deps.
|
|
@@ -37,6 +38,8 @@ import {
|
|
|
37
38
|
desiredStatusLineCommand,
|
|
38
39
|
findAgentsBlock,
|
|
39
40
|
agentsBlockVersion,
|
|
41
|
+
newerBlockWords,
|
|
42
|
+
isNewerBlock,
|
|
40
43
|
agentsBlockTemplatePath,
|
|
41
44
|
renderAgentsBlock,
|
|
42
45
|
loadLayout,
|
|
@@ -185,6 +188,15 @@ export function analyseBlock(
|
|
|
185
188
|
a.current = withBlock.find((e) => e.file === preferred.file) || withBlock[0] || null;
|
|
186
189
|
a.duplicates = withBlock.filter((e) => e !== a.current);
|
|
187
190
|
if (!a.current) return { ...a, state: "ours-absent" };
|
|
191
|
+
// A marker above this copy's template is never rendered down (the install
|
|
192
|
+
// spec, contracts 4 and 6, as amended 2026-10-14). Placement still decides
|
|
193
|
+
// first (rungs B4–B6), and a block that moves, migrates or is deduplicated
|
|
194
|
+
// carries the newest block's own text; one that stays is `ours-newer`
|
|
195
|
+
// (B7), before the render is compared, so its roster is never re-rendered.
|
|
196
|
+
const newest = withBlock.reduce((n, e) => (e.block.v > n.block.v ? e : n), a.current);
|
|
197
|
+
a.newer = isNewerBlock(newest.block.v, a.version) ? newest : null;
|
|
198
|
+
const words = a.newer ? newerBlockWords(a.newer.block.v, a.version) : null;
|
|
199
|
+
const carried = a.newer ? ` with its own text (${words})` : "";
|
|
188
200
|
// A block this harness CANNOT READ moves, and its target is created if it has
|
|
189
201
|
// to be: that is the whole difference between a second harness arriving and a
|
|
190
202
|
// preference. A block it can read moves only when the preferred file already
|
|
@@ -197,17 +209,25 @@ export function analyseBlock(
|
|
|
197
209
|
return {
|
|
198
210
|
...a,
|
|
199
211
|
state: "ours-stale",
|
|
200
|
-
reason: `in ${a.current.file}, which ${reader || s.harness_display || "this harness"} does not read; install moves it to ${preferred.file}`,
|
|
212
|
+
reason: `in ${a.current.file}, which ${reader || s.harness_display || "this harness"} does not read; install moves it to ${preferred.file}${carried}`,
|
|
201
213
|
};
|
|
202
214
|
}
|
|
203
215
|
if (a.current.file !== preferred.file && preferred.present)
|
|
204
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
...a,
|
|
218
|
+
state: "ours-stale",
|
|
219
|
+
reason: `in ${a.current.file}; install migrates it to ${preferred.file}${carried}`,
|
|
220
|
+
};
|
|
205
221
|
if (a.duplicates.length)
|
|
206
222
|
return {
|
|
207
223
|
...a,
|
|
208
224
|
state: "ours-stale",
|
|
209
|
-
reason:
|
|
225
|
+
reason:
|
|
226
|
+
a.newer && a.newer !== a.current
|
|
227
|
+
? `also in ${a.duplicates.map((e) => e.file).join(", ")}; install keeps the one in ${a.current.file}, and ${a.newer.file}'s carries v${a.newer.block.v}, newer than this copy's v${a.version}: its text replaces the block in ${a.current.file}`
|
|
228
|
+
: `also in ${a.duplicates.map((e) => e.file).join(", ")}; install keeps the one in ${a.current.file}${carried}`,
|
|
210
229
|
};
|
|
230
|
+
if (a.newer) return { ...a, state: "ours-newer", reason: `${words}: left as it is` };
|
|
211
231
|
if (a.current.block.v === a.version && a.current.block.block === a.desired) return { ...a, state: "ours-current" };
|
|
212
232
|
return {
|
|
213
233
|
...a,
|
|
@@ -223,8 +243,15 @@ export function analyseBlock(
|
|
|
223
243
|
|
|
224
244
|
// `renderRoot` (contract 4′, two phases): the root the surface is planned
|
|
225
245
|
// AGAINST — the host's install path a registration produces — when it differs
|
|
226
|
-
// from the root the templates are read from. Defaults to `root`.
|
|
227
|
-
|
|
246
|
+
// from the root the templates are read from. Defaults to `root`. `harness` is
|
|
247
|
+
// the one the entry is FOR: its launcher lives under its own state directory,
|
|
248
|
+
// whichever harness launched this process (the install spec, contract 6, as
|
|
249
|
+
// amended 2026-10-14).
|
|
250
|
+
export function analyseJsonEntry(
|
|
251
|
+
projectDir,
|
|
252
|
+
s,
|
|
253
|
+
{ root = pluginRoot(), home = homedir(), renderRoot = root, harness = null } = {},
|
|
254
|
+
) {
|
|
228
255
|
const path = join(projectDir, s.file);
|
|
229
256
|
const pointer = s.marker?.pointer || "statusLine.command";
|
|
230
257
|
const entryKey = pointer.split(".")[0];
|
|
@@ -263,7 +290,7 @@ export function analyseJsonEntry(projectDir, s, { root = pluginRoot(), home = ho
|
|
|
263
290
|
a.ours =
|
|
264
291
|
statusLineIsOurWiring(a.curCmd, projectDir, home, root) ||
|
|
265
292
|
statusLineIsOurWiring(a.curCmd, projectDir, home, renderRoot);
|
|
266
|
-
a.desired = desiredStatusLineCommand(projectDir, renderRoot, home).command;
|
|
293
|
+
a.desired = desiredStatusLineCommand(projectDir, renderRoot, home, harness?.id).command;
|
|
267
294
|
if (!a.curEntry) return { ...a, state: "ours-absent" };
|
|
268
295
|
if (!a.ours) return { ...a, state: "theirs", reason: "a status line we did not write owns the slot" };
|
|
269
296
|
if (a.curCmd === a.desired) return { ...a, state: "ours-current" };
|
|
@@ -317,7 +344,7 @@ export function analyseStampedFile(
|
|
|
317
344
|
const tpl = readText(src);
|
|
318
345
|
if (!tpl.present || tpl.text === null)
|
|
319
346
|
return { ...a, state: "unparseable", refusal: `the source ${s.source} is missing from the plugin at ${root}` };
|
|
320
|
-
const rendered = renderStatusLineLauncher(tpl.text, renderRoot, projectDir);
|
|
347
|
+
const rendered = renderStatusLineLauncher(tpl.text, renderRoot, projectDir, process.env, harness);
|
|
321
348
|
if (rendered === null)
|
|
322
349
|
return { ...a, state: "unparseable", refusal: "the launcher template is not one this installer knows how to fill" };
|
|
323
350
|
const pkg = pluginVersionAt(root, harness) || "0.0.0";
|
|
@@ -791,7 +818,7 @@ export function surfaceStates(
|
|
|
791
818
|
const hasOurs = states.some(
|
|
792
819
|
(x) =>
|
|
793
820
|
(x.kind === "exclusive" && ["current", "stale"].includes(x.state)) ||
|
|
794
|
-
(x.kind === "shared" && ["ours-current", "ours-stale"].includes(x.state)),
|
|
821
|
+
(x.kind === "shared" && ["ours-current", "ours-stale", "ours-newer"].includes(x.state)),
|
|
795
822
|
);
|
|
796
823
|
if (detected.includes(m.id) || hasOurs) {
|
|
797
824
|
out.used.push(m.id);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- projectstore:agents
|
|
1
|
+
<!-- projectstore:agents v5 (managed by projectstore — edit outside markers) -->
|
|
2
2
|
## projectstore agents
|
|
3
3
|
|
|
4
4
|
- **A feature-sized request opens a vault artifact before it opens an editor.**
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
harness-level instruction contradicts this block, say so and ask which wins.
|
|
12
12
|
Resolving it silently is how the contradiction becomes invisible to the person
|
|
13
13
|
who could have settled it.
|
|
14
|
-
- When spawning any agent below, resolve its model from
|
|
15
|
-
`.projectstore/harness/<harness>.json` → `agents.per_agent.<name>.model ?? agents.default.model
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
- When spawning any agent below, resolve its model and its effort from
|
|
15
|
+
`.projectstore/harness/<harness>.json` → `agents.per_agent.<name>.model ?? agents.default.model`
|
|
16
|
+
and `agents.per_agent.<name>.effort ?? agents.default.effort`, where `<name>` is
|
|
17
|
+
the **bare** agent name (`critic` for `projectstore:critic`), and pass them as
|
|
18
|
+
the spawn's model and effort parameters, the effort only when the spawn offers
|
|
19
|
+
one. No key — pass nothing.
|
|
18
20
|
- After authoring or revising any vault artifact (ADR/research/epic/story) or
|
|
19
21
|
design proposal: run the `projectstore:critic` agent on it before treating it final.
|
|
20
22
|
- Before implementing an epic/story: consult `projectstore:planner` — it plans
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore-claude",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.4",
|
|
4
4
|
"description": "projectstore for Claude Code: the core pinned and bundled, the harness fixed. A release artifact; the way in is npx projectstore setup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"projectstore",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"README.md"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"projectstore": "=0.30.
|
|
40
|
+
"projectstore": "=0.30.4"
|
|
41
41
|
},
|
|
42
42
|
"bundleDependencies": [
|
|
43
43
|
"projectstore"
|