projectstore-codex 0.0.1 → 0.28.1
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/.codex-plugin/plugin.json +48 -0
- package/README.md +15 -7
- package/bin/projectstore-codex.mjs +88 -0
- package/hooks/hooks.json +59 -0
- package/node_modules/projectstore/.claude-plugin/marketplace.json +40 -0
- package/node_modules/projectstore/.claude-plugin/plugin.json +23 -0
- package/node_modules/projectstore/.mcp.json +14 -0
- package/node_modules/projectstore/AGENTS.md +26 -0
- package/node_modules/projectstore/LICENSE +21 -0
- package/node_modules/projectstore/README.md +284 -0
- package/node_modules/projectstore/agents/archaeologist.md +76 -0
- package/node_modules/projectstore/agents/clerk.md +93 -0
- package/node_modules/projectstore/agents/critic.md +94 -0
- package/node_modules/projectstore/agents/librarian.md +81 -0
- package/node_modules/projectstore/agents/planner.md +80 -0
- package/node_modules/projectstore/agents/reviewer.md +98 -0
- package/node_modules/projectstore/bin/projectstore.mjs +7 -0
- package/node_modules/projectstore/commands/adr.md +57 -0
- package/node_modules/projectstore/commands/agents.md +180 -0
- package/node_modules/projectstore/commands/bind.md +128 -0
- package/node_modules/projectstore/commands/codemap.md +50 -0
- package/node_modules/projectstore/commands/concept.md +17 -0
- package/node_modules/projectstore/commands/doctor.md +166 -0
- package/node_modules/projectstore/commands/epic.md +40 -0
- package/node_modules/projectstore/commands/graph.md +56 -0
- package/node_modules/projectstore/commands/kanban.md +40 -0
- package/node_modules/projectstore/commands/meeting.md +17 -0
- package/node_modules/projectstore/commands/reconcile.md +73 -0
- package/node_modules/projectstore/commands/research.md +17 -0
- package/node_modules/projectstore/commands/review.md +89 -0
- package/node_modules/projectstore/commands/runbook.md +17 -0
- package/node_modules/projectstore/commands/scaffold.md +23 -0
- package/node_modules/projectstore/commands/search.md +22 -0
- package/node_modules/projectstore/commands/spec.md +91 -0
- package/node_modules/projectstore/commands/status.md +27 -0
- package/node_modules/projectstore/commands/statusline.md +46 -0
- package/node_modules/projectstore/commands/story.md +113 -0
- package/node_modules/projectstore/docs/extending.md +172 -0
- package/node_modules/projectstore/docs/getting-started.md +133 -0
- package/node_modules/projectstore/docs/harnesses.md +163 -0
- package/node_modules/projectstore/docs/how-it-works.md +263 -0
- package/node_modules/projectstore/docs/images/loop-light.svg +94 -0
- package/node_modules/projectstore/docs/images/loop.svg +93 -0
- package/node_modules/projectstore/docs/images/statusline-hud.png +0 -0
- package/node_modules/projectstore/docs/images/team-light.svg +79 -0
- package/node_modules/projectstore/docs/images/team.svg +79 -0
- package/node_modules/projectstore/harnesses/claude-code.json +483 -0
- package/node_modules/projectstore/harnesses/codex.json +332 -0
- package/node_modules/projectstore/hooks/hooks.json +59 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +121 -0
- package/node_modules/projectstore/hooks/session-rules.mjs +63 -0
- package/node_modules/projectstore/hooks/session-start.mjs +301 -0
- package/node_modules/projectstore/hooks/session-stop.mjs +84 -0
- package/node_modules/projectstore/package.json +70 -0
- package/node_modules/projectstore/scaffold/checklists.json +88 -0
- package/node_modules/projectstore/scaffold/headings.json +171 -0
- package/node_modules/projectstore/scaffold/layouts/engineering.json +85 -0
- package/node_modules/projectstore/scripts/binding.mjs +165 -0
- package/node_modules/projectstore/scripts/build-adapters.mjs +264 -0
- package/node_modules/projectstore/scripts/cli.mjs +595 -0
- package/node_modules/projectstore/scripts/codemap.mjs +99 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +127 -0
- package/node_modules/projectstore/scripts/doctor.mjs +2127 -0
- package/node_modules/projectstore/scripts/draft.mjs +261 -0
- package/node_modules/projectstore/scripts/graph.mjs +219 -0
- package/node_modules/projectstore/scripts/harness.mjs +608 -0
- package/node_modules/projectstore/scripts/install-harness.mjs +1387 -0
- package/node_modules/projectstore/scripts/kanban.mjs +174 -0
- package/node_modules/projectstore/scripts/lib.mjs +3085 -0
- package/node_modules/projectstore/scripts/mcp.mjs +391 -0
- package/node_modules/projectstore/scripts/portable-registration.mjs +198 -0
- package/node_modules/projectstore/scripts/provenance.mjs +375 -0
- package/node_modules/projectstore/scripts/query.mjs +490 -0
- package/node_modules/projectstore/scripts/reconcile.mjs +422 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +141 -0
- package/node_modules/projectstore/scripts/statusline.mjs +253 -0
- package/node_modules/projectstore/scripts/story-section.mjs +209 -0
- package/node_modules/projectstore/scripts/surfaces.mjs +421 -0
- package/node_modules/projectstore/scripts/tokens.mjs +449 -0
- package/node_modules/projectstore/scripts/touch-session.mjs +336 -0
- package/node_modules/projectstore/scripts/version-guard.mjs +255 -0
- package/node_modules/projectstore/scripts/worktree.mjs +109 -0
- package/node_modules/projectstore/skills/projectstore-decision-detector/SKILL.md +40 -0
- package/node_modules/projectstore/skills/projectstore-peer-reviewer/SKILL.md +38 -0
- package/node_modules/projectstore/skills/projectstore-story-completion/SKILL.md +50 -0
- package/node_modules/projectstore/skills/projectstore-vault-communication/SKILL.md +96 -0
- package/node_modules/projectstore/templates/claude-md-block.md.tmpl +26 -0
- package/node_modules/projectstore/templates/de/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/de/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/de/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/de/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/de/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/de/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/de/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/de/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/de/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/de/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/de/strings.json +6 -0
- package/node_modules/projectstore/templates/en/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/en/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/en/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/en/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/en/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/en/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/en/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/en/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/en/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/en/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/en/strings.json +6 -0
- package/node_modules/projectstore/templates/es/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/es/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/es/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/es/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/es/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/es/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/es/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/es/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/es/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/es/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/es/strings.json +6 -0
- package/node_modules/projectstore/templates/fr/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/fr/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/fr/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/fr/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/fr/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/fr/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/fr/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/fr/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/fr/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/fr/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/fr/strings.json +6 -0
- package/node_modules/projectstore/templates/ru/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/ru/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/ru/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/ru/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/ru/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/ru/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/ru/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/ru/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/ru/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/ru/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/ru/strings.json +6 -0
- package/node_modules/projectstore/templates/zh/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/zh/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/zh/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/zh/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/zh/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/zh/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/zh/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/zh/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/zh/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/zh/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/zh/strings.json +6 -0
- package/package.json +36 -14
- package/plugin.json +53 -0
- package/skills/projectstore-adr/SKILL.md +76 -0
- package/skills/projectstore-agents/SKILL.md +50 -0
- package/skills/projectstore-archaeologist/SKILL.md +109 -0
- package/skills/projectstore-bind/SKILL.md +44 -0
- package/skills/projectstore-clerk/SKILL.md +126 -0
- package/skills/projectstore-codemap/SKILL.md +69 -0
- package/skills/projectstore-concept/SKILL.md +36 -0
- package/skills/projectstore-critic/SKILL.md +127 -0
- package/skills/projectstore-decision-detector/SKILL.md +59 -0
- package/skills/projectstore-doctor/SKILL.md +33 -0
- package/skills/projectstore-epic/SKILL.md +59 -0
- package/skills/projectstore-graph/SKILL.md +75 -0
- package/skills/projectstore-kanban/SKILL.md +60 -0
- package/skills/projectstore-librarian/SKILL.md +114 -0
- package/skills/projectstore-meeting/SKILL.md +36 -0
- package/skills/projectstore-peer-reviewer/SKILL.md +57 -0
- package/skills/projectstore-planner/SKILL.md +113 -0
- package/skills/projectstore-reconcile/SKILL.md +92 -0
- package/skills/projectstore-research/SKILL.md +36 -0
- package/skills/projectstore-review/SKILL.md +108 -0
- package/skills/projectstore-reviewer/SKILL.md +131 -0
- package/skills/projectstore-runbook/SKILL.md +36 -0
- package/skills/projectstore-scaffold/SKILL.md +42 -0
- package/skills/projectstore-search/SKILL.md +41 -0
- package/skills/projectstore-spec/SKILL.md +110 -0
- package/skills/projectstore-status/SKILL.md +47 -0
- package/skills/projectstore-statusline/SKILL.md +29 -0
- package/skills/projectstore-story/SKILL.md +132 -0
- package/skills/projectstore-story-completion/SKILL.md +69 -0
- package/skills/projectstore-vault-communication/SKILL.md +115 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// projectstore — surfaces.mjs
|
|
2
|
+
//
|
|
3
|
+
// The STATE of every installed surface, read but never written: what is on
|
|
4
|
+
// disk at each path the manifest names, whether it is ours, and if so whether
|
|
5
|
+
// it is current. install-harness.mjs turns these states into actions behind
|
|
6
|
+
// its gate; doctor turns them into findings. Neither derives a state itself,
|
|
7
|
+
// so the two can never disagree about a file — and neither knows a harness
|
|
8
|
+
// id: everything here is keyed by the manifest's surfaces.<key>.kind and
|
|
9
|
+
// .format.
|
|
10
|
+
//
|
|
11
|
+
// Direction: installer → surfaces ← doctor, and surfaces → provenance,
|
|
12
|
+
// lib.mjs. Doctor imports this module DYNAMICALLY, inside the one check that
|
|
13
|
+
// needs it, because hooks/session-start.mjs imports doctor statically and the
|
|
14
|
+
// install spec keeps provenance.mjs out of the SessionStart module graph.
|
|
15
|
+
//
|
|
16
|
+
// Read-only by construction: no write call appears here (the suite greps for
|
|
17
|
+
// them). stamp() is a pure render; its text is what install would write and
|
|
18
|
+
// its hash is what the state ladder compares.
|
|
19
|
+
//
|
|
20
|
+
// Normative: the install spec — contract 0 (exclusive vs shared), 3–4 (the
|
|
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, unparseable),
|
|
23
|
+
// 12 (current, last written by), 14 (host-managed surfaces have no state to
|
|
24
|
+
// derive), 16 (a harness is in use when detected or when it has files of
|
|
25
|
+
// ours). Pure node, no external deps.
|
|
26
|
+
|
|
27
|
+
import { readFileSync, existsSync, realpathSync, readdirSync } from "node:fs";
|
|
28
|
+
import { join, resolve } from "node:path";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { loadHarnesses, loadHarness, harnessIds, detectHarnesses, sourceHarness, MANIFEST_DIR } from "./harness.mjs";
|
|
31
|
+
import { stamp, deriveState, parseProvenance, sourceHash, STALE, STALE_TEXT, FOREIGN_TEXT } from "./provenance.mjs";
|
|
32
|
+
import {
|
|
33
|
+
pluginRoot,
|
|
34
|
+
renderStatusLineLauncher,
|
|
35
|
+
statusLineIsOurWiring,
|
|
36
|
+
isPluginCacheRoot,
|
|
37
|
+
desiredStatusLineCommand,
|
|
38
|
+
findAgentsBlock,
|
|
39
|
+
agentsBlockVersion,
|
|
40
|
+
agentsBlockTemplatePath,
|
|
41
|
+
renderAgentsBlock,
|
|
42
|
+
loadLayout,
|
|
43
|
+
readConfigAt,
|
|
44
|
+
claudeHome,
|
|
45
|
+
whichOnPath,
|
|
46
|
+
installedPluginEntries,
|
|
47
|
+
pluginEnabled,
|
|
48
|
+
treeDigest,
|
|
49
|
+
packageDigest,
|
|
50
|
+
cmpPrecedence,
|
|
51
|
+
layoutPaths,
|
|
52
|
+
RUNTIME_GITIGNORE_HEADER,
|
|
53
|
+
LAUNCHER_HEADER,
|
|
54
|
+
} from "./lib.mjs";
|
|
55
|
+
import { analysePortableRegistration, portableRegistrationPaths, portablePayloadRoot } from "./portable-registration.mjs";
|
|
56
|
+
|
|
57
|
+
export const GENERATOR = "scripts/install-harness.mjs";
|
|
58
|
+
export const INSTALLED_REMEDY = "projectstore doctor reports this file when it is stale; run install again to refresh it.";
|
|
59
|
+
// The line every launcher has carried since the template was written; the
|
|
60
|
+
// recogniser for a pre-provenance launcher (contract 4, rung 1″). A substring
|
|
61
|
+
// of the template's own header, so a reworded comment cannot turn every
|
|
62
|
+
// existing install foreign.
|
|
63
|
+
export { LAUNCHER_HEADER };
|
|
64
|
+
|
|
65
|
+
export function readText(p) {
|
|
66
|
+
if (!existsSync(p)) return { present: false, text: null };
|
|
67
|
+
try {
|
|
68
|
+
return { present: true, text: readFileSync(p, "utf8") };
|
|
69
|
+
} catch {
|
|
70
|
+
return { present: true, text: null };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function pluginVersionAt(root, harness) {
|
|
75
|
+
const rel = harness?.version_file || sourceHarness()?.version_file || ".claude-plugin/plugin.json";
|
|
76
|
+
try {
|
|
77
|
+
return String(JSON.parse(readFileSync(join(root, rel), "utf8")).version || "");
|
|
78
|
+
} catch {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Is this text a file we wrote — stamped, or a pre-provenance launcher?
|
|
84
|
+
export function isOurFile(text) {
|
|
85
|
+
if (typeof text !== "string") return false;
|
|
86
|
+
return Boolean(parseProvenance(text)) || text.includes(LAUNCHER_HEADER);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── markdown-block: the ADR-002 block ─────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export function analyseBlock(projectDir, s, { root = pluginRoot(), manifestDir = MANIFEST_DIR } = {}) {
|
|
92
|
+
const files = s.files || ["AGENTS.md", "CLAUDE.md"];
|
|
93
|
+
const PREFERRED = files[0], FALLBACK = files[files.length - 1];
|
|
94
|
+
// LOOK across every file any manifest names; WRITE to one this harness can
|
|
95
|
+
// read. The two are different questions and conflating them is what let the
|
|
96
|
+
// block be created twice: a harness that scanned only its own list could not
|
|
97
|
+
// see the copy another harness had already written, so it created a second.
|
|
98
|
+
// Scanning is free and static — the union is manifest data, not project
|
|
99
|
+
// state — while the write target stays this manifest's own preference.
|
|
100
|
+
const scan = [...files, ...[...loadHarnesses(manifestDir).values()]
|
|
101
|
+
.flatMap((m) => m.surfaces?.agents_block?.files || [])
|
|
102
|
+
.filter((f) => !files.includes(f))];
|
|
103
|
+
const found = scan.map((f) => ({ file: f, path: join(projectDir, f), ...readText(join(projectDir, f)) }))
|
|
104
|
+
.map((e) => ({ ...e, block: e.text !== null ? findAgentsBlock(e.text) : null, own: files.includes(e.file) }));
|
|
105
|
+
const withBlock = found.filter((e) => e.block);
|
|
106
|
+
const preferred = found.find((e) => e.file === PREFERRED && e.present) || found.find((e) => e.file === FALLBACK);
|
|
107
|
+
const claude = found.find((e) => e.file === FALLBACK);
|
|
108
|
+
const importLine = `@${PREFERRED}`;
|
|
109
|
+
const a = { files: found, own: files, withBlock, preferred, claude, importLine, PREFERRED, FALLBACK, entryKey: "projectstore:agents", version: null, desired: null, current: null, state: "ours-absent", reason: null, refusal: null };
|
|
110
|
+
|
|
111
|
+
const unclosed = withBlock.find((e) => e.block.unclosed);
|
|
112
|
+
if (unclosed && unclosed.block.wrapped) return { ...a, state: "unparseable", refusal: `${unclosed.file}:${unclosed.block.line}: the projectstore:agents open marker does not close on its own line — the parser reads one line. Put \`-->\` back on the marker's line, then run /projectstore:agents register` };
|
|
113
|
+
if (unclosed) return { ...a, state: "unparseable", refusal: `${unclosed.file}: the block opens and never closes — restore the closing marker or remove the block by hand` };
|
|
114
|
+
const twice = withBlock.find((e) => e.block.count > 1);
|
|
115
|
+
if (twice) return { ...a, state: "unparseable", refusal: `${twice.file} carries the block more than once — keep exactly one` };
|
|
116
|
+
|
|
117
|
+
const tmpl = readText(agentsBlockTemplatePath(root));
|
|
118
|
+
if (!tmpl.present || tmpl.text === null) return { ...a, state: "unparseable", refusal: `the block's source is missing from the plugin at ${agentsBlockTemplatePath(root)}` };
|
|
119
|
+
a.version = agentsBlockVersion(tmpl.text);
|
|
120
|
+
const cfg = readConfigAt(projectDir);
|
|
121
|
+
let roster = null;
|
|
122
|
+
if (cfg && typeof cfg.layout === "string") {
|
|
123
|
+
try { roster = loadLayout(cfg.layout, root).agents || null; } catch { roster = null; }
|
|
124
|
+
}
|
|
125
|
+
a.desired = renderAgentsBlock(tmpl.text, roster);
|
|
126
|
+
a.current = withBlock.find((e) => e.file === preferred.file) || withBlock[0] || null;
|
|
127
|
+
a.duplicates = withBlock.filter((e) => e !== a.current);
|
|
128
|
+
if (!a.current) return { ...a, state: "ours-absent" };
|
|
129
|
+
// A block this harness CANNOT READ moves, and its target is created if it has
|
|
130
|
+
// to be: that is the whole difference between a second harness arriving and a
|
|
131
|
+
// preference. A block it can read moves only when the preferred file already
|
|
132
|
+
// exists — so a Claude-Code-only project keeps its block in CLAUDE.md and
|
|
133
|
+
// never acquires an AGENTS.md it did not ask for, while installing Codex into
|
|
134
|
+
// that same project moves the substance to the file Codex can read and leaves
|
|
135
|
+
// CLAUDE.md importing it.
|
|
136
|
+
if (!a.current.own) return { ...a, state: "ours-stale", reason: `in ${a.current.file}, which ${s.harness_display || "this harness"} does not read; install moves it to ${preferred.file}` };
|
|
137
|
+
if (a.current.file !== preferred.file && preferred.present) return { ...a, state: "ours-stale", reason: `in ${a.current.file}; install migrates it to ${preferred.file}` };
|
|
138
|
+
if (a.duplicates.length) return { ...a, state: "ours-stale", reason: `also in ${a.duplicates.map((e) => e.file).join(", ")}; install keeps the one in ${a.current.file}` };
|
|
139
|
+
if (a.current.block.v === a.version && a.current.block.block === a.desired) return { ...a, state: "ours-current" };
|
|
140
|
+
return { ...a, state: "ours-stale", reason: a.current.block.v !== a.version ? `v${a.current.block.v} → v${a.version}` : "content differs from the current source" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── json-entry: one entry in a JSON file the user co-owns ─────────────
|
|
144
|
+
|
|
145
|
+
// `renderRoot` (contract 4′, two phases): the root the surface is planned
|
|
146
|
+
// AGAINST — the host's install path a registration produces — when it differs
|
|
147
|
+
// from the root the templates are read from. Defaults to `root`.
|
|
148
|
+
export function analyseJsonEntry(projectDir, s, { root = pluginRoot(), home = homedir(), renderRoot = root } = {}) {
|
|
149
|
+
const path = join(projectDir, s.file);
|
|
150
|
+
const pointer = s.marker?.pointer || "statusLine.command";
|
|
151
|
+
const entryKey = pointer.split(".")[0];
|
|
152
|
+
const cur = readText(path);
|
|
153
|
+
const a = { path, entryKey, cur, settings: {}, curEntry: null, curCmd: null, ours: false, desired: null, state: "ours-absent", reason: null };
|
|
154
|
+
if (cur.present) {
|
|
155
|
+
if (cur.text === null) return { ...a, state: "unparseable", reason: "the file cannot be read" };
|
|
156
|
+
try { a.settings = JSON.parse(cur.text); } catch { return { ...a, state: "unparseable", reason: "the file is not valid JSON — nothing outside a marked entry is touched, so nothing is written" }; }
|
|
157
|
+
if (!a.settings || typeof a.settings !== "object" || Array.isArray(a.settings)) return { ...a, state: "unparseable", reason: "the file is not a JSON object" };
|
|
158
|
+
}
|
|
159
|
+
a.curEntry = a.settings[entryKey] ?? null;
|
|
160
|
+
a.curCmd = a.curEntry && typeof a.curEntry.command === "string" ? a.curEntry.command : null;
|
|
161
|
+
// "Did we write this?" is asked of the EXISTING entry, written by whatever
|
|
162
|
+
// root was current then: a dev checkout's direct wiring stays ours when the
|
|
163
|
+
// plan now renders against a registration's install path (reviewer, 2026-09-05).
|
|
164
|
+
a.ours = statusLineIsOurWiring(a.curCmd, projectDir, home, root) || statusLineIsOurWiring(a.curCmd, projectDir, home, renderRoot);
|
|
165
|
+
a.desired = desiredStatusLineCommand(projectDir, renderRoot, home).command;
|
|
166
|
+
if (!a.curEntry) return { ...a, state: "ours-absent" };
|
|
167
|
+
if (!a.ours) return { ...a, state: "theirs", reason: "a status line we did not write owns the slot" };
|
|
168
|
+
if (a.curCmd === a.desired) return { ...a, state: "ours-current" };
|
|
169
|
+
return { ...a, state: "ours-stale", reason: "the command no longer matches this installation" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── mjs: an exclusive, provenance-stamped file ────────────────────────
|
|
173
|
+
|
|
174
|
+
export function analyseStampedFile(projectDir, s, { root = pluginRoot(), home = homedir(), harness = null, renderRoot = root } = {}) {
|
|
175
|
+
const path = join(projectDir, s.file);
|
|
176
|
+
let file = readText(path);
|
|
177
|
+
// The manifest may name where an earlier release wrote this file
|
|
178
|
+
// (legacy_file, the layout ADR): read it when the current path is absent, so
|
|
179
|
+
// the file is classified and, once the new one is written, cleaned up.
|
|
180
|
+
const legacyPath = s.legacy_file ? join(projectDir, s.legacy_file) : null;
|
|
181
|
+
const atLegacy = !file.present && legacyPath && existsSync(legacyPath);
|
|
182
|
+
if (atLegacy) file = readText(legacyPath);
|
|
183
|
+
const a = { path, file, produced: true, ours: null, refusal: null, stamped: null, state: "absent", reason: null, writtenBy: null, sameProject: false, legacy: false, renderRoot, legacyPath: atLegacy ? legacyPath : null };
|
|
184
|
+
if (s.condition === "plugin_cache_install" && !isPluginCacheRoot(renderRoot, home)) {
|
|
185
|
+
// Not produced for this installation (a dev checkout is wired directly).
|
|
186
|
+
a.produced = false;
|
|
187
|
+
if (!file.present) return { ...a, state: "absent" };
|
|
188
|
+
a.ours = isOurFile(file.text);
|
|
189
|
+
return { ...a, state: a.ours ? "stale" : "foreign", reason: a.ours ? "not produced for this installation (a dev checkout is wired directly) — left in place; a prune of a launcher this root did not write needs the root that wrote it, or uninstall" : null };
|
|
190
|
+
}
|
|
191
|
+
const src = join(root, s.source);
|
|
192
|
+
const tpl = readText(src);
|
|
193
|
+
if (!tpl.present || tpl.text === null) return { ...a, state: "unparseable", refusal: `the source ${s.source} is missing from the plugin at ${root}` };
|
|
194
|
+
const rendered = renderStatusLineLauncher(tpl.text, renderRoot, projectDir);
|
|
195
|
+
if (rendered === null) return { ...a, state: "unparseable", refusal: "the launcher template is not one this installer knows how to fill" };
|
|
196
|
+
const pkg = pluginVersionAt(root, harness) || "0.0.0";
|
|
197
|
+
a.pkg = pkg;
|
|
198
|
+
a.stamped = stamp(rendered, { format: s.format, src: s.source, srcHash: sourceHash(tpl.text), pkg, project: projectDir, harness: harness?.id || sourceHarness()?.id || "harness", generator: GENERATOR, remedy: INSTALLED_REMEDY });
|
|
199
|
+
let st = deriveState({ file, sourceHash: sourceHash(tpl.text), pkg, renderNowHash: a.stamped.render, project: projectDir });
|
|
200
|
+
// Contract 4, rung 1″: a launcher written before provenance existed carries
|
|
201
|
+
// no line but the template's own header. It is ours, stale, replaceable.
|
|
202
|
+
if (st.state === "foreign" && typeof file.text === "string" && file.text.includes(LAUNCHER_HEADER)) { st = { ...st, state: "stale", reason: STALE.PLUGIN }; a.legacy = true; }
|
|
203
|
+
a.installedPkg = st.provenance?.pkg || null;
|
|
204
|
+
return { ...a, state: st.state, reason: st.reason ? STALE_TEXT[st.reason] + (a.legacy ? " (pre-provenance file)" : "") : null, writtenBy: st.writtenBy, sameProject: st.sameProject };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── host-plugin-registration: our marketplace directory + the host's registry ──
|
|
208
|
+
//
|
|
209
|
+
// Contract 4′ (2026-09-05): a total, ordered function over registry facts —
|
|
210
|
+
// our directory under the harness home (owned as a whole, recognised by the
|
|
211
|
+
// provenance field in its manifest), the host's marketplace and plugin
|
|
212
|
+
// registries, the project's settings, and PATH. Reads only; the host binary is
|
|
213
|
+
// located, never run.
|
|
214
|
+
|
|
215
|
+
function samePath(a, b) {
|
|
216
|
+
const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
217
|
+
return Boolean(a && b) && real(a) === real(b);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readJson(p) {
|
|
221
|
+
try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function registrationPaths(s, { home = homedir(), projectDir = null, harness = null } = {}) {
|
|
225
|
+
const hh = claudeHome(home);
|
|
226
|
+
const dir = join(hh, ...s.dir);
|
|
227
|
+
const reg = s.registry || {};
|
|
228
|
+
const cfgDir = harness?.runtime?.harness_dir || ".claude";
|
|
229
|
+
return {
|
|
230
|
+
dir,
|
|
231
|
+
manifest: join(dir, s.manifest),
|
|
232
|
+
payload: join(dir, s.plugin_subdir),
|
|
233
|
+
installed: join(hh, ...(reg.installed || ["plugins", "installed_plugins.json"])),
|
|
234
|
+
marketplaces: join(hh, ...(reg.marketplaces || ["plugins", "known_marketplaces.json"])),
|
|
235
|
+
cacheDir: join(hh, ...(reg.cache_dir || ["plugins", "cache"])),
|
|
236
|
+
projectSettings: projectDir ? join(projectDir, cfgDir, ...(reg.project_settings || ["settings.json"])) : null,
|
|
237
|
+
userSettings: join(hh, ...(reg.user_settings || ["settings.json"])),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function analyseRegistration(projectDir, s, { root = pluginRoot(), home = homedir(), harness = null, env = process.env } = {}) {
|
|
242
|
+
const id = `${s.plugin_name}@${s.marketplace_name}`;
|
|
243
|
+
const paths = registrationPaths(s, { home, projectDir, harness });
|
|
244
|
+
const pkg = pluginVersionAt(root, harness) || "0.0.0";
|
|
245
|
+
const a = { id, paths, path: paths.dir, entryKey: id, pkg, bin: whichOnPath(s.cli?.bin || "claude", env), produced: !(s.condition === "npm_package_root" && isPluginCacheRoot(root, home)),
|
|
246
|
+
dir: { present: existsSync(paths.dir), manifest: null, prov: null, pkg: null, disabled: [], digestOk: null }, known: null, registeredHere: null, installed: null, installedVersion: null, installPath: null,
|
|
247
|
+
enabled: null, others: [], otherProjects: 0, writtenBy: null, newer: false, state: "absent", reason: null, refusal: null };
|
|
248
|
+
|
|
249
|
+
// Machine-global facts: our directory, the host's marketplace registry, every row of ours.
|
|
250
|
+
if (a.dir.present) {
|
|
251
|
+
a.dir.manifest = readJson(paths.manifest);
|
|
252
|
+
const prov = a.dir.manifest && a.dir.manifest[s.provenance_key];
|
|
253
|
+
if (prov && typeof prov === "object" && typeof prov.pkg === "string") {
|
|
254
|
+
a.dir.prov = prov; a.dir.pkg = prov.pkg;
|
|
255
|
+
a.dir.disabled = Array.isArray(prov.disabled) ? prov.disabled.filter((x) => typeof x === "string") : [];
|
|
256
|
+
if (typeof prov.project === "string" && !samePath(prov.project, projectDir)) a.writtenBy = prov.project;
|
|
257
|
+
// The payload digest: a copy that did not finish, or a hand edit, is not current.
|
|
258
|
+
if (prov.digest && typeof prov.digest.sha256 === "string") {
|
|
259
|
+
try { const d = treeDigest(paths.payload); a.dir.digestOk = d.count === prov.digest.count && d.sha256 === prov.digest.sha256; } catch { a.dir.digestOk = false; }
|
|
260
|
+
} else a.dir.digestOk = null; // no digest recorded: nothing to compare
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const known = readJson(paths.marketplaces);
|
|
264
|
+
const mp = known && known[s.marketplace_name];
|
|
265
|
+
a.known = mp ? { path: (mp.installLocation || mp.source?.path || null), source: mp.source?.source || null } : null;
|
|
266
|
+
const all = installedPluginEntries(home, projectDir).filter((e) => e.key === id);
|
|
267
|
+
// Per-project facts: the checkout's own settings file and the registry row carrying its path.
|
|
268
|
+
const local = readJson(paths.projectSettings);
|
|
269
|
+
const here = local && local[s.registry?.known_pointer || "extraKnownMarketplaces"] && local[s.registry?.known_pointer || "extraKnownMarketplaces"][s.marketplace_name];
|
|
270
|
+
a.registeredHere = here ? { path: here.source?.path || null } : null;
|
|
271
|
+
const enabledHere = (local && local[s.registry?.enabled_pointer || "enabledPlugins"]) || {};
|
|
272
|
+
a.disabledHere = Object.keys(enabledHere).filter((k) => enabledHere[k] === false);
|
|
273
|
+
const mine = all.filter((e) => e.projectPath && samePath(e.projectPath, projectDir));
|
|
274
|
+
a.otherProjects = all.filter((e) => !e.projectPath || !samePath(e.projectPath, projectDir)).length;
|
|
275
|
+
const row = mine.find((e) => e.present) || mine[0] || null;
|
|
276
|
+
a.installed = row ? { path: row.path, version: row.version, present: row.present, scope: row.scope } : null;
|
|
277
|
+
a.installPath = row ? row.path : null;
|
|
278
|
+
a.installedVersion = row ? row.version : null;
|
|
279
|
+
a.enabled = pluginEnabled(id, home, projectDir);
|
|
280
|
+
// Competing registrations of the same plugin, enabled for this project.
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
for (const e of installedPluginEntries(home, projectDir)) {
|
|
283
|
+
if (e.key === id || seen.has(e.key) || !e.present || e.enabled === false) continue;
|
|
284
|
+
if (e.projectPath && !samePath(e.projectPath, projectDir)) continue;
|
|
285
|
+
seen.add(e.key); a.others.push({ key: e.key, path: e.path, version: e.version });
|
|
286
|
+
}
|
|
287
|
+
a.newer = Boolean(a.dir.pkg) && cmpPrecedence(a.dir.pkg, pkg) > 0;
|
|
288
|
+
// Same version, different payload: the directory's recorded digest against
|
|
289
|
+
// this package's — contract 4's rung 3 (source changed) for a directory.
|
|
290
|
+
a.contentDiffers = null;
|
|
291
|
+
if (a.dir.prov && a.dir.pkg === pkg && a.dir.prov.digest && typeof a.dir.prov.digest.sha256 === "string") {
|
|
292
|
+
try { const d = packageDigest(root); a.contentDiffers = d.sha256 !== a.dir.prov.digest.sha256 || d.count !== a.dir.prov.digest.count; } catch { a.contentDiffers = null; }
|
|
293
|
+
}
|
|
294
|
+
// The version the host would install from the directory as it will stand after this run.
|
|
295
|
+
a.targetPkg = a.newer ? a.dir.pkg : pkg;
|
|
296
|
+
a.predictedInstallPath = join(paths.cacheDir, s.marketplace_name, s.plugin_name, a.targetPkg);
|
|
297
|
+
const nothingOfOursHere = !a.registeredHere && !mine.length;
|
|
298
|
+
const nothingOfOurs = !a.dir.present && !a.known && !all.length && nothingOfOursHere;
|
|
299
|
+
const bin = s.cli?.bin || "claude";
|
|
300
|
+
|
|
301
|
+
// The ladder (contract 4′).
|
|
302
|
+
if (!a.bin && nothingOfOurs) return { ...a, state: "unavailable", reason: `\`${bin}\` is not on PATH — the registration needs the host's CLI; the other surfaces still install` };
|
|
303
|
+
if (a.dir.present && !a.dir.prov) return { ...a, state: "foreign", refusal: `${paths.dir} exists and its ${s.manifest} carries no \`${s.provenance_key}\` field of ours — a directory we did not write sits at our path; move it if it is yours, or delete it to let install take the name` };
|
|
304
|
+
if (a.known && a.known.path && !samePath(a.known.path, paths.dir)) return { ...a, state: "foreign", refusal: `the host's registry names marketplace \`${s.marketplace_name}\` at ${a.known.path}, not at ${paths.dir} — a registration we did not make holds our name; \`${bin} plugin marketplace remove ${s.marketplace_name}\` lets install take it` };
|
|
305
|
+
if (a.registeredHere && a.registeredHere.path && !samePath(a.registeredHere.path, paths.dir)) return { ...a, state: "foreign", refusal: `${paths.projectSettings} declares marketplace \`${s.marketplace_name}\` at ${a.registeredHere.path}, not at ${paths.dir} — remove that entry to let install take the name` };
|
|
306
|
+
if (nothingOfOurs) return { ...a, state: "absent" };
|
|
307
|
+
// The directory is shared by every checkout on the machine; a project that never registered is absent, whatever the directory holds.
|
|
308
|
+
if (nothingOfOursHere) return { ...a, state: "absent", reason: a.dir.present ? `the marketplace directory is present${a.writtenBy ? ` (written from ${a.writtenBy})` : ""}; this checkout is not registered` : null };
|
|
309
|
+
if (!a.dir.present) return { ...a, state: "stale", reason: STALE_TEXT[STALE.PLUGIN] + " (the directory is gone)" };
|
|
310
|
+
if (cmpPrecedence(a.dir.pkg, pkg) < 0) return { ...a, state: "stale", reason: STALE_TEXT[STALE.PLUGIN] + ` (directory at ${a.dir.pkg}, package at ${pkg})` };
|
|
311
|
+
if (a.dir.digestOk === false) return { ...a, state: "stale", reason: "edited by hand or half-written (the payload does not match the digest its manifest carries)" };
|
|
312
|
+
if (a.contentDiffers === true) return { ...a, state: "stale", reason: STALE_TEXT[STALE.PLUGIN] + ` (same version ${pkg}, different content — the directory's digest is not this package's)` };
|
|
313
|
+
if (!a.known || !a.registeredHere) return { ...a, state: "stale", reason: STALE_TEXT[STALE.CONFIG] + (a.known ? " (this checkout does not declare the marketplace)" : " (the host does not know the marketplace)") };
|
|
314
|
+
if (!a.installed || !a.installed.present) return { ...a, state: "stale", reason: "not installed" + (a.installed ? " (the host's install path is gone)" : " (registered, not installed for this checkout)") };
|
|
315
|
+
if (a.installedVersion !== a.dir.pkg) return { ...a, state: "stale", reason: STALE_TEXT[STALE.PLUGIN] + ` (installed ${a.installedVersion}, directory ${a.dir.pkg})` };
|
|
316
|
+
if (!a.enabled) return { ...a, state: "stale", reason: "disabled for this checkout" };
|
|
317
|
+
return { ...a, state: "current", reason: a.newer ? `the directory is at ${a.dir.pkg}, newer than this package (${pkg})${a.writtenBy ? `, written from ${a.writtenBy}` : ""} — not downgraded` : null };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── the project-level layout: legacy / new / both (the layout ADR) ─────
|
|
321
|
+
//
|
|
322
|
+
// Which of the two layouts a project holds, file by file — what the `layout`
|
|
323
|
+
// migration item and doctor's layout-legacy read (layout spec, contracts 6–7).
|
|
324
|
+
// The legacy runtime directory is ours when its nested .gitignore carries the
|
|
325
|
+
// header every release since 0.6 wrote; a directory without it is left alone.
|
|
326
|
+
export function analyseLayout(projectDir, { harness = null } = {}) {
|
|
327
|
+
const p = layoutPaths(projectDir, { harnessDir: harness?.runtime?.harness_dir || null });
|
|
328
|
+
const has = (f) => existsSync(f);
|
|
329
|
+
const legacy = {
|
|
330
|
+
dir: p.legacy.dir,
|
|
331
|
+
binding: has(p.legacy.binding), runtime: has(p.legacy.runtime), state: has(p.legacy.state), launcher: has(p.legacy.launcher),
|
|
332
|
+
entryLog: has(p.legacy.entryLog), welcomed: has(p.legacy.welcomed), sessionId: has(p.legacy.sessionId),
|
|
333
|
+
runtimeOurs: false, stateFiles: [],
|
|
334
|
+
};
|
|
335
|
+
if (legacy.runtime) {
|
|
336
|
+
try { legacy.runtimeOurs = readFileSync(p.legacy.gitignore, "utf8").includes(RUNTIME_GITIGNORE_HEADER); } catch { legacy.runtimeOurs = false; }
|
|
337
|
+
if (legacy.state) { try { legacy.stateFiles = readdirSync(p.legacy.state).filter((n) => n !== ".gitignore"); } catch {} }
|
|
338
|
+
}
|
|
339
|
+
const current = { root: has(p.root), binding: has(p.binding) };
|
|
340
|
+
const any = legacy.binding || legacy.runtime || legacy.welcomed || legacy.sessionId;
|
|
341
|
+
// Two bindings whose only difference is the legacy agents block are an
|
|
342
|
+
// interrupted move (the new file written, the old not yet removed): resumable.
|
|
343
|
+
let resumable = false;
|
|
344
|
+
if (legacy.binding && current.binding) {
|
|
345
|
+
try {
|
|
346
|
+
const { agents, ...oldRest } = JSON.parse(readFileSync(p.legacy.binding, "utf8"));
|
|
347
|
+
const cur = JSON.parse(readFileSync(p.binding, "utf8"));
|
|
348
|
+
resumable = JSON.stringify(oldRest) === JSON.stringify(cur);
|
|
349
|
+
} catch { resumable = false; }
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
paths: p, legacy, current, resumable,
|
|
353
|
+
twoConfigs: legacy.binding && current.binding,
|
|
354
|
+
pending: any,
|
|
355
|
+
state: !any ? "current" : (legacy.binding && current.binding) ? "both" : legacy.binding ? "legacy" : "partial",
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── every surface, for doctor ─────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
export { analysePortableRegistration, portableRegistrationPaths };
|
|
362
|
+
const ANALYSERS = { "markdown-block": analyseBlock, "json-entry": analyseJsonEntry, "mjs": analyseStampedFile, "host-plugin-registration": analyseRegistration, "portable-plugin-registration": analysePortableRegistration };
|
|
363
|
+
|
|
364
|
+
// The states of every non-host, supported surface of every harness this
|
|
365
|
+
// project uses — detected by directory, or carrying a file of ours (contract
|
|
366
|
+
// 16). Host-managed rows have nothing to derive (contract 14).
|
|
367
|
+
export function surfaceStates(projectDir, { home = homedir(), root = pluginRoot(), manifestDir = MANIFEST_DIR, harnesses = null, env = process.env } = {}) {
|
|
368
|
+
projectDir = resolve(projectDir);
|
|
369
|
+
const detected = detectHarnesses(projectDir, { dir: manifestDir }).map((d) => d.id);
|
|
370
|
+
const out = { projectDir, detected, used: [], installable: harnessIds(manifestDir), states: [] };
|
|
371
|
+
for (const m of loadHarnesses(manifestDir).values()) {
|
|
372
|
+
if (harnesses && !harnesses.includes(m.id)) continue;
|
|
373
|
+
// A registration conditioned on a distribution root has no state to read
|
|
374
|
+
// from a run that has none — the installer defers it before reading any,
|
|
375
|
+
// and so does this (S2, the 2026-10-03 review): reporting it from the
|
|
376
|
+
// core printed "<core> is not a portable plugin root" into every project
|
|
377
|
+
// that merely contained the harness's directory.
|
|
378
|
+
const rows = Object.entries(m.surfaces || {}).filter(([k, s]) => !k.startsWith("_") && s.kind !== "host" && s.supported !== false && ANALYSERS[s.format]
|
|
379
|
+
&& !(s.condition === "distribution_root" && !portablePayloadRoot(s, { root, env })));
|
|
380
|
+
const states = [];
|
|
381
|
+
for (const [key, s] of rows) {
|
|
382
|
+
const a = ANALYSERS[s.format](projectDir, s, { root, home, harness: m, env });
|
|
383
|
+
const entry = s.kind === "shared" ? (a.entryKey || s.marker?.pointer || null) : null;
|
|
384
|
+
const path = a.legacyPath || a.path || (a.current ? a.current.path : (a.preferred ? a.preferred.path : join(projectDir, s.file || "")));
|
|
385
|
+
const row = { harness: m.id, surface: key, kind: s.kind, path, entry, state: a.state, reason: a.reason || a.refusal || null, writtenBy: a.writtenBy || null, sameProject: Boolean(a.sameProject), produced: a.produced !== false, legacy: Boolean(a.legacy), installedPkg: a.installedPkg || null, present: a.file ? a.file.present : (a.current ? true : (a.curEntry ? true : false)) };
|
|
386
|
+
if (s.kind === "registration") Object.assign(row, {
|
|
387
|
+
entry: a.id,
|
|
388
|
+
present: Boolean(a.dir?.present || a.ownership || a.known || a.market || a.installed),
|
|
389
|
+
produced: a.produced,
|
|
390
|
+
pkg: a.pkg || a.desiredVersion || null,
|
|
391
|
+
dirPkg: a.dir?.pkg || a.ownership?.version || null,
|
|
392
|
+
installedVersion: a.installedVersion,
|
|
393
|
+
installPath: a.installPath,
|
|
394
|
+
enabled: a.enabled,
|
|
395
|
+
others: a.others || [],
|
|
396
|
+
otherProjects: a.otherProjects || 0,
|
|
397
|
+
writtenBy: a.writtenBy || null,
|
|
398
|
+
newer: Boolean(a.newer),
|
|
399
|
+
bin: a.bin,
|
|
400
|
+
// What this registration silenced for the checkout and the checkout
|
|
401
|
+
// still holds off: exactly the set uninstall re-enables (contract 13).
|
|
402
|
+
silenced: (a.dir?.disabled || []).filter((k) => (a.disabledHere || []).includes(k)),
|
|
403
|
+
});
|
|
404
|
+
states.push(row);
|
|
405
|
+
}
|
|
406
|
+
// A registration is the host's to load and never counts as a file of ours
|
|
407
|
+
// (contract 16): it decides nothing about whether the harness is in use.
|
|
408
|
+
const hasOurs = states.some((x) => (x.kind === "exclusive" && ["current", "stale"].includes(x.state)) || (x.kind === "shared" && ["ours-current", "ours-stale"].includes(x.state)));
|
|
409
|
+
if (detected.includes(m.id) || hasOurs) {
|
|
410
|
+
out.used.push(m.id);
|
|
411
|
+
// The agents block is deliberately shared across harnesses. Its presence
|
|
412
|
+
// must not advertise an unavailable global registration for every other
|
|
413
|
+
// harness on the machine; a registration joins doctor only when that
|
|
414
|
+
// harness is detected in the project or the registration actually exists.
|
|
415
|
+
out.states.push(...states.filter((x) => x.kind !== "registration" || detected.includes(m.id) || x.present));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export { FOREIGN_TEXT };
|