projectstore-claude 0.30.7 โ 0.30.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/.mcp.json +1 -5
- package/node_modules/projectstore/README.md +6 -6
- package/node_modules/projectstore/agents/planner.md +6 -0
- package/node_modules/projectstore/agents/reviewer.md +11 -1
- package/node_modules/projectstore/commands/review.md +2 -2
- package/node_modules/projectstore/commands/statusline.md +1 -1
- package/node_modules/projectstore/docs/contracts-and-tests.md +107 -0
- package/node_modules/projectstore/docs/how-it-works.md +15 -12
- package/node_modules/projectstore/harnesses/claude-code.json +7 -6
- package/node_modules/projectstore/harnesses/codex.json +4 -3
- package/node_modules/projectstore/hooks/band.tsx +17 -15
- package/node_modules/projectstore/hooks/pre-compact.mjs +25 -8
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/convergence.mjs +2 -4
- package/node_modules/projectstore/scripts/diff-refs.mjs +39 -0
- package/node_modules/projectstore/scripts/doctor.mjs +360 -5
- package/node_modules/projectstore/scripts/fetch-shell.mjs +5 -1
- package/node_modules/projectstore/scripts/harness.mjs +32 -0
- package/node_modules/projectstore/scripts/install-harness.mjs +1 -1
- package/node_modules/projectstore/scripts/lib.mjs +31 -0
- package/node_modules/projectstore/scripts/mcp-server.mjs +11 -4
- package/node_modules/projectstore/scripts/term.mjs +3 -3
- package/node_modules/projectstore/scripts/touch-session.mjs +1 -1
- package/node_modules/projectstore/scripts/version-guard.mjs +5 -10
- package/node_modules/projectstore/templates/de/spec.md.tmpl +9 -1
- package/node_modules/projectstore/templates/en/spec.md.tmpl +8 -1
- package/node_modules/projectstore/templates/es/spec.md.tmpl +9 -1
- package/node_modules/projectstore/templates/fr/spec.md.tmpl +9 -1
- package/node_modules/projectstore/templates/ru/spec.md.tmpl +8 -1
- package/node_modules/projectstore/templates/zh/spec.md.tmpl +8 -1
- package/package.json +2 -2
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"name": "projectstore",
|
|
13
13
|
"displayName": "projectstore",
|
|
14
14
|
"description": "๐ Your agent runs the project through a verified loop: task โ artifact (ADR ยท spec ยท epic ยท story) โ adversarial critic โ backlog โ planner โ reviewer โ done. Plain markdown in an Obsidian-friendly vault, every write approved by you โ and any model can pick the project up tomorrow.",
|
|
15
|
-
"version": "0.30.
|
|
15
|
+
"version": "0.30.9",
|
|
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.30.
|
|
4
|
+
"version": "0.30.9",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ProjectStore
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> ProjectStore is a method and a framework for building apps and services of any size with AI agents. It keeps the project's record โ what was decided, what is in flight, what was checked โ as reviewed markdown in git, readable by any agent and anyone with the repo.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/SmartAndPoint/ProjectStore/releases) [](./LICENSE) [](https://github.com/SmartAndPoint/ProjectStore/stargazers)
|
|
6
6
|
|
|
@@ -183,7 +183,7 @@ npx projectstore codemap --for scripts/lib.mjs
|
|
|
183
183
|
The same eight reads are an MCP server. The plugin registers it through its own `.mcp.json`, so a Claude Code session has `status`, `search`, `get_artifact`, `neighbors`, `lineage`, `code_refs`, `orientation` and `doctor` as tools with no shell; every tool result is the CLI's `--json` for the same arguments. Elsewhere:
|
|
184
184
|
|
|
185
185
|
```
|
|
186
|
-
npx projectstore mcp --project
|
|
186
|
+
npx projectstore mcp --project .
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
Binding, too โ naming the vault is the confirmation, and changing it needs `--rebind`. `init` creates a whole vault in one step, with no `mkdir` first: the directory, its git repository, the layout's folders and their README indexes; `scaffold --write` adds the folders to a vault that was bound without them:
|
|
@@ -240,7 +240,7 @@ Each setting has exactly one writer, and the plan says which file it changes:
|
|
|
240
240
|
|
|
241
241
|
- **The agents' models and efforts** live in `.projectstore/harness/<harness>.json`. That file is committed, so a change there is a team change.
|
|
242
242
|
- **The status line, the band, the language and the other binding keys** live in `.projectstore/projectstore.json`, which stays on this machine.
|
|
243
|
-
- **The spec policy and the lifecycle gates** live in the vault's `.projectstore.json`, which commits with the vault. Setting `spec_policy=required` records when it took effect; stories closed before then are exempt.
|
|
243
|
+
- **The spec policy and the lifecycle gates** live in the vault's `.projectstore.json`, which commits with the vault. Setting `spec_policy=required` records when it took effect; stories closed before then are exempt. A spec's contracts can carry WHEN/THEN scenarios, and a test can cite a contract, so doctor can report the contracts no test covers: see [`docs/contracts-and-tests.md`](./docs/contracts-and-tests.md).
|
|
244
244
|
|
|
245
245
|
The vault itself is changed by `bind --rebind`, which configure names when asked. In a Claude Code session, `/projectstore:statusline` and `/projectstore:agents configure` run the same writers.
|
|
246
246
|
|
|
@@ -398,10 +398,10 @@ ProjectStore has no server, no account and no telemetry. It collects nothing abo
|
|
|
398
398
|
### What it runs
|
|
399
399
|
|
|
400
400
|
- **Node.js.** The hooks, the MCP server, the status line and the `projectstore` command are Node.js scripts in the plugin. They start other scripts of the plugin through the same Node. When setup keeps a project on its git-marketplace copy, it runs that copy's own `bin/projectstore.mjs` to read the copy's plan and, once you confirm, to apply it.
|
|
401
|
-
- **git**, on this machine only. ProjectStore never commits, pushes, pulls or fetches. The SessionStart hook runs `git rev-parse` in a project that is not bound, to see whether it is a worktree of a bound checkout; no other hook starts a program. Bind, init and setup run `git rev-parse --show-toplevel` to find the repository and `git init` to give a new vault its own repository. They run `git config --get user.name` to propose the default author; the binding keeps it, and each new artifact names it as its author. Doctor runs `git check-ignore` and `git ls-files` to check what git ignores and tracks,
|
|
401
|
+
- **git**, on this machine only. ProjectStore never commits, pushes, pulls or fetches. The SessionStart hook runs `git rev-parse` in a project that is not bound, to see whether it is a worktree of a bound checkout; no other hook starts a program. Bind, init and setup run `git rev-parse --show-toplevel` to find the repository and `git init` to give a new vault its own repository. They run `git config --get user.name` to propose the default author; the binding keeps it, and each new artifact names it as its author. Doctor runs `git check-ignore` and `git ls-files` to check what git ignores and tracks, `git diff --name-only` and `git log` to see what has changed, and `git grep --untracked` to find the tests that cite a spec's contracts. The reviewer's list of the files a story touched comes from `git diff` and `git log` too. The reviewer and archaeologist agents read the project's git history (`git status` and `git diff` or `git log` among others) through the agent's own tools, as any command the agent runs.
|
|
402
402
|
- **The agent CLIs, `claude` and `codex`.** Setup, install, upgrade and uninstall run their plugin commands, such as `plugin marketplace add` and `plugin install` to register ProjectStore or `plugin uninstall` to remove it. Each one runs after you confirm the plan, which shows it verbatim. The model menu of `configure` also runs `codex debug models` to read Codex's model list, which Codex refreshes over the network with its own login.
|
|
403
403
|
- **npm**, from setup, install and upgrade only. They run `npm config get registry` to learn which registry your npm uses. When they set up Codex, they also run `npm install` with `--ignore-scripts` to fetch the `projectstore-codex` package at the running version into a folder in ProjectStore's cache, because the Codex plugin ships in that package. No install script of that package runs.
|
|
404
|
-
- **The band above the prompt** (Claude Code) is a module that runs inside Claude Code's own process, not in Node. It reads the binding, the session's pointer and its epic's files, and writes a heartbeat under `.projectstore/state/sessions
|
|
404
|
+
- **The band above the prompt** (Claude Code) is a module that runs inside Claude Code's own process, not in Node. It reads the binding, the session's pointer and its epic's files, and writes a heartbeat, `.band-<session id>.json` under `.projectstore/state/sessions/`, while it draws; it starts no program and makes no request. It watches agent starts (`agent.spawn`), each agent's requests and tool calls, and each turn's end, only to show a ProjectStore agent's row. It passes each of these on unchanged, and it changes no agent, permission or setting.
|
|
405
405
|
- **Your own status line.** When ProjectStore's status line is on and your project's `.claude/settings.json` or your user `settings.json` already names a status line command, ProjectStore runs that command through your shell on every render, passes it the status data Claude Code gives a status line on its standard input, and shows what it prints under its own line.
|
|
406
406
|
|
|
407
407
|
The package also ships `scripts/version-guard.mjs`, which runs `npm pack --dry-run` as the release's own check. The plugin never runs it.
|
|
@@ -488,7 +488,7 @@ npx projectstore bind ~/vaults/other --rebind
|
|
|
488
488
|
|
|
489
489
|
## Extending
|
|
490
490
|
|
|
491
|
-
See [`docs/extending.md`](./docs/extending.md) for adding layouts, templates, and skills, and [`docs/harnesses.md`](./docs/harnesses.md) for which coding agents projectstore runs on โ what "experimental" means there, and what adding one takes.
|
|
491
|
+
See [`docs/extending.md`](./docs/extending.md) for adding layouts, templates, and skills, [`docs/contracts-and-tests.md`](./docs/contracts-and-tests.md) for linking a spec's contracts to tests, and [`docs/harnesses.md`](./docs/harnesses.md) for which coding agents projectstore runs on โ what "experimental" means there, and what adding one takes.
|
|
492
492
|
|
|
493
493
|
## Contributing
|
|
494
494
|
|
|
@@ -69,6 +69,12 @@ generic best practice โ cite the real files and patterns you found.
|
|
|
69
69
|
state, ordering, prior fixes this change could regress.
|
|
70
70
|
6. **Tests** โ where new tests go, the harness/fixtures to reuse (cite), the
|
|
71
71
|
cases that matter โ mapped to the story's acceptance criteria when given.
|
|
72
|
+
A covering spec's contract may carry `- WHEN โฆ THEN โฆ` scenario items: list
|
|
73
|
+
them as the test cases to write, and say each new test cites its contract
|
|
74
|
+
with a token `[<spec id or alias>#<n>]` in its name or a comment beside it
|
|
75
|
+
(written here as `[example#3]`; never put a real spec's token in a plan or
|
|
76
|
+
doc โ doctor would count it). A contract marked `(verified: live)` or
|
|
77
|
+
`(verified: manual)` is checked outside the test suite: name how.
|
|
72
78
|
7. **Plan** โ a short ordered step list the engineer can follow.
|
|
73
79
|
8. **Proposed `code_refs`** โ the paths/globs this epic (and story) will own once
|
|
74
80
|
implemented, ready for `/projectstore:codemap set`. You PROPOSE; the command
|
|
@@ -62,6 +62,15 @@ Code that satisfies no criterion: flag as scope creep, gently. A story is
|
|
|
62
62
|
or accidental?
|
|
63
63
|
- **Tests** โ do new/changed paths have tests that ASSERT the behavior? Run them
|
|
64
64
|
cheaply when checkable (read-only).
|
|
65
|
+
- **Contract citations** โ a new test that covers a covering spec's contract
|
|
66
|
+
cites it with `[<spec id or alias>#<n>]` (written here as `[example#3]`).
|
|
67
|
+
Each of these is a finding:
|
|
68
|
+
- a new test that covers a contract and cites nothing;
|
|
69
|
+
- a citation on a test that does not assert the contract's observable
|
|
70
|
+
outcome โ an empty or trivially passing test with a token is worse than no
|
|
71
|
+
token;
|
|
72
|
+
- a real token outside the tests (docs, README, plans, code that is not a
|
|
73
|
+
test), because doctor counts it as coverage. Docs use only `example`.
|
|
65
74
|
|
|
66
75
|
## Discovery โ filtering
|
|
67
76
|
Report every finding, severity + confidence annotated; recall is your job,
|
|
@@ -79,7 +88,8 @@ say so plainly.
|
|
|
79
88
|
2. **Acceptance matrix** โ one line per criterion: status + evidence. Include
|
|
80
89
|
the covering specs' attributed + unattributed acceptance items (additive).
|
|
81
90
|
Format each evidence value so it can be persisted verbatim into the story
|
|
82
|
-
file at close: `โ evidence: <test | command | file:line>`
|
|
91
|
+
file at close: `โ evidence: <test | command | file:line>` (a test may be
|
|
92
|
+
named by its contract token, `[example#3]`, which doctor checks) โ the close gate
|
|
83
93
|
(`/projectstore:story close`) copies your matrix into the checkboxes.
|
|
84
94
|
3. **Findings** โ severity-rated: `๐ด blocker` / `๐ก should-fix` / `๐ข nit`; each
|
|
85
95
|
with file:line, confidence, why it matters, and a specific fix.
|
|
@@ -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
|
|
8
|
+
Resolution is **per-session, with no cross-session read and no read of the vault** (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 runs `configure --set statusline=on|off` (the configure spec, contracts 6 and 9): install's own items record `statusline.enabled` and write or remove the `settings.local.json` entry and the launcher, in one plan. The command never writes `projectstore.json` itself. 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
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Contracts and tests
|
|
2
|
+
|
|
3
|
+
A spec's behavioral contracts are numbered and testable. This page shows how
|
|
4
|
+
to write a contract so a test follows from it, how a test cites the contract
|
|
5
|
+
it covers, and what doctor reports. It describes Part A of the ADR *Spec
|
|
6
|
+
policy, revised: contract scenarios and test citations that doctor counts,
|
|
7
|
+
and a declared per-story opt-out from a spec*.
|
|
8
|
+
|
|
9
|
+
All of it is optional. A spec without scenarios, and a test without a
|
|
10
|
+
citation, stay valid.
|
|
11
|
+
|
|
12
|
+
Every example on this page uses the key `example`. It is reserved: no spec
|
|
13
|
+
may use it as an id or alias, so a token written with it never resolves and
|
|
14
|
+
never counts as coverage. A real token in a doc would count, so docs use only
|
|
15
|
+
`example`.
|
|
16
|
+
|
|
17
|
+
## Scenarios under a contract
|
|
18
|
+
|
|
19
|
+
A contract may carry list items that start with `WHEN`, `GIVEN`, `AND` or
|
|
20
|
+
`THEN`:
|
|
21
|
+
|
|
22
|
+
```markdown
|
|
23
|
+
3. **A stale index is reported.** Doctor compares each folder index with the
|
|
24
|
+
artifacts on disk and reports a row that no longer matches.
|
|
25
|
+
- GIVEN an index row whose status differs from the artifact's frontmatter
|
|
26
|
+
- WHEN doctor runs with --vault
|
|
27
|
+
- THEN it reports an index finding that names the folder
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The items are a list, so Markdown keeps one step per line. The keywords stay
|
|
31
|
+
in English in every language, like `โ stories:`: a tool matches them
|
|
32
|
+
literally.
|
|
33
|
+
|
|
34
|
+
Doctor reads scenarios for one thing only. If a contract's scenario has no
|
|
35
|
+
`WHEN` (or `GIVEN`), or no `THEN`, it reports an info.
|
|
36
|
+
|
|
37
|
+
## A contract no test can cover
|
|
38
|
+
|
|
39
|
+
Some contracts are checked on a real host after install, or by a person.
|
|
40
|
+
Mark them in the contract's first paragraph:
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
7. **The band draws in a live session.** (verified: live)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use `(verified: live)` or `(verified: manual)`. Doctor does not count a
|
|
47
|
+
marked contract as uncovered.
|
|
48
|
+
|
|
49
|
+
## Citing a contract from a test
|
|
50
|
+
|
|
51
|
+
A test cites a contract with a bracketed token, `[<key>#<n>]`, in its name or
|
|
52
|
+
in a comment beside it:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// [example#3]
|
|
56
|
+
test("doctor reports a stale index row", () => {
|
|
57
|
+
// โฆ assert the finding names the folder
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `<key>` is the spec's frontmatter `id`, or its `alias`. Matching is
|
|
62
|
+
case-sensitive.
|
|
63
|
+
- `<n>` is the contract number in ASCII: `0`, `4`, `4'` for `4โฒ`, `4''` for
|
|
64
|
+
`4โณ`. A step nested under contract 4 is `4/1''`. A cited step does not
|
|
65
|
+
cover its parent.
|
|
66
|
+
- The closing bracket ends the token, so `[example#4]` never matches
|
|
67
|
+
contract `4'` or `40`. A token followed by `(` is a Markdown link, and a
|
|
68
|
+
`[[โฆ]]` is a wikilink. Neither is a citation.
|
|
69
|
+
|
|
70
|
+
A slug id runs to about 60 characters, so give a spec a short alias in its
|
|
71
|
+
frontmatter:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
alias: billing
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Its tests then cite its contracts with `billing` as the key. (This page
|
|
78
|
+
writes that out in words rather than as a token, because a token in a doc
|
|
79
|
+
is scanned like one in a test.)
|
|
80
|
+
|
|
81
|
+
An alias is frozen like a slug, because renaming it breaks every citation.
|
|
82
|
+
Two specs may not share an alias, and an alias may not be another spec's id.
|
|
83
|
+
|
|
84
|
+
A citation is a claim: the reviewer checks that a cited test asserts the
|
|
85
|
+
contract's outcome. An empty test with a token is a review finding.
|
|
86
|
+
|
|
87
|
+
## What doctor reports
|
|
88
|
+
|
|
89
|
+
Doctor (`--vault`) scans the bound project with `git grep --untracked`, so a
|
|
90
|
+
test you have not added yet is found too. `.gitignore`d files, a vault
|
|
91
|
+
inside the project, `node_modules/` and the other generated paths are
|
|
92
|
+
skipped. Tests outside the bound project, or in a submodule, are not seen.
|
|
93
|
+
|
|
94
|
+
| Finding | Level | When |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `contract-coverage` | info | One line per active spec. With no citation: "N contracts, 0 cited". Once a spec has a citation, or sets `coverage: report` in its frontmatter: the contracts no test cites |
|
|
97
|
+
| `citation-unknown` | warn | A token whose key is no spec's id or alias, or whose number is not a contract of that spec |
|
|
98
|
+
| `citation-superseded` | info | A token that cites a superseded spec |
|
|
99
|
+
| `scenario-shape` | info | A scenario without a WHEN/GIVEN or a THEN |
|
|
100
|
+
| `spec-alias` | issue | A duplicate alias, an alias equal to another spec's id, or the reserved `example` |
|
|
101
|
+
| `evidence-citation` | warn | Under `lifecycle_gates: on`: a story's evidence names a token that no file in the project carries |
|
|
102
|
+
|
|
103
|
+
Each report names the checkout it read. A worktree on an old branch, or a
|
|
104
|
+
second project bound to the same vault, can see different coverage.
|
|
105
|
+
|
|
106
|
+
Doctor never runs a test. CI and `npm test` do that. Doctor checks the
|
|
107
|
+
record: which contracts are cited, and whether a cited token still exists.
|
|
@@ -87,25 +87,28 @@ Edits made through the shell are not seen; a Read of the story sets the pointer.
|
|
|
87
87
|
|
|
88
88
|
Continuity across a compaction is delivered by **SessionStart**, not by
|
|
89
89
|
PreCompact โ and that is a correction, not a design flourish. PreCompact has no
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
`additionalContext`, so for its early life the hook assembled a rich packet,
|
|
91
|
+
printed a red *"Hook JSON output validation failed"*, and reached nobody. Its
|
|
92
|
+
`systemMessage` is no channel either: Claude Code discards that field on
|
|
93
|
+
PreCompact. What it does with a PreCompact hook is echo the hook's raw stdout in
|
|
94
|
+
the `/compact` output.
|
|
95
95
|
|
|
96
96
|
So the two hooks now split the work by what each can actually do.
|
|
97
97
|
|
|
98
|
-
PreCompact prints one line, for you:
|
|
98
|
+
PreCompact prints one plain line, for you:
|
|
99
99
|
|
|
100
100
|
```
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
at session start"
|
|
106
|
-
}
|
|
101
|
+
โฟ Compacted (ctrl+o to see full summary)
|
|
102
|
+
PreCompact [...pre-compact.mjs] completed successfully: projectstore:
|
|
103
|
+
compacting โ vault myapp-vault, in flight: `epics/WEB-101/epic.md`,
|
|
104
|
+
orientation and this session's recent files return at session start
|
|
107
105
|
```
|
|
108
106
|
|
|
107
|
+
On a manual `/compact` the model reads that line too, after the summary. On an
|
|
108
|
+
automatic compaction it is shown to no one. (On Codex, where none of this is
|
|
109
|
+
measured yet, the hook keeps printing its line as a `{"systemMessage": โฆ}`
|
|
110
|
+
object.)
|
|
111
|
+
|
|
109
112
|
That last clause is conditional on purpose. It appears only when the compaction
|
|
110
113
|
is **manual** and `auto_inject` is on โ the two things the hook can check at the
|
|
111
114
|
moment it speaks. On an automatic compaction the plugin makes no claim that
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"_root_placeholder_literal_comment": "true = the harness passes the placeholder through as literal text and the installer must substitute an absolute path; false = the harness expands it. Claude Code expands ${CLAUDE_PLUGIN_ROOT}, so false. Measured value for Codex comes from the spike."
|
|
116
116
|
},
|
|
117
117
|
"output_channels": {
|
|
118
|
-
"_comment": "Per-event output channels (generation spec, contract 17). `fields` are the hookSpecificOutput keys the event honours; `system_message` is the top-level systemMessage; `decision` names the decision object where one exists; an optional `input_fields` lists the stdin keys; Stop's `carrier` is the shape the entry reminder's Stop carrier prints (additionalContext | block | null) โ our choice where both are honoured. `evidence` is `measured` only where this repository's own hooks observe the fact, `documented` where the Claude Code hooks reference says so and we never emit it.",
|
|
118
|
+
"_comment": "Per-event output channels (generation spec, contract 17). `fields` are the hookSpecificOutput keys the event honours; `system_message` is the top-level systemMessage; `decision` names the decision object where one exists; an optional `input_fields` lists the stdin keys; Stop's `carrier` is the shape the entry reminder's Stop carrier prints (additionalContext | block | null) โ our choice where both are honoured; PreCompact's `carrier` is how hooks/pre-compact.mjs prints its line (stdout | null, null keeping the {\"systemMessage\"} object). `system_message` is true where the field is honoured, false where it is measured discarded, null where unmeasured. `evidence` is `measured` only where this repository's own hooks observe the fact, `documented` where the Claude Code hooks reference says so and we never emit it.",
|
|
119
119
|
"SessionStart": {
|
|
120
120
|
"fields": [
|
|
121
121
|
"additionalContext"
|
|
@@ -195,10 +195,11 @@
|
|
|
195
195
|
},
|
|
196
196
|
"PreCompact": {
|
|
197
197
|
"fields": [],
|
|
198
|
-
"system_message":
|
|
198
|
+
"system_message": false,
|
|
199
199
|
"decision": null,
|
|
200
|
+
"carrier": "stdout",
|
|
200
201
|
"evidence": "measured",
|
|
201
|
-
"note": "Measured NEGATIVELY: an output object containing additionalContext is rejected whole, taking systemMessage down with it (
|
|
202
|
+
"note": "Measured NEGATIVELY: an output object containing additionalContext is rejected whole, taking systemMessage down with it (vault story: PreCompact survival packet targets a channel PreCompact does not have). A top-level `decision: \"block\"` exists (documented; never emitted here). The systemMessage field is discarded (hooks reference; measured on 2.1.312, 2026-10-28): what carries the line is the hook's raw stdout, echoed in the /compact output as `PreCompact [<command>] completed successfully: <stdout>` (the same form was seen on 2026-08-18). On a manual compaction the user sees it and the model reads it as <local-command-stdout> after the summary; on an automatic one nothing is shown, and the compacting model appears to receive it (nothing relies on that). stderr reaches no one. Vault story: On Claude Code PreCompact speaks plain text, because the host echoes its stdout and discards systemMessage."
|
|
202
203
|
}
|
|
203
204
|
},
|
|
204
205
|
"surfaces": {
|
|
@@ -310,9 +311,9 @@
|
|
|
310
311
|
"dir": ".",
|
|
311
312
|
"file": ".mcp.json",
|
|
312
313
|
"entry": "scripts/mcp-server.mjs",
|
|
313
|
-
"entry_reason": "The script .mcp.json runs, under the plugin root, with
|
|
314
|
+
"entry_reason": "The script .mcp.json runs, under the plugin root, with no other argument: the project comes from CLAUDE_PROJECT_DIR, which the host exports to the server (launch.project_dir_in_env), since 0.30.9 (PS-DIRECTORY, the story \"The MCP server's entry names no variable but ${CLAUDE_PLUGIN_ROOT}โฆ\"). It lives under scripts/, never bin/ (a plugin's top-level bin/ is on the host's Bash PATH), and imports nothing from cli.mjs, so the server's imports reach no install verb (PS-DIRECTORY, the story \"The MCP server's entry reaches no install verb through a read-verb runner that shares the CLI's verb tableโฆ\"; the MCP ADR's decision 1 as amended). The CLI's `mcp` verb serves the same way, for a host without a plugin channel.",
|
|
314
315
|
"scope": "plugin",
|
|
315
|
-
"scope_reason": "Claude Code loads a plugin's .mcp.json from the plugin installation itself and expands ${CLAUDE_PLUGIN_ROOT} and
|
|
316
|
+
"scope_reason": "Claude Code loads a plugin's .mcp.json from the plugin installation itself and expands ${CLAUDE_PLUGIN_ROOT} in it and exports CLAUDE_PROJECT_DIR to the server per session, so the one shipped file binds each server to one project (MCP ADR decision 6, amended 2026-09-05); nothing is installed and nothing is derived โ the host kind install-harness skips, which the install spec's contract 0 does not yet name.",
|
|
316
317
|
"launch": {
|
|
317
318
|
"cwd": "project",
|
|
318
319
|
"project_dir_in_env": true,
|
|
@@ -325,7 +326,7 @@
|
|
|
325
326
|
],
|
|
326
327
|
"default_syntax": "${VAR:-default}",
|
|
327
328
|
"unset_placeholder_in_env": "literal",
|
|
328
|
-
"unset_placeholder_in_args": "inferred literal โ the
|
|
329
|
+
"unset_placeholder_in_args": "inferred literal โ measured through the 0.30.8 form, which the server refused as a placeholder",
|
|
329
330
|
"entrypoint": "sdk-cli (headless -p), plugin loaded with --plugin-dir; the marketplace install and an interactive session are A9's",
|
|
330
331
|
"protocol_era": "initialize",
|
|
331
332
|
"protocol_version_seen": "2025-11-25",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"_root_placeholder_literal_comment": "true: Codex substitutes nothing in a hook command string. It does not need to โ the hook process receives PLUGIN_ROOT in its environment (measured). The generator therefore renders an absolute interpreter and an absolute script path; a bare `node` is a PATH gamble."
|
|
73
73
|
},
|
|
74
74
|
"output_channels": {
|
|
75
|
-
"_comment": "Per-event channels (generation spec, contract 17, as amended 2026-10-15). `fields` are the hookSpecificOutput keys the event honours โ the output side, as on every manifest; `input_fields` are the stdin keys it delivers, captured from live firings (759 on 2026-09-07/08; Run 4 of *Codex hook contract measured in a live session*, codex-cli 0.162.1, 2026-10-15, traces t1โt7). `decision` names the decision object where one exists; Stop's `carrier` is the shape the entry reminder's Stop carrier prints (additionalContext | block | null). `system_message` stays null: documented, not measured. `evidence` is about the output side.",
|
|
75
|
+
"_comment": "Per-event channels (generation spec, contract 17, as amended 2026-10-15). `fields` are the hookSpecificOutput keys the event honours โ the output side, as on every manifest; `input_fields` are the stdin keys it delivers, captured from live firings (759 on 2026-09-07/08; Run 4 of *Codex hook contract measured in a live session*, codex-cli 0.162.1, 2026-10-15, traces t1โt7). `decision` names the decision object where one exists; Stop's `carrier` is the shape the entry reminder's Stop carrier prints (additionalContext | block | null); PreCompact's `carrier` is how hooks/pre-compact.mjs prints its line (stdout | null). `system_message` stays null: documented, not measured. `evidence` is about the output side.",
|
|
76
76
|
"SessionStart": {
|
|
77
77
|
"input_fields": [
|
|
78
78
|
"session_id",
|
|
@@ -185,8 +185,9 @@
|
|
|
185
185
|
"fields": [],
|
|
186
186
|
"system_message": null,
|
|
187
187
|
"decision": null,
|
|
188
|
+
"carrier": null,
|
|
188
189
|
"evidence": "documented",
|
|
189
|
-
"note": "trigger is `auto` on an automatic compaction, and PostCompact follows (Run 4, t2:5โ6). No permission_mode. Whether a systemMessage reaches anyone on Codex is unmeasured."
|
|
190
|
+
"note": "trigger is `auto` on an automatic compaction, and PostCompact follows (Run 4, t2:5โ6). No permission_mode. Whether a systemMessage reaches anyone on Codex is unmeasured, so no carrier is set and hooks/pre-compact.mjs keeps its {\"systemMessage\"} object here."
|
|
190
191
|
}
|
|
191
192
|
},
|
|
192
193
|
"surfaces": {
|
|
@@ -239,7 +240,7 @@
|
|
|
239
240
|
"kind": "host",
|
|
240
241
|
"scope": "project",
|
|
241
242
|
"scope_reason": "Codex auto-discovers a plugin-root .mcp.json even when the manifest does not declare mcpServers โ measured.",
|
|
242
|
-
"why_unsupported": "Our .mcp.json is in Claude Code's dialect: its argv
|
|
243
|
+
"why_unsupported": "Our .mcp.json is in Claude Code's dialect: its argv needs ${CLAUDE_PLUGIN_ROOT} expanded, which nothing substitutes there, and the server needs CLAUDE_PROJECT_DIR in its environment, which Codex does not export. With its earlier argv, which named the project variable too, the server failed its handshake and printed a red error in every session. Ships only once the Codex dialect is rendered and the spike's criterion (h) says what cwd and environment the server receives."
|
|
243
244
|
},
|
|
244
245
|
"skills": {
|
|
245
246
|
"supported": true,
|
|
@@ -197,14 +197,14 @@ async function storiesOf($: EngineInterface, b: Band, epicDir: string): Promise<
|
|
|
197
197
|
list: async (dir: string) => {
|
|
198
198
|
const entries = await $.fs.list(dir)
|
|
199
199
|
const out = []
|
|
200
|
-
for (const
|
|
201
|
-
let kind =
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
let kind = entry.kind
|
|
202
202
|
if (kind === 'other') {
|
|
203
203
|
// A symbolic link: the board follows it, so the band does too.
|
|
204
|
-
try { kind = (await $.fs.stat(`${dir}/${
|
|
204
|
+
try { kind = (await $.fs.stat(`${dir}/${entry.name}`, { resolve: true })).kind } catch { kind = 'other' }
|
|
205
205
|
}
|
|
206
|
-
seen.push(`${dir}/${
|
|
207
|
-
out.push({ ...
|
|
206
|
+
seen.push(`${dir}/${entry.name}:${kind}:${entry.mtimeMs}`)
|
|
207
|
+
out.push({ ...entry, kind })
|
|
208
208
|
}
|
|
209
209
|
return out
|
|
210
210
|
},
|
|
@@ -240,9 +240,9 @@ async function storiesOf($: EngineInterface, b: Band, epicDir: string): Promise<
|
|
|
240
240
|
})
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
const
|
|
244
|
-
b.stories.set(epicDir,
|
|
245
|
-
return
|
|
243
|
+
const fresh = { sig, counts: foldStages(statuses) as BandCounts, items }
|
|
244
|
+
b.stories.set(epicDir, fresh)
|
|
245
|
+
return fresh
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
// This session's story, from its file (contract 8): the item the pointer's
|
|
@@ -313,10 +313,12 @@ async function scan($: EngineInterface, b: Band, root: string, sid: string | nul
|
|
|
313
313
|
// โโ The clock: scan, heartbeat, prune (contract 10) โโโโโโโโโโโโโโโโโโโ
|
|
314
314
|
|
|
315
315
|
async function writeHeartbeat($: EngineInterface, root: string, sid: string | null, body: unknown): Promise<boolean> {
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
// An id that cannot name a file writes nothing. The path is spelled out,
|
|
317
|
+
// folder and name, so the directory's scan can read where the band writes;
|
|
318
|
+
// a node test holds it to layoutPaths() and heartbeatFile().
|
|
319
|
+
if (!heartbeatFile(sid)) return false
|
|
318
320
|
try {
|
|
319
|
-
await $.fs.write(`${root}
|
|
321
|
+
await $.fs.write(`${root}/.projectstore/state/sessions/.band-${sid}.json`, JSON.stringify(body) + '\n')
|
|
320
322
|
return true
|
|
321
323
|
} catch {
|
|
322
324
|
// A heartbeat missed: the status line keeps its own line, and the next
|
|
@@ -341,11 +343,11 @@ async function prune($: EngineInterface, now: number) {
|
|
|
341
343
|
if (live.length !== list.length) await update($, crew, () => live)
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
async function show($: EngineInterface, b: Band,
|
|
345
|
-
const key = JSON.stringify(
|
|
346
|
+
async function show($: EngineInterface, b: Band, shown: BandView | null) {
|
|
347
|
+
const key = JSON.stringify(shown)
|
|
346
348
|
if (key === b.shownKey) return
|
|
347
349
|
b.shownKey = key
|
|
348
|
-
await update($, view, () =>
|
|
350
|
+
await update($, view, () => shown)
|
|
349
351
|
}
|
|
350
352
|
|
|
351
353
|
async function tick($: EngineInterface, b: Band) {
|
|
@@ -362,7 +364,7 @@ async function tick($: EngineInterface, b: Band) {
|
|
|
362
364
|
b.scan = { gen, startedAt: now }
|
|
363
365
|
const current = () => b.scan?.gen === gen
|
|
364
366
|
const scanning = scan($, b, root, sid)
|
|
365
|
-
.then(
|
|
367
|
+
.then(found => (current() ? show($, b, found) : undefined))
|
|
366
368
|
.catch(() => undefined)
|
|
367
369
|
.finally(() => {
|
|
368
370
|
if (current()) b.scan = null
|
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Fires before context compaction (both manual /compact and automatic).
|
|
5
5
|
//
|
|
6
|
-
// ONE
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// ONE line, through the identified harness's PreCompact carrier (spec
|
|
7
|
+
// contract 22, as amended 2026-10-28). On Claude Code that is plain stdout: the
|
|
8
|
+
// host discards a PreCompact `systemMessage` and echoes the hook's raw stdout in
|
|
9
|
+
// the /compact output instead, where the user sees it and, on a manual
|
|
10
|
+
// compaction, the model reads it after the summary. An automatic compaction
|
|
11
|
+
// shows it to no one. Elsewhere โ nothing identified, or nothing measured โ the
|
|
12
|
+
// line keeps the {"systemMessage"} object it has always had.
|
|
13
|
+
//
|
|
14
|
+
// PreCompact has no `additionalContext`: the runtime rejects the whole object
|
|
15
|
+
// when one is present, so for its early history this hook assembled a packet,
|
|
16
|
+
// printed a red validation error, and reached nobody. Agent-facing continuity
|
|
17
|
+
// after a compaction is SessionStart's job, on `source: "compact"` (spec
|
|
18
|
+
// contract 19).
|
|
12
19
|
//
|
|
13
20
|
// What this hook may CLAIM is narrower than what it knows, and deliberately so:
|
|
14
21
|
// - naming the in-flight artifact is always safe โ it is a fact about the log
|
|
@@ -32,12 +39,22 @@ import {
|
|
|
32
39
|
resolveInFlightArtifact,
|
|
33
40
|
pathCell,
|
|
34
41
|
commandForm,
|
|
42
|
+
preCompactCarrier,
|
|
35
43
|
} from "../scripts/lib.mjs";
|
|
36
44
|
|
|
45
|
+
// The file's one writer to stdout, for the success path and the catch-all
|
|
46
|
+
// alike. A plain line stays one line: the object form escaped a line break in a
|
|
47
|
+
// vault name or an error message for free, the text form has to fold it.
|
|
48
|
+
//
|
|
37
49
|
// Exits after the flush, never before: process.exit does not drain a pending
|
|
38
50
|
// pipe write, and the budgeted read below can still be outstanding.
|
|
39
|
-
function emit(
|
|
40
|
-
|
|
51
|
+
function emit(line) {
|
|
52
|
+
let carrier = null;
|
|
53
|
+
try {
|
|
54
|
+
carrier = preCompactCarrier();
|
|
55
|
+
} catch {}
|
|
56
|
+
const out = carrier === "stdout" ? line.replace(/[\r\n]+/g, " ") : JSON.stringify({ systemMessage: line });
|
|
57
|
+
process.stdout.write(out + "\n", () => process.exit(0));
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
// Contract 13's budget applies here too. The session file lives inside the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.9",
|
|
4
4
|
"description": "Your agent runs the project through a verified loop: task โ artifact (ADR / spec / epic / story) โ adversarial critic โ backlog โ planner โ reviewer โ done. Plain markdown in git โ any model can pick the project up tomorrow.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"project-management",
|
|
@@ -72,8 +72,6 @@ import { registrationPaths } from "./surfaces.mjs";
|
|
|
72
72
|
// (rule 9's command-form rule). The shells bundle it; the notice and the stop
|
|
73
73
|
// name the package that runs, never a shell.
|
|
74
74
|
export const PACKAGE = "projectstore";
|
|
75
|
-
// What the read uses when the user's registry cannot be resolved.
|
|
76
|
-
export const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
77
75
|
// The abbreviated packument: what `npm install` itself reads (8 728 bytes
|
|
78
76
|
// against 50 896 for the full document, measured 2026-10-09).
|
|
79
77
|
export const ACCEPT = "application/vnd.npm.install-v1+json";
|
|
@@ -90,8 +88,8 @@ export const MIN_READ_MS = 50;
|
|
|
90
88
|
export const FRESH_MS = 24 * 60 * 60 * 1000;
|
|
91
89
|
export const NO_CHECK_ENV = "PROJECTSTORE_NO_UPDATE_CHECK";
|
|
92
90
|
|
|
93
|
-
// Strict semver 2.0 (
|
|
94
|
-
// anything else counts as missing โ never printed, never compared.
|
|
91
|
+
// Strict semver 2.0 (the specification's own grammar): a `dist-tags.latest`
|
|
92
|
+
// that is anything else counts as missing โ never printed, never compared.
|
|
95
93
|
const NUM = "(?:0|[1-9]\\d*)";
|
|
96
94
|
const PRE = `(?:${NUM}|\\d*[A-Za-z-][0-9A-Za-z-]*)`;
|
|
97
95
|
const SEMVER = new RegExp(
|
|
@@ -64,6 +64,45 @@ export function lastCommitMs(cwd = projectRoot()) {
|
|
|
64
64
|
return Number.isFinite(t) ? t : null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// The citation scan (the spec policy ADR, Decision 7): every line in the
|
|
68
|
+
// project, untracked files included, that looks like a contract citation.
|
|
69
|
+
// `.gitignore` is honoured, `exclude` paths (the vault, when it sits inside
|
|
70
|
+
// the project) are skipped, and SOURCE_IGNORE paths are dropped. Unlike gitIn
|
|
71
|
+
// this tells "no match" (status ok, no lines) from "not a repository"
|
|
72
|
+
// (no-git) and from every other failure โ no git binary, a timeout, an exit
|
|
73
|
+
// git explains on stderr (unknown, with that reason): only the first is an
|
|
74
|
+
// answer.
|
|
75
|
+
export function grepProjectLines(cwd, pattern, { exclude = [], timeout = 10000, ignore = IGNORE } = {}) {
|
|
76
|
+
const args = [
|
|
77
|
+
"grep",
|
|
78
|
+
"--untracked",
|
|
79
|
+
"-n",
|
|
80
|
+
"-I",
|
|
81
|
+
"-E",
|
|
82
|
+
"-e",
|
|
83
|
+
pattern,
|
|
84
|
+
"--",
|
|
85
|
+
".",
|
|
86
|
+
...exclude.map((p) => `:(exclude)${p}`),
|
|
87
|
+
];
|
|
88
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout, maxBuffer: 64 * 1024 * 1024 });
|
|
89
|
+
if (r.error?.code === "ENOENT") return { status: "unknown", reason: "git is not installed", lines: [] };
|
|
90
|
+
if (r.error || r.signal) return { status: "unknown", reason: "the scan timed out", lines: [] };
|
|
91
|
+
if (r.status === 1) return { status: "ok", lines: [] };
|
|
92
|
+
if (/not a git repository/i.test(r.stderr || "")) return { status: "no-git", lines: [] };
|
|
93
|
+
if (r.status !== 0) {
|
|
94
|
+
const why = (r.stderr || "").trim().split("\n")[0] || `git exited ${r.status}`;
|
|
95
|
+
return { status: "unknown", reason: why, lines: [] };
|
|
96
|
+
}
|
|
97
|
+
const lines = [];
|
|
98
|
+
for (const raw of r.stdout.split("\n")) {
|
|
99
|
+
const m = raw.match(/^(.*?):(\d+):(.*)$/);
|
|
100
|
+
if (!m || ignore.some((rx) => rx.test(m[1]))) continue;
|
|
101
|
+
lines.push({ file: m[1], line: Number(m[2]), text: m[3] });
|
|
102
|
+
}
|
|
103
|
+
return { status: "ok", lines };
|
|
104
|
+
}
|
|
105
|
+
|
|
67
106
|
function filterFiles(list) {
|
|
68
107
|
return [...new Set(list)]
|
|
69
108
|
.map((f) => f.trim())
|