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.
- package/bin/projectstore-claude.mjs +1 -1
- 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 +17 -0
- package/node_modules/projectstore/commands/bind.md +6 -17
- package/node_modules/projectstore/commands/doctor.md +12 -7
- package/node_modules/projectstore/commands/statusline.md +1 -1
- package/node_modules/projectstore/commands/story.md +15 -2
- package/node_modules/projectstore/docs/how-it-works.md +5 -3
- package/node_modules/projectstore/harnesses/claude-code.json +5 -5
- package/node_modules/projectstore/harnesses/codex.json +4 -5
- package/node_modules/projectstore/hooks/band.tsx +48 -20
- package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
- package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
- package/node_modules/projectstore/hooks/session-start.mjs +50 -18
- package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
- package/node_modules/projectstore/package.json +11 -2
- package/node_modules/projectstore/scripts/band-shared.mjs +141 -24
- package/node_modules/projectstore/scripts/band.mjs +72 -21
- package/node_modules/projectstore/scripts/binding.mjs +616 -85
- package/node_modules/projectstore/scripts/build-adapters.mjs +73 -29
- package/node_modules/projectstore/scripts/cli.mjs +1015 -185
- package/node_modules/projectstore/scripts/codemap.mjs +22 -7
- package/node_modules/projectstore/scripts/convergence.mjs +253 -83
- package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
- package/node_modules/projectstore/scripts/doctor-report.mjs +91 -23
- package/node_modules/projectstore/scripts/doctor.mjs +1317 -371
- package/node_modules/projectstore/scripts/draft.mjs +5 -2
- package/node_modules/projectstore/scripts/fetch-shell.mjs +106 -20
- package/node_modules/projectstore/scripts/graph.mjs +19 -13
- package/node_modules/projectstore/scripts/harness.mjs +123 -48
- package/node_modules/projectstore/scripts/install-harness.mjs +3511 -616
- package/node_modules/projectstore/scripts/kanban.mjs +2 -1
- package/node_modules/projectstore/scripts/lib.mjs +694 -159
- package/node_modules/projectstore/scripts/mcp.mjs +386 -105
- package/node_modules/projectstore/scripts/portable-registration.mjs +224 -51
- package/node_modules/projectstore/scripts/provenance.mjs +26 -10
- package/node_modules/projectstore/scripts/query.mjs +378 -80
- package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
- package/node_modules/projectstore/scripts/run-host.mjs +62 -17
- package/node_modules/projectstore/scripts/scaffold.mjs +89 -28
- package/node_modules/projectstore/scripts/setup.mjs +987 -0
- package/node_modules/projectstore/scripts/stages.mjs +5 -1
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
- package/node_modules/projectstore/scripts/statusline.mjs +32 -14
- package/node_modules/projectstore/scripts/story-section.mjs +39 -17
- package/node_modules/projectstore/scripts/surfaces.mjs +416 -95
- package/node_modules/projectstore/scripts/term.mjs +382 -97
- package/node_modules/projectstore/scripts/tokens.mjs +106 -28
- package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
- package/node_modules/projectstore/scripts/version-guard.mjs +74 -27
- package/node_modules/projectstore/scripts/worktree.mjs +16 -5
- 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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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`.**
|
|
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.
|
|
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. **
|
|
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
|
|
70
|
-
|
|
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.
|
|
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 `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
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":
|
|
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.
|
|
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#
|
|
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
|
|
337
|
-
"welcome": ["**About future updates**: run
|
|
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
|
|
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
|
|
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,
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
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
|
|
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
|
|
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
|
|
223
|
-
|
|
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
|
|
226
|
-
b.stories.set(epicDir,
|
|
227
|
-
return
|
|
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
|
|
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:
|
|
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
|
-
|
|
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">{
|
|
473
|
-
<Text dimColor>{
|
|
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(
|
|
109
|
-
|
|
110
|
-
|
|
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"),
|
|
28
|
+
const critic = roleForm("critic"),
|
|
29
|
+
reviewer = roleForm("reviewer");
|
|
29
30
|
const shared = sharedRoleForm("critic");
|
|
30
|
-
const bridge =
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
50
|
-
|
|
51
|
-
|
|
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 {
|
|
77
|
+
try {
|
|
78
|
+
main();
|
|
79
|
+
} catch {
|
|
74
80
|
// A hook must never break session startup.
|
|
75
81
|
}
|