projectstore-claude 0.30.14 โ 0.30.16
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/README.md +5 -0
- package/node_modules/projectstore/agents/planner.md +4 -1
- package/node_modules/projectstore/agents/reviewer.md +77 -2
- package/node_modules/projectstore/commands/doctor.md +11 -0
- package/node_modules/projectstore/commands/graph.md +5 -1
- package/node_modules/projectstore/commands/story.md +4 -1
- package/node_modules/projectstore/docs/contracts-and-tests.md +94 -2
- package/node_modules/projectstore/docs/extending.md +10 -0
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scaffold/guides/spec.md +6 -2
- package/node_modules/projectstore/scripts/configure-menu.mjs +4 -0
- package/node_modules/projectstore/scripts/configure.mjs +80 -7
- package/node_modules/projectstore/scripts/diff-refs.mjs +15 -10
- package/node_modules/projectstore/scripts/doctor.mjs +181 -12
- package/node_modules/projectstore/scripts/flow-policy.mjs +264 -0
- package/node_modules/projectstore/scripts/graph.mjs +12 -0
- package/node_modules/projectstore/scripts/lib.mjs +131 -2
- package/node_modules/projectstore/scripts/mcp.mjs +2 -2
- package/node_modules/projectstore/scripts/query.mjs +8 -2
- package/node_modules/projectstore/scripts/spec-drift.mjs +513 -0
- package/node_modules/projectstore/scripts/verbs.mjs +1 -1
- package/node_modules/projectstore/templates/de/spec.md.tmpl +5 -1
- package/node_modules/projectstore/templates/en/spec.md.tmpl +5 -1
- package/node_modules/projectstore/templates/es/spec.md.tmpl +5 -1
- package/node_modules/projectstore/templates/fr/spec.md.tmpl +5 -1
- package/node_modules/projectstore/templates/ru/spec.md.tmpl +5 -1
- package/node_modules/projectstore/templates/zh/spec.md.tmpl +5 -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.16",
|
|
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.16",
|
|
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",
|
|
@@ -242,6 +242,11 @@ Each setting has exactly one writer, and the plan says which file it changes:
|
|
|
242
242
|
- **The agents' models and efforts** live in `.projectstore/harness/<harness>.json`. That file is committed, so a change there is a team change.
|
|
243
243
|
- **The status line, the band, the language and the other binding keys** live in `.projectstore/projectstore.json`, which stays on this machine.
|
|
244
244
|
- **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 story whose work needs no spec can say why in `no_spec`, and `doctor --vault --policy required` shows what the policy would report before you switch it on. 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).
|
|
245
|
+
- **The flow policy** lives there too, under `flow`: each person's time on the project and so their WIP limit, how long work may sit in a status, and the windows for throughput and capacity. No key has a default, so a check whose key is not set does not run. People are named as stories write their assignee, never by email address, and their names, time shares and leave dates commit with the vault. These keys are set with `--set` only; the view names each form. For example:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npx projectstore configure --set flow.wip.full=3 --set 'flow.people[First Last].time=part'
|
|
249
|
+
```
|
|
245
250
|
|
|
246
251
|
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.
|
|
247
252
|
|
|
@@ -44,7 +44,10 @@ spec, say so FIRST โ under spec-first the spec must exist and be `active`
|
|
|
44
44
|
before implementation starts (suggest `/projectstore:spec`). Routing rule:
|
|
45
45
|
spec contracts = durable how; the story's `## Implementation Plan` = per-story
|
|
46
46
|
route (your output feeds it via `/projectstore:story plan`); `## Technical
|
|
47
|
-
Notes` = incidental constraints discovered during work.
|
|
47
|
+
Notes` = incidental constraints discovered during work. When the story changes
|
|
48
|
+
a covering spec's contracts, name each contract it will amend and the marker's
|
|
49
|
+
kind (`ADDED`, `MODIFIED` or `REMOVED`, in a `> amended YYYY-MM-DD by <ref>: โฆ`
|
|
50
|
+
block under the contract; `guide spec` has the rules).
|
|
48
51
|
|
|
49
52
|
**Spec or no spec.** When the story has no covering spec, say whether it
|
|
50
53
|
needs one, and why. Work whose "how" is non-trivial, or that will touch three
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
|
-
description: Opus (max-effort) STORY-CONFORMANCE reviewer for projectstore-bound projects โ a narrow, vault-aware role, NOT a general code reviewer. Invoke AFTER writing code, BEFORE committing or marking a story done. Verifies the diff actually closes the story โ per-acceptance-criterion evidence โ then correctness / regressions / codebase-fit / tests, severity + confidence rated, self-audited. Proposes the story's code_refs update. Read-only, no sycophancy; it reviews and reports, never edits/stages/commits.
|
|
3
|
+
description: Opus (max-effort) STORY-CONFORMANCE reviewer for projectstore-bound projects โ a narrow, vault-aware role, NOT a general code reviewer. Invoke AFTER writing code, BEFORE committing or marking a story done. Verifies the diff actually closes the story โ per-acceptance-criterion evidence โ then correctness / regressions / codebase-fit / tests, severity + confidence rated, self-audited. Proposes the story's code_refs update. Asked for spec drift on a named active spec, it checks that spec's contracts against its covered stories' code_refs instead and reports, per contract, holds / drifted / cannot tell. Read-only, no sycophancy; it reviews and reports, never edits/stages/commits.
|
|
4
4
|
model: opus
|
|
5
5
|
effort: max
|
|
6
6
|
tools: Read, Grep, Glob, Bash, WebFetch
|
|
@@ -11,6 +11,11 @@ context, separate from the author โ so a false "looks good" can't slip through
|
|
|
11
11
|
self-approval. A false approval costs far more than a false rejection: a story
|
|
12
12
|
marked done that isn't closed is exactly the vault-rot this role exists to stop.
|
|
13
13
|
|
|
14
|
+
**Which job.** Given a diff or a story, you check story conformance:
|
|
15
|
+
everything in this file except the Spec-drift mode section. Asked for spec
|
|
16
|
+
drift on a named spec, you run the Spec-drift mode section instead; its own
|
|
17
|
+
report replaces the Output section's verdict and acceptance matrix.
|
|
18
|
+
|
|
14
19
|
Inspect everything yourself: `git status`, `git diff`, `git diff --staged`, and
|
|
15
20
|
read the changed files in FULL context (not just the hunks). Locate the bound
|
|
16
21
|
vault (`.projectstore/projectstore.json` โ `vault_path`) and read the target story โ
|
|
@@ -37,7 +42,10 @@ covering spec: its Acceptance items attributed to this story (`โ stories:
|
|
|
37
42
|
verify them exactly like the story's own criteria. A story closes only when
|
|
38
43
|
both sets are green and every covering spec is `active`. Report a covering
|
|
39
44
|
spec still in `draft` as a blocker under `spec_policy: required` (vault's
|
|
40
|
-
`.projectstore.json`).
|
|
45
|
+
`.projectstore.json`). When the story edited a spec's contracts, check that
|
|
46
|
+
each edit carries an amendment marker (`> amended YYYY-MM-DD by <ref>: <KIND> โ
|
|
47
|
+
<why>` under the contract): from the vault's git history when the vault has
|
|
48
|
+
one, else from the story's own account; an unmarked edit is a finding.
|
|
41
49
|
|
|
42
50
|
**A declared opt-out, at close.** A story with `no_spec: "<reason>"` has no
|
|
43
51
|
covering spec on purpose. Compute its files (Output 4). If it touches three
|
|
@@ -57,6 +65,73 @@ which). With neither, that is a ๐ก should-fix, never a ๐ด: the line is a hin
|
|
|
57
65
|
not a gate, and it never holds a story open. With no line, or only its
|
|
58
66
|
placeholder, say nothing about it.
|
|
59
67
|
|
|
68
|
+
## Spec-drift mode
|
|
69
|
+
Only when the caller asks for spec drift on a named spec. You check that spec
|
|
70
|
+
against the code, one spec per run. Phases 0 to 2 and the Output section are
|
|
71
|
+
the story-conformance job and do not apply; "Discovery โ filtering" and the
|
|
72
|
+
Self-audit do.
|
|
73
|
+
|
|
74
|
+
1. **Input.** Run
|
|
75
|
+
`node "${CLAUDE_PLUGIN_ROOT}/scripts/spec-drift.mjs" <spec id, alias or file stem>`,
|
|
76
|
+
with `--since <date>` when the caller names one (it overrides every
|
|
77
|
+
anchor). It prints facts as JSON: the contracts with their markers (each
|
|
78
|
+
with its story's status), removed state and anchor (the date of the
|
|
79
|
+
contract's last valid marker, else the spec's `date:`); the covered
|
|
80
|
+
stories with their status and `code_refs`, empty and missing refs listed;
|
|
81
|
+
the refs expanded to files classed `source`, `tests` or `surface`, with
|
|
82
|
+
line counts and the commits since each anchor (`changed_since`: a count
|
|
83
|
+
and the last three); the files citing each contract (`cited_by`, tests
|
|
84
|
+
normally); and the
|
|
85
|
+
scan status, `ok`, `no-git` or `unknown`. A null is unknown, not empty. A
|
|
86
|
+
spec that is not `active` is refused with exit 1: report its status and
|
|
87
|
+
stop. History is a hint: an empty one is no evidence that a contract holds.
|
|
88
|
+
2. **Budget.** The report states the input size: files and lines by class,
|
|
89
|
+
and the number of contracts. When the spec has more contracts that are not
|
|
90
|
+
removed (`totals.contracts_open`) than the caller's budget, 8 when none is
|
|
91
|
+
named, return the size and the contract list (number and heading), ask
|
|
92
|
+
your caller for contract numbers and give no verdict. Given numbers (up to
|
|
93
|
+
the budget), judge those and list the rest as `not checked`.
|
|
94
|
+
3. **Reading.** Pre-commit to the two or three contracts whose covered files
|
|
95
|
+
changed most since their anchor (`files_changed`, `changed_since`) and hunt
|
|
96
|
+
those first. Read by symbol: search for the function, flag, command, path
|
|
97
|
+
or token a contract names, in `source` and `surface` files alike, and read
|
|
98
|
+
around each hit; a whole file only when the contract is about all of it.
|
|
99
|
+
4. **Verdicts**, one per contract judged:
|
|
100
|
+
- `holds`: a line of code, quoted verbatim with its file:line, produces the
|
|
101
|
+
contract's outcome. A line outside the bundle's files is marked
|
|
102
|
+
`(outside the bundle)`. A `holds` without a quoted line is `cannot tell`.
|
|
103
|
+
- `drifted`: the code contradicts the contract as written, a named file,
|
|
104
|
+
command or token that is gone included. Cite the contradicting line or,
|
|
105
|
+
for something missing, the search that found nothing.
|
|
106
|
+
- `cannot tell`, with its reason, such as: no covered code, a rule about
|
|
107
|
+
people or prose, a `(verified: live|manual)` contract no line can show
|
|
108
|
+
(`verified` in the bundle), a scan status of `unknown`, or code read that
|
|
109
|
+
settles nothing. When the code a contract asks for is not found and the
|
|
110
|
+
story behind its last marker has not reached `review`, the verdict is
|
|
111
|
+
`cannot tell (pending)`. A contract amended by a story in `review` or
|
|
112
|
+
`done` is judged as usual.
|
|
113
|
+
- A removed contract is listed as `removed` and not judged.
|
|
114
|
+
5. **Proposals.** Each `drifted` comes with a proposal and a lean: a marker
|
|
115
|
+
carried by an open story, a new story that brings the spec to the code, or
|
|
116
|
+
a follow-up story that brings the code back; never a closed story. Each
|
|
117
|
+
goes through the normal approval-gated commands, run by the caller.
|
|
118
|
+
6. **Facts, not drift.** Empty or missing `code_refs` (`refs.empty`,
|
|
119
|
+
`refs.missing`, `refs.unresolved`) and uncited contracts (`cited_by: []`)
|
|
120
|
+
are listed as facts, not drift; so are doctor's `code-refs` and
|
|
121
|
+
`contract-coverage` findings.
|
|
122
|
+
7. **Report**, your last message, in place of the Output section:
|
|
123
|
+
1. **Input** โ the spec (title, id, status), the bundle's `checkout`, the scan status, files and
|
|
124
|
+
lines by class, contracts judged, `not checked` and removed, and
|
|
125
|
+
`--since` when given.
|
|
126
|
+
2. **Contracts** โ one row per contract: number, heading, verdict and its
|
|
127
|
+
evidence (the quoted line with file:line, the contradicting line or the
|
|
128
|
+
empty search, or the reason).
|
|
129
|
+
3. **Drift proposals** โ for each `drifted`: the proposal, the lean and why.
|
|
130
|
+
4. **Facts** โ empty and missing refs, uncited contracts, unknown history.
|
|
131
|
+
5. **Open Questions** โ low-confidence verdicts, surfaced not blocking.
|
|
132
|
+
|
|
133
|
+
You write nothing in this mode: no vault file, no code, no story.
|
|
134
|
+
|
|
60
135
|
## Phase 0 โ Pre-commitment
|
|
61
136
|
From the story + file list, predict the 3-5 most likely gaps ("AC #3 needs an
|
|
62
137
|
error path the diff doesn't touch"; "touches a cache โ invalidation risk"). Write
|
|
@@ -194,6 +194,17 @@ You are running projectstore diagnostics (ADR-005: umbrella doctor).
|
|
|
194
194
|
gates that have no date, `review-by` names an ADR due for review, and
|
|
195
195
|
`dry-run` heads a `--policy` report. None of them is repairable by `--fix`:
|
|
196
196
|
each is a judgement, or a `configure` step its message names.
|
|
197
|
+
- The amendment checks read the markers under a spec's contracts
|
|
198
|
+
(`> amended YYYY-MM-DD by <ref>: ADDED|MODIFIED|REMOVED โ <why>`) in every
|
|
199
|
+
spec, under every policy: `amendment-form` (a marker that is not the grammar,
|
|
200
|
+
comes before the first contract or after a `REMOVED`; it counts for
|
|
201
|
+
nothing), `amendment-story` (an issue: the ref names no single story),
|
|
202
|
+
`amendment-link` (neither the story nor the spec lists the other) and
|
|
203
|
+
`amendment-date` (not a date, before the spec's `date:`, after tomorrow, or
|
|
204
|
+
before the previous marker under that contract). `citation-removed` is a
|
|
205
|
+
token in the project that cites a removed contract or step, with the why of
|
|
206
|
+
its marker. None of them is repairable by `--fix`: the marker and the test
|
|
207
|
+
are the author's text.
|
|
197
208
|
- The guide checks read the vault's `guides/` folder, the per-kind overrides
|
|
198
209
|
of the authoring guides: `guide-shape` (an override that does not parse, so
|
|
199
210
|
readers fall back to the package default), `guide-unknown` (a guide for a
|
|
@@ -51,6 +51,10 @@ vault-link-graph-derived-view-and-shared-link-resolver).
|
|
|
51
51
|
artifact's full typed neighborhood โ outgoing AND incoming edges โ in one
|
|
52
52
|
call. Node keys are full vault-relative paths, never short names.
|
|
53
53
|
- Edge kinds: wikilink, mdlink, supersedes, spec-covers, spec-implements-adr,
|
|
54
|
-
epic-contains, dead, ambiguous, out-of-scope. Nothing resolves
|
|
54
|
+
epic-contains, amends, dead, ambiguous, out-of-scope. Nothing resolves
|
|
55
|
+
silently. `amends` runs from a story to a spec one of its amendment markers
|
|
56
|
+
(`> amended YYYY-MM-DD by <ref>: <KIND> โ <why>` under a contract) changes:
|
|
57
|
+
one edge per pair; a marker whose ref resolves to no story is a `dead` edge
|
|
58
|
+
from the spec.
|
|
55
59
|
- Hand-edits to graph.md never stick: doctor flags staleness, reconcile
|
|
56
60
|
repairs. Fix the source artifact, then regenerate.
|
|
@@ -118,7 +118,10 @@ the two cannot collide).
|
|
|
118
118
|
today's date, then one bullet per question with its answer; a later plan
|
|
119
119
|
run appends its own dated line to the same subsection. An answer that changes a
|
|
120
120
|
contract of the covering spec says which one, and the spec is amended
|
|
121
|
-
the
|
|
121
|
+
before the code is written: that contract's body is rewritten to the text
|
|
122
|
+
in force, with a marker under it,
|
|
123
|
+
`> amended YYYY-MM-DD by <EPIC-ID>/<story-id>: ADDED|MODIFIED|REMOVED โ <why>`
|
|
124
|
+
(see `guide spec`).
|
|
122
125
|
|
|
123
126
|
3. **Fill the section content** in the returned `content` before preview:
|
|
124
127
|
- `plan` โ write the Implementation Plan. When the story's `specs:` names a
|
|
@@ -7,7 +7,9 @@ policy, revised: contract scenarios and test citations that doctor counts,
|
|
|
7
7
|
and a declared per-story opt-out from a spec*, and decisions 6 and 7 of the
|
|
8
8
|
ADR *One authoring guide per artifact kind, read by the author before the
|
|
9
9
|
draft and by the critic after, overridable from the vault* (edge and failure
|
|
10
|
-
marks, and test stories)
|
|
10
|
+
marks, and test stories), and the ADR *An active spec is amended by a marked
|
|
11
|
+
block under the contract, naming the story, the date and the kind, which
|
|
12
|
+
doctor checks and the graph draws* (amendment markers).
|
|
11
13
|
|
|
12
14
|
All of it is optional. A spec without scenarios, and a test without a
|
|
13
15
|
citation, stay valid.
|
|
@@ -171,6 +173,91 @@ Two specs may not share an alias, and an alias may not be another spec's id.
|
|
|
171
173
|
A citation is a claim: the reviewer checks that a cited test asserts the
|
|
172
174
|
contract's outcome. An empty test with a token is a review finding.
|
|
173
175
|
|
|
176
|
+
## Amending a contract
|
|
177
|
+
|
|
178
|
+
A later story that changes a contract of an active spec rewrites the
|
|
179
|
+
contract's body to the text in force and puts a marker under it: a
|
|
180
|
+
blockquote after the contract and before the next one, whose first paragraph
|
|
181
|
+
reads
|
|
182
|
+
|
|
183
|
+
```markdown
|
|
184
|
+
4. **Exports.** The export runs nightly and keeps seven copies.
|
|
185
|
+
|
|
186
|
+
> amended 2026-11-10 by example/story-keep-seven: MODIFIED โ seven copies, not three.
|
|
187
|
+
>
|
|
188
|
+
> Replaced:
|
|
189
|
+
>
|
|
190
|
+
> > keeps three copies
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `amended`, `by`, the kind and `Removed, see below.` stay English in every
|
|
194
|
+
locale. The ref is any form a spec's `stories:` entry takes
|
|
195
|
+
(`EPIC-ID/<story-id>`, the story part a short id or the full slug; a ref
|
|
196
|
+
without `EPIC-ID/` resolves to no story). The colon may be `:` or `๏ผ`, the
|
|
197
|
+
dash `โ`, `โ` or `-`. A marker may wrap: its first paragraph is read with
|
|
198
|
+
its lines joined.
|
|
199
|
+
- `MODIFIED`: the body holds the new text; the block quotes what it replaced
|
|
200
|
+
and why. A step keeps its number: a removed step stays as
|
|
201
|
+
`2. Removed, see below.`, a new step takes the list's next number.
|
|
202
|
+
- `ADDED`: a new contract takes the highest number in the file, removed ones
|
|
203
|
+
included, plus one. A number is never reused.
|
|
204
|
+
- `REMOVED`: the contract keeps its number and its heading, and its body
|
|
205
|
+
becomes `Removed, see below.` This is a tombstone. Nothing follows a
|
|
206
|
+
`REMOVED` marker; a rule that comes back is `ADDED` under a new number.
|
|
207
|
+
- The story and the spec list each other: the story in the spec's
|
|
208
|
+
`stories:`, the spec in the story's `specs:`. The amending story is a
|
|
209
|
+
covered story.
|
|
210
|
+
- Markers inside a fence or an HTML comment do not count, and neither does a
|
|
211
|
+
marker whose ref starts with `example/` (it is documentation, like the one
|
|
212
|
+
above). Older `> *Amended โฆ` blocks are not read; the first marker under a
|
|
213
|
+
contract folds them into the body and quotes them inside its own block.
|
|
214
|
+
|
|
215
|
+
A tombstone stays forever, so a citation keeps its meaning: a test that
|
|
216
|
+
still cites `[example#4]` after contract 4 was removed is reported as
|
|
217
|
+
`citation-removed`, with the marker's why, and the removed contract leaves
|
|
218
|
+
the coverage count. Evidence in a closed story that names it is not
|
|
219
|
+
reported. `graph.md` and `lineage` show an `amends` edge from the story to
|
|
220
|
+
the spec.
|
|
221
|
+
|
|
222
|
+
## Checking a spec against the code
|
|
223
|
+
|
|
224
|
+
A spec can drift from the code after its stories close: a later story
|
|
225
|
+
changes the behaviour without a marker, or the code changes outside any
|
|
226
|
+
story. Ask the reviewer for spec drift on a named spec, and it checks that
|
|
227
|
+
spec against the code at its covered stories' `code_refs`. Run it when the
|
|
228
|
+
covered code changed since the contracts' last markers, not before every
|
|
229
|
+
release.
|
|
230
|
+
|
|
231
|
+
The reviewer reads a bundle of facts first:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
node scripts/spec-drift.mjs example --since 2026-11-01
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
It prints JSON: each contract with its markers, removed state and anchor
|
|
238
|
+
(the date of its last marker, else the spec's `date:`; `--since` overrides
|
|
239
|
+
it), the tests that cite it as `[example#2]`, the covered stories and their
|
|
240
|
+
`code_refs` (empty and missing ones listed), the files those refs name with
|
|
241
|
+
their class (`source`, `tests` or `surface`), line counts and commits since
|
|
242
|
+
each anchor, and a scan status: `ok`, `no-git` or `unknown`. A spec that is
|
|
243
|
+
not `active` exits 1.
|
|
244
|
+
|
|
245
|
+
The reviewer judges up to eight contracts per run unless you name another
|
|
246
|
+
budget. A spec with more asks you for contract numbers first. Each contract
|
|
247
|
+
judged gets one verdict:
|
|
248
|
+
|
|
249
|
+
| Verdict | Means |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `holds` | A line of code, quoted with its file:line, produces the contract's outcome |
|
|
252
|
+
| `drifted` | The code contradicts the contract, or a file, command or token it names is gone; the report cites the line, or the search that found nothing |
|
|
253
|
+
| `cannot tell` | With a reason: no covered code, a rule about people, a live or manual contract, an unknown scan; `cannot tell (pending)` when the code is missing and the story behind the last marker has not reached review |
|
|
254
|
+
|
|
255
|
+
Each drift comes with a proposal: a marker carried by an open story, a new
|
|
256
|
+
story that brings the spec to the code, or a follow-up story that brings the
|
|
257
|
+
code back. Empty `code_refs` and uncited contracts are listed as facts, not
|
|
258
|
+
drift. The reviewer writes nothing; every change goes through the usual
|
|
259
|
+
approval-gated commands.
|
|
260
|
+
|
|
174
261
|
## What doctor reports
|
|
175
262
|
|
|
176
263
|
Doctor (`--vault`) scans the bound project with `git grep --untracked`, so a
|
|
@@ -182,11 +269,16 @@ skipped. Tests outside the bound project, or in a submodule, are not seen.
|
|
|
182
269
|
|---|---|---|
|
|
183
270
|
| `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 |
|
|
184
271
|
| `citation-unknown` | warn | A token whose key is no spec's id or alias, or whose number is not a contract of that spec |
|
|
272
|
+
| `citation-removed` | warn | A token that cites a removed contract, or a step that reads `Removed, see below.`; the message quotes the marker's why |
|
|
185
273
|
| `citation-superseded` | info | A token that cites a superseded spec |
|
|
186
274
|
| `scenario-shape` | info | A scenario without a WHEN/GIVEN or a THEN |
|
|
187
275
|
| `contract-happy-path-only` | info | One line per active spec: the contracts that carry scenarios and have none marked `(edge)` or `(failure)` |
|
|
188
276
|
| `spec-alias` | issue | A duplicate alias, an alias equal to another spec's id, or the reserved `example` |
|
|
189
|
-
| `evidence-citation` | warn | Under `lifecycle_gates: on`: a story's evidence names a token that no file in the project carries |
|
|
277
|
+
| `evidence-citation` | warn | Under `lifecycle_gates: on`: a story's evidence names a token that no file in the project carries (a removed contract is skipped) |
|
|
278
|
+
| `amendment-form` | warn | A blockquote under the contracts that starts `amended <date> by` but is not the grammar, comes before the first contract, or follows a `REMOVED`; it counts for nothing |
|
|
279
|
+
| `amendment-story` | issue | A marker's ref names no story, or more than one |
|
|
280
|
+
| `amendment-link` | warn | Neither the marker's story nor the spec lists the other (one side only is `spec-links`) |
|
|
281
|
+
| `amendment-date` | warn | A marker's date is not a date, is before the spec's `date:`, after tomorrow, or before the previous marker under the same contract |
|
|
190
282
|
|
|
191
283
|
Each report names the checkout it read. A worktree on an old branch, or a
|
|
192
284
|
second project bound to the same vault, can see different coverage.
|
|
@@ -222,6 +222,16 @@ each key activates.
|
|
|
222
222
|
warn.
|
|
223
223
|
- `doctor --vault --policy required[@<date>]` shows what `required` would
|
|
224
224
|
report, without writing anything.
|
|
225
|
+
- `flow` holds the team's flow policy: `wip.full` and `wip.part`, a
|
|
226
|
+
`people` map from each assignee's name to `time` (`full` or `part`), an
|
|
227
|
+
optional own `wip`, `git_names` and `left`, ages in days under `age_days`,
|
|
228
|
+
and `idle_days`, `window_weeks`, `capacity_weeks` (`[low, high]`),
|
|
229
|
+
`active_epics_max` and `grooming_days`. `groomed_at` is a stamp. No key has
|
|
230
|
+
a default in code, and a value of the wrong type counts as absent. A
|
|
231
|
+
person's WIP limit is their own `wip`, else `wip.<time>`, else none
|
|
232
|
+
(`wipLimitOf` in `scripts/flow-policy.mjs`). `configure --set` writes these
|
|
233
|
+
keys and merges them into the block, keeping the rest. Names are the
|
|
234
|
+
assignee as stories write it, never an email address.
|
|
225
235
|
|
|
226
236
|
## Contributing back
|
|
227
237
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.16",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
guide_for: spec
|
|
3
|
-
guide_version:
|
|
3
|
+
guide_version: 2
|
|
4
4
|
default_review: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -18,6 +18,10 @@ The systems analyst who writes the contract a test can be written from before th
|
|
|
18
18
|
- Mandatory sections present (How we solve / Behavioral contracts / Acceptance); Modules-on-disk and Testing may be omitted only for research spikes.
|
|
19
19
|
- For a spike spec: contracts read as questions-to-answer plus exit criteria.
|
|
20
20
|
- No TODOs, no '...', no empty bullets in the final body.
|
|
21
|
+
- A change to an active spec's contract carries a marker under it, `> amended YYYY-MM-DD by <EPIC-ID>/<story-id>: ADDED|MODIFIED|REMOVED โ <why>`; no new block is written in the older `> *Amended โฆ` form.
|
|
22
|
+
- No contract or step number is reused: ADDED takes the highest number in the file, removed contracts included, plus one; REMOVED keeps the number and heading with the body `Removed, see below.`, and a removed step stays as `N. Removed, see below.`
|
|
23
|
+
- Under MODIFIED the body is rewritten to the text in force, and the block quotes the text it replaced; a change written only in the block, over an unchanged body, fails.
|
|
24
|
+
- The first marker under a contract folds that contract's older blocks into the body without changing their meaning, and quotes them inside its own block.
|
|
21
25
|
|
|
22
26
|
## Common mistakes
|
|
23
27
|
|
|
@@ -36,4 +40,4 @@ The systems analyst who writes the contract a test can be written from before th
|
|
|
36
40
|
|
|
37
41
|
## Length
|
|
38
42
|
|
|
39
|
-
A contract is one paragraph and at most six scenario items. A spec past twelve contracts or 400 lines covers two subsystems: split it.
|
|
43
|
+
A contract is one paragraph and at most six scenario items. A spec past twelve contracts or 400 lines covers two subsystems: split it. Blocks under contracts (amendment markers and the text they quote) do not count toward this length.
|
|
@@ -174,6 +174,10 @@ export function menuGroups(f, settings, catalogs, { menu, now = Date.now, caps:
|
|
|
174
174
|
"The vault's own rules, shared by everyone who uses the vault: whether a story needs a spec, and whether doctor checks a story's plan and close. Kept in the vault's .projectstore.json; commit the vault after a change.",
|
|
175
175
|
items: list.map((s) => plainItem(f, s, null)),
|
|
176
176
|
});
|
|
177
|
+
// The flow policy is `--set` only (the configure spec, contract 2 as
|
|
178
|
+
// amended 2026-11-10): the menu has no widget for a keyed list, and the
|
|
179
|
+
// view names each key's form. Skipped here on purpose, not forgotten.
|
|
180
|
+
else if (g === "flow") continue;
|
|
177
181
|
}
|
|
178
182
|
return groups;
|
|
179
183
|
}
|
|
@@ -31,7 +31,8 @@ import {
|
|
|
31
31
|
fromTerminalIn,
|
|
32
32
|
commandForm,
|
|
33
33
|
} from "./lib.mjs";
|
|
34
|
-
import { planBindingKeys, applyBindingKeys, languageNames } from "./binding.mjs";
|
|
34
|
+
import { planBindingKeys, applyBindingKeys, languageNames, shellQuote } from "./binding.mjs";
|
|
35
|
+
import { isFlowSet, parseFlowSet, flowRows, flowForm, blockedAt, flowAt } from "./flow-policy.mjs";
|
|
35
36
|
import { bandKeys, epicIds, planBand, applyBand } from "./band.mjs";
|
|
36
37
|
import { planAgentsChange, applyAgentsChange } from "./agents-change.mjs";
|
|
37
38
|
import { plan as planInstall, apply as applyInstall, planOptions, isInteractive } from "./install-harness.mjs";
|
|
@@ -63,7 +64,13 @@ const parseBool = (v) => {
|
|
|
63
64
|
};
|
|
64
65
|
const onOff = (b) => (b === true ? "on" : b === false ? "off" : "not set");
|
|
65
66
|
const shown = (v) =>
|
|
66
|
-
v === null || v === undefined || v === ""
|
|
67
|
+
v === null || v === undefined || v === ""
|
|
68
|
+
? "โ"
|
|
69
|
+
: Array.isArray(v)
|
|
70
|
+
? v.join(", ") || "โ"
|
|
71
|
+
: typeof v === "object"
|
|
72
|
+
? JSON.stringify(v)
|
|
73
|
+
: String(v);
|
|
67
74
|
const tildeOf = (p, home) =>
|
|
68
75
|
home && (p === home || String(p).startsWith(home + "/")) ? "~" + String(p).slice(home.length) : String(p);
|
|
69
76
|
|
|
@@ -265,6 +272,22 @@ export function settingsOf(f) {
|
|
|
265
272
|
pol("spec_policy", v.spec_policy || "optional", "policy"),
|
|
266
273
|
pol("lifecycle_gates", Object.hasOwn(v, "lifecycle_gates") ? String(v.lifecycle_gates) : "not set", "bool"),
|
|
267
274
|
);
|
|
275
|
+
// The flow policy (contract 3 as amended 2026-11-10): its own group,
|
|
276
|
+
// listed only while a key is set, so a vault with no `flow` prints what it
|
|
277
|
+
// printed before. `--set` only: the menu shows none of these rows.
|
|
278
|
+
for (const r of f.vaultCfg && !f.vaultCfg.corrupt ? flowRows(v.flow) : [])
|
|
279
|
+
out.push({
|
|
280
|
+
group: "flow",
|
|
281
|
+
key: r.key,
|
|
282
|
+
value: r.value,
|
|
283
|
+
source: "vault_config",
|
|
284
|
+
file,
|
|
285
|
+
class: "changeable",
|
|
286
|
+
team: true,
|
|
287
|
+
writer: "vault",
|
|
288
|
+
rail: null,
|
|
289
|
+
person: r.person,
|
|
290
|
+
});
|
|
268
291
|
}
|
|
269
292
|
for (const s of out) s.set = setForm(s, f);
|
|
270
293
|
return out;
|
|
@@ -273,6 +296,11 @@ export function settingsOf(f) {
|
|
|
273
296
|
// The --set form a key is changed with, for the view and the envelope.
|
|
274
297
|
function setForm(s, f) {
|
|
275
298
|
if (s.class !== "changeable") return null;
|
|
299
|
+
if (s.group === "flow") {
|
|
300
|
+
const form = flowForm(s.key);
|
|
301
|
+
// A person's name may hold a space or a dot, and `[`โฆ`]` globs in zsh.
|
|
302
|
+
return form && s.person !== null ? `--set ${shellQuote(form.slice("--set ".length))}` : form;
|
|
303
|
+
}
|
|
276
304
|
const value =
|
|
277
305
|
s.rail === "bool"
|
|
278
306
|
? "on|off"
|
|
@@ -301,6 +329,19 @@ export function resolveSets(f, sets, { harness = null, nearest = null } = {}) {
|
|
|
301
329
|
const known = [...new Set(keys.map((s) => s.key))];
|
|
302
330
|
const harnessesNamed = f.used.map((h) => h.display_name || h.id).join(", ") || "none";
|
|
303
331
|
for (const raw of sets) {
|
|
332
|
+
// A flow key is parsed whole, before the split at the first `=`: a
|
|
333
|
+
// person's name may hold a dot (contract 2 as amended 2026-11-10).
|
|
334
|
+
if (isFlowSet(raw)) {
|
|
335
|
+
const vcfg = f.vaultCfg && !f.vaultCfg.corrupt ? f.vaultCfg.cfg : {};
|
|
336
|
+
const r = parseFlowSet(raw, { nearest, flow: vcfg.flow });
|
|
337
|
+
if (r.usage) return { usage: r.usage };
|
|
338
|
+
const { key, path, value } = r.change;
|
|
339
|
+
if (!f.vault) return { usage: `${key} lives in the vault's config, and this binding names no vault` };
|
|
340
|
+
if (seen.has(key)) return { usage: `${key} is named twice` };
|
|
341
|
+
seen.add(key);
|
|
342
|
+
out.push({ key, writer: "vault", path: ["flow", ...path], value });
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
304
345
|
const at = String(raw).indexOf("=");
|
|
305
346
|
if (at < 1) return { usage: `--set takes <key>=<value>, got ${raw}` };
|
|
306
347
|
let key = String(raw).slice(0, at).trim();
|
|
@@ -464,20 +505,39 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
464
505
|
);
|
|
465
506
|
const strict = readVaultConfigStrict(f.vault);
|
|
466
507
|
if (strict.corrupt) return refuse(`${strict.path} is not valid JSON โ fix or remove it first`);
|
|
467
|
-
|
|
508
|
+
// A flow change carries a path into the `flow` object; a node on it that
|
|
509
|
+
// exists and is not an object is never replaced (contract 11).
|
|
510
|
+
for (const c of policy.filter((c) => c.path)) {
|
|
511
|
+
const at = blockedAt(strict.cfg, c.path.slice(1));
|
|
512
|
+
if (at) return refuse(`${c.key}: ${at} is not an object โ fix or remove it first`);
|
|
513
|
+
}
|
|
514
|
+
const patch = {};
|
|
515
|
+
for (const c of policy) {
|
|
516
|
+
const path = c.path || [c.key];
|
|
517
|
+
let node = patch;
|
|
518
|
+
for (const step of path.slice(0, -1)) node = Object.hasOwn(node, step) ? node[step] : (node[step] = {});
|
|
519
|
+
node[path[path.length - 1]] = c.value;
|
|
520
|
+
}
|
|
468
521
|
const next = nextVaultConfig(strict.cfg, patch, new Date().toISOString());
|
|
522
|
+
const flowBefore = (c) => flowAt(strict.cfg.flow, c.path.slice(1)) ?? null;
|
|
469
523
|
const inEffect = (k) => strict.cfg[k] ?? (k === "spec_policy" ? "optional" : undefined);
|
|
470
524
|
// `lifecycle_gates=on` on a vault already on but with no gates_since is a
|
|
471
525
|
// change: it stamps the date the gates count from (the configure spec,
|
|
472
526
|
// contract 7 as amended 2026-11-08).
|
|
473
527
|
const stampsGates = (c) => c.key === "lifecycle_gates" && c.value === "on" && !strict.cfg.gates_since;
|
|
474
|
-
const changed = policy.filter((c) =>
|
|
528
|
+
const changed = policy.filter((c) =>
|
|
529
|
+
c.path
|
|
530
|
+
? JSON.stringify(flowBefore(c)) !== JSON.stringify(c.value)
|
|
531
|
+
: inEffect(c.key) !== c.value || stampsGates(c),
|
|
532
|
+
);
|
|
475
533
|
for (const c of changed)
|
|
476
534
|
items.push({
|
|
477
535
|
key: c.key,
|
|
478
536
|
file: tildeOf(strict.path, f.home),
|
|
479
|
-
before:
|
|
480
|
-
|
|
537
|
+
before: c.path
|
|
538
|
+
? shown(flowBefore(c) ?? "not set")
|
|
539
|
+
: shown(strict.cfg[c.key] ?? (c.key === "spec_policy" ? "optional" : "not set")),
|
|
540
|
+
after: shown(c.value ?? "not set"),
|
|
481
541
|
team: true,
|
|
482
542
|
note:
|
|
483
543
|
c.key === "spec_policy" && next.spec_policy_since !== strict.cfg.spec_policy_since
|
|
@@ -486,7 +546,13 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
486
546
|
? GATES_NOTE
|
|
487
547
|
: null,
|
|
488
548
|
});
|
|
489
|
-
if (changed.length)
|
|
549
|
+
if (changed.length)
|
|
550
|
+
steps.push({
|
|
551
|
+
writer: "vault",
|
|
552
|
+
patch,
|
|
553
|
+
keys: changed.map((c) => c.key),
|
|
554
|
+
flowPaths: policy.filter((c) => c.path).map((c) => c.path.slice(1)),
|
|
555
|
+
});
|
|
490
556
|
}
|
|
491
557
|
|
|
492
558
|
// The overlays: one agents change per harness, through the core
|
|
@@ -741,6 +807,12 @@ export function applyChanges(f, steps, { env = process.env, home = homedir(), ro
|
|
|
741
807
|
// by every checkout, and a change made while the question waited is kept.
|
|
742
808
|
const now = readVaultConfigStrict(f.vault);
|
|
743
809
|
if (now.corrupt) throw new Error(`${now.path} stopped being valid JSON after the plan`);
|
|
810
|
+
// A flow path whose node stopped being an object since the PLAN is
|
|
811
|
+
// refused here too, never replaced (contract 11 as amended 2026-11-10).
|
|
812
|
+
for (const path of s.flowPaths || []) {
|
|
813
|
+
const at = blockedAt(now.cfg, path);
|
|
814
|
+
if (at) throw new Error(`${at} stopped being an object after the plan โ fix or remove it first`);
|
|
815
|
+
}
|
|
744
816
|
writeVaultConfig(f.vault, nextVaultConfig(now.cfg, s.patch, new Date().toISOString()));
|
|
745
817
|
wrote.push(vaultConfigPath(f.vault));
|
|
746
818
|
} else if (s.writer === "overlay") {
|
|
@@ -793,6 +865,7 @@ const GROUP_TITLES = (f) => ({
|
|
|
793
865
|
surfaces: "Status line and band",
|
|
794
866
|
vault: "Vault and templates",
|
|
795
867
|
policy: "Policy",
|
|
868
|
+
flow: "Flow policy",
|
|
796
869
|
...Object.fromEntries(f.used.map((h) => [`agents:${h.id}`, `Agents ยท ${h.display_name || h.id}`])),
|
|
797
870
|
});
|
|
798
871
|
|
|
@@ -103,6 +103,20 @@ export function grepProjectLines(cwd, pattern, { exclude = [], timeout = 10000,
|
|
|
103
103
|
return { status: "ok", lines };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// The `--since` window git is given, shared with spec-drift.mjs.
|
|
107
|
+
//
|
|
108
|
+
// A bare YYYY-MM-DD gets midnight appended, and it is not cosmetic. Git's
|
|
109
|
+
// approxidate reads a date with no time as THAT DATE AT THE CURRENT TIME OF
|
|
110
|
+
// DAY, so `--since=2026-09-08` run at 17:46 silently excludes everything
|
|
111
|
+
// committed earlier the same day. `started_at` is written as a bare date,
|
|
112
|
+
// so a story started and finished in one day proposed `files: []` and the
|
|
113
|
+
// caller had no way to tell that from "nothing was committed" (measured
|
|
114
|
+
// 2026-09-08: --since=2026-09-08 returned nothing while
|
|
115
|
+
// --since="2026-09-08 00:00" returned the day's commit).
|
|
116
|
+
export function sinceWindow(since) {
|
|
117
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(since) ? `${since} 00:00:00` : since;
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
function filterFiles(list) {
|
|
107
121
|
return [...new Set(list)]
|
|
108
122
|
.map((f) => f.trim())
|
|
@@ -128,16 +142,7 @@ function main() {
|
|
|
128
142
|
} else if (since) {
|
|
129
143
|
// committer-date window; work committed before the story went in-progress
|
|
130
144
|
// is invisible by design (story-scoped attribution).
|
|
131
|
-
|
|
132
|
-
// A bare YYYY-MM-DD gets midnight appended, and it is not cosmetic. Git's
|
|
133
|
-
// approxidate reads a date with no time as THAT DATE AT THE CURRENT TIME OF
|
|
134
|
-
// DAY, so `--since=2026-09-08` run at 17:46 silently excludes everything
|
|
135
|
-
// committed earlier the same day. `started_at` is written as a bare date,
|
|
136
|
-
// so a story started and finished in one day proposed `files: []` and the
|
|
137
|
-
// caller had no way to tell that from "nothing was committed" (measured
|
|
138
|
-
// 2026-09-08: --since=2026-09-08 returned nothing while
|
|
139
|
-
// --since="2026-09-08 00:00" returned the day's commit).
|
|
140
|
-
const window = /^\d{4}-\d{2}-\d{2}$/.test(since) ? `${since} 00:00:00` : since;
|
|
145
|
+
const window = sinceWindow(since);
|
|
141
146
|
const out = git(["log", `--since=${window}`, "--name-only", "--pretty=format:"]);
|
|
142
147
|
committed = out ? out.split("\n") : [];
|
|
143
148
|
usedRange = `--since=${window}`;
|