@erclx/canon 5.7.0 → 5.9.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.
- package/README.md +4 -2
- package/claude/.claude-plugin/plugin.json +2 -2
- package/claude/skills/auto-ship/SKILL.md +2 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +2 -1
- package/claude/skills/draft-and-pick/SKILL.md +9 -6
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-pr/references/evidence.md +1 -1
- package/claude/skills/git-ship/REQUIREMENT.md +3 -2
- package/claude/skills/git-ship/SKILL.md +25 -2
- package/claude/skills/review-address/SKILL.md +6 -4
- package/claude/skills/review-craft/REQUIREMENT.md +8 -2
- package/claude/skills/review-craft/SKILL.md +63 -7
- package/claude/skills/review-craft/references/adopted.md +49 -0
- package/docs/agents/commands.md +2 -0
- package/docs/agents/hooks.md +56 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/pr-evidence.md +33 -17
- package/docs/agents/records-push.md +1 -1
- package/docs/agents/records-validate.md +1 -1
- package/docs/agents/scripting.md +6 -0
- package/docs/agents/tasks-archive.md +4 -2
- package/docs/agents/tasks.md +1 -1
- package/docs/agents/worktrees.md +2 -2
- package/docs/target/scenarios.md +2 -2
- package/package.json +2 -2
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli.ts +2 -0
- package/src/commands/claude.ts +1 -1
- package/src/commands/docs.ts +51 -11
- package/src/commands/hooks.ts +67 -0
- package/src/commands/pr.ts +146 -76
- package/src/commands/standards.ts +33 -2
- package/src/commands/tasks.ts +45 -0
- package/src/commands/tooling.ts +3 -3
- package/src/commands/upgrade.ts +3 -2
- package/src/commands/worktrees.ts +4 -3
- package/src/docs/list.ts +103 -0
- package/src/docs/read.ts +63 -19
- package/src/gov/stacks.ts +1 -1
- package/src/help.ts +2 -0
- package/src/hooks/post-merge.ts +331 -0
- package/src/pr/evidence.ts +21 -0
- package/src/records/validate.ts +1 -38
- package/src/standards/list.ts +33 -0
- package/src/standards/read.ts +13 -0
- package/src/standards/scope.ts +56 -0
- package/src/sync/check.ts +38 -4
- package/src/tasks/archive.ts +168 -3
- package/src/tooling/inject.ts +4 -3
- package/src/tooling/manifest.ts +62 -8
- package/src/tooling/scan.ts +4 -3
- package/src/tooling/stamp.ts +11 -2
- package/src/tooling/subfolder.ts +7 -3
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/scripts/verify.json +3 -0
- package/tooling/base/configs/scripts/verify.sh +1 -110
- package/tooling/base/configs/scripts/verify.ts +378 -0
- package/tooling/base/manifest.toml +1 -0
- package/tooling/base/reference.md +5 -3
- package/tooling/go/configs/scripts/verify.json +10 -0
- package/tooling/go/reference.md +5 -4
- package/tooling/php/configs/scripts/verify.json +7 -0
- package/tooling/php/reference.md +5 -4
- package/tooling/python/configs/scripts/verify.json +4 -0
- package/tooling/python/reference.md +3 -3
- package/tooling/web/configs/scripts/verify.json +3 -0
- package/tooling/web/reference.md +3 -3
- package/scripts/docs/list.sh +0 -184
- package/scripts/lib/frontmatter.sh +0 -30
- package/scripts/standards/list.sh +0 -115
- package/src/commands/pass-through.ts +0 -39
- package/tooling/go/configs/scripts/verify.sh +0 -142
- package/tooling/php/configs/scripts/verify.sh +0 -147
- package/tooling/python/configs/scripts/verify.sh +0 -141
- package/tooling/web/configs/scripts/verify.sh +0 -153
|
@@ -6,10 +6,10 @@ description: What canon pr evidence compares, the marker that lets it edit its o
|
|
|
6
6
|
# The pull request evidence comparison
|
|
7
7
|
|
|
8
8
|
`canon pr evidence` renders one comment naming every changed image under an
|
|
9
|
-
`evidence/` path segment, comparing each against the pull request's merge base
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
`evidence/` path segment, comparing each against the pull request's merge base.
|
|
10
|
+
`git-pr` posts it when a pull request opens or is edited, and `git-followup`
|
|
11
|
+
posts it again after every later push, so a reviewer never has to open Files
|
|
12
|
+
Changed to see what a case looked like before and after.
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
canon pr evidence
|
|
@@ -55,9 +55,9 @@ image or marks the case new.
|
|
|
55
55
|
| `gh-failed` | `gh` could not answer for this repository or branch, or read its comments. |
|
|
56
56
|
| `no-branch` | The pull request carries no head branch name. |
|
|
57
57
|
| `no-object-head` | The pull request object reported no head commit. |
|
|
58
|
-
| `no-base` |
|
|
59
|
-
| `unreadable-
|
|
60
|
-
| `
|
|
58
|
+
| `no-base` | GitHub reported no merge base for the pull request. |
|
|
59
|
+
| `unreadable-changes` | GitHub could not list what the pull request changed. |
|
|
60
|
+
| `would-empty` | The render holds no cases and the marked comment holds some. No body. |
|
|
61
61
|
|
|
62
62
|
`no-evidence` is not a refusal a caller reports. A project on a stack that
|
|
63
63
|
carries no evidence path, such as `base` or `python`, hits this reason on
|
|
@@ -83,16 +83,32 @@ reporting the fields absent.
|
|
|
83
83
|
|
|
84
84
|
## What `[number]` selects, and what it does not
|
|
85
85
|
|
|
86
|
-
Naming a number picks
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
head
|
|
86
|
+
Naming a number picks the pull request the whole record describes: its head
|
|
87
|
+
commit, its merge base, its changed set, and the marked comment to edit in
|
|
88
|
+
place. The changed paths and whether each was added or modified come from the
|
|
89
|
+
pull request's files endpoint, paged to the end, and the merge base comes from
|
|
90
|
+
the compare endpoint, so the record is the same from any checkout. A run from
|
|
91
|
+
the main worktree against a branch built elsewhere renders that branch's
|
|
92
|
+
comparison.
|
|
93
|
+
|
|
94
|
+
The trade is that the verb no longer sees evidence that is uncommitted or
|
|
95
|
+
unpushed, which the comment's head-pinned image links could not show anyway.
|
|
96
|
+
A path the pull request renamed or copied counts as added, since the new path
|
|
97
|
+
has no counterpart at the merge base, so a rename loses its before image and
|
|
98
|
+
renders `*(new)*`. A path the pull request removed is dropped, since it has no
|
|
99
|
+
head image to show. A failed files read refuses as `unreadable-changes` rather
|
|
100
|
+
than rendering a short set, and a missing merge base refuses as `no-base`.
|
|
101
|
+
|
|
102
|
+
## A render never empties a comment that carries cases
|
|
103
|
+
|
|
104
|
+
When the render holds no cases and the marked comment holds some, the verb
|
|
105
|
+
refuses `would-empty` and prints no body. A render with no cases cannot tell a
|
|
106
|
+
case removed on purpose from a short read, so the destructive direction needs a
|
|
107
|
+
person: they edit the comment directly. The record keeps `commentId` and the
|
|
108
|
+
carried fields, so the caller can still find the comment. A comment holding
|
|
109
|
+
only a preview or a checklist has no cases to lose and still re-renders `ok`.
|
|
110
|
+
`no-evidence` without a flag returns before any render, so the guard touches
|
|
111
|
+
only `--preview`, `--local`, and `--checklist` calls.
|
|
96
112
|
|
|
97
113
|
## One comment, found by its own marker
|
|
98
114
|
|
|
@@ -58,4 +58,4 @@ The two `pull` refusals exist because the directions are not symmetric. A push o
|
|
|
58
58
|
|
|
59
59
|
## When it runs
|
|
60
60
|
|
|
61
|
-
`.husky/post-merge` runs `push` after the task archiving
|
|
61
|
+
`canon hooks post-merge`, which `.husky/post-merge` calls, runs `push` after the task archiving, on every merge rather than only on one that archived a task. A review report and a memory entry both land on runs that close nothing. The verb exits zero whatever the push answers, so an unreachable remote neither aborts the hook nor delays the archiving before it, and a checkout that never ran the setup reports nothing. `hooks.md` covers the verb. Anything the hook misses is covered by running the verb by hand.
|
|
@@ -46,7 +46,7 @@ A memory `category` is compared against the sentence-case form of the filename p
|
|
|
46
46
|
|
|
47
47
|
The standards filename check derives a word from the governed path rather than counting words in the name. Every member of the corpus is named for the artifact its scope statement governs, and one word is what that produces rather than the rule itself, so a check keyed on word count would pass a conforming single word naming the wrong artifact. Each path segment offers its own word, a dotted container segment offers none, a hyphenated segment offers its parts, and the singular and the plural both match.
|
|
48
48
|
|
|
49
|
-
The scope statement is read exactly as `
|
|
49
|
+
The scope statement is read exactly as `canon standards list` reads it for the catalog's `appliesTo` field: backticked spans in the first sentence alone, with an attribute standard resolving where that sentence backticks nothing and the statement says it governs an attribute. One sentence read two ways would let a standard pass the check while publishing a different jurisdiction to every consumer of the catalog. An attribute standard is exempt from the filename derivation because it governs no path to derive from, and a statement that anchors nothing and claims no attribute is reported as `scope-unanchored` rather than passed, since silence there would let the backticks be removed to disable the check.
|
|
50
50
|
|
|
51
51
|
An absent `## Scope` section suppresses the filename finding. The name derives from the statement, so a missing section leaves nothing to derive against, and reporting both would name one defect twice and point the fix at the wrong file. The walk skips `index.md`, which is generated from its siblings rather than authored.
|
|
52
52
|
|
package/docs/agents/scripting.md
CHANGED
|
@@ -35,6 +35,12 @@ declares nothing a parser can read. A consumer mapping a file to its governing
|
|
|
35
35
|
standards reads this rather than holding a table of its own, and reports an empty
|
|
36
36
|
array rather than skipping the standard behind it.
|
|
37
37
|
|
|
38
|
+
Each standard also carries `source`, the labeled path of the copy `canon standards
|
|
39
|
+
<name>` would read: `standards/<name>.md` for the working root and
|
|
40
|
+
`<canon>/standards/<name>.md` for the package corpus. A project's own standard
|
|
41
|
+
lists beside the package's, and one sharing a package name lists once, as the
|
|
42
|
+
project's copy.
|
|
43
|
+
|
|
38
44
|
`canon claude seeds list` reads the same plan `canon claude init` applies, so the
|
|
39
45
|
listing and the install cannot disagree. It now reports
|
|
40
46
|
`canon/context/index.md`, which `init` has always installed and the listing
|
|
@@ -30,11 +30,13 @@ The task carries its plan with it. When the closing task is the last live one wh
|
|
|
30
30
|
|
|
31
31
|
Every other relative link in the task is re-resolved for the deeper folder too, whether its target moved or not: `Groundwork:`, `Intake:`, a `Plan:` or `Ready:` line whose target stayed put, and a link in the body. Fenced blocks, inline code spans, absolute URLs, `#` anchors, and rooted paths are left alone. `canon tasks decline` applies the same rebase, since `declined/` sits at the same depth. The plan retarget runs before the rebase and writes each moved link relative to the live board, so the rebase carries it into the deeper folder like every other link, while the ready retarget runs after it.
|
|
32
32
|
|
|
33
|
+
Every other task file linking to the moved task is repointed at its new path too. The pass reads each markdown file in `.canon/tasks/`, `archive/`, and `declined/` except the moved file, `index.md`, `priority.md`, `backlog.md`, and the session handoffs, and resolves each relative link against the citing file's own folder. Only a link resolving to the old path is rewritten, with its `#` anchor kept, so an `archive/<stem>.md` link already correct, or the same stem in another folder, stays as written. Inline links and reference-style definitions (`[label]: path`) are both rewritten. Fenced blocks and inline code spans are skipped. `priority.md` and `backlog.md` are left alone because `canon tasks validate` reads a blocker there only while its target carries no `/`. Links broken before the move stay broken, and citations from plans, groundwork, and intake records are not read. The `relinked` array on the success record carries a root-relative `file` and a `links` count per file written, and is empty when nothing cited the task. The pass runs after the move, so a file it cannot read or write never stops the ordering-row removal or the index regen that follow. Each such file is left as it was and reported in `relinkFailed` as a root-relative `file` and a `message`, with a warning on stderr, and the verb still exits `0` because the move itself landed. A caller reading the record should treat a non-empty `relinkFailed` as links still pointing at the old path. `canon hooks post-merge`, which `.husky/post-merge` calls, reads the field and prints one `Links not repointed in:` line naming the files, since it discards the archive's stderr and nothing else on that unattended path would show the failure.
|
|
34
|
+
|
|
33
35
|
The ready folder the task's `Ready:` line names moves with the plan to `.canon/ready/archive/`, falling back to a `.canon/ready/<nn>-<slug>/` path in the plan when the line is absent. The `Ready:` line and the plan's literal path to the folder are retargeted, and the `ready` field on the success record carries `from` and `to`, or `null` when nothing moved. A folder that resolves to nothing, sits already archived, or meets a taken destination moves nothing and refuses nothing.
|
|
34
36
|
|
|
35
37
|
A plan several tasks share stays where it is, and its ready folder with it, and the task archives anyway. Moving it on the first task to close strands every sibling's pointer at a path that has gone, and `.canon/plans/` is gitignored so no history recovers the target. A `Plan:` line resolving to no file leaves the plan alone too, since a pointer somebody typed wrong is not a plan to move and holding the whole archive over it would park the board behind a repair the merge cannot make.
|
|
36
38
|
|
|
37
|
-
The merge is what settles a plan. `.husky/post-merge` reads the pull request number off the squash subject and calls this verb, so both halves close in one act with nobody naming a file. That is why the move sits inside this verb rather than in a second call the hook would make after it, which could leave the task archived and the plan live.
|
|
39
|
+
The merge is what settles a plan. `canon hooks post-merge`, which `.husky/post-merge` calls, reads the pull request number off the squash subject and calls this verb, so both halves close in one act with nobody naming a file. That is why the move sits inside this verb rather than in a second call the hook would make after it, which could leave the task archived and the plan live.
|
|
38
40
|
|
|
39
41
|
`bad-input` covers a malformed command line, which all three task verbs answer the same way. It is separate from `ambiguous` and `no-match` because those describe the board, and a caller that passed two selectors would otherwise be sent to repair a task citation that is fine.
|
|
40
42
|
|
|
@@ -72,7 +74,7 @@ Exit codes: `0` declined, `1` refused. The `reason` field carries which gate fir
|
|
|
72
74
|
|
|
73
75
|
The decision is written onto the task as a `Declined: <reason>, <who> on <YYYY-MM-DD>` line, anchored the same way `Pull request:` is, after the last origin line the task carries.
|
|
74
76
|
|
|
75
|
-
The task carries its plans with it the same way archive does, each one the declining task is the last live citation of, and the success record carries the same `plans` array. A declined task's plan lands in `.canon/plans/archive/`, indistinguishable from a shipped one by folder alone. The task file under `.canon/tasks/declined/` is what records which it was.
|
|
77
|
+
The task carries its plans with it the same way archive does, each one the declining task is the last live citation of, and the success record carries the same `plans` array. Inbound links to the declined task are repointed at `declined/<stem>.md` and reported in `relinked`, as the archive section describes. A declined task's plan lands in `.canon/plans/archive/`, indistinguishable from a shipped one by folder alone. The task file under `.canon/tasks/declined/` is what records which it was.
|
|
76
78
|
|
|
77
79
|
Skills branch on the reason rather than on the exit code, the same rule `canon tasks archive` states:
|
|
78
80
|
|
package/docs/agents/tasks.md
CHANGED
|
@@ -35,7 +35,7 @@ canon tasks next-label --json | jq -r '.label'
|
|
|
35
35
|
|
|
36
36
|
## Archive and decline
|
|
37
37
|
|
|
38
|
-
`canon tasks archive` moves a shipped task into `.canon/tasks/archive/`, selected by its stem or by the pull request it carries, and `canon tasks decline` moves one decided against into `.canon/tasks/declined/`. Both carry the plan and the ready folder along when the task is their last citation. The selectors, the gates, and the refusal reasons are in `tasks-archive.md`.
|
|
38
|
+
`canon tasks archive` moves a shipped task into `.canon/tasks/archive/`, selected by its stem or by the pull request it carries, and `canon tasks decline` moves one decided against into `.canon/tasks/declined/`. Both carry the plan and the ready folder along when the task is their last citation, and both repoint every link to the moved task from another task file. The selectors, the gates, and the refusal reasons are in `tasks-archive.md`.
|
|
39
39
|
|
|
40
40
|
## Plan verbs
|
|
41
41
|
|
package/docs/agents/worktrees.md
CHANGED
|
@@ -53,9 +53,9 @@ Field order is part of the contract. A shell reads this record with a pattern ra
|
|
|
53
53
|
|
|
54
54
|
## What calls it
|
|
55
55
|
|
|
56
|
-
`.husky/post-merge` runs `canon worktrees reclaim --json` on every merge, between the records push and the upgrade
|
|
56
|
+
`canon hooks post-merge`, which `.husky/post-merge` calls, runs `canon worktrees reclaim --json` on every merge, between the records push and the upgrade, so a merged branch's worktree goes without a person remembering. `CANON_SKIP_RECLAIM=1` turns that step off. `hooks.md` covers the verb.
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
That call carries no `--root` and runs from the hook's own working directory, unlike the two steps before it. Git runs a hook from the top level of the worktree the pull happened in, and the verb reads its own working directory to refuse that worktree as `current-worktree`, so a root argument would turn the running worktree into an ordinary candidate and let a pull inside a linked worktree delete the ground under itself.
|
|
59
59
|
|
|
60
60
|
## What makes a worktree reclaimable
|
|
61
61
|
|
package/docs/target/scenarios.md
CHANGED
|
@@ -46,11 +46,11 @@ canon tooling sync vite-react ./frontend --skip base --write
|
|
|
46
46
|
canon tooling sync python ./backend --skip base --write
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
`--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite), and its own stack reference reads through `canon tooling reference <stack>`.
|
|
49
|
+
`--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. The one exception is base's verify runner, `scripts/verify.sh`, `scripts/verify.ts`, and `scripts/verify.json`, which every subtree still receives so it can verify on its own. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite), and its own stack reference reads through `canon tooling reference <stack>`.
|
|
50
50
|
|
|
51
51
|
A sync whose target sits below the git root knows it is writing into a subfolder:
|
|
52
52
|
|
|
53
53
|
- It withholds `.github/`, since GitHub reads workflows only at the repository root, and names each withheld file. Run the subtree from a job in the root workflow instead, with `working-directory` set to the subtree path the sync prints, such as `working-directory: frontend`.
|
|
54
54
|
- It writes a nested `cspell.json` that registers the subtree's word lists, so a root `cspell '**'` reads them for files under that folder. It leaves a spell config the subtree already has alone.
|
|
55
55
|
- It installs no dependencies and adds no scripts to a subtree without a `package.json`. It names what it skipped and ends on `run 'bun init' in <path>, then sync again`.
|
|
56
|
-
- The subtree's
|
|
56
|
+
- The subtree carries no `check` script, since that lives in the root's base layer, so run `./scripts/verify.sh` there. It skips the format, spell, and shell phases a subtree does not declare, since the root runs them, and still fails on a missing lint, typecheck, or test script, the phases its stack's `scripts/verify.json` requires.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erclx/canon",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "5.9.0",
|
|
5
|
+
"description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"canon": "./src/cli.ts"
|
|
@@ -34,6 +34,7 @@ export const CORPORA: readonly string[] = [
|
|
|
34
34
|
export const PRACTICE_SKILLS: readonly string[] = [
|
|
35
35
|
join(SHIPPED_CORPUS, 'codebase-layout'),
|
|
36
36
|
join(SHIPPED_CORPUS, 'test-craft'),
|
|
37
|
+
join(SHIPPED_CORPUS, 'review-craft'),
|
|
37
38
|
]
|
|
38
39
|
|
|
39
40
|
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
package/src/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { register as migrate } from '@/commands/migrate'
|
|
|
30
30
|
import { register as records } from '@/commands/records'
|
|
31
31
|
import { register as sessions } from '@/commands/sessions'
|
|
32
32
|
import { register as worktrees } from '@/commands/worktrees'
|
|
33
|
+
import { register as hooks } from '@/commands/hooks'
|
|
33
34
|
import { register as audits } from '@/commands/audits'
|
|
34
35
|
import { register as gate } from '@/commands/gate'
|
|
35
36
|
import { register as secrets } from '@/commands/secrets'
|
|
@@ -91,6 +92,7 @@ migrate(program)
|
|
|
91
92
|
sessions(program)
|
|
92
93
|
targets(program)
|
|
93
94
|
worktrees(program)
|
|
95
|
+
hooks(program)
|
|
94
96
|
secrets(program)
|
|
95
97
|
deps(program)
|
|
96
98
|
labels(program)
|
package/src/commands/claude.ts
CHANGED
|
@@ -111,7 +111,7 @@ interface PluginUpdateOptions {
|
|
|
111
111
|
/**
|
|
112
112
|
* `no-claude` and `no-plugin` name permanent conditions on a machine that
|
|
113
113
|
* never carries the marketplace plugin at all, matching the `gh-missing` and
|
|
114
|
-
* `no-repository` reasons
|
|
114
|
+
* `no-repository` reasons the post-merge hook's other two steps already stay
|
|
115
115
|
* quiet on forever rather than nagging a project that will never fix them.
|
|
116
116
|
* Every other reason is a real defect and prints.
|
|
117
117
|
*/
|
package/src/commands/docs.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
|
-
import {
|
|
2
|
+
import { type DocEntry, listDocs } from '@/docs/list'
|
|
3
3
|
import { listTopics, readTopic, resolveTopic } from '@/docs/read'
|
|
4
|
-
import { execScript } from '@/exec'
|
|
5
4
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
|
|
6
5
|
import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
|
|
7
6
|
|
|
@@ -22,19 +21,60 @@ export function register(program: Command): void {
|
|
|
22
21
|
'',
|
|
23
22
|
].join('\n'),
|
|
24
23
|
)
|
|
25
|
-
.action(
|
|
26
|
-
|
|
27
|
-
intro('canon docs')
|
|
28
|
-
await execScript('docs/list.sh', [])
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
process.exitCode = get(topic)
|
|
24
|
+
.action((topic: string | undefined) => {
|
|
25
|
+
process.exitCode = topic === undefined ? list({}) : get(topic)
|
|
33
26
|
})
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
docs
|
|
29
|
+
.command('list')
|
|
30
|
+
.description('List the docs and domain context with their descriptions')
|
|
31
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
32
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
33
|
+
.action((opts: ListOptions) => {
|
|
34
|
+
process.exitCode = list(opts)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ListOptions {
|
|
39
|
+
readonly json?: boolean
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The text listing is frame lines on stderr and the `--json` record is the one
|
|
44
|
+
* thing on stdout, so a caller piping it through a wrapper reads JSON alone.
|
|
45
|
+
* The frame opens after the `--json` branch and never wraps it.
|
|
46
|
+
*/
|
|
47
|
+
function list(opts: ListOptions): number {
|
|
48
|
+
const catalog = listDocs(PROJECT_ROOT)
|
|
49
|
+
const mismatch = checkoutMismatchWarning(process.cwd())
|
|
50
|
+
|
|
51
|
+
if (opts.json) {
|
|
52
|
+
if (mismatch !== undefined) logWarn(mismatch)
|
|
53
|
+
const docs: DocEntry[] = catalog.hasContext
|
|
54
|
+
? [...catalog.docs, ...catalog.context]
|
|
55
|
+
: catalog.docs
|
|
56
|
+
process.stdout.write(`${JSON.stringify({ docs })}\n`)
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
intro('canon docs')
|
|
61
|
+
if (mismatch !== undefined) logWarn(mismatch)
|
|
62
|
+
|
|
63
|
+
logStep('Docs')
|
|
64
|
+
for (const entry of catalog.docs) logInfo(describeEntry(entry))
|
|
65
|
+
|
|
66
|
+
if (catalog.hasContext) {
|
|
67
|
+
logStep('Domain context')
|
|
68
|
+
for (const entry of catalog.context) logInfo(describeEntry(entry))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
outro()
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const describeEntry = (entry: DocEntry): string =>
|
|
76
|
+
`${entry.name} : ${entry.description}`
|
|
77
|
+
|
|
38
78
|
/**
|
|
39
79
|
* Writes the document body to stdout and every frame line to stderr, so a
|
|
40
80
|
* caller capturing the output with `$(...)` receives the document alone.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import {
|
|
3
|
+
readMergedSubjects,
|
|
4
|
+
runCanon,
|
|
5
|
+
runPostMerge,
|
|
6
|
+
STEP_VERBS,
|
|
7
|
+
} from '@/hooks/post-merge'
|
|
8
|
+
import { mainWorktreeRoot } from '@/worktree'
|
|
9
|
+
|
|
10
|
+
interface PostMergeOptions {
|
|
11
|
+
readonly root?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function register(program: Command): void {
|
|
15
|
+
const hooks = program
|
|
16
|
+
.command('hooks')
|
|
17
|
+
.description('Run the steps a git hook drives')
|
|
18
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
19
|
+
|
|
20
|
+
hooks
|
|
21
|
+
.command('post-merge')
|
|
22
|
+
.description('Run the post-merge steps in order and report each in a line')
|
|
23
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
24
|
+
.option(
|
|
25
|
+
'--root <path>',
|
|
26
|
+
'Main worktree root holding the board and the records',
|
|
27
|
+
)
|
|
28
|
+
.addHelpText(
|
|
29
|
+
'after',
|
|
30
|
+
[
|
|
31
|
+
'',
|
|
32
|
+
'Runs these steps in order, each a child call reading its --json record:',
|
|
33
|
+
...Object.values(STEP_VERBS).map((verb) => ` canon ${verb.join(' ')}`),
|
|
34
|
+
'',
|
|
35
|
+
'The archive runs once per pull request number the merge brought in.',
|
|
36
|
+
'Lines go to stderr, and an ordinary merge prints nothing.',
|
|
37
|
+
'',
|
|
38
|
+
'The reclaim runs from the working directory with no root, so a pull',
|
|
39
|
+
'inside a linked worktree never removes the worktree it stands in.',
|
|
40
|
+
'',
|
|
41
|
+
'Environment:',
|
|
42
|
+
' CANON_SKIP_RECLAIM=1 skip the worktree reclaim',
|
|
43
|
+
' CANON_SKIP_UPGRADE=1 skip the CLI reinstall',
|
|
44
|
+
' CANON_SKIP_PLUGIN_UPDATE=1 skip the plugin update',
|
|
45
|
+
'',
|
|
46
|
+
'Exit codes:',
|
|
47
|
+
' 0 always, since a hook failing aborts nothing useful',
|
|
48
|
+
'',
|
|
49
|
+
'Examples:',
|
|
50
|
+
' canon hooks post-merge',
|
|
51
|
+
' CANON_SKIP_UPGRADE=1 canon hooks post-merge --root "$root"',
|
|
52
|
+
'',
|
|
53
|
+
].join('\n'),
|
|
54
|
+
)
|
|
55
|
+
.action(async (opts: PostMergeOptions) => {
|
|
56
|
+
const cwd = process.cwd()
|
|
57
|
+
await runPostMerge({
|
|
58
|
+
root: opts.root ?? (await mainWorktreeRoot()),
|
|
59
|
+
cwd,
|
|
60
|
+
subjects: await readMergedSubjects(cwd),
|
|
61
|
+
env: process.env,
|
|
62
|
+
run: runCanon,
|
|
63
|
+
write: (text) => process.stderr.write(text),
|
|
64
|
+
})
|
|
65
|
+
process.exitCode = 0
|
|
66
|
+
})
|
|
67
|
+
}
|