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.
- package/README.md +13 -12
- package/bin/projectstore-claude.mjs +2 -2
- 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 +153 -77
- 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/extending.md +11 -6
- package/node_modules/projectstore/docs/getting-started.md +21 -1
- package/node_modules/projectstore/docs/harnesses.md +13 -10
- package/node_modules/projectstore/docs/how-it-works.md +6 -4
- package/node_modules/projectstore/harnesses/claude-code.json +16 -15
- package/node_modules/projectstore/harnesses/codex.json +12 -13
- 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 +188 -39
- 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 +3532 -618
- 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 +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
|
|
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
|
|
83
|
+
npx projectstore setup
|
|
82
84
|
```
|
|
83
85
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
17
|
-
"why_not_scripted": "Two channels.
|
|
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
|
-
"
|
|
20
|
-
"/plugin marketplace add SmartAndPoint/ProjectStore && /plugin install projectstore@SmartAndPoint
|
|
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
|
|
28
|
-
"Pin a git-marketplace release
|
|
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
|
|
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
|
|
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":
|
|
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.
|
|
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#
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
332
|
+
"docs": "https://github.com/SmartAndPoint/ProjectStore#install"
|
|
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
|
-
"update_instructions": "
|
|
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
|
|
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
|
}
|
|
@@ -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 {
|
|
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 =
|
|
68
|
-
|
|
69
|
-
|
|
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 {
|
|
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
|
-
?
|
|
193
|
+
? cfg && cfg.vault_path
|
|
188
194
|
? `👋 projectstore: first-run welcome shown. Bound to ${cfg.vault_path}. ${updateHint().line}`
|
|
189
|
-
:
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
291
|
-
|
|
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(
|