projectstore-claude 0.29.3 → 0.30.0

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.
Files changed (57) hide show
  1. package/README.md +13 -12
  2. package/bin/projectstore-claude.mjs +2 -2
  3. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  4. package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
  5. package/node_modules/projectstore/README.md +153 -77
  6. package/node_modules/projectstore/commands/bind.md +6 -17
  7. package/node_modules/projectstore/commands/doctor.md +12 -7
  8. package/node_modules/projectstore/commands/statusline.md +1 -1
  9. package/node_modules/projectstore/commands/story.md +15 -2
  10. package/node_modules/projectstore/docs/extending.md +11 -6
  11. package/node_modules/projectstore/docs/getting-started.md +21 -1
  12. package/node_modules/projectstore/docs/harnesses.md +13 -10
  13. package/node_modules/projectstore/docs/how-it-works.md +6 -4
  14. package/node_modules/projectstore/harnesses/claude-code.json +16 -15
  15. package/node_modules/projectstore/harnesses/codex.json +12 -13
  16. package/node_modules/projectstore/hooks/band.tsx +48 -20
  17. package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
  18. package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
  19. package/node_modules/projectstore/hooks/session-start.mjs +50 -18
  20. package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
  21. package/node_modules/projectstore/package.json +11 -2
  22. package/node_modules/projectstore/scripts/band-shared.mjs +141 -24
  23. package/node_modules/projectstore/scripts/band.mjs +72 -21
  24. package/node_modules/projectstore/scripts/binding.mjs +616 -85
  25. package/node_modules/projectstore/scripts/build-adapters.mjs +73 -29
  26. package/node_modules/projectstore/scripts/cli.mjs +1015 -185
  27. package/node_modules/projectstore/scripts/codemap.mjs +22 -7
  28. package/node_modules/projectstore/scripts/convergence.mjs +253 -83
  29. package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
  30. package/node_modules/projectstore/scripts/doctor-report.mjs +91 -23
  31. package/node_modules/projectstore/scripts/doctor.mjs +1317 -371
  32. package/node_modules/projectstore/scripts/draft.mjs +5 -2
  33. package/node_modules/projectstore/scripts/fetch-shell.mjs +188 -39
  34. package/node_modules/projectstore/scripts/graph.mjs +19 -13
  35. package/node_modules/projectstore/scripts/harness.mjs +123 -48
  36. package/node_modules/projectstore/scripts/install-harness.mjs +3532 -618
  37. package/node_modules/projectstore/scripts/kanban.mjs +2 -1
  38. package/node_modules/projectstore/scripts/lib.mjs +694 -159
  39. package/node_modules/projectstore/scripts/mcp.mjs +386 -105
  40. package/node_modules/projectstore/scripts/portable-registration.mjs +224 -51
  41. package/node_modules/projectstore/scripts/provenance.mjs +26 -10
  42. package/node_modules/projectstore/scripts/query.mjs +378 -80
  43. package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
  44. package/node_modules/projectstore/scripts/run-host.mjs +62 -17
  45. package/node_modules/projectstore/scripts/scaffold.mjs +89 -28
  46. package/node_modules/projectstore/scripts/setup.mjs +987 -0
  47. package/node_modules/projectstore/scripts/stages.mjs +5 -1
  48. package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
  49. package/node_modules/projectstore/scripts/statusline.mjs +32 -14
  50. package/node_modules/projectstore/scripts/story-section.mjs +39 -17
  51. package/node_modules/projectstore/scripts/surfaces.mjs +416 -95
  52. package/node_modules/projectstore/scripts/term.mjs +382 -97
  53. package/node_modules/projectstore/scripts/tokens.mjs +106 -28
  54. package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
  55. package/node_modules/projectstore/scripts/version-guard.mjs +74 -27
  56. package/node_modules/projectstore/scripts/worktree.mjs +16 -5
  57. package/package.json +3 -3
@@ -1,5 +1,17 @@
1
1
  # Getting started with projectstore
2
2
 
3
+ ## Install
4
+
5
+ From a terminal in your project:
6
+
7
+ ```
8
+ npx projectstore setup
9
+ ```
10
+
11
+ Setup finds the coding agents the project uses (Claude Code, Codex or both), asks a few questions — among them where the vault lives — then shows one plan and asks once before it writes. It binds the vault, or creates one when the folder is missing or empty. The README's [Install](../README.md#install) section has the details, the global form and the registry-mirror form.
12
+
13
+ The next two sections are the other ways in: a checkout, and the git marketplace inside a Claude Code session. After either of them, the first-time setup below binds the vault.
14
+
3
15
  ## Install (local dev)
4
16
 
5
17
  Until projectstore is published to a public marketplace, install it from a local clone.
@@ -132,6 +144,14 @@ What the language does and does not change: section headings, table labels and p
132
144
 
133
145
  ## Updating to a new version
134
146
 
147
+ Installed with setup or from npm: from a terminal in your project, name the version you want to run:
148
+
149
+ ```
150
+ npx projectstore@<version> upgrade
151
+ ```
152
+
153
+ Installed from the git marketplace, in a Claude Code session:
154
+
135
155
  ```
136
156
  /plugin marketplace update SmartAndPoint
137
157
  /reload-plugins
@@ -139,4 +159,4 @@ What the language does and does not change: section headings, table labels and p
139
159
 
140
160
  Or enable auto-update once (`/plugin` → **Marketplaces** → **SmartAndPoint** → toggle **auto-update**) and Claude Code will detect new releases at startup.
141
161
 
142
- **After any update, run `/projectstore:doctor`.** It compares your project's wiring against what the new version expects and names each fix with the command to run — stale agents block in `CLAUDE.md` (`/projectstore:agents register`), leftover agent copies that override nothing (`/projectstore:agents configure`), auto-update still off (the exact setting and file), a newer release than the one running. `doctor --fix` applies the install-side repairs interactively; `/projectstore:reconcile` rebuilds the board/indexes/code-map if content drifted. Silence at session start means healthy — the cheap checks run automatically and only speak up when something is wrong.
162
+ **After any update, run `/projectstore:doctor` in a session.** It compares your project's wiring against what the new version expects and names each fix with the command to run: a stale agents block in `CLAUDE.md` gets `/projectstore:agents register` as its fix and leftover agent copies that override nothing get `/projectstore:agents configure` as theirs. It also names auto-update still off (the exact setting and file) and a newer release than the one running. `doctor --fix` applies the install-side repairs interactively; `/projectstore:reconcile` rebuilds the board/indexes/code-map if content drifted. Silence at session start means healthy — the cheap checks run automatically and only speak up when something is wrong.
@@ -75,24 +75,27 @@ not started from a terminal can miss a `node` that only `.zprofile` puts on
75
75
  `PATH`.
76
76
 
77
77
  The shell is published as `projectstore-codex` from 0.28.1, ahead of that live
78
- run, by the maintainer's decision:
78
+ run, by the maintainer's decision. Nobody types its name: setup, install and
79
+ upgrade fetch it at their own version and plan Codex from it. From a terminal
80
+ in your project:
79
81
 
80
82
  ```sh
81
- npx projectstore-codex install --project "$PWD"
83
+ npx projectstore setup
82
84
  ```
83
85
 
84
- For later releases, `npx projectstore-codex@<version> upgrade --project "$PWD"`.
85
- Installation is user-global because Codex stores marketplace registrations and
86
- plugin caches in `CODEX_HOME`; the agents block in `AGENTS.md` remains
87
- project-local. Ordinary uninstall leaves the global plugin in place; add
88
- `--global` only when you mean to remove it for every project. From this
89
- checkout, the same flow runs against a built tarball:
86
+ To upgrade Codex alone, naming the version you want to run:
90
87
 
91
88
  ```sh
92
- npm run shells:build -- --only projectstore-codex --dev --out dist
93
- npx --package "./dist/projectstore-codex-$(node -p 'require("./package.json").version').tgz" projectstore-codex install --project "$PWD"
89
+ npx projectstore@<version> upgrade --harness codex
94
90
  ```
95
91
 
92
+ Installation is user-global because Codex stores marketplace registrations and
93
+ plugin caches in `CODEX_HOME`; the agents block in `AGENTS.md` remains
94
+ project-local. Ordinary uninstall leaves the global plugin in place; add
95
+ `--global` only when you mean to remove it for every project. A checkout's own
96
+ build runs through the same flow: [`packaging/README.md`](../packaging/README.md#running-a-checkouts-build)
97
+ has the tarball loop.
98
+
96
99
  What is known, and how:
97
100
 
98
101
  - **Hooks load from Codex's default file, so the shell carries no root
@@ -67,8 +67,10 @@ sessions can't silently overwrite each other's new artifact.
67
67
 
68
68
  **The status line is stricter still**: it renders from a *per-session* pointer
69
69
  (`<project>/.projectstore/state/sessions/<session_id>.json`, written by the
70
- PreToolUse hook with denormalized titles) and performs **zero cross-session and
71
- zero vault reads**. 2–6 parallel sessions each see exactly their own epic/story;
70
+ tool hooks with denormalized titles — after a write, from the file as written —
71
+ by `story close` through the core, and refreshed at session start) and performs
72
+ **zero cross-session and zero vault reads**. A story that is done, closed or
73
+ parked leaves the line: it shows the epic alone, as the band does. 2–6 parallel sessions each see exactly their own epic/story;
72
74
  a fresh session gets a localized "no epic or story in this session yet" line; a
73
75
  corrupt pointer shows an error marker, never a false "no work". The renderer also
74
76
  drops a `.last-render.json` breadcrumb so `doctor` can detect session-id
@@ -162,7 +164,7 @@ resistance. v0.13 makes that safe instead of forbidden:
162
164
  (`--write`): each approved target is recomputed from the vault state at
163
165
  write time and replaced atomically, so a stale preview is never what lands;
164
166
  `--write` is also a sanctioned headless entry point (cron/CI repair job).
165
- - A throttled PreToolUse **nudge** fires when vault files are edited directly:
167
+ - A throttled PostToolUse **nudge** fires when vault files are edited directly:
166
168
  "run reconcile afterwards so the derived views stay in sync" (disable with
167
169
  `"guard": "off"` in `projectstore.json`).
168
170
  - A once-per-session **entry reminder** fires on PostToolUse when a session has
@@ -189,7 +191,7 @@ resistance. v0.13 makes that safe instead of forbidden:
189
191
  sees Bash-mediated writes, which bypass hook path extraction entirely.
190
192
 
191
193
  The division of labor: deterministic checks catch the mechanical 90% for free;
192
- the LLM critic (`/projectstore:review`) is saved for the judgment calls.
194
+ the LLM critic that `/projectstore:review` runs is saved for the judgment calls.
193
195
 
194
196
  ## Naming a session after the work it settled on
195
197
 
@@ -11,26 +11,27 @@
11
11
  "date": "2026-09-04"
12
12
  },
13
13
  "install": {
14
- "mechanism": "the package's own registration through its distribution shell, or the built-in plugin marketplace",
14
+ "mechanism": "the package's own registration (setup or install, from npm), or the built-in plugin marketplace",
15
15
  "shell": "projectstore-claude",
16
- "shell_reason": "the distribution shell that installs this harness from npm (the layout ADR decision 6; the shells ADR): `npx projectstore-claude install --project <dir>` is the core's `install --harness claude-code`, with the core pinned at the same version and bundled inside the shell's tarball. Data, not `projectstore-<id>`: the id is claude-code and the reserved, published name is projectstore-claude. Every command a finding or a deferred reason names is built from this key (harness.mjs packageCommand).",
17
- "why_not_scripted": "Two channels. `npx projectstore-claude install --project <dir>` registers the plugin for that project from the npm package itself — the shell bundles the core and fixes the harness; the core's low-level form is `npx projectstore install --harness claude-code --project <dir>` — through a local marketplace under the Claude home, driven by Claude Code's own `claude plugin` CLI. `/plugin install projectstore@SmartAndPoint` installs it from the git marketplace. `/plugin` lists and toggles either. Upgrading the npm registration is `npx projectstore-claude@<version> upgrade --project <dir>` — that is also the pin.",
16
+ "shell_reason": "the distribution shell of this harness on npm (the layout ADR decision 6; the shells ADR): `projectstore-claude install` is the core's `install --harness claude-code`, with the core pinned at the same version and bundled inside the shell's tarball. Data, not `projectstore-<id>`: the id is claude-code and the reserved, published name is projectstore-claude. Since the front-door ADR the shell is a release artifact: no command a person is told to type names it (packageCommand builds the core's form, `npx projectstore <verb> --harness claude-code`), and this key tells the core which harness a running shell fixes (harness.mjs bundlingShellHarnessId, speakingHarnessId).",
17
+ "why_not_scripted": "Two channels. The npm package registers the plugin for a project itself — run `npx projectstore setup` or the core's low-level `install --harness claude-code` from a terminal in the project — through a local marketplace under the Claude home, driven by Claude Code's own `claude plugin` CLI. `/plugin install projectstore@SmartAndPoint` installs it from the git marketplace. `/plugin` lists and toggles either. The npm registration is upgraded, and pinned, by `npx projectstore@<version> upgrade` from a terminal in the project.",
18
18
  "steps": [
19
- "npx projectstore-claude install --project \"$PWD\" (npm; registers the plugin for this project — the shell runs the core's install with the harness fixed)",
20
- "/plugin marketplace add SmartAndPoint/ProjectStore && /plugin install projectstore@SmartAndPoint (git marketplace, user-wide)",
19
+ "npm, for this checkout (the core registers the plugin), from a terminal in the project: npx projectstore install --harness claude-code",
20
+ "git marketplace, user-wide, in a Claude Code session: /plugin marketplace add SmartAndPoint/ProjectStore && /plugin install projectstore@SmartAndPoint",
21
21
  "restart Claude Code"
22
22
  ],
23
23
  "next": [
24
24
  "restart Claude Code in this project"
25
25
  ],
26
26
  "notes": [
27
- "Git marketplace only: auto-update is OFF by default for third-party plugins: /plugin -> Marketplaces -> SmartAndPoint -> toggle auto-update. The npm registration updates when you run `npx projectstore-claude upgrade --project <dir>`.",
28
- "Pin a git-marketplace release with: /plugin marketplace add SmartAndPoint/ProjectStore#<tag>; pin the npm registration with npx projectstore-claude@<version>.",
27
+ "Git marketplace only: auto-update is OFF by default for third-party plugins: /plugin -> Marketplaces -> SmartAndPoint -> toggle auto-update. The npm registration updates when you run `npx projectstore@<version> upgrade` from a terminal in the project.",
28
+ "Pin a git-marketplace release by adding the marketplace at a tag: /plugin marketplace add SmartAndPoint/ProjectStore#<tag>",
29
+ "The npm registration is pinned by the version its upgrade names: npx projectstore@<version> upgrade",
29
30
  "Two enabled copies of the plugin (git marketplace and npm) in one checkout would load twice; the npm registration silences the other one for that checkout only (settings.local.json), and says so in its preview.",
30
31
  "From a checkout instead: claude --plugin-dir ./ProjectStore",
31
32
  "The npm registration is per checkout (local scope) and per machine; a fresh clone or a new worktree runs the install command again. It must run from a terminal outside a Claude Code session: the host CLI and a live session both rewrite settings files."
32
33
  ],
33
- "docs": "https://github.com/SmartAndPoint/ProjectStore#install--one-message"
34
+ "docs": "https://github.com/SmartAndPoint/ProjectStore#install"
34
35
  },
35
36
  "runtime": {
36
37
  "detect_env": [
@@ -134,16 +135,16 @@
134
135
  "system_message": true,
135
136
  "decision": "permissionDecision",
136
137
  "evidence": "measured",
137
- "note": "systemMessage measured — scripts/touch-session.mjs uses it for the pointer nudge and the name offer. additionalContext and permissionDecision (allow|deny|ask) are documented; projectstore never emits them. A separate PermissionRequest event carries its own decision object."
138
+ "note": "systemMessage measured — scripts/touch-session.mjs used it for the raw-edit nudge and uses it for the name offer (the nudge moved to PostToolUse with the write's pointer patch, ADR-006 amended 2026-10-12). additionalContext and permissionDecision (allow|deny|ask) are documented; projectstore never emits them. A separate PermissionRequest event carries its own decision object."
138
139
  },
139
140
  "PostToolUse": {
140
141
  "fields": [
141
142
  "additionalContext"
142
143
  ],
143
- "system_message": null,
144
+ "system_message": true,
144
145
  "decision": null,
145
146
  "evidence": "measured",
146
- "note": "scripts/touch-session.mjs — the entry-rule reminder's primary carrier (entry-rule spec, contract 14)."
147
+ "note": "scripts/touch-session.mjs — the entry-rule reminder's primary carrier (entry-rule spec, contract 14). The measurement covers additionalContext only. systemMessage carries the raw-edit nudge since the pointer patch moved after the write (ADR-006, amended 2026-10-12): documented for every event, not yet observed on this one — the closed-story story's live check records it."
147
148
  },
148
149
  "PostToolUseFailure": {
149
150
  "fields": [
@@ -299,7 +300,7 @@
299
300
  "file": ".projectstore/state/claude-code/statusline.mjs",
300
301
  "source": "scripts/statusline-launcher.mjs",
301
302
  "condition": "plugin_cache_install",
302
- "file_reason": "the harness's generated runtime files live under .projectstore/state/<harness id>/ (layout ADR decision 4); the 0.27.x path .claude/.projectstore/statusline.mjs is read as ours through 0.29",
303
+ "file_reason": "the harness's generated runtime files live under .projectstore/state/<harness id>/ (layout ADR decision 4); the 0.27.x path .claude/.projectstore/statusline.mjs is read as ours through 0.30",
303
304
  "legacy_file": ".claude/.projectstore/statusline.mjs"
304
305
  },
305
306
  "mcp": {
@@ -494,18 +495,18 @@
494
495
  "ui_vocabulary_reason": "This harness's UI affordances and tool names: words that mean nothing on another harness. The generator refuses them in another harness's rendered tree, and the runtime vocabulary lint refuses them in what the core prints for another harness (generation spec, contracts 6 and 18). A word here must be followed by a non-word character to count, so `/plugins/` in a path is not `/plugin`.",
495
496
  "update_hint": {
496
497
  "line": "See /plugin → Marketplaces to enable auto-update.",
497
- "welcome": ["**About future updates**: Claude Code does NOT auto-update third-party marketplaces by default. To get notified of new releases (v0.7+):", "1. Open `/plugin` → **Marketplaces** tab.", "2. Find **SmartAndPoint**.", "3. Toggle **auto-update** on.", "", "Without it, you'd run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#updates for details."],
498
+ "welcome": ["**About future updates**: Claude Code does NOT auto-update third-party marketplaces by default. To get notified of new releases (v0.7+):", "1. Open `/plugin` → **Marketplaces** tab.", "2. Find **SmartAndPoint**.", "3. Toggle **auto-update** on.", "", "Without it, you'd run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#upgrading for details."],
498
499
  "reason": "What the first-run welcome and its one-line notice say about updates (generation spec, contract 18): `line` ends the notice, `welcome` is the welcome's update block. update_instructions is the long-form guidance; this is the part a session prints, kept here so no hook names a harness's update path."
499
500
  },
500
501
  "update_instructions": [
501
- "**About future updates**: installed from the git marketplace, Claude Code does NOT auto-update third-party marketplaces by default (an npm registration updates with `npx projectstore-claude upgrade --project <dir>`). To get notified of new releases (v0.7+):",
502
+ "**About future updates**: installed from the git marketplace, Claude Code does NOT auto-update third-party marketplaces by default; an npm registration updates with `npx projectstore@<version> upgrade` from a terminal in the project. To get notified of new git-marketplace releases (v0.7+):",
502
503
  "1. Open `/plugin` → **Marketplaces** tab.",
503
504
  "2. Find **SmartAndPoint**.",
504
505
  "3. Toggle **auto-update** on.",
505
506
  "",
506
507
  "Without it, you would run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#upgrading for details.",
507
508
  "",
508
- "**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."
509
+ "**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 in the project, outside the session. For a git-marketplace install it is the installed copy's own `bin/projectstore.mjs` with `upgrade --harness claude-code --no-register`, which leaves the registration as it is. For the npm registration it is `npx projectstore@<version> upgrade --harness claude-code --no-register` and nothing else. 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 form without `--no-register`, 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."
509
510
  ],
510
511
  "agent_translation": {
511
512
  "carries_model": true,
@@ -311,33 +311,32 @@
311
311
  },
312
312
  "rewrites": [],
313
313
  "install": {
314
- "mechanism": "the package's portable plugin root, registered through its distribution shell and Codex's own marketplace CLI",
314
+ "mechanism": "the package's portable plugin root, fetched in its distribution shell and registered through Codex's own marketplace CLI",
315
315
  "shell": "projectstore-codex",
316
316
  "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.",
317
- "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.",
317
+ "why_not_scripted": "The installer does not edit Codex's registry or cache. It atomically stages the plugin source from the shell it fetched, 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.",
318
318
  "steps": [
319
- "npx projectstore-codex install --project \"$PWD\"",
319
+ "from a terminal in the project (the core fetches the Codex shell at its own version): npx projectstore install --harness codex",
320
320
  "restart Codex and approve the ProjectStore hooks when prompted",
321
- "run `$projectstore-bind <vault-path>` in Codex"
321
+ "in Codex, bind a vault: $projectstore-bind <vault-path>"
322
322
  ],
323
323
  "next": [
324
- "restart Codex from a terminal and approve the ProjectStore hooks when it asks",
325
- "in an unbound project, run `$projectstore-bind <vault-path>` in Codex"
324
+ "restart Codex from a terminal and approve the ProjectStore hooks when it asks"
326
325
  ],
327
326
  "notes": [
328
327
  "The installation is user-global because Codex's marketplace, plugin row and cache live in CODEX_HOME; the AGENTS.md block is project-local.",
329
- "Upgrade with `npx projectstore-codex@<version> upgrade --project \"$PWD\"`; the named package version is also the plugin version.",
328
+ "Upgrade with `npx projectstore@<version> upgrade --harness codex` from a terminal in the project; the version it names is also the plugin version.",
330
329
  "A second enabled ProjectStore plugin from another marketplace is a refusal, not silently disabled globally. Remove or disable that older registration explicitly, then retry.",
331
- "Ordinary uninstall removes only project-owned surfaces. `npx projectstore-codex uninstall --global --project \"$PWD\"` also removes the user-global plugin registration after a preview."
330
+ "Ordinary uninstall removes only project-owned surfaces. `npx projectstore uninstall --harness codex --global` also removes the user-global plugin registration after a preview."
332
331
  ],
333
- "docs": "https://github.com/SmartAndPoint/ProjectStore#install--one-message"
332
+ "docs": "https://github.com/SmartAndPoint/ProjectStore#install"
334
333
  },
335
334
  "update_hint": {
336
- "line": "Update with `{upgrade}`.",
337
- "welcome": ["**About future updates**: run `{upgrade}` from a terminal, then restart Codex and approve any hook it asks about.", "", "See https://github.com/SmartAndPoint/ProjectStore#updates for details."],
338
- "reason": "What the first-run welcome and its one-line notice say about updates (generation spec, contract 18): `line` ends the notice, `welcome` is the welcome's update block, and `{upgrade}` is filled with this harness's shell command at latest, so what is printed runs as printed. update_instructions is the long-form guidance; this is the part a session prints, kept here so no hook names a harness's update path."
335
+ "line": "Update from a terminal in this project:\n{upgrade}",
336
+ "welcome": ["**About future updates**: from a terminal in this project, run:", "", "```", "{upgrade}", "```", "", "Then restart Codex and approve any hook it asks about. See https://github.com/SmartAndPoint/ProjectStore#upgrading for details."],
337
+ "reason": "What the first-run welcome and its one-line notice say about updates (generation spec, contract 18): `line` ends the notice, `welcome` is the welcome's update block, and `{upgrade}` is filled with the core's upgrade for this harness at latest (`--harness codex`, no `--project`), so what is printed runs as printed. Each says where to run it and puts the command alone on its line (the story \"User-facing text says where to run and names no project path and no shell\", rules 4 and 5). update_instructions is the long-form guidance; this is the part a session prints, kept here so no hook names a harness's update path."
339
338
  },
340
- "update_instructions": "Run `npx projectstore-codex@<version> upgrade --project <dir>`. The installer atomically replaces its stable source and `codex plugin add projectstore@projectstore-npx` materialises the new version; this was measured across two dev builds on codex-cli 0.153.4. Restart Codex afterwards so the new skills and hooks are loaded.",
339
+ "update_instructions": "From a terminal in the project, run `npx projectstore@<version> upgrade --harness codex` and restart Codex afterwards, so the new skills and hooks are loaded. The run fetches the shell at that version, atomically replaces its stable source, and `codex plugin add projectstore@projectstore-npx` materialises the new version; this was measured across two dev builds on codex-cli 0.153.4.",
341
340
  "agent_translation": {
342
341
  "carries_model": false,
343
342
  "carries_model_reason": "The surface projectstore ships — a plugin's agents/ entry — carries no model on Codex: surfaces.agents is supported:false because no route from a plugin's agents/ directory to collaborationspawn_agent has been found (measured). Codex DOES have a definition file that carries model and effort, recorded below, but it is project- or user-scoped and not plugin-shippable, so contract 12 holds as written: what we ship cannot carry a model. Was true here until 2026-09-08, which contradicted both contract 12 and this file's own agents row.",
@@ -40,6 +40,9 @@ import {
40
40
  segments,
41
41
  stepOf,
42
42
  storyFiles,
43
+ storyIsCurrent,
44
+ storyKeyOf,
45
+ storyLayout,
43
46
  subjectLabel,
44
47
  subjectPath,
45
48
  totalOf,
@@ -80,6 +83,9 @@ const steps = atom({ plugin: 'projectstore', key: 'steps' } as const, {} as Reco
80
83
 
81
84
  type Json = Record<string, any>
82
85
  type Rendered = 'drew' | 'survey' | 'nothing' | 'unknown'
86
+ // One story file as the scan read it: its key (storyKeyOf), title and status.
87
+ type StoryItem = { key: string; title: string | null; status: string | null }
88
+ type Stories = { counts: BandCounts; items: StoryItem[] }
83
89
 
84
90
  // What one load of the module remembers between events.
85
91
  type Band = {
@@ -90,7 +96,7 @@ type Band = {
90
96
  gen: number
91
97
  scan: { gen: number; startedAt: number } | null
92
98
  shownKey: string
93
- stories: Map<string, { sig: string; counts: BandCounts }>
99
+ stories: Map<string, { sig: string } & Stories>
94
100
  shipped: Map<string, Json>
95
101
  place: { vault: string | null; kinds: string[] }
96
102
  }
@@ -180,10 +186,11 @@ async function layoutOf($: EngineInterface, b: Band, name: string) {
180
186
 
181
187
  // ── The scan (contracts 2, 4–9, 14) ───────────────────────────────────
182
188
 
183
- // The epic's stories by stage, re-read only when a listing changed. A story
184
- // that cannot be read keeps the last good counts (none yet: no progress row),
185
- // never a partial fold — the band draws less, never wrong.
186
- async function countsOf($: EngineInterface, b: Band, epicDir: string): Promise<BandCounts | null> {
189
+ // The epic's stories by stage, and each one's key, title and status (the
190
+ // story row's, contract 8), re-read only when a listing changed. A story that
191
+ // cannot be read keeps the last good result (none yet: no progress row and no
192
+ // story row), never a partial fold — the band draws less, never wrong.
193
+ async function storiesOf($: EngineInterface, b: Band, epicDir: string): Promise<Stories | null> {
187
194
  const seen: string[] = []
188
195
  const fs = {
189
196
  list: async (dir: string) => {
@@ -213,18 +220,38 @@ async function countsOf($: EngineInterface, b: Band, epicDir: string): Promise<B
213
220
  const files: string[] = await storyFiles(fs, epicDir)
214
221
  const sig = seen.sort().join('|')
215
222
  const hit = b.stories.get(epicDir)
216
- if (hit && hit.sig === sig) return hit.counts
223
+ if (hit && hit.sig === sig) return hit
217
224
  const statuses: unknown[] = []
225
+ const items: StoryItem[] = []
218
226
  for (const f of files) {
219
227
  const text = await readText($, f)
220
- // An unreadable story (an evicted iCloud file): the last good counts, and
228
+ // An unreadable story (an evicted iCloud file): the last good result, and
221
229
  // nothing cached, so the next scan tries again.
222
- if (text === null) return hit?.counts ?? null
223
- statuses.push((parseFrontmatter(text).data as Json).status)
230
+ if (text === null) return hit ?? null
231
+ const data = parseFrontmatter(text).data as Json
232
+ statuses.push(data.status)
233
+ const key = storyKeyOf(f.slice(epicDir.length + 1))
234
+ if (key) {
235
+ items.push({
236
+ key,
237
+ title: typeof data.title === 'string' && data.title ? data.title : null,
238
+ status: data.status === null || data.status === undefined || data.status === '' ? null : String(data.status),
239
+ })
240
+ }
224
241
  }
225
- const counts = foldStages(statuses) as BandCounts
226
- b.stories.set(epicDir, { sig, counts })
227
- return counts
242
+ const next = { sig, counts: foldStages(statuses) as BandCounts, items }
243
+ b.stories.set(epicDir, next)
244
+ return next
245
+ }
246
+
247
+ // This session's story, from its file (contract 8): the item the pointer's
248
+ // key names, while it is work the session is moving. Never the pointer's own
249
+ // title or status — the pointer can be older than the file.
250
+ function storyOf(pointer: Json | null, stories: Stories | null) {
251
+ const key = pointer && typeof pointer.active_story === 'string' ? (pointer.active_story as string) : null
252
+ const item = key && stories ? stories.items.find(i => i.key === key) : null
253
+ if (!item || !storyIsCurrent(item.status)) return null
254
+ return { title: item.title || item.key, status: item.status }
228
255
  }
229
256
 
230
257
  async function scan($: EngineInterface, b: Band, root: string, sid: string | null): Promise<BandView | null> {
@@ -258,16 +285,14 @@ async function scan($: EngineInterface, b: Band, root: string, sid: string | nul
258
285
  if (text === null) return null
259
286
  const fm = parseFrontmatter(text).data as Json
260
287
  const registry = await shippedJson($, b, 'scaffold/headings.json')
261
- const story = focus.why === 'this' && pointer && typeof pointer.story_title === 'string' && pointer.story_title
262
- ? { title: pointer.story_title as string, status: typeof pointer.story_status === 'string' ? (pointer.story_status as string) : null }
263
- : null
288
+ const stories = await storiesOf($, b, epicDir)
264
289
  return {
265
290
  id: typeof fm.id === 'string' && fm.id ? fm.id : focus.id,
266
291
  title: typeof fm.title === 'string' && fm.title ? fm.title : focus.id,
267
292
  goal: goalOf(text, goalForms(registry, lang)),
268
- counts: await countsOf($, b, epicDir),
293
+ counts: stories ? stories.counts : null,
269
294
  why: focus.why,
270
- story,
295
+ story: focus.why === 'this' ? storyOf(pointer as Json | null, stories) : null,
271
296
  version: binding.statusline?.show_version === false ? null : await versionOf($, b),
272
297
  hint: band.hint_seen !== true,
273
298
  words,
@@ -466,11 +491,14 @@ export const register: Register = on => {
466
491
  }
467
492
  }
468
493
 
469
- const story = rows.includes('story') && v.story ? (
494
+ // The title is cut here so the status is never the part the host's
495
+ // truncation takes (contract 8); truncate-end stays as a backstop.
496
+ const sl = v.story ? storyLayout(w.band_story, v.story.title, v.story.status, e.props.bodyColumns) : null
497
+ const story = rows.includes('story') && sl ? (
470
498
  <Text wrap="truncate-end">
471
499
  <Text dimColor> {w.band_story} </Text>
472
- <Text color="cyan">{v.story.title}</Text>
473
- <Text dimColor>{v.story.status ? ` · ${v.story.status.replace(/[_-]/g, ' ')}` : ''}</Text>
500
+ <Text color="cyan">{sl.title}</Text>
501
+ <Text dimColor>{sl.tail}</Text>
474
502
  </Text>
475
503
  ) : null
476
504
 
@@ -57,10 +57,7 @@ async function readActivityBudgeted(vault, sid, budgetMs) {
57
57
  const deadline = new Promise((resolve) => {
58
58
  timer = setTimeout(() => resolve(null), budgetMs);
59
59
  });
60
- const out = await Promise.race([
61
- readActivityAsync(vault, sid).catch(() => []),
62
- deadline,
63
- ]);
60
+ const out = await Promise.race([readActivityAsync(vault, sid).catch(() => []), deadline]);
64
61
  clearTimeout(timer);
65
62
  return out;
66
63
  }
@@ -85,9 +82,7 @@ async function main() {
85
82
  try {
86
83
  layout = loadLayout(cfg.layout);
87
84
  } catch {}
88
- const artifact = activity && layout
89
- ? resolveInFlightArtifact(activity, layout, cfg.vault_path)
90
- : null;
85
+ const artifact = activity && layout ? resolveInFlightArtifact(activity, layout, cfg.vault_path) : null;
91
86
 
92
87
  const parts = [`vault ${basename(cfg.vault_path)}`];
93
88
  // Dropped rather than rendered empty when nothing resolves.
@@ -105,9 +100,11 @@ async function main() {
105
100
  // Recent files are a separate referent with a separate condition: an empty
106
101
  // log renders no continuity section, so the stronger wording would be false
107
102
  // on a path the activity log's last-tool-wins dedupe makes ordinary.
108
- parts.push(activity && activity.length > 0
109
- ? "orientation and this session's recent files return at session start"
110
- : "orientation returns at session start");
103
+ parts.push(
104
+ activity && activity.length > 0
105
+ ? "orientation and this session's recent files return at session start"
106
+ : "orientation returns at session start",
107
+ );
111
108
  } else {
112
109
  // True on every path: plugin commands are installation-scoped and not
113
110
  // gated by auto_inject.
@@ -25,11 +25,13 @@ import { readConfig, readStdinJson, adoptHookInput, roleForm, sharedRoleForm } f
25
25
  // told once that both name one role — otherwise the second rule below would
26
26
  // have it report the block as a contradiction every session.
27
27
  function rules() {
28
- const critic = roleForm("critic"), reviewer = roleForm("reviewer");
28
+ const critic = roleForm("critic"),
29
+ reviewer = roleForm("reviewer");
29
30
  const shared = sharedRoleForm("critic");
30
- const bridge = shared === critic
31
- ? ""
32
- : `\n\nThe \`AGENTS.md\` block may name roles as \`${shared}\`; that is the same role, called \`${critic}\` here.`;
31
+ const bridge =
32
+ shared === critic
33
+ ? ""
34
+ : `\n\nThe \`AGENTS.md\` block may name roles as \`${shared}\`; that is the same role, called \`${critic}\` here.`;
33
35
  return `# projectstore — standing rules for this session
34
36
 
35
37
  **Artifact-first order.** A feature-sized request opens a vault artifact before
@@ -46,9 +48,11 @@ contradiction becomes invisible to the person who could have settled it.${bridge
46
48
  }
47
49
 
48
50
  function emit(additionalContext) {
49
- process.stdout.write(JSON.stringify({
50
- hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
51
- }) + "\n");
51
+ process.stdout.write(
52
+ JSON.stringify({
53
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
54
+ }) + "\n",
55
+ );
52
56
  }
53
57
 
54
58
  function main() {
@@ -70,6 +74,8 @@ function main() {
70
74
  emit(rules());
71
75
  }
72
76
 
73
- try { main(); } catch {
77
+ try {
78
+ main();
79
+ } catch {
74
80
  // A hook must never break session startup.
75
81
  }
@@ -13,7 +13,7 @@
13
13
  // open. Bounded and O(1) in vault size by construction.
14
14
 
15
15
  import { existsSync, writeFileSync, mkdirSync } from "node:fs";
16
- import { join, dirname } from "node:path";
16
+ import { dirname } from "node:path";
17
17
  import {
18
18
  readConfig,
19
19
  gatherVaultFacts,
@@ -30,6 +30,7 @@ import {
30
30
  ensureStateRoot,
31
31
  syncStatusLine,
32
32
  cleanupStaleSessionState,
33
+ refreshStoryPointer,
33
34
  armReminder,
34
35
  truncEnd,
35
36
  truncFront,
@@ -64,9 +65,10 @@ function welcomedMarkerWritePath(proj) {
64
65
  // the one instruction it carries branches on the binding; everything else is
65
66
  // the same message.
66
67
  function buildWelcome(cfg = null) {
67
- const start = cfg && cfg.vault_path
68
- ? `**Already bound**: this project's vault is \`${truncFront(String(cfg.vault_path), PATH_CELL)}\`. Ask for what you want — the agent picks up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation, and you approve every write. If the vault has no folders yet, \`${commandForm("scaffold")}\` lays them out.`
69
- : `**To start using it**: run \`${commandForm("bind", { args: "<vault-path>" })}\` and point it at an Obsidian vault (or any folder). After that, the agent will pick up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation; you only approve the writes.`;
68
+ const start =
69
+ cfg && cfg.vault_path
70
+ ? `**Already bound**: this project's vault is \`${truncFront(String(cfg.vault_path), PATH_CELL)}\`. Ask for what you want — the agent picks up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation, and you approve every write. If the vault has no folders yet, \`${commandForm("scaffold")}\` lays them out.`
71
+ : `**To start using it**: run \`${commandForm("bind", { args: "<vault-path>" })}\` and point it at an Obsidian vault (or any folder). After that, the agent will pick up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation; you only approve the writes.`;
70
72
  return [
71
73
  "# 👋 projectstore is loaded for the first time in this project",
72
74
  "",
@@ -178,15 +180,25 @@ async function main() {
178
180
  // an unbound one that is not a worktree pays a single ~10 ms probe.
179
181
  let binding = null;
180
182
  if (!cfg) {
181
- try { binding = resolveBinding(proj); } catch {}
183
+ try {
184
+ binding = resolveBinding(proj);
185
+ } catch {}
182
186
  }
183
187
  const welcome = showWelcomeOnce(proj, cfg);
184
188
  // The person's channel carries one instruction, and it has to be the right
185
- // one: a bound project is told what it is bound to, not to bind again.
189
+ // one: a bound project is told what it is bound to, not to bind again. A
190
+ // command stands alone on its line, the update hint on lines of its own
191
+ // after it (the story "User-facing text says where to run…", rules 4 and 5).
186
192
  const welcomeSystemMessage = welcome
187
- ? (cfg && cfg.vault_path
193
+ ? cfg && cfg.vault_path
188
194
  ? `👋 projectstore: first-run welcome shown. Bound to ${cfg.vault_path}. ${updateHint().line}`
189
- : `👋 projectstore: first-run welcome shown. Start with ${commandForm("bind", { args: "<vault-path>" })}. ${updateHint().line}`)
195
+ : [
196
+ "👋 projectstore: first-run welcome shown. Start with:",
197
+ commandForm("bind", { args: "<vault-path>" }),
198
+ updateHint().line,
199
+ ]
200
+ .filter(Boolean)
201
+ .join("\n")
190
202
  : null;
191
203
 
192
204
  if (!cfg) {
@@ -196,8 +208,10 @@ async function main() {
196
208
  // whose parent is already bound. Ordering is the cheap fix; the small
197
209
  // redundancy is accepted rather than papered over with copy that will rot.
198
210
  const offer = binding && binding.state === "inheritable" ? bindingOfferText(binding) : "";
211
+ // What binding does first, then the command alone on its line: an
212
+ // in-session form that can carry the parent's vault path (rule 4).
199
213
  const offerSystemMessage = offer
200
- ? `projectstore: this worktree is unbound — ${inheritForm(binding.vaultPath, { layout: binding.layout, language: binding.language })} ${bindInherits() ? "adopts the binding of" : "binds it to the vault, layout and language of"} the checkout it was forked from.`
214
+ ? `projectstore: this worktree is unbound. ${bindInherits() ? "Adopt the binding of" : "Bind it to the vault, layout and language of"} the checkout it was forked from, in the session:\n${inheritForm(binding.vaultPath, { layout: binding.layout, language: binding.language })}`
201
215
  : null;
202
216
  const body = offer + (welcome || "");
203
217
  if (body) {
@@ -213,10 +227,23 @@ async function main() {
213
227
  // Opt-in status line: keep settings.local.json pointed at this plugin
214
228
  // version's statusline.mjs (self-heals on update). Best-effort; a settings
215
229
  // write must never break session-context injection.
216
- try { syncStatusLine(cfg, proj); } catch {}
230
+ try {
231
+ syncStatusLine(cfg, proj);
232
+ } catch {}
217
233
  // Statusline-feature housekeeping, like syncStatusLine — must run even when
218
234
  // auto_inject=false (touch-session writes pointers regardless of it).
219
- try { cleanupStaleSessionState(proj); } catch {}
235
+ try {
236
+ cleanupStaleSessionState(proj);
237
+ } catch {}
238
+ // This session's story fields from the story file, once (ADR-006, amended
239
+ // 2026-10-12): a pointer written before a close, or a story closed elsewhere
240
+ // since, stops naming finished work. A bounded read; a lost race writes
241
+ // nothing.
242
+ if (sid) {
243
+ try {
244
+ await refreshStoryPointer(proj, cfg.vault_path, sid);
245
+ } catch {}
246
+ }
220
247
 
221
248
  // Arm BEFORE the auto_inject gate. The entry reminder's markers must be
222
249
  // re-armed after a compaction whether or not this session injects context —
@@ -231,7 +258,9 @@ async function main() {
231
258
  // context while its marker persists on disk. Arming lets it fire once more;
232
259
  // the cap of two is enforced by the election, not here.
233
260
  if (sid && (input?.source === "compact" || input?.source === "clear")) {
234
- try { armReminder(proj, sid); } catch {}
261
+ try {
262
+ armReminder(proj, sid);
263
+ } catch {}
235
264
  }
236
265
 
237
266
  if (cfg.auto_inject === false) {
@@ -272,23 +301,26 @@ async function main() {
272
301
  }
273
302
  }
274
303
 
275
- // Cheap install-only doctor subset (ADR-005): one line, only when N > 0;
276
- // aborted past its budget rather than reporting a false "clean".
304
+ // Cheap install-only doctor subset (ADR-005): one part, only when N > 0;
305
+ // aborted past its budget rather than reporting a false "clean". Its doctor
306
+ // form stands alone on its line, as every command of this line does.
277
307
  let doctorMsg = null;
278
308
  let offers = [];
279
309
  try {
280
310
  const r = runStartupChecks(cfg, proj);
281
311
  if (r.skipped) {
282
- doctorMsg = `projectstore doctor: startup checks skipped — run ${commandForm("doctor")}`;
312
+ doctorMsg = `projectstore doctor: startup checks skipped. Run them in the session:\n${commandForm("doctor")}`;
283
313
  } else if (r.count > 0) {
284
- doctorMsg = `projectstore doctor: ${r.count} install issue(s) — run ${commandForm("doctor")}`;
314
+ doctorMsg = `projectstore doctor: ${r.count} install issue(s). See them in the session:\n${commandForm("doctor")}`;
285
315
  }
286
316
  // Offers (doctor's OFFER_CHECKS): one-time steps a user should see once,
287
317
  // e.g. the re-stamp after a plugin update — not issues, not silent.
288
318
  offers = (r.offers || []).map((m) => `projectstore: ${m}`);
289
319
  } catch {}
290
- const systemMessage =
291
- [welcomeSystemMessage, doctorMsg, ...offers].filter(Boolean).join(" · ") || null;
320
+ // Each part on lines of its own: an offer ends with its command alone on
321
+ // its line, and a part joined after it with ` · ` would put text after the
322
+ // command (the story "User-facing text says where to run…", rule 4).
323
+ const systemMessage = [welcomeSystemMessage, doctorMsg, ...offers].filter(Boolean).join("\n") || null;
292
324
 
293
325
  if (gatherError) {
294
326
  emit(