@descent-vtt/spec-brief 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +162 -29
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.js +61 -43
  5. package/dist/cli.js.map +1 -1
  6. package/dist/collisions.js +13 -8
  7. package/dist/collisions.js.map +1 -1
  8. package/dist/config.js +2 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/corpus.d.ts +5 -0
  11. package/dist/corpus.js +7 -0
  12. package/dist/corpus.js.map +1 -1
  13. package/dist/glob.d.ts +6 -5
  14. package/dist/glob.js +22 -103
  15. package/dist/glob.js.map +1 -1
  16. package/dist/lint.js +1 -0
  17. package/dist/lint.js.map +1 -1
  18. package/dist/report.d.ts +18 -4
  19. package/dist/report.js +98 -28
  20. package/dist/report.js.map +1 -1
  21. package/dist/rules.d.ts +2 -0
  22. package/dist/rules.js +21 -8
  23. package/dist/rules.js.map +1 -1
  24. package/dist/schedule.js +12 -10
  25. package/dist/schedule.js.map +1 -1
  26. package/dist/schema.d.ts +5 -1
  27. package/dist/schema.js +6 -2
  28. package/dist/schema.js.map +1 -1
  29. package/dist/scope.d.ts +26 -4
  30. package/dist/scope.js +64 -8
  31. package/dist/scope.js.map +1 -1
  32. package/dist/trigger.js +59 -3
  33. package/dist/trigger.js.map +1 -1
  34. package/dist/types.d.ts +9 -0
  35. package/dist/types.js.map +1 -1
  36. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
  37. package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
  38. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  39. package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
  40. package/dist/vendor/spec-core/pattern/glob.js +207 -17
  41. package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
  42. package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
  43. package/dist/vendor/spec-core/pattern/index.js +1 -1
  44. package/dist/vendor/spec-core/pattern/index.js.map +1 -1
  45. package/dist/vendor/spec-core/text/index.d.ts +1 -0
  46. package/dist/vendor/spec-core/text/index.js +1 -0
  47. package/dist/vendor/spec-core/text/index.js.map +1 -1
  48. package/dist/vendor/spec-core/text/width.d.ts +30 -0
  49. package/dist/vendor/spec-core/text/width.js +113 -0
  50. package/dist/vendor/spec-core/text/width.js.map +1 -0
  51. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,72 @@
1
1
  # Changelog
2
2
 
3
- Notable changes, newest first. Versions follow [semver](https://semver.org).
3
+ Notable changes, newest first. Before 1.0, a minor release (0.2 to 0.3)
4
+ carries anything that can turn a passing run red or change what a script
5
+ reads - a finding not reported before, a changed exit code, input refused
6
+ that was accepted, a flag, key or JSON field removed or renamed after a
7
+ release of warning - and new features; a patch release carries only fixes
8
+ that report less, crashes, speed and documentation, so `^0.3.0` takes no
9
+ release that can turn a run red. This is the family's policy, recorded in
10
+ [spec-core ADR-0009](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md);
11
+ releases up to 0.2.7 came before it, and several of their patches changed
12
+ what a run reports.
13
+
14
+ ## 0.3.0
15
+
16
+ Deferral triggers and placeholders written in Chinese are read, the GitLab
17
+ fingerprint no longer changes with a message's wording, `matrix` names a file
18
+ the tree holds, and Chinese columns line up. spec-core is at 5666c96.
19
+
20
+ ### Changed
21
+
22
+ - spec-core at 5666c96: `.//docs`, `./{/docs,src}` and `{.//docs,src}` read
23
+ as relative patterns, where they were refused as rooted (ADR-0005).
24
+ Upgrading: a scope refused before is read now; run `spec-brief matrix`.
25
+ - A front-matter key may be written in any script: `狀態: 已接受` is an
26
+ `unknown-field` warning where it was a `front-matter` error, and
27
+ `status.field` may name `狀態` or `状态`. Upgrading: write `status:`, or
28
+ set `status.field`.
29
+ - `archive`, `unarchive` and `new` write a status word such as `封存`
30
+ unquoted. Upgrading: a script matching the written line sees
31
+ `status: 封存`.
32
+ - A deferral trigger that names only a time in Chinese (`下個月`,
33
+ `2026年10月`, `第三季`, full-width `Q3`) is refused by `deferral-trigger`
34
+ (ADR-0003). Upgrading: name the event that brings the work back.
35
+ - The default `placeholders` gain `待定`, `未定`, `待補`, `待確認` and their
36
+ Simplified forms, and a full-width colon ends a placeholder as a colon
37
+ does. Upgrading: fill the section, or list the old `placeholders`.
38
+ - The GitLab fingerprint is built from the rule, the file, the brief and
39
+ the finding's subject, never its message. Upgrading: the first merge
40
+ request after upgrading shows each issue resolved and found again once.
41
+ - `matrix` and `schedule` name a file the tree holds where both patterns
42
+ match one outside the protections, or say the file named is an example
43
+ (ADR-0005).
44
+ - A pair the search could not decide is a `collision` (an error) when the
45
+ tree holds a file both patterns match and neither brief protects.
46
+ Upgrading: run the two briefs in different waves, or narrow a scope.
47
+
48
+ ### Added
49
+
50
+ - `inTree` on each pair of patterns in `matrix` and `schedule` JSON
51
+ (`schemaVersion` stays 2); `meet` takes the tree's files.
52
+ - A rule's result, a plugin's included, may carry a `subject`, which the
53
+ GitLab fingerprint is built from.
54
+
55
+ ### Fixed
56
+
57
+ - `list`, `schedule` and `matrix` line up columns holding Chinese.
58
+ - `unknown-field` no longer suggests a key that shares nothing with the one
59
+ written (`狀態` was asked about `id`).
60
+ - `list` shows the status word the brief writes, such as `封存`, not its
61
+ English equivalent.
62
+ - `list` and `schedule` show a title without the id it repeats.
63
+ - `--help` lists the formats each command writes.
64
+
65
+ ### Documentation
66
+
67
+ - The README is reorganised into lists and tables, with a "Scope patterns"
68
+ list of one rule per item; SECURITY.md says how to report a vulnerability
69
+ privately.
4
70
 
5
71
  ## 0.2.7
6
72
 
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  A *brief* is the input to one round of work - for a coding agent or a person: what done looks like, what the round must not touch, and the checks that prove it finished. spec-brief manages briefs as files in your repository. It has no runtime dependencies, reads and writes nothing but Markdown in two directories, and never writes to git.
6
6
 
7
+ spec-brief is one of the [spec-\* tools](https://github.com/DescentVTT/spec-core#the-family). The family's words - brief, round, wave, scope, the two kinds of plugin - are defined in [concepts](https://github.com/DescentVTT/spec-core/blob/main/docs/concepts.md), and the [tutorial](https://github.com/DescentVTT/spec-core/blob/main/docs/tutorial.md) takes one round from the brief to the archive in ten steps.
8
+
7
9
  ```bash
8
10
  npm install --save-dev @descent-vtt/spec-brief
9
11
  npx spec-brief init
@@ -48,11 +50,34 @@ Every privilege change issues a new session token and invalidates the old one.
48
50
  - [ ] a rotated token is rejected by every endpoint
49
51
  ```
50
52
 
51
- Work put off for later is a brief too, with `status: deferred` and a `trigger` naming the event that brings it back - "when the second tenant signs", "when p95 exceeds 200 ms", never a date. A deferred brief stays in the briefs directory, is never ready, and runs in no wave until someone sets it `active`.
53
+ Work put off for later is a brief too, with `status: deferred` and a `trigger` naming the event that brings it back - "when the second tenant signs", "when p95 exceeds 200 ms", `當第二個租戶簽約時`, never a date: not `Q3`, not `下個月` (next month), not `2026年10月`. A deferred brief stays in the briefs directory, is never ready, and runs in no wave until someone sets it `active`.
54
+
55
+ - The id comes from the file name (`012_rotate-session-tokens.md`) or from an `id` field.
56
+ - `affectedFiles` is the scope the round may write.
57
+ - `protectedFiles` is what it is not empowered to change, and archival refuses a round that changed it.
58
+ - Protection wins: what a round may write is `affectedFiles` less `protectedFiles`, so "all of `src/` but the schema" is `affectedFiles: [src/**]` with `protectedFiles: [src/db/schema.ts]`.
59
+
60
+ ### Scope patterns
61
+
62
+ Scopes are globs in spec-core's `path` dialect, the one every spec-\* tool reads: `*`, `?`, `[a-z]` and `{a,b}` within a name, `**` for any number of directories, compared case-sensitively on every host. The rules:
52
63
 
53
- The id comes from the file name (`012_rotate-session-tokens.md`) or from an `id` field. `affectedFiles` is the scope the round may write; `protectedFiles` is what it is not empowered to change, and archival refuses a round that changed it. Protection wins: what a round may write is `affectedFiles` less `protectedFiles`, so "all of `src/` but the schema" is `affectedFiles: [src/**]` with `protectedFiles: [src/db/schema.ts]`.
64
+ - `**` is a whole segment: `docs/**.md` is refused, since tools read it three ways - write `docs/**/*.md` for any depth, or `docs/*.md` for one level.
65
+ - Parentheses are literal, `C++(notes).md`, unless a group holds a `|`: an extended glob such as `+(a|b)` is refused, and written `{a,b}`.
66
+ - A trailing `/**` or `/` is a directory's contents - at least one name below it, never the directory itself - on a brace alternative as on the whole pattern: `{src/,lib}` is `src/` or `lib`.
67
+ - Each alternative names a path under the root, as a whole pattern does: `./` is refused, and so is `{./,src}`.
68
+ - A pattern is relative to the repository root, so a leading `/` is refused, on an alternative as on the whole pattern: `/docs` and `{/docs,src}` alike. A leading `./` takes the slashes after it along, as POSIX reads them: `.//docs` is `docs`, and `./{/docs,src}` is `docs` or `src`.
69
+ - After a name, a `/` inside braces names nothing: `docs/{/a,b}` is `docs/a` or `docs/b`.
70
+ - A path with no glob syntax is read from the tree: a file the tree holds is that file, a directory it holds is everything beneath it, and a path it does not hold yet is a file unless it ends in `/`.
71
+ - Write `src/newmod/` for a directory the round creates; `lint` notes a bare `src/newmod` it cannot place.
54
72
 
55
- Scopes are globs in spec-core's `path` dialect, the one every spec-\* tool reads: `*`, `?`, `[a-z]` and `{a,b}` within a name, `**` for any number of directories, compared case-sensitively on every host. `**` is a whole segment: `docs/**.md` is refused, since tools read it three ways - write `docs/**/*.md` for any depth, or `docs/*.md` for one level. Parentheses are literal, `C++(notes).md`, unless a group holds a `|`: an extended glob such as `+(a|b)` is refused, and written `{a,b}`. A trailing `/**` or `/` is a directory's contents - at least one name below it, never the directory itself - on a brace alternative as on the whole pattern: `{src/,lib}` is `src/` or `lib`. Each alternative names a path under the root, as a whole pattern does: `./` is refused, and so is `{./,src}`. A pattern is relative to the repository root, so a leading `/` is refused, on an alternative as on the whole pattern: `/docs`, `.//docs` and `{/docs,src}` alike. After a name, a `/` inside braces names nothing: `docs/{/a,b}` is `docs/a` or `docs/b`. A path with no glob syntax is read from the tree: a file the tree holds is that file, a directory it holds is everything beneath it, and a path it does not hold yet is a file unless it ends in `/`. Write `src/newmod/` for a directory the round creates; `lint` notes a bare `src/newmod` it cannot place.
73
+ ### How a brief is read
74
+
75
+ A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure.
76
+
77
+ - **A section**, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections.
78
+ - **Section names** compare without case, typographic quotes, emphasis, a leading number or a trailing colon, and a heading may add a qualifier after a separator: `## 2. Commander’s Intent:` fills `Commander's Intent`, and `## Invariants (must hold)` fills `Invariants`.
79
+ - **A task** is a list item outside a block quote with `[ ]`, `[x]` or `[X]`.
80
+ - **The links archival rewrites** are the ones that write a destination: `[text](dest)`, `![alt](dest)` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once. A definition is one where CommonMark reads it, opening a paragraph or under another: the shape of one under a paragraph's text, or in alt text, is text and stays as written.
56
81
 
57
82
  ## Commands
58
83
 
@@ -80,7 +105,16 @@ briefs/035_the-background-side.md
80
105
 
81
106
  ### `spec-brief list`
82
107
 
83
- Live briefs with their status, wave, task count and readiness. A brief is *ready* when every brief it depends on is archived, and it is neither a draft nor deferred. `--ready` shows only those, which is the question an orchestrator asks; `--archived` includes the archive; `--format json` gives an orchestrator the whole table, and the sections the configuration asks for, each with its `hint`.
108
+ Live briefs with their status, in the word the brief writes (`封存` where the configuration names it for archived), wave, task count, readiness and title.
109
+
110
+ - **The title** is shown without the id it repeats: `new` writes `# 012 — Rotate tokens`, and the table has an id column, so the title shown is `Rotate tokens`. Only the brief's own id comes off, and only before a dash, a colon or a spaced hyphen, so `Fix - the login bug` is shown whole. `schedule` shows titles the same way, and `--format json` gives the title as written.
111
+ - **A brief is *ready*** when every brief it depends on is archived, and it is neither a draft nor deferred.
112
+
113
+ | Option | Shows |
114
+ | --- | --- |
115
+ | `--ready` | Only the ready briefs, which is the question an orchestrator asks. |
116
+ | `--archived` | The archive as well. |
117
+ | `--format json` | The whole table, for an orchestrator, and the sections the configuration asks for, each with its `hint`. |
84
118
 
85
119
  ### `spec-brief matrix`
86
120
 
@@ -96,11 +130,21 @@ wave 1 · 3 briefs
96
130
  ? 003 declares no affectedFiles and cannot be checked
97
131
  ```
98
132
 
99
- Scopes are intersected as globs, not compared as strings: the two above share no prefix and still meet, and the file named is one both patterns match and neither brief protects - always a file, never a directory. Two briefs whose scopes meet in several places are one collision, listing every pair of patterns that meets. A brief with no scope is reported as unscoped rather than counted as safe. The search for a shared file has a budget; a pair it cannot decide within it is marked `?` and reported as `collision-undecided`, a warning, never as a collision and never as clean. Exit 1 on a collision.
133
+ Scopes are intersected as globs, not compared as strings: the two above share no prefix and still meet. Exit 1 on a collision.
134
+
135
+ - **The file named** is one both patterns match and neither brief protects - always a file, never a directory. It is a file the tree holds where there is one, the first in git's order; where the tree holds none, it is the shortest path the search built, said to be an example: `both cover src/newmod/a.ts, an example not in the tree`. `--format json` says which, as `inTree` on each pair of patterns: `true`, `false`, or `null` when the tree could not be read.
136
+ - **One collision per pair of briefs**: two briefs whose scopes meet in several places are one collision, listing every pair of patterns that meets.
137
+ - **Unscoped**: a brief with no scope is reported as unscoped rather than counted as safe.
138
+ - **Undecided**: the search for a shared file has a budget; a pair it cannot decide within it is marked `?` and reported as `collision-undecided`, a warning, never as a collision and never as clean - unless the tree holds a file both patterns match and neither brief protects, which proves the collision.
100
139
 
101
140
  ### `spec-brief schedule`
102
141
 
103
- Computes the waves the live briefs can run in, drafts included, and sets each beside the wave it declares. A brief runs after every dependency that is still live - an archived one is done - and shares a wave with no brief whose writable scope meets its own. Briefs are taken in dependency order, ties broken by id, and each goes into the lowest wave that is after its dependencies and holds nothing it collides with. A brief with no `affectedFiles` cannot be proved apart from anything, so it runs in a wave of its own and is reported as `unscoped`. The first wave is the lowest any brief declares, or 1.
142
+ Computes the waves the live briefs can run in, drafts included, and sets each beside the wave it declares:
143
+
144
+ - A brief runs after every dependency that is still live - an archived one is done - and shares a wave with no brief whose writable scope meets its own.
145
+ - Briefs are taken in dependency order, ties broken by id, and each goes into the lowest wave that is after its dependencies and holds nothing it collides with.
146
+ - A brief with no `affectedFiles` cannot be proved apart from anything, so it runs in a wave of its own and is reported as `unscoped`.
147
+ - The first wave is the lowest any brief declares, or 1.
104
148
 
105
149
  ```text
106
150
  wave 1 · 2 briefs
@@ -118,32 +162,96 @@ deferred: 006
118
162
  2 briefs would move; "spec-brief schedule --write" writes the waves
119
163
  ```
120
164
 
121
- Each move says why: the dependency that sets the earliest wave, and for every wave passed over, the brief there and the file both would write. It says first whether the declared wave holds - whether `lint` and `matrix` would pass it: no brief there that it collides with, its dependencies in earlier waves, and the briefs that depend on it in later ones. A deferred brief, and one that waits on it, is placed nowhere; a dependency cycle is an error, and nothing in or after it is placed. `--write` sets `wave` in the front matter of each brief whose wave changes - every other line as it was, in one transaction - and writes nothing over a cycle. A comment after the old wave, `wave: 2 # after the review`, stays after the new one. A brief whose lines end in both LF and CRLF is written with the ending of its first line throughout, as `archive` writes one. `--format json` gives an orchestrator the waves, each brief's declared and proposed wave with the reasons and whether the declared one holds, and what waits on what; `sarif`, `github` and `gitlab` carry a `wave-schedule` finding per move.
165
+ Each move says why:
166
+
167
+ - first, whether the declared wave holds - whether `lint` and `matrix` would pass it: no brief there that it collides with, its dependencies in earlier waves, and the briefs that depend on it in later ones;
168
+ - the dependency that sets the earliest wave;
169
+ - for every wave passed over, the brief there and the file both would write.
170
+
171
+ A deferred brief, and one that waits on it, is placed nowhere; a dependency cycle is an error, and nothing in or after it is placed.
172
+
173
+ `--write` sets `wave` in the front matter of each brief whose wave changes, in one transaction, and writes nothing over a cycle:
174
+
175
+ - every other line stays as it was;
176
+ - a comment after the old wave, `wave: 2 # after the review`, stays after the new one;
177
+ - a brief whose lines end in both LF and CRLF is written with the ending of its first line throughout, as `archive` writes one.
178
+
179
+ `--format json` gives an orchestrator the waves, each brief's declared and proposed wave with the reasons and whether the declared one holds, and what waits on what; `sarif`, `github` and `gitlab` carry a `wave-schedule` finding per move.
122
180
 
123
- The result is the same every time for the same briefs. It is a valid schedule, not always the shortest: no fast method promises the fewest waves, and a person can always move a brief later by hand, which `lint` and `matrix` then check. So a move is an error only when the declared wave does not hold, or there is none: a declared wave that holds is the person's to keep, and its move is a note. Exit 0 when every declared wave holds, 1 when a brief declares no wave or one that does not hold, or the dependencies form a cycle, 2 when the run cannot be trusted.
181
+ The result is the same every time for the same briefs. It is a valid schedule, not always the shortest: no fast method promises the fewest waves, and a person can always move a brief later by hand, which `lint` and `matrix` then check. So a move is an error only when the declared wave does not hold, or there is none: a declared wave that holds is the person's to keep, and its move is a note.
182
+
183
+ | Exit | When |
184
+ | --- | --- |
185
+ | `0` | Every declared wave holds. |
186
+ | `1` | A brief declares no wave or one that does not hold, or the dependencies form a cycle. |
187
+ | `2` | The run cannot be trusted. |
124
188
 
125
189
  ### `spec-brief archive <brief>`
126
190
 
127
191
  Closes a round. Refused, with every reason, when:
128
192
 
129
193
  - the brief has lint errors, is a draft, or depends on a brief that is still live;
130
- - a task item is neither ticked nor dispositioned - an open box counts as closed when a note under it starts with one of the `dispositions` (`**Delegated`, `**Accepted debt`, `**Rejected` by default), a note being a line under the box, above any box nested in it, that starts a bullet, a paragraph after a blank line, or a line after one that ends a sentence. A line that continues a sentence is the box's text wrapping, not a note, so `- [ ] Explain why the` over ` **Rejected** designs failed` is open, and `- [ ] Add a cache.` over ` **Rejected** by 012` is closed;
194
+ - a task item is neither ticked nor dispositioned:
195
+ - an open box counts as closed when a note under it starts with one of the `dispositions` (`**Delegated`, `**Accepted debt`, `**Rejected` by default);
196
+ - a note is a line under the box, above any box nested in it, that starts a bullet, a paragraph after a blank line, or a line after one that ends a sentence;
197
+ - a line that continues a sentence is the box's text wrapping, not a note, so `- [ ] Explain why the` over ` **Rejected** designs failed` is open, and `- [ ] Add a cache.` over ` **Rejected** by 012` is closed;
131
198
  - the working tree holds uncommitted work outside the brief directories (`--allow-dirty` to proceed);
132
- - the round's commit changed a file in `protectedFiles` - one refusal per file, naming it, unless a plugin that verifies rulings [waives it](#waiving-a-refusal).
199
+ - the round's commit changed a file in `protectedFiles` - one refusal per file, naming it, unless a spec-brief plugin that verifies rulings [waives it](#waiving-a-refusal).
200
+
201
+ Two warnings, which `--strict` turns into refusals:
202
+
203
+ - Changes outside `affectedFiles` are a warning, and an error under `--strict`.
204
+ - A scope is never passed by checks that measured nothing. When the files the round changed are unknown - no `--commit`, `--base` or `archiving.base`, `--no-git`, or a commit already in the base branch, as on `main` after the merge - a brief that declares one gets `scope-unmeasured`, a warning, and a refusal under `--strict`.
205
+
206
+ Otherwise, in one transaction, it:
133
207
 
134
- Changes outside `affectedFiles` are a warning, and an error under `--strict`. A scope is never passed by checks that measured nothing: when the files the round changed are unknown - no `--commit`, `--base` or `archiving.base`, `--no-git`, or a commit already in the base branch, as on `main` after the merge - a brief that declares one gets `scope-unmeasured`, a warning, and a refusal under `--strict`. Otherwise it writes the archived brief with its status set, a frozen banner under the front matter, every relative link rewritten for the archive directory, and an `integrity` hash; rewrites the links other live briefs hold to it; and removes the original. With `archiving.rewriteLinks` off, no link is rewritten, and the links other live briefs are left holding are reported as `stale-link`. `--commit <rev>` records the commit and the files it changed; `--base <rev>` measures from the merge base instead, for a branch of several commits; `--pr <n>` links the pull request; `--summary <text>` is what the round did, in the banner. `--dry-run` prints the plan and the banner and writes nothing. Archiving an archived brief does nothing and exits 0.
208
+ - writes the archived brief with its status set, a frozen banner under the front matter, every relative link rewritten for the archive directory, and an `integrity` hash;
209
+ - rewrites the links other live briefs hold to it;
210
+ - removes the original.
211
+
212
+ With `archiving.rewriteLinks` off, no link is rewritten, and the links other live briefs are left holding are reported as `stale-link`. Archiving an archived brief does nothing and exits 0.
213
+
214
+ | Option | What it does |
215
+ | --- | --- |
216
+ | `--commit <rev>` | Records the commit and the files it changed. |
217
+ | `--base <rev>` | Measures from the merge base instead, for a branch of several commits. |
218
+ | `--pr <n>` | Links the pull request. |
219
+ | `--summary <text>` | What the round did, in the banner. |
220
+ | `--dry-run` | Prints the plan and the banner, and writes nothing. |
135
221
 
136
222
  spec-brief reads git and never writes it. Review the change and commit it with the round.
137
223
 
138
224
  ### `spec-brief unarchive <brief>`
139
225
 
140
- Reopens an archived brief: the banner and the hash come off, the status goes back to the live word, and the links are rewritten again. A brief archived and reopened is the brief it was, blank lines and final newline included, with four exceptions: the status line is written in its plain spelling; a relative link that had to be rewritten comes back in its shortest form (`./b.md` returns as `b.md`); an empty front matter block, `---` over `---`, comes back as none, since archival gives a brief with no front matter a block for its hash and reopening takes away a block it leaves empty; and a brief whose lines end in both LF and CRLF comes back with the ending of its first line throughout.
226
+ Reopens an archived brief: the banner and the hash come off, the status goes back to the live word, and the links are rewritten again. A brief archived and reopened is the brief it was, blank lines and final newline included, with four exceptions:
227
+
228
+ 1. The status line is written in its plain spelling.
229
+ 2. A relative link that had to be rewritten comes back in its shortest form (`./b.md` returns as `b.md`).
230
+ 3. An empty front matter block, `---` over `---`, comes back as none, since archival gives a brief with no front matter a block for its hash and reopening takes away a block it leaves empty.
231
+ 4. A brief whose lines end in both LF and CRLF comes back with the ending of its first line throughout.
141
232
 
142
233
  ### Every command
143
234
 
144
- `--root <dir>` runs from another directory. `--config <file>` names the configuration; `--no-config` uses the defaults. `--format` is `pretty`, `json`, `sarif`, `github` or `gitlab` where the command reports findings. `--strict` makes warnings fail the run. `--no-git` leaves git out even inside a repository: the tree is read from disk, and `archive` records no commit. `--color` and `--no-color` override `NO_COLOR`, `FORCE_COLOR` and the terminal check. `--help` and `--version` do what they say.
235
+ | Flag | What it does |
236
+ | --- | --- |
237
+ | `--root <dir>` | Runs from another directory. |
238
+ | `--config <file>` | Names the configuration. |
239
+ | `--no-config` | Uses the defaults. |
240
+ | `--format <format>` | `pretty` by default, or `json`; `lint`, `matrix` and `schedule`, which report findings, also write `sarif`, `github` and `gitlab`, and `--help` lists each command's formats. |
241
+ | `--strict` | Makes warnings fail the run. |
242
+ | `--no-git` | Leaves git out even inside a repository: the tree is read from disk, and `archive` records no commit. |
243
+ | `--color`, `--no-color` | Override `NO_COLOR`, `FORCE_COLOR` and the terminal check. |
244
+ | `--help`, `--version` | Do what they say. |
245
+
246
+ **Exit codes:**
145
247
 
146
- **Exit codes:** `0` clean, `1` findings, a collision, or a refused action, `2` the run could not be trusted - a bad flag, a configuration that does not load, a brief that does not exist. A run over a briefs directory that does not exist exits 2, because a check over nothing looks exactly like a clean one.
248
+ | Code | Means |
249
+ | --- | --- |
250
+ | `0` | Clean. |
251
+ | `1` | Findings, a collision, or a refused action. |
252
+ | `2` | The run could not be trusted - a bad flag, a configuration that does not load, a brief that does not exist. |
253
+
254
+ A run over a briefs directory that does not exist exits 2, because a check over nothing looks exactly like a clean one.
147
255
 
148
256
  ## Configuration
149
257
 
@@ -157,11 +265,11 @@ Reopens an archived brief: the banner and the hash come off, the status goes bac
157
265
  | `exclude` | `[]` | File-name globs that are never briefs, such as an index. |
158
266
  | `template` | `null` | A template file for `new`, with `{id}`, `{title}`, `{date}`, `{type}`, `{wave}`, `{status}`. |
159
267
  | `id` | `{ "source": "filename", "separator": "_", "digits": 3 }` | Where an id comes from, and how a new one is written. |
160
- | `status` | `{ "field": "status", "draft": "draft", "active": "active", "deferred": "deferred", "archived": "archived" }` | The words a repository uses. `field: null` reads status from location alone; `draft` and `deferred` may be `null` where a repository has no such word. |
161
- | `sections` | Intent, Negative Scope, Not Empowered (optional), Invariants (checklist) | Sections every live brief carries: a name, or `{ name, aliases, mustContain, checklist, optional, hint }`. `hint` says what the section must answer: a new brief carries it as a comment under the heading, a finding that the section is missing or unwritten gives it as the next step, and the JSON of `list` and `lint` reports it, so a tool helping to write a brief can ask for each section by what it is for. |
268
+ | `status` | `{ "field": "status", "draft": "draft", "active": "active", "deferred": "deferred", "archived": "archived" }` | The words a repository uses. `field` may be a key of any script, `狀態` as well as `status`, followed by an ASCII colon in the brief; `field: null` reads status from location alone; `draft` and `deferred` may be `null` where a repository has no such word. |
269
+ | `sections` | Intent, Negative Scope, Not Empowered (optional), Invariants (checklist) | Sections every live brief carries: a name, or `{ name, aliases, mustContain, checklist, optional, hint }`. `hint` is below. |
162
270
  | `sectionOrder` | `false` | Sections must appear in the listed order. |
163
271
  | `types` | `feature`, `defect`, `refactor`, `chore` | Brief types and the sections each adds. |
164
- | `placeholders` | `TBD`, `TODO`, `FIXME`, ... | Words that mark a section as unwritten. |
272
+ | `placeholders` | `TBD`, `TODO`, `FIXME`, ..., `待定`, `未定`, `待補`, `待確認` and their Simplified forms | Words that mark a section as unwritten: alone, or followed by a space, a colon (`:` too), a period or a dash. |
165
273
  | `fields` | `[]` | Front-matter keys the repository uses beyond the built-in ones. |
166
274
  | `archiving.tasks` | `"all"` | `"all"`, or the sections whose boxes must be closed. |
167
275
  | `archiving.dispositions` | `**Delegated`, `**Accepted debt`, `**Rejected` | What a note under an open box starts with to close it. |
@@ -172,6 +280,12 @@ Reopens an archived brief: the banner and the hash come off, the status goes bac
172
280
  | `rules` | `{}` | Severity per rule: `off`, `note`, `warning` or `error`. |
173
281
  | `plugins` | `[]` | Modules that contribute rules: a path or package, or `{ module, options }`. |
174
282
 
283
+ A section's `hint` says what the section must answer, so a tool helping to write a brief can ask for each section by what it is for:
284
+
285
+ - a new brief carries it as a comment under the heading;
286
+ - a finding that the section is missing or unwritten gives it as the next step;
287
+ - the JSON of `list` and `lint` reports it.
288
+
175
289
  A repository whose briefs have eight ordered sections, `proposed` and `archived` for words, and a banner of its own:
176
290
 
177
291
  ```json
@@ -198,9 +312,7 @@ A repository whose briefs have eight ordered sections, `proposed` and `archived`
198
312
  }
199
313
  ```
200
314
 
201
- Section names compare without case, typographic quotes, emphasis, a leading number or a trailing colon, and a heading may add a qualifier after a separator: `## 2. Commander’s Intent:` fills `Commander's Intent`, and `## Invariants (must hold)` fills `Invariants`.
202
-
203
- A brief is read as CommonMark reads it wherever that decides what is code or a comment - fenced and indented code, `<pre>`, `<script>`, `<style>` and `<textarea>` blocks, code spans, HTML comments - and nothing inside those is structure. A section, and the title, is an ATX heading, `## Name`, outside a block quote: prose over a `---` line is prose and a rule, and a heading quoted from another document is not one of the brief's sections. A task is a list item outside a block quote with `[ ]`, `[x]` or `[X]`. The links archival rewrites are the ones that write a destination: `[text](dest)`, `![alt](dest)` - one inside a link's text too - and a definition, `[label]: dest`, through which a reference is rewritten once. A definition is one where CommonMark reads it, opening a paragraph or under another: the shape of one under a paragraph's text, or in alt text, is text and stays as written.
315
+ How a heading fills a section, and what counts as a heading, a task or a link, is under [How a brief is read](#how-a-brief-is-read).
204
316
 
205
317
  ## Rules
206
318
 
@@ -218,21 +330,21 @@ A brief is read as CommonMark reads it wherever that decides what is code or a c
218
330
  | `missing-section` | error | A required section that is absent. |
219
331
  | `duplicate-section` | warning | A section that appears twice. |
220
332
  | `empty-section` | error | A section with no content; a comment is not content. A warning in a draft. |
221
- | `placeholder` | error | A section holding only `TBD`, `TODO` or an empty box. A warning in a draft. |
333
+ | `placeholder` | error | A section holding only `TBD`, `TODO`, `待定` or an empty box. A warning in a draft. |
222
334
  | `missing-checklist` | error | A checklist section with no task item. A warning in a draft. |
223
335
  | `must-contain` | error | A section without the text the configuration requires of it. |
224
336
  | `section-order` | error | Sections out of the configured order, when one is configured. |
225
337
  | `dependency` | error | A dependency on itself, or on a brief that does not exist. |
226
338
  | `dependency-cycle` | error | Live briefs that depend on each other in a cycle, reported once, as a path. |
227
339
  | `wave-order` | error | A live dependency that does not run in an earlier wave. |
228
- | `deferral-trigger` | error | A deferred brief with no `trigger`, or one that is only a date or a time - `2026-10`, `Q3`, `next month`, `October` - or a placeholder, rather than an event such as "when the second tenant signs". |
340
+ | `deferral-trigger` | error | A deferred brief with no `trigger`, or one that is only a date or a time - `2026-10`, `Q3`, `next month`, `October`, `下個月`, `2026年10月`, `第三季` - or a placeholder, rather than an event such as "when the second tenant signs". Full-width `Q3` reads as `Q3`. |
229
341
  | `glob` | error | A scope pattern spec-brief cannot read. |
230
342
  | `scope-contradiction` | error | Patterns in `affectedFiles` that `protectedFiles` cover entirely, so nothing of them is writable - one finding per brief, naming each. A pattern the search cannot decide is a warning. |
231
343
  | `glob-matches-nothing` | note | A scope pattern that matches no file git sees, tracked or untracked - expected when the round creates it. |
232
344
  | `literal-read-as-file` | note | A path with no glob syntax that the tree does not hold and whose name has no extension, such as `src/newmod`: read as a file, and `src/newmod/` if a directory was meant. Inside braces the advice is the pattern written with the `/` added, `lib/{util,new/}`, or the alternative in an entry of its own, `lib/new/`. |
233
345
  | `archive-freeze` | error | An archived brief that changed after it was archived. |
234
346
  | `collision` | error | Two briefs in one wave whose scopes can name the same file (`matrix`). |
235
- | `collision-undecided` | warning | Two briefs in one wave whose collision the search could not decide within its budget (`matrix`), or a wave `schedule` passed over for it. |
347
+ | `collision-undecided` | warning | Two briefs in one wave whose collision the search could not decide within its budget and no file of the tree proves (`matrix`), or a wave `schedule` passed over for it. |
236
348
  | `unscoped` | note | A brief sharing a wave that declares no scope (`matrix`), or one `schedule` runs alone. |
237
349
  | `shared-directory` | off | Two briefs in one wave writing into the same directory (`matrix`). |
238
350
  | `wave-schedule` | error | A brief with no wave, or a declared wave that does not hold - a collision in it, a dependency out of order - with the wave `schedule` computes and why (`schedule`). A declared wave that holds and is not the computed one is a note. |
@@ -250,7 +362,12 @@ A brief is read as CommonMark reads it wherever that decides what is code or a c
250
362
  - run: npx spec-brief schedule --format github
251
363
  ```
252
364
 
253
- `github` writes workflow commands, which annotate the pull request with no upload and no permission. `sarif` writes SARIF 2.1.0 for code-scanning upload. `gitlab` writes a GitLab Code Quality report - an array of `{ description, check_name, fingerprint, severity, location: { path, lines: { begin } } }` - for a merge request to show:
365
+ | Format | What it writes |
366
+ | --- | --- |
367
+ | `github` | Workflow commands, which annotate the pull request with no upload and no permission. |
368
+ | `sarif` | SARIF 2.1.0, for code-scanning upload. |
369
+ | `gitlab` | A GitLab Code Quality report - an array of `{ description, check_name, fingerprint, severity, location: { path, lines: { begin } } }` - for a merge request to show. |
370
+ | `json` | A versioned document for anything else: `schemaVersion` changes when a field changes meaning, and is 2 since a collision became a pair of briefs rather than a pair of patterns. |
254
371
 
255
372
  ```yaml
256
373
  spec-brief:
@@ -260,7 +377,11 @@ spec-brief:
260
377
  codequality: gl-code-quality.json
261
378
  ```
262
379
 
263
- An error is `major`, a warning `minor` and a note `info`; GitLab's `critical` and `blocker` are for security holes and crashes, which no finding here is. The fingerprint is a SHA-256 of the rule, the file and the message, and not the line, so a finding that moves is the same issue. `json` is a versioned document for anything else: `schemaVersion` changes when a field changes meaning, and is 2 since a collision became a pair of briefs rather than a pair of patterns.
380
+ In the GitLab report:
381
+
382
+ - An error is `major`, a warning `minor` and a note `info`; GitLab's `critical` and `blocker` are for security holes and crashes, which no finding here is.
383
+ - The `description` is the message and the next action.
384
+ - The fingerprint is a SHA-256 of what the finding is: its rule, its file, its brief, and what it is about there - a section, a pattern, a dependency, the other brief of a pair - never the message, the hint or the line. So a finding that moves, or is worded another way, or a collision that names another file both briefs write, is the same issue; two findings alike in all four, such as two problems in one front matter, are told apart by their order.
264
385
 
265
386
  ## As a library
266
387
 
@@ -282,7 +403,7 @@ Everything below the engine is a pure function of text: `parseBrief`, `lint`, `c
282
403
 
283
404
  ## Plugins
284
405
 
285
- A plugin is a module that exports `{ name, rules }`, or a function of its configured options that returns one. Its rules run beside the built-in ones as `<name>/<rule>`, and configuration sets their severity like any other.
406
+ A plugin here is a spec-brief plugin, a module this tool loads, not a Claude Code plugin. It exports `{ name, rules }`, or a function of its configured options that returns one. Its rules run beside the built-in ones as `<name>/<rule>`, and configuration sets their severity like any other.
286
407
 
287
408
  ```js
288
409
  // tools/departures.mjs
@@ -301,13 +422,21 @@ export default (options) => ({
301
422
  { "plugins": [{ "module": "./tools/departures.mjs", "options": { "marker": "Signed-off-by" } }] }
302
423
  ```
303
424
 
425
+ A result is `{ line, message }`, with optional fields:
426
+
427
+ | Field | What it is |
428
+ | --- | --- |
429
+ | `hint` | The next action. |
430
+ | `severity` | Lower than the rule's, for this one finding. |
431
+ | `subject` | What the finding is about, where the rule can report several in one brief; GitLab's fingerprint is built from it. |
432
+
304
433
  This is where integrations belong. A tool that defines a format - signed departures, recorded reproducers, a code graph's blast radius - is the one that can check it, and spec-brief carries no copy of formats it does not own. Loading a plugin runs its code, exactly as loading a linter configuration does.
305
434
 
306
435
  A path is relative to the root. A package is found from the root as an `import` there would find it, its `exports` read under the import conditions, so a plugin published as ESM only loads, and so does a subpath such as `@descent-vtt/spec-harness/spec-brief-plugin`.
307
436
 
308
437
  ### Waiving a refusal
309
438
 
310
- A plugin may also export `waive`, which lets the archive accept a change its own check allows - spec-harness's plugin verifies a signed ruling for a protected path, which spec-brief does not know how to read.
439
+ A plugin may also export `waive`, which lets the archive accept a change its own check allows - spec-harness's spec-brief plugin verifies a signed ruling for a protected path, which spec-brief does not know how to read.
311
440
 
312
441
  ```js
313
442
  export default () => ({
@@ -321,7 +450,11 @@ export default () => ({
321
450
  });
322
451
  ```
323
452
 
324
- `findings` are the archive's refusals, as a frozen copy: a hook reads them and cannot change them, and a write to them throws. A `protected-file` refusal is one per file and names its `path`; an `out-of-scope` refusal, which refuses only under `--strict`, lists its `paths`. Those two are the only ones a plugin can waive: a waiver matches one of the plan's own refusals by rule and path and turns it into a `waived` note naming the plugin, the rule, the path and the reason; a waiver for any other rule is ignored with a `waiver-ignored` warning. The hook is asked only when the plan has a refusal it could lift. A hook that returns nothing waives nothing. One that throws - a write to `findings` included - or answers in another shape stops the run with exit 2, as a plugin that fails to load does.
453
+ - `findings` are the archive's refusals, as a frozen copy: a hook reads them and cannot change them, and a write to them throws.
454
+ - Two refusals can be waived, and no others: `protected-file`, one per file, which names its `path`; and `out-of-scope`, which refuses only under `--strict` and lists its `paths`.
455
+ - A waiver matches one of the plan's own refusals by rule and path and turns it into a `waived` note naming the plugin, the rule, the path and the reason. A waiver for any other rule is ignored with a `waiver-ignored` warning.
456
+ - The hook is asked only when the plan has a refusal it could lift.
457
+ - A hook that returns nothing waives nothing. One that throws - a write to `findings` included - or answers in another shape stops the run with exit 2, as a plugin that fails to load does.
325
458
 
326
459
  ## What it does not do
327
460
 
@@ -332,7 +465,7 @@ export default () => ({
332
465
 
333
466
  ## Design
334
467
 
335
- The decisions and what they cost are in [`docs/adr/`](https://github.com/DescentVTT/spec-brief/blob/main/docs/adr/README.md).
468
+ The decisions and what they cost are in [`docs/adr/`](https://github.com/DescentVTT/spec-brief/blob/main/docs/adr/README.md). Working on spec-brief itself: [CONTRIBUTING.md](https://github.com/DescentVTT/spec-brief/blob/main/CONTRIBUTING.md); reporting a vulnerability: [SECURITY.md](https://github.com/DescentVTT/spec-brief/blob/main/SECURITY.md).
336
469
 
337
470
  ## License
338
471
 
package/dist/cli.d.ts CHANGED
@@ -23,8 +23,8 @@ export interface CliIO {
23
23
  readonly cwd?: string;
24
24
  readonly env?: Readonly<Record<string, string | undefined>>;
25
25
  }
26
- export declare const HELP = "spec-brief - lint, schedule and archive task briefs\n\nUsage:\n spec-brief <command> [options]\n\nCommands:\n init write .spec-brief.json and the brief directories\n new <title> scaffold a brief with the next free id\n lint [brief...] check briefs against the configuration\n list briefs with their status, wave and readiness\n matrix scope collisions between briefs in the same wave\n schedule the waves the live briefs can run in, computed\n archive <brief> close a round: check it, stamp it, freeze it, move it\n unarchive <brief> reopen an archived brief\n\nOptions for every command:\n --root <dir> run from this directory instead of the current one\n --config <file> use this configuration file\n --no-config use the defaults, ignoring any configuration file\n --format <format> pretty (default), json, sarif, github or gitlab\n --strict treat warnings as errors\n --no-git leave git out; read the files on disk instead\n --color, --no-color force colour on or off\n -h, --help show this help\n -v, --version show the version\n\ninit:\n --briefs <dir> the directory of live briefs; default briefs\n --archive <dir> the directory of archived briefs; default <briefs>/archive\n\nnew:\n --id <id> use this id instead of the next free one\n --type <type> a brief type from the configuration\n --wave <n> the wave it runs in\n --depends-on <ids> comma-separated ids it runs after\n --date <YYYY-MM-DD> the date in its front matter; default today\n\nlist:\n --ready only briefs that can run now: dependencies archived, not drafts or deferred\n --archived include archived briefs\n\nmatrix:\n --all-waves compare every live brief, whatever its wave\n\nschedule:\n --write write each proposed wave into its brief's front matter\n\narchive:\n --pr <number> the pull request the round merged in\n --commit <rev> the commit the round landed as\n --base <rev> the branch it started from; the diff runs from the merge base\n --summary <text> what the round did, for the banner\n --date <YYYY-MM-DD> the archival date; default today\n --allow-dirty archive although the tree has uncommitted work\n --dry-run print the plan and change nothing\n\nunarchive:\n --dry-run print the plan and change nothing\n\nExit codes: 0 clean, 1 findings or refused, 2 the run could not be trusted.\n";
27
26
  type Values = Record<string, string | boolean | undefined>;
27
+ export declare const HELP: string;
28
28
  interface Parsed {
29
29
  readonly command: string | undefined;
30
30
  readonly values: Values;
package/dist/cli.js CHANGED
@@ -24,6 +24,63 @@ export class UsageError extends Error {
24
24
  this.name = 'UsageError';
25
25
  }
26
26
  }
27
+ const GLOBAL = {
28
+ root: { type: 'string' },
29
+ config: { type: 'string' },
30
+ 'no-config': { type: 'boolean' },
31
+ format: { type: 'string' },
32
+ strict: { type: 'boolean' },
33
+ 'no-git': { type: 'boolean' },
34
+ color: { type: 'boolean' },
35
+ 'no-color': { type: 'boolean' },
36
+ help: { type: 'boolean', short: 'h' },
37
+ version: { type: 'boolean', short: 'v' },
38
+ };
39
+ const COMMANDS = {
40
+ init: { options: { briefs: { type: 'string' }, archive: { type: 'string' } }, formats: ['pretty', 'json'] },
41
+ new: {
42
+ options: {
43
+ id: { type: 'string' },
44
+ type: { type: 'string' },
45
+ wave: { type: 'string' },
46
+ 'depends-on': { type: 'string' },
47
+ date: { type: 'string' },
48
+ },
49
+ formats: ['pretty', 'json'],
50
+ },
51
+ lint: { options: {}, formats: FORMATS },
52
+ list: { options: { ready: { type: 'boolean' }, archived: { type: 'boolean' } }, formats: ['pretty', 'json'] },
53
+ matrix: { options: { 'all-waves': { type: 'boolean' } }, formats: FORMATS },
54
+ schedule: { options: { write: { type: 'boolean' } }, formats: FORMATS },
55
+ archive: {
56
+ options: {
57
+ pr: { type: 'string' },
58
+ commit: { type: 'string' },
59
+ base: { type: 'string' },
60
+ summary: { type: 'string' },
61
+ date: { type: 'string' },
62
+ 'allow-dirty': { type: 'boolean' },
63
+ 'dry-run': { type: 'boolean' },
64
+ },
65
+ formats: ['pretty', 'json'],
66
+ },
67
+ unarchive: { options: { 'dry-run': { type: 'boolean' } }, formats: ['pretty', 'json'] },
68
+ };
69
+ /**
70
+ * The formats each command writes, read from the table `main` checks a
71
+ * `--format` against, so the help names no format a command refuses: the
72
+ * finding formats are `lint`'s, `matrix`'s and `schedule`'s alone.
73
+ */
74
+ function formatsHelp() {
75
+ const groups = new Map();
76
+ for (const [command, spec] of Object.entries(COMMANDS)) {
77
+ const formats = spec.formats.join(', ');
78
+ groups.set(formats, [...(groups.get(formats) ?? []), command]);
79
+ }
80
+ const rows = [...groups].map(([formats, commands]) => [commands.join(', '), formats]);
81
+ const width = Math.max(...rows.map(([commands]) => commands.length));
82
+ return rows.map(([commands, formats]) => ` ${commands.padEnd(width)} ${formats}`).join('\n');
83
+ }
27
84
  export const HELP = `spec-brief - lint, schedule and archive task briefs
28
85
 
29
86
  Usage:
@@ -43,13 +100,16 @@ Options for every command:
43
100
  --root <dir> run from this directory instead of the current one
44
101
  --config <file> use this configuration file
45
102
  --no-config use the defaults, ignoring any configuration file
46
- --format <format> pretty (default), json, sarif, github or gitlab
103
+ --format <format> the output format, pretty by default; each command's are below
47
104
  --strict treat warnings as errors
48
105
  --no-git leave git out; read the files on disk instead
49
106
  --color, --no-color force colour on or off
50
107
  -h, --help show this help
51
108
  -v, --version show the version
52
109
 
110
+ Formats:
111
+ ${formatsHelp()}
112
+
53
113
  init:
54
114
  --briefs <dir> the directory of live briefs; default briefs
55
115
  --archive <dir> the directory of archived briefs; default <briefs>/archive
@@ -85,48 +145,6 @@ unarchive:
85
145
 
86
146
  Exit codes: 0 clean, 1 findings or refused, 2 the run could not be trusted.
87
147
  `;
88
- const GLOBAL = {
89
- root: { type: 'string' },
90
- config: { type: 'string' },
91
- 'no-config': { type: 'boolean' },
92
- format: { type: 'string' },
93
- strict: { type: 'boolean' },
94
- 'no-git': { type: 'boolean' },
95
- color: { type: 'boolean' },
96
- 'no-color': { type: 'boolean' },
97
- help: { type: 'boolean', short: 'h' },
98
- version: { type: 'boolean', short: 'v' },
99
- };
100
- const COMMANDS = {
101
- init: { options: { briefs: { type: 'string' }, archive: { type: 'string' } }, formats: ['pretty', 'json'] },
102
- new: {
103
- options: {
104
- id: { type: 'string' },
105
- type: { type: 'string' },
106
- wave: { type: 'string' },
107
- 'depends-on': { type: 'string' },
108
- date: { type: 'string' },
109
- },
110
- formats: ['pretty', 'json'],
111
- },
112
- lint: { options: {}, formats: FORMATS },
113
- list: { options: { ready: { type: 'boolean' }, archived: { type: 'boolean' } }, formats: ['pretty', 'json'] },
114
- matrix: { options: { 'all-waves': { type: 'boolean' } }, formats: FORMATS },
115
- schedule: { options: { write: { type: 'boolean' } }, formats: FORMATS },
116
- archive: {
117
- options: {
118
- pr: { type: 'string' },
119
- commit: { type: 'string' },
120
- base: { type: 'string' },
121
- summary: { type: 'string' },
122
- date: { type: 'string' },
123
- 'allow-dirty': { type: 'boolean' },
124
- 'dry-run': { type: 'boolean' },
125
- },
126
- formats: ['pretty', 'json'],
127
- },
128
- unarchive: { options: { 'dry-run': { type: 'boolean' } }, formats: ['pretty', 'json'] },
129
- };
130
148
  /** Global options that take a value, so the value is not mistaken for the command. */
131
149
  const VALUED = new Set(['--root', '--config', '--format']);
132
150
  function commandOf(argv) {