projectstore-claude 0.28.0-rc.3 โ 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/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/README.md +40 -20
- package/node_modules/projectstore/commands/agents.md +7 -1
- package/node_modules/projectstore/commands/doctor.md +10 -3
- package/node_modules/projectstore/docs/harnesses.md +38 -24
- package/node_modules/projectstore/harnesses/claude-code.json +1 -1
- package/node_modules/projectstore/harnesses/codex.json +4 -3
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/doctor.mjs +146 -31
- package/node_modules/projectstore/scripts/harness.mjs +48 -16
- package/node_modules/projectstore/scripts/install-harness.mjs +18 -10
- package/node_modules/projectstore/scripts/lib.mjs +54 -0
- package/node_modules/projectstore/scripts/surfaces.mjs +6 -3
- package/package.json +2 -2
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"name": "projectstore",
|
|
13
13
|
"displayName": "projectstore",
|
|
14
14
|
"description": "๐ Your agent runs the project through a verified loop: task โ artifact (ADR ยท spec ยท epic ยท story) โ adversarial critic โ backlog โ planner โ reviewer โ done. Plain markdown in an Obsidian-friendly vault, every write approved by you โ and any model can pick the project up tomorrow.",
|
|
15
|
-
"version": "0.28.
|
|
15
|
+
"version": "0.28.1",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "Evgenii Konev",
|
|
18
18
|
"email": "ekonev@smartandpoint.com",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
3
|
"displayName": "projectstore",
|
|
4
|
-
"version": "0.28.
|
|
4
|
+
"version": "0.28.1",
|
|
5
5
|
"description": "Your agent runs the project through a verified loop: task โ artifact (ADR / spec / epic / story) โ adversarial critic โ backlog โ planner โ reviewer โ done. Plain markdown in git โ any model can pick the project up tomorrow.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Evgenii Konev @ SmartAndPoint",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/SmartAndPoint/ProjectStore/releases) [](./LICENSE) [](https://github.com/SmartAndPoint/ProjectStore/stargazers)
|
|
6
6
|
|
|
7
|
-
A project workflow plugin for [Claude Code](https://claude.com/claude-code) and [OpenAI Codex](https://developers.openai.com/codex/) (experimental).
|
|
7
|
+
A project workflow plugin for [Claude Code](https://claude.com/claude-code) and [OpenAI Codex](https://developers.openai.com/codex/) (experimental). Both are released together at one version, and each installs with one command.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -66,6 +66,14 @@ Open Claude Code in your project and say:
|
|
|
66
66
|
|
|
67
67
|
That's the whole setup. Claude adds the marketplace, installs the plugin, and walks you through binding a vault, scaffolding it and wiring the status line โ every step previewed, nothing written without your Yes.
|
|
68
68
|
|
|
69
|
+
**On Codex**, run one command from a terminal in your project:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
npx projectstore-codex install --project "$PWD"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then restart Codex, approve the ProjectStore hooks when it asks, and run `$projectstore-bind ~/Documents/my-project-vault`. Codex support is newer and still labelled experimental: [`docs/harnesses.md`](./docs/harnesses.md#codex) says what has been verified and what has not. A project can be worked from both agents: they share the binding in `.projectstore/`, and each keeps its own overlay and session state beside it ([running both over one project](./docs/harnesses.md#running-both-over-one-project)).
|
|
76
|
+
|
|
69
77
|
<details>
|
|
70
78
|
<summary>Prefer to type it yourself?</summary>
|
|
71
79
|
|
|
@@ -88,28 +96,26 @@ npx projectstore-claude install --project "$PWD"
|
|
|
88
96
|
|
|
89
97
|
The same tree is published to npm as [`projectstore`](https://www.npmjs.com/package/projectstore) โ one source package carrying every harness's manifest โ and `projectstore-claude` is its Claude Code shell: the core pinned at the same version and bundled inside, the harness fixed, so the one command has the same shape on every harness. It registers the plugin with Claude Code: it writes a small local marketplace of its own under your Claude home, then drives `claude plugin marketplace add` / `plugin install` **at local scope**, so the registration lands in this checkout's `.claude/settings.local.json` and nowhere else. Every host command is printed before it runs; naming the harness is the confirmation. Restart Claude Code afterwards. A git-marketplace copy already enabled for the checkout is silenced there (not globally) so the plugin does not load twice; `uninstall` turns it back on. Pin or upgrade with `npx projectstore-claude@<version> upgrade --project "$PWD"` โ the version you name is the version you run. The core's low-level form, `npx projectstore <verb> --harness claude-code โฆ`, is exactly what the shell runs. bun works the same on the packed bin.
|
|
90
98
|
|
|
91
|
-
**Codex
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
**Codex has its own shell with the same one-command shape:**
|
|
100
|
+
`npx projectstore-codex install --project "$PWD"`. It carries a canonical
|
|
101
|
+
portable manifest, namespaced workflow and role skills, lifecycle hooks, and
|
|
102
|
+
the exact bundled core. It stages a stable marketplace under `CODEX_HOME`,
|
|
103
|
+
drives `codex plugin marketplace add` and `codex plugin add`, then reads the
|
|
104
|
+
installation back and verifies its version and payload digest. Restart Codex,
|
|
105
|
+
approve the hooks, and run `$projectstore-bind <vault-path>`. For later
|
|
106
|
+
releases, `npx projectstore-codex@<version> upgrade --project "$PWD"`. Because
|
|
107
|
+
Codex's plugin registry is user-global, ordinary uninstall removes only the
|
|
108
|
+
project's agents block; `uninstall --global` is the explicit machine-wide
|
|
109
|
+
removal. Codex is **experimental** until a live session has exercised every
|
|
110
|
+
surface it installs; [`docs/harnesses.md`](./docs/harnesses.md) says what has
|
|
111
|
+
been measured and what has not. From a checkout, the same npx path runs against
|
|
112
|
+
a built tarball:
|
|
95
113
|
|
|
96
114
|
```sh
|
|
97
115
|
npm run shells:build -- --only projectstore-codex --dev --out dist
|
|
98
116
|
npx --package "./dist/projectstore-codex-$(node -p 'require("./package.json").version').tgz" projectstore-codex install --project "$PWD"
|
|
99
117
|
```
|
|
100
118
|
|
|
101
|
-
The Codex shell carries a canonical portable manifest, namespaced workflow and
|
|
102
|
-
role skills, lifecycle hooks, and the exact bundled core. It stages a stable
|
|
103
|
-
marketplace under `CODEX_HOME`, drives `codex plugin marketplace add` and
|
|
104
|
-
`codex plugin add`, then reads the installation back and verifies its version
|
|
105
|
-
and payload digest. Restart Codex, approve the hooks, and run
|
|
106
|
-
`$projectstore-bind <vault-path>`. After the first explicit npm publication,
|
|
107
|
-
the shorter command is
|
|
108
|
-
`npx projectstore-codex@<version> install --project "$PWD"`; use its `upgrade`
|
|
109
|
-
verb for later releases. Because Codex's
|
|
110
|
-
plugin registry is user-global, ordinary uninstall removes only the project's
|
|
111
|
-
agents block; `uninstall --global` is the explicit machine-wide removal.
|
|
112
|
-
|
|
113
119
|
The package also carries a `bin`. Without a session โ in CI, or in a shell โ the same core answers token-free, with a `--json` envelope on every verb:
|
|
114
120
|
|
|
115
121
|
```
|
|
@@ -141,7 +147,7 @@ npx projectstore bind ~/vaults/my-project
|
|
|
141
147
|
npx projectstore init ~/vaults/new-project --language ru
|
|
142
148
|
```
|
|
143
149
|
|
|
144
|
-
`projectstore-claude`, `projectstore-codex` and `projectstore-opencode` are this package's per-harness shells โ the core pinned and bundled, the harness fixed. Codex's
|
|
150
|
+
`projectstore-claude`, `projectstore-codex` and `projectstore-opencode` are this package's per-harness shells โ the core pinned and bundled, the harness fixed. The Claude Code and Codex shells are published at the core's version; Codex stays labelled experimental until a live run has exercised every surface it installs ([`docs/harnesses.md`](./docs/harnesses.md)). The opencode shell publishes after its plugin root is rendered. The other `projectstore-*` names are reserved placeholders pointing back here. One source package, one version, N tarballs.
|
|
145
151
|
</details>
|
|
146
152
|
|
|
147
153
|
## Upgrading
|
|
@@ -168,9 +174,14 @@ bound before 0.28. What an existing project sees afterwards, and why:
|
|
|
168
174
|
`npx projectstore-claude upgrade` on a git-marketplace install does more: it
|
|
169
175
|
also registers the plugin from npm for this checkout and turns the
|
|
170
176
|
git-marketplace copy off here, so `/plugin update` stops reaching the
|
|
171
|
-
checkout.
|
|
177
|
+
checkout. The 0.28.0-rc.1 and rc.2 startup lines named that shell form
|
|
178
|
+
(`npx projectstore-claude@<version> upgrade`, without `--no-register`), so on
|
|
179
|
+
those, update first and run what the new startup line names. If it already
|
|
180
|
+
happened,
|
|
172
181
|
`npx projectstore-claude@<version> uninstall --surface plugin --project "$PWD"`
|
|
173
|
-
turns the git-marketplace copy back on.
|
|
182
|
+
(0.28.0 or later) turns the git-marketplace copy back on. That copy must be 0.28 or later โ an
|
|
183
|
+
older one reads a moved project as unbound โ so update it first if it is not.
|
|
184
|
+
Restart, then run
|
|
174
185
|
`/projectstore:doctor --fix` in the new session, which re-stamps the status
|
|
175
186
|
line against that copy.
|
|
176
187
|
- **The status line keeps rendering.** A launcher written by an earlier version
|
|
@@ -197,6 +208,15 @@ bound before 0.28. What an existing project sees afterwards, and why:
|
|
|
197
208
|
(`projectstore-decision-detector`, `projectstore-peer-reviewer`,
|
|
198
209
|
`projectstore-story-completion`, `projectstore-vault-communication`). Nothing
|
|
199
210
|
in a project names them; only a skill listing shows the new names.
|
|
211
|
+
- **Rolling back** to 0.27.x before the move: everything keeps working, since
|
|
212
|
+
0.27.x still reads the old layout. It rewrites the status-line launcher in its
|
|
213
|
+
own form, as it always did; coming forward again, the startup line names the
|
|
214
|
+
move once more, and the move re-stamps the launcher. If a session already
|
|
215
|
+
re-stamped the status line or re-registered the agents block before the move
|
|
216
|
+
(rc.2's `/projectstore:doctor --fix` did both), 0.27.x reports a foreign
|
|
217
|
+
status line and a v4 block and leaves both alone โ unless you run its
|
|
218
|
+
`/projectstore:agents register`, which rewrites the block; the status line
|
|
219
|
+
still renders, and the move settles both.
|
|
200
220
|
- **Rolling back** to 0.27.x after the move: 0.27.x looks for its binding under
|
|
201
221
|
`.claude/`, finds none and offers `bind`. Do not accept. A re-bind writes
|
|
202
222
|
`.claude/projectstore.json` again, and 0.28's `install` and `upgrade` refuse
|
|
@@ -28,7 +28,13 @@ subcommand (`.projectstore/projectstore.json`; else point to `/projectstore:bind
|
|
|
28
28
|
prose byte-identical.
|
|
29
29
|
3. A non-zero exit is a refusal โ a duplicated or unclosed block, a missing
|
|
30
30
|
template โ relay it and stop. Never write the block with the Write or Edit
|
|
31
|
-
tool: the verb is the only writer (install spec, contract 6).
|
|
31
|
+
tool: the verb is the only writer (install spec, contract 6). One exception,
|
|
32
|
+
read from the output, not assumed: when it shows the block applied and the
|
|
33
|
+
`layout` item skipped as deferred to a terminal outside the session, the
|
|
34
|
+
exit 1 is that deferral. The block is registered โ say so, and for the move
|
|
35
|
+
relay the command the startup line or `/projectstore:doctor`'s
|
|
36
|
+
`layout-legacy` finding names; never compose one. Any other non-zero exit
|
|
37
|
+
is a refusal.
|
|
32
38
|
|
|
33
39
|
## `unregister` โ remove what register added
|
|
34
40
|
|
|
@@ -25,6 +25,8 @@ You are running projectstore diagnostics (ADR-005: umbrella doctor).
|
|
|
25
25
|
is in the report, it goes first and its command is the one repair** for a
|
|
26
26
|
stale-launcher `surface`, a v3 `agents-block` and `agents-in-binding` as
|
|
27
27
|
well: relay it and run nothing in-session for those (see its bullet below).
|
|
28
|
+
The report already shows the v3 block, and a block Claude Code cannot see,
|
|
29
|
+
as info that points at the move.
|
|
28
30
|
- `worktree-unbound` โ this checkout is a git worktree of a bound one. Offer
|
|
29
31
|
`/projectstore:bind --inherit`, and say what it does: copies the parent's
|
|
30
32
|
binding, leaves the vault shared and unchanged, carries no session state.
|
|
@@ -35,7 +37,10 @@ You are running projectstore diagnostics (ADR-005: umbrella doctor).
|
|
|
35
37
|
- `agents-block` duplicate or stale โ show the finding, then (after approval)
|
|
36
38
|
run `node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" install --harness claude-code --surface agents_block --project "${CLAUDE_PROJECT_DIR}"`
|
|
37
39
|
and print its output: it removes the copy in the non-preferred file and
|
|
38
|
-
keeps the preferred one current.
|
|
40
|
+
keeps the preferred one current. A block Claude Code cannot see โ in
|
|
41
|
+
`AGENTS.md`, with no `@AGENTS.md` line in `CLAUDE.md` โ is the same repair:
|
|
42
|
+
the verb adds the import. For another harness the finding names its own
|
|
43
|
+
`--harness`; relay that command. Never Edit or Write the block yourself โ
|
|
39
44
|
the verb is its only writer (install spec, contract 6).
|
|
40
45
|
- `statusline` issues โ offer running `/projectstore:statusline on|off`,
|
|
41
46
|
which installs or removes the entry and the launcher behind a preview
|
|
@@ -74,8 +79,10 @@ You are running projectstore diagnostics (ADR-005: umbrella doctor).
|
|
|
74
79
|
git-marketplace install or a checkout, the `projectstore-claude` shell for
|
|
75
80
|
the npm registration. Relay the finding's command verbatim and never
|
|
76
81
|
substitute the other form โ the shell, run for a git-marketplace install,
|
|
77
|
-
would also move this checkout to the npm channel.
|
|
78
|
-
|
|
82
|
+
would also move this checkout to the npm channel. When the finding carries
|
|
83
|
+
advice instead of a command โ the installed copy predates the move, or
|
|
84
|
+
predates `--no-register` โ relay the advice and never compose a command
|
|
85
|
+
yourself. Never move the files yourself. While this finding is in the report, its command is also the one
|
|
79
86
|
repair for a stale-launcher `surface`, a v3 `agents-block` and
|
|
80
87
|
`agents-in-binding`: do not run the in-session `install` or `upgrade` for
|
|
81
88
|
those โ the deferred move makes that run stop part-way.
|
|
@@ -23,8 +23,10 @@ and the measurements in the manifest came from that run.
|
|
|
23
23
|
**experimental** means `verified` is `null`. Every field that was not measured
|
|
24
24
|
is absent or `null` rather than guessed. It is enough to run on; it is not enough
|
|
25
25
|
to promise. An experimental harness is never the source layout. A generated
|
|
26
|
-
adapter may exist while it is experimental
|
|
27
|
-
|
|
26
|
+
adapter may exist while it is experimental. Its distribution shell may
|
|
27
|
+
publish before the live run that sets `verified`, but only by the maintainer's
|
|
28
|
+
decision (Codex's does, from 0.28.1); the label stays experimental until that
|
|
29
|
+
run.
|
|
28
30
|
|
|
29
31
|
The label is not prose. It is derived from the manifest, and
|
|
30
32
|
`tests/portability.test.mjs` fails if this table and `verified` disagree.
|
|
@@ -45,38 +47,50 @@ model names and those are harness-specific โ is
|
|
|
45
47
|
|
|
46
48
|
Experimental, measured on `codex-cli 0.153.4`. The initial spike captured 759
|
|
47
49
|
hook firings. The 2026-09-30 gate then built the npm shell from a packed core,
|
|
48
|
-
passed
|
|
50
|
+
passed Codex's plugin validator, installed and upgraded it through an isolated
|
|
49
51
|
`CODEX_HOME`, verified the materialised cache by version and digest, and loaded
|
|
50
|
-
|
|
52
|
+
the `projectstore-status` skill in a fresh Codex session. The validator (the
|
|
53
|
+
plugin-creator skill's `validate_plugin.py`) reads only
|
|
54
|
+
`.codex-plugin/plugin.json`. The canonical root `plugin.json` was validated
|
|
55
|
+
separately on 2026-10-04, against the schema it declares (Agent Plugins 1.0.0).
|
|
51
56
|
|
|
52
57
|
That run exercised one skill, not every installed surface, so `verified` stays
|
|
53
58
|
`null`. The hooks are the reason it matters. On 2026-10-03 the first real
|
|
54
|
-
install's cached hooks could not load the vault in any session. They were run
|
|
55
|
-
hand through `zsh -lc
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
install's cached hooks could not load the vault in any session. They were run
|
|
60
|
+
by hand through `zsh -lc`; the failure did not depend on the shell form. In the
|
|
61
|
+
first session the failure sat in the model's context under the welcome, which
|
|
62
|
+
was all the user saw. The core had taken the shell's root for its own. That is
|
|
63
|
+
fixed, and the suite now runs every rendered hook from the built shell. A live
|
|
64
|
+
Codex session firing them from an installed release is still owed.
|
|
65
|
+
|
|
66
|
+
How 0.153.4 starts a hook was read from its source, not measured: under the
|
|
67
|
+
session's shell as `<shell> -c`, with the environment the Codex process had when
|
|
68
|
+
it built the session's hooks. `$SHELL -lc` (or `/bin/sh -lc`) is the fallback
|
|
69
|
+
when the hooks are built without exactly one ready local environment. So a hook
|
|
70
|
+
finds `node` on the Codex process's own `PATH`, plus whatever `.zshenv` adds:
|
|
71
|
+
zsh reads `.zshenv` for a `-c` command, not `.zprofile` or `.zshrc`, so a Codex
|
|
72
|
+
not started from a terminal can miss a `node` that only `.zprofile` puts on
|
|
73
|
+
`PATH`.
|
|
74
|
+
|
|
75
|
+
The shell is published as `projectstore-codex` from 0.28.1, ahead of that live
|
|
76
|
+
run, by the maintainer's decision:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
npx projectstore-codex install --project "$PWD"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For later releases, `npx projectstore-codex@<version> upgrade --project "$PWD"`.
|
|
83
|
+
Installation is user-global because Codex stores marketplace registrations and
|
|
84
|
+
plugin caches in `CODEX_HOME`; the agents block in `AGENTS.md` remains
|
|
85
|
+
project-local. Ordinary uninstall leaves the global plugin in place; add
|
|
86
|
+
`--global` only when you mean to remove it for every project. From this
|
|
87
|
+
checkout, the same flow runs against a built tarball:
|
|
65
88
|
|
|
66
89
|
```sh
|
|
67
90
|
npm run shells:build -- --only projectstore-codex --dev --out dist
|
|
68
91
|
npx --package "./dist/projectstore-codex-$(node -p 'require("./package.json").version').tgz" projectstore-codex install --project "$PWD"
|
|
69
92
|
```
|
|
70
93
|
|
|
71
|
-
The shell package is release-gated until its first npm publication. From this
|
|
72
|
-
repository, the same flow is exercised against the packed tarball. Installation
|
|
73
|
-
is user-global because Codex stores marketplace registrations and plugin caches
|
|
74
|
-
in `CODEX_HOME`; the agents block in `AGENTS.md` remains project-local. After
|
|
75
|
-
the first explicit npm publication, the shorter registry form is
|
|
76
|
-
`npx projectstore-codex@<version> install --project "$PWD"` (or `upgrade`). Ordinary
|
|
77
|
-
uninstall leaves the global plugin in place; add `--global` only when you mean
|
|
78
|
-
to remove it for every project.
|
|
79
|
-
|
|
80
94
|
What is known, and how:
|
|
81
95
|
|
|
82
96
|
- **Hooks are rendered; their live firing from an installed release is not
|
|
@@ -472,7 +472,7 @@
|
|
|
472
472
|
"",
|
|
473
473
|
"Without it, you would run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#upgrading for details.",
|
|
474
474
|
"",
|
|
475
|
-
"**Upgrading from 0.27.x to 0.28**: restart after the update, then move the project's files to `.projectstore/` with the one command the first session names, run from a terminal outside the session: for a git-marketplace install, the installed copy's own `bin/projectstore.mjs upgrade --harness claude-code --no-register --project <dir>`, which leaves the registration as it is; for the npm registration, `npx projectstore-claude@<version> upgrade --no-register --project <dir>`. The same run re-stamps the status-line launcher, and until then every reader falls back to the old paths. `status` and `search` answer from artifact facts; `doctor` gains surface, version-drift and mcp lines and exits 1 on findings; the plugin registers an MCP server the host may ask you to approve once.
|
|
475
|
+
"**Upgrading from 0.27.x to 0.28**: restart after the update, then move the project's files to `.projectstore/` with the one command the first session names, run from a terminal outside the session: for a git-marketplace install, the installed copy's own `bin/projectstore.mjs upgrade --harness claude-code --no-register --project <dir>`, which leaves the registration as it is; for the npm registration, `npx projectstore-claude@<version> upgrade --no-register --project <dir>`. The same run re-stamps the status-line launcher, and until then every reader falls back to the old paths. `status` and `search` answer from artifact facts; `doctor` gains surface, version-drift and mcp lines and exits 1 on findings; the plugin registers an MCP server the host may ask you to approve once. On 0.28.0-rc.1 or rc.2, update first: their startup line named the shell form without `--no-register` (`npx projectstore-claude@<version> upgrade`), which also switches a git-marketplace install to npm for the checkout (the README's Upgrading section has the way back). Before the move, a rollback to 0.27.x keeps working and the move is the way forward again; after the move, 0.27.x reads the project as unbound."
|
|
476
476
|
],
|
|
477
477
|
"agent_translation": {
|
|
478
478
|
"carries_model": true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Harness capability manifest โ Codex. The committed adapters/codex tree renders the source commands, roles, passive skills and hooks into Codex vocabulary. Measurements began with 759 captured hook firings on 2026-09-07/08; the complete built shell, isolated marketplace install, upgrade, validator and fresh-session skill load passed on codex-cli 0.153.4 on 2026-09-30.",
|
|
2
|
+
"_comment": "Harness capability manifest โ Codex. The committed adapters/codex tree renders the source commands, roles, passive skills and hooks into Codex vocabulary. Measurements began with 759 captured hook firings on 2026-09-07/08; the complete built shell, isolated marketplace install, upgrade, validator and fresh-session skill load passed on codex-cli 0.153.4 on 2026-09-30. The validator reads only .codex-plugin/plugin.json; the canonical root plugin.json was validated separately on 2026-10-04 against the schema it declares (Agent Plugins 1.0.0).",
|
|
3
3
|
"id": "codex",
|
|
4
4
|
"display_name": "Codex",
|
|
5
5
|
"emit": true,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"output_dir": "adapters/codex",
|
|
8
8
|
"docs": "https://developers.openai.com/codex/",
|
|
9
9
|
"verified": null,
|
|
10
|
-
"verified_reason": "Null, restored 2026-10-03. docs/harnesses.md calls a harness supported only when every installed surface has been exercised end to end, and contract 16 of the generation spec (Generated harness surfaces: manifests, the generator and the three invariants) derives that label from this field. The 2026-09-30 run (session 01a0f079-7ecd-7331-95a9-2471def3cb22) exercised one surface: a fresh `codex exec` session loaded projectstore-status from the materialised cache. The hooks were not exercised, and the first real install showed why that matters. Its cached hooks, run by hand through `zsh -lc` (
|
|
10
|
+
"verified_reason": "Null, restored 2026-10-03. docs/harnesses.md calls a harness supported only when every installed surface has been exercised end to end, and contract 16 of the generation spec (Generated harness surfaces: manifests, the generator and the three invariants) derives that label from this field. The 2026-09-30 run (session 01a0f079-7ecd-7331-95a9-2471def3cb22) exercised one surface: a fresh `codex exec` session loaded projectstore-status from the materialised cache. The hooks were not exercised, and the first real install showed why that matters. Its cached hooks, run by hand through `zsh -lc` (a form the failure did not depend on: read from source, 0.153.4 runs a hook as `<session shell> -c` with the Codex process's environment, and `$SHELL -lc` only as a fallback), could not load the vault in any session; in the first, the failure sat beneath the welcome. The core had taken the shell's root for its own. That is fixed and pinned by a test that runs every rendered hook from the built shell. This block is set by a live run that exercises every installed surface: the hooks once registration has settled, including PreCompact, and the skill flows. The shell is published from 0.28.1, ahead of that run, by the maintainer's decision of 2026-10-04; publication does not set this block.",
|
|
11
11
|
"runtime": {
|
|
12
12
|
"detect_env": [
|
|
13
13
|
"PLUGIN_ROOT",
|
|
@@ -290,7 +290,8 @@
|
|
|
290
290
|
"rewrites": [],
|
|
291
291
|
"install": {
|
|
292
292
|
"mechanism": "the package's portable plugin root, registered through its distribution shell and Codex's own marketplace CLI",
|
|
293
|
-
"
|
|
293
|
+
"shell": "projectstore-codex",
|
|
294
|
+
"shell_reason": "The built `projectstore-codex` shell carries the rendered Codex skills and hooks plus an exact bundled core, fixes --harness codex, stages a stable marketplace under CODEX_HOME, and asks Codex itself to install it. It is published from 0.28.1, by the maintainer's decision of 2026-10-04. Until then this key was absent, so generated guidance could not send users to the reserved 0.0.1 placeholder; the core alone is not Codex's plugin root and cannot register it.",
|
|
294
295
|
"why_not_scripted": "The shell does not edit Codex's registry or cache. It atomically stages the plugin source, previews the host commands, then runs `codex plugin marketplace add` and `codex plugin add`; a read-back verifies the installed version and payload digest. The global plugin survives ordinary project uninstall; `uninstall --global` is the explicit destructive form.",
|
|
295
296
|
"steps": [
|
|
296
297
|
"npx projectstore-codex install --project \"$PWD\"",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Your agent runs the project through a verified loop: task โ artifact (ADR / spec / epic / story) โ adversarial critic โ backlog โ planner โ reviewer โ done. Plain markdown in git โ any model can pick the project up tomorrow.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"project-management",
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
lastVaultActivityMs,
|
|
61
61
|
ENTRY_IGNORE,
|
|
62
62
|
AGENTS_BLOCK_OPEN_SRC,
|
|
63
|
+
AGENTS_BLOCK_CLOSE,
|
|
63
64
|
agentsBlockVersion,
|
|
64
65
|
findAgentsBlock,
|
|
65
66
|
statusLineLauncherPath,
|
|
@@ -75,8 +76,11 @@ import {
|
|
|
75
76
|
hostSettingsPath,
|
|
76
77
|
readOverlayAt, layoutRoster,
|
|
77
78
|
installChannel,
|
|
79
|
+
cmpPrecedence,
|
|
80
|
+
cmpVersion,
|
|
81
|
+
blockVisibleTo,
|
|
78
82
|
} from "./lib.mjs";
|
|
79
|
-
import { agentOverrides, childEnv, sourceHarness, runtimeEnvNames, loadHarness, detectHarnesses, configPath as harnessConfigPath, packageCommand } from "./harness.mjs";
|
|
83
|
+
import { agentOverrides, childEnv, sourceHarness, runtimeEnvNames, loadHarness, detectHarnesses, identifiedHarnessId, configPath as harnessConfigPath, packageCommand } from "./harness.mjs";
|
|
80
84
|
|
|
81
85
|
// A remedy used to interpolate the surface's harness variable here. It cannot:
|
|
82
86
|
// measured 2026-09-06, NO harness gives its Bash tool that variable, and a
|
|
@@ -387,9 +391,14 @@ export function checkPendingUpgrade(proj, home = homedir(), root = pluginRoot())
|
|
|
387
391
|
relative(proj, lp))];
|
|
388
392
|
}
|
|
389
393
|
|
|
390
|
-
|
|
394
|
+
// The two findings the layout move itself repairs carry what they say without
|
|
395
|
+
// their remedy, so a pending move can name itself instead (foldIntoMove).
|
|
396
|
+
const moveRepairs = (f, fact) => ({ ...f, byMove: fact });
|
|
397
|
+
|
|
398
|
+
export function checkAgentsBlock(proj, { env = process.env, root = pluginRoot() } = {}) {
|
|
391
399
|
const out = [];
|
|
392
400
|
const AGENT_BLOCK_VERSION = agentsBlockVersion();
|
|
401
|
+
const texts = {};
|
|
393
402
|
// One parser for every reader (findAgentsBlock): its count is the loose one,
|
|
394
403
|
// so a good block plus a re-wrapped marker in one file is "more than once"
|
|
395
404
|
// here exactly as install and uninstall see it (both refuse), never a quiet
|
|
@@ -397,6 +406,7 @@ export function checkAgentsBlock(proj) {
|
|
|
397
406
|
// registered" is the reading that makes install append a second block.
|
|
398
407
|
let blocks = 0;
|
|
399
408
|
let wrappedFiles = 0;
|
|
409
|
+
let unclosedFiles = 0;
|
|
400
410
|
const perFile = {};
|
|
401
411
|
const staleVersions = [];
|
|
402
412
|
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
@@ -404,6 +414,7 @@ export function checkAgentsBlock(proj) {
|
|
|
404
414
|
if (!existsSync(p)) continue;
|
|
405
415
|
let text;
|
|
406
416
|
try { text = readFileSync(p, "utf8"); } catch { continue; }
|
|
417
|
+
texts[name] = text;
|
|
407
418
|
const f = findAgentsBlock(text);
|
|
408
419
|
if (!f) continue;
|
|
409
420
|
perFile[name] = f.count;
|
|
@@ -414,6 +425,13 @@ export function checkAgentsBlock(proj) {
|
|
|
414
425
|
`${name}:${f.line}: the projectstore:agents open marker does not close on its own line โ put \`-->\` back on the marker's line, then run /projectstore:agents register (install and uninstall refuse until it does).`, name));
|
|
415
426
|
continue;
|
|
416
427
|
}
|
|
428
|
+
if (f.unclosed) {
|
|
429
|
+
// Named here as the wrapped marker is, so the startup count carries it:
|
|
430
|
+
// the agents-block plan refuses it, and the layout move with it.
|
|
431
|
+
unclosedFiles++;
|
|
432
|
+
out.push(finding("install", "issue", "agents-block",
|
|
433
|
+
`${name}: the projectstore:agents block opens and never closes โ close it with \`${AGENTS_BLOCK_CLOSE}\` or delete the half block, then run /projectstore:agents register (install and uninstall refuse until then).`, name));
|
|
434
|
+
}
|
|
417
435
|
for (const m of text.matchAll(AGENT_BLOCK_MARKER)) {
|
|
418
436
|
const v = parseInt(m[1], 10);
|
|
419
437
|
if (v !== AGENT_BLOCK_VERSION) staleVersions.push({ file: name, v });
|
|
@@ -427,25 +445,75 @@ export function checkAgentsBlock(proj) {
|
|
|
427
445
|
// One block in each file is a state install resolves (it keeps the
|
|
428
446
|
// preferred file's); two in one file is not, and stays an issue โ and a
|
|
429
447
|
// wrapped marker anywhere means install refuses, so the "both files"
|
|
430
|
-
// advice is withheld while one is wrapped.
|
|
448
|
+
// advice is withheld while one is wrapped or never closes.
|
|
431
449
|
const twiceInOne = Object.entries(perFile).find(([, n]) => n > 1);
|
|
432
450
|
if (twiceInOne) {
|
|
433
451
|
out.push(finding("install", "issue", "agents-block",
|
|
434
452
|
`${twiceInOne[0]} carries the projectstore:agents block ${twiceInOne[1]} times โ keep exactly one; install refuses until it does.`, twiceInOne[0]));
|
|
435
|
-
} else if (wrappedFiles) {
|
|
453
|
+
} else if (wrappedFiles || unclosedFiles) {
|
|
436
454
|
// already named above, file by file
|
|
437
455
|
} else {
|
|
438
456
|
out.push(finding("install", "warn", "agents-block",
|
|
439
457
|
`The projectstore:agents block is in both CLAUDE.md and AGENTS.md โ run /projectstore:agents register: install keeps the one in ${(sourceHarness()?.surfaces?.agents_block?.files || ["AGENTS.md"])[0]} and removes the other.`));
|
|
440
458
|
}
|
|
441
459
|
}
|
|
460
|
+
// A state the agents-block plan refuses โ a wrapped marker, a block that
|
|
461
|
+
// never closes, a block twice in one file โ refuses the move with it, so
|
|
462
|
+
// nothing here is the move's to repair while one stands.
|
|
463
|
+
const refuses = wrappedFiles > 0 || unclosedFiles > 0 || Object.values(perFile).some((n) => n > 1);
|
|
442
464
|
for (const s of staleVersions) {
|
|
443
|
-
|
|
444
|
-
|
|
465
|
+
const fact = `Agents block in ${s.file} is v${s.v}, expected v${AGENT_BLOCK_VERSION}`;
|
|
466
|
+
const f = finding("install", "issue", "agents-block", `${fact} โ re-run /projectstore:agents register.`, s.file);
|
|
467
|
+
out.push(refuses ? f : moveRepairs(f, fact));
|
|
468
|
+
}
|
|
469
|
+
// Placement, held to the predicate install plans from (the install spec,
|
|
470
|
+
// contract 6 as amended after the rc.3 tag): one well-formed block, seen by
|
|
471
|
+
// every harness the project uses. rc.1 and rc.2 left a block in an
|
|
472
|
+
// AGENTS.md-only project with no CLAUDE.md, so Claude Code saw nothing, and
|
|
473
|
+
// nothing said so. Used means detected by directory, or identified from the
|
|
474
|
+
// environment โ never the source harness a terminal run falls back to, so a
|
|
475
|
+
// Codex-only project hears nothing about CLAUDE.md (contract 16). An issue
|
|
476
|
+
// for the harness that identified itself, a warning for one only detected.
|
|
477
|
+
const blockFile = blocks === 1 && !refuses ? Object.keys(perFile)[0] : null;
|
|
478
|
+
if (blockFile) {
|
|
479
|
+
const identified = identifiedHarnessId(env);
|
|
480
|
+
const used = new Set([...detectHarnesses(proj).map((d) => d.id), ...(identified ? [identified] : [])]);
|
|
481
|
+
for (const id of used) {
|
|
482
|
+
const m = loadHarness(id);
|
|
483
|
+
if (!m || blockVisibleTo(m, blockFile, texts)) continue;
|
|
484
|
+
const ab = m.surfaces.agents_block;
|
|
485
|
+
const why = (ab.files || []).includes(blockFile)
|
|
486
|
+
? `${ab.reads_natively} does not import it (\`@${blockFile}\`)`
|
|
487
|
+
: `it reads ${(ab.files || []).join(" and ")} only`;
|
|
488
|
+
const fact = `The projectstore:agents block is in ${blockFile}, which ${m.display_name} does not see: ${why}`;
|
|
489
|
+
// The resolved-root form the surface remedy uses: the running copy's own bin.
|
|
490
|
+
const remedy = `install the block for ${m.display_name}: node "${join(root, "bin", "projectstore.mjs")}" install --harness ${id} --surface agents_block --project "${proj}"`;
|
|
491
|
+
const f = finding("install", id === identified ? "issue" : "warn", "agents-block", `${fact} โ ${remedy}.`, blockFile);
|
|
492
|
+
// The layout's harness is the one the move's command installs for, so the
|
|
493
|
+
// move plans this import; another harness's is not the move's to repair.
|
|
494
|
+
out.push(id === sourceHarness()?.id ? moveRepairs(f, fact) : f);
|
|
495
|
+
}
|
|
445
496
|
}
|
|
446
497
|
return out;
|
|
447
498
|
}
|
|
448
499
|
|
|
500
|
+
// While the layout move is pending, the findings the move itself repairs fold
|
|
501
|
+
// into it instead of being counted beside it (the layout spec, contract 7 as
|
|
502
|
+
// amended after the rc.3 tag; the upgrade story's O1): the stale block, which
|
|
503
|
+
// the move re-registers, and the block's invisibility to the layout's
|
|
504
|
+
// harness, whose import the move plans. Nothing else folds โ a block twice in
|
|
505
|
+
// one file, one that never closes or a wrapped marker makes the agents-block
|
|
506
|
+
// plan refuse, which blocks the move itself. The message points at the layout
|
|
507
|
+
// finding rather than computing its command again. The tag is internal: no
|
|
508
|
+
// finding leaves here carrying it.
|
|
509
|
+
export function foldIntoMove(findings) {
|
|
510
|
+
const layout = findings.find((f) => f.check === "layout-legacy" || f.check === "layout-two-configs");
|
|
511
|
+
const step = layout && (layout.check === "layout-two-configs"
|
|
512
|
+
? "delete the binding the layout-two-configs finding names, then run the layout move, which repairs this"
|
|
513
|
+
: "the layout move repairs this: run what the layout-legacy finding names");
|
|
514
|
+
return findings.map(({ byMove, ...f }) => (byMove && layout ? { ...f, level: "info", message: `${byMove} โ ${step}.` } : f));
|
|
515
|
+
}
|
|
516
|
+
|
|
449
517
|
// Both scopes are walked. The original reason ("project > user > plugin, so a
|
|
450
518
|
// copy in either scope shadows the bundle") turned out to be wrong โ a copy
|
|
451
519
|
// shadows NOTHING, because plugin agents register under a scoped id and copies
|
|
@@ -509,8 +577,10 @@ export async function checkHarnessSurfaces(_cfg, proj, { home = homedir(), root
|
|
|
509
577
|
}
|
|
510
578
|
} else if (s.surface === "agents_block") {
|
|
511
579
|
// Version drift and duplicates are checkAgentsBlock's; what only the
|
|
512
|
-
// state knows is content that differs at the same version
|
|
513
|
-
//
|
|
580
|
+
// state knows is content that differs at the same version. A block that
|
|
581
|
+
// never closes is named by both: checkAgentsBlock carries it to the
|
|
582
|
+
// startup line, and the state names it with the file's own reason, as a
|
|
583
|
+
// wrapped marker already was.
|
|
514
584
|
if (s.state === "unparseable") {
|
|
515
585
|
out.push(finding("install", "issue", "surface", `${where} โ ${s.reason}`, where));
|
|
516
586
|
} else if (s.state === "ours-stale" && /content differs|migrates/.test(s.reason || "")) {
|
|
@@ -590,13 +660,38 @@ export function checkOverlays(cfg, proj, { root = pluginRoot(), home = homedir()
|
|
|
590
660
|
// itself); a competing copy enabled beside ours โ an issue (two enabled copies
|
|
591
661
|
// of one plugin); a competitor alone โ an info naming the npm path; foreign โ
|
|
592
662
|
// never repairable; the host CLI missing โ an info.
|
|
593
|
-
export function checkPluginRegistration(proj, states = []) {
|
|
663
|
+
export function checkPluginRegistration(proj, states = [], { home = homedir() } = {}) {
|
|
594
664
|
const out = [];
|
|
595
665
|
for (const s of states.filter((x) => x.kind === "registration")) {
|
|
596
666
|
// The shell form when the manifest names a shell (contract 12): the
|
|
597
667
|
// command a user can paste, built in one place.
|
|
598
668
|
const h = loadHarness(s.harness) || { id: s.harness };
|
|
599
669
|
const refresh = packageCommand(h, "upgrade", { version: s.pkg || "latest", args: `--surface ${s.surface} --project "${proj}"` });
|
|
670
|
+
// A copy this registration silenced for the checkout and the checkout
|
|
671
|
+
// still holds off, one per key (the install spec, contract 13 as amended
|
|
672
|
+
// after the rc.3 tag): rc.1 and rc.2's startup offer left exactly this on
|
|
673
|
+
// git-marketplace projects, and nothing said so. Only for a registration
|
|
674
|
+
// this checkout holds; the record lives in our directory, so a directory
|
|
675
|
+
// removed by hand takes it along and leaves nothing to name.
|
|
676
|
+
if ((s.silenced || []).length && (s.state === "current" || s.state === "stale")) {
|
|
677
|
+
// The row this checkout loads: its own local-scope row first, then a
|
|
678
|
+
// user-scope one โ never another checkout's (layoutRemedy's rule).
|
|
679
|
+
const real = (x) => { try { return realpathSync.native(x); } catch { return resolve(x); } };
|
|
680
|
+
const here = real(proj);
|
|
681
|
+
const rows = installedPluginEntries(home, proj).filter((e) => e.present && (!e.projectPath || real(e.projectPath) === here));
|
|
682
|
+
for (const key of s.silenced) {
|
|
683
|
+
const row = rows.filter((e) => e.key === key).sort((a, b) => Number(Boolean(b.projectPath)) - Number(Boolean(a.projectPath)))[0];
|
|
684
|
+
if (!row) {
|
|
685
|
+
out.push(finding("install", "info", "plugin-registration", `${key} is held off in this checkout's local settings, where the npm registration turned it off โ and that copy is no longer installed, so the entry is stale.`, s.path));
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
// The release line, not the build: a 0.28 release candidate reads a
|
|
689
|
+
// moved project; 0.27.x reads it as unbound. No version reads as old.
|
|
690
|
+
const old = !row.version || cmpVersion(row.version, "0.28.0") < 0;
|
|
691
|
+
out.push(finding("install", "info", "plugin-registration",
|
|
692
|
+
`${key} (${row.version || "no version recorded"}) is off for this checkout: the npm registration turned it off when it registered, so the checkout no longer loads that copy and a /plugin update no longer reaches the project. ${old ? "Update that copy first โ 0.27.x reads a moved project as unbound. " : ""}To go back to it: from a terminal outside the session, ${packageCommand(h, "uninstall", { version: s.pkg || "latest", args: `--surface ${s.surface} --project "${proj}"` })}, restart, then /projectstore:doctor --fix. If moving to npm was meant, ignore this.`, s.path));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
600
695
|
const others = (s.others || []).map((o) => `${o.key} (${o.version || "?"})`).join(", ");
|
|
601
696
|
if (s.state === "foreign") {
|
|
602
697
|
out.push(finding("install", "issue", "plugin-registration-foreign", `${s.reason} โ install, uninstall and upgrade refuse it; nothing repairs it.`, s.path));
|
|
@@ -770,6 +865,21 @@ export function checkLayout(proj, harness = sourceHarness(), { level = "warn", r
|
|
|
770
865
|
// the move never needs the registration, so a misread channel can cost a
|
|
771
866
|
// launcher stamp but never a channel switch. A copy that predates the move
|
|
772
867
|
// (no bin/projectstore.mjs โ 0.27.x) cannot run it: update that copy first.
|
|
868
|
+
// Nor can one whose CLI predates --no-register (takesNoRegister).
|
|
869
|
+
//
|
|
870
|
+
// Whether a registry copy can run the command named for it. `--no-register`
|
|
871
|
+
// arrived in 0.28.0-rc.3, and rc.1 and rc.2 parse strictly, so the command
|
|
872
|
+
// exits 2 on the flag and writes nothing (the layout spec, contract 12 as
|
|
873
|
+
// amended after the rc.3 tag). Read from the copy's own parse table, never
|
|
874
|
+
// from its version: a never-published 0.28.0 build ranks above rc.3 and lacks
|
|
875
|
+
// the flag, while a copy labelled rc.2 taken from main at 418448e has it. A
|
|
876
|
+
// file read, because this runs in the SessionStart subset: never an import()
|
|
877
|
+
// of the copy, never a spawn. Add files to the read, never drop cli.mjs:
|
|
878
|
+
// released copies keep their parse there.
|
|
879
|
+
export function takesNoRegister(copy) {
|
|
880
|
+
try { return readFileSync(join(copy, "scripts", "cli.mjs"), "utf8").includes('"no-register"'); } catch { return false; }
|
|
881
|
+
}
|
|
882
|
+
|
|
773
883
|
export function layoutRemedy(proj, { root = pluginRoot(), home = homedir(), harness = sourceHarness(), env = process.env } = {}) {
|
|
774
884
|
// A session under a relocated host home hands it on: a terminal without it
|
|
775
885
|
// would classify the copy as a checkout, render no launcher and never clear
|
|
@@ -790,10 +900,22 @@ export function layoutRemedy(proj, { root = pluginRoot(), home = homedir(), harn
|
|
|
790
900
|
.filter((e) => e.present && e.enabled && (!e.projectPath || mine(e)))
|
|
791
901
|
.sort((a, b) => (Number(mine(b)) - Number(mine(a))) || (b.at - a.at))[0];
|
|
792
902
|
if (copy) {
|
|
903
|
+
const v = copy.version ? ` (${copy.version})` : "";
|
|
793
904
|
if (!existsSync(join(copy.path, "bin", "projectstore.mjs"))) {
|
|
794
|
-
return { advice: `This project's projectstore plugin${
|
|
905
|
+
return { advice: `This project's projectstore plugin${v} predates the move and cannot run it: update it first (in Claude Code: /plugin marketplace update, then /plugin update, then restart), and the startup line names the command` };
|
|
906
|
+
}
|
|
907
|
+
// Chosen first, gated after: the copy is what the project runs, so an
|
|
908
|
+
// incapable one gets advice even beside a capable user-scope copy.
|
|
909
|
+
const viaRegistration = installChannel(copy.path, { home, harness }) === "registration";
|
|
910
|
+
if (!takesNoRegister(copy.path)) {
|
|
911
|
+
return { advice: viaRegistration
|
|
912
|
+
// Its channel's ordinary refresh, at the running version: the plain
|
|
913
|
+
// upgrade re-registers from npm โ not a switch for an npm project โ and
|
|
914
|
+
// moves the project in the same run.
|
|
915
|
+
? `This project's projectstore plugin${v} is the npm registration's copy and predates --no-register, so it cannot run the move as named: refresh that registration, which moves the project too, from a terminal outside the session: ${prefix}${packageCommand(harness, "upgrade", { version: pluginVersion(root) || "latest", args: `--project "${proj}"` })}`
|
|
916
|
+
: `This project's projectstore plugin${v} predates --no-register and cannot run the move as named: update it first (in Claude Code: /plugin marketplace update, then /plugin update, then restart), and the startup line names the command` };
|
|
795
917
|
}
|
|
796
|
-
return
|
|
918
|
+
return viaRegistration ? shell(copy.version) : own(copy.path);
|
|
797
919
|
}
|
|
798
920
|
return channel === "package" ? shell(pluginVersion(root)) : own(root);
|
|
799
921
|
}
|
|
@@ -865,16 +987,6 @@ export function checkVaultGit(cfg) {
|
|
|
865
987
|
"Vault is not a git repository โ the knowledge has no history/blame/review. Consider `git init` (doctor --fix offers it).")];
|
|
866
988
|
}
|
|
867
989
|
|
|
868
|
-
function versionNewer(a, b) {
|
|
869
|
-
const pa = String(a).split(".").map(Number);
|
|
870
|
-
const pb = String(b).split(".").map(Number);
|
|
871
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
872
|
-
const x = pa[i] || 0, y = pb[i] || 0;
|
|
873
|
-
if (x !== y) return x > y;
|
|
874
|
-
}
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
990
|
// Marketplace auto-update (maintainer request 2026-07-03): third-party
|
|
879
991
|
// marketplaces do NOT auto-update by default, so a stale plugin looks like
|
|
880
992
|
// "the feature is broken". Read the real registries and, when the flag is
|
|
@@ -948,7 +1060,8 @@ export function checkAutoUpdate(home = homedir()) {
|
|
|
948
1060
|
const latest = (catalog.plugins || []).find((p) => p.name === name)?.version;
|
|
949
1061
|
const running = (named && named.version)
|
|
950
1062
|
|| JSON.parse(readFileSync(join(root, ".claude-plugin", "plugin.json"), "utf8")).version;
|
|
951
|
-
|
|
1063
|
+
// Precedence, not the triple: a session on 0.28.0-rc.3 hears that 0.28.0 is out.
|
|
1064
|
+
if (latest && running && cmpPrecedence(latest, running) > 0) {
|
|
952
1065
|
out.push(finding("install", "warn", "auto-update",
|
|
953
1066
|
`A newer ${name} is available: v${latest} (running v${running}) โ run /plugin marketplace update ${marketplace}, then /reload-plugins.`));
|
|
954
1067
|
}
|
|
@@ -1852,7 +1965,7 @@ export async function runInstallChecks(cfg, proj, opts = {}) {
|
|
|
1852
1965
|
...checkLayoutTemplates(cfg),
|
|
1853
1966
|
...checkHooksAlive(cfg),
|
|
1854
1967
|
...checkStatusline(cfg, proj),
|
|
1855
|
-
...checkAgentsBlock(proj),
|
|
1968
|
+
...checkAgentsBlock(proj, { env: opts.env, root: opts.root }),
|
|
1856
1969
|
...checkOverrideCopies(proj),
|
|
1857
1970
|
...checkEnvModel(),
|
|
1858
1971
|
...checkEnvEffort(),
|
|
@@ -1867,9 +1980,9 @@ export async function runInstallChecks(cfg, proj, opts = {}) {
|
|
|
1867
1980
|
let read = null;
|
|
1868
1981
|
try { read = await readSurfaceStates(proj, opts); } catch {} // reported as a warn by checkHarnessSurfaces
|
|
1869
1982
|
out.push(...await checkHarnessSurfaces(cfg, proj, { ...opts, read }));
|
|
1870
|
-
if (read) out.push(...checkPluginRegistration(proj, read.result.states));
|
|
1983
|
+
if (read) out.push(...checkPluginRegistration(proj, read.result.states, { home: opts.home }));
|
|
1871
1984
|
if (read) out.push(...checkVersionDrift(opts.home, read.result.states, proj));
|
|
1872
|
-
return out;
|
|
1985
|
+
return foldIntoMove(out);
|
|
1873
1986
|
}
|
|
1874
1987
|
|
|
1875
1988
|
export function runVaultChecks(cfg) {
|
|
@@ -1932,24 +2045,26 @@ export function runStartupChecks(cfg, proj, budgetMs = 150) {
|
|
|
1932
2045
|
];
|
|
1933
2046
|
const findings = [];
|
|
1934
2047
|
for (const step of steps) {
|
|
1935
|
-
if (Date.now() - started > budgetMs) return { skipped: true, count: 0, findings };
|
|
2048
|
+
if (Date.now() - started > budgetMs) return { skipped: true, count: 0, findings: foldIntoMove(findings) };
|
|
1936
2049
|
try { findings.push(...step()); } catch {}
|
|
1937
2050
|
}
|
|
2051
|
+
// The findings the move repairs are not counted beside it (foldIntoMove).
|
|
2052
|
+
const settled = foldIntoMove(findings);
|
|
1938
2053
|
// While the move is pending, the re-stamp is not a step of its own: the move
|
|
1939
2054
|
// re-stamps the launcher at its new path (the layout spec, contract 7 as
|
|
1940
2055
|
// amended 2026-10-03), and an in-session `doctor --fix` would write that
|
|
1941
2056
|
// launcher and then stop at the deferred move with exit 1. So the line names
|
|
1942
2057
|
// one step, and says what it covers when the dropped offer would have fired.
|
|
1943
|
-
const movePending =
|
|
1944
|
-
const restamp =
|
|
1945
|
-
const offers =
|
|
2058
|
+
const movePending = settled.some((f) => f.check === "layout-legacy" || f.check === "layout-two-configs");
|
|
2059
|
+
const restamp = settled.some((f) => f.level === "info" && f.check === "upgrade");
|
|
2060
|
+
const offers = settled
|
|
1946
2061
|
.filter((f) => f.level === "info" && OFFER_CHECKS.has(f.check) && !(movePending && f.check === "upgrade"))
|
|
1947
2062
|
.map((f) => (movePending && restamp && f.check === "layout-legacy" ? `${f.message} The same run re-stamps the status line launcher.` : f.message));
|
|
1948
2063
|
return {
|
|
1949
2064
|
skipped: false,
|
|
1950
|
-
count:
|
|
2065
|
+
count: settled.filter((f) => f.level === "issue").length,
|
|
1951
2066
|
offers,
|
|
1952
|
-
findings,
|
|
2067
|
+
findings: settled,
|
|
1953
2068
|
};
|
|
1954
2069
|
}
|
|
1955
2070
|
|
|
@@ -120,7 +120,49 @@ export function detectHarnessId(env = process.env, dir = MANIFEST_DIR) {
|
|
|
120
120
|
function detect(env, dir) {
|
|
121
121
|
const forced = env.PROJECTSTORE_HARNESS;
|
|
122
122
|
if (forced && loadHarnesses(dir).has(forced)) return forced;
|
|
123
|
+
const best = ranked(env, dir);
|
|
124
|
+
// A strong signal is the harness identifying itself, and it decides.
|
|
125
|
+
if (best && best.strong) return best.id;
|
|
123
126
|
|
|
127
|
+
// Only weak signals. That is not enough to switch harness: the project
|
|
128
|
+
// itself carries better evidence โ whichever harness directory is present
|
|
129
|
+
// in it is the harness this project is used from. (Until 2026-09-06 the
|
|
130
|
+
// evidence was "which harness directory holds our config"; the binding is
|
|
131
|
+
// harness-neutral now and carries no such signal.)
|
|
132
|
+
const cwd = process.cwd();
|
|
133
|
+
for (const m of loadHarnesses(dir).values()) {
|
|
134
|
+
const d = m.runtime?.harness_dir;
|
|
135
|
+
if (d && existsSync(join(cwd, d))) return m.id;
|
|
136
|
+
}
|
|
137
|
+
if (best) return best.id;
|
|
138
|
+
const src = sourceHarness(dir);
|
|
139
|
+
return src ? src.id : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The harness that identifies ITSELF from the environment, or null: a strong
|
|
143
|
+
// signal (its own plugin-root or project-dir variable, not one another
|
|
144
|
+
// harness shares), or its session marker โ runtime.session_env, which a Bash
|
|
145
|
+
// tool inside a session carries while the variables a hook receives are
|
|
146
|
+
// absent (measured 2026-09-05). Never the weak ranking, never the cwd's
|
|
147
|
+
// directory, never the source harness detectHarnessId falls back to. A
|
|
148
|
+
// finding that must not fire for a harness the project only might use asks
|
|
149
|
+
// this (the install spec, contract 6 as amended after the rc.3 tag).
|
|
150
|
+
export const IDENTIFIED_ENV = "PROJECTSTORE_IDENTIFIED";
|
|
151
|
+
export function identifiedHarnessId(env = process.env, dir = MANIFEST_DIR) {
|
|
152
|
+
const forced = env.PROJECTSTORE_HARNESS;
|
|
153
|
+
if (forced && loadHarnesses(dir).has(forced)) return forced;
|
|
154
|
+
// A parent of ours decided before it named the project in the harness's own
|
|
155
|
+
// vocabulary (childEnv), so its answer stands โ an empty one included.
|
|
156
|
+
if (env[IDENTIFIED_ENV] !== undefined) return loadHarnesses(dir).has(env[IDENTIFIED_ENV]) ? env[IDENTIFIED_ENV] : null;
|
|
157
|
+
const best = ranked(env, dir);
|
|
158
|
+
if (best && best.strong) return best.id;
|
|
159
|
+
for (const m of loadHarnesses(dir).values()) {
|
|
160
|
+
if ((m.runtime?.session_env || []).some((k) => env[k])) return m.id;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ranked(env, dir) {
|
|
124
166
|
// Explicit plugin-root/home variables beat merely-present ones: a shell that
|
|
125
167
|
// exports CODEX_HOME globally should not make a Claude Code session read as
|
|
126
168
|
// Codex when Claude Code also handed us CLAUDE_PLUGIN_ROOT.
|
|
@@ -168,22 +210,7 @@ function detect(env, dir) {
|
|
|
168
210
|
const rank = (strongHits > 0 ? 1e6 : 0) + strongHits * 1e3 + weakHits;
|
|
169
211
|
if (!best || rank > best.rank) best = { id: m.id, rank, strong: strongHits > 0 };
|
|
170
212
|
}
|
|
171
|
-
|
|
172
|
-
if (best && best.strong) return best.id;
|
|
173
|
-
|
|
174
|
-
// Only weak signals. That is not enough to switch harness: the project
|
|
175
|
-
// itself carries better evidence โ whichever harness directory is present
|
|
176
|
-
// in it is the harness this project is used from. (Until 2026-09-06 the
|
|
177
|
-
// evidence was "which harness directory holds our config"; the binding is
|
|
178
|
-
// harness-neutral now and carries no such signal.)
|
|
179
|
-
const cwd = process.cwd();
|
|
180
|
-
for (const m of loadHarnesses(dir).values()) {
|
|
181
|
-
const d = m.runtime?.harness_dir;
|
|
182
|
-
if (d && existsSync(join(cwd, d))) return m.id;
|
|
183
|
-
}
|
|
184
|
-
if (best) return best.id;
|
|
185
|
-
const src = sourceHarness(dir);
|
|
186
|
-
return src ? src.id : null;
|
|
213
|
+
return best;
|
|
187
214
|
}
|
|
188
215
|
|
|
189
216
|
// Test seam: the detected id is memoised for the process; a test that changes
|
|
@@ -474,6 +501,11 @@ export function configPath(projectDir, env = process.env) {
|
|
|
474
501
|
export function childEnv(base = process.env, { projectRoot: root, pluginRoot: plugin } = {}) {
|
|
475
502
|
const out = { ...base };
|
|
476
503
|
const r = activeHarness(base)?.runtime || {};
|
|
504
|
+
// Who called is decided here, before the project is named below: a
|
|
505
|
+
// project-dir variable we write would otherwise read, in the child, as the
|
|
506
|
+
// harness identifying itself โ a terminal `doctor` took Claude Code for the
|
|
507
|
+
// session's own harness (the review of the post-rc.3 fixes, 2026-10-04).
|
|
508
|
+
if (base[IDENTIFIED_ENV] === undefined) out[IDENTIFIED_ENV] = identifiedHarnessId(base) || "";
|
|
477
509
|
if (r.project_dir_env && root) out[r.project_dir_env] = root;
|
|
478
510
|
// A caller that runs its own copy of the core (the npm bin) names it, so a
|
|
479
511
|
// child never resolves templates or its version from a sibling install.
|
|
@@ -67,7 +67,7 @@ import { loadHarness, loadHarnesses, harnessIds, sourceHarness, detectHarnesses,
|
|
|
67
67
|
import { FOREIGN_TEXT, GRAMMAR_VERSION } from "./provenance.mjs";
|
|
68
68
|
import { analyseBlock, analyseJsonEntry, analyseStampedFile, analyseRegistration, analysePortableRegistration, analyseLayout, isOurFile, readText } from "./surfaces.mjs";
|
|
69
69
|
import { payloadFiles, renderPortableCatalog } from "./portable-registration.mjs";
|
|
70
|
-
import { pluginRoot, writeFileAtomic, writeExclusiveMetadata, ensureStateDir, ensureRuntimeDir, removeAgentsBlock, replaceAgentsBlock, readConfigAt, isPluginCacheRoot, isEphemeralRoot, statusLineIsOurWiring, claudeHome, packageDigest, writeOwnTree, removeOwnTree,
|
|
70
|
+
import { pluginRoot, writeFileAtomic, writeExclusiveMetadata, ensureStateDir, ensureRuntimeDir, removeAgentsBlock, replaceAgentsBlock, readConfigAt, isPluginCacheRoot, isEphemeralRoot, statusLineIsOurWiring, claudeHome, packageDigest, writeOwnTree, removeOwnTree, cmpPrecedence, importsLine, whichOnPath as whichOnPathFromLib, moveStateDir, mergeEntryLog, movePath, removeInside, statusLineScriptPath, layoutPaths, stagePortableMarketplace, finishPortableMarketplace, rollbackPortableMarketplace, removeTreeUnder } from "./lib.mjs";
|
|
71
71
|
|
|
72
72
|
import { GENERATOR } from "./surfaces.mjs";
|
|
73
73
|
export { GENERATOR };
|
|
@@ -112,7 +112,7 @@ function planAgentsBlock(ctx, key, s) {
|
|
|
112
112
|
const items = [];
|
|
113
113
|
if (a.refusal) return [{ surface: key, kind: "shared", path: a.files[0].path, entry: "projectstore:agents", state: "refused", action: "refuse", reason: a.refusal }];
|
|
114
114
|
|
|
115
|
-
const hasImport = (text) =>
|
|
115
|
+
const hasImport = (text) => importsLine(text, importLine);
|
|
116
116
|
// A CLAUDE.md that is nothing but the import registration added is ours to
|
|
117
117
|
// delete when the block goes (ADR-002 decision 4); anything else stays.
|
|
118
118
|
const onlyImport = (text) => String(text ?? "").split("\n").every((l) => !l.trim() || l.trim() === importLine);
|
|
@@ -256,7 +256,7 @@ function planAgentsBlock(ctx, key, s) {
|
|
|
256
256
|
const rewrite = items.find((i) => i.action === "remove" && i.path === e.path);
|
|
257
257
|
// An absent native file is empty text, not a reason to skip: it is created.
|
|
258
258
|
const text = rewrite ? rewrite.after : (e.present ? e.text : "");
|
|
259
|
-
if (typeof text !== "string" || text
|
|
259
|
+
if (typeof text !== "string" || importsLine(text, line)) continue;
|
|
260
260
|
const after = line + "\n" + (text.startsWith("\n") || !text.trim() ? "" : "\n") + text;
|
|
261
261
|
if (rewrite) { rewrite.after = after; rewrite.deleteIfEmpty = false; rewrite.reason += `; ${line} import added`; }
|
|
262
262
|
else items.push({ surface: `${key}_import`, kind: "shared", path: e.path, entry: line, state: "ours-absent", action: e.present ? "add" : "create",
|
|
@@ -271,14 +271,17 @@ function planAgentsBlock(ctx, key, s) {
|
|
|
271
271
|
// One host command as a plan step: the verbatim argv (the manifest's
|
|
272
272
|
// subcommand with its placeholders filled), why it runs, and the host-owned
|
|
273
273
|
// files it is known to touch (measured 2026-09-05 โ the manifest's cli.verified).
|
|
274
|
+
// A portable registration keeps its marketplace and enablement stanzas in one
|
|
275
|
+
// global config (registry.global_config; Codex's config.toml, measured
|
|
276
|
+
// 2026-09-07), so the preview names that file wherever a step rewrites it.
|
|
274
277
|
function hostStep(a, s, name, fill, why) {
|
|
275
278
|
const template = s.cli.commands[name];
|
|
276
279
|
if (!Array.isArray(template) || !template.length) throw new Error(`${s.format}: host operation ${name} is not declared`);
|
|
277
280
|
const argv = template.map((t) => t.replace(/\{(\w+)\}/g, (_, k) => fill[k] ?? `{${k}}`));
|
|
278
281
|
const p = a.paths;
|
|
279
282
|
const touches = {
|
|
280
|
-
validate: [], marketplace_add: [p.marketplaces, p.projectSettings], marketplace_update: [p.marketplaces], marketplace_remove: [p.marketplaces, p.projectSettings],
|
|
281
|
-
install: [p.installed, p.projectSettings, p.cacheDir], update: [p.installed, p.cacheDir], uninstall: [p.installed, p.projectSettings], disable: [p.projectSettings], enable: [p.projectSettings],
|
|
283
|
+
validate: [], marketplace_add: [p.marketplaces, p.globalConfig, p.projectSettings], marketplace_update: [p.marketplaces], marketplace_remove: [p.marketplaces, p.globalConfig, p.projectSettings],
|
|
284
|
+
install: [p.installed, p.globalConfig, p.projectSettings, p.cacheDir], update: [p.installed, p.cacheDir], uninstall: [p.installed, p.globalConfig, p.projectSettings], disable: [p.projectSettings], enable: [p.projectSettings],
|
|
282
285
|
}[name] || [];
|
|
283
286
|
return { kind: "host", name, bin: s.cli.bin, argv, why, touches: touches.filter(Boolean) };
|
|
284
287
|
}
|
|
@@ -455,7 +458,7 @@ function planRegistration(ctx, key, s) {
|
|
|
455
458
|
// pack โ install โ fix โ pack loop never bumps it. The host will not
|
|
456
459
|
// re-copy at an equal version (measured), so that refresh is uninstall + install.
|
|
457
460
|
const sameVersionDiffers = a.contentDiffers === true;
|
|
458
|
-
const rewrite = !a.dir.present || (
|
|
461
|
+
const rewrite = !a.dir.present || (cmpPrecedence(a.dir.pkg, a.pkg) < 0) || a.dir.digestOk === false || sameVersionDiffers;
|
|
459
462
|
const disabled = [...new Set([...a.dir.disabled, ...a.others.map((o) => o.key)])];
|
|
460
463
|
const digest = rewrite ? packageDigest(root) : (a.dir.prov?.digest || null);
|
|
461
464
|
const files = digest ? digest.count : 0;
|
|
@@ -596,7 +599,7 @@ export function plan(projectDir, { harnesses = [], mode = "install", env = proce
|
|
|
596
599
|
// once whatever --surface names โ planned first, so every surface below is
|
|
597
600
|
// planned against the new paths; its cleanup is planned last (below).
|
|
598
601
|
const layoutHarness = loadHarness(ids.find((id) => { const p = layoutPaths(projectDir, { harnessDir: loadHarness(id).runtime?.harness_dir || null }); return existsSync(p.legacy.binding) || existsSync(p.legacy.runtime); }) || ids[0]);
|
|
599
|
-
const layoutCtx = { projectDir, mode, env, home, root, harness: layoutHarness, incomplete: false };
|
|
602
|
+
const layoutCtx = { projectDir, mode, env, home, root, harness: layoutHarness, incomplete: false, surfaces };
|
|
600
603
|
const layout = planLayout(layoutCtx);
|
|
601
604
|
for (const item of layout.first) out.items.push({ harness: layoutHarness.id, ...item });
|
|
602
605
|
if (layoutCtx.incomplete) out.incomplete = true;
|
|
@@ -691,7 +694,12 @@ function planLayout(ctx) {
|
|
|
691
694
|
const base = { surface: "layout", kind: "layout", path: P.legacy.binding, entry: null, state: a.state, reason: null };
|
|
692
695
|
if (mode === "uninstall") {
|
|
693
696
|
// Disowning: the legacy runtime directory goes with the new one when it is
|
|
694
|
-
// ours (its header); the legacy binding is bind's, never uninstall's.
|
|
697
|
+
// ours (its header); the legacy binding is bind's, never uninstall's. A
|
|
698
|
+
// narrowed uninstall leaves it alone: the way back from an npm switch is
|
|
699
|
+
// `uninstall --surface plugin`, and it deleted a not-yet-moved project's
|
|
700
|
+
// sessions, log and the launcher its status line still ran (the critic's
|
|
701
|
+
// fourth pass, 2026-10-04). `surfaces` is null when nothing was named.
|
|
702
|
+
if (ctx.surfaces) return none;
|
|
695
703
|
if (!a.legacy.runtime || !a.legacy.runtimeOurs) return none;
|
|
696
704
|
return { first: [], last: [{ ...base, surface: "layout_cleanup", path: P.legacy.runtime, state: "legacy", action: "remove", reason: "the legacy runtime directory is ours (its .gitignore header) and goes with the state", steps: [
|
|
697
705
|
{ kind: "remove-legacy-runtime", path: P.legacy.runtime, why: "the pre-0.28 state directory, removed whole" },
|
|
@@ -714,7 +722,7 @@ function planLayout(ctx) {
|
|
|
714
722
|
const last = [];
|
|
715
723
|
if (a.legacy.launcher || a.legacy.runtime) {
|
|
716
724
|
const cleanup = [];
|
|
717
|
-
if (a.legacy.launcher) cleanup.push({ kind: "remove-legacy-launcher", path: P.legacy.launcher, why: "removed once the new launcher is written and the settings entry names it โ or at once when the status-line slot is not ours (foreign, or empty because the status line is off); kept
|
|
725
|
+
if (a.legacy.launcher) cleanup.push({ kind: "remove-legacy-launcher", path: P.legacy.launcher, why: "removed once the new launcher is written and the settings entry names it โ or at once when the status-line slot is not ours (foreign, or empty because the status line is off); kept while a settings file the host reads (this project's local or committed settings, or the user's) still runs it as the status line โ a script that calls it indirectly is not seen" });
|
|
718
726
|
if (a.legacy.runtime) cleanup.push({ kind: "rmdir-legacy", path: P.legacy.runtime, why: "the emptied pre-0.28 runtime directory" });
|
|
719
727
|
last.push({ ...base, surface: "layout_cleanup", path: P.legacy.runtime, state: "legacy", action: "cleanup", reason: null, steps: cleanup });
|
|
720
728
|
}
|
|
@@ -767,7 +775,7 @@ function applyLayout(p, i, { failed, home = homedir() }) {
|
|
|
767
775
|
if (text === null) out.steps.push({ kind: st.kind, ok: true, removed: false });
|
|
768
776
|
else if (!moved && slotOurs) out.steps.push({ kind: st.kind, ok: true, removed: false, reason: "no launcher at the new path yet (this root does not produce one) โ left in place" });
|
|
769
777
|
else if (!isOurFile(text)) out.steps.push({ kind: st.kind, ok: true, removed: false, reason: "not ours โ left in place" });
|
|
770
|
-
else if (named) out.steps.push({ kind: st.kind, ok: true, removed: false, reason: "
|
|
778
|
+
else if (named) out.steps.push({ kind: st.kind, ok: true, removed: false, reason: "a settings file the host reads still runs it as the status line โ left in place" });
|
|
771
779
|
else out.steps.push({ kind: st.kind, ok: true, removed: removeInside(st.path, within) });
|
|
772
780
|
}
|
|
773
781
|
else if (st.kind === "rmdir-legacy") {
|
|
@@ -318,6 +318,9 @@ export function claudeHome(home = homedir()) {
|
|
|
318
318
|
return harnessAgentHome(process.env, home);
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// The release triple only: 0.28.0-rc.2 and 0.28.0 compare equal. Right for
|
|
322
|
+
// "which release line" (the layout window's sunset); wrong for "which build is
|
|
323
|
+
// newer" โ that is cmpPrecedence.
|
|
321
324
|
export function cmpVersion(a, b) {
|
|
322
325
|
const A = String(a || "0").split(".").map((n) => parseInt(n, 10) || 0);
|
|
323
326
|
const B = String(b || "0").split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -325,6 +328,33 @@ export function cmpVersion(a, b) {
|
|
|
325
328
|
return 0;
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
// Semver 2.0 precedence: 0.28.0-rc.2 < 0.28.0-rc.3 < 0.28.0. A registration
|
|
332
|
+
// made at a release candidate read as current against the release while the
|
|
333
|
+
// comparison stopped at the triple, so `upgrade` never refreshed it (the
|
|
334
|
+
// critic's probe, 2026-10-03). Build metadata (`+โฆ`) never ranks.
|
|
335
|
+
export function cmpPrecedence(a, b) {
|
|
336
|
+
const parse = (v) => {
|
|
337
|
+
const s = String(v || "0").split("+")[0];
|
|
338
|
+
const dash = s.indexOf("-");
|
|
339
|
+
return { core: (dash < 0 ? s : s.slice(0, dash)).split(".").map((n) => parseInt(n, 10) || 0), pre: dash < 0 ? [] : s.slice(dash + 1).split(".") };
|
|
340
|
+
};
|
|
341
|
+
const A = parse(a), B = parse(b);
|
|
342
|
+
for (let i = 0; i < 3; i++) if ((A.core[i] || 0) !== (B.core[i] || 0)) return (A.core[i] || 0) - (B.core[i] || 0);
|
|
343
|
+
// A release outranks every candidate for it.
|
|
344
|
+
if (!A.pre.length || !B.pre.length) return B.pre.length - A.pre.length;
|
|
345
|
+
for (let i = 0; i < Math.max(A.pre.length, B.pre.length); i++) {
|
|
346
|
+
if (i >= A.pre.length) return -1;
|
|
347
|
+
if (i >= B.pre.length) return 1;
|
|
348
|
+
const x = A.pre[i], y = B.pre[i];
|
|
349
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
|
|
350
|
+
if (nx && ny) { if (Number(x) !== Number(y)) return Number(x) - Number(y); continue; }
|
|
351
|
+
// A numeric identifier ranks below an alphanumeric one.
|
|
352
|
+
if (nx !== ny) return nx ? -1 : 1;
|
|
353
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
354
|
+
}
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
328
358
|
// Where the CURRENTLY installed projectstore lives, per Claude Code's own
|
|
329
359
|
// plugin registry. The cache path carries the version
|
|
330
360
|
// (โฆ/plugins/cache/<marketplace>/projectstore/<version>), so anything that
|
|
@@ -863,6 +893,30 @@ export function findAgentsBlock(text) {
|
|
|
863
893
|
return { present: true, v: Number(m[1]), start: m.index, end, unclosed: false, count, block: text.slice(m.index, end) };
|
|
864
894
|
}
|
|
865
895
|
|
|
896
|
+
// Whether `text` holds `line` as a line of its own, trimmed and exact โ the
|
|
897
|
+
// installer's match for the `@<file>` import, so `@./AGENTS.md` reads as
|
|
898
|
+
// absent to install and doctor alike.
|
|
899
|
+
export function importsLine(text, line) {
|
|
900
|
+
return String(text ?? "").split("\n").some((l) => l.trim() === line);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Whether a harness sees the one agents block where it stands โ the predicate
|
|
904
|
+
// doctor reports by (the install spec, contract 6 as amended after the rc.3
|
|
905
|
+
// tag). Install's placement rules agree with it: it moves a block whose file
|
|
906
|
+
// is not among the harness's files and bridges one that is, through the same
|
|
907
|
+
// import match (importsLine); a test holds the two to one answer. Visible when
|
|
908
|
+
// the block's file is among the harness's agents_block.files and is either the
|
|
909
|
+
// file it reads by itself or imported from that file. A harness with no agents
|
|
910
|
+
// block has nothing to see it with. `texts` maps a file name to its text; an
|
|
911
|
+
// absent file is undefined.
|
|
912
|
+
export function blockVisibleTo(manifest, file, texts = {}) {
|
|
913
|
+
const ab = manifest?.surfaces?.agents_block;
|
|
914
|
+
if (!ab || ab.supported === false) return true;
|
|
915
|
+
if (!(ab.files || []).includes(file)) return false;
|
|
916
|
+
const native = ab.reads_natively;
|
|
917
|
+
return !native || native === file || importsLine(texts[native], `@${file}`);
|
|
918
|
+
}
|
|
919
|
+
|
|
866
920
|
// Replace the block in place, else append it after the user's own content.
|
|
867
921
|
export function replaceAgentsBlock(text, block) {
|
|
868
922
|
const base = String(text ?? "");
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
pluginEnabled,
|
|
48
48
|
treeDigest,
|
|
49
49
|
packageDigest,
|
|
50
|
-
|
|
50
|
+
cmpPrecedence,
|
|
51
51
|
layoutPaths,
|
|
52
52
|
RUNTIME_GITIGNORE_HEADER,
|
|
53
53
|
LAUNCHER_HEADER,
|
|
@@ -284,7 +284,7 @@ export function analyseRegistration(projectDir, s, { root = pluginRoot(), home =
|
|
|
284
284
|
if (e.projectPath && !samePath(e.projectPath, projectDir)) continue;
|
|
285
285
|
seen.add(e.key); a.others.push({ key: e.key, path: e.path, version: e.version });
|
|
286
286
|
}
|
|
287
|
-
a.newer = Boolean(a.dir.pkg) &&
|
|
287
|
+
a.newer = Boolean(a.dir.pkg) && cmpPrecedence(a.dir.pkg, pkg) > 0;
|
|
288
288
|
// Same version, different payload: the directory's recorded digest against
|
|
289
289
|
// this package's โ contract 4's rung 3 (source changed) for a directory.
|
|
290
290
|
a.contentDiffers = null;
|
|
@@ -307,7 +307,7 @@ export function analyseRegistration(projectDir, s, { root = pluginRoot(), home =
|
|
|
307
307
|
// The directory is shared by every checkout on the machine; a project that never registered is absent, whatever the directory holds.
|
|
308
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
309
|
if (!a.dir.present) return { ...a, state: "stale", reason: STALE_TEXT[STALE.PLUGIN] + " (the directory is gone)" };
|
|
310
|
-
if (
|
|
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
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
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
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)") };
|
|
@@ -397,6 +397,9 @@ export function surfaceStates(projectDir, { home = homedir(), root = pluginRoot(
|
|
|
397
397
|
writtenBy: a.writtenBy || null,
|
|
398
398
|
newer: Boolean(a.newer),
|
|
399
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)),
|
|
400
403
|
});
|
|
401
404
|
states.push(row);
|
|
402
405
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore-claude",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Installs projectstore for Claude Code from npm: the core pinned and bundled, the harness fixed โ npx projectstore-claude install --project \"$PWD\".",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"projectstore",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"README.md"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"projectstore": "=0.28.
|
|
40
|
+
"projectstore": "=0.28.1"
|
|
41
41
|
},
|
|
42
42
|
"bundleDependencies": [
|
|
43
43
|
"projectstore"
|