@erclx/canon 5.22.0 → 5.23.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/target-check/SKILL.md +9 -9
- package/claude/skills/target-check/references/domains.md +11 -5
- package/docs/agents/architecture-record.md +2 -2
- package/docs/agents/index.md +1 -1
- package/docs/agents/tasks-validate.md +4 -2
- package/docs/agents/tasks.md +1 -1
- package/docs/target/projects.md +1 -1
- package/package.json +1 -1
- package/src/commands/tasks.ts +16 -0
- package/src/tasks/validate.ts +65 -0
- package/standards/architecture.md +2 -2
- package/standards/board.md +2 -0
- package/standards/requirements.md +3 -1
- package/tooling/claude/seeds/.claude/hooks/path-form.sh +2 -2
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -2
- package/tooling/claude/seeds/.claude/hooks/{tasks-index.sh → records-index.sh} +42 -35
- package/tooling/claude/seeds/.claude/settings.json +1 -5
- package/tooling/claude/seeds/canon/ARCHITECTURE.md +1 -1
- package/tooling/claude/seeds/canon/REQUIREMENTS.md +2 -0
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +0 -95
|
@@ -26,7 +26,7 @@ CANON_NON_INTERACTIVE=1 canon sync --check . --json
|
|
|
26
26
|
CANON_NON_INTERACTIVE=1 canon context audit --json
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
The first answers five domains from one read, being `skew`, `domains`, `seeds`, `tooling`, and the layout fields `unmigrated`, `superseded`, and `reverse`. The second deepens a domain that first read only counts.
|
|
29
|
+
The first answers five domains from one read, being `skew`, `domains`, `seeds`, `tooling`, and the layout fields `unmigrated`, `superseded`, `unmovedSurfaces`, and `reverse`. The second deepens a domain that first read only counts.
|
|
30
30
|
|
|
31
31
|
Then run `CANON_NON_INTERACTIVE=1 canon records validate <kind> --json` once per record folder the target carries, taking `<kind>` from what is present under the record root rather than from a list held here.
|
|
32
32
|
|
|
@@ -76,14 +76,14 @@ Lead with the table, then one detail block per domain that is not current:
|
|
|
76
76
|
```markdown
|
|
77
77
|
Target check: `<path>`, against canon `<version>`.
|
|
78
78
|
|
|
79
|
-
| Domain | State | Evidence
|
|
80
|
-
| ---------------- | ------------------------- |
|
|
81
|
-
| Toolkit version | current / behind / unread | `skew.installed` against `latest`
|
|
82
|
-
| `canon/` folder | current / behind / unread |
|
|
83
|
-
| Governance rules | current / behind / unread | the per-state counts
|
|
84
|
-
| Tooling | current / behind / unread | what a sync would change
|
|
85
|
-
| Seeds | current / behind / unread | the per-state counts
|
|
86
|
-
| Claude harness | unchecked | no verb reads it
|
|
79
|
+
| Domain | State | Evidence |
|
|
80
|
+
| ---------------- | ------------------------- | ---------------------------------------------- |
|
|
81
|
+
| Toolkit version | current / behind / unread | `skew.installed` against `latest` |
|
|
82
|
+
| `canon/` folder | current / behind / unread | the layout fields, including `unmovedSurfaces` |
|
|
83
|
+
| Governance rules | current / behind / unread | the per-state counts |
|
|
84
|
+
| Tooling | current / behind / unread | what a sync would change |
|
|
85
|
+
| Seeds | current / behind / unread | the per-state counts |
|
|
86
|
+
| Claude harness | unchecked | no verb reads it |
|
|
87
87
|
|
|
88
88
|
No overall verdict. The harness row is unchecked rather than passing.
|
|
89
89
|
```
|
|
@@ -21,19 +21,25 @@ This domain is first because it conditions the other five. A target a release be
|
|
|
21
21
|
|
|
22
22
|
## 2. The `canon/` folder
|
|
23
23
|
|
|
24
|
-
Read
|
|
24
|
+
Read four fields off the same report, then two verbs:
|
|
25
25
|
|
|
26
26
|
- `unmigrated`: a domain still at the root layout with nothing under `.claude/`. The most urgent finding, because such a domain reports no drift of its own while being entirely behind.
|
|
27
27
|
- `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own.
|
|
28
|
+
- `unmovedSurfaces`: a surface held only at its `.claude/` spelling, each entry carrying `rel` and `movesTo`. Every other field reads clean for such a target, and the report's drift status ignores it by design, so read the array itself rather than an exit status.
|
|
28
29
|
- `reverse`: `unclaimed` names folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`.
|
|
29
30
|
- `canon context audit --json`: required sections, entry length, citations, and index drift across the context folder.
|
|
30
31
|
- `canon records validate <kind> --json`, once per record folder the target carries.
|
|
31
32
|
|
|
32
|
-
- Current: `unmigrated` and `
|
|
33
|
-
- Behind: any of those carrying an entry. Name the entry and its attribution
|
|
34
|
-
- Unread: `historyUnavailable` set on `reverse`, a `reverse` key the report does not carry at all, or either verb refusing. A missing
|
|
33
|
+
- Current: `unmigrated`, `superseded`, and `unmovedSurfaces` all empty, `reverse.unclaimed` carrying nothing outside `project` attribution, and both verbs finding nothing.
|
|
34
|
+
- Behind: any of those carrying an entry. Name the entry and its attribution, and for `unmovedSurfaces` each `rel` with its `movesTo`.
|
|
35
|
+
- Unread: `historyUnavailable` set on `reverse`, a `reverse` or `unmovedSurfaces` key the report does not carry at all, or either verb refusing. A missing key is a binary predating the field rather than a target with nothing to report, so name the installed version beside it.
|
|
35
36
|
|
|
36
|
-
Routes by finding.
|
|
37
|
+
Routes by finding.
|
|
38
|
+
|
|
39
|
+
- `unmovedSurfaces` routes to `canon migrate surface-roots`, which only reports its plan until `--write` is passed. Name the verb with each entry's `rel` and `movesTo`, and run neither form.
|
|
40
|
+
- `unmigrated` routes to a relocation the user runs, since no command moves it.
|
|
41
|
+
- `superseded` routes to a split the user makes, since no command moves the content and no skill proposes it.
|
|
42
|
+
- An `unclaimed` folder carrying `project` attribution routes nowhere, since history proved the project owns it.
|
|
37
43
|
|
|
38
44
|
## 3. Governance rules
|
|
39
45
|
|
|
@@ -19,7 +19,7 @@ What the derivation costs where it does apply is that the ceiling rises when a d
|
|
|
19
19
|
|
|
20
20
|
The decision word check counts each decision's prose against the cap the record states in a clause of the form `at most 150 words a decision`. The count runs below the `###` heading with fenced lines dropped, so neither the heading nor a template block inside a decision counts, and the report and the gate read the same number. The JSON record carries the clause as `architecture.wordCap`, and the audit catalog counts the decisions past it as `recordOverWords`, absent where the record states no clause.
|
|
21
21
|
|
|
22
|
-
The risk bullet check counts every list item under `## Risks / open questions`, nested ones included, against a clause of the form `at most 6 risk bullets`, since a nested bullet is weight the reader pays for like any other. Both clauses can share the entry cap's sentence, and each gates only a record stating it, for the reason the cap does. A record stating a risk cap and carrying no Risks section is reported as absent rather than passed as zero bullets. The JSON record carries the clause as `architecture.riskCap` and the count as `architecture.risksBullets`, and the audit catalog counts a section past its cap as `recordOverRisks`, absent where the record states no clause.
|
|
22
|
+
The risk bullet check counts every list item under `## Risks / open questions`, nested ones included, against a clause of the form `at most 6 risk bullets`, since a nested bullet is weight the reader pays for like any other. Both clauses can share the entry cap's sentence, and each gates only a record stating it, for the reason the cap does. The record `canon claude init` seeds into a new project states all three caps in that one sentence, so a new project gates on them from its first decision, and a project that installed earlier keeps whatever its own record states. A record stating a risk cap and carrying no Risks section is reported as absent rather than passed as zero bullets. The JSON record carries the clause as `architecture.riskCap` and the count as `architecture.risksBullets`, and the audit catalog counts a section past its cap as `recordOverRisks`, absent where the record states no clause.
|
|
23
23
|
|
|
24
24
|
The coverage report classifies each decision as carrying a countable claim, a structural invariant, or neither, then reports each testable entry against whether it names a check that exists. A countable claim carries a figure a run could recompute, and an invariant quantifies over a named tree closely enough that a walk could falsify it. A check is a `scripts/**.sh` path the entry spells that is on disk, or a `canon` invocation matching a registered audit, so coverage reads the entry rather than the tree and a claim some check happens to cover without the entry saying so reads as unchecked.
|
|
25
25
|
|
|
@@ -33,4 +33,4 @@ The `## Risks / open questions` section is weighed the same way, reported apart
|
|
|
33
33
|
|
|
34
34
|
## The requirements record
|
|
35
35
|
|
|
36
|
-
The audit reads `canon/REQUIREMENTS.md` under its own step and counts every word below the frontmatter against the cap the record states in a clause of the form `This record holds at most 600 words.` The cap is the record's own, so a record stating none is measured and never gated, and a project with no record is reported as out of scope. The JSON record carries the reading as `requirements`, null where the project carries no record and absent under `--citations-only`, and the audit catalog counts a record past its cap as `requirementsOverWords`.
|
|
36
|
+
The audit reads `canon/REQUIREMENTS.md` under its own step and counts every word below the frontmatter against the cap the record states in a clause of the form `This record holds at most 600 words.` The cap is the record's own, so a record stating none is measured and never gated, and a project with no record is reported as out of scope. The seeded record states the 600-word cap. The JSON record carries the reading as `requirements`, null where the project carries no record and absent under `--citations-only`, and the audit catalog counts a record past its cap as `requirementsOverWords`.
|
package/docs/agents/index.md
CHANGED
|
@@ -62,7 +62,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
62
62
|
- [Targets](targets.md): The projects this toolkit installed into, the record the install writes against the sweep that backs it, what bounds each answer, and the cross-target pull request read
|
|
63
63
|
- [Tasks archive](tasks-archive.md): Archiving a shipped task by stem or pull request, declining one decided against, the refusal reasons each carries, and how the plan and ready folder move with the task
|
|
64
64
|
- [Tasks plans](tasks-plans.md): The five verbs that read or write a task's plan, being where it sits and who cites it, whether it waits on the operator, the branch it derives, what a branch reached against what it declared, and writing its Plan line
|
|
65
|
-
- [Tasks validate](tasks-validate.md): Validating the board against the tree, the seven checks, the untested, claims, and unplaced arrays, the exit codes, and the refusal reasons
|
|
65
|
+
- [Tasks validate](tasks-validate.md): Validating the board against the tree, the seven checks, the untested, claims, wide, and unplaced arrays, the exit codes, and the refusal reasons
|
|
66
66
|
- [Tasks](tasks.md): Claiming the next phase label, recording a pull request number and closing an outcome, listing each task's readiness, and where archiving, the plan verbs, and board validation are described
|
|
67
67
|
- [Teach authoring](teach-authoring.md): Resolving what the next lesson needs before it is written, rendering a lesson body's blocks to HTML, and rewriting the root listing, a contents page, and each lesson's chrome and quiz stepper from its marker regions
|
|
68
68
|
- [Teach](teach.md): Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, viewing one with its chrome refreshed, where the lesson authoring verbs are described, the refusal reasons, and why every write here runs through a verb
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Tasks validate
|
|
3
|
-
description: Validating the board against the tree, the seven checks, the untested, claims, and unplaced arrays, the exit codes, and the refusal reasons
|
|
3
|
+
description: Validating the board against the tree, the seven checks, the untested, claims, wide, and unplaced arrays, the exit codes, and the refusal reasons
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Tasks validate
|
|
@@ -103,7 +103,9 @@ That claim collides with every row a later session writes under the folder, and
|
|
|
103
103
|
|
|
104
104
|
The scan reaches `## Run now` and stops, where the collision check stops. A cell in another group describes work nobody has planned, written as a sentence and rewritten once a plan exists, so a claim read off one reports on prose rather than on a file set. A parked folder claim surfaces when its row is promoted, which is when the cell becomes something a dispatcher can act on.
|
|
105
105
|
|
|
106
|
-
A
|
|
106
|
+
A code span in any cell that the preview cannot break lands in a fourth array, `wide`, with the same `group`, `subject`, and `message` fields. The scan measures a span's longest whitespace-free run and flags one past 60 characters, or a brace-glob at any width. A 57-character span fit the pane and a 78-character one did not, so the limit is a heuristic and the array moves no exit code. A link target is skipped, since only its label renders, and the scan reaches every group, since the overflow it exists for sat under `## Up next`.
|
|
107
|
+
|
|
108
|
+
A task file neither surface names lands in a fifth array, on the same reasoning:
|
|
107
109
|
|
|
108
110
|
```json
|
|
109
111
|
{
|
package/docs/agents/tasks.md
CHANGED
|
@@ -118,6 +118,6 @@ canon tasks list --json | jq -r '.tasks[] | select(.readiness == "backlog") | .s
|
|
|
118
118
|
|
|
119
119
|
## Validate
|
|
120
120
|
|
|
121
|
-
`canon tasks validate` reports what each row of `priority.md` and each line of `backlog.md` claims against what the tree holds. Its seven checks, the
|
|
121
|
+
`canon tasks validate` reports what each row of `priority.md` and each line of `backlog.md` claims against what the tree holds. Its seven checks, the four arrays that move no exit code, and the refusals are in `tasks-validate.md`.
|
|
122
122
|
|
|
123
123
|
For the board format, see `standards/board.md`. For the `Pull request:` line and the archive rules, see `standards/tasks.md`.
|
package/docs/target/projects.md
CHANGED
|
@@ -71,7 +71,7 @@ Keep the `## Scripts` table in `canon/context/development.md` current as scripts
|
|
|
71
71
|
|
|
72
72
|
Scaffold installs tooling and seeds. It does not fill the planning docs or the design system. Complete those before the first feature session:
|
|
73
73
|
|
|
74
|
-
1. Fill `canon/REQUIREMENTS.md` and `canon/ARCHITECTURE.md`. The seed provides the files, the scope and decisions are yours to write.
|
|
74
|
+
1. Fill `canon/REQUIREMENTS.md` and `canon/ARCHITECTURE.md`. The seed provides the files, the scope and decisions are yours to write. Each seed states its own caps, 12 decisions of at most 150 words and 6 risk bullets for the architecture record and 600 words for the requirements, and `canon context audit` gates on them. Edit a number in the record to loosen it.
|
|
75
75
|
2. For a UI project, invoke `canon:design-extract` to draft `canon/DESIGN.md`. With no UI code yet it takes the greenfield path and proposes tokens from the requirements and a `## Personality` section. Skip for non-UI projects.
|
|
76
76
|
3. Optionally invoke `canon:draft-diagram` to draft entries under `.canon/diagrams/` from the architecture and the requirements. One file per diagram kind, so a later refresh of one kind leaves the others untouched. It renders each diagram it writes to verify the layout, which downloads the Mermaid CLI on first use and takes about 15 seconds.
|
|
77
77
|
4. Start the feature loop. See [AI workflow](../workflow/ai-workflow.md) for the per-feature sequence.
|
package/package.json
CHANGED
package/src/commands/tasks.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import {
|
|
43
43
|
type Finding,
|
|
44
44
|
type FolderClaim,
|
|
45
|
+
type WideToken,
|
|
45
46
|
type Unplaced,
|
|
46
47
|
type Untested,
|
|
47
48
|
type ValidateOutcome,
|
|
@@ -1411,6 +1412,16 @@ function reportValidation(
|
|
|
1411
1412
|
for (const claim of outcome.claims) logWarn(describeClaim(claim))
|
|
1412
1413
|
}
|
|
1413
1414
|
|
|
1415
|
+
// The width limit is a heuristic over one preview pane, so a wide token
|
|
1416
|
+
// reports and moves no exit code, and a measure failing on a cell that
|
|
1417
|
+
// fits would teach a reader to skip it.
|
|
1418
|
+
logStep('Wide tokens')
|
|
1419
|
+
if (outcome.wide.length === 0) {
|
|
1420
|
+
logInfo('every code span fits the preview pane')
|
|
1421
|
+
} else {
|
|
1422
|
+
for (const token of outcome.wide) logWarn(describeWide(token))
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1414
1425
|
// Unplaced is the normal state between a session filing a task and an
|
|
1415
1426
|
// orchestrator placing its row, so it moves no exit code. It still reports,
|
|
1416
1427
|
// since it is the only local detector for a row a hand-edit dropped or a
|
|
@@ -1436,6 +1447,7 @@ function reportValidation(
|
|
|
1436
1447
|
findings: outcome.findings,
|
|
1437
1448
|
untested: outcome.untested,
|
|
1438
1449
|
claims: outcome.claims,
|
|
1450
|
+
wide: outcome.wide,
|
|
1439
1451
|
unplaced: outcome.unplaced,
|
|
1440
1452
|
})}\n`,
|
|
1441
1453
|
)
|
|
@@ -1457,6 +1469,10 @@ function describeClaim(claim: FolderClaim): string {
|
|
|
1457
1469
|
return `${claim.group}: ${claim.subject} ${claim.message}`
|
|
1458
1470
|
}
|
|
1459
1471
|
|
|
1472
|
+
function describeWide(token: WideToken): string {
|
|
1473
|
+
return `${token.group}: ${token.subject} ${token.message}`
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1460
1476
|
function describeUnplaced(task: Unplaced): string {
|
|
1461
1477
|
return `${task.subject} ${task.message}`
|
|
1462
1478
|
}
|
package/src/tasks/validate.ts
CHANGED
|
@@ -89,6 +89,20 @@ export interface FolderClaim {
|
|
|
89
89
|
readonly message: string
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* A code span in a board cell that the markdown preview cannot break, so it
|
|
94
|
+
* pushes the table past the pane and makes it scroll sideways. Either the
|
|
95
|
+
* longest whitespace-free run is wider than `WIDE_LIMIT`, or the span is a
|
|
96
|
+
* brace-glob, which is written out as separate paths. The limit is a heuristic
|
|
97
|
+
* over one pane, which is why it reports beside the findings and moves no exit
|
|
98
|
+
* code.
|
|
99
|
+
*/
|
|
100
|
+
export interface WideToken {
|
|
101
|
+
readonly group: BoardGroup
|
|
102
|
+
readonly subject: string
|
|
103
|
+
readonly message: string
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
/**
|
|
93
107
|
* A task file neither surface names. That is the normal state between a
|
|
94
108
|
* session filing it and a live orchestrator placing it, so it reports beside
|
|
@@ -124,6 +138,8 @@ export interface BoardRow {
|
|
|
124
138
|
readonly ordinal: OrdinalWord | 'last' | undefined
|
|
125
139
|
/** Whether a `Waiting on` cell ranks its row against a sibling row or a class of rows, false when the group fixes no such column. */
|
|
126
140
|
readonly ranked: boolean
|
|
141
|
+
/** Every cell of the row as `splitCells` read it, which the wide-token scan walks. */
|
|
142
|
+
readonly cells: readonly string[]
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
export interface ValidateReport {
|
|
@@ -135,6 +151,7 @@ export interface ValidateReport {
|
|
|
135
151
|
readonly findings: readonly Finding[]
|
|
136
152
|
readonly untested: readonly Untested[]
|
|
137
153
|
readonly claims: readonly FolderClaim[]
|
|
154
|
+
readonly wide: readonly WideToken[]
|
|
138
155
|
readonly unplaced: readonly Unplaced[]
|
|
139
156
|
}
|
|
140
157
|
|
|
@@ -461,6 +478,7 @@ export function readBoard(text: string): {
|
|
|
461
478
|
waiting,
|
|
462
479
|
ordinal: waiting ? readOrdinal(waiting) : undefined,
|
|
463
480
|
ranked: waiting ? readRank(waiting) : false,
|
|
481
|
+
cells,
|
|
464
482
|
})
|
|
465
483
|
}
|
|
466
484
|
|
|
@@ -992,6 +1010,52 @@ function checkFolderClaims(
|
|
|
992
1010
|
return claims
|
|
993
1011
|
}
|
|
994
1012
|
|
|
1013
|
+
/**
|
|
1014
|
+
* The widest the longest unbroken run in a code span may be before a table
|
|
1015
|
+
* scrolls in the preview pane. A 57-character span fit and a 78-character one
|
|
1016
|
+
* did not, so the limit sits at the nearest round number above the first.
|
|
1017
|
+
*/
|
|
1018
|
+
const WIDE_LIMIT = 60
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Reports a code span a table cell cannot wrap. A span breaks only at
|
|
1022
|
+
* whitespace, so the longest whitespace-free run is what is measured, and a
|
|
1023
|
+
* link target is stripped first because only its label renders. Every group is
|
|
1024
|
+
* scanned, since the overflow this exists for happened under `## Up next`,
|
|
1025
|
+
* where the folder-claim scan would not have looked. Prose breaks at spaces
|
|
1026
|
+
* and bare words at hyphens, so only a span is read.
|
|
1027
|
+
*/
|
|
1028
|
+
function checkWideTokens(rows: readonly BoardRow[]): WideToken[] {
|
|
1029
|
+
const wide: WideToken[] = []
|
|
1030
|
+
|
|
1031
|
+
for (const row of rows) {
|
|
1032
|
+
for (const cell of row.cells) {
|
|
1033
|
+
const spans = cell.replace(/\]\([^)]*\)/g, ']').match(/`[^`]+`/g) ?? []
|
|
1034
|
+
|
|
1035
|
+
for (const span of spans) {
|
|
1036
|
+
const text = span.slice(1, -1)
|
|
1037
|
+
const run = Math.max(...text.split(/\s+/).map((part) => part.length))
|
|
1038
|
+
|
|
1039
|
+
if (/\{[^}]*,[^}]*\}/.test(text)) {
|
|
1040
|
+
wide.push({
|
|
1041
|
+
group: row.group,
|
|
1042
|
+
subject: subjectOf(row),
|
|
1043
|
+
message: `holds the brace-glob ${span}, which renders as one unbroken token. Write it out as separate paths.`,
|
|
1044
|
+
})
|
|
1045
|
+
} else if (run > WIDE_LIMIT) {
|
|
1046
|
+
wide.push({
|
|
1047
|
+
group: row.group,
|
|
1048
|
+
subject: subjectOf(row),
|
|
1049
|
+
message: `holds a ${run}-character token, ${span}, past the ${WIDE_LIMIT} a preview pane fits, so its table scrolls sideways.`,
|
|
1050
|
+
})
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return wide
|
|
1057
|
+
}
|
|
1058
|
+
|
|
995
1059
|
/**
|
|
996
1060
|
* Reports a task file neither the board nor the backlog names. Under the
|
|
997
1061
|
* roster-checked hand-off, filing a task and placing its row are two acts a
|
|
@@ -1327,6 +1391,7 @@ export async function validateBoard(
|
|
|
1327
1391
|
findings,
|
|
1328
1392
|
untested: parked.untested,
|
|
1329
1393
|
claims: checkFolderClaims(rows, root),
|
|
1394
|
+
wide: checkWideTokens(rows),
|
|
1330
1395
|
unplaced: checkUnplaced(rows, backlog, stems),
|
|
1331
1396
|
}
|
|
1332
1397
|
}
|
|
@@ -88,7 +88,7 @@ Every session pays for this file before any work starts, so a heavy read is a re
|
|
|
88
88
|
- `This record holds at most <n> decisions.`
|
|
89
89
|
- `at most <n> words a decision`, counting a decision's prose below its heading, fenced lines left out
|
|
90
90
|
- `at most <n> risk bullets`, counting every list item under `## Risks / open questions`, nested ones included
|
|
91
|
-
- The three can share one sentence, such as `This record holds at most 12 decisions, at most 150 words a decision, and at most 6 risk bullets.` Each cap is the record's own, so a record stating none is measured and never gated
|
|
91
|
+
- The three can share one sentence, such as `This record holds at most 12 decisions, at most 150 words a decision, and at most 6 risk bullets.` Each cap is the record's own, so a record stating none is measured and never gated. The template and the seed a new project installs state all three, and a project loosens one by editing the number in its own record.
|
|
92
92
|
- Set the decision cap near one decision per slot plus a small margin, the word cap near what one trade takes to state, about 150, and the risk cap at what a reader holds in mind at once, about 6.
|
|
93
93
|
- At the decision cap, merge two decisions or retire one before adding another, and name which in the change that does it. Never pack two decisions under one heading, which the count cannot see.
|
|
94
94
|
- At the word cap, cut a decision to the choice, the alternative that lost, the reason, and the revisit sentence. What it sheds is a mechanism or a measurement, which goes to the domain context entry it constrains, never under a second heading.
|
|
@@ -106,7 +106,7 @@ The revisit sentence closes the reasoning of every decision. The anchor sentence
|
|
|
106
106
|
|
|
107
107
|
## Overview
|
|
108
108
|
|
|
109
|
-
This record holds at most 12 decisions. Every decision closes with a revisit sentence.
|
|
109
|
+
This record holds at most 12 decisions, at most 150 words a decision, and at most 6 risk bullets. Every decision closes with a revisit sentence.
|
|
110
110
|
|
|
111
111
|
## Key technical decisions
|
|
112
112
|
|
package/standards/board.md
CHANGED
|
@@ -57,6 +57,8 @@ Each group fixes its own columns, which follow from the test above it rather tha
|
|
|
57
57
|
|
|
58
58
|
A `Touches` cell is copied from its plan's own list, so it states what the branch sets out to write rather than a bound on it, and a branch outgrows it while the row still reads as it did at dispatch. Correct the cell from the branch rather than from the plan once one is running, since the plan is the prediction that already went stale and only the diff says what was written. `canon tasks plan-reach <plan>` reads that diff against every live plan and every cell in this group, and the row's owner is the one who writes the correction: a worker never edits this board.
|
|
59
59
|
|
|
60
|
+
A cell holds no code span whose longest unbroken run passes 60 characters, since a span has no break point and a wider one makes its table scroll sideways in the preview. Write a brace-glob out as separate paths for the same reason. `canon tasks validate` reports both in its `wide` array and moves no exit code.
|
|
61
|
+
|
|
60
62
|
### Row order
|
|
61
63
|
|
|
62
64
|
Row position inside `## Needs a plan` is the order those tasks get planned in, top first. The three tests answer whether a task can start, which is mechanical, and none of them answers which task is worth starting, which is a judgment no column holds. Position is where that judgment is recorded, so the top row is the answer to what to plan next and a reader needs no other surface to get it. The other two groups take the same reading, and it costs them little, since a group holding what is already planned is short by construction.
|
|
@@ -40,7 +40,7 @@ Use `## Problem`, `## Goals`, `## Non-goals`, `## MVP features`, and `## Constra
|
|
|
40
40
|
|
|
41
41
|
## Length
|
|
42
42
|
|
|
43
|
-
This file loads into every session, so its weight is paid before any work starts. Cap it by stating the clause `This record holds at most <n> words.` in the record itself, where a checker reads it and counts every word below the frontmatter. The cap is the record's own, so a record stating none is measured and never gated.
|
|
43
|
+
This file loads into every session, so its weight is paid before any work starts. Cap it by stating the clause `This record holds at most <n> words.` in the record itself, where a checker reads it and counts every word below the frontmatter. The cap is the record's own, so a record stating none is measured and never gated. The template and the seed a new project installs state it at 600, and a project loosens it by editing the number in its own record. At the cap, cut a goal or a constraint that no longer changes a decision before adding one.
|
|
44
44
|
|
|
45
45
|
## Lifecycle
|
|
46
46
|
|
|
@@ -59,6 +59,8 @@ State each entry as an outcome the consumer reaches, never as the mechanism that
|
|
|
59
59
|
```markdown
|
|
60
60
|
# Requirements
|
|
61
61
|
|
|
62
|
+
This record holds at most 600 words.
|
|
63
|
+
|
|
62
64
|
## Problem
|
|
63
65
|
|
|
64
66
|
## Goals
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
# its surface, not from this hook or the output rule.
|
|
13
13
|
#
|
|
14
14
|
# The worktree branch comes from the path rather than from `git rev-parse`,
|
|
15
|
-
# the way `
|
|
16
|
-
#
|
|
15
|
+
# the way `records-index.sh` already derives its main root from a path suffix
|
|
16
|
+
# instead of the session. A git call would answer for
|
|
17
17
|
# whatever directory the hook's own process happens to run in, which is not
|
|
18
18
|
# necessarily the worktree the write came from.
|
|
19
19
|
|
|
@@ -39,8 +39,8 @@ url=$(printf '%s' "$stdout" | grep -Eo 'https://github\.com/[^[:space:]]+/pull/[
|
|
|
39
39
|
# CLAUDE_PROJECT_DIR is the session's own worktree rather than the main root,
|
|
40
40
|
# so a worker building in a linked worktree would log into a folder that dies
|
|
41
41
|
# with the worktree. The log is a denominator across a wave rather than a
|
|
42
|
-
# per-session record, so strip back to the main root the way
|
|
43
|
-
#
|
|
42
|
+
# per-session record, so strip back to the main root the way
|
|
43
|
+
# records-index.sh already derives its own, off a path suffix.
|
|
44
44
|
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
45
45
|
case "$root" in
|
|
46
46
|
*/.claude/worktrees/*) root="${root%/.claude/worktrees/*}" ;;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
# Regenerates
|
|
3
|
+
# Regenerates the index.md of a gitignored record folder, `tasks` or `memory`,
|
|
4
|
+
# after a file in it changes.
|
|
4
5
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
6
|
+
# These folders are gitignored, so the whole-repo walk in `bun run check` drops
|
|
7
|
+
# them and never regenerates their index. Naming the file as a positional
|
|
7
8
|
# argument to `canon indexes regen` below bypasses that filter, which makes this
|
|
8
|
-
# hook the only trigger that reaches
|
|
9
|
+
# hook the only trigger that reaches either folder.
|
|
9
10
|
|
|
10
11
|
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
11
12
|
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
@@ -25,45 +26,51 @@ esac
|
|
|
25
26
|
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
|
|
26
27
|
[ -n "$file_path" ] || exit 0
|
|
27
28
|
|
|
28
|
-
# Both record roots, since a project the move has reached keeps its
|
|
29
|
+
# Both record roots, since a project the move has reached keeps its records under
|
|
29
30
|
# `.canon/` and a guard fixed at the old spelling stops matching with nothing
|
|
30
31
|
# said. The index then goes stale while every save reports success.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# taken out of.
|
|
40
|
-
case "$file_path" in
|
|
41
|
-
*/.claude/tasks/index.md | */.claude/tasks/archive/* | */.claude/tasks/declined/*) exit 0 ;;
|
|
42
|
-
*/.canon/tasks/index.md | */.canon/tasks/archive/* | */.canon/tasks/declined/*) exit 0 ;;
|
|
43
|
-
esac
|
|
44
|
-
|
|
32
|
+
#
|
|
33
|
+
# A shell `case` `*` crosses `/`, so each arm also matches a file in a subfolder
|
|
34
|
+
# the index does not render: an archived or declined task, a memory receipt, a
|
|
35
|
+
# retired memory entry, or the index itself. Those exit here, before the regen
|
|
36
|
+
# call. The exclusions are per folder, since a task under `review/` is indexed
|
|
37
|
+
# and a memory `declined/` is not special. The arms are ordered tasks first,
|
|
38
|
+
# and no path carries both segments today.
|
|
39
|
+
#
|
|
45
40
|
# The walk-up boundary has to come from the path, not from the session. Shared
|
|
46
41
|
# scratch resolves at the main worktree root, so a session inside a linked
|
|
47
42
|
# worktree passes a path that sits outside its own project directory and the
|
|
48
|
-
# default boundary would reject it.
|
|
49
|
-
#
|
|
50
|
-
# The index this would have rebuilt is read out of the same branch, so both
|
|
51
|
-
# messages below name the file that actually went stale rather than one root's
|
|
52
|
-
# spelling of it.
|
|
43
|
+
# default boundary would reject it. The messages below name the index read out
|
|
44
|
+
# of the same match, so they name the file that actually went stale.
|
|
53
45
|
case "$file_path" in
|
|
54
|
-
*/.canon/tasks/*)
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
*/.canon/tasks/index.md | */.canon/tasks/archive/* | */.canon/tasks/declined/*) exit 0 ;;
|
|
47
|
+
*/.claude/tasks/index.md | */.claude/tasks/archive/* | */.claude/tasks/declined/*) exit 0 ;;
|
|
48
|
+
*/.canon/memory/index.md | */.canon/memory/review/* | */.canon/memory/archive/*) exit 0 ;;
|
|
49
|
+
*/.claude/memory/index.md | */.claude/memory/review/* | */.claude/memory/archive/*) exit 0 ;;
|
|
50
|
+
*/.canon/tasks/*.md)
|
|
51
|
+
folder=tasks
|
|
52
|
+
base=.canon
|
|
53
|
+
;;
|
|
54
|
+
*/.claude/tasks/*.md)
|
|
55
|
+
folder=tasks
|
|
56
|
+
base=.claude
|
|
57
57
|
;;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
*/.canon/memory/*.md)
|
|
59
|
+
folder=memory
|
|
60
|
+
base=.canon
|
|
61
61
|
;;
|
|
62
|
+
*/.claude/memory/*.md)
|
|
63
|
+
folder=memory
|
|
64
|
+
base=.claude
|
|
65
|
+
;;
|
|
66
|
+
*) exit 0 ;;
|
|
62
67
|
esac
|
|
68
|
+
root="${file_path%/"$base/$folder"/*}"
|
|
69
|
+
index="$base/$folder/index.md"
|
|
63
70
|
[ -n "$root" ] || exit 0
|
|
64
71
|
|
|
65
72
|
# Report a missing CLI rather than exiting quietly. The path guard above already
|
|
66
|
-
# scopes this to a
|
|
73
|
+
# scopes this to a record-file edit, so the message only fires where the stale
|
|
67
74
|
# index it warns about is the actual outcome.
|
|
68
75
|
if ! command -v canon >/dev/null 2>&1; then
|
|
69
76
|
msg="canon is not on PATH, so $index was not regenerated and is now stale. Install the toolkit CLI or run canon indexes regen by hand."
|
|
@@ -73,18 +80,18 @@ if ! command -v canon >/dev/null 2>&1; then
|
|
|
73
80
|
fi
|
|
74
81
|
|
|
75
82
|
# `--no-stage` because a hook has no business touching the index. On a project
|
|
76
|
-
# whose
|
|
77
|
-
# files to whatever commit is being assembled.
|
|
83
|
+
# whose folder is not gitignored, the default auto-stage would silently add
|
|
84
|
+
# record files to whatever commit is being assembled.
|
|
78
85
|
output=$(canon indexes regen --no-stage --root "$root" "$file_path" 2>&1) && exit 0
|
|
79
86
|
|
|
80
|
-
# Regen failed, which on
|
|
87
|
+
# Regen failed, which on these folders means a file is missing `title` or
|
|
81
88
|
# `description`. Report it. Nothing else can: the folder is gitignored, so the
|
|
82
89
|
# whole-repo walk never reaches it and no gate stage will ever fail on a stale
|
|
83
90
|
# index. Staying quiet here is what makes the drift permanent.
|
|
84
91
|
errors=$(printf '%s\n' "$output" | grep '^ERROR: ' | head -5)
|
|
85
92
|
[ -n "$errors" ] || errors="$output"
|
|
86
93
|
|
|
87
|
-
msg="
|
|
94
|
+
msg="The $folder index regen failed, so $index is now stale. Fix the frontmatter and save again. $errors"
|
|
88
95
|
jq -nc --arg msg "$msg" \
|
|
89
96
|
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|
|
90
97
|
exit 0
|
|
@@ -39,11 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"type": "command",
|
|
46
|
-
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/memory-index.sh"
|
|
42
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/records-index.sh"
|
|
47
43
|
},
|
|
48
44
|
{
|
|
49
45
|
"type": "command",
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
# Regenerates .claude/memory/index.md after a memory file changes.
|
|
4
|
-
#
|
|
5
|
-
# The memory folder is gitignored, so the whole-repo walk in `bun run check`
|
|
6
|
-
# drops it and never regenerates this index. Naming the file as a positional
|
|
7
|
-
# argument to `canon indexes regen` below bypasses that filter, which makes this
|
|
8
|
-
# hook the only trigger that reaches the folder.
|
|
9
|
-
|
|
10
|
-
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
11
|
-
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
12
|
-
# rather than `timeout cat`, which macOS does not ship.
|
|
13
|
-
IFS= read -r -d '' -t 2 input
|
|
14
|
-
[ -n "$input" ] || {
|
|
15
|
-
printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
|
|
16
|
-
exit 1
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
|
|
20
|
-
case "$tool" in
|
|
21
|
-
Write | Edit | MultiEdit) ;;
|
|
22
|
-
*) exit 0 ;;
|
|
23
|
-
esac
|
|
24
|
-
|
|
25
|
-
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
|
|
26
|
-
[ -n "$file_path" ] || exit 0
|
|
27
|
-
|
|
28
|
-
# Both record roots, since a project the move has reached keeps its memory pen
|
|
29
|
-
# under `.canon/` and a guard fixed at the old spelling stops matching with
|
|
30
|
-
# nothing said. The index then goes stale while every save reports success.
|
|
31
|
-
case "$file_path" in
|
|
32
|
-
*/.claude/memory/*.md | */.canon/memory/*.md) ;;
|
|
33
|
-
*) exit 0 ;;
|
|
34
|
-
esac
|
|
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
|
-
|
|
46
|
-
case "$file_path" in
|
|
47
|
-
*/.claude/memory/index.md | */.canon/memory/index.md) exit 0 ;;
|
|
48
|
-
esac
|
|
49
|
-
|
|
50
|
-
# The walk-up boundary has to come from the path, not from the session. Shared
|
|
51
|
-
# scratch resolves at the main worktree root, so a session inside a linked
|
|
52
|
-
# worktree passes a path that sits outside its own project directory and the
|
|
53
|
-
# default boundary would reject it.
|
|
54
|
-
#
|
|
55
|
-
# The index this would have rebuilt is read out of the same branch, so both
|
|
56
|
-
# messages below name the file that actually went stale rather than one root's
|
|
57
|
-
# spelling of it.
|
|
58
|
-
case "$file_path" in
|
|
59
|
-
*/.canon/memory/*)
|
|
60
|
-
root="${file_path%/.canon/memory/*}"
|
|
61
|
-
index=".canon/memory/index.md"
|
|
62
|
-
;;
|
|
63
|
-
*)
|
|
64
|
-
root="${file_path%/.claude/memory/*}"
|
|
65
|
-
index=".claude/memory/index.md"
|
|
66
|
-
;;
|
|
67
|
-
esac
|
|
68
|
-
[ -n "$root" ] || exit 0
|
|
69
|
-
|
|
70
|
-
# Report a missing CLI rather than exiting quietly. The path guard above already
|
|
71
|
-
# scopes this to a memory-file edit, so the message only fires where the stale
|
|
72
|
-
# index it warns about is the actual outcome.
|
|
73
|
-
if ! command -v canon >/dev/null 2>&1; then
|
|
74
|
-
msg="canon is not on PATH, so $index was not regenerated and is now stale. Install the toolkit CLI or run canon indexes regen by hand."
|
|
75
|
-
jq -nc --arg msg "$msg" \
|
|
76
|
-
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|
|
77
|
-
exit 0
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
# `--no-stage` because a hook has no business touching the index. On a project
|
|
81
|
-
# whose memory folder is not gitignored, the default auto-stage would silently
|
|
82
|
-
# add memory files to whatever commit is being assembled.
|
|
83
|
-
output=$(canon indexes regen --no-stage --root "$root" "$file_path" 2>&1) && exit 0
|
|
84
|
-
|
|
85
|
-
# Regen failed, which on this folder means a memory file is missing `title` or
|
|
86
|
-
# `description`. Report it. Nothing else can: the folder is gitignored, so the
|
|
87
|
-
# whole-repo walk never reaches it and no gate stage will ever fail on a stale
|
|
88
|
-
# index. Staying quiet here is what makes the drift permanent.
|
|
89
|
-
errors=$(printf '%s\n' "$output" | grep '^ERROR: ' | head -5)
|
|
90
|
-
[ -n "$errors" ] || errors="$output"
|
|
91
|
-
|
|
92
|
-
msg="Memory index regen failed, so $index is now stale. Fix the frontmatter and save again. $errors"
|
|
93
|
-
jq -nc --arg msg "$msg" \
|
|
94
|
-
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|
|
95
|
-
exit 0
|