projectstore-claude 0.30.8 → 0.30.10
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 -5
- package/node_modules/projectstore/agents/archaeologist.md +1 -1
- package/node_modules/projectstore/agents/critic.md +26 -3
- package/node_modules/projectstore/agents/librarian.md +1 -1
- package/node_modules/projectstore/agents/planner.md +13 -1
- package/node_modules/projectstore/agents/reviewer.md +19 -2
- package/node_modules/projectstore/commands/adr.md +10 -0
- package/node_modules/projectstore/commands/bind.md +4 -3
- package/node_modules/projectstore/commands/concept.md +9 -0
- package/node_modules/projectstore/commands/doctor.md +17 -4
- package/node_modules/projectstore/commands/epic.md +10 -0
- package/node_modules/projectstore/commands/meeting.md +9 -0
- package/node_modules/projectstore/commands/research.md +9 -0
- package/node_modules/projectstore/commands/review.md +11 -7
- package/node_modules/projectstore/commands/runbook.md +9 -0
- package/node_modules/projectstore/commands/spec.md +10 -0
- package/node_modules/projectstore/commands/story.md +25 -1
- package/node_modules/projectstore/docs/contracts-and-tests.md +107 -0
- package/node_modules/projectstore/docs/extending.md +44 -5
- package/node_modules/projectstore/harnesses/claude-code.json +3 -3
- package/node_modules/projectstore/harnesses/codex.json +1 -1
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scaffold/guides/adr.md +42 -0
- package/node_modules/projectstore/scaffold/guides/concept.md +10 -0
- package/node_modules/projectstore/scaffold/guides/epic.md +36 -0
- package/node_modules/projectstore/scaffold/guides/meeting.md +11 -0
- package/node_modules/projectstore/scaffold/guides/research.md +16 -0
- package/node_modules/projectstore/scaffold/guides/runbook.md +12 -0
- package/node_modules/projectstore/scaffold/guides/spec.md +39 -0
- package/node_modules/projectstore/scaffold/guides/story.md +34 -0
- package/node_modules/projectstore/scripts/cli.mjs +55 -1
- package/node_modules/projectstore/scripts/configure-menu.mjs +2 -1
- package/node_modules/projectstore/scripts/configure.mjs +12 -2
- package/node_modules/projectstore/scripts/diff-refs.mjs +39 -0
- package/node_modules/projectstore/scripts/doctor.mjs +648 -33
- package/node_modules/projectstore/scripts/guides.mjs +217 -0
- package/node_modules/projectstore/scripts/harness.mjs +2 -0
- package/node_modules/projectstore/scripts/lib.mjs +163 -1
- package/node_modules/projectstore/scripts/mcp-server.mjs +11 -4
- package/node_modules/projectstore/scripts/mcp.mjs +30 -3
- package/node_modules/projectstore/scripts/read-verbs.mjs +59 -3
- package/node_modules/projectstore/scripts/scaffold.mjs +1 -1
- package/node_modules/projectstore/scripts/story-section.mjs +16 -1
- package/node_modules/projectstore/scripts/verbs.mjs +33 -1
- package/node_modules/projectstore/scripts/version-guard.mjs +5 -10
- package/node_modules/projectstore/skills/projectstore-peer-reviewer/SKILL.md +9 -3
- package/node_modules/projectstore/templates/de/spec.md.tmpl +9 -1
- package/node_modules/projectstore/templates/de/story.md.tmpl +1 -0
- package/node_modules/projectstore/templates/en/spec.md.tmpl +8 -1
- package/node_modules/projectstore/templates/en/story.md.tmpl +1 -0
- package/node_modules/projectstore/templates/es/spec.md.tmpl +9 -1
- package/node_modules/projectstore/templates/es/story.md.tmpl +1 -0
- package/node_modules/projectstore/templates/fr/spec.md.tmpl +9 -1
- package/node_modules/projectstore/templates/fr/story.md.tmpl +1 -0
- package/node_modules/projectstore/templates/ru/spec.md.tmpl +8 -1
- package/node_modules/projectstore/templates/ru/story.md.tmpl +1 -0
- package/node_modules/projectstore/templates/zh/spec.md.tmpl +8 -1
- package/node_modules/projectstore/templates/zh/story.md.tmpl +1 -0
- package/package.json +2 -2
- package/node_modules/projectstore/scaffold/checklists.json +0 -88
|
@@ -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.
|
|
@@ -6,7 +6,8 @@ Since v0.14 the kind machinery is layout-driven: `draft.mjs` builds ANY kind
|
|
|
6
6
|
declared in the layout, and doctor's template check follows the layout instead
|
|
7
7
|
of a hardcoded list. A new kind needs **six touch points** — note that **all
|
|
8
8
|
six live inside the plugin installation**, not in your vault (there is no
|
|
9
|
-
vault-side layout or template override)
|
|
9
|
+
vault-side layout or template override). The one exception is the guide
|
|
10
|
+
(step 4): a vault may override it, file by file.
|
|
10
11
|
|
|
11
12
|
1. **Layout folder entry** — `scaffold/layouts/<name>.json` → `folders`:
|
|
12
13
|
|
|
@@ -46,8 +47,31 @@ vault-side layout or template override):
|
|
|
46
47
|
but an empty date sorts first, so the kind's rows pile up at the top of
|
|
47
48
|
its index and stay there.
|
|
48
49
|
|
|
49
|
-
4. **
|
|
50
|
-
|
|
50
|
+
4. **Guide file** — `scaffold/guides/<kind>.md`, the kind's authoring guide:
|
|
51
|
+
frontmatter `guide_for: <kind>`, `guide_version: 1` and `default_review`
|
|
52
|
+
(bare values, no trailing comments), then `## Lens`, `## Must`,
|
|
53
|
+
`## Common mistakes`, `## Do not write` and `## Length`. Only `## Must` is
|
|
54
|
+
required; its plain bullets are the checklist the critic checks. The kind's
|
|
55
|
+
command reads the guide before the draft, `/projectstore:review` and the
|
|
56
|
+
critic check its `## Must`, and the `projectstore-peer-reviewer` skill reads
|
|
57
|
+
its `default_review`. The headings are English in every vault language; the
|
|
58
|
+
package defaults are English only. Bump `guide_version` whenever you change
|
|
59
|
+
a default's text, then record it:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
node tests/fixtures/guides.mjs --write
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A project overrides a guide in its vault, as `<vault>/guides/<kind>.md`,
|
|
66
|
+
which replaces the package file whole. To start from the default:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
npx projectstore guide eject <kind> --write
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Doctor reports a vault guide that does not parse (`guide-shape`), names an
|
|
73
|
+
unknown kind (`guide-unknown`), or was made from an older default
|
|
74
|
+
(`guide-stale`).
|
|
51
75
|
|
|
52
76
|
5. **Command prompt** — `commands/<kind>.md`, a prompt (not code). It renders
|
|
53
77
|
the draft with the line below, previews it, and gates every write behind
|
|
@@ -181,8 +205,23 @@ nothing checking that they agree — see the PS-I18N epic's backlog.
|
|
|
181
205
|
|
|
182
206
|
`<vault>/.projectstore.json` (vault root — committed with the vault, survives
|
|
183
207
|
clones) carries `spec_policy: required|optional`, `lifecycle_gates: on|off`,
|
|
184
|
-
and `
|
|
185
|
-
`commands/doctor.md
|
|
208
|
+
`spec_policy_since` and `gates_since` (ISO-8601). See ADR-007 and the spec
|
|
209
|
+
policy ADR in the project vault, and `commands/doctor.md`, for which checks
|
|
210
|
+
each key activates.
|
|
211
|
+
|
|
212
|
+
- `configure` stamps `spec_policy_since` when the policy becomes `required`,
|
|
213
|
+
and `gates_since` when the lifecycle gates turn on. A done story closed
|
|
214
|
+
before the date is legacy. The lifecycle checks read `spec_policy_since`,
|
|
215
|
+
then `gates_since`.
|
|
216
|
+
- A story may declare `no_spec: "<reason>"` at the plan gate instead of a
|
|
217
|
+
covering spec. Under `required`, doctor accepts it. Under any policy it
|
|
218
|
+
checks the declaration:
|
|
219
|
+
- an empty reason, or `no_spec` beside `specs:`, is an issue;
|
|
220
|
+
- three or more source files in the story's `code_refs` is a warn;
|
|
221
|
+
- when opt-outs outnumber covered stories among code stories, that is a
|
|
222
|
+
warn.
|
|
223
|
+
- `doctor --vault --policy required[@<date>]` shows what `required` would
|
|
224
|
+
report, without writing anything.
|
|
186
225
|
|
|
187
226
|
## Contributing back
|
|
188
227
|
|
|
@@ -311,9 +311,9 @@
|
|
|
311
311
|
"dir": ".",
|
|
312
312
|
"file": ".mcp.json",
|
|
313
313
|
"entry": "scripts/mcp-server.mjs",
|
|
314
|
-
"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.",
|
|
315
315
|
"scope": "plugin",
|
|
316
|
-
"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.",
|
|
317
317
|
"launch": {
|
|
318
318
|
"cwd": "project",
|
|
319
319
|
"project_dir_in_env": true,
|
|
@@ -326,7 +326,7 @@
|
|
|
326
326
|
],
|
|
327
327
|
"default_syntax": "${VAR:-default}",
|
|
328
328
|
"unset_placeholder_in_env": "literal",
|
|
329
|
-
"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",
|
|
330
330
|
"entrypoint": "sdk-cli (headless -p), plugin loaded with --plugin-dir; the marketplace install and an interactive session are A9's",
|
|
331
331
|
"protocol_era": "initialize",
|
|
332
332
|
"protocol_version_seen": "2025-11-25",
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"kind": "host",
|
|
241
241
|
"scope": "project",
|
|
242
242
|
"scope_reason": "Codex auto-discovers a plugin-root .mcp.json even when the manifest does not declare mcpServers — measured.",
|
|
243
|
-
"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."
|
|
244
244
|
},
|
|
245
245
|
"skills": {
|
|
246
246
|
"supported": true,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.10",
|
|
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",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
guide_for: adr
|
|
3
|
+
guide_version: 1
|
|
4
|
+
default_review: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Lens
|
|
8
|
+
|
|
9
|
+
The architect who will be asked in a year why this was decided. Write for a stranger who has only this file: they must be able to implement the decision, and to find what was rejected and why.
|
|
10
|
+
|
|
11
|
+
## Must
|
|
12
|
+
|
|
13
|
+
- Decision section is unambiguous — a stranger could implement it from this file alone, with no other context.
|
|
14
|
+
- At least one named Alternative is present with an explicit rejection reason ('Rejected because: ...').
|
|
15
|
+
- Consequences include at least one Negative / trade-off stated honestly. Reject sales framing.
|
|
16
|
+
- All hand-wavy referents ('we', 'users', 'the system') are resolved to concrete actors or components.
|
|
17
|
+
- No marketing language: 'blazingly', 'robust', 'best-in-class', 'magnificent', 'rock-solid', etc.
|
|
18
|
+
- Context explains why the decision is needed NOW (vs. a year ago or in 6 months).
|
|
19
|
+
- References cite at least one external doc / prior ADR / RFC if relevant.
|
|
20
|
+
- Status, Date, Authors metadata filled (not template placeholders).
|
|
21
|
+
- No TODOs, no '...', no empty bullets in the final body.
|
|
22
|
+
|
|
23
|
+
## Common mistakes
|
|
24
|
+
|
|
25
|
+
- The Decision restates the Context or lists options instead of deciding. Number each decision, so a test or a reviewer can tell whether the code does it.
|
|
26
|
+
- Alternatives are straw men: no real Pros, and a "Rejected because" that fits any project. Name this project's constraint.
|
|
27
|
+
- Claims about the code written from memory. Cite the file and symbol; copy counts and numbers from the source or a measurement.
|
|
28
|
+
- Consequences list only the good. Negative / trade-offs is the section a critic reads first: state the cost and who pays it.
|
|
29
|
+
- A rule with no enforcement point. Say which test, doctor check or command text makes it true.
|
|
30
|
+
- An accepted ADR it amends or contradicts is not named, or is contradicted silently.
|
|
31
|
+
- Artifacts named by session shorthand instead of their title (and, for an epic or story, its parent).
|
|
32
|
+
|
|
33
|
+
## Do not write
|
|
34
|
+
|
|
35
|
+
- A plan, a file list or a task list: those belong to the spec and the story.
|
|
36
|
+
- The Decision restated under Rationale.
|
|
37
|
+
- Marketing adjectives, or hedges (might, ideally, probably) inside a Decision.
|
|
38
|
+
- The ADR's own revision history in the body: the vault history holds it.
|
|
39
|
+
|
|
40
|
+
## Length
|
|
41
|
+
|
|
42
|
+
One decision fits one screen; the whole ADR 100 to 250 lines. Past 300, split it into ADRs with their own lifecycles.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
guide_for: epic
|
|
3
|
+
guide_version: 1
|
|
4
|
+
default_review: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Lens
|
|
8
|
+
|
|
9
|
+
The product owner who must say, later, whether this epic worked. State the outcome in terms someone can check, then the stories that deliver it.
|
|
10
|
+
|
|
11
|
+
## Must
|
|
12
|
+
|
|
13
|
+
- Goal is measurable — success can be verified, not just described.
|
|
14
|
+
- Context explains why this epic exists and which downstream pieces depend on it.
|
|
15
|
+
- Stories table has at least 2 entries; first story description is concrete enough to start.
|
|
16
|
+
- Expected Results contains testable outcomes, not aspirations.
|
|
17
|
+
- Dependencies list external blockers (people, systems, decisions), not just sibling stories.
|
|
18
|
+
- Open Questions are real unknowns, not rhetorical filler.
|
|
19
|
+
- Priority assigned (p0 / p1 / p2), not left as default if epic is non-trivial.
|
|
20
|
+
|
|
21
|
+
## Common mistakes
|
|
22
|
+
|
|
23
|
+
- A Goal that describes activity (build, improve, refactor), not a result that can be verified.
|
|
24
|
+
- Expected Results that are aspirations. Each one should be a check someone can run or read.
|
|
25
|
+
- A Stories table that is a to-do dump: a vague first story, no order.
|
|
26
|
+
- Dependencies that name only sibling stories. The blockers are people, systems and decisions outside the epic.
|
|
27
|
+
- `code_refs` left empty after the first story lands, or the priority left at the default.
|
|
28
|
+
|
|
29
|
+
## Do not write
|
|
30
|
+
|
|
31
|
+
- Implementation detail (files, functions): it belongs in the spec and the plans.
|
|
32
|
+
- A story's acceptance criteria or an ADR's text copied into the epic: link them.
|
|
33
|
+
|
|
34
|
+
## Length
|
|
35
|
+
|
|
36
|
+
Goal in two to four sentences; the whole epic 40 to 100 lines plus the stories table.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
guide_for: research
|
|
3
|
+
guide_version: 1
|
|
4
|
+
default_review: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Must
|
|
8
|
+
|
|
9
|
+
- Question is sharp — answerable by yes/no/comparison, not 'tell me about X'.
|
|
10
|
+
- TL;DR is a single paragraph that gives the conclusion before the body.
|
|
11
|
+
- Method describes how findings were gathered (docs read, benchmarks run, experts asked) — not just 'I researched'.
|
|
12
|
+
- Comparison table has populated Pros AND Cons cells; empty cells flagged.
|
|
13
|
+
- Conclusion explicitly ties back to the original Question. Recommendation is actionable.
|
|
14
|
+
- References are linkable (URL / file path), not 'see [some doc]'.
|
|
15
|
+
- No unstated benchmarks ('it's faster' without numbers).
|
|
16
|
+
- Findings are evidence-based, not opinion. Where opinion appears, it's labelled.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
guide_for: runbook
|
|
3
|
+
guide_version: 1
|
|
4
|
+
default_review: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Must
|
|
8
|
+
|
|
9
|
+
- Purpose states WHEN to run this runbook (trigger conditions).
|
|
10
|
+
- Prerequisites are listable checks, not prose.
|
|
11
|
+
- Verification step exists and is testable.
|
|
12
|
+
- Rollback step exists for any irreversible action.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
guide_for: spec
|
|
3
|
+
guide_version: 1
|
|
4
|
+
default_review: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Lens
|
|
8
|
+
|
|
9
|
+
The systems analyst who writes the contract a test can be written from before the code exists. Each contract states an observable outcome and the scenario that would prove it.
|
|
10
|
+
|
|
11
|
+
## Must
|
|
12
|
+
|
|
13
|
+
- How we solve references at least one ADR by id; the references resolve.
|
|
14
|
+
- Behavioral contracts are numbered and individually testable — each states an observable outcome, not an implementation step.
|
|
15
|
+
- Acceptance items are checkboxes; acceptance is explicitly ADDITIVE to the covered stories' own criteria, not a replacement.
|
|
16
|
+
- Acceptance items that belong to specific stories carry a '— stories: <id>' attribution; items meant for all covered stories are unattributed on purpose.
|
|
17
|
+
- frontmatter stories: uses <epic-id>/<story-id> path form, inline flow list.
|
|
18
|
+
- Mandatory sections present (How we solve / Behavioral contracts / Acceptance); Modules-on-disk and Testing may be omitted only for research spikes.
|
|
19
|
+
- For a spike spec: contracts read as questions-to-answer plus exit criteria.
|
|
20
|
+
- No TODOs, no '...', no empty bullets in the final body.
|
|
21
|
+
|
|
22
|
+
## Common mistakes
|
|
23
|
+
|
|
24
|
+
- A contract that describes an implementation step (calls X, stores Y) instead of what a caller observes.
|
|
25
|
+
- Scenarios that show only the happy path. Add the boundary, the invalid input and the failed dependency a production run will hit.
|
|
26
|
+
- Acceptance that restates the covered stories' own criteria. It is additive: each item adds something only the spec promises.
|
|
27
|
+
- Numbers, tables and contract references copied from memory. Open the ADR and the sibling spec and copy from them.
|
|
28
|
+
- Frontmatter lists in block form (the parser reads inline flow), or an `adr:` id that does not resolve.
|
|
29
|
+
- A citation token for a real key written in prose or an example. Use the reserved key, as in `[example#1]`.
|
|
30
|
+
|
|
31
|
+
## Do not write
|
|
32
|
+
|
|
33
|
+
- The argument for the decision, or its alternatives: link the ADR.
|
|
34
|
+
- A plan or a task list: that is the story's Implementation Plan.
|
|
35
|
+
- Files the spec does not govern, or code beyond a three-line illustration.
|
|
36
|
+
|
|
37
|
+
## Length
|
|
38
|
+
|
|
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.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
guide_for: story
|
|
3
|
+
guide_version: 1
|
|
4
|
+
default_review: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Lens
|
|
8
|
+
|
|
9
|
+
The engineer who picks this up in a fresh session, with none of this conversation. Say who benefits and what changes, in steps small enough to finish one at a time, and criteria a test can be written from.
|
|
10
|
+
|
|
11
|
+
## Must
|
|
12
|
+
|
|
13
|
+
- Description states the user/actor and the value of the change.
|
|
14
|
+
- Decomposition checklist has items granular enough to finish in a session each.
|
|
15
|
+
- Acceptance Criteria are testable (someone could write tests from these).
|
|
16
|
+
- Technical Notes flag any non-obvious risks or constraints.
|
|
17
|
+
|
|
18
|
+
## Common mistakes
|
|
19
|
+
|
|
20
|
+
- Acceptance criteria that repeat decomposition tasks (implement X) instead of an outcome someone can observe.
|
|
21
|
+
- A decomposition item the size of the story (Tests, Docs) with no named cases.
|
|
22
|
+
- Contract numbers and tables quoted from memory. Open the covering spec or ADR and copy them.
|
|
23
|
+
- A non-trivial "how" with neither a `specs:` entry nor a `no_spec` reason.
|
|
24
|
+
- Technical Notes left empty while the risks are known. Put the trap there, not only in the chat.
|
|
25
|
+
|
|
26
|
+
## Do not write
|
|
27
|
+
|
|
28
|
+
- The Implementation Plan or the Final Summary at creation: the gates write them.
|
|
29
|
+
- A spec's contracts restated in full: cite them by number.
|
|
30
|
+
- A criterion that cannot fail.
|
|
31
|
+
|
|
32
|
+
## Length
|
|
33
|
+
|
|
34
|
+
Description one to three short paragraphs; five to nine decomposition items; four to ten criteria; the plan a thin route of at most 40 lines.
|
|
@@ -68,7 +68,8 @@ import {
|
|
|
68
68
|
SUBCOMMANDS as BAND_SUBCOMMANDS,
|
|
69
69
|
} from "./band.mjs";
|
|
70
70
|
import { dispatch, envelope, usageFail, ownEnv, nearest, notBoundText, PACKAGE_ROOT } from "./verbs.mjs";
|
|
71
|
-
import { READ_HANDLERS } from "./read-verbs.mjs";
|
|
71
|
+
import { READ_HANDLERS, guideVault } from "./read-verbs.mjs";
|
|
72
|
+
import { checkGuideName, planEject, applyEject } from "./guides.mjs";
|
|
72
73
|
|
|
73
74
|
// What this module exported before the table moved, from where it lives now.
|
|
74
75
|
export {
|
|
@@ -640,8 +641,61 @@ async function runInstallVerb({ row, values, project, env, cwd, stdin, stdout, a
|
|
|
640
641
|
// Every verb's handler, keyed by verb: the read half from read-verbs.mjs,
|
|
641
642
|
// the rest here. Exported so a test can wrap one (setup's sandboxed home) in
|
|
642
643
|
// a table of its own.
|
|
644
|
+
// guide — the read is the read runner's; `guide eject <kind>` is the one
|
|
645
|
+
// write (the authoring-guides ADR, decision 5): it copies the package guide
|
|
646
|
+
// into <vault>/guides/ with based_on, create-only, and confirms as scaffold
|
|
647
|
+
// does — a terminal asks, --write without one is the confirmation.
|
|
648
|
+
async function runGuideVerb(ctx) {
|
|
649
|
+
const { row, values, positionals, cfg, project, env, stdin, stdout, stderr, ask } = ctx;
|
|
650
|
+
if (positionals[0] !== "eject") return READ_HANDLERS.guide(ctx);
|
|
651
|
+
const usage = (m) => usageFail(Object.assign(new Error(m), { code: "USAGE" }), { verb: row.verb, ...ctx });
|
|
652
|
+
const name = positionals[1];
|
|
653
|
+
if (!name) return usage("guide eject takes a kind, such as `guide eject spec`");
|
|
654
|
+
if (values.section || values.source) return usage("--section and --source belong to the read, not to eject");
|
|
655
|
+
try {
|
|
656
|
+
checkGuideName(name);
|
|
657
|
+
} catch (e) {
|
|
658
|
+
return usageFail(e, { verb: row.verb, ...ctx });
|
|
659
|
+
}
|
|
660
|
+
const emit = (ok, result, text, code) => {
|
|
661
|
+
if (values.json) stdout.write(JSON.stringify(envelope(row.verb, project, ok, result), null, 2) + "\n");
|
|
662
|
+
else (ok ? stdout : stderr).write(text);
|
|
663
|
+
return code;
|
|
664
|
+
};
|
|
665
|
+
const plan = planEject(name, { vault: guideVault(cfg, project), root: PACKAGE_ROOT });
|
|
666
|
+
const result = (written) => ({ name, target: plan.target, based_on: plan.based_on ?? null, written });
|
|
667
|
+
if (!plan.ok) return emit(false, { ...result(false), error: plan.refusal }, `guide eject: ${plan.refusal}\n`, 1);
|
|
668
|
+
const preview = [
|
|
669
|
+
`guide eject ${name} — creates ${plan.target} (based_on: ${plan.based_on}):`,
|
|
670
|
+
"",
|
|
671
|
+
plan.content.replace(/\n*$/, ""),
|
|
672
|
+
"",
|
|
673
|
+
].join("\n");
|
|
674
|
+
if (!values.write)
|
|
675
|
+
return emit(
|
|
676
|
+
true,
|
|
677
|
+
result(false),
|
|
678
|
+
`${preview}\nNothing written. To create it, ${fromTerminalIn(project)}:\n${packageCommand([], "guide", { args: `eject ${name} --write` })}\n`,
|
|
679
|
+
0,
|
|
680
|
+
);
|
|
681
|
+
const interactive =
|
|
682
|
+
!values.json &&
|
|
683
|
+
(ask ? true : (await import("./install-harness.mjs")).isInteractive({ stdin, stdout, env, json: false }));
|
|
684
|
+
if (interactive) {
|
|
685
|
+
stdout.write(preview + "\n");
|
|
686
|
+
if (!(await term.askApply(1, { stdin, stdout, ask, caps: term.caps(stdout, env) }))) {
|
|
687
|
+
stdout.write("Nothing written.\n");
|
|
688
|
+
return 1;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const done = applyEject(plan);
|
|
692
|
+
if (!done.written) return emit(false, { ...result(false), error: done.refusal }, `guide eject: ${done.refusal}\n`, 1);
|
|
693
|
+
return emit(true, result(true), `Created ${plan.target}.\n`, 0);
|
|
694
|
+
}
|
|
695
|
+
|
|
643
696
|
export const HANDLERS = Object.freeze({
|
|
644
697
|
...READ_HANDLERS,
|
|
698
|
+
guide: runGuideVerb,
|
|
645
699
|
setup: runSetupVerb,
|
|
646
700
|
configure: runConfigureVerb,
|
|
647
701
|
reconcile: runReconcile,
|
|
@@ -104,7 +104,8 @@ const KEY = Object.freeze({
|
|
|
104
104
|
lifecycle_gates: {
|
|
105
105
|
text: () => "Whether doctor checks a story's plan and close sections.",
|
|
106
106
|
values: {
|
|
107
|
-
on: () =>
|
|
107
|
+
on: () =>
|
|
108
|
+
"doctor checks a story's plan when it starts, and its final summary and evidence when it closes. Stories closed after now are checked.",
|
|
108
109
|
off: () => "The plan and close sections are still written; doctor does not check them.",
|
|
109
110
|
},
|
|
110
111
|
},
|
|
@@ -53,6 +53,7 @@ const CLEAR = "-";
|
|
|
53
53
|
const LANGUAGE_NOTE =
|
|
54
54
|
"new artifacts, the derived views on their next reconcile, the plan and close sections of stories still in flight, the session header and the status line take the new language; existing artifacts and the vault's READMEs are untouched";
|
|
55
55
|
const STAMP_NOTE = "stories closed before now are exempt from the spec and lifecycle gates";
|
|
56
|
+
const GATES_NOTE = "stories closed after now are checked by the lifecycle gates";
|
|
56
57
|
|
|
57
58
|
const parseBool = (v) => {
|
|
58
59
|
const s = String(v).trim().toLowerCase();
|
|
@@ -466,7 +467,11 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
466
467
|
const patch = Object.fromEntries(policy.map((c) => [c.key, c.value]));
|
|
467
468
|
const next = nextVaultConfig(strict.cfg, patch, new Date().toISOString());
|
|
468
469
|
const inEffect = (k) => strict.cfg[k] ?? (k === "spec_policy" ? "optional" : undefined);
|
|
469
|
-
|
|
470
|
+
// `lifecycle_gates=on` on a vault already on but with no gates_since is a
|
|
471
|
+
// change: it stamps the date the gates count from (the configure spec,
|
|
472
|
+
// contract 7 as amended 2026-11-08).
|
|
473
|
+
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));
|
|
470
475
|
for (const c of changed)
|
|
471
476
|
items.push({
|
|
472
477
|
key: c.key,
|
|
@@ -474,7 +479,12 @@ export function planChanges(f, changes, { env = process.env, home = homedir(), r
|
|
|
474
479
|
before: shown(strict.cfg[c.key] ?? (c.key === "spec_policy" ? "optional" : "not set")),
|
|
475
480
|
after: shown(c.value),
|
|
476
481
|
team: true,
|
|
477
|
-
note:
|
|
482
|
+
note:
|
|
483
|
+
c.key === "spec_policy" && next.spec_policy_since !== strict.cfg.spec_policy_since
|
|
484
|
+
? STAMP_NOTE
|
|
485
|
+
: c.key === "lifecycle_gates" && next.gates_since !== strict.cfg.gates_since
|
|
486
|
+
? GATES_NOTE
|
|
487
|
+
: null,
|
|
478
488
|
});
|
|
479
489
|
if (changed.length) steps.push({ writer: "vault", patch, keys: changed.map((c) => c.key) });
|
|
480
490
|
}
|
|
@@ -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())
|