projectstore-claude 0.29.3 → 0.29.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/bin/projectstore-claude.mjs +1 -1
  2. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  3. package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
  4. package/node_modules/projectstore/README.md +17 -0
  5. package/node_modules/projectstore/commands/bind.md +6 -17
  6. package/node_modules/projectstore/commands/doctor.md +12 -7
  7. package/node_modules/projectstore/commands/statusline.md +1 -1
  8. package/node_modules/projectstore/commands/story.md +15 -2
  9. package/node_modules/projectstore/docs/how-it-works.md +5 -3
  10. package/node_modules/projectstore/harnesses/claude-code.json +5 -5
  11. package/node_modules/projectstore/harnesses/codex.json +4 -5
  12. package/node_modules/projectstore/hooks/band.tsx +48 -20
  13. package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
  14. package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
  15. package/node_modules/projectstore/hooks/session-start.mjs +50 -18
  16. package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
  17. package/node_modules/projectstore/package.json +11 -2
  18. package/node_modules/projectstore/scripts/band-shared.mjs +141 -24
  19. package/node_modules/projectstore/scripts/band.mjs +72 -21
  20. package/node_modules/projectstore/scripts/binding.mjs +616 -85
  21. package/node_modules/projectstore/scripts/build-adapters.mjs +73 -29
  22. package/node_modules/projectstore/scripts/cli.mjs +1015 -185
  23. package/node_modules/projectstore/scripts/codemap.mjs +22 -7
  24. package/node_modules/projectstore/scripts/convergence.mjs +253 -83
  25. package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
  26. package/node_modules/projectstore/scripts/doctor-report.mjs +91 -23
  27. package/node_modules/projectstore/scripts/doctor.mjs +1317 -371
  28. package/node_modules/projectstore/scripts/draft.mjs +5 -2
  29. package/node_modules/projectstore/scripts/fetch-shell.mjs +106 -20
  30. package/node_modules/projectstore/scripts/graph.mjs +19 -13
  31. package/node_modules/projectstore/scripts/harness.mjs +123 -48
  32. package/node_modules/projectstore/scripts/install-harness.mjs +3511 -616
  33. package/node_modules/projectstore/scripts/kanban.mjs +2 -1
  34. package/node_modules/projectstore/scripts/lib.mjs +694 -159
  35. package/node_modules/projectstore/scripts/mcp.mjs +386 -105
  36. package/node_modules/projectstore/scripts/portable-registration.mjs +224 -51
  37. package/node_modules/projectstore/scripts/provenance.mjs +26 -10
  38. package/node_modules/projectstore/scripts/query.mjs +378 -80
  39. package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
  40. package/node_modules/projectstore/scripts/run-host.mjs +62 -17
  41. package/node_modules/projectstore/scripts/scaffold.mjs +89 -28
  42. package/node_modules/projectstore/scripts/setup.mjs +987 -0
  43. package/node_modules/projectstore/scripts/stages.mjs +5 -1
  44. package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
  45. package/node_modules/projectstore/scripts/statusline.mjs +32 -14
  46. package/node_modules/projectstore/scripts/story-section.mjs +39 -17
  47. package/node_modules/projectstore/scripts/surfaces.mjs +416 -95
  48. package/node_modules/projectstore/scripts/term.mjs +382 -97
  49. package/node_modules/projectstore/scripts/tokens.mjs +106 -28
  50. package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
  51. package/node_modules/projectstore/scripts/version-guard.mjs +74 -27
  52. package/node_modules/projectstore/scripts/worktree.mjs +16 -5
  53. package/package.json +2 -2
@@ -24,7 +24,7 @@ const HARNESS = "claude-code";
24
24
  // scripts/cli.mjs; the packaging test pins it). Any other verb — doctor,
25
25
  // status, search, --version — passes through untouched: the core refuses an
26
26
  // option a verb does not declare, so a blanket insert would break `doctor`.
27
- const HARNESS_VERBS = new Set(["plan","install","uninstall","upgrade","agents"]);
27
+ const HARNESS_VERBS = new Set(["setup","plan","install","uninstall","upgrade","agents"]);
28
28
 
29
29
  // The bundled core, by path — never by package resolution: a hoisted or global
30
30
  // copy at another version is exactly the pairing the pin exists to prevent,
@@ -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.29.3",
15
+ "version": "0.29.4",
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.29.3",
4
+ "version": "0.29.4",
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",
@@ -307,6 +307,23 @@ See [`docs/extending.md`](./docs/extending.md) for adding layouts, templates, an
307
307
 
308
308
  Issues and discussions: https://github.com/SmartAndPoint/ProjectStore/issues. PRs welcome — adding a layout is a good first contribution (see `scaffold/layouts/engineering.json` for the format).
309
309
 
310
+ The published package has no dependencies; the linter and the formatter are dev-only, pinned in `package-lock.json`. Every pull request runs the tests, ESLint and Prettier:
311
+
312
+ ```sh
313
+ npm test # Node 20+, no install needed
314
+ npm ci # once, for the lint tools (Node 20.19+, 22.13+ or 24+)
315
+ npm run lint
316
+ npm run format # rewrites; npm run format:check only reports
317
+ ```
318
+
319
+ One formatting-only commit is listed in `.git-blame-ignore-revs`. GitHub skips it in blame by itself; a local clone needs this once:
320
+
321
+ ```sh
322
+ git config blame.ignoreRevsFile .git-blame-ignore-revs
323
+ ```
324
+
325
+ A branch opened before that commit rebases in two steps, so that the lint fixes before it are kept: `git rebase <the commit before it>` (real conflicts, resolved as usual), then `git rebase --onto <it> <the commit before it>`, where every conflict is formatting only: take your side, run `npx prettier --write <file>`, `git add` it and continue.
326
+
310
327
  ## License
311
328
 
312
329
  MIT — see [`LICENSE`](./LICENSE).
@@ -3,7 +3,7 @@ description: Bind this project to an Obsidian vault (or any markdown directory)
3
3
  argument-hint: <vault-path> | --inherit [--layout engineering] [--lang en|ru|es|de|fr|zh]
4
4
  ---
5
5
 
6
- You are binding the current project to a markdown vault for projectstore. The config is written by the core's `bind` / `init` verbs (roadmap A8); this interview decides the values, previews them, and asks — it never writes `projectstore.json` itself, except on the inherit path (step 0a), which copies the parent's file verbatim, and the two later stamps in steps 11–12 (`autoupdate_asked`, `statusline`), which `Edit` the file the verb wrote. The interview's `--lang` is the verb's `--language`.
6
+ You are binding the current project to a markdown vault for projectstore. The config is written by the core's `bind` / `init` verbs (roadmap A8); this interview decides the values, previews them, and asks — it never writes `projectstore.json` itself, except on the inherit path (step 0a), which copies the parent's file verbatim, and the one later stamp in step 11 (`statusline`), which `Edit`s the file the verb wrote. The interview's `--lang` is the verb's `--language`.
7
7
 
8
8
  Parse `$ARGUMENTS`:
9
9
  - First positional arg: vault path. Expand `~` if present.
@@ -48,10 +48,10 @@ Steps:
48
48
 
49
49
  It prints `{state, worktree, mainCheckout, vaultPath}` and writes nothing.
50
50
  - `state` is **not** `inheritable` → say why in one line (not a worktree, its parent is unbound too, or git could not answer) and fall through to step 1, which needs a vault path; if none was given, ask for one.
51
- - `state` is `inheritable` → read the parent's `<mainCheckout>/.projectstore/projectstore.json` (or, in a checkout not yet migrated, the legacy `<mainCheckout>/.claude/projectstore.json`), show it **verbatim as a code block** — but for a `channel_switch` key, which is left out (below) — and ask via AskUserQuestion: "Adopt the binding of `<mainCheckout>` (vault `<vaultPath>`)?" — Yes / No.
52
- - On **Yes**: Write that JSON verbatim, without `channel_switch`, to `<project>/.projectstore/projectstore.json`, **and write `<project>/.projectstore/.gitignore` beside it** with the two lines `projectstore.json` and `state/` under a comment saying they are machine-local (the verb writes them itself on every other path; this one bypasses the verb, and without them the worktree commits an absolute vault path into the index it shares with its parent). If that file already exists, add any missing line and leave the rest — it is line-merged. Then **jump straight to step 10** (the summary). Steps 1–9 and 11–12 are decisions the parent already made and the copied config already carries — layout, language, statusline, agent models, auto-update. Do not re-ask them, and do not scaffold: the vault exists and is shared.
51
+ - `state` is `inheritable` → read the parent's `<mainCheckout>/.projectstore/projectstore.json` (or, in a checkout not yet migrated, the legacy `<mainCheckout>/.claude/projectstore.json`), show it **verbatim as a code block** — but for the `channel_switch` and `channel_keep` keys, which are left out (below) — and ask via AskUserQuestion: "Adopt the binding of `<mainCheckout>` (vault `<vaultPath>`)?" — Yes / No.
52
+ - On **Yes**: Write that JSON verbatim, without `channel_switch` and `channel_keep`, to `<project>/.projectstore/projectstore.json`, **and write `<project>/.projectstore/.gitignore` beside it** with the two lines `projectstore.json` and `state/` under a comment saying they are machine-local (the verb writes them itself on every other path; this one bypasses the verb, and without them the worktree commits an absolute vault path into the index it shares with its parent). If that file already exists, add any missing line and leave the rest — it is line-merged. Then **jump straight to step 10** (the summary). Steps 1–9 and 11 are decisions the parent already made and the copied config already carries — layout, language, statusline, agent models. Do not re-ask them, and do not scaffold: the vault exists and is shared.
53
53
  - Copy the **binding only** (`projectstore.json`). Never copy `.projectstore/state/` — per-session state belonging to the other checkout — and never `harness/`: the overlays are committed and arrive from git.
54
- - **Never copy `channel_switch`.** It records that the parent checkout was switched to the npm install — a machine-local fact of that checkout, not of the binding it shares. Every other key is copied verbatim.
54
+ - **Never copy `channel_switch` or `channel_keep`.** They record which channel the parent checkout chose — switched to the npm install, or kept on the git marketplace — a machine-local fact of that checkout's own registry rows, not of the binding it shares. Every other key is copied verbatim.
55
55
  - Do not add a provenance key to the config. The parent is resolvable from git at any time; a key would be a second source of truth for the same fact.
56
56
 
57
57
  1. **Validate the vault path** read-only first: `ls -A "<path>"` (the verb has no dry run — running it on a fresh project would write the config before step 4's approval). If it does not exist, or exists and is empty, ask the user (via AskUserQuestion) whether to create the vault there; on Yes, step 5 runs `init` instead of `bind`. `init` creates the whole vault and binds: the directory (unless it is there and empty), its git repository with no commit, and the layout's folders and READMEs. Never `mkdir` it yourself. A directory that already holds files is bound with `bind`; `init` would bind it too, but never scaffolds or `git init`s inside it.
@@ -81,7 +81,7 @@ Steps:
81
81
  node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" bind "<vault-path>" [--layout <name>] [--language <code>] [--rebind]
82
82
  ```
83
83
 
84
- `init "<vault-path>" …` instead when step 1 chose to create the vault — it creates the whole vault, so step 7 has nothing to offer after it; `--rebind` only when step 0 ended on **Replace bind**. Naming the vault is the verb's confirmation (there is no `--yes`); the interview's AskUserQuestion in step 4 is the in-session gate. Print the verb's output. A non-zero exit is a refusal or a usage error — relay it and stop — except an `init` whose output reports `git init failed` or `Scaffold failed`: the binding is written, so relay the remedy line it prints and continue. Steps 11 and 12 below `Edit` the file this step wrote; keep them after it.
84
+ `init "<vault-path>" …` instead when step 1 chose to create the vault — it creates the whole vault, so step 7 has nothing to offer after it; `--rebind` only when step 0 ended on **Replace bind**. Naming the vault is the verb's confirmation (there is no `--yes`); the interview's AskUserQuestion in step 4 is the in-session gate. Print the verb's output. A non-zero exit is a refusal or a usage error — relay it and stop — except an `init` whose output reports `git init failed` or `Scaffold failed`: the binding is written, so relay the remedy line it prints and continue. Step 11 below `Edit`s the file this step wrote; keep it after it.
85
85
 
86
86
  6. **Check `.gitignore`**: read `<project>/.gitignore` if it exists. Our own files are self-ignored inside `.projectstore/` — the verb writes that `.gitignore` itself, carrying `projectstore.json` and `state/`, with `harness/` committed on purpose (the layout ADR); on the inherit path of step 0a, which bypasses the verb, you wrote both by hand. Unless `.claude/` is ignored wholesale, the one machine-specific entry left is the host's `.claude/settings.local.json`. If it is missing, offer (AskUserQuestion) to append it. If the user declines, skip silently.
87
87
 
@@ -115,18 +115,7 @@ Steps:
115
115
 
116
116
  10. **Print summary**: confirm the bind, list the layout's folders, suggest next commands (`/projectstore:status`, `/projectstore:adr "<first decision>"`, `/projectstore:epic <ID> "<title>"`).
117
117
 
118
- 11. **Auto-update reminder** (v0.7+, only on first successful bind in this project): After Step 5 (config write), check whether the newly-written config has `autoupdate_asked: true`. If not, ask the user via AskUserQuestion:
119
-
120
- > "Claude Code does not auto-update third-party marketplaces by default. Want to enable auto-update for the SmartAndPoint marketplace so you'll be notified about future projectstore releases?"
121
-
122
- Options:
123
- - **Yes, show me how** (Recommended) — respond with: "Open `/plugin` → **Marketplaces** tab → **SmartAndPoint** → toggle **auto-update** on. New releases (v0.7+) will be detected at Claude Code startup; you'll need to run `/reload-plugins` after the notification to activate them."
124
- - **No, I'll handle it manually** — respond with: "OK. To pull the latest version at any time, run `/plugin marketplace update SmartAndPoint`, then `/reload-plugins`."
125
- - **Already enabled** — respond with: "Great. New releases will be detected at the next Claude Code startup."
126
-
127
- After the question is answered (regardless of choice), Edit `<project>/.projectstore/projectstore.json` to add `"autoupdate_asked": true` to the JSON object. This guarantees we ask only once per project.
128
-
129
- 12. **Status line offer** (v0.13, ADR-006 — the final step, language is known by now): read `${CLAUDE_PLUGIN_ROOT}/templates/<lang>/strings.json` (fall back to `en`) and the plugin version, then show the fully rendered example:
118
+ 11. **Status line offer** (v0.13, ADR-006 — the final step, language is known by now): read `${CLAUDE_PLUGIN_ROOT}/templates/<lang>/strings.json` (fall back to `en`) and the plugin version, then show the fully rendered example:
130
119
 
131
120
  > `[PS#<version>] 📚 <statusline_example_epic> › <statusline_example_story> (in-progress)`
132
121
 
@@ -66,20 +66,25 @@ You are running projectstore diagnostics (ADR-005: umbrella doctor).
66
66
  The verbs refuse it in code; this clause is the belt.
67
67
  - `version-drift` → report only: name both versions and where each was
68
68
  read; the fix is the host's update path (`/plugin update` for a git-marketplace
69
- copy; for the npm registration, the `projectstore-claude` shell's `upgrade`
70
- from a terminal — the `plugin-registration` finding spells the command),
69
+ copy; for the npm registration, the package's `upgrade` from a terminal
70
+ in the project — the `plugin-registration` finding spells the command),
71
71
  not ours.
72
72
  - `layout-legacy` (warn; the startup line carries it as an offer) → the project
73
73
  still holds the pre-0.28 layout (`.claude/projectstore.json`,
74
- `.claude/.projectstore/` — legacy, read through 0.29). The migration is one
74
+ `.claude/.projectstore/` — legacy, read through 0.30). The migration is one
75
75
  previewed `layout` item of `upgrade`, run **from a terminal outside this
76
76
  session** (it moves files this session reads and writes; the verb defers
77
77
  inside one). The finding names the form for the channel this plugin was
78
78
  installed through: the installed copy's own `bin/projectstore.mjs` for a
79
- git-marketplace install or a checkout, the `projectstore-claude` shell for
80
- the npm registration. Relay the finding's command verbatim and never
81
- substitute the other form — the shell, run for a git-marketplace install,
82
- would also move this checkout to the npm channel. When the finding carries
79
+ git-marketplace install or a checkout, the package's `upgrade` for the npm
80
+ registration. Relay the finding's command verbatim, with the directory its
81
+ words name, and never substitute the other form or drop its
82
+ `--no-register`. Both forms carry that flag, so neither changes the
83
+ channel: the named form runs the copy this project loads, so the move
84
+ re-stamps the launcher that copy renders, while the other form runs a
85
+ different copy. The package's `upgrade` without `--no-register` would
86
+ register the npm copy and turn a git-marketplace copy off for this
87
+ checkout. When the finding carries
83
88
  advice instead of a command — the installed copy predates the move, or
84
89
  predates `--no-register` — relay the advice and never compose a command
85
90
  yourself. Never move the files yourself. While this finding is in the report, its command is also the one
@@ -5,7 +5,7 @@ argument-hint: "on | off | status"
5
5
 
6
6
  You are toggling the projectstore status line for THIS project. When enabled, `install-harness.mjs` points the project's `.claude/settings.local.json` at a launcher it writes at `.projectstore/state/claude-code/statusline.mjs`. That path carries no version, so it never goes stale: the launcher resolves the *currently installed* plugin on every render, and an update is reflected on the spot instead of one restart later. (A dev checkout or `--plugin-dir` root is wired straight to its `scripts/statusline.mjs` — those paths have no version to drift.) The line renders `[PS#<version>] 📚 <epic> › <story> (<status>)` **composed above** any existing HUD (e.g. oh-my-claudecode), never replacing it. While this session's band above the prompt is drawing (`/projectstore:bar`), the line gives way to it — your base HUD stays — and it comes back within 30 s of the band stopping.
7
7
 
8
- Resolution is **per-session with zero cross-session and zero vault reads** (ADR-006): the 📚 segment comes from this session's pointer (`.projectstore/state/sessions/<session_id>.json`, maintained by the PreToolUse hook); a fresh session shows an explicit localized cold-start line ("No epic or story in this session yet" / «Эпик и стори ещё не в работе в этой сессии») — never a silent blank; a corrupt pointer shows an error-marked string. Strings localize via `templates/<lang>/strings.json` (en fallback). The version badge is controlled by `statusline.show_version` (default true).
8
+ Resolution is **per-session with zero cross-session and zero vault reads** (ADR-006): the 📚 segment comes from this session's pointer (`.projectstore/state/sessions/<session_id>.json`, maintained by the tool hooks — a write's patch after the write — by `story close`, and at session start; a done, closed or parked story leaves the segment); a fresh session shows an explicit localized cold-start line ("No epic or story in this session yet" / «Эпик и стори ещё не в работе в этой сессии») — never a silent blank; a corrupt pointer shows an error-marked string. Strings localize via `templates/<lang>/strings.json` (en fallback). The version badge is controlled by `statusline.show_version` (default true).
9
9
 
10
10
  This command flips the flag in `.projectstore/projectstore.json` and then runs the install verb, which writes `settings.local.json` and the launcher behind a preview. The SessionStart hook only refreshes an entry that is already ours and removes it when the flag is off — it never creates one.
11
11
 
@@ -92,11 +92,11 @@ the two cannot collide).
92
92
  <baseline-path>`), the derived targets (`kanban`, plus `indexes=<epic
93
93
  folder>` when status changed), and the doctor pre-state line. On a clean
94
94
  report (`verbatim: true`, `stopped_at: null`, doctor no worse than the
95
- pre-state) skip steps 6-6b — the clerk's report is the write evidence. On a
95
+ pre-state) skip steps 6-6b and run step 6c — the clerk's report is the write evidence. On a
96
96
  stopped report: fix what diverged, then re-delegate (the resume rule: after
97
97
  the copy, the ceremony restarts at reconcile, not at the race gate) or
98
98
  finish the remaining steps yourself (the copy is idempotent).
99
- - No clerk available → perform steps 6-6b yourself. There is no fallback
99
+ - No clerk available → perform steps 6-6c yourself. There is no fallback
100
100
  agent: a general-purpose writer is an unpinned procedure.
101
101
 
102
102
  6. **On Yes** (undelegated path): Write the full file. On a `plan`, finish by
@@ -111,3 +111,16 @@ the two cannot collide).
111
111
  — a close is not done while doctor got worse. Then suggest the reviewer's
112
112
  proposed `code_refs` via `/projectstore:codemap set` (the reviewer computes it
113
113
  from `scripts/diff-refs.mjs --since <started_at>`).
114
+
115
+ 6c. **(close only, both paths) Refresh this session's story**, here in the
116
+ main thread, after the clerk's clean report or after 6b:
117
+
118
+ ```bash
119
+ node "${CLAUDE_PLUGIN_ROOT}/bin/projectstore.mjs" session refresh "<story-path>" --json --project "${CLAUDE_PROJECT_DIR}"
120
+ ```
121
+
122
+ `<story-path>` is the absolute `path` the compute script returned in
123
+ step 2. The copy went through Bash, which no hook sees, so without this the
124
+ session's band and status line keep naming the closed story. The core finds
125
+ the session itself; with none it writes nothing and exits 0. A non-zero
126
+ exit is reported and never undoes the close.
@@ -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
@@ -134,16 +134,16 @@
134
134
  "system_message": true,
135
135
  "decision": "permissionDecision",
136
136
  "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."
137
+ "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
138
  },
139
139
  "PostToolUse": {
140
140
  "fields": [
141
141
  "additionalContext"
142
142
  ],
143
- "system_message": null,
143
+ "system_message": true,
144
144
  "decision": null,
145
145
  "evidence": "measured",
146
- "note": "scripts/touch-session.mjs — the entry-rule reminder's primary carrier (entry-rule spec, contract 14)."
146
+ "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
147
  },
148
148
  "PostToolUseFailure": {
149
149
  "fields": [
@@ -299,7 +299,7 @@
299
299
  "file": ".projectstore/state/claude-code/statusline.mjs",
300
300
  "source": "scripts/statusline-launcher.mjs",
301
301
  "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",
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.30",
303
303
  "legacy_file": ".claude/.projectstore/statusline.mjs"
304
304
  },
305
305
  "mcp": {
@@ -494,7 +494,7 @@
494
494
  "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
495
  "update_hint": {
496
496
  "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."],
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#upgrading for details."],
498
498
  "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
499
  },
500
500
  "update_instructions": [
@@ -321,8 +321,7 @@
321
321
  "run `$projectstore-bind <vault-path>` in Codex"
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.",
@@ -333,9 +332,9 @@
333
332
  "docs": "https://github.com/SmartAndPoint/ProjectStore#install--one-message"
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
339
  "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.",
341
340
  "agent_translation": {
@@ -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
  }