@erclx/canon 4.121.0 → 4.123.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/auto-ship/SKILL.md +6 -5
- package/claude/skills/auto-ship/references/tier-failures.md +7 -4
- package/claude/skills/backlog-triage/REQUIREMENT.md +47 -0
- package/claude/skills/backlog-triage/SKILL.md +139 -0
- package/claude/skills/canon-rollout/SKILL.md +6 -6
- package/claude/skills/context-fold/REQUIREMENT.md +75 -0
- package/claude/skills/context-fold/SKILL.md +162 -0
- package/claude/skills/{docs-fold → context-fold}/references/anchor-sweep.md +1 -1
- package/claude/skills/{docs-fold → context-fold}/references/classify.md +4 -2
- package/claude/skills/context-fold/references/context-refresh.md +48 -0
- package/claude/skills/context-fold/references/promoted-pages.md +29 -0
- package/claude/skills/context-fold/references/receipt-sweep.md +43 -0
- package/claude/skills/context-fold/references/unusable-baseline.md +25 -0
- package/claude/skills/{docs-fold → context-fold}/references/wireframe-sweep.md +1 -1
- package/claude/skills/docs-fold/REQUIREMENT.md +8 -58
- package/claude/skills/docs-fold/SKILL.md +3 -278
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
- package/claude/skills/docs-sync/SKILL.md +1 -1
- package/claude/skills/draft-context/REQUIREMENT.md +2 -2
- package/claude/skills/draft-context/SKILL.md +3 -3
- package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +2 -2
- package/claude/skills/git-pr/SKILL.md +8 -146
- package/claude/skills/git-pr/references/evidence.md +114 -0
- package/claude/skills/git-pr/references/labels.md +20 -0
- package/claude/skills/git-pr/references/task-number.md +26 -0
- package/claude/skills/git-ship/SKILL.md +5 -4
- package/claude/skills/memory-capture/REQUIREMENT.md +2 -2
- package/claude/skills/memory-capture/SKILL.md +19 -9
- package/claude/skills/memory-review/REQUIREMENT.md +5 -3
- package/claude/skills/memory-review/SKILL.md +22 -107
- package/claude/skills/memory-review/references/later-phases.md +111 -0
- package/claude/skills/memory-review/references/receipt-format.md +19 -3
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/review-branch/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +4 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
- package/claude/skills/session-resume/REQUIREMENT.md +1 -1
- package/claude/skills/session-resume/SKILL.md +4 -3
- package/claude/skills/session-worktree/SKILL.md +4 -10
- package/claude/skills/task-board/REQUIREMENT.md +1 -1
- package/claude/skills/task-board/SKILL.md +7 -81
- package/claude/skills/task-board/references/archive.md +54 -0
- package/claude/skills/task-board/references/decline.md +40 -0
- package/claude/skills/teach-workspace/SKILL.md +8 -105
- package/claude/skills/teach-workspace/references/lesson-craft.md +51 -1
- package/claude/skills/teach-workspace/references/promotion.md +41 -0
- package/docs/agents/architecture-record.md +26 -0
- package/docs/agents/commands.md +87 -154
- package/docs/agents/context-audit-checks.md +2 -20
- package/docs/agents/context-audit.md +1 -1
- package/docs/agents/context-classify.md +1 -1
- package/docs/agents/drift-surfaces.md +192 -0
- package/docs/agents/index.md +12 -4
- package/docs/agents/install-and-sync.md +2 -181
- package/docs/agents/markdown-audit-weight.md +73 -0
- package/docs/agents/markdown-audit.md +3 -90
- package/docs/agents/migrate.md +34 -0
- package/docs/agents/records-stale.md +62 -0
- package/docs/agents/records.md +5 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/tasks-archive.md +81 -0
- package/docs/agents/tasks-plans.md +149 -0
- package/docs/agents/tasks-validate.md +131 -0
- package/docs/agents/tasks.md +7 -334
- package/docs/target-projects.md +3 -3
- package/docs/workflow/ai-workflow.md +34 -29
- package/docs/workflow/operating-model.md +1 -1
- package/governance/rules/claude/555-tasks.md +2 -1
- package/governance/rules/core/045-memory.md +1 -1
- package/package.json +1 -1
- package/src/claude/cases/setup.ts +1 -1
- package/src/claude/cases/workflow.ts +22 -1
- package/src/commands/records.ts +146 -0
- package/src/commands/sandbox.ts +6 -4
- package/src/commands/tasks.ts +39 -29
- package/src/context/classify/extract.ts +1 -1
- package/src/context/classify/ollama.ts +1 -1
- package/src/context/classify/run.ts +1 -1
- package/src/gate/measures.ts +1 -1
- package/src/gate/stages.ts +1 -0
- package/src/migrate/skill-names.ts +15 -2
- package/src/records/stale.ts +243 -0
- package/src/sandbox/expect.ts +1 -1
- package/src/tasks/archive.ts +223 -92
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/record.ts +60 -17
- package/src/tasks/validate.ts +37 -4
- package/standards/board.md +136 -0
- package/standards/commit.md +5 -0
- package/standards/groundwork.md +25 -36
- package/standards/index.md +2 -1
- package/standards/memory.md +13 -1
- package/standards/plan.md +20 -34
- package/standards/readme.md +8 -77
- package/standards/requirements.md +2 -2
- package/standards/tasks.md +19 -126
- package/standards/versioning.md +1 -1
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/.husky/post-merge +1 -1
- package/tooling/base/configs/commitlint.config.js +27 -1
- package/tooling/base/reference.md +1 -1
- package/tooling/claude/reference.md +1 -1
- package/tooling/go/reference.md +1 -1
- package/tooling/nextjs/reference.md +1 -1
- package/tooling/php/reference.md +1 -1
- package/tooling/python/reference.md +1 -1
- package/tooling/vite-react/reference.md +1 -1
- package/tooling/web/reference.md +1 -1
|
@@ -55,11 +55,12 @@ One session works for most features. Prefer splitting across two sessions only w
|
|
|
55
55
|
Work in Claude Code directly. It reads `CLAUDE.md` automatically and has full file access, no pasting needed.
|
|
56
56
|
|
|
57
57
|
- When the input is a pile of findings rather than one feature, invoke `canon:plan-intake` first. It files the dump into `.canon/intake/<nn>-<slug>/`, one item per finding carrying a problem measured against the tree, a proposed fix, and a verdict, then names which items are plan-ready, which need measuring, and which are already settled.
|
|
58
|
+
- When the backlog has grown past what anyone rereads, invoke `canon:backlog-triage`. It files one item per backlog row into an intake folder, suggesting decline, archive, keep, or promote and leaning toward decline, and once `canon:plan-intake-answer` has taken your answers, a second run applies the approved verdicts through the task verbs.
|
|
58
59
|
- When the current state is unmeasured and more than one approach is live, invoke `canon:plan-groundwork` first. It opens a track folder under `.canon/groundwork/<nn>-<slug>/` and ends in a decision, which may be to do nothing. Skip it when the approach is already settled.
|
|
59
60
|
- Invoke `canon:plan-feature` to scan for code-level conflicts and ambiguities, confirm approach before proceeding
|
|
60
61
|
- Implement the feature, then Claude Code runs the commands defined in `CLAUDE.md`, fixes failures, and iterates until all pass
|
|
61
62
|
- For UI changes, invoke `canon:ui-checklist` to write what a reviewer has to look at and name any behavior shipping without a test
|
|
62
|
-
End the session once the feature works and tests pass. Invoke `canon:
|
|
63
|
+
End the session once the feature works and tests pass. Invoke `canon:context-fold` to capture any decisions made during implementation before closing.
|
|
63
64
|
|
|
64
65
|
The routing test is whether the repository can answer an item today. A session grepping handles the yes, and a groundwork track handles the no.
|
|
65
66
|
|
|
@@ -115,11 +116,13 @@ A person points it at a private repository once and both verbs refuse until they
|
|
|
115
116
|
|
|
116
117
|
`canon records prune-tmp` reports scratch under `.canon/tmp` nobody has touched inside an age window, and deletes it only with `--write`. A `tmp/handoff/` folder and the live `tmp/pr/poll/` baseline are never offered, since a reader deletes a handoff once it is read and a poll baseline is state a session still needs. See [records](../agents/records.md) for the candidate shapes and the exit codes.
|
|
117
118
|
|
|
119
|
+
`canon records stale memory` reads the memory pen as a review queue. It names each entry due for review, meaning one carrying no `reviewed` date or one older than 30 days, and every backticked path an entry cites that the tree no longer holds. Due entries come first, with the ones citing a missing path ahead of the rest, so a review takes the first batch rather than the whole pen. It writes nothing. See [records stale](../agents/records-stale.md) for how a path is read and the refusals.
|
|
120
|
+
|
|
118
121
|
A plan that ships is archived, never deleted. `canon tasks archive` moves it to `.canon/plans/archive/` alongside the task it belonged to and retargets that task's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. An archive sits inside the record folder it archives rather than beside it, so one ignore entry and one backed-folder entry cover a record and everything it has retired. The folder is gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
|
|
119
122
|
|
|
120
|
-
A branch review report takes the other route and is swept rather than archived. `review-branch` writes it flat into `.canon/review/` as `branch-<slug>.md`, the session addressing it reads it once, and the durable record of what a review found is the comment `review-pr` posts on the pull request, so `
|
|
123
|
+
A branch review report takes the other route and is swept rather than archived. `review-branch` writes it flat into `.canon/review/` as `branch-<slug>.md`, the session addressing it reads it once, and the durable record of what a review found is the comment `review-pr` posts on the pull request, so `context-fold` deletes any report whose branch is gone. The body that writes a report owns how long it lives, which leaves the shipping branch's own report on disk through the run that cites it and collects it a branch later. What that loses is a local-only review on a branch that never opened a pull request, which is why the report says so where a reader meets it.
|
|
121
124
|
|
|
122
|
-
`canon:
|
|
125
|
+
`canon:context-fold` decides which task closed by reading the diff rather than the conversation. It resolves a merge base against `origin/main`, unions the committed diff with the working tree and untracked files, then matches unchecked outcomes on the board against what shipped. A task that shipped without ever being discussed still gets marked. Requirements, architecture, and design stay session-sourced, because a diff cannot carry a judgment.
|
|
123
126
|
|
|
124
127
|
`.canon/tasks/` is gitignored and resolves at the main worktree root, so every session shares one board. One file per task is what keeps concurrent sessions from overwriting each other, since a gitignored board has no history to recover a lost write from. Its `index.md` is generated by a hook rather than by `bun run check`, because the whole-repo index walk skips gitignored folders.
|
|
125
128
|
|
|
@@ -164,15 +167,15 @@ The list stays written in the skill body as the fallback for a target whose inst
|
|
|
164
167
|
|
|
165
168
|
#### Memory in the chain
|
|
166
169
|
|
|
167
|
-
`git-ship` runs its verify gate and then opens on `memory-capture`, which sends what the session learned to the surface that owns it. `autoship` reaches the same step by invoking that skill at its Step 8 rather than restating the order. A fact about a domain carrying an entry in `canon/context/index.md` is routed to that entry, and `
|
|
170
|
+
`git-ship` runs its verify gate and then opens on `memory-capture`, which sends what the session learned to the surface that owns it. `autoship` reaches the same step by invoking that skill at its Step 8 rather than restating the order. A fact about a domain carrying an entry in `canon/context/index.md` is routed to that entry, and `context-fold` folds it in on the next step, so it ships in the same pull request. Anything no entry owns stays a file in `.canon/memory/`.
|
|
168
171
|
|
|
169
172
|
Capture leads rather than trails because a routed fact edits a tracked file, which has to reach the branch before the commit steps run.
|
|
170
173
|
|
|
171
|
-
The chain ends at capture, and review is run on its own.
|
|
174
|
+
The chain ends at capture, and review is run on its own. Capture closes with a line naming `/canon:memory-review` once `canon records stale memory` counts 25 or more entries due, and says nothing below that.
|
|
172
175
|
|
|
173
|
-
Run `memory-review` standalone to curate the
|
|
176
|
+
Run `memory-review` standalone to curate the pen a batch at a time. Each pass takes the first 25 due entries in the order the stale verb reports, opening on those citing a path the tree no longer holds, and a named list overrides that order. An entry it retires moves to `.canon/memory/archive/` rather than being deleted, since a bulk pass has no undo. An entry it keeps is stamped with a `reviewed` date, which takes it out of the queue for 30 days.
|
|
174
177
|
|
|
175
|
-
The receipt is collected once every item on it has been decided, and it survives untouched while any item is still pending. Whichever runs first takes it: Apply collects the receipt it has resolved, and `
|
|
178
|
+
The receipt is collected once every item on it has been decided, and it survives untouched while any item is still pending. Whichever runs first takes it: Apply collects the receipt it has resolved, and `context-fold` scans the folder on every shipped branch for one an earlier session left behind. Before the file goes, each declined item is folded into the entry it was about, since a promotion survives in its target and in git while a decline is recorded nowhere else. `canon standards memory` states what a fold writes and which entry types take one.
|
|
176
179
|
|
|
177
180
|
### UI polish
|
|
178
181
|
|
|
@@ -211,16 +214,17 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
211
214
|
|
|
212
215
|
### Decide what to build
|
|
213
216
|
|
|
214
|
-
| Skill | When to use
|
|
215
|
-
| -------------------------- |
|
|
216
|
-
| `canon:plan-intake` | When the input is a pile of findings rather than one feature
|
|
217
|
-
| `canon:plan-intake-answer` | When an intake folder holds unread slots waiting on your decision
|
|
218
|
-
| `canon:
|
|
219
|
-
| `canon:
|
|
220
|
-
| `canon:
|
|
221
|
-
| `canon:
|
|
222
|
-
| `canon:
|
|
223
|
-
| `canon:
|
|
217
|
+
| Skill | When to use |
|
|
218
|
+
| -------------------------- | --------------------------------------------------------------------------------- |
|
|
219
|
+
| `canon:plan-intake` | When the input is a pile of findings rather than one feature |
|
|
220
|
+
| `canon:plan-intake-answer` | When an intake folder holds unread slots waiting on your decision |
|
|
221
|
+
| `canon:backlog-triage` | When the backlog needs pruning, to file a verdict per row and apply approved ones |
|
|
222
|
+
| `canon:plan-groundwork` | When the state is unmeasured and more than one approach is live |
|
|
223
|
+
| `canon:decision-escalate` | When open decisions turn on your preference and want batching into one set |
|
|
224
|
+
| `canon:draft-and-pick` | When the call is taste and wants several candidates rendered side by side |
|
|
225
|
+
| `canon:task-board` | When a decided item needs a file on the board, or a shipped one needs archiving |
|
|
226
|
+
| `canon:plan-feature` | When the approach is settled and the next step is a plan |
|
|
227
|
+
| `canon:codebase-layout` | When a plan or change adds a file and you need to decide which folder holds it |
|
|
224
228
|
|
|
225
229
|
### Build the feature
|
|
226
230
|
|
|
@@ -249,17 +253,18 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
249
253
|
|
|
250
254
|
### Ship it
|
|
251
255
|
|
|
252
|
-
| Skill | When to use
|
|
253
|
-
| ---------------------- |
|
|
254
|
-
| `canon:git-ship` | To run the whole post-feature chain from the verify gate through open PR
|
|
255
|
-
| `canon:memory-capture` | First skill in that chain, to route what the session learned to the surface owning it
|
|
256
|
-
| `canon:
|
|
257
|
-
| `canon:docs-
|
|
258
|
-
| `canon:
|
|
259
|
-
| `canon:git-
|
|
260
|
-
| `canon:git-
|
|
261
|
-
| `canon:git-
|
|
262
|
-
| `canon:
|
|
256
|
+
| Skill | When to use |
|
|
257
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------- |
|
|
258
|
+
| `canon:git-ship` | To run the whole post-feature chain from the verify gate through open PR |
|
|
259
|
+
| `canon:memory-capture` | First skill in that chain, to route what the session learned to the surface owning it |
|
|
260
|
+
| `canon:context-fold` | When decisions diverged from the plan, or a shipped task needs its outcomes marked |
|
|
261
|
+
| `canon:docs-fold` | Never on purpose. A pointer at the old name that names `context-fold` <!-- canon-keep-retired --> |
|
|
262
|
+
| `canon:docs-sync` | When a change since main left `README.md` or `docs/` stale |
|
|
263
|
+
| `canon:git-stage` | When the staged set spans several concerns and wants one commit each |
|
|
264
|
+
| `canon:git-commit` | When the staged set is one concern, or was staged hunk by hand |
|
|
265
|
+
| `canon:git-branch` | When a branch name needs generating or renaming to conventional form |
|
|
266
|
+
| `canon:git-pr` | When a pull request needs a title and body written from the diff |
|
|
267
|
+
| `canon:memory-review` | When the pen has grown, to propose where each entry belongs |
|
|
263
268
|
|
|
264
269
|
### After the pull request opens
|
|
265
270
|
|
|
@@ -331,7 +336,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
331
336
|
|
|
332
337
|
Every row answers a question rather than marking a point in a project's life, so a phase above would send a reader to the wrong group.
|
|
333
338
|
|
|
334
|
-
A learning workspace produces two halves and only one of them leaves. A lesson is worked through once and stays in the workspace, and a reference page or a glossary carries no learner, so it belongs wherever the project already keeps prose on that subject. Asking `canon:teach-workspace` to promote sorts each durable page by who owns its subject, sending an Anthropic-owned subject to the wiki, an internal one to the matching context entry, and everything else, subject-neutral material included, to the public docs. It proposes and waits, because a promoted page is public prose that needs a line naming who owns the subject, and it writes nothing to a destination: each page the operator confirms goes to a handoff file that `canon:
|
|
339
|
+
A learning workspace produces two halves and only one of them leaves. A lesson is worked through once and stays in the workspace, and a reference page or a glossary carries no learner, so it belongs wherever the project already keeps prose on that subject. Asking `canon:teach-workspace` to promote sorts each durable page by who owns its subject, sending an Anthropic-owned subject to the wiki, an internal one to the matching context entry, and everything else, subject-neutral material included, to the public docs. It proposes and waits, because a promoted page is public prose that needs a line naming who owns the subject, and it writes nothing to a destination: each page the operator confirms goes to a handoff file that `canon:context-fold` folds in from a branch. A project with no wiki folder gets a refusal naming `canon wiki init` rather than a folder it never asked for.
|
|
335
340
|
|
|
336
341
|
## Feedback routing
|
|
337
342
|
|
|
@@ -218,7 +218,7 @@ coordination costs more than the change it was too small.
|
|
|
218
218
|
|
|
219
219
|
Two tiers hold work at different altitudes.
|
|
220
220
|
|
|
221
|
-
- Tasks (`.canon/tasks/`): the active few pulled into the current turn, one file each. Gitignored, high churn. Shape governed by `standards/tasks.md`. `priority.md` beside them carries execution order, and `backlog.md` carries what nobody is scheduling.
|
|
221
|
+
- Tasks (`.canon/tasks/`): the active few pulled into the current turn, one file each. Gitignored, high churn. Shape governed by `standards/tasks.md`. `priority.md` beside them carries execution order, and `backlog.md` carries what nobody is scheduling, both shaped by `standards/board.md`.
|
|
222
222
|
- Edits: a few lines, done immediately with no ceremony.
|
|
223
223
|
|
|
224
224
|
Nothing above these sequences work into versions. Scope is stated in
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Route .canon/tasks/ edits to the tasks standard for
|
|
2
|
+
description: Route .canon/tasks/ edits to the tasks standard for task files and the board standard for priority.md and backlog.md
|
|
3
3
|
paths:
|
|
4
4
|
- '.canon/tasks/**'
|
|
5
5
|
---
|
|
@@ -9,4 +9,5 @@ paths:
|
|
|
9
9
|
## Authority
|
|
10
10
|
|
|
11
11
|
- Follow the tasks standard for filenames, frontmatter, what belongs, and the task file format. It is the single source. Read it with `canon standards tasks`.
|
|
12
|
+
- Follow the board standard for `priority.md` and `backlog.md`, their readiness groups, and row order. Read it with `canon standards board`.
|
|
12
13
|
- Never hand-edit `.canon/tasks/index.md`. A hook regenerates it from sibling frontmatter.
|
|
@@ -7,5 +7,5 @@ description: Keep memory writes scoped to .canon/memory/ and out of context-owne
|
|
|
7
7
|
## Writing memory
|
|
8
8
|
|
|
9
9
|
- Write all memory files to `.canon/memory/`, not `~/.claude/projects/`
|
|
10
|
-
- A fact about a domain goes to that domain's `canon/context/` entry, not to memory. `canon:memory-capture` routes it there and `canon:
|
|
10
|
+
- A fact about a domain goes to that domain's `canon/context/` entry, not to memory. `canon:memory-capture` routes it there and `canon:context-fold` folds it in. Memory keeps only what no context entry owns. Report it rather than proceeding silently when either skill does not resolve. Both ship with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have them.
|
|
11
11
|
- Never delete a memory entry. Retire one by moving it to `.canon/memory/archive/`, which `canon records push` backs. A bulk retire runs through the shell, where no file edit fires a path-scoped rule.
|
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
52
52
|
},
|
|
53
53
|
// The performing skill wins a phrase naming its operation over the front
|
|
54
54
|
// door's own routing framing, even though canon-operator's description
|
|
55
|
-
// quotes a phrase near this one. See canon/context/cli/audits.md.
|
|
55
|
+
// quotes a phrase near this one. See canon/context/cli/audits/routing.md.
|
|
56
56
|
{
|
|
57
57
|
prompt:
|
|
58
58
|
"Whatever the right toolkit command is, get this project's rules installed.",
|
|
@@ -35,7 +35,13 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
prompt:
|
|
38
|
-
'
|
|
38
|
+
'Fold what we decided this session into the context entries and mark off what shipped on the task board.',
|
|
39
|
+
expect: 'context-fold',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
prompt:
|
|
43
|
+
'I typed the old fold skill name and it says it was renamed, where did it go?',
|
|
44
|
+
// The pointer answers under its old name on purpose. canon-keep-retired
|
|
39
45
|
expect: 'docs-fold',
|
|
40
46
|
},
|
|
41
47
|
{
|
|
@@ -104,6 +110,16 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
104
110
|
'What should a review of a change look for beyond bugs, and how much evidence does a finding need?',
|
|
105
111
|
expect: 'review-craft',
|
|
106
112
|
},
|
|
113
|
+
{
|
|
114
|
+
prompt:
|
|
115
|
+
'Go through every task sitting in the backlog and tell me which ones are still worth doing.',
|
|
116
|
+
expect: 'backlog-triage',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
prompt:
|
|
120
|
+
'Our backlog only ever grows, measure each row and suggest which to decline.',
|
|
121
|
+
expect: 'backlog-triage',
|
|
122
|
+
},
|
|
107
123
|
{
|
|
108
124
|
prompt: 'Draft me a script with beats for a screencast recording.',
|
|
109
125
|
expect: 'draft-screencast',
|
|
@@ -194,6 +210,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
194
210
|
prompt: 'Sort through this brain dump and write it up as findings.',
|
|
195
211
|
expect: 'plan-intake',
|
|
196
212
|
},
|
|
213
|
+
{
|
|
214
|
+
prompt:
|
|
215
|
+
'Here are my scattered notes from this week, file each one against the codebase with a verdict.',
|
|
216
|
+
expect: 'plan-intake',
|
|
217
|
+
},
|
|
197
218
|
{
|
|
198
219
|
prompt:
|
|
199
220
|
"Go through what's still unanswered in the intake folder and decide.",
|
package/src/commands/records.ts
CHANGED
|
@@ -24,6 +24,12 @@ import {
|
|
|
24
24
|
type SizeOutcome,
|
|
25
25
|
sizeRecords,
|
|
26
26
|
} from '@/records/size'
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_REVIEW_DAYS,
|
|
29
|
+
type StaleEntry,
|
|
30
|
+
type StaleReport,
|
|
31
|
+
staleMemory,
|
|
32
|
+
} from '@/records/stale'
|
|
27
33
|
import {
|
|
28
34
|
type Finding,
|
|
29
35
|
type FindingRemedy,
|
|
@@ -75,6 +81,13 @@ interface PruneCommandOptions extends ValidateCommandOptions {
|
|
|
75
81
|
readonly olderThan?: string
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
interface StaleCommandOptions extends ValidateCommandOptions {
|
|
85
|
+
readonly days?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The one kind `stale` reads, since only a memory entry carries a review date. */
|
|
89
|
+
const STALE_KINDS = ['memory'] as const
|
|
90
|
+
|
|
78
91
|
interface OrdinalCommandOptions extends ValidateCommandOptions {
|
|
79
92
|
readonly claim?: boolean
|
|
80
93
|
}
|
|
@@ -247,6 +260,52 @@ export function register(program: Command): void {
|
|
|
247
260
|
process.exitCode = await runSize(opts)
|
|
248
261
|
})
|
|
249
262
|
|
|
263
|
+
records
|
|
264
|
+
.command('stale')
|
|
265
|
+
.description(
|
|
266
|
+
'Report which entries are due for review and which cite paths the tree no longer holds',
|
|
267
|
+
)
|
|
268
|
+
.argument('<kind>', `Record folder: ${STALE_KINDS.join(', ')}`)
|
|
269
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
270
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
271
|
+
.option(
|
|
272
|
+
'--days <n>',
|
|
273
|
+
`Days after a review before an entry is due again (default ${DEFAULT_REVIEW_DAYS})`,
|
|
274
|
+
)
|
|
275
|
+
.option('--root <path>', 'Project root, defaulting to the main worktree')
|
|
276
|
+
.addHelpText(
|
|
277
|
+
'after',
|
|
278
|
+
[
|
|
279
|
+
'',
|
|
280
|
+
'Reads the top-level entries of the memory folder, never review/, archive/,',
|
|
281
|
+
'or index.md. An entry is due when it carries no reviewed date, a reviewed',
|
|
282
|
+
'value that is not a date, or one older than --days. A path is a backticked',
|
|
283
|
+
'token holding a / and a file extension, resolved against the project root',
|
|
284
|
+
'after any :line or #heading anchor is stripped. Tokens under .canon/,',
|
|
285
|
+
'scratch, or worktrees, and any holding a placeholder, glob, variable, or',
|
|
286
|
+
'elided ... segment, are never reported.',
|
|
287
|
+
'',
|
|
288
|
+
'Entries are ordered due first, then those citing a missing path, then the',
|
|
289
|
+
'longest unreviewed, then by name, so a review takes the first N as a batch.',
|
|
290
|
+
'',
|
|
291
|
+
'Exit codes:',
|
|
292
|
+
' 0 the reading completed, whatever it found',
|
|
293
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
294
|
+
'',
|
|
295
|
+
'It gates nothing and writes nothing. A due entry is a queue position rather',
|
|
296
|
+
'than a failure, and a missing path is a prompt for the review to judge.',
|
|
297
|
+
'',
|
|
298
|
+
'Examples:',
|
|
299
|
+
' canon records stale memory',
|
|
300
|
+
' canon records stale memory --json',
|
|
301
|
+
' canon records stale memory --days 60 --json',
|
|
302
|
+
'',
|
|
303
|
+
].join('\n'),
|
|
304
|
+
)
|
|
305
|
+
.action(async (kind: string, opts: StaleCommandOptions) => {
|
|
306
|
+
process.exitCode = await runStale(kind, opts)
|
|
307
|
+
})
|
|
308
|
+
|
|
250
309
|
records
|
|
251
310
|
.command('prune-tmp')
|
|
252
311
|
.description('Report scratch nobody has touched in a while, and delete it')
|
|
@@ -484,6 +543,93 @@ async function runPruneTmp(opts: PruneCommandOptions): Promise<number> {
|
|
|
484
543
|
return reportPrune(outcome, emitJson)
|
|
485
544
|
}
|
|
486
545
|
|
|
546
|
+
async function runStale(
|
|
547
|
+
kind: string,
|
|
548
|
+
opts: StaleCommandOptions,
|
|
549
|
+
): Promise<number> {
|
|
550
|
+
const emitJson = opts.json ?? false
|
|
551
|
+
|
|
552
|
+
if (!(STALE_KINDS as readonly string[]).includes(kind)) {
|
|
553
|
+
return reportRefusal(
|
|
554
|
+
'canon records stale',
|
|
555
|
+
{
|
|
556
|
+
reason: 'unknown-kind',
|
|
557
|
+
message: `Not a kind stale reads: ${kind}. Expected one of: ${STALE_KINDS.join(', ')}.`,
|
|
558
|
+
},
|
|
559
|
+
emitJson,
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const days = Number(opts.days ?? DEFAULT_REVIEW_DAYS)
|
|
564
|
+
|
|
565
|
+
if (!Number.isInteger(days) || days <= 0) {
|
|
566
|
+
return reportRefusal(
|
|
567
|
+
'canon records stale',
|
|
568
|
+
{
|
|
569
|
+
reason: 'bad-days',
|
|
570
|
+
message: `--days must be a positive whole number of days, got ${opts.days}.`,
|
|
571
|
+
},
|
|
572
|
+
emitJson,
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
577
|
+
const outcome = await staleMemory(root, days)
|
|
578
|
+
|
|
579
|
+
if (!outcome.ok)
|
|
580
|
+
return reportRefusal('canon records stale', outcome, emitJson)
|
|
581
|
+
|
|
582
|
+
if (emitJson) {
|
|
583
|
+
process.stdout.write(`${JSON.stringify(outcome)}\n`)
|
|
584
|
+
return 0
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
reportStale(outcome)
|
|
588
|
+
return 0
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function staleRow(entry: StaleEntry): string[] {
|
|
592
|
+
return [
|
|
593
|
+
entry.name,
|
|
594
|
+
entry.reviewed ?? (entry.invalidReviewed ? 'invalid' : 'never'),
|
|
595
|
+
entry.unresolved.join(', '),
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function reportStale(outcome: StaleReport): void {
|
|
600
|
+
const due = outcome.entries.filter((entry) => entry.due)
|
|
601
|
+
const moved = outcome.entries.filter((entry) => entry.unresolved.length > 0)
|
|
602
|
+
|
|
603
|
+
intro('canon records stale')
|
|
604
|
+
logStep('Due')
|
|
605
|
+
|
|
606
|
+
if (due.length === 0) {
|
|
607
|
+
logInfo(`none, every entry reviewed within ${plural(outcome.days, 'day')}`)
|
|
608
|
+
} else {
|
|
609
|
+
const headers = ['entry', 'reviewed', 'unresolved']
|
|
610
|
+
const rows = due.map(staleRow)
|
|
611
|
+
const widths = headers.map((header, column) =>
|
|
612
|
+
columnWidth(
|
|
613
|
+
header,
|
|
614
|
+
rows.map((row) => row[column]),
|
|
615
|
+
),
|
|
616
|
+
)
|
|
617
|
+
const render = (cells: readonly string[]): string =>
|
|
618
|
+
cells
|
|
619
|
+
.map((cell, column) => cell.padEnd(widths[column]))
|
|
620
|
+
.join(' ')
|
|
621
|
+
.trimEnd()
|
|
622
|
+
|
|
623
|
+
pipeOutput([render(headers), ...rows.map(render)].join('\n'))
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
logStep('Total')
|
|
627
|
+
logInfo(
|
|
628
|
+
`${outcome.due} of ${outcome.total} due, ${moved.length} citing a missing path, in ${outcome.folder}`,
|
|
629
|
+
)
|
|
630
|
+
outro()
|
|
631
|
+
}
|
|
632
|
+
|
|
487
633
|
function pruneRow(entry: PruneUnit): string[] {
|
|
488
634
|
return [
|
|
489
635
|
entry.path,
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -350,7 +350,9 @@ function runCheck(
|
|
|
350
350
|
|
|
351
351
|
const parsed = parseTarget(target)
|
|
352
352
|
if (parsed === undefined) {
|
|
353
|
-
logError(
|
|
353
|
+
logError(
|
|
354
|
+
'Invalid target. Use <category>:<command>, e.g. claude:context-fold.',
|
|
355
|
+
)
|
|
354
356
|
outro()
|
|
355
357
|
process.exitCode = 1
|
|
356
358
|
return
|
|
@@ -418,7 +420,7 @@ export function register(program: Command): void {
|
|
|
418
420
|
.description('Check a provisioned sandbox against a scenario expectation')
|
|
419
421
|
.argument(
|
|
420
422
|
'<target>',
|
|
421
|
-
'Scenario as <category>:<command>, e.g. claude:
|
|
423
|
+
'Scenario as <category>:<command>, e.g. claude:context-fold',
|
|
422
424
|
)
|
|
423
425
|
.argument('[arm]', 'Named scenario arm, e.g. drift')
|
|
424
426
|
.helpOption('-h, --help', 'Show this help message')
|
|
@@ -443,8 +445,8 @@ export function register(program: Command): void {
|
|
|
443
445
|
[
|
|
444
446
|
'',
|
|
445
447
|
'Examples:',
|
|
446
|
-
' canon sandbox check claude:
|
|
447
|
-
' canon sandbox check claude:
|
|
448
|
+
' canon sandbox check claude:context-fold drift',
|
|
449
|
+
' canon sandbox check claude:context-fold drift --envelope run.json --json',
|
|
448
450
|
'',
|
|
449
451
|
'Exit codes: 0 on pass or unchecked, 1 on failure.',
|
|
450
452
|
'With --strict, unchecked exits 1 as well.',
|
package/src/commands/tasks.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type DeclineOutcome,
|
|
12
12
|
declineTask,
|
|
13
13
|
type PlanCitations,
|
|
14
|
+
type PlanMove,
|
|
14
15
|
planCitations,
|
|
15
16
|
} from '@/tasks/archive'
|
|
16
17
|
import {
|
|
@@ -154,9 +155,9 @@ export function register(program: Command): void {
|
|
|
154
155
|
'after',
|
|
155
156
|
[
|
|
156
157
|
'',
|
|
157
|
-
'The task carries
|
|
158
|
-
'plan, and the archived task keeps a working
|
|
159
|
-
'path. A plan several tasks share stays where it is.',
|
|
158
|
+
'The task carries each plan its Plan: line links when no other live',
|
|
159
|
+
'task cites that plan, and the archived task keeps a working pointer',
|
|
160
|
+
'at each new path. A plan several tasks share stays where it is.',
|
|
160
161
|
'',
|
|
161
162
|
'The ready folder the task names moves to .canon/ready/archive/ with its',
|
|
162
163
|
'plan, and the Ready: line and the plan path to it are retargeted. A',
|
|
@@ -260,6 +261,7 @@ export function register(program: Command): void {
|
|
|
260
261
|
'',
|
|
261
262
|
'Locations:',
|
|
262
263
|
' unstated the task carries no Plan: line',
|
|
264
|
+
' several the Plan: line links more than one plan, named in targets',
|
|
263
265
|
' live the target resolves inside .canon/plans/',
|
|
264
266
|
' archived the target resolves inside .canon/plans/archive/',
|
|
265
267
|
' outside the target resolves somewhere else',
|
|
@@ -448,12 +450,14 @@ export function register(program: Command): void {
|
|
|
448
450
|
'',
|
|
449
451
|
'Exit codes:',
|
|
450
452
|
' 0 the line was added, corrected, or already correct',
|
|
451
|
-
' 1 refused,
|
|
453
|
+
' 1 refused, including several-plans for a line linking more than one',
|
|
454
|
+
' plan, with the reason on stderr or in the JSON record',
|
|
452
455
|
'',
|
|
453
456
|
'It writes Plan: [<label>](<target>) right after the H1, corrects the',
|
|
454
|
-
'target in place when the line exists
|
|
455
|
-
'and a board-relative path the way canon
|
|
456
|
-
'from a linked worktree, since it
|
|
457
|
+
'target in place when the line exists and names the one it replaced,',
|
|
458
|
+
'and resolves both a bare slug and a board-relative path the way canon',
|
|
459
|
+
'tasks plan-answers does. Safe from a linked worktree, since it',
|
|
460
|
+
'resolves the board root in-process.',
|
|
457
461
|
'',
|
|
458
462
|
'Examples:',
|
|
459
463
|
' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
|
|
@@ -793,6 +797,7 @@ function reportPlanLink(
|
|
|
793
797
|
path: relative(root, outcome.path),
|
|
794
798
|
plan: outcome.plan,
|
|
795
799
|
action: outcome.action,
|
|
800
|
+
replaced: outcome.replaced,
|
|
796
801
|
})}\n`,
|
|
797
802
|
)
|
|
798
803
|
return 0
|
|
@@ -801,6 +806,7 @@ function reportPlanLink(
|
|
|
801
806
|
intro('canon tasks plan-link')
|
|
802
807
|
logStep(outcome.action === 'unchanged' ? 'Already recorded' : 'Recorded')
|
|
803
808
|
logInfo(`${outcome.stem} names plan ${outcome.plan}`)
|
|
809
|
+
if (outcome.replaced) logInfo(`replaced ${outcome.replaced}`)
|
|
804
810
|
if (outcome.action !== 'unchanged') logAdd(relative(root, outcome.path))
|
|
805
811
|
outro()
|
|
806
812
|
|
|
@@ -975,6 +981,10 @@ function reportCitations(
|
|
|
975
981
|
function describeCitations(outcome: PlanCitations): string {
|
|
976
982
|
if (outcome.location === 'unstated') return 'carries no Plan: line.'
|
|
977
983
|
|
|
984
|
+
if (outcome.location === 'several') {
|
|
985
|
+
return `links ${outcome.targets.length} plans (${outcome.targets.join(', ')}), so no single plan is the sweep's to settle.`
|
|
986
|
+
}
|
|
987
|
+
|
|
978
988
|
if (outcome.location === 'archived') {
|
|
979
989
|
return `points at ${outcome.target}, which an earlier sweep already archived.`
|
|
980
990
|
}
|
|
@@ -1396,11 +1406,7 @@ function report(
|
|
|
1396
1406
|
logStep('Archived')
|
|
1397
1407
|
logRemove(relative(root, outcome.from))
|
|
1398
1408
|
logAdd(relative(root, outcome.to))
|
|
1399
|
-
|
|
1400
|
-
logRemove(relative(root, outcome.plan.from))
|
|
1401
|
-
logAdd(relative(root, outcome.plan.to))
|
|
1402
|
-
logInfo('retargeted the Plan: line')
|
|
1403
|
-
}
|
|
1409
|
+
logPlanMoves(outcome.plans, root)
|
|
1404
1410
|
if (outcome.ready) {
|
|
1405
1411
|
logRemove(relative(root, outcome.ready.from))
|
|
1406
1412
|
logAdd(relative(root, outcome.ready.to))
|
|
@@ -1414,6 +1420,24 @@ function report(
|
|
|
1414
1420
|
return 0
|
|
1415
1421
|
}
|
|
1416
1422
|
|
|
1423
|
+
function logPlanMoves(plans: readonly PlanMove[], root: string): void {
|
|
1424
|
+
for (const plan of plans) {
|
|
1425
|
+
logRemove(relative(root, plan.from))
|
|
1426
|
+
logAdd(relative(root, plan.to))
|
|
1427
|
+
}
|
|
1428
|
+
if (plans.length > 0) logInfo('retargeted the Plan: line')
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function planMovesFor(
|
|
1432
|
+
plans: readonly PlanMove[],
|
|
1433
|
+
root: string,
|
|
1434
|
+
): readonly { readonly from: string; readonly to: string }[] {
|
|
1435
|
+
return plans.map((plan) => ({
|
|
1436
|
+
from: relative(root, plan.from),
|
|
1437
|
+
to: relative(root, plan.to),
|
|
1438
|
+
}))
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1417
1441
|
function recordFor(
|
|
1418
1442
|
outcome: ArchiveOutcome,
|
|
1419
1443
|
root: string,
|
|
@@ -1434,12 +1458,7 @@ function recordFor(
|
|
|
1434
1458
|
to: relative(root, outcome.to),
|
|
1435
1459
|
priorityRowRemoved: outcome.priorityRowRemoved,
|
|
1436
1460
|
indexRegenerated: outcome.indexRegenerated,
|
|
1437
|
-
|
|
1438
|
-
? {
|
|
1439
|
-
from: relative(root, outcome.plan.from),
|
|
1440
|
-
to: relative(root, outcome.plan.to),
|
|
1441
|
-
}
|
|
1442
|
-
: null,
|
|
1461
|
+
plans: planMovesFor(outcome.plans, root),
|
|
1443
1462
|
ready: outcome.ready
|
|
1444
1463
|
? {
|
|
1445
1464
|
from: relative(root, outcome.ready.from),
|
|
@@ -1537,11 +1556,7 @@ function reportDecline(
|
|
|
1537
1556
|
logStep('Declined')
|
|
1538
1557
|
logRemove(relative(root, outcome.from))
|
|
1539
1558
|
logAdd(relative(root, outcome.to))
|
|
1540
|
-
|
|
1541
|
-
logRemove(relative(root, outcome.plan.from))
|
|
1542
|
-
logAdd(relative(root, outcome.plan.to))
|
|
1543
|
-
logInfo('retargeted the Plan: line')
|
|
1544
|
-
}
|
|
1559
|
+
logPlanMoves(outcome.plans, root)
|
|
1545
1560
|
if (outcome.priorityRowRemoved) logInfo('cleared the ordering row')
|
|
1546
1561
|
if (outcome.backlogRowRemoved) logInfo('cleared the backlog row')
|
|
1547
1562
|
if (outcome.indexRegenerated) logInfo('regenerated index.md')
|
|
@@ -1571,11 +1586,6 @@ function declineRecordFor(
|
|
|
1571
1586
|
priorityRowRemoved: outcome.priorityRowRemoved,
|
|
1572
1587
|
backlogRowRemoved: outcome.backlogRowRemoved,
|
|
1573
1588
|
indexRegenerated: outcome.indexRegenerated,
|
|
1574
|
-
|
|
1575
|
-
? {
|
|
1576
|
-
from: relative(root, outcome.plan.from),
|
|
1577
|
-
to: relative(root, outcome.plan.to),
|
|
1578
|
-
}
|
|
1579
|
-
: null,
|
|
1589
|
+
plans: planMovesFor(outcome.plans, root),
|
|
1580
1590
|
}
|
|
1581
1591
|
}
|
|
@@ -182,7 +182,7 @@ export function sectionAround(
|
|
|
182
182
|
* requested canonical doc types.
|
|
183
183
|
*
|
|
184
184
|
* Reads the working tree rather than `HEAD`, matching `listChangedFiles`: a
|
|
185
|
-
* `
|
|
185
|
+
* `context-fold` run checking a session's own edits sees them before they are
|
|
186
186
|
* committed. A file the range deleted contributes nothing, since there is no
|
|
187
187
|
* current section left to read a hunk's chunk against.
|
|
188
188
|
*/
|
|
@@ -14,7 +14,7 @@ export const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434'
|
|
|
14
14
|
* 600s ceiling meant for a batch research run, not a per-call budget for an
|
|
15
15
|
* interactive verb. 30s is chosen here as a deviation, wide enough that a
|
|
16
16
|
* shared GPU under another local session's load (measured coexisting at 12GB)
|
|
17
|
-
* still has room, while short enough that a `
|
|
17
|
+
* still has room, while short enough that a `context-fold` run does not hang
|
|
18
18
|
* indefinitely on a backend that stopped responding mid-call.
|
|
19
19
|
*/
|
|
20
20
|
export const OLLAMA_TIMEOUT_MS = 30_000
|
|
@@ -165,7 +165,7 @@ interface ModelLayerPlan {
|
|
|
165
165
|
* Decides whether the model layer runs, probing reachability before any real
|
|
166
166
|
* call. A configured-but-unreachable backend is a plan the caller reports and
|
|
167
167
|
* falls back from, never a refusal: the groundwork decision reads "warn and
|
|
168
|
-
* continue," and a refused run here would fail `
|
|
168
|
+
* continue," and a refused run here would fail `context-fold` for a reason that
|
|
169
169
|
* has nothing to do with the branch it is checking.
|
|
170
170
|
*/
|
|
171
171
|
async function planModelLayer(
|
package/src/gate/measures.ts
CHANGED
|
@@ -1242,7 +1242,7 @@ export const pluginManifests: Measure = async (ctx) => {
|
|
|
1242
1242
|
return {
|
|
1243
1243
|
emissions: [],
|
|
1244
1244
|
unmeasured:
|
|
1245
|
-
'claude is on PATH and claude --version fails, so the install brought down no platform-native binary and no manifest was read. Raise or lower the pinned version at the Install Plugin CLI step in .github/workflows/verify.yml, and record the move in canon/context/ci.md.',
|
|
1245
|
+
'claude is on PATH and claude --version fails, so the install brought down no platform-native binary and no manifest was read. Raise or lower the pinned version at the Install Plugin CLI step in .github/workflows/verify.yml, and record the move in canon/context/ci/checks.md.',
|
|
1246
1246
|
}
|
|
1247
1247
|
}
|
|
1248
1248
|
|