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.
Files changed (30) hide show
  1. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  2. package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
  3. package/node_modules/projectstore/README.md +5 -0
  4. package/node_modules/projectstore/agents/planner.md +4 -1
  5. package/node_modules/projectstore/agents/reviewer.md +77 -2
  6. package/node_modules/projectstore/commands/doctor.md +11 -0
  7. package/node_modules/projectstore/commands/graph.md +5 -1
  8. package/node_modules/projectstore/commands/story.md +4 -1
  9. package/node_modules/projectstore/docs/contracts-and-tests.md +94 -2
  10. package/node_modules/projectstore/docs/extending.md +10 -0
  11. package/node_modules/projectstore/package.json +1 -1
  12. package/node_modules/projectstore/scaffold/guides/spec.md +6 -2
  13. package/node_modules/projectstore/scripts/configure-menu.mjs +4 -0
  14. package/node_modules/projectstore/scripts/configure.mjs +80 -7
  15. package/node_modules/projectstore/scripts/diff-refs.mjs +15 -10
  16. package/node_modules/projectstore/scripts/doctor.mjs +181 -12
  17. package/node_modules/projectstore/scripts/flow-policy.mjs +264 -0
  18. package/node_modules/projectstore/scripts/graph.mjs +12 -0
  19. package/node_modules/projectstore/scripts/lib.mjs +131 -2
  20. package/node_modules/projectstore/scripts/mcp.mjs +2 -2
  21. package/node_modules/projectstore/scripts/query.mjs +8 -2
  22. package/node_modules/projectstore/scripts/spec-drift.mjs +513 -0
  23. package/node_modules/projectstore/scripts/verbs.mjs +1 -1
  24. package/node_modules/projectstore/templates/de/spec.md.tmpl +5 -1
  25. package/node_modules/projectstore/templates/en/spec.md.tmpl +5 -1
  26. package/node_modules/projectstore/templates/es/spec.md.tmpl +5 -1
  27. package/node_modules/projectstore/templates/fr/spec.md.tmpl +5 -1
  28. package/node_modules/projectstore/templates/ru/spec.md.tmpl +5 -1
  29. package/node_modules/projectstore/templates/zh/spec.md.tmpl +5 -1
  30. 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.14",
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.14",
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 silently.
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 way it records its other amendments before the code is written.
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.14",
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: 1
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 === "" ? "โ€”" : Array.isArray(v) ? v.join(", ") || "โ€”" : String(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
- const patch = Object.fromEntries(policy.map((c) => [c.key, c.value]));
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) => inEffect(c.key) !== c.value || stampsGates(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: shown(strict.cfg[c.key] ?? (c.key === "spec_policy" ? "optional" : "not set")),
480
- after: shown(c.value),
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) steps.push({ writer: "vault", patch, keys: changed.map((c) => c.key) });
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}`;