@erclx/canon 4.85.0 → 4.86.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 +1 -1
- package/claude/skills/docs-fold/SKILL.md +1 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +2 -1
- package/claude/skills/draft-and-pick/SKILL.md +3 -2
- package/claude/skills/draft-and-pick/references/live-arms.md +19 -0
- package/claude/skills/git-ship/SKILL.md +1 -1
- package/claude/skills/memory-capture/SKILL.md +1 -1
- package/claude/skills/memory-review/SKILL.md +13 -13
- package/claude/skills/memory-review/references/receipt-format.md +1 -1
- package/claude/skills/role-worker/SKILL.md +2 -1
- package/claude/skills/ux-walkthrough/SKILL.md +2 -2
- package/claude/skills/ux-walkthrough/references/candidate-pages.md +1 -16
- package/docs/agents/commands.md +3 -0
- package/docs/workflow/ai-workflow.md +1 -1
- package/governance/rules/claude/563-ready.md +11 -0
- package/governance/rules/core/045-memory.md +1 -1
- package/package.json +1 -1
- package/src/commands/migrate.ts +142 -0
- package/src/migrate/record-layout.ts +346 -0
- package/src/migrate/record-tree.ts +1 -1
- package/src/record-root.ts +1 -0
- package/standards/index.md +1 -0
- package/standards/memory.md +1 -1
- package/standards/plan.md +2 -0
- package/standards/ready.md +105 -0
- package/standards/skill.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +10 -0
|
@@ -212,7 +212,7 @@ Respond with up to five lines:
|
|
|
212
212
|
<N minor findings kept in .canon/review/branch/review-<slug>.md>
|
|
213
213
|
<N facts routed to context entries>
|
|
214
214
|
<N memories captured in .canon/memory/>
|
|
215
|
-
<Memory proposal at .canon/review/memory
|
|
215
|
+
<Memory proposal at .canon/memory/review/memory-review-<slug>.md>
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
`<state>` is whatever the Step 8 read returned, being `draft` or `ready, unsupervised`, rather than the state the undo asked for. Writing the word `draft` there unconditionally is what this line used to do, and it named a state no step had read.
|
|
@@ -228,7 +228,7 @@ Sweep the branch reports this session never opened. List `.canon/review/branch/r
|
|
|
228
228
|
|
|
229
229
|
What that removes is a local-only review on a branch deleted before it opened a pull request. `review-branch` says so where a reader meets the report, and the sweep runs anyway rather than keeping every report against the one case, since nothing else ever clears them.
|
|
230
230
|
|
|
231
|
-
Memory receipts sweep board-wide rather than by slug. Scan every `.canon/review/memory
|
|
231
|
+
Memory receipts sweep board-wide rather than by slug. Scan every `.canon/memory/review/memory-review-*.md`, not only the one matching this slug. `memory-review` writes its receipt after this skill has run in every ship chain, so a sweep keyed on the current slug looks for a file that does not exist yet, and no later branch looks for it either because a slug is unique per feature. Scanning the folder is what makes the sweep fire at all.
|
|
232
232
|
|
|
233
233
|
For each receipt, count the H2 items still marked 📝 pending:
|
|
234
234
|
|
|
@@ -19,11 +19,12 @@ Without this skill, a session facing a decision nobody can settle from a diff:
|
|
|
19
19
|
- Reads a driver record as a verdict and drops an arm on it. `canon drive` reports findings and never gates, so a run filtering the arms on a probe reading has made a claim the probe catalog has not earned.
|
|
20
20
|
- Reads a color without compositing its alpha, samples a round control at the corner of its bounding box, or confirms a panel's dimensions while it sits above the viewport. Each reads as a passing measurement about something no reader sees.
|
|
21
21
|
- Meets a machine with no browser binary and reports on the arms anyway, since nothing in the default flow separates a render that failed from one that was never attempted.
|
|
22
|
+
- Hand-writes a mockup of a surface a running app already serves, which drifts from what ships by a few pixels and a few words the moment the app's own markup or stylesheet moves on.
|
|
22
23
|
|
|
23
24
|
## Must
|
|
24
25
|
|
|
25
26
|
- Produce three to five arms with the shipping state among them, each carrying an id, a label, and what the arm costs.
|
|
26
|
-
- Author the whole candidate set as one self-contained page and render it once, so the comparison arrives as one image.
|
|
27
|
+
- Author the whole candidate set as one self-contained page and render it once, so the comparison arrives as one image, except where the surface under decision is a running app: there the page links a copy of the built stylesheet rather than inlining it, per `references/live-arms.md`.
|
|
27
28
|
- Vary one property across the arms, so the answer names a difference.
|
|
28
29
|
- Render before reporting on any arm, and report a missing browser binary as a refusal naming the repair rather than describing an arm nobody has seen.
|
|
29
30
|
- Take the pick through the structured question surface, with the recommendation ranked first and marked, and every option carrying its cost.
|
|
@@ -28,8 +28,9 @@ Write every arm side by side on one self-contained HTML page at `<dest>/candidat
|
|
|
28
28
|
- One page for the pick, never a set of separate images handed to the operator to compare from memory. The comparison they judge is `candidates.html`, which Step 3 renders and Step 4 asks about. The per-arm files exist only for Step 6's archival capture, once the pick is made, and only the last pass through this step is what Step 6 finds there.
|
|
29
29
|
- Wrap each arm's markup in the same class on both files, chosen once per run and reused everywhere, so one selector addresses an arm on the combined page and on its own standalone file alike.
|
|
30
30
|
- Label each arm on the page with its id and its cost, so the render carries what the question will ask about.
|
|
31
|
-
-
|
|
32
|
-
-
|
|
31
|
+
- Take the live-app branch instead when the surface under decision is a running app: lift the rendered markup and link a copy of the built stylesheet rather than inlining, per `${CLAUDE_SKILL_DIR}/references/live-arms.md`.
|
|
32
|
+
- On the default path, inline every style, script, and asset the page needs. The render reads the file off disk, so a page reaching for a build step or a network font renders without it and the arms differ by something nobody chose.
|
|
33
|
+
- On the default path, declare a font stack the machine resolves, such as `system-ui` behind a generic fallback. The render refuses a page that would rewrap against a substitute rather than shipping a false comparison, so a page naming no font at all is refused on whatever the default resolves to.
|
|
33
34
|
- Vary one property across the arms. A page whose arms differ in three ways answers no question, since the pick cannot say which difference decided it.
|
|
34
35
|
|
|
35
36
|
## Step 3: render and hand off
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Live app arms
|
|
2
|
+
|
|
3
|
+
Read when the surface under decision is a running app rather than a static mockup: the operator's request names a route or URL of the project's own app, or the decision is about a surface a build command serves. Skip it for a decision with nothing running to lift from, which stays on Step 2's default inlined path.
|
|
4
|
+
|
|
5
|
+
## Lift from the app
|
|
6
|
+
|
|
7
|
+
- Dump the rendered markup of the surface under decision from the built page, after it has loaded, into a file under `<dest>`. Strip scripts from the dump.
|
|
8
|
+
- Copy the built stylesheet and font files beside the page, at the path the dumped markup links them from, so a candidate page renders with the stylesheet the app ships. Every arm then differs from the shipped page only by what the arm names.
|
|
9
|
+
- Trim a large dump to the part the decision needs, and keep the wrapper classes intact so layout rules keyed to them still resolve.
|
|
10
|
+
- Size each frame to the content width the decision names rather than the window width, since a container query reads the frame.
|
|
11
|
+
- Write copy an arm introduces with `write-human`, and take every other word from the dump.
|
|
12
|
+
|
|
13
|
+
## The page
|
|
14
|
+
|
|
15
|
+
- Generate the page with a short script, one per round, so a shared change is one edit and a rerun.
|
|
16
|
+
- Put a theme button on the page that flips the app's own theme switch on the root, rather than drawing each frame once per theme. Read `?arm=<id>&theme=<name>` to strip every other arm and the button for capture. Skip the button for an app that ships one theme.
|
|
17
|
+
- Print each frame's measurement under it from a script in the page, so the numbers the operator reads are the browser's.
|
|
18
|
+
- Write an index page linking one page per arm when an arm is a whole page or a route rather than a frame, with thumbnails copied beside it.
|
|
19
|
+
- Name anything the preview cannot reproduce, such as an asset whose colors follow the browser rather than the page's theme button, rather than fixing the page around it.
|
|
@@ -58,7 +58,7 @@ Output up to four lines:
|
|
|
58
58
|
✅ Shipped
|
|
59
59
|
<N facts routed to context entries>
|
|
60
60
|
<N memories captured in .canon/memory/>
|
|
61
|
-
<Memory proposal at .canon/review/memory
|
|
61
|
+
<Memory proposal at .canon/memory/review/memory-review-<slug>.md>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
Omit the second line if nothing routed. Omit the third and fourth if `memory-capture` wrote no memory file this session, since an empty pen means no scoped review and no proposal.
|
|
@@ -68,7 +68,7 @@ The handoff is a file rather than a spoken result so the routed fact survives a
|
|
|
68
68
|
|
|
69
69
|
## Step 4: dedupe
|
|
70
70
|
|
|
71
|
-
For each remaining candidate, grep `.canon/memory/` for an existing file on the same topic. If one exists, update it in place rather than create a new file. Read it first and write the whole file back, since the guard above rules out editing a line inside it.
|
|
71
|
+
For each remaining candidate, grep the top-level `*.md` files in `.canon/memory/` for an existing file on the same topic, never its `review/` or `archive/` subfolders, since a hit there is a receipt or a retired entry rather than a live one to update. If one exists, update it in place rather than create a new file. Read it first and write the whole file back, since the guard above rules out editing a line inside it.
|
|
72
72
|
|
|
73
73
|
## Step 5: write the residue
|
|
74
74
|
|
|
@@ -5,7 +5,7 @@ description: Reviews `.canon/memory/` and proposes per-entry actions (promote to
|
|
|
5
5
|
|
|
6
6
|
# Memory review
|
|
7
7
|
|
|
8
|
-
This skill drives the full memory review lifecycle in five phases. Pick the phase from what the user said and whether a review receipt already exists at `<main-root>/.canon/review/memory
|
|
8
|
+
This skill drives the full memory review lifecycle in five phases. Pick the phase from what the user said and whether a review receipt already exists at `<main-root>/.canon/memory/review/memory-review-*.md`.
|
|
9
9
|
|
|
10
10
|
What an entry looks like and why a retired one is moved rather than deleted are fixed by `${CLAUDE_SKILL_DIR}/../../standards/memory.md`. Read it before rewriting an entry, since a promotion rewrites the rule and a rewrite has to leave the entry conforming.
|
|
11
11
|
|
|
@@ -23,7 +23,7 @@ If the user re-pings the skill with no new phrase and a receipt exists, default
|
|
|
23
23
|
|
|
24
24
|
- All `.canon/memory/` reads, edits, and archive moves resolve at the main worktree root, not the current worktree. Resolve that root the way `session-worktree` does.
|
|
25
25
|
- If no `.canon/memory/` directory exists at the main worktree root, stop: `❌ No .canon/memory/ directory found.`
|
|
26
|
-
- If `.canon/memory/` contains no `*.md` entries other than `index.md`, stop: `✅ No memory entries to review.`
|
|
26
|
+
- If `.canon/memory/` contains no top-level `*.md` entries other than `index.md`, stop: `✅ No memory entries to review.` The pen holds two subfolders now, `review/` and `archive/`, and neither is a memory entry, so this count and every entry read below stay at the top level and never recurse into either.
|
|
27
27
|
- Cleanup is exempt from the two stops above. It works on receipts in `.canon/review/`, and a drained pen is the normal state once Apply has run, so a pen-shaped stop would strand the receipt it exists to delete.
|
|
28
28
|
- Resolve the main root via `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`. All review and memory reads anchor here.
|
|
29
29
|
- From a linked worktree the file-editing tools refuse every main-root path, so each write below goes out through `Bash` as a plain single command. The receipt and a memory entry are both short and this session has read them whole, so a rewrite replaces the file with a heredoc rather than editing a line inside it. Promotion targets are tracked files at `pwd` and keep taking `Edit`.
|
|
@@ -42,7 +42,7 @@ Propose is the ship-time entry point. The ship skills run it right after capture
|
|
|
42
42
|
Read in parallel from the project root:
|
|
43
43
|
|
|
44
44
|
- `.canon/memory/index.md`: the generated index
|
|
45
|
-
- every other `*.md` file
|
|
45
|
+
- every other top-level `*.md` file in `.canon/memory/`, never its `review/` or `archive/` subfolders: individual entries with frontmatter (`title`, `description`, `category`)
|
|
46
46
|
|
|
47
47
|
### Step 2: read promotion targets
|
|
48
48
|
|
|
@@ -72,7 +72,7 @@ For each in-scope entry (see Scope), pick one action:
|
|
|
72
72
|
- **Hand off to governance**: the rule is coding-standards class (typescript, testing, naming, error-handling, performance, logging, concurrency, planning), never a cross-domain behavior rule. Do not author the rule file inline. Never edit the synced `.claude/rules/` copies of toolkit rules, because `canon gov sync` overwrites them. Stop at handoff. This and **Promote to an always-loaded rule** never both claim one entry: class names the topic (coding-standards routes here), firing axis names the rest (applies-every-session routes to the rule promote).
|
|
73
73
|
- In the toolkit repo, point the user at `internal-governance` and `${CLAUDE_SKILL_DIR}/../../standards/rule.md`, which own the source-of-truth rules under `governance/rules/`.
|
|
74
74
|
- In a target project, point the user at the `create-rule` skill, which scaffolds a project-local rule under `.claude/rules/`.
|
|
75
|
-
- **Retire**: the rule is stale, already absorbed into a durable surface, too vague to phrase as a rule, or a one-time incident narrative. Apply moves the file to `.canon/
|
|
75
|
+
- **Retire**: the rule is stale, already absorbed into a durable surface, too vague to phrase as a rule, or a one-time incident narrative. Apply moves the file to `.canon/memory/archive/` rather than deleting it.
|
|
76
76
|
|
|
77
77
|
Retire is an archive, not a deletion, which `${CLAUDE_SKILL_DIR}/../../standards/memory.md` states as the rule and this skill executes. The archive is worth less than a plan's, since a promoted entry survives in its destination and a stale one is discarded on purpose, which is why the move is cheap rather than free.
|
|
78
78
|
|
|
@@ -92,11 +92,11 @@ Rules that resist crisp one-line phrasing default to **Retire** over promote. Ne
|
|
|
92
92
|
|
|
93
93
|
Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. Fall back to `latest` on an empty result.
|
|
94
94
|
|
|
95
|
-
Write the full proposal to `.canon/review/memory
|
|
95
|
+
Write the full proposal to `.canon/memory/review/memory-review-<slug>.md` at the main worktree root. Do not print it inline. Read `${CLAUDE_SKILL_DIR}/references/receipt-format.md` for the file structure, the item template, and how each action type varies the body. The four phases below rewrite items inside an existing receipt rather than authoring one, so none of them opens it.
|
|
96
96
|
|
|
97
97
|
A phase changing items reads the receipt, applies every change for that phase, and writes the whole file back in one command. Batching is what keeps a per-item rewrite from costing a full read each time, and it is the only route from a linked worktree, where the guard above rules out editing a line in place.
|
|
98
98
|
|
|
99
|
-
Tell the user `✅ Wrote proposal to .canon/review/memory
|
|
99
|
+
Tell the user `✅ Wrote proposal to .canon/memory/review/memory-review-<slug>.md`. Ask them to fill in `Decision:` per item, then re-ping with "discuss" for question rounds or "apply" to commit.
|
|
100
100
|
|
|
101
101
|
Rewrite the review file in place whenever the proposal changes mid-review. The file stays the source of truth for the current decisions.
|
|
102
102
|
|
|
@@ -104,7 +104,7 @@ Rewrite the review file in place whenever the proposal changes mid-review. The f
|
|
|
104
104
|
|
|
105
105
|
Trigger: user says "challenge the promotes", "challenge before apply", or asks for a high-bar pass. Run before Apply. No mutations to memory files or promotion targets. Review file only.
|
|
106
106
|
|
|
107
|
-
1. Read the latest `.canon/review/memory
|
|
107
|
+
1. Read the latest `.canon/memory/review/memory-review-*.md` at the main root.
|
|
108
108
|
2. For each promote item, apply three tests:
|
|
109
109
|
- **Absorbed**: grep the target surface for the rule's keywords. If already stated or implied, flip to retire.
|
|
110
110
|
- **Delta**: if the rule is a nice-to-have next to existing bullets, flip to retire.
|
|
@@ -115,7 +115,7 @@ Trigger: user says "challenge the promotes", "challenge before apply", or asks f
|
|
|
115
115
|
|
|
116
116
|
Trigger: user says "discuss", "respond to questions", or any `Decision:` value contains `?` or an unrecognized verb. No mutations to memory files or targets. Review file only. Multi-round.
|
|
117
117
|
|
|
118
|
-
1. Read the latest `.canon/review/memory
|
|
118
|
+
1. Read the latest `.canon/memory/review/memory-review-*.md` at the main root.
|
|
119
119
|
2. For each item whose `Decision:` contains `?` or any unrecognized verb (anything other than `apply`, `skip`, `defer`):
|
|
120
120
|
- Write a `Take:` line under `Decision:`, separated by exactly one blank line. If a `Take:` line already exists, overwrite it.
|
|
121
121
|
- Format: pick + one-line reason. Max 2 sentences. Decision-help style. State the recommendation (`apply` / `skip` / `retire` / specific alternative) first, then the reason. Do not enumerate tradeoffs unless one changes the call.
|
|
@@ -157,7 +157,7 @@ Action by action type:
|
|
|
157
157
|
- **Hand off**: do not edit governance. Archive the memory file only if the user confirmed the handoff explicitly. Otherwise leave it in place.
|
|
158
158
|
- **Retire**: archive the memory file.
|
|
159
159
|
|
|
160
|
-
Archiving means creating `.canon/
|
|
160
|
+
Archiving means creating `.canon/memory/archive/` at the main worktree root and moving the file there under its original name, overwriting any file already at that name. Send the `mkdir -p` and the `mv` as two plain commands rather than joining them with `&&`, which is refused as compound from a linked worktree. Never delete a memory entry. Nothing recovers one from a gitignored folder.
|
|
161
161
|
|
|
162
162
|
Do not hand-edit `.canon/memory/index.md`. Once every archive move is done, regenerate it instead:
|
|
163
163
|
|
|
@@ -189,11 +189,11 @@ End with: `✅ Applied: <nums> | ⏭ Skipped: <nums> | 📝 Pending: <nums>`. Om
|
|
|
189
189
|
|
|
190
190
|
Trigger: user says "cleanup" or "delete the receipt" after Apply has run.
|
|
191
191
|
|
|
192
|
-
Cleanup folds one receipt's skips and removes that receipt, and does nothing else. It is the fallback route now that Apply and `docs-fold` Step 10 each collect a resolved receipt on their own, so it reaches a file those two left behind rather than being the only collector. Apply is still the only phase that moves a memory entry out of the pen, and it does so per approved item into `.canon/
|
|
192
|
+
Cleanup folds one receipt's skips and removes that receipt, and does nothing else. It is the fallback route now that Apply and `docs-fold` Step 10 each collect a resolved receipt on their own, so it reaches a file those two left behind rather than being the only collector. Apply is still the only phase that moves a memory entry out of the pen, and it does so per approved item into `.canon/memory/archive/`. A user asking to sweep stale memories wants Propose, which classifies entries and writes a decision slot per entry.
|
|
193
193
|
|
|
194
|
-
If no `.canon/review/memory
|
|
194
|
+
If no `.canon/memory/review/memory-review-*.md` exists at the main root, stop: `✅ No review receipt to clean up.` Every other refusal in this skill carries a message, and the phase reads a receipt before it does anything else.
|
|
195
195
|
|
|
196
|
-
1. Read the latest `.canon/review/memory
|
|
196
|
+
1. Read the latest `.canon/memory/review/memory-review-*.md` at the main root and confirm Apply has run against it. If any item is still 📝 pending, stop and name the pending numbers.
|
|
197
197
|
2. Collect it per the collection rule in `${CLAUDE_SKILL_DIR}/../../standards/memory.md`, folding each ⏭ skipped item before the file goes. The fold happens wherever a receipt is collected, so this phase runs the same rule the Apply sweep does.
|
|
198
198
|
3. Delete that one file. Leave every other receipt beside it in place, because the pending test above covers the file it read and nothing has tested the rest.
|
|
199
199
|
4. Leave every memory entry in the pen. A skip records the decline on the entry and keeps the file, and applied promotions, governance handoffs, and user-type memories each stay as the review left them.
|
|
@@ -206,7 +206,7 @@ Output one line per action taken in the most recent phase:
|
|
|
206
206
|
|
|
207
207
|
- `✅ Promoted: .canon/memory/<memory-file> → <target>`
|
|
208
208
|
- `✅ Handed off: .canon/memory/<memory-file> → governance`
|
|
209
|
-
- `📦 Retired: .canon/memory/<memory-file> → .canon/
|
|
209
|
+
- `📦 Retired: .canon/memory/<memory-file> → .canon/memory/archive/`
|
|
210
210
|
- `🗑 Swept: .canon/review/<review-file>, folded <n> skips`
|
|
211
211
|
- `⏭ Kept: .canon/review/<review-file>, <n> items pending`
|
|
212
212
|
|
|
@@ -5,7 +5,7 @@ description: The proposal file structure, its item template, and how each action
|
|
|
5
5
|
|
|
6
6
|
# Memory review receipt format
|
|
7
7
|
|
|
8
|
-
The shape Step 4 of the Propose phase writes to `.canon/review/memory
|
|
8
|
+
The shape Step 4 of the Propose phase writes to `.canon/memory/review/memory-review-<slug>.md`. Only Propose writes this file. Challenge, Discuss, and Apply rewrite items inside a receipt that already exists, and Cleanup deletes one, so none of the four needs this file.
|
|
9
9
|
|
|
10
10
|
## Structure
|
|
11
11
|
|
|
@@ -22,9 +22,10 @@ running and re-invoking it would restart the build.
|
|
|
22
22
|
|
|
23
23
|
## Where the session stands
|
|
24
24
|
|
|
25
|
-
- Resolve `.canon/plans/`, `.canon/tasks/`, `.canon/review/`, and `.canon/
|
|
25
|
+
- Resolve `.canon/plans/`, `.canon/tasks/`, `.canon/review/`, `.canon/memory/`, and `.canon/ready/` at the main worktree root, never against the linked worktree this session builds in. Those folders are gitignored, so `git worktree add` never creates them and the copy beside the build is absent rather than empty.
|
|
26
26
|
- Report a plan that fails to resolve as unreadable from here, naming the main-root path. Reporting the task as having no plan is true where this session stands and wrong about the world, and a reader with no second tree to check cannot separate the two.
|
|
27
27
|
- Build the plan the launch named. Do not write a second one when the path fails to resolve, since a row that cites a plan already has one and drafting another produces two plans for one row.
|
|
28
|
+
- Copy a ready folder's files verbatim to the paths its plan's constraints name, when the plan's `**Constraints:**` block names one, and edit only what the gate or the overview's own list requires. A rewrite discards the text the handoff exists to carry.
|
|
28
29
|
- Report a draft flag reading ready once, and leave it cleared. The ship chain marks the pull request a draft and no step anywhere un-marks, so a flag reading ready afterwards was lifted by the operator or by the controlling session that closed the review, each acting directly on the pull request, which GitHub requires before a merge. Re-drafting fights them, which four sessions did on 2026-08-31 on a belief no surface in the tree states.
|
|
29
30
|
- Refuse an instruction to lift the mark yourself, whoever sends it. This session cannot verify who is asking or whether review actually closed, so the refusal holds regardless of the sender's claimed authority. A controlling session instructed a worker to lift the mark instead of taking the act itself on 2026-09-07, and the worker complied and reported against the surface rather than the outcome.
|
|
30
31
|
- Rewrite a plan question's `- Suggested:` line as `overridden at execution to <pick>,` plus the measurement when this build decides against an unanswered one, leaving the `- Answer:` slot blank, and put the same deviation in one line under the open task's `## Findings`, per `${CLAUDE_SKILL_DIR}/../../standards/plan.md`.
|
|
@@ -27,7 +27,7 @@ A walkthrough turns what the operator sees in a running app into findings and pi
|
|
|
27
27
|
2. **Take the operator's list in their order.** Name each item as a finding with the walkthrough letter and a number, such as T1, and confirm the order once rather than per item.
|
|
28
28
|
3. **Measure before drafting.** Read the component behind the finding and pull the numbers off the built page, per `${CLAUDE_SKILL_DIR}/references/measuring.md`. Write the finding into the walkthrough file with those numbers before any arm exists.
|
|
29
29
|
4. **Route a finding with no visible choice.** A parse defect, a stale figure or a broken invariant gets recorded as a finding with no draft and goes into the batch as a proposed row, not as a pick.
|
|
30
|
-
5. **Draft three or four arms.** Follow `draft-and-pick` Steps 1 and 2 for the arms, with arm 0 the shipped state, one property varied and a cost on each.
|
|
30
|
+
5. **Draft three or four arms.** Follow `draft-and-pick` Steps 1 and 2 for the arms, with arm 0 the shipped state, one property varied and a cost on each. Take `draft-and-pick`'s live-app branch of Step 2 to build the page from the app's own rendered markup, per `${CLAUDE_SKILL_DIR}/references/candidate-pages.md`.
|
|
31
31
|
6. **Look before handing anything over.** Capture every arm in every theme the app ships into `.canon/walkthroughs/<nn>-<slug>/evidence/<nn>-<slug>/`, with the finding's number as the prefix, open the captures, and fix what rendered wrong before the operator sees the page.
|
|
32
32
|
7. **Hand the link, then ask.** Emit `http://localhost:<port>/<nn>-<slug>/candidates.html` in a message that ends the turn, confirmed with a `200`, carrying no question. Take any reply after that message as the operator having looked, an explicit "go" included, rather than holding for a stated confirmation, and only then put the choice through the structured question surface with the recommendation first.
|
|
33
33
|
8. **Record the pick.** Write what won, what it beat and by which numbers, where the evidence is, and the build criteria, per `${CLAUDE_SKILL_DIR}/references/record.md`. Read every figure you quote back from the page or the data file first, and correct the record where the question quoted one wrong.
|
|
@@ -44,7 +44,7 @@ A walkthrough turns what the operator sees in a running app into findings and pi
|
|
|
44
44
|
|
|
45
45
|
## What this delegates
|
|
46
46
|
|
|
47
|
-
- `draft-and-pick` owns the arm discipline and the structured question. This walkthrough
|
|
47
|
+
- `draft-and-pick` owns the arm discipline and the structured question. This walkthrough takes its live-app branch of Step 2 and departs from its Step 6 apply and delete, for the reasons `${CLAUDE_SKILL_DIR}/references/candidate-pages.md` states.
|
|
48
48
|
- `write-human` carries the voice of every recorded passage and any copy an arm puts in front of a reader.
|
|
49
49
|
- `plan-feature` and whoever dispatches builds turn a batch into plans and code.
|
|
50
50
|
- `canon capture`, `canon serve` and `canon sessions list` own the render, the address and the roster.
|
|
@@ -2,25 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Read when drafting arms for a finding. Skip it for a finding with no visible choice.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Dump the rendered markup of the surface under test from the built page, after it has loaded, into a JSON file in scratch. Strip scripts from the dump.
|
|
8
|
-
- Copy the built stylesheet and font files under `.canon/tmp/`, at the path the dumped markup links them from, so a candidate page renders with the stylesheet the app ships. Every arm then differs from the shipped page only by what the arm names.
|
|
9
|
-
- Trim a large dump to the part the finding needs, and keep the wrapper classes intact so layout rules keyed to them still resolve.
|
|
10
|
-
- Size each frame to the content width the finding names rather than the window width, since a container query reads the frame.
|
|
11
|
-
- Write copy an arm introduces with `write-human`, and take every other word from the dump.
|
|
12
|
-
|
|
13
|
-
## The page
|
|
14
|
-
|
|
15
|
-
- Generate the page with a short script, one per round, so a shared change is one edit and a rerun.
|
|
16
|
-
- Put a theme button on the page that flips the app's own theme switch on the root, rather than drawing each frame once per theme. Read `?arm=<id>&theme=<name>` to strip every other arm and the button for capture. Skip the button for an app that ships one theme.
|
|
17
|
-
- Print each frame's measurement under it from a script in the page, so the numbers the operator reads are the browser's.
|
|
18
|
-
- Write an index page linking one page per arm when an arm is a whole page or a route rather than a frame, with thumbnails copied beside it.
|
|
19
|
-
- Name anything the preview cannot reproduce, such as an asset whose colors follow the browser rather than the page's theme button, rather than fixing the page around it.
|
|
5
|
+
Follow `${CLAUDE_SKILL_DIR}/../draft-and-pick/SKILL.md` Step 2's live-app branch for lifting the markup, copying the stylesheet, and building the page, which routes to its own `references/live-arms.md`. What follows is only where this walkthrough departs from that shared path.
|
|
20
6
|
|
|
21
7
|
## Where this departs from draft-and-pick
|
|
22
8
|
|
|
23
9
|
- Capture into the walkthrough's own numbered evidence folder instead of the folder `draft-and-pick` Step 6 archives arms into, per `${CLAUDE_SKILL_DIR}/references/record.md`.
|
|
24
|
-
- Link the app's built stylesheet instead of inlining every asset. Lifted markup needs the real stylesheet, and an inlined copy is the drift this avoids.
|
|
25
10
|
- Keep `candidates.html` and never apply the winning arm. The walkthrough records and a build applies.
|
|
26
11
|
- Leave `.canon/tmp/<nn>-<slug>/` in place until the picks are built, and never delete the round's evidence folder.
|
package/docs/agents/commands.md
CHANGED
|
@@ -62,6 +62,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
62
62
|
| `canon migrate record-tree` | Repoint the old-root citations inside the records themselves, scoped to the live folders and reporting every line without `--write` (`--root`, `--json`) |
|
|
63
63
|
| `canon migrate rule-layout` | Move a target's installed rules from the flat `.claude/rules/<subdir>/` layout to `.claude/rules/canon/<subdir>/`, reporting the plan without `--write` (`--root`, `--json`) |
|
|
64
64
|
| `canon migrate scratch-evidence` | Move a folder under `.tmp/` that a durable record cites as evidence to `.canon/review/evidence/`, repointing every citation live or archived, reporting the plan without `--write` (`--root`, `--json`) |
|
|
65
|
+
| `canon migrate record-layout` | Fold review receipts from `.canon/review/memory/` and the retired-entry archive from `.canon/tmp/memory-archive/` under the memory pen itself, repointing every citation live or archived, reporting the plan without `--write` (`--root`, `--json`) |
|
|
65
66
|
| `canon sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
|
|
66
67
|
| `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
|
|
67
68
|
| `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
|
|
@@ -187,6 +188,8 @@ Every citation in scope is reported with its file, its line number, and the line
|
|
|
187
188
|
|
|
188
189
|
A destination that already holds the same bytes marks the flat file a duplicate, which `--write` deletes rather than moves. One holding different bytes is a real collision: neither file is touched, it is reported by name, and every other planned move still applies independently. Run this before `canon gov install` or `canon gov sync` against a target still on the flat layout, since neither bootstrap verb detects or clears it on its own.
|
|
189
190
|
|
|
191
|
+
`migrate record-layout` folds two record folders under the memory pen: review receipts move from `.canon/review/memory/` to `.canon/memory/review/`, and retired entries move from `.canon/tmp/memory-archive/` to `.canon/memory/archive/`, backed for the first time since `memory/` is a folder `canon records push` carries and `tmp/` is not. Each move is refused independently when its destination already exists, and the citation repoint sweeps every folder `canon records push` carries, archives included, since an archived receipt still cites the row it retired. The same `canon-keep-record-root` marker and second-run idempotence check apply. A receipt sitting at the flat `review/` root, from before `.canon/review/memory/` existed, matches neither mapped move and is reported by name rather than moved.
|
|
192
|
+
|
|
190
193
|
## Version skew
|
|
191
194
|
|
|
192
195
|
`canon sync --check` and `canon claude skills drift` are the two moments a target
|
|
@@ -166,7 +166,7 @@ Capture leads rather than trails because a routed fact edits a tracked file, whi
|
|
|
166
166
|
|
|
167
167
|
If capture wrote at least one memory file, `memory-review` then proposes a decision-ready fix scoped to those entries while context is fresh, otherwise it is skipped. It stops at Propose. Review the receipt and run Apply yourself, on its own commit separate from the feature.
|
|
168
168
|
|
|
169
|
-
Run `memory-review` standalone to curate the whole pen. An entry it retires moves to `.canon/
|
|
169
|
+
Run `memory-review` standalone to curate the whole pen. An entry it retires moves to `.canon/memory/archive/` rather than being deleted, since a bulk pass has no undo.
|
|
170
170
|
|
|
171
171
|
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 `docs-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.
|
|
172
172
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route .canon/ready/ edits to the ready standard for the folder layout, the overview frontmatter, and the thin-plan contract
|
|
3
|
+
paths:
|
|
4
|
+
- '.canon/ready/**'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Ready standards
|
|
8
|
+
|
|
9
|
+
## Authority
|
|
10
|
+
|
|
11
|
+
- Follow the ready standard for the folder layout, ordinal naming, the overview frontmatter, the mirrored tree, and the thin-plan contract. It is the single source. Read it with `canon standards ready`.
|
|
@@ -8,5 +8,5 @@ description: Keep memory writes scoped to .canon/memory/ and out of context-owne
|
|
|
8
8
|
|
|
9
9
|
- Write all memory files to `.canon/memory/`, not `~/.claude/projects/`
|
|
10
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:docs-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
|
-
- Never delete a memory entry. Retire one by moving it to `.canon/
|
|
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.
|
|
12
12
|
- Follow the memory standard for the filename and type prefix, the frontmatter, the body shape each type carries, and the lifecycle. Read it with `canon standards memory`. Check every entry in the pen against that standard and fix what breaks it, since nothing keeps the folder conforming on its own.
|
package/package.json
CHANGED
package/src/commands/migrate.ts
CHANGED
|
@@ -30,6 +30,13 @@ import {
|
|
|
30
30
|
planSurfaceRootsMove,
|
|
31
31
|
type SurfaceRootsPlan,
|
|
32
32
|
} from '@/migrate/surface-roots'
|
|
33
|
+
import {
|
|
34
|
+
applyRecordLayout,
|
|
35
|
+
planRecordLayout,
|
|
36
|
+
readRecordLayoutCorpus,
|
|
37
|
+
type RecordLayoutPlan,
|
|
38
|
+
walkRecordLayoutCorpus,
|
|
39
|
+
} from '@/migrate/record-layout'
|
|
33
40
|
import {
|
|
34
41
|
applyScratchEvidence,
|
|
35
42
|
planScratchEvidence,
|
|
@@ -746,6 +753,102 @@ function toScratchEvidenceRecord(
|
|
|
746
753
|
}
|
|
747
754
|
}
|
|
748
755
|
|
|
756
|
+
interface RecordLayoutOptions {
|
|
757
|
+
readonly json?: boolean
|
|
758
|
+
readonly write?: boolean
|
|
759
|
+
readonly root?: string
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Moves review receipts from `.canon/review/memory/` to `.canon/memory/review/`
|
|
764
|
+
* and retired entries from `.canon/tmp/memory-archive/` to
|
|
765
|
+
* `.canon/memory/archive/`, and repoints the citations that name either.
|
|
766
|
+
*/
|
|
767
|
+
async function runRecordLayout(opts: RecordLayoutOptions): Promise<number> {
|
|
768
|
+
const root = opts.root ?? process.cwd()
|
|
769
|
+
|
|
770
|
+
const files = await walkRecordLayoutCorpus(root)
|
|
771
|
+
const sources = await readRecordLayoutCorpus(files)
|
|
772
|
+
const plan = planRecordLayout(root, sources)
|
|
773
|
+
|
|
774
|
+
if (opts.json) {
|
|
775
|
+
process.stdout.write(
|
|
776
|
+
`${JSON.stringify(toRecordLayoutRecord(plan, opts.write))}\n`,
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
reportRecordLayout(plan)
|
|
781
|
+
|
|
782
|
+
if (plan.collisions.length > 0) {
|
|
783
|
+
logError(
|
|
784
|
+
`${plural(plan.collisions.length, 'destination')} already occupied. Neither side moved.`,
|
|
785
|
+
)
|
|
786
|
+
for (const collision of plan.collisions) logError(` ${collision}`)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (plan.moves.length === 0 && plan.entries.length === 0) {
|
|
790
|
+
return plan.collisions.length > 0 ? 1 : 0
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (!opts.write) {
|
|
794
|
+
logWarn('Nothing was written. Pass --write to apply this plan.')
|
|
795
|
+
return 2
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const applied = await applyRecordLayout(plan)
|
|
799
|
+
logStep(
|
|
800
|
+
`Moved ${plural(applied.moved, 'folder')} and rewrote ${plural(applied.written, 'file')}.`,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
if (applied.failed.length > 0) {
|
|
804
|
+
logError(`Could not write ${plural(applied.failed.length, 'file')}.`)
|
|
805
|
+
for (const path of applied.failed) logError(` ${path}`)
|
|
806
|
+
return 1
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return plan.collisions.length > 0 ? 1 : 0
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function reportRecordLayout(plan: RecordLayoutPlan): void {
|
|
813
|
+
logInfo(`${plural(plan.moves.length, 'folder')} to move.`)
|
|
814
|
+
for (const move of plan.moves) {
|
|
815
|
+
logInfo(` ${move.from} -> ${move.to}`)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
logInfo(
|
|
819
|
+
`${plural(plan.entries.length, 'file')} to change, ${plural(plan.rewritten, 'citation')} to rewrite.`,
|
|
820
|
+
)
|
|
821
|
+
for (const entry of plan.entries) {
|
|
822
|
+
logInfo(` ${entry.path}: ${plural(entry.rewritten, 'citation')}`)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (plan.strays.length > 0) {
|
|
826
|
+
logWarn(
|
|
827
|
+
`${plural(plan.strays.length, 'stray receipt')} at the flat review/ root. Not moved, move by hand into memory/review/.`,
|
|
828
|
+
)
|
|
829
|
+
for (const stray of plan.strays) logWarn(` ${stray}`)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function toRecordLayoutRecord(
|
|
834
|
+
plan: RecordLayoutPlan,
|
|
835
|
+
wrote: boolean | undefined,
|
|
836
|
+
): unknown {
|
|
837
|
+
return {
|
|
838
|
+
ok: true,
|
|
839
|
+
wrote: wrote === true,
|
|
840
|
+
moves: plan.moves.map((move) => ({ from: move.from, to: move.to })),
|
|
841
|
+
collisions: plan.collisions,
|
|
842
|
+
files: plan.entries.length,
|
|
843
|
+
rewritten: plan.rewritten,
|
|
844
|
+
strays: plan.strays,
|
|
845
|
+
paths: plan.entries.map((entry) => ({
|
|
846
|
+
path: entry.path,
|
|
847
|
+
rewritten: entry.rewritten,
|
|
848
|
+
})),
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
749
852
|
interface RuleLayoutOptions {
|
|
750
853
|
readonly json?: boolean
|
|
751
854
|
readonly write?: boolean
|
|
@@ -1031,6 +1134,45 @@ export function register(program: Command): void {
|
|
|
1031
1134
|
process.exitCode = await runScratchEvidence(opts)
|
|
1032
1135
|
})
|
|
1033
1136
|
|
|
1137
|
+
migrate
|
|
1138
|
+
.command('record-layout')
|
|
1139
|
+
.description('Fold memory review receipts and archive under memory/')
|
|
1140
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
1141
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
1142
|
+
.option('--write', 'Apply the plan rather than reporting it')
|
|
1143
|
+
.option(
|
|
1144
|
+
'--root <path>',
|
|
1145
|
+
'Project root, defaulting to the working directory',
|
|
1146
|
+
)
|
|
1147
|
+
.addHelpText(
|
|
1148
|
+
'after',
|
|
1149
|
+
[
|
|
1150
|
+
'',
|
|
1151
|
+
'Moves review receipts from .canon/review/memory/ to',
|
|
1152
|
+
'.canon/memory/review/, and retired entries from',
|
|
1153
|
+
'.canon/tmp/memory-archive/ to .canon/memory/archive/, backed for',
|
|
1154
|
+
'the first time, and repoints every citation that names either,',
|
|
1155
|
+
'live or archived.',
|
|
1156
|
+
'',
|
|
1157
|
+
'A receipt sitting at the flat review/ root, the shape memory-review',
|
|
1158
|
+
'wrote before review/memory/ existed, is reported rather than moved.',
|
|
1159
|
+
'',
|
|
1160
|
+
'Exit codes:',
|
|
1161
|
+
' 0 nothing to move, or --write applied the whole plan',
|
|
1162
|
+
' 1 a write failed, or an unresolved collision remains',
|
|
1163
|
+
' 2 a plan exists and --write was not passed',
|
|
1164
|
+
'',
|
|
1165
|
+
'Examples:',
|
|
1166
|
+
' canon migrate record-layout',
|
|
1167
|
+
' canon migrate record-layout --write',
|
|
1168
|
+
' canon migrate record-layout --json',
|
|
1169
|
+
'',
|
|
1170
|
+
].join('\n'),
|
|
1171
|
+
)
|
|
1172
|
+
.action(async (opts: RecordLayoutOptions) => {
|
|
1173
|
+
process.exitCode = await runRecordLayout(opts)
|
|
1174
|
+
})
|
|
1175
|
+
|
|
1034
1176
|
migrate
|
|
1035
1177
|
.command('rename')
|
|
1036
1178
|
.description('Rewrite every unprotected aitk token to canon')
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory pen's review receipts and retired entries, folded under the pen
|
|
3
|
+
* itself rather than sitting in three separate record folders.
|
|
4
|
+
*
|
|
5
|
+
* `.canon/review/memory/` and `.canon/tmp/memory-archive/` move to
|
|
6
|
+
* `.canon/memory/review/` and `.canon/memory/archive/`, the second one backed
|
|
7
|
+
* for the first time: `canon records push` carries every top-level `.canon/`
|
|
8
|
+
* entry except `EXCLUDED_ENTRIES`, and `tmp/` is one of the three names that
|
|
9
|
+
* set excludes. `canon/ARCHITECTURE.md`'s "A durable record is named for what
|
|
10
|
+
* it is, not for how long it lives" already names the cost this closes: two
|
|
11
|
+
* surfaces both named for memory archived to two different places.
|
|
12
|
+
*
|
|
13
|
+
* The move and the citation repoint follow `scratch-evidence.ts`'s shape: a
|
|
14
|
+
* dry run by default, `--write` to apply, a collision refusal, the
|
|
15
|
+
* `canon-keep-record-root` marker honored, and archives swept on purpose,
|
|
16
|
+
* since an archived receipt still cites the row it retired.
|
|
17
|
+
*
|
|
18
|
+
* `moves` is a data table rather than one function per pair, so a later
|
|
19
|
+
* intake batch in the same folder-layout group appends an entry instead of
|
|
20
|
+
* restructuring the module.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
24
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
25
|
+
import { dirname, join } from 'node:path'
|
|
26
|
+
import { presentFolders } from '@/records/backup'
|
|
27
|
+
import { recordDir, SCRATCH, spell, type RecordRoot } from '@/record-root'
|
|
28
|
+
|
|
29
|
+
const CANON: RecordRoot = '.canon'
|
|
30
|
+
const CLAUDE: RecordRoot = '.claude'
|
|
31
|
+
|
|
32
|
+
export interface RecordLayoutMove {
|
|
33
|
+
readonly from: readonly string[]
|
|
34
|
+
readonly to: readonly string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Every folder this migration moves, as a record-relative path on each side.
|
|
39
|
+
*
|
|
40
|
+
* Derived from the plan's own mapping: the review folder's memory receipts
|
|
41
|
+
* fold into the pen at `memory/review/`, and the scratch archive folds in at
|
|
42
|
+
* `memory/archive/`. A later batch in the same intake group appends here
|
|
43
|
+
* rather than adding a second table.
|
|
44
|
+
*/
|
|
45
|
+
export const RECORD_LAYOUT_MOVES: readonly RecordLayoutMove[] = [
|
|
46
|
+
{ from: ['review', 'memory'], to: ['memory', 'review'] },
|
|
47
|
+
{ from: [SCRATCH, 'memory-archive'], to: ['memory', 'archive'] },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
/** Where a move's source sits today. */
|
|
51
|
+
export function sourcePath(root: string, move: RecordLayoutMove): string {
|
|
52
|
+
const [folder, ...rest] = move.from
|
|
53
|
+
return recordDir(root, folder, ...rest)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Where it lands after, always under whichever root `memory/` resolves at. */
|
|
57
|
+
export function destinationPath(root: string, move: RecordLayoutMove): string {
|
|
58
|
+
const [folder, ...rest] = move.to
|
|
59
|
+
return recordDir(root, folder, ...rest)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function escape(value: string): string {
|
|
63
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The prefixes a citation of a move's source is spelled with: absolute at
|
|
68
|
+
* either record root, spelled the way each root spells the leading segment,
|
|
69
|
+
* and relative one and two directories up, in both spellings, since a
|
|
70
|
+
* relative citation carries no root of its own to read the spelling from.
|
|
71
|
+
*/
|
|
72
|
+
function oldPrefixes(from: readonly string[]): readonly string[] {
|
|
73
|
+
const [head, ...rest] = from
|
|
74
|
+
const canonSuffix = [spell(CANON, head), ...rest].join('/')
|
|
75
|
+
const claudeSuffix = [spell(CLAUDE, head), ...rest].join('/')
|
|
76
|
+
|
|
77
|
+
const prefixes = new Set<string>([
|
|
78
|
+
`.canon/${canonSuffix}/`,
|
|
79
|
+
`.claude/${claudeSuffix}/`,
|
|
80
|
+
])
|
|
81
|
+
for (const suffix of [canonSuffix, claudeSuffix]) {
|
|
82
|
+
prefixes.add(`../${suffix}/`)
|
|
83
|
+
prefixes.add(`../../${suffix}/`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [...prefixes]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function citationPattern(move: RecordLayoutMove): RegExp {
|
|
90
|
+
const alternation = oldPrefixes(move.from).map(escape).join('|')
|
|
91
|
+
return new RegExp(`(?:${alternation})`, 'g')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Rewrite {
|
|
95
|
+
readonly pattern: RegExp
|
|
96
|
+
readonly destination: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One rewrite per move, paired with its citation pattern. */
|
|
100
|
+
function buildRewrites(moves: readonly RecordLayoutMove[]): readonly Rewrite[] {
|
|
101
|
+
return moves.map((move) => ({
|
|
102
|
+
pattern: citationPattern(move),
|
|
103
|
+
destination: `.canon/${move.to.join('/')}/`,
|
|
104
|
+
}))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Marks a line naming a moved path on purpose, the same marker
|
|
109
|
+
* `scratch-evidence.ts` and `records.ts` read: on the line itself or on the
|
|
110
|
+
* nearest non-blank line above it.
|
|
111
|
+
*/
|
|
112
|
+
const KEEP_MARKER = 'canon-keep-record-root'
|
|
113
|
+
|
|
114
|
+
function isKept(lines: readonly string[], index: number): boolean {
|
|
115
|
+
if (lines[index]?.includes(KEEP_MARKER)) return true
|
|
116
|
+
|
|
117
|
+
let above = index - 1
|
|
118
|
+
while (above >= 0 && lines[above]?.trim() === '') above -= 1
|
|
119
|
+
|
|
120
|
+
return above >= 0 && (lines[above]?.includes(KEEP_MARKER) ?? false)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rewriteLine(line: string, rewrites: readonly Rewrite[]): string {
|
|
124
|
+
return rewrites.reduce(
|
|
125
|
+
(current, { pattern, destination }) =>
|
|
126
|
+
current.replace(pattern, destination),
|
|
127
|
+
line,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface RewriteOutcome {
|
|
132
|
+
readonly text: string
|
|
133
|
+
readonly count: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Rewrites every unmarked citation a move covers into its destination,
|
|
138
|
+
* counting each as it goes. A file naming no such path returns
|
|
139
|
+
* byte-identical with a count of zero, and a marked line is returned
|
|
140
|
+
* unchanged and uncounted.
|
|
141
|
+
*/
|
|
142
|
+
function applyRewrites(
|
|
143
|
+
text: string,
|
|
144
|
+
rewrites: readonly Rewrite[],
|
|
145
|
+
): RewriteOutcome {
|
|
146
|
+
const lines = text.split('\n')
|
|
147
|
+
let count = 0
|
|
148
|
+
|
|
149
|
+
const rewritten = lines.map((line, index) => {
|
|
150
|
+
if (isKept(lines, index)) return line
|
|
151
|
+
|
|
152
|
+
for (const { pattern } of rewrites) {
|
|
153
|
+
count += [...line.matchAll(pattern)].length
|
|
154
|
+
}
|
|
155
|
+
return rewriteLine(line, rewrites)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return { text: rewritten.join('\n'), count }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The files under every present backed folder at `root`, archives included. */
|
|
162
|
+
export async function walkRecordLayoutCorpus(root: string): Promise<string[]> {
|
|
163
|
+
const files: string[] = []
|
|
164
|
+
|
|
165
|
+
for (const folder of presentFolders(root)) {
|
|
166
|
+
const dir = recordDir(root, folder)
|
|
167
|
+
if (!existsSync(dir)) continue
|
|
168
|
+
|
|
169
|
+
const glob = new Bun.Glob('**/*')
|
|
170
|
+
for await (const path of glob.scan({
|
|
171
|
+
cwd: dir,
|
|
172
|
+
onlyFiles: true,
|
|
173
|
+
dot: true,
|
|
174
|
+
})) {
|
|
175
|
+
files.push(join(dir, path))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return files.sort()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface RecordLayoutSource {
|
|
183
|
+
readonly path: string
|
|
184
|
+
readonly text: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reads every file the walk found, skipping one that carries a NUL byte. */
|
|
188
|
+
export async function readRecordLayoutCorpus(
|
|
189
|
+
paths: readonly string[],
|
|
190
|
+
): Promise<RecordLayoutSource[]> {
|
|
191
|
+
const sources: RecordLayoutSource[] = []
|
|
192
|
+
|
|
193
|
+
for (const path of paths) {
|
|
194
|
+
const bytes = await readFile(path).catch(() => undefined)
|
|
195
|
+
if (bytes === undefined || bytes.includes(0)) continue
|
|
196
|
+
|
|
197
|
+
sources.push({ path, text: bytes.toString('utf8') })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return sources
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface FolderMove {
|
|
204
|
+
readonly move: RecordLayoutMove
|
|
205
|
+
readonly from: string
|
|
206
|
+
readonly to: string
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface CitationEntry {
|
|
210
|
+
readonly path: string
|
|
211
|
+
readonly text: string
|
|
212
|
+
readonly rewritten: number
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface RecordLayoutPlan {
|
|
216
|
+
readonly moves: readonly FolderMove[]
|
|
217
|
+
readonly collisions: readonly string[]
|
|
218
|
+
readonly entries: readonly CitationEntry[]
|
|
219
|
+
readonly rewritten: number
|
|
220
|
+
readonly strays: readonly string[]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const STRAY_RECEIPT_PATTERN = /^memory-review-.*\.md$/
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A receipt sitting at the flat `review/` root, the shape `memory-review`
|
|
227
|
+
* wrote before `.canon/review/memory/` existed. Neither mapped move covers
|
|
228
|
+
* it, since it sits one level above the folder either move reads, so it is
|
|
229
|
+
* reported rather than moved: widening the table for one stray file trades a
|
|
230
|
+
* data-shaped mapping for a special case.
|
|
231
|
+
*/
|
|
232
|
+
export function strayReceipts(root: string): string[] {
|
|
233
|
+
const dir = recordDir(root, 'review')
|
|
234
|
+
const entries = existsSync(dir)
|
|
235
|
+
? readdirSync(dir, { withFileTypes: true })
|
|
236
|
+
: []
|
|
237
|
+
|
|
238
|
+
return entries
|
|
239
|
+
.filter((entry) => entry.isFile() && STRAY_RECEIPT_PATTERN.test(entry.name))
|
|
240
|
+
.map((entry) => join(dir, entry.name))
|
|
241
|
+
.sort()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Every mapped move found on disk, with its destination, refusing a move
|
|
246
|
+
* whose destination is already occupied rather than merging into it.
|
|
247
|
+
*/
|
|
248
|
+
export function planFolderMoves(root: string): {
|
|
249
|
+
moves: FolderMove[]
|
|
250
|
+
collisions: string[]
|
|
251
|
+
} {
|
|
252
|
+
const moves: FolderMove[] = []
|
|
253
|
+
const collisions: string[] = []
|
|
254
|
+
|
|
255
|
+
for (const move of RECORD_LAYOUT_MOVES) {
|
|
256
|
+
const from = sourcePath(root, move)
|
|
257
|
+
if (!existsSync(from)) continue
|
|
258
|
+
|
|
259
|
+
const to = destinationPath(root, move)
|
|
260
|
+
if (existsSync(to)) {
|
|
261
|
+
collisions.push(to)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
moves.push({ move, from, to })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { moves, collisions }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* What the migration would do, without doing it. Pure over the sources it is
|
|
273
|
+
* handed, so a caller reports and applies from the same value. A file whose
|
|
274
|
+
* text does not change is dropped.
|
|
275
|
+
*/
|
|
276
|
+
export function planRecordLayout(
|
|
277
|
+
root: string,
|
|
278
|
+
sources: readonly RecordLayoutSource[],
|
|
279
|
+
): RecordLayoutPlan {
|
|
280
|
+
const { moves, collisions } = planFolderMoves(root)
|
|
281
|
+
const collidedDestinations = new Set(collisions)
|
|
282
|
+
const covered = RECORD_LAYOUT_MOVES.filter(
|
|
283
|
+
(move) => !collidedDestinations.has(destinationPath(root, move)),
|
|
284
|
+
)
|
|
285
|
+
const rewrites = buildRewrites(covered)
|
|
286
|
+
const entries: CitationEntry[] = []
|
|
287
|
+
|
|
288
|
+
for (const source of sources) {
|
|
289
|
+
const { text, count } = applyRewrites(source.text, rewrites)
|
|
290
|
+
if (count === 0) continue
|
|
291
|
+
|
|
292
|
+
entries.push({ path: source.path, text, rewritten: count })
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
moves,
|
|
297
|
+
collisions,
|
|
298
|
+
entries,
|
|
299
|
+
rewritten: entries.reduce((sum, entry) => sum + entry.rewritten, 0),
|
|
300
|
+
strays: strayReceipts(root),
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface RecordLayoutResult {
|
|
305
|
+
readonly moved: number
|
|
306
|
+
readonly written: number
|
|
307
|
+
readonly failed: readonly string[]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Writes the plan: every citation rewrite first, then every folder move.
|
|
312
|
+
*
|
|
313
|
+
* A citation can sit inside a file the plan is about to move, since the walk
|
|
314
|
+
* carries archives on purpose and a receipt can cite the row it retired.
|
|
315
|
+
* Rewriting first is what keeps that write landing on a path that still
|
|
316
|
+
* exists: renaming the folder first would send `writeFile` at the pre-move
|
|
317
|
+
* path into a directory `rename` already cleared.
|
|
318
|
+
*/
|
|
319
|
+
export async function applyRecordLayout(
|
|
320
|
+
plan: RecordLayoutPlan,
|
|
321
|
+
): Promise<RecordLayoutResult> {
|
|
322
|
+
let moved = 0
|
|
323
|
+
let written = 0
|
|
324
|
+
const failed: string[] = []
|
|
325
|
+
|
|
326
|
+
for (const entry of plan.entries) {
|
|
327
|
+
const done = await writeFile(entry.path, entry.text)
|
|
328
|
+
.then(() => true)
|
|
329
|
+
.catch(() => false)
|
|
330
|
+
|
|
331
|
+
if (done) written += 1
|
|
332
|
+
else failed.push(entry.path)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const move of plan.moves) {
|
|
336
|
+
await mkdir(dirname(move.to), { recursive: true })
|
|
337
|
+
const done = await rename(move.from, move.to)
|
|
338
|
+
.then(() => true)
|
|
339
|
+
.catch(() => false)
|
|
340
|
+
|
|
341
|
+
if (done) moved += 1
|
|
342
|
+
else failed.push(move.from)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { moved, written, failed }
|
|
346
|
+
}
|
|
@@ -60,7 +60,7 @@ export const OBJECT_STORE = '.records.git'
|
|
|
60
60
|
* `archive` is the substantive one: an archived plan or a retired memory entry
|
|
61
61
|
* describes work that closed, and a path inside that sentence is history rather
|
|
62
62
|
* than a pointer. It is pruned at any depth because the archives do not all sit
|
|
63
|
-
* at the same one, `review/
|
|
63
|
+
* at the same one, `memory/review/archive/` being two levels down.
|
|
64
64
|
*/
|
|
65
65
|
export const PRUNED_SEGMENTS: readonly string[] = [
|
|
66
66
|
'archive',
|
package/src/record-root.ts
CHANGED
package/standards/index.md
CHANGED
|
@@ -27,6 +27,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
27
27
|
- [Pull request reference](pr.md): Pull request title and body conventions
|
|
28
28
|
- [Publish reference](publish.md): Scan an author runs against finished text, the cross-reference form each destination takes, and the response to an unreadable source
|
|
29
29
|
- [Readme reference](readme.md): Readme voice, structure, and content conventions
|
|
30
|
+
- [Ready reference](ready.md): Folder layout, ordinal naming, the overview frontmatter, the thin-plan contract, and the archive lifecycle for a finished-file handoff
|
|
30
31
|
- [Requirements reference](requirements.md): Shape and content rules for canon/REQUIREMENTS.md
|
|
31
32
|
- [Governance rule reference](rule.md): Rule frontmatter, body shape, and voice for .claude/rules files
|
|
32
33
|
- [Session map reference](session.md): Filename and location, the sections a handoff carries, the write and read procedures, and how a role extends it
|
package/standards/memory.md
CHANGED
|
@@ -88,7 +88,7 @@ Capture the pattern rather than the recovery. What was tried, what failed, and w
|
|
|
88
88
|
|
|
89
89
|
## Links
|
|
90
90
|
|
|
91
|
-
Link a related entry as `[[name]]`, where `name` is the target's filename stem without the extension. Link freely:
|
|
91
|
+
Link a related entry as `[[name]]`, where `name` is the target's filename stem without the extension. Link freely: an entry sits flat at the top level and the links are the only structure among them. `review/` and `archive/` hold receipts and retirements rather than entries, so neither takes a link.
|
|
92
92
|
|
|
93
93
|
- Place links inside the body part they support, not in a list of their own at the end.
|
|
94
94
|
- A link naming an entry nobody has written yet is legal, and it marks a rule worth writing rather than a defect.
|
package/standards/plan.md
CHANGED
|
@@ -85,6 +85,8 @@ A constraint measured against work in flight expires when that work merges, and
|
|
|
85
85
|
|
|
86
86
|
A dead constraint fails silently in the expensive direction. A session honoring one ships the dangling citation the change created and reports success, where a session crossing a live constraint collides visibly and is caught.
|
|
87
87
|
|
|
88
|
+
A constraint naming a `.canon/ready/` folder is a third shape beside the two above. It makes that folder's files the verbatim source for the paths this plan's `**Files to touch:**` lists, per `ready.md`, so the executing session copies those paths rather than authoring them.
|
|
89
|
+
|
|
88
90
|
### Risks
|
|
89
91
|
|
|
90
92
|
- Name the collision rather than the category. A risk a reader cannot act on is padding.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Ready reference
|
|
3
|
+
description: Folder layout, ordinal naming, the overview frontmatter, the thin-plan contract, and the archive lifecycle for a finished-file handoff
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ready reference
|
|
7
|
+
|
|
8
|
+
Applies to a ready folder at `.canon/ready/<nn>-<slug>/`. A warm session that has already written a skill, a rule, or another finished file uses it to hand the exact text to the worker that ships it, since a plan only describes a change and a cold worker reading a description writes the file again from scratch. The folder holds the finished files themselves, laid out at their destination paths, so the worker's job is to copy rather than to author.
|
|
9
|
+
|
|
10
|
+
The folder is gitignored, and backed wherever a records remote is configured: `canon records push` and `canon records pull` protect it against the machine being lost there, refuse with `no-remote` where it is not, and protect nothing against a folder deleted before anyone has pushed. That is why the archive step below is a move rather than a cleanup.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
Governs a ready folder under `.canon/ready/<nn>-<slug>/`: folder layout, ordinal naming, the overview's frontmatter, what the mirrored tree holds, the thin-plan contract that ships it, and the lifecycle from the live folder to the archive.
|
|
15
|
+
|
|
16
|
+
Does not govern:
|
|
17
|
+
|
|
18
|
+
- The thin plan itself, its filename, its sections, and its suggested-and-answer contract: `plan.md`
|
|
19
|
+
- The task file that reaches a ready folder through a plan, and the origin line pointing back at it: `tasks.md`
|
|
20
|
+
- Voice, rhythm, and sentence construction: the `write-human` skill
|
|
21
|
+
- Headings, punctuation, word choice, and file references: `markdown.md`
|
|
22
|
+
- Whether a change earns a ready folder over a plan a worker builds from scratch, which belongs to the warm session deciding how to hand off its own work
|
|
23
|
+
|
|
24
|
+
## What a working ready folder looks like
|
|
25
|
+
|
|
26
|
+
A ready folder works when a worker that has never seen the warm session's conversation can copy from it alone:
|
|
27
|
+
|
|
28
|
+
- Which destination path does each file land at, and does the folder hold nothing else?
|
|
29
|
+
- What is the worker still responsible for that the files themselves do not carry, such as a docs sync, a sandbox scenario, or a test?
|
|
30
|
+
- What branch type does the change take?
|
|
31
|
+
- Is every path the folder mirrors also declared in the thin plan's `**Files to touch:**`?
|
|
32
|
+
|
|
33
|
+
A ready folder failing these is non-conforming even when it satisfies every shape rule below.
|
|
34
|
+
|
|
35
|
+
## Folder name
|
|
36
|
+
|
|
37
|
+
- Name the folder `<nn>-<slug>`, a two-digit zero-padded ordinal followed by a kebab-case slug matching the plan's own slug.
|
|
38
|
+
- The ordinal marks a folder per handoff, opened once by the warm session that writes it. It runs on its own sequence, separate from groundwork and intake's shared one, since a ready folder is not a measurement track.
|
|
39
|
+
- With no folder holding an entry yet, the first one opened takes `01`. Read the highest existing `.canon/ready/<nn>-*/` folder, including the archive, and take the next integer.
|
|
40
|
+
- Never renumber an existing folder. The ordinal is the order it opened, and the pull request that shipped it cites the folder by that name.
|
|
41
|
+
|
|
42
|
+
## 00-overview.md
|
|
43
|
+
|
|
44
|
+
Every ready folder carries `00-overview.md` at its root, beside the mirrored tree. It orients the worker and states what the files themselves cannot.
|
|
45
|
+
|
|
46
|
+
- `title` (required): the change in sentence case
|
|
47
|
+
- `description` (required): one line naming what the handoff carries
|
|
48
|
+
- `type` (required): the branch type the plan should take, one of the types `branch.md` fixes
|
|
49
|
+
- `destinations` (required): the list of destination paths the folder mirrors, matching the thin plan's `**Files to touch:**` exactly
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
---
|
|
53
|
+
title: <Change in sentence case>
|
|
54
|
+
description: <one line naming what the handoff carries>
|
|
55
|
+
type: <feat | fix | chore | ...>
|
|
56
|
+
destinations:
|
|
57
|
+
- <path/to/file>
|
|
58
|
+
---
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Below the frontmatter, state in prose what the worker still owns beyond copying the files: a docs sync, a sandbox scenario update, a test the files do not include, or "nothing further" where the files are the whole of the change.
|
|
62
|
+
|
|
63
|
+
## The mirrored tree
|
|
64
|
+
|
|
65
|
+
- Every other file in the folder sits at the same relative path its destination has in the project, so `standards/ready.md` inside the destination tree sits at `<nn>-<slug>/standards/ready.md` inside the ready folder.
|
|
66
|
+
- Carry no file the destination tree would not carry. A ready folder is a source for `git mv`-shaped copies, not a scratch pad for the warm session's own notes. Anything else belongs in the plan or in the pull request body.
|
|
67
|
+
- Write each file exactly as it should land. The worker copies verbatim and edits only what the gate or the overview's own list requires, so a placeholder or a half-finished passage ships as written.
|
|
68
|
+
|
|
69
|
+
## The thin-plan contract
|
|
70
|
+
|
|
71
|
+
- A ready folder ships through an ordinary task row and a plan at `.canon/plans/feature-<slug>.md`, per `plan.md`. No new plan shape exists for it.
|
|
72
|
+
- Name the ready folder in the plan's `**Constraints:**`, stating that the folder's files are the verbatim source for the paths the plan's `**Files to touch:**` lists.
|
|
73
|
+
- List every destination path in `**Files to touch:**`, matching `00-overview.md`'s `destinations` field. A path the plan omits is invisible to `plan-reach`'s collision check, so a mismatch between the two lists is a defect in the plan rather than a variant the standard permits.
|
|
74
|
+
- Keep the plan itself thin. Its `**Files to touch:**` entries may point at the ready folder's own copy for the reason behind each file rather than restating it, since the overview and the files already carry the detail a plan would otherwise duplicate.
|
|
75
|
+
|
|
76
|
+
## Lifecycle
|
|
77
|
+
|
|
78
|
+
- Write the ready folder in the same session that writes the files it carries. A folder assembled later from memory is a plan with extra steps, not a handoff.
|
|
79
|
+
- Move the folder to `.canon/ready/archive/<nn>-<slug>/` by hand when the task that shipped it archives. No board verb currently automates this move. `canon tasks archive` moves the task and its plan and leaves the ready folder where it is.
|
|
80
|
+
- Never delete a ready folder. The archived copy sits beside the merged pull request as the exact text that shipped, the way an archived plan sits beside the reasoning that produced it.
|
|
81
|
+
|
|
82
|
+
## Anti-patterns
|
|
83
|
+
|
|
84
|
+
- **The folder with an undeclared destination.** A file the plan's `**Files to touch:**` does not list passes the collision check unseen, and a second track can write the same path without either side finding out.
|
|
85
|
+
- **The rewritten copy.** A worker that reads the folder's files as inspiration and writes its own version loses the exact text the handoff exists to carry.
|
|
86
|
+
- **The folder as scratch.** Notes, alternates, or draft passages left in the folder beside the real files leave the worker guessing which is the source.
|
|
87
|
+
- **The folder left live after shipping.** A ready folder nobody moves to the archive reads as unshipped work to the next session that lists the live folder.
|
|
88
|
+
|
|
89
|
+
## Template
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
.canon/ready/<nn>-<slug>/
|
|
93
|
+
├── 00-overview.md
|
|
94
|
+
└── <path/to/file> # mirrors the destination tree, one entry per file
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
---
|
|
99
|
+
title: <Change in sentence case>
|
|
100
|
+
description: <one line naming what the handoff carries>
|
|
101
|
+
type: <feat | fix | chore | ...>
|
|
102
|
+
destinations:
|
|
103
|
+
- <path/to/file>
|
|
104
|
+
---
|
|
105
|
+
```
|
package/standards/skill.md
CHANGED
|
@@ -210,7 +210,7 @@ Without this skill, a session <observed failure>, <observed failure>.
|
|
|
210
210
|
### Output and tuning
|
|
211
211
|
|
|
212
212
|
- Skill success lines emit the full relative path from the project root (`<dir>/<file>`) for any file written, updated, or deleted. A bare filename names a file the reader cannot open. The `## Output` section of the project's instruction file sets the form that path takes, so a skill body states which path is emitted and leaves the form to that section.
|
|
213
|
-
- Before a skill writes anything, decide whether the output is a deliverable the project keeps or a toolkit session record. A deliverable lands among the project's own tracked files. A session record lands under `.canon/`, in the named subfolder for its kind (`tasks/`, `plans/`, `review/`, `memory/`, `groundwork/`, `intake/`, `proposals/`, `diagrams/`, `teach/`, or `walkthroughs/`, with `tmp/` for scratch nothing else claims), never in a folder the body invents. `canon/ARCHITECTURE.md`'s per-folder decisions are the precedent for which kind takes which folder.
|
|
213
|
+
- Before a skill writes anything, decide whether the output is a deliverable the project keeps or a toolkit session record. A deliverable lands among the project's own tracked files. A session record lands under `.canon/`, in the named subfolder for its kind (`tasks/`, `plans/`, `review/`, `memory/`, `groundwork/`, `intake/`, `proposals/`, `diagrams/`, `teach/`, `ready/`, or `walkthroughs/`, with `tmp/` for scratch nothing else claims), never in a folder the body invents. `canon/ARCHITECTURE.md`'s per-folder decisions are the precedent for which kind takes which folder.
|
|
214
214
|
- Codify a skill's posted or generated output as a fenced template, and keep the body consistent with every capability the frontmatter description names.
|
|
215
215
|
- When a skill gathers user input or pre-seeds a template, attach a concrete proposed default to every question, derived from project context. Accept "use defaults" as a bulk-confirm.
|
|
216
216
|
- Separate correctness axes (routing, sourcing, escalation, decline) from shape axes (line count, formatting, variant sprawl) when tuning a skill. Tighten only on correctness regressions. Do not convert soft caps to hard caps for aesthetic drift when correctness passes.
|
|
@@ -33,6 +33,16 @@ case "$file_path" in
|
|
|
33
33
|
*) exit 0 ;;
|
|
34
34
|
esac
|
|
35
35
|
|
|
36
|
+
# A shell `case` `*` crosses `/`, so the match above also catches a receipt
|
|
37
|
+
# under memory/review/ or a retired entry under memory/archive/. Neither is
|
|
38
|
+
# a pen entry the index renders, so both exit here before the regen call.
|
|
39
|
+
case "$file_path" in
|
|
40
|
+
*/.claude/memory/review/* | */.canon/memory/review/* | \
|
|
41
|
+
*/.claude/memory/archive/* | */.canon/memory/archive/*)
|
|
42
|
+
exit 0
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
|
|
36
46
|
case "$file_path" in
|
|
37
47
|
*/.claude/memory/index.md | */.canon/memory/index.md) exit 0 ;;
|
|
38
48
|
esac
|