@erclx/canon 4.121.0 → 4.123.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +6 -5
- package/claude/skills/auto-ship/references/tier-failures.md +7 -4
- package/claude/skills/backlog-triage/REQUIREMENT.md +47 -0
- package/claude/skills/backlog-triage/SKILL.md +139 -0
- package/claude/skills/canon-rollout/SKILL.md +6 -6
- package/claude/skills/context-fold/REQUIREMENT.md +75 -0
- package/claude/skills/context-fold/SKILL.md +162 -0
- package/claude/skills/{docs-fold → context-fold}/references/anchor-sweep.md +1 -1
- package/claude/skills/{docs-fold → context-fold}/references/classify.md +4 -2
- package/claude/skills/context-fold/references/context-refresh.md +48 -0
- package/claude/skills/context-fold/references/promoted-pages.md +29 -0
- package/claude/skills/context-fold/references/receipt-sweep.md +43 -0
- package/claude/skills/context-fold/references/unusable-baseline.md +25 -0
- package/claude/skills/{docs-fold → context-fold}/references/wireframe-sweep.md +1 -1
- package/claude/skills/docs-fold/REQUIREMENT.md +8 -58
- package/claude/skills/docs-fold/SKILL.md +3 -278
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
- package/claude/skills/docs-sync/SKILL.md +1 -1
- package/claude/skills/draft-context/REQUIREMENT.md +2 -2
- package/claude/skills/draft-context/SKILL.md +3 -3
- package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +2 -2
- package/claude/skills/git-pr/SKILL.md +8 -146
- package/claude/skills/git-pr/references/evidence.md +114 -0
- package/claude/skills/git-pr/references/labels.md +20 -0
- package/claude/skills/git-pr/references/task-number.md +26 -0
- package/claude/skills/git-ship/SKILL.md +5 -4
- package/claude/skills/memory-capture/REQUIREMENT.md +2 -2
- package/claude/skills/memory-capture/SKILL.md +19 -9
- package/claude/skills/memory-review/REQUIREMENT.md +5 -3
- package/claude/skills/memory-review/SKILL.md +22 -107
- package/claude/skills/memory-review/references/later-phases.md +111 -0
- package/claude/skills/memory-review/references/receipt-format.md +19 -3
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/review-branch/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +4 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
- package/claude/skills/session-resume/REQUIREMENT.md +1 -1
- package/claude/skills/session-resume/SKILL.md +4 -3
- package/claude/skills/session-worktree/SKILL.md +4 -10
- package/claude/skills/task-board/REQUIREMENT.md +1 -1
- package/claude/skills/task-board/SKILL.md +7 -81
- package/claude/skills/task-board/references/archive.md +54 -0
- package/claude/skills/task-board/references/decline.md +40 -0
- package/claude/skills/teach-workspace/SKILL.md +8 -105
- package/claude/skills/teach-workspace/references/lesson-craft.md +51 -1
- package/claude/skills/teach-workspace/references/promotion.md +41 -0
- package/docs/agents/architecture-record.md +26 -0
- package/docs/agents/commands.md +87 -154
- package/docs/agents/context-audit-checks.md +2 -20
- package/docs/agents/context-audit.md +1 -1
- package/docs/agents/context-classify.md +1 -1
- package/docs/agents/drift-surfaces.md +192 -0
- package/docs/agents/index.md +12 -4
- package/docs/agents/install-and-sync.md +2 -181
- package/docs/agents/markdown-audit-weight.md +73 -0
- package/docs/agents/markdown-audit.md +3 -90
- package/docs/agents/migrate.md +34 -0
- package/docs/agents/records-stale.md +62 -0
- package/docs/agents/records.md +5 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/tasks-archive.md +81 -0
- package/docs/agents/tasks-plans.md +149 -0
- package/docs/agents/tasks-validate.md +131 -0
- package/docs/agents/tasks.md +7 -334
- package/docs/target-projects.md +3 -3
- package/docs/workflow/ai-workflow.md +34 -29
- package/docs/workflow/operating-model.md +1 -1
- package/governance/rules/claude/555-tasks.md +2 -1
- package/governance/rules/core/045-memory.md +1 -1
- package/package.json +1 -1
- package/src/claude/cases/setup.ts +1 -1
- package/src/claude/cases/workflow.ts +22 -1
- package/src/commands/records.ts +146 -0
- package/src/commands/sandbox.ts +6 -4
- package/src/commands/tasks.ts +39 -29
- package/src/context/classify/extract.ts +1 -1
- package/src/context/classify/ollama.ts +1 -1
- package/src/context/classify/run.ts +1 -1
- package/src/gate/measures.ts +1 -1
- package/src/gate/stages.ts +1 -0
- package/src/migrate/skill-names.ts +15 -2
- package/src/records/stale.ts +243 -0
- package/src/sandbox/expect.ts +1 -1
- package/src/tasks/archive.ts +223 -92
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/record.ts +60 -17
- package/src/tasks/validate.ts +37 -4
- package/standards/board.md +136 -0
- package/standards/commit.md +5 -0
- package/standards/groundwork.md +25 -36
- package/standards/index.md +2 -1
- package/standards/memory.md +13 -1
- package/standards/plan.md +20 -34
- package/standards/readme.md +8 -77
- package/standards/requirements.md +2 -2
- package/standards/tasks.md +19 -126
- package/standards/versioning.md +1 -1
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/.husky/post-merge +1 -1
- package/tooling/base/configs/commitlint.config.js +27 -1
- package/tooling/base/reference.md +1 -1
- package/tooling/claude/reference.md +1 -1
- package/tooling/go/reference.md +1 -1
- package/tooling/nextjs/reference.md +1 -1
- package/tooling/php/reference.md +1 -1
- package/tooling/python/reference.md +1 -1
- package/tooling/vite-react/reference.md +1 -1
- package/tooling/web/reference.md +1 -1
|
@@ -21,13 +21,18 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
|
|
|
21
21
|
* reading in sequence with `draft-screencast` as one three-step pipeline with
|
|
22
22
|
* no prefix forcing that reading.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* Two rows rename a skill rather than retiring a prefix. `ui-test` became
|
|
25
25
|
* `ui-checklist` when the skill stopped writing tests and shrank to the
|
|
26
26
|
* checklist a reviewer reads, so the old name described a job it no longer
|
|
27
27
|
* does. `claude-ui-test` is retargeted onto the same destination rather than
|
|
28
28
|
* left pointing at the retired name, which lands a target on the oldest
|
|
29
29
|
* spelling in one pass instead of two.
|
|
30
30
|
*
|
|
31
|
+
* `docs-fold` became `context-fold` because the skill folds a session into the
|
|
32
|
+
* context entries, the canonical records, and the task board, while "docs" in
|
|
33
|
+
* its name collided with `docs-sync`, which owns `docs/`. `claude-docs` is
|
|
34
|
+
* retargeted onto `context-fold` for the same reason `claude-ui-test` was.
|
|
35
|
+
*
|
|
31
36
|
* Every name takes two words. Ten of these would have landed as a bare single
|
|
32
37
|
* word under a plain strip, and a bare word such as `review` or `docs` is a
|
|
33
38
|
* substring of ordinary prose with no token left for a later sweep to find.
|
|
@@ -47,7 +52,8 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
47
52
|
'claude-autoship': 'auto-ship',
|
|
48
53
|
'claude-design-extract': 'design-extract',
|
|
49
54
|
'claude-diagram': 'draft-diagram',
|
|
50
|
-
'claude-docs': '
|
|
55
|
+
'claude-docs': 'context-fold',
|
|
56
|
+
'docs-fold': 'context-fold',
|
|
51
57
|
'claude-feature': 'plan-feature',
|
|
52
58
|
'claude-groundwork': 'plan-groundwork',
|
|
53
59
|
'claude-intake': 'plan-intake',
|
|
@@ -105,6 +111,11 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
105
111
|
* what shipped or what a session ran under whatever name was current then, so
|
|
106
112
|
* rewriting one makes it testify to a release or a run that never happened.
|
|
107
113
|
*
|
|
114
|
+
* The pointer left at `docs-fold` is excluded because it exists to answer under
|
|
115
|
+
* the old name, so a later run would move it onto the skill it points at. The
|
|
116
|
+
* exclusion is only safe once the real body has moved, since excluding the path
|
|
117
|
+
* ahead of the first sweep leaves that body behind.
|
|
118
|
+
*
|
|
108
119
|
* The record archives need no entry here. They are gitignored, so the tracked
|
|
109
120
|
* listing every sweep reads never reaches them.
|
|
110
121
|
*/
|
|
@@ -117,5 +128,7 @@ export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
|
|
|
117
128
|
'CHANGELOG.md',
|
|
118
129
|
'src/migrate/skill-names.ts',
|
|
119
130
|
'src/migrate/skill-names.test.ts',
|
|
131
|
+
'claude/skills/docs-fold/SKILL.md',
|
|
132
|
+
'claude/skills/docs-fold/REQUIREMENT.md',
|
|
120
133
|
],
|
|
121
134
|
})
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
3
|
+
import { join, relative } from 'node:path'
|
|
4
|
+
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
5
|
+
import {
|
|
6
|
+
CREATION_ROOT,
|
|
7
|
+
RECORD_ROOTS,
|
|
8
|
+
recordDir,
|
|
9
|
+
SCRATCH,
|
|
10
|
+
spell,
|
|
11
|
+
} from '@/record-root'
|
|
12
|
+
|
|
13
|
+
/** The window after which a reviewed entry is due again, in days. */
|
|
14
|
+
export const DEFAULT_REVIEW_DAYS = 30
|
|
15
|
+
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
const MEMORY_INDEX = 'index.md'
|
|
19
|
+
|
|
20
|
+
export interface StaleEntry {
|
|
21
|
+
/** The filename stem, which is what a review names an entry by. */
|
|
22
|
+
readonly name: string
|
|
23
|
+
readonly category: string | null
|
|
24
|
+
/** `YYYY-MM-DD`, or null when the entry carries no readable review date. */
|
|
25
|
+
readonly reviewed: string | null
|
|
26
|
+
/** The raw value of a `reviewed` field that is not a date, present only then. */
|
|
27
|
+
readonly invalidReviewed?: string
|
|
28
|
+
readonly due: boolean
|
|
29
|
+
/** Backticked paths the entry cites that the project root does not hold. */
|
|
30
|
+
readonly unresolved: readonly string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StaleReport {
|
|
34
|
+
readonly ok: true
|
|
35
|
+
readonly root: string
|
|
36
|
+
/** The pen read, relative to the root. */
|
|
37
|
+
readonly folder: string
|
|
38
|
+
readonly days: number
|
|
39
|
+
readonly total: number
|
|
40
|
+
readonly due: number
|
|
41
|
+
readonly entries: readonly StaleEntry[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const STALE_REFUSALS = ['no-folder'] as const
|
|
45
|
+
|
|
46
|
+
export type StaleRefusal = (typeof STALE_REFUSALS)[number]
|
|
47
|
+
|
|
48
|
+
export interface StaleRefused {
|
|
49
|
+
readonly ok: false
|
|
50
|
+
readonly reason: StaleRefusal
|
|
51
|
+
readonly message: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type StaleOutcome = StaleReport | StaleRefused
|
|
55
|
+
|
|
56
|
+
const REVIEWED_FIELD = /^reviewed:[ \t]*(.*?)[ \t]*$/m
|
|
57
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
|
|
58
|
+
const RAW_FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reads `reviewed` off the raw block rather than the parsed fields, for the
|
|
62
|
+
* reason `hasOpeningDate` in the validator gives: a YAML parser resolves a
|
|
63
|
+
* bare `YYYY-MM-DD` to a date on one schema and a string on another.
|
|
64
|
+
*
|
|
65
|
+
* A value that is not a calendar date reads as never reviewed and is carried
|
|
66
|
+
* back so the review can see it, rather than throwing over one hand edit.
|
|
67
|
+
*/
|
|
68
|
+
function readReviewed(
|
|
69
|
+
text: string,
|
|
70
|
+
): Pick<StaleEntry, 'reviewed' | 'invalidReviewed'> {
|
|
71
|
+
const block = RAW_FRONTMATTER.exec(text)?.[1]
|
|
72
|
+
const match = block ? REVIEWED_FIELD.exec(block) : null
|
|
73
|
+
if (!match) return { reviewed: null }
|
|
74
|
+
|
|
75
|
+
const value = match[1].replace(/^(['"])(.*)\1$/, '$2')
|
|
76
|
+
const parsed = Date.parse(`${value}T00:00:00Z`)
|
|
77
|
+
const isDate =
|
|
78
|
+
ISO_DATE.test(value) &&
|
|
79
|
+
Number.isFinite(parsed) &&
|
|
80
|
+
new Date(parsed).toISOString().slice(0, 10) === value
|
|
81
|
+
|
|
82
|
+
return isDate
|
|
83
|
+
? { reviewed: value }
|
|
84
|
+
: { reviewed: null, invalidReviewed: value }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Tokens that name a session record, scratch at either root, or a checkout,
|
|
89
|
+
* none of which a tree commits. The scratch spellings come from `spell` so the
|
|
90
|
+
* legacy root's variant is stated in one place.
|
|
91
|
+
*/
|
|
92
|
+
const UNTRACKED_PREFIXES = [
|
|
93
|
+
...RECORD_ROOTS.map((at) => `${at}/${spell(at, SCRATCH)}/`),
|
|
94
|
+
`${CREATION_ROOT}/`,
|
|
95
|
+
'.claude/worktrees/',
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
const INLINE_CODE = /`([^`\n]+)`/g
|
|
99
|
+
const LINE_ANCHOR = /:\d+(?:[-:]\d+)*$/
|
|
100
|
+
const FILE_EXTENSION = /\.[A-Za-z0-9]+$/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reduces one backticked token to the repository path it cites, or undefined
|
|
104
|
+
* when it cites none.
|
|
105
|
+
*
|
|
106
|
+
* A path is a token carrying a `/` and ending in a file extension once any
|
|
107
|
+
* line or heading anchor is stripped. A placeholder, an elided path, a glob, a
|
|
108
|
+
* variable, a URL, and a path outside the project all look like paths and
|
|
109
|
+
* resolve against nothing in the tree, so each is dropped rather than reported.
|
|
110
|
+
*/
|
|
111
|
+
function toCitedPath(token: string): string | undefined {
|
|
112
|
+
if (/\s/.test(token) || token.includes('://')) return undefined
|
|
113
|
+
if (/[<*{$]/.test(token) || token.includes('...')) return undefined
|
|
114
|
+
if (/^(?:\/|~|\.\.\/)/.test(token)) return undefined
|
|
115
|
+
|
|
116
|
+
const path = token
|
|
117
|
+
.replace(/#.*$/, '')
|
|
118
|
+
.replace(LINE_ANCHOR, '')
|
|
119
|
+
.replace(/^\.\//, '')
|
|
120
|
+
|
|
121
|
+
if (!path.includes('/') || !FILE_EXTENSION.test(path)) return undefined
|
|
122
|
+
if (UNTRACKED_PREFIXES.some((prefix) => path.startsWith(prefix)))
|
|
123
|
+
return undefined
|
|
124
|
+
|
|
125
|
+
return path
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Every distinct repository path a body cites in inline code, in order. */
|
|
129
|
+
export function citedPaths(body: string): string[] {
|
|
130
|
+
const paths = new Set<string>()
|
|
131
|
+
|
|
132
|
+
for (const match of body.matchAll(INLINE_CODE)) {
|
|
133
|
+
const path = toCitedPath(match[1])
|
|
134
|
+
if (path) paths.add(path)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return [...paths]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isDue(reviewed: string | null, days: number, now: number): boolean {
|
|
141
|
+
if (reviewed === null) return true
|
|
142
|
+
return now - Date.parse(`${reviewed}T00:00:00`) > days * DAY_MS
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readEntry(
|
|
146
|
+
root: string,
|
|
147
|
+
dir: string,
|
|
148
|
+
file: string,
|
|
149
|
+
days: number,
|
|
150
|
+
now: number,
|
|
151
|
+
): Promise<StaleEntry> {
|
|
152
|
+
const text = (await readFile(join(dir, file), 'utf8')).replaceAll(
|
|
153
|
+
'\r\n',
|
|
154
|
+
'\n',
|
|
155
|
+
)
|
|
156
|
+
const frontmatter = parseFrontmatter(text)
|
|
157
|
+
const review = readReviewed(text)
|
|
158
|
+
const body = text.slice(RAW_FRONTMATTER.exec(text)?.[0].length ?? 0)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
name: file.replace(/\.md$/, ''),
|
|
162
|
+
category: readField(frontmatter, 'category') ?? null,
|
|
163
|
+
...review,
|
|
164
|
+
due: isDue(review.reviewed, days, now),
|
|
165
|
+
unresolved: citedPaths(body).filter(
|
|
166
|
+
(path) => !existsSync(join(root, path)),
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The order a review takes a batch in: due entries first, and among them the
|
|
173
|
+
* ones citing a tree that moved, then the longest unreviewed, then by name so
|
|
174
|
+
* two runs over one pen agree.
|
|
175
|
+
*/
|
|
176
|
+
function compareEntries(left: StaleEntry, right: StaleEntry): number {
|
|
177
|
+
if (left.due !== right.due) return left.due ? -1 : 1
|
|
178
|
+
|
|
179
|
+
const leftMoved = left.unresolved.length > 0
|
|
180
|
+
const rightMoved = right.unresolved.length > 0
|
|
181
|
+
if (leftMoved !== rightMoved) return leftMoved ? -1 : 1
|
|
182
|
+
|
|
183
|
+
if (left.reviewed !== right.reviewed) {
|
|
184
|
+
if (left.reviewed === null) return -1
|
|
185
|
+
if (right.reviewed === null) return 1
|
|
186
|
+
return left.reviewed < right.reviewed ? -1 : 1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return left.name.localeCompare(right.name)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reports each memory entry's review state and the paths it cites that the
|
|
194
|
+
* project no longer holds, ordered so a caller takes the first N as a batch.
|
|
195
|
+
*
|
|
196
|
+
* It reads the top level of the pen alone. `review/` holds receipts and
|
|
197
|
+
* `archive/` holds retirements, and neither is an entry a review would queue.
|
|
198
|
+
* Paths resolve against the project root rather than the entry's folder, since
|
|
199
|
+
* an entry cites the tree it was written about. It reports and never writes.
|
|
200
|
+
*
|
|
201
|
+
* `now` is a parameter so a test can pin the window against fixture dates.
|
|
202
|
+
*/
|
|
203
|
+
export async function staleMemory(
|
|
204
|
+
root: string,
|
|
205
|
+
days: number = DEFAULT_REVIEW_DAYS,
|
|
206
|
+
now: number = Date.now(),
|
|
207
|
+
): Promise<StaleOutcome> {
|
|
208
|
+
const dir = recordDir(root, 'memory')
|
|
209
|
+
|
|
210
|
+
if (!existsSync(dir)) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
reason: 'no-folder',
|
|
214
|
+
message: `No memory folder at ${dir}, so there are no entries to read.`,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const listed = await readdir(dir, { withFileTypes: true })
|
|
219
|
+
const files = listed
|
|
220
|
+
.filter(
|
|
221
|
+
(entry) =>
|
|
222
|
+
entry.isFile() &&
|
|
223
|
+
entry.name.endsWith('.md') &&
|
|
224
|
+
entry.name !== MEMORY_INDEX,
|
|
225
|
+
)
|
|
226
|
+
.map((entry) => entry.name)
|
|
227
|
+
|
|
228
|
+
const entries = (
|
|
229
|
+
await Promise.all(
|
|
230
|
+
files.map((file) => readEntry(root, dir, file, days, now)),
|
|
231
|
+
)
|
|
232
|
+
).toSorted(compareEntries)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
root,
|
|
237
|
+
folder: relative(root, dir),
|
|
238
|
+
days,
|
|
239
|
+
total: entries.length,
|
|
240
|
+
due: entries.filter((entry) => entry.due).length,
|
|
241
|
+
entries,
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/sandbox/expect.ts
CHANGED
|
@@ -178,7 +178,7 @@ function contentArray(value: unknown): ContentAssertion[] {
|
|
|
178
178
|
// A bare key written below a `[[content]]` header belongs to that table in
|
|
179
179
|
// TOML, not to the document, so a declaration listing `manual` or
|
|
180
180
|
// `max_turns` after its content blocks parses clean and silently asserts
|
|
181
|
-
// neither. The arm then named `claude/ui-test
|
|
181
|
+
// neither. The arm then named `claude/ui-test` (canon-keep-retired), now
|
|
182
182
|
// `claude/ui-checklist`, shipped that way: a turn ceiling that
|
|
183
183
|
// never ran and five manual entries that never reached the unchecked count,
|
|
184
184
|
// while `canon sandbox coverage` counted the arm as armed. Nothing at the
|