@erclx/canon 4.73.0 → 4.74.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/plan-feature/SKILL.md +2 -2
- package/claude/skills/plan-groundwork/SKILL.md +6 -5
- package/claude/skills/plan-intake/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -1
- package/claude/skills/task-board/SKILL.md +37 -3
- package/docs/agents/commands.md +1 -0
- package/docs/agents/index.md +1 -1
- package/docs/agents/install-and-sync.md +7 -4
- package/docs/agents/records.md +29 -9
- package/docs/agents/tasks.md +2 -0
- package/docs/target-projects.md +4 -0
- package/package.json +1 -1
- package/src/commands/records.ts +138 -0
- package/src/commands/transcripts.ts +20 -3
- package/src/init/plan.ts +8 -1
- package/src/init/steps.ts +17 -0
- package/src/intake/folder.ts +1 -1
- package/src/records/backup.ts +110 -38
- package/src/records/ordinal.ts +243 -0
- package/src/records/validate.ts +28 -0
- package/src/tasks/answers.ts +10 -1
- package/src/transcripts/fetch.ts +31 -1
- package/standards/groundwork.md +1 -1
- package/standards/intake.md +1 -1
- package/standards/plan.md +1 -0
- package/tooling/base/configs/.husky/post-merge +27 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +5 -4
|
@@ -52,8 +52,8 @@ What this skill adds on top of the standard:
|
|
|
52
52
|
|
|
53
53
|
A plan sequencing its work into batches declares where one pull request ends, here rather than at ship time. A dependency chain cannot be split once it is built, so the choice exists only while the batches are still a plan.
|
|
54
54
|
|
|
55
|
-
- One batch is one pull request.
|
|
56
|
-
-
|
|
55
|
+
- One batch is one pull request, and one pull request is one plan file, named for its own concern rather than a numeric suffix. `canon tasks plan-branch` derives one branch from one plan filename, so a `**Batch N**` sub-heading inside one file's `**Files to touch:**` shares that one branch across every batch, and the batch that merges first strands every batch behind it with nothing left to open a pull request against. Write a plan opening five batches as five files before the first line of any of them.
|
|
56
|
+
- State a dependent batch's dependency on the ones before it in its own `**Constraints:**`, naming the earlier batch's slug and stacking rather than merging into it. The batches are built in order and depend on each other in that order, which is what a stack expresses, so the dependency argues for stacking and never for collapsing two batches into one file or one review.
|
|
57
57
|
- Mark each batch dependent or independent by comparing the file sets rather than the descriptions. A batch sharing no file with another is independent and earns its own branch even where both arrived in one request. One run measured at 68 files carried three such batches into a single review because nothing drew the boundary.
|
|
58
58
|
- Sequence a sweep last. A batch deliberately rewriting files earlier batches touched is coherent as the final one and forces every batch behind it into one review anywhere else.
|
|
59
59
|
- Keep each batch's commits contiguous once the work starts. A later fix to an earlier batch belongs on that batch's own commits, since a batch interrupted by another cannot be lifted onto its own branch afterward.
|
|
@@ -46,11 +46,12 @@ List `.canon/groundwork/` from the project root and match the topic against the
|
|
|
46
46
|
|
|
47
47
|
Never match against `.claude/` itself. That directory holds every other workflow surface, so a topic matched there lands on a folder that was never a track.
|
|
48
48
|
|
|
49
|
-
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `ts-migration` over `migration-research`.
|
|
49
|
+
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `ts-migration` over `migration-research`. That is open mode outright: no folder can carry a slug nothing matched yet, so there is nothing to route on. `<nn>` for it is claimed in Open mode Step 1 rather than here, since the qualifying test below has to run before the folder exists.
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- Present
|
|
51
|
+
A matched folder instead routes on its own `<nn>-<slug>/`:
|
|
52
|
+
|
|
53
|
+
- Present without `06-decision.md`: resume
|
|
54
|
+
- Present and the user judges it ready: close
|
|
54
55
|
|
|
55
56
|
Detect the mode from the folder. Do not ask which one to run.
|
|
56
57
|
|
|
@@ -73,7 +74,7 @@ The standard sets the open question format and requires it inside a topic file a
|
|
|
73
74
|
|
|
74
75
|
## Open mode
|
|
75
76
|
|
|
76
|
-
1.
|
|
77
|
+
1. Claim the ordinal and create the folder in one act with `canon records ordinal groundwork <slug> --claim`, per `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md`. Where the installed binary carries no such subcommand, fall back to listing both `.canon/groundwork/` and `.canon/intake/` and creating `.canon/groundwork/<nn>-<slug>/` at the highest ordinal present across the two, incremented.
|
|
77
78
|
2. Write `README.md` first. Writing it first forces the question of what the track is for.
|
|
78
79
|
3. Write `01-current-state.md` by measuring now. Never carry a figure from a previous session or from recall without re-measuring it. Measure only what an open question in the folder needs. A number with no question attached is how groundwork turns into the work.
|
|
79
80
|
4. Write `00-scope.md` when the track is large enough to run away. Skip it on a small track.
|
|
@@ -42,7 +42,7 @@ List `.canon/intake/` from the project root and match the topic against the slug
|
|
|
42
42
|
|
|
43
43
|
Never match against `.claude/` itself. That directory holds every other workflow surface, so a topic matched there lands on a folder that was never an intake.
|
|
44
44
|
|
|
45
|
-
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `toolkit-overview` over `august-triage`.
|
|
45
|
+
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `toolkit-overview` over `august-triage`. Claim the ordinal and create the folder in one act with `canon records ordinal intake <slug> --claim`, per `${CLAUDE_SKILL_DIR}/../../standards/intake.md`. Where the installed binary carries no such subcommand, fall back to listing both `.canon/intake/` and `.canon/groundwork/` and taking the highest ordinal present across the two, incremented. An absent folder opens, and a present one resumes by appending items and revising verdicts the tree has moved under.
|
|
46
46
|
|
|
47
47
|
## Step 2: orient
|
|
48
48
|
|
|
@@ -28,6 +28,7 @@ Run `canon tasks plan-answers <plan> --json` and read `launchable` off the recor
|
|
|
28
28
|
- `launchable: false`: the row is not dispatchable. Report every entry in `open`, each carrying the question label and the reason its suggestion gave for needing a person, and hand the row to the human-launch line below. Never fill the slot on the operator's behalf, which is the one move the plan standard forbids outright.
|
|
29
29
|
- `reason: archived`: the row's plan sits in `.canon/plans/archive/` and describes work that already shipped. Repoint the row at a live plan rather than dispatching, since `auto-ship` Step 1 refuses the same file and the worker would meet that refusal after the launch spent.
|
|
30
30
|
- The command refuses for any other reason, or the record carries no `launchable` key: treat the row as unverified rather than clear, name what could not be read, and fall back to the human. A gate that reads nothing and proceeds is the gate not running.
|
|
31
|
+
- The same read also refuses a plan still staging its batches in one file with `**Batch N**` sub-headings. That reports as an entry in `open` labeled `Batch staging`, so a staged plan reads as `launchable: false` the same way an unanswered operator call does, and the row waits on a split rather than on the operator.
|
|
31
32
|
|
|
32
33
|
Branch on `launchable` rather than on the exit code, which a shell function wrapping `canon` can flatten to zero and so read a held row as a clear one.
|
|
33
34
|
|
|
@@ -24,7 +24,7 @@ Take the board rows in board order and finish one before opening the next. Clear
|
|
|
24
24
|
The blocker cell states what the row waits on, and each kind is tested differently. `canon tasks validate` already re-takes the first two and reports the rest as untested, so run it first and re-take by hand only what it names.
|
|
25
25
|
|
|
26
26
|
- Collision with a track in flight: the validator tests the file the cell cites against the Touches column of every `## Run now` row. A track that merged since the row was parked is no longer in flight, whatever the sets still share. A cell naming the file in prose rather than in backticks cites nothing, so write the collision the way the board format spells it and the check picks the row up on the next run.
|
|
27
|
-
- A dependency on another task: the validator opens the task a link in the cell names. One whose outcomes are all `[x]`, or one already
|
|
27
|
+
- A dependency on another task: the validator opens the task a link in the cell names. One whose outcomes are all `[x]`, one already archived, or one already declined, holds nothing. A cell naming the task in prose resolves to no file, so open it by hand and rewrite the cell as a link.
|
|
28
28
|
- A condition about the tree, such as a count of some shape or the presence of a construct: measure it again, per Two ways a re-test goes wrong below.
|
|
29
29
|
- Waiting on a plan: nothing external holds the row, so the pass writes the plan rather than testing anything. See The plan half below.
|
|
30
30
|
- Waiting on an operator action, such as a run that happens from a shell: record it as untestable this pass and name what the operator has to do. A session cannot clear it, and re-measuring it every pass is waste.
|
|
@@ -72,6 +72,7 @@ A scoping defect can wear a blocker. A task whose file set collides with every o
|
|
|
72
72
|
```plaintext
|
|
73
73
|
Re-tested: <row>, <blocker> → <what the measurement showed>
|
|
74
74
|
Cleared: <row>, now <group>, plan at <path>
|
|
75
|
+
Declined: <row>, waited on <task> which was declined, now <group>
|
|
75
76
|
Still parked: <row>, <blocker> re-confirmed against <what was measured>
|
|
76
77
|
Untestable: <row>, waits on <operator action>
|
|
77
78
|
Split: <task> into <tasks>, file set collided with everything by construction
|
|
@@ -14,7 +14,7 @@ Read `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` before writing any file. It
|
|
|
14
14
|
- Resolve the board at the main worktree root, not `pwd`. Run `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd` outside a git repo. Every read and write below resolves against that root. The board is gitignored scratch shared across worktrees, so a linked worktree writing to its own `pwd` creates a second board nothing else reads.
|
|
15
15
|
- From a linked worktree the file-editing tools refuse that root, so a new task file goes out through `Bash` as a plain single command carrying a heredoc. Archiving already runs through `canon tasks archive`, which resolves the root in-process. Marking an outcome shipped is `docs-fold` and runs through `canon tasks outcome`. Resolve that root the way `session-worktree` does.
|
|
16
16
|
- If `.canon/tasks/` does not exist at that root, stop: `❌ No .canon/tasks/ board. Run canon claude init to set it up.`
|
|
17
|
-
- Route on the request rather than on a flag. Creating names work that does not exist yet, archiving names a task file already
|
|
17
|
+
- Route on the request rather than on a flag. Creating names work that does not exist yet, archiving names a task file already shipped, and declining names one decided against. If the request fits none of the three, stop: `❌ Ambiguous. Say whether to create a task, archive one, or decline one.`
|
|
18
18
|
- Never hand-edit `.canon/tasks/index.md`. A hook regenerates it from sibling frontmatter after a write. Do not run the regen command directly, except after a shell write from a linked worktree: the hook matches `Write|Edit|MultiEdit` and nothing fires on `Bash`, so that one case regenerates explicitly with `canon indexes regen --no-stage --root <main-root> <main-root>/.canon/tasks/index.md`.
|
|
19
19
|
|
|
20
20
|
## Create
|
|
@@ -63,13 +63,13 @@ Scan for work that has been decided and would otherwise be forgotten. Three orig
|
|
|
63
63
|
|
|
64
64
|
List `.canon/groundwork/` and run `gh issue list --state open` when a remote is configured, then grep the board for each track name and issue number. Report any with no task, one line each.
|
|
65
65
|
|
|
66
|
-
Read the dumps through `canon intake list --json`, which reports items, open, unread, and malformed per folder and owns the parse of the answer contract `${CLAUDE_SKILL_DIR}/../../standards/intake.md` fixes. Then grep
|
|
66
|
+
Read the dumps through `canon intake list --json`, which reports items, open, unread, and malformed per folder and owns the parse of the answer contract `${CLAUDE_SKILL_DIR}/../../standards/intake.md` fixes. Then grep `.canon/tasks/`, `.canon/tasks/archive/`, and `.canon/tasks/declined/` for each folder slug. A dump with no live task is the ordinary shape of one already promoted and either shipped or declined, so a check reading the board by itself reports every settled folder as abandoned.
|
|
67
67
|
|
|
68
68
|
A dump is the stronger case for this scan rather than the weaker one. A track holds one question and stays visible, while a dump holds dozens of items whose verdicts were reached and then left with nothing carrying them forward.
|
|
69
69
|
|
|
70
70
|
Those two reads give four states, and the first three earn a line each:
|
|
71
71
|
|
|
72
|
-
- Every item answered, `malformed` at zero, and
|
|
72
|
+
- Every item answered, `malformed` at zero, and none of the board, the archive, or the declined folder cites it. Decided work nobody promoted, which is what this step exists to find.
|
|
73
73
|
- Unread items. The folder is waiting on the operator rather than forgotten, so it takes its own wording and never lands in the block above.
|
|
74
74
|
- `malformed` above zero. An item carrying no answer slot can be reached by no verb, so name the folder as a file to repair rather than as work in either state above.
|
|
75
75
|
- The archive cites it. Promoted and shipped, so say nothing.
|
|
@@ -129,6 +129,40 @@ Leave `TASK-ARCHIVE.md` alone when it is present in the archive folder. It recor
|
|
|
129
129
|
|
|
130
130
|
The command drops the task's row from `.canon/tasks/priority.md` and leaves prose alone. Remove any sentence that still names the archived task or counts the rows that changed, since a stale count reads as board state.
|
|
131
131
|
|
|
132
|
+
## Decline
|
|
133
|
+
|
|
134
|
+
A task decided against carries no `post-merge` hook of its own, so every decline request arrives here directly rather than through work the hook already did.
|
|
135
|
+
|
|
136
|
+
Do not move the file, edit `priority.md` or `backlog.md`, or regenerate the index by hand. `canon tasks decline` owns all three as one unit.
|
|
137
|
+
|
|
138
|
+
### Step 1: gather the reason
|
|
139
|
+
|
|
140
|
+
Ask for the reason when the request does not carry one, and stop rather than guessing: `❌ No reason. Say why the task is being declined.` The command takes it as `--reason <text>` and refuses without it, so gathering it here saves a round trip through that refusal.
|
|
141
|
+
|
|
142
|
+
### Step 2: run the decline
|
|
143
|
+
|
|
144
|
+
Pass the task's filename stem:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
canon tasks decline <stem> --reason "<text>" [--by <name>] --json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The command refuses rather than reports, and the refusal reaches this skill through the record rather than through the exit, the same wrapper hazard `canon tasks archive` carries. Branch on `ok`, then on `reason`.
|
|
151
|
+
|
|
152
|
+
On success the record carries `from`, `to`, `priorityRowRemoved`, `backlogRowRemoved`, and `indexRegenerated`, where `backlogRowRemoved` is decline's own field since a task can be declined straight off `backlog.md` and archive never checks that file. It also carries `plan` when the task was the last live citation of a live plan, holding the `from` and `to` of the plan moved alongside it, the same shape `canon tasks archive` uses for its own `plan` field.
|
|
153
|
+
|
|
154
|
+
### Step 3: route on a refusal
|
|
155
|
+
|
|
156
|
+
- `no-match`: the stem does not name exactly one task. Either none matches, or exactly one starts with it and the full name is needed. Check the name against the listed stems.
|
|
157
|
+
- `ambiguous`: the stem is a prefix more than one task starts with, unlike archive's own `ambiguous`, which fires on a shared pull request. Decline takes no pull-request selector, so this is the only route to it. Pass the full stem.
|
|
158
|
+
- `bad-input`: the command line was wrong rather than the board. Read the message, fix the arguments, and run it again. Nothing on the board needs repair, which is what separates this from the two above.
|
|
159
|
+
|
|
160
|
+
Do not move a plan by hand from this skill. The command carries the plan with the task when no other live task cites it, and retargets the declined task's `Plan:` line at the new path. A second mover drifts into relocating the same file differently.
|
|
161
|
+
|
|
162
|
+
### Step 4: clear prose naming the task
|
|
163
|
+
|
|
164
|
+
The command drops the task's row from `.canon/tasks/priority.md` or `.canon/tasks/backlog.md` and leaves prose alone. Remove any sentence that still names the declined task or counts the rows that changed, since a stale count reads as board state.
|
|
165
|
+
|
|
132
166
|
## Output
|
|
133
167
|
|
|
134
168
|
Emit the full relative path from the project root for every file written or moved. A bare filename names a file the reader cannot open.
|
package/docs/agents/commands.md
CHANGED
|
@@ -48,6 +48,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
48
48
|
| `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`) |
|
|
49
49
|
| `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
50
50
|
| `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
|
|
51
|
+
| `canon records ordinal` | Report or claim the next ordinal `intake` and `groundwork` share, creating the folder atomically (`--claim`, `--json`) |
|
|
51
52
|
| `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
|
|
52
53
|
| `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
|
|
53
54
|
| `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
package/docs/agents/index.md
CHANGED
|
@@ -29,7 +29,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
29
29
|
- [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
|
|
30
30
|
- [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
|
|
31
31
|
- [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
|
|
32
|
-
- [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
32
|
+
- [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
33
33
|
- [Restated instructions](restated.md): Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the shipped skill bodies, and each other, how a match is decided, the three classes, which surface a later edit starts from, and why the sweep reports rather than gates
|
|
34
34
|
- [Review classification](review-classification.md): Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
|
|
35
35
|
- [Routing report](routing.md): Reading per CLAUDE.md section how many bullets name a path, what counts as naming one, when a rule counts as covering it, the two refusals, and why the verb reports rather than gates
|
|
@@ -402,16 +402,19 @@ so this section reports and gates nothing.
|
|
|
402
402
|
|
|
403
403
|
## Bootstrap
|
|
404
404
|
|
|
405
|
-
`canon init` installs up to
|
|
405
|
+
`canon init` installs up to five core domains and reports each one independently. A
|
|
406
406
|
domain that fails does not abort the run, so the command finishes the rest and
|
|
407
407
|
exits 1 naming the failures. Passing any flag skips the confirmation prompt,
|
|
408
408
|
which is what makes it scriptable.
|
|
409
409
|
|
|
410
410
|
`--stack` defaults to `base`, and the default
|
|
411
411
|
does not read as a passed flag, so a bare `canon init` installs governance and
|
|
412
|
-
still prompts. `--skip` takes `wiki` and `
|
|
413
|
-
without aborting on any other value.
|
|
414
|
-
|
|
412
|
+
still prompts. `--skip` takes `wiki`, `governance`, and `records`, and warns
|
|
413
|
+
without aborting on any other value. Records has nothing to run non-interactively,
|
|
414
|
+
since the private backup repository does not exist yet at scaffold time, so its
|
|
415
|
+
step only prints the one-time setup reminder and `--skip records` silences it.
|
|
416
|
+
There is no `--standards` and no `--snippets`, since no run writes either corpus
|
|
417
|
+
into the target.
|
|
415
418
|
|
|
416
419
|
## Unguarded tooling primitives
|
|
417
420
|
|
package/docs/agents/records.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Records
|
|
3
|
-
description: The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
3
|
+
description: The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Records
|
|
@@ -39,14 +39,14 @@ Nothing fires it automatically. The five record folders are gitignored, so the s
|
|
|
39
39
|
|
|
40
40
|
### What each kind checks
|
|
41
41
|
|
|
42
|
-
| Kind | What it reports
|
|
43
|
-
| ------------ |
|
|
44
|
-
| `plans` | A filename that is not `feature-<slug>.md`, a missing `# Feature:` heading, a missing required section, a files-to-touch entry naming no file or saying nothing about one,
|
|
45
|
-
| `groundwork` | A track with no `README.md` or no `01-current-state.md`, a file missing `title` or `description`, a `README.md` with no `date` as `YYYY-MM-DD`, an unnumbered file, and a track holding a decision without its handoff or the reverse
|
|
46
|
-
| `intake` | A dump with no `00-overview.md`, the same frontmatter and numbering checks, an item missing any of `Problem`, `Fix`, `Worth it`, or `You`, and an item carrying `Open` with no `Suggested`
|
|
47
|
-
| `memory` | A filename whose prefix names none of the four types, an entry missing `title`, `description`, or `category`, a `category` disagreeing with that prefix, a title repeating the filename, and a rule-bearing body missing a part
|
|
48
|
-
| `standards` | A standard missing `title` or `description`, an absent `## Scope` section, a scope section carrying no `Does not govern:` list, a statement anchoring nothing, and a filename naming no part of the path the statement governs
|
|
49
|
-
| `teach` | A workspace folder carrying no two-digit ordinal, an absent `MISSION.md`, `RESOURCES.md`, or `GLOSSARY.md`, a file missing `title` or `description`, a mission with no `date` as `YYYY-MM-DD` or no `## Success looks like` section, an unnumbered learning record, and a reference page opening with an ordinal
|
|
42
|
+
| Kind | What it reports |
|
|
43
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `plans` | A filename that is not `feature-<slug>.md`, a missing `# Feature:` heading, a missing required section, a files-to-touch entry naming no file or saying nothing about one, a question carrying no suggestion or no answer slot, and a batch still staged with a `**Batch N**` sub-heading inside the one file rather than split into its own plan |
|
|
45
|
+
| `groundwork` | A track with no `README.md` or no `01-current-state.md`, a file missing `title` or `description`, a `README.md` with no `date` as `YYYY-MM-DD`, an unnumbered file, and a track holding a decision without its handoff or the reverse |
|
|
46
|
+
| `intake` | A dump with no `00-overview.md`, the same frontmatter and numbering checks, an item missing any of `Problem`, `Fix`, `Worth it`, or `You`, and an item carrying `Open` with no `Suggested` |
|
|
47
|
+
| `memory` | A filename whose prefix names none of the four types, an entry missing `title`, `description`, or `category`, a `category` disagreeing with that prefix, a title repeating the filename, and a rule-bearing body missing a part |
|
|
48
|
+
| `standards` | A standard missing `title` or `description`, an absent `## Scope` section, a scope section carrying no `Does not govern:` list, a statement anchoring nothing, and a filename naming no part of the path the statement governs |
|
|
49
|
+
| `teach` | A workspace folder carrying no two-digit ordinal, an absent `MISSION.md`, `RESOURCES.md`, or `GLOSSARY.md`, a file missing `title` or `description`, a mission with no `date` as `YYYY-MM-DD` or no `## Success looks like` section, an unnumbered learning record, and a reference page opening with an ordinal |
|
|
50
50
|
|
|
51
51
|
The half-closed track is the groundwork check a reader cannot run by eye. A folder holding `06` without `07` reads as closed to anyone scanning filenames while the file a returning session actually opens is absent.
|
|
52
52
|
|
|
@@ -104,6 +104,26 @@ A transform runs only where the missing value is recoverable from the file itsel
|
|
|
104
104
|
|
|
105
105
|
Exit codes: `0` nothing carried a known transform, or `--write` repaired everything it found. `1` refused for a reason `validate` shares, every candidate it found failed to repair, or `--write` repaired only some of them. `2` a record carries a known transform and `--write` was not passed.
|
|
106
106
|
|
|
107
|
+
## Ordinal
|
|
108
|
+
|
|
109
|
+
`canon records ordinal <kind> <slug>` reports the next ordinal `intake` and `groundwork` share, or claims it with `--claim`. The two kinds share one sequence, per `standards/intake.md` and `standards/groundwork.md`, so this reads both `.canon/intake/` and `.canon/groundwork/` regardless of which kind was asked for.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
canon records ordinal intake my-topic
|
|
113
|
+
canon records ordinal groundwork my-topic --claim
|
|
114
|
+
canon records ordinal groundwork my-topic --claim --json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Option | Behavior |
|
|
118
|
+
| --------------- | ------------------------------------------------------ |
|
|
119
|
+
| `--json` | Add a machine-readable record on stdout |
|
|
120
|
+
| `--claim` | Create the folder atomically instead of only reporting |
|
|
121
|
+
| `--root <path>` | Project root, defaulting to the main worktree |
|
|
122
|
+
|
|
123
|
+
Without `--claim` this only reports, so two sessions reading at once can still report the same number, which is what let two sessions open two different record folders under one ordinal on the same day. `--claim` closes that: it reserves the number at a path both an `intake` claim and a `groundwork` claim resolve to identically, whichever kind is asking, and only creates the kind's own `<nn>-<slug>/` folder once that reservation is won. A losing reservation is retried against a freshly read ordinal rather than reported as a collision, bounded to five attempts before refusing as `ordinal-contended`.
|
|
124
|
+
|
|
125
|
+
Exit codes: `0` reported the next ordinal, or `--claim` created the folder. `1` refused, `unknown-kind` when the argument names neither `intake` nor `groundwork`. `2` `--claim` lost every retry to a collision.
|
|
126
|
+
|
|
107
127
|
## Size
|
|
108
128
|
|
|
109
129
|
`canon records size` reports what each record folder holds and how much of it is recent. It reads the ten backed folders named under Push and pull, plus the scratch folder, and it gates nothing.
|
package/docs/agents/tasks.md
CHANGED
|
@@ -141,6 +141,8 @@ A relative path resolves against the project root first and against `.canon/task
|
|
|
141
141
|
|
|
142
142
|
`canon tasks plan-citations` reads a task's `Plan:` line against those same two bases in the opposite order, and tests that the target lands under the live plans folder, which this verb does not. Both answer the same file for every spelling a board writes. Liveness is a separate refusal here: a plan resolving inside `.canon/plans/archive/` returns `archived` rather than a launchable reading, since it answers every question and describes work that already shipped.
|
|
143
143
|
|
|
144
|
+
A plan still staging its batches with a `**Batch N**` sub-heading inside one file's `**Files to touch:**` reads `launchable: false` the same way, alongside the operator-call case. It reports as an entry in `open` labeled `Batch staging`, stating that the plan must split into one file per batch before it can dispatch, since the row waits on a split rather than on the operator.
|
|
145
|
+
|
|
144
146
|
| Option | Effect |
|
|
145
147
|
| --------------- | ------------------------------------------- |
|
|
146
148
|
| `--json` | Emit a machine-readable record on stdout |
|
package/docs/target-projects.md
CHANGED
|
@@ -143,6 +143,10 @@ The first line takes the `.canon/` ignore entry, and the verb refuses until the
|
|
|
143
143
|
|
|
144
144
|
Order matters between the first line and the two under it. The sync prunes the twelve old `.claude/` ignore entries down to the one `.canon/` line, which leaves every record still at the old root visible to git and therefore to the verb. The sweep passes over them on purpose, reporting a count of what it left alone rather than reading the memory pen and the groundwork trails as source.
|
|
145
145
|
|
|
146
|
+
### Back records up off this machine, once
|
|
147
|
+
|
|
148
|
+
Moving records under `.canon/` relocates them, and relocating them is not the same as backing them up. Run `canon records push` once a private repository exists for them, and it prints the one-time setup command when it finds none, since the task board, the memory pen, and the groundwork trails otherwise live on one disk with nothing pushing them off it. `canon init` prints the same reminder as a notice-only step rather than trying to run this non-interactively, since the private repository does not exist yet at scaffold time.
|
|
149
|
+
|
|
146
150
|
Read the `ok` field out of the `--json` record rather than the exit code. A shell profile that wraps `canon` in a function takes its status from whatever the function runs last, so an absent subcommand and a clean run can both exit 0, and a reader watching the exit alone concludes the move happened.
|
|
147
151
|
|
|
148
152
|
A tracked file that names an old record path on purpose, such as prose dating a decision, keeps it by carrying `canon-keep-record-root` on that line or the nearest non-blank line above. The report pass prints every file it would rewrite, which is where to catch one before `--write` runs.
|
package/package.json
CHANGED
package/src/commands/records.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { join } from 'node:path'
|
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { BACKED_FOLDERS, pullRecords, pushRecords } from '@/records/backup'
|
|
5
5
|
import { migrateRecord } from '@/records/migrate'
|
|
6
|
+
import {
|
|
7
|
+
type ClaimOutcome,
|
|
8
|
+
claimOrdinal,
|
|
9
|
+
highestOrdinal,
|
|
10
|
+
isOrdinalKind,
|
|
11
|
+
ORDINAL_KINDS,
|
|
12
|
+
} from '@/records/ordinal'
|
|
6
13
|
import {
|
|
7
14
|
type FolderSize,
|
|
8
15
|
formatBytes,
|
|
@@ -40,6 +47,9 @@ const EXIT_FINDINGS = 2
|
|
|
40
47
|
/** Returned when a record carries a known transform and `--write` was not passed. */
|
|
41
48
|
const EXIT_MIGRATABLE = 2
|
|
42
49
|
|
|
50
|
+
/** Returned when `--claim` loses every retry to a collision. */
|
|
51
|
+
const EXIT_CONTENDED = 2
|
|
52
|
+
|
|
43
53
|
interface ValidateCommandOptions {
|
|
44
54
|
readonly json?: boolean
|
|
45
55
|
readonly root?: string
|
|
@@ -51,6 +61,10 @@ interface MigrateCommandOptions extends ValidateCommandOptions {
|
|
|
51
61
|
readonly write?: boolean
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
interface OrdinalCommandOptions extends ValidateCommandOptions {
|
|
65
|
+
readonly claim?: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
export function register(program: Command): void {
|
|
55
69
|
const records = program
|
|
56
70
|
.command('records')
|
|
@@ -140,6 +154,49 @@ export function register(program: Command): void {
|
|
|
140
154
|
process.exitCode = await runMigrate(kind, opts)
|
|
141
155
|
})
|
|
142
156
|
|
|
157
|
+
records
|
|
158
|
+
.command('ordinal')
|
|
159
|
+
.description(
|
|
160
|
+
'Report or claim the next ordinal shared by intake and groundwork folders',
|
|
161
|
+
)
|
|
162
|
+
.argument('<kind>', `Ordinal-bearing folder: ${ORDINAL_KINDS.join(', ')}`)
|
|
163
|
+
.argument('<slug>', 'The kebab-case slug the new folder will carry')
|
|
164
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
165
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
166
|
+
.option(
|
|
167
|
+
'--claim',
|
|
168
|
+
'Create the folder atomically instead of only reporting the ordinal',
|
|
169
|
+
)
|
|
170
|
+
.option('--root <path>', 'Project root, defaulting to the main worktree')
|
|
171
|
+
.addHelpText(
|
|
172
|
+
'after',
|
|
173
|
+
[
|
|
174
|
+
'',
|
|
175
|
+
'intake and groundwork folders share one ordinal sequence, so this reads',
|
|
176
|
+
'both .canon/intake/ and .canon/groundwork/ regardless of which kind was',
|
|
177
|
+
'asked for.',
|
|
178
|
+
'',
|
|
179
|
+
'Exit codes:',
|
|
180
|
+
' 0 reported the next ordinal, or --claim created the folder',
|
|
181
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
182
|
+
' 2 --claim lost every retry to a collision',
|
|
183
|
+
'',
|
|
184
|
+
'Without --claim this only reports, so two sessions reading at once can',
|
|
185
|
+
'still report the same number. --claim resolves that by creating the',
|
|
186
|
+
'folder as part of the same act, retrying past a losing race rather than',
|
|
187
|
+
'reporting one.',
|
|
188
|
+
'',
|
|
189
|
+
'Examples:',
|
|
190
|
+
' canon records ordinal intake my-topic',
|
|
191
|
+
' canon records ordinal groundwork my-topic --claim',
|
|
192
|
+
' canon records ordinal groundwork my-topic --claim --json',
|
|
193
|
+
'',
|
|
194
|
+
].join('\n'),
|
|
195
|
+
)
|
|
196
|
+
.action(async (kind: string, slug: string, opts: OrdinalCommandOptions) => {
|
|
197
|
+
process.exitCode = await runOrdinal(kind, slug, opts)
|
|
198
|
+
})
|
|
199
|
+
|
|
143
200
|
records
|
|
144
201
|
.command('size')
|
|
145
202
|
.description('Report what each record folder holds and how much is recent')
|
|
@@ -726,3 +783,84 @@ export function migrateExitCode(
|
|
|
726
783
|
if (!write) return EXIT_MIGRATABLE
|
|
727
784
|
return refused.length > 0 ? 1 : 0
|
|
728
785
|
}
|
|
786
|
+
|
|
787
|
+
async function runOrdinal(
|
|
788
|
+
kind: string,
|
|
789
|
+
slug: string,
|
|
790
|
+
opts: OrdinalCommandOptions,
|
|
791
|
+
): Promise<number> {
|
|
792
|
+
const emitJson = opts.json ?? false
|
|
793
|
+
|
|
794
|
+
if (!isOrdinalKind(kind)) {
|
|
795
|
+
return reportRefusal(
|
|
796
|
+
'canon records ordinal',
|
|
797
|
+
{
|
|
798
|
+
reason: 'unknown-kind',
|
|
799
|
+
message: `Not an ordinal-bearing kind: ${kind}. Expected one of: ${ORDINAL_KINDS.join(', ')}.`,
|
|
800
|
+
},
|
|
801
|
+
emitJson,
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
806
|
+
const claim = opts.claim ?? false
|
|
807
|
+
|
|
808
|
+
if (!claim) {
|
|
809
|
+
const next = String((await highestOrdinal(root)) + 1).padStart(2, '0')
|
|
810
|
+
|
|
811
|
+
if (emitJson) {
|
|
812
|
+
process.stdout.write(
|
|
813
|
+
`${JSON.stringify({ ok: true, root, kind, slug, ordinal: next, claimed: false })}\n`,
|
|
814
|
+
)
|
|
815
|
+
} else {
|
|
816
|
+
intro('canon records ordinal')
|
|
817
|
+
logStep('Next')
|
|
818
|
+
logInfo(`${next}-${slug} (report only, pass --claim to create it)`)
|
|
819
|
+
outro()
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return 0
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return reportOrdinal(root, await claimOrdinal(root, kind, slug), emitJson)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function reportOrdinal(
|
|
829
|
+
root: string,
|
|
830
|
+
outcome: ClaimOutcome,
|
|
831
|
+
emitJson: boolean,
|
|
832
|
+
): number {
|
|
833
|
+
if (!outcome.ok) {
|
|
834
|
+
if (emitJson) {
|
|
835
|
+
process.stderr.write(`${outcome.message}\n`)
|
|
836
|
+
process.stdout.write(
|
|
837
|
+
`${JSON.stringify({
|
|
838
|
+
ok: false,
|
|
839
|
+
reason: outcome.reason,
|
|
840
|
+
message: outcome.message,
|
|
841
|
+
lastOrdinal: outcome.lastOrdinal,
|
|
842
|
+
})}\n`,
|
|
843
|
+
)
|
|
844
|
+
return EXIT_CONTENDED
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
intro('canon records ordinal')
|
|
848
|
+
logStep('Refused')
|
|
849
|
+
logError(outcome.message)
|
|
850
|
+
outro()
|
|
851
|
+
return EXIT_CONTENDED
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (emitJson) {
|
|
855
|
+
process.stdout.write(
|
|
856
|
+
`${JSON.stringify({ root, ...outcome, claimed: true })}\n`,
|
|
857
|
+
)
|
|
858
|
+
} else {
|
|
859
|
+
intro('canon records ordinal')
|
|
860
|
+
logStep('Claimed')
|
|
861
|
+
logInfo(outcome.path)
|
|
862
|
+
outro()
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return 0
|
|
866
|
+
}
|
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
|
+
import { recordDir } from '@/record-root'
|
|
3
4
|
import { ensureYtDlp, fetchOne } from '@/transcripts/fetch'
|
|
4
5
|
import { palette } from '@/ui'
|
|
6
|
+
import { mainWorktreeRoot } from '@/worktree'
|
|
5
7
|
|
|
6
8
|
interface TranscriptOptions {
|
|
7
|
-
out
|
|
9
|
+
out?: string
|
|
8
10
|
keepTimestamps?: boolean
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A caller-supplied `--out` resolves against the CWD, since naming a path
|
|
15
|
+
* explicitly opts out of the backed default. With none, the destination is
|
|
16
|
+
* the backed `transcripts` record folder at the main worktree root, which
|
|
17
|
+
* `canon records push` and `canon records pull` carry along with the rest.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveOutDir(opts: TranscriptOptions): Promise<string> {
|
|
20
|
+
return opts.out
|
|
21
|
+
? resolve(process.cwd(), opts.out)
|
|
22
|
+
: recordDir(await mainWorktreeRoot(), 'transcripts')
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export function register(program: Command): void {
|
|
12
26
|
program
|
|
13
27
|
.command('transcripts <url>')
|
|
14
28
|
.description('Fetch a YouTube transcript with metadata frontmatter')
|
|
15
|
-
.option(
|
|
29
|
+
.option(
|
|
30
|
+
'-o, --out <path>',
|
|
31
|
+
'Output directory, defaulting to the backed transcripts record folder',
|
|
32
|
+
)
|
|
16
33
|
.option(
|
|
17
34
|
'--keep-timestamps',
|
|
18
35
|
'Prefix each line with [mm:ss] instead of prose',
|
|
19
36
|
)
|
|
20
37
|
.action(async (url: string, opts: TranscriptOptions) => {
|
|
21
|
-
const outDir =
|
|
38
|
+
const outDir = await resolveOutDir(opts)
|
|
22
39
|
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
23
40
|
process.stderr.write(
|
|
24
41
|
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}canon transcripts${NC}\n`,
|
package/src/init/plan.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SKIPPABLE_DOMAINS = ['wiki', 'governance'] as const
|
|
1
|
+
export const SKIPPABLE_DOMAINS = ['wiki', 'governance', 'records'] as const
|
|
2
2
|
|
|
3
3
|
export type SkippableDomain = (typeof SKIPPABLE_DOMAINS)[number]
|
|
4
4
|
|
|
@@ -104,6 +104,13 @@ export function planInit(flags: InitFlags): InitPlan {
|
|
|
104
104
|
})
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
if (!flags.skip.skipped.has('records')) {
|
|
108
|
+
preview.push({
|
|
109
|
+
level: 'info',
|
|
110
|
+
text: 'records (one-time backup setup notice)',
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
const total = preview.filter((line) => line.level === 'info').length
|
|
108
115
|
|
|
109
116
|
return { preview, total }
|
package/src/init/steps.ts
CHANGED
|
@@ -4,6 +4,13 @@ import type { DomainStep } from '@/init/run'
|
|
|
4
4
|
/** Builds the child-process invocation for one domain. */
|
|
5
5
|
export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* States the same fact as `docs/target-projects.md`'s backup section in its
|
|
9
|
+
* own words. Keep the two in step by hand; nothing compares them.
|
|
10
|
+
*/
|
|
11
|
+
const RECORDS_SETUP_NOTICE =
|
|
12
|
+
"No private repository to push to yet. Run 'canon records push' once one exists to print the one-time setup command, or --skip records to silence this."
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Orders the domains an init installs. Base tooling seeds the files the later
|
|
9
16
|
* domains install alongside, so the sequence is part of the contract rather
|
|
@@ -56,6 +63,16 @@ export function buildSteps(
|
|
|
56
63
|
})
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
if (!flags.skip.skipped.has('records')) {
|
|
67
|
+
// `skip` here means there is nothing to run non-interactively, not that
|
|
68
|
+
// the caller opted out, unlike every other push of this kind above.
|
|
69
|
+
steps.push({
|
|
70
|
+
kind: 'skip',
|
|
71
|
+
label: 'Records backup',
|
|
72
|
+
notice: RECORDS_SETUP_NOTICE,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
return steps
|
|
60
77
|
}
|
|
61
78
|
|
package/src/intake/folder.ts
CHANGED
|
@@ -138,7 +138,7 @@ function matchSlug(names: readonly string[], slug: string): SlugMatch {
|
|
|
138
138
|
return { kind: 'none' }
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function extractOrdinal(name: string): string {
|
|
141
|
+
export function extractOrdinal(name: string): string {
|
|
142
142
|
return /^\d{2,}-/.exec(name)?.[0].slice(0, -1) ?? ''
|
|
143
143
|
}
|
|
144
144
|
|