@erclx/canon 5.42.0 → 5.44.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 +3 -3
- package/claude/skills/backlog-triage/references/apply.md +6 -6
- package/claude/skills/canon-cli/SKILL.md +4 -4
- package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
- package/claude/skills/canvas-board/SKILL.md +1 -1
- package/claude/skills/ci-workflow/REQUIREMENT.md +2 -1
- package/claude/skills/ci-workflow/SKILL.md +12 -2
- package/claude/skills/ci-workflow/references/workflows.md +12 -9
- package/claude/skills/context-fold/references/context.md +1 -1
- package/claude/skills/create-rule/SKILL.md +1 -0
- package/claude/skills/create-skill/REQUIREMENT.md +1 -1
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/claude/skills/create-skill/references/skill-requirement.md +68 -9
- package/claude/skills/create-skill/references/skill.md +1 -1
- package/claude/skills/decision-escalate/SKILL.md +1 -2
- package/claude/skills/design-identity/REQUIREMENT.md +40 -0
- package/claude/skills/design-identity/SKILL.md +138 -0
- package/claude/skills/{draft-identity → design-identity}/references/card-route.md +2 -2
- package/claude/skills/design-taste/REQUIREMENT.md +4 -4
- package/claude/skills/design-taste/SKILL.md +2 -2
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/preflight.md +1 -1
- package/claude/skills/document-health/references/axes.md +2 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -69
- package/claude/skills/draft-and-pick/SKILL.md +3 -153
- package/claude/skills/draft-doc/REQUIREMENT.md +9 -30
- package/claude/skills/draft-doc/SKILL.md +4 -76
- package/claude/skills/draft-figure/REQUIREMENT.md +8 -42
- package/claude/skills/draft-figure/SKILL.md +3 -77
- package/claude/skills/draft-identity/REQUIREMENT.md +8 -23
- package/claude/skills/draft-identity/SKILL.md +3 -130
- package/claude/skills/draft-ready/REQUIREMENT.md +8 -37
- package/claude/skills/draft-ready/SKILL.md +3 -128
- package/claude/skills/draft-screencast/REQUIREMENT.md +8 -23
- package/claude/skills/draft-screencast/SKILL.md +3 -62
- package/claude/skills/draft-slides/REQUIREMENT.md +8 -27
- package/claude/skills/draft-slides/SKILL.md +3 -72
- package/claude/skills/draw-figure/REQUIREMENT.md +59 -0
- package/claude/skills/draw-figure/SKILL.md +85 -0
- package/claude/skills/{draft-figure → draw-figure}/references/architecture-set.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/hand-drawn.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/mermaid-path.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/mermaid.md +1 -1
- package/claude/skills/git-commit/references/commit.md +1 -5
- package/claude/skills/git-pr/references/pr.md +1 -0
- package/claude/skills/make-deck/REQUIREMENT.md +44 -0
- package/claude/skills/make-deck/SKILL.md +80 -0
- package/claude/skills/markdown-craft/REQUIREMENT.md +1 -1
- package/claude/skills/markdown-craft/SKILL.md +2 -2
- package/claude/skills/pick-by-eye/REQUIREMENT.md +86 -0
- package/claude/skills/pick-by-eye/SKILL.md +161 -0
- package/claude/skills/plan-feature/SKILL.md +3 -2
- package/claude/skills/plan-feature/references/plan.md +25 -11
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/plan-intake/SKILL.md +1 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +54 -0
- package/claude/skills/plan-ready/SKILL.md +136 -0
- package/claude/skills/{draft-ready → plan-ready}/references/assembly.md +1 -1
- package/claude/skills/{draft-ready → plan-ready}/references/ready.md +1 -1
- package/claude/skills/pr-evidence/references/evidence.md +1 -1
- package/claude/skills/record-screencast/REQUIREMENT.md +3 -3
- package/claude/skills/record-screencast/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +5 -2
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +18 -13
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +5 -4
- package/claude/skills/role-worker/REQUIREMENT.md +2 -0
- package/claude/skills/role-worker/SKILL.md +35 -2
- package/claude/skills/script-screencast/REQUIREMENT.md +40 -0
- package/claude/skills/script-screencast/SKILL.md +70 -0
- package/claude/skills/{draft-screencast → script-screencast}/references/beats-template.md +2 -2
- package/claude/skills/search-craft/REQUIREMENT.md +4 -4
- package/claude/skills/search-craft/SKILL.md +2 -2
- package/claude/skills/target-setup/SKILL.md +1 -0
- package/claude/skills/task-board/SKILL.md +11 -9
- package/claude/skills/task-board/references/board.md +15 -12
- package/claude/skills/teach-workspace/SKILL.md +1 -1
- package/claude/skills/teach-workspace/references/lesson-craft.md +2 -2
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- package/claude/skills/write-doc/REQUIREMENT.md +46 -0
- package/claude/skills/write-doc/SKILL.md +83 -0
- package/claude/skills/{draft-doc → write-doc}/references/context.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/docs.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/readme.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/wiki-kind.md +4 -4
- package/claude/skills/{draft-doc → write-doc}/references/wiki.md +1 -1
- package/docs/agents/audits.md +1 -0
- package/docs/agents/markdown-audit.md +4 -2
- package/docs/agents/migrate.md +13 -10
- package/docs/target/migrations.md +28 -4
- package/docs/target/projects.md +1 -1
- package/docs/workflow/parallel-features.md +3 -2
- package/docs/workflow/skill-map-on-request.md +18 -12
- package/docs/workflow/skill-map.md +13 -12
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +4 -0
- package/governance/ledger/framework/astro.md +73 -0
- package/governance/ledger/framework/fastapi.md +76 -0
- package/governance/ledger/framework/nextjs.md +86 -0
- package/governance/ledger/framework/react.md +83 -0
- package/governance/ledger/framework/shadcn.md +60 -0
- package/governance/ledger/framework/tailwind.md +55 -0
- package/governance/rules/canon/context.md +2 -1
- package/governance/rules/canon/plan.md +3 -5
- package/governance/rules/canon/scratch.md +3 -1
- package/governance/rules/canon/task-scope.md +2 -1
- package/governance/rules/canon/tasks.md +2 -1
- package/governance/rules/canon/worktrees.md +5 -2
- package/governance/rules/claude/output.md +7 -4
- package/governance/rules/claude/skill.md +6 -4
- package/governance/rules/framework/astro.md +12 -14
- package/governance/rules/framework/fastapi.md +20 -24
- package/governance/rules/framework/nextjs.md +19 -27
- package/governance/rules/framework/react.md +22 -33
- package/governance/rules/framework/shadcn.md +11 -15
- package/governance/rules/framework/tailwind.md +12 -14
- package/governance/rules/tooling/ci-workflow.md +6 -39
- package/governance/rules/ui/design-reference.md +37 -0
- package/governance/rules/writing/markdown.md +3 -2
- package/package.json +6 -6
- package/src/audits/catalog.ts +42 -0
- package/src/claude/cases/authoring.ts +15 -3
- package/src/claude/cases/misc.ts +12 -0
- package/src/claude/cases/workflow.ts +19 -1
- package/src/claude/settings.ts +8 -4
- package/src/claude/skills-audit.ts +113 -1
- package/src/commands/claude.ts +45 -12
- package/src/commands/demo.ts +1 -1
- package/src/commands/gov.ts +198 -0
- package/src/commands/markdown.ts +1 -1
- package/src/commands/migrate.ts +125 -2
- package/src/commands/pr.ts +34 -83
- package/src/commands/tasks.ts +383 -2
- package/src/commands/tooling.ts +3 -3
- package/src/commands/worktrees.ts +49 -0
- package/src/context/audit.ts +1 -1
- package/src/demo/beats.ts +1 -1
- package/src/gate/stages.ts +15 -0
- package/src/gov/audit.ts +441 -0
- package/src/listeners/belongs.ts +21 -0
- package/src/listeners/machine.ts +93 -0
- package/src/listeners/parse.ts +41 -0
- package/src/markdown/scope.ts +45 -35
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/session-defaults.ts +138 -0
- package/src/migrate/skill-names.ts +68 -21
- package/src/pr/frames.ts +14 -3
- package/src/pr/local.ts +4 -69
- package/src/records/validate.ts +31 -0
- package/src/sync/check.ts +15 -0
- package/src/tasks/board.ts +854 -0
- package/src/tasks/record.ts +20 -1
- package/src/tasks/validate.ts +206 -9
- package/src/tooling/stamp.ts +56 -12
- package/src/worktrees/stop.ts +92 -0
- package/tooling/astro/configs/card-src/pages/og-card.astro +1 -1
- package/tooling/astro/reference.md +2 -2
- package/tooling/base/configs/.github/workflows/phase-label-gate.yml +6 -2
- package/tooling/base/configs/commitlint.config.js +0 -20
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/workflows/verify.yml +4 -1
- package/tooling/claude/reference.md +13 -7
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +65 -0
- package/tooling/claude/seeds/.claude/settings.json +8 -0
- package/tooling/claude/user/settings.template.json +0 -4
- package/tooling/cloudflare/configs/.github/workflows/deploy.yml +5 -5
- package/tooling/public/configs/.github/dependabot.yml +12 -0
- package/tooling/public/configs/.github/workflows/release-please.yml +30 -0
- package/tooling/public/manifest.toml +8 -0
- package/tooling/public/reference.md +53 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/bug.yml +48 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/feature.yml +16 -0
- package/tooling/public/seeds/.release-please-manifest.json +3 -0
- package/tooling/public/seeds/CONTRIBUTING.md +50 -0
- package/tooling/public/seeds/LICENSE +21 -0
- package/tooling/public/seeds/SECURITY.md +13 -0
- package/tooling/public/seeds/release-please-config.json +12 -0
- package/tooling/shell/configs/.github/workflows/shell.yml +4 -1
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +6 -3
- package/tooling/web/configs/.github/workflows/verify.yml +15 -12
- /package/claude/skills/{draft-figure → draw-figure}/references/figures.md +0 -0
- /package/claude/skills/{draft-slides → make-deck}/references/deck.md +0 -0
- /package/claude/skills/{draft-and-pick → pick-by-eye}/references/live-arms.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/agent-facing.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/application.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/cli.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/library.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/plugin.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/requirements.md +0 -0
package/src/tasks/record.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
readPullRequest,
|
|
17
17
|
tasksDir,
|
|
18
18
|
} from '@/tasks/archive'
|
|
19
|
+
import { checkWaiting, type LinkPromotion, promoteOnLink } from '@/tasks/board'
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Lines a task carries above its outcomes naming where the work came from. The
|
|
@@ -79,6 +80,8 @@ export interface PlanRecorded {
|
|
|
79
80
|
readonly action: LineAction
|
|
80
81
|
/** The target the line named before a correction. Undefined unless it differed. */
|
|
81
82
|
readonly replaced: string | undefined
|
|
83
|
+
/** Whether the link moved the task's row out of `## Needs a plan`. */
|
|
84
|
+
readonly board: LinkPromotion
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
export type PlanOutcome = PlanRecorded | RecordRefused
|
|
@@ -472,7 +475,13 @@ export async function recordPlan(
|
|
|
472
475
|
stem: string,
|
|
473
476
|
reference: string,
|
|
474
477
|
add = false,
|
|
478
|
+
waiting?: string,
|
|
475
479
|
): Promise<PlanOutcome> {
|
|
480
|
+
if (waiting !== undefined) {
|
|
481
|
+
const bad = checkWaiting(waiting)
|
|
482
|
+
if (bad) return refuse('bad-input', bad.message, bad.detail)
|
|
483
|
+
}
|
|
484
|
+
|
|
476
485
|
const opened = await openTask(root, { kind: 'stem', stem })
|
|
477
486
|
if ('ok' in opened) return opened
|
|
478
487
|
|
|
@@ -500,7 +509,17 @@ export async function recordPlan(
|
|
|
500
509
|
const { text, action, replaced } = written
|
|
501
510
|
if (action !== 'unchanged') await writeFile(path, text)
|
|
502
511
|
|
|
503
|
-
|
|
512
|
+
const board = await promoteOnLink(root, opened.stem, { waiting })
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
ok: true,
|
|
516
|
+
stem: opened.stem,
|
|
517
|
+
path,
|
|
518
|
+
plan: target,
|
|
519
|
+
action,
|
|
520
|
+
replaced,
|
|
521
|
+
board,
|
|
522
|
+
}
|
|
504
523
|
}
|
|
505
524
|
|
|
506
525
|
/**
|
package/src/tasks/validate.ts
CHANGED
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
resolveLivePlan,
|
|
13
13
|
tasksDir,
|
|
14
14
|
} from '@/tasks/archive'
|
|
15
|
+
import {
|
|
16
|
+
deriveCells,
|
|
17
|
+
GROUP_COLUMNS,
|
|
18
|
+
renderedLength,
|
|
19
|
+
WAITING_LIMIT,
|
|
20
|
+
} from '@/tasks/board'
|
|
15
21
|
import { gitTrunkReader, type TrunkReader } from '@/tasks/trunk'
|
|
16
22
|
|
|
17
23
|
const ORDERING_FILE = 'priority.md'
|
|
@@ -50,6 +56,9 @@ export const FINDING_KINDS = [
|
|
|
50
56
|
'row-misordered',
|
|
51
57
|
'row-unranked',
|
|
52
58
|
'touches-unstated',
|
|
59
|
+
'touches-stale',
|
|
60
|
+
'columns-mismatched',
|
|
61
|
+
'cell-long',
|
|
53
62
|
'blocker-settled',
|
|
54
63
|
'blocker-unresolved',
|
|
55
64
|
'blocker-declined',
|
|
@@ -145,6 +154,8 @@ export interface BoardRow {
|
|
|
145
154
|
readonly plan: string | undefined
|
|
146
155
|
/** Absent when the group fixes no `Touches` column, empty when it read none. */
|
|
147
156
|
readonly touches: readonly string[] | undefined
|
|
157
|
+
/** The `Touches` cell as written, which the stale check compares against the derivation. */
|
|
158
|
+
readonly touchesText: string | undefined
|
|
148
159
|
/** Absent when the group fixes no `Waiting on` column, which is `## Run now`. */
|
|
149
160
|
readonly waiting: string | undefined
|
|
150
161
|
/** The ordinal phrase a `Waiting on` cell states about its own position, undefined when the cell carries none. */
|
|
@@ -271,8 +282,8 @@ function splitCells(line: string): string[] {
|
|
|
271
282
|
return line
|
|
272
283
|
.trim()
|
|
273
284
|
.replace(/^\|/, '')
|
|
274
|
-
.replace(
|
|
275
|
-
.split(
|
|
285
|
+
.replace(/(?<!\\)\|$/, '')
|
|
286
|
+
.split(/(?<!\\)\|/)
|
|
276
287
|
.map((cell) => cell.trim())
|
|
277
288
|
}
|
|
278
289
|
|
|
@@ -288,6 +299,21 @@ function isGroup(heading: string): heading is BoardGroup {
|
|
|
288
299
|
return (BOARD_GROUPS as readonly string[]).includes(heading)
|
|
289
300
|
}
|
|
290
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Whether a header is the set of columns its group owns. Order is left to the
|
|
304
|
+
* reader, which resolves every column by name, so only a missing or a foreign
|
|
305
|
+
* column is a finding.
|
|
306
|
+
*/
|
|
307
|
+
function ownsColumns(group: BoardGroup, header: readonly string[]): boolean {
|
|
308
|
+
const owned = GROUP_COLUMNS[group].map((column) => column.toLowerCase())
|
|
309
|
+
const named = header.map((cell) => cell.toLowerCase())
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
named.length === owned.length &&
|
|
313
|
+
owned.every((column) => named.includes(column))
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
291
317
|
const ORDINAL_WORDS = [
|
|
292
318
|
'first',
|
|
293
319
|
'second',
|
|
@@ -460,6 +486,14 @@ export function readBoard(text: string): {
|
|
|
460
486
|
}
|
|
461
487
|
|
|
462
488
|
header = cells
|
|
489
|
+
if (!ownsColumns(group, cells)) {
|
|
490
|
+
findings.push({
|
|
491
|
+
kind: 'columns-mismatched',
|
|
492
|
+
group,
|
|
493
|
+
subject: line.trim(),
|
|
494
|
+
message: `is not the ${GROUP_COLUMNS[group].join(', ')} header ${group} owns.`,
|
|
495
|
+
})
|
|
496
|
+
}
|
|
463
497
|
continue
|
|
464
498
|
}
|
|
465
499
|
|
|
@@ -489,6 +523,7 @@ export function readBoard(text: string): {
|
|
|
489
523
|
stem: target ? stemOf(target) : undefined,
|
|
490
524
|
plan,
|
|
491
525
|
touches: touchesAt >= 0 ? readPaths(cells[touchesAt] ?? '') : undefined,
|
|
526
|
+
touchesText: touchesAt >= 0 ? (cells[touchesAt] ?? '') : undefined,
|
|
492
527
|
waiting,
|
|
493
528
|
ordinal: waiting ? readOrdinal(waiting) : undefined,
|
|
494
529
|
ranked: waiting ? readRank(waiting) : false,
|
|
@@ -1125,6 +1160,83 @@ function citedStem(cell: string): string | undefined {
|
|
|
1125
1160
|
return target ? stemOf(target) : undefined
|
|
1126
1161
|
}
|
|
1127
1162
|
|
|
1163
|
+
/** The task stems in each folder a bare label can resolve through, live first. */
|
|
1164
|
+
interface LabelFolders {
|
|
1165
|
+
readonly live: readonly string[]
|
|
1166
|
+
readonly archived: readonly string[]
|
|
1167
|
+
readonly declined: readonly string[]
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
type LabelCitation =
|
|
1171
|
+
| { readonly kind: 'stems'; readonly stems: readonly string[] }
|
|
1172
|
+
| { readonly kind: 'ambiguous'; readonly label: string }
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Reads every bare `vNNN.N` phase label a `Waiting on` cell names outside a
|
|
1176
|
+
* link, each resolved to the one task it labels. The live folder answers first,
|
|
1177
|
+
* since a live task and a declined one can share a label and the live one is
|
|
1178
|
+
* what the row waits on. Two matches in the folder that answers leave the row
|
|
1179
|
+
* untested rather than guessing between them, and a label no folder holds is
|
|
1180
|
+
* read as prose rather than as a broken pointer, since a version number in a
|
|
1181
|
+
* sentence is not always a citation.
|
|
1182
|
+
*/
|
|
1183
|
+
function bareLabels(cell: string, folders: LabelFolders): LabelCitation {
|
|
1184
|
+
const prose = cell.replace(/\[[^\]]*\]\([^)]*\)/g, ' ')
|
|
1185
|
+
const stems: string[] = []
|
|
1186
|
+
|
|
1187
|
+
for (const match of prose.matchAll(/\bv\d+\.\d+\b/g)) {
|
|
1188
|
+
const prefix = `${match[0]}-`
|
|
1189
|
+
for (const folder of [folders.live, folders.archived, folders.declined]) {
|
|
1190
|
+
const hits = folder.filter((stem) => stem.startsWith(prefix))
|
|
1191
|
+
if (hits.length > 1) return { kind: 'ambiguous', label: match[0] }
|
|
1192
|
+
if (hits.length === 1) {
|
|
1193
|
+
if (!stems.includes(hits[0])) stems.push(hits[0])
|
|
1194
|
+
break
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return { kind: 'stems', stems }
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Tests every task a cell cites by bare label, and reports the row only when
|
|
1204
|
+
* none of them is still open. A cell naming settled history beside its live
|
|
1205
|
+
* blocker would otherwise read as settled on the history alone.
|
|
1206
|
+
*/
|
|
1207
|
+
async function checkCitedTasks(
|
|
1208
|
+
group: BoardGroup,
|
|
1209
|
+
subject: string,
|
|
1210
|
+
stems: readonly string[],
|
|
1211
|
+
root: string,
|
|
1212
|
+
trunk: TrunkReader,
|
|
1213
|
+
): Promise<CitedResult> {
|
|
1214
|
+
const results = await Promise.all(
|
|
1215
|
+
stems.map((stem) => checkCitedTask(group, subject, stem, root, trunk)),
|
|
1216
|
+
)
|
|
1217
|
+
const open = results.some(
|
|
1218
|
+
(result) => result.findings.length === 0 && result.untested.length === 0,
|
|
1219
|
+
)
|
|
1220
|
+
if (open) return nothing()
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
findings: results.flatMap((result) => result.findings),
|
|
1224
|
+
untested: results.flatMap((result) => result.untested),
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
async function readLabelFolders(root: string): Promise<LabelFolders> {
|
|
1229
|
+
const read = async (dir: string): Promise<string[]> =>
|
|
1230
|
+
existsSync(dir) ? listTaskStems(dir) : []
|
|
1231
|
+
const [live, archived, declined] = await Promise.all([
|
|
1232
|
+
read(tasksDir(root)),
|
|
1233
|
+
read(archiveDir(root)),
|
|
1234
|
+
read(declinedDir(root)),
|
|
1235
|
+
])
|
|
1236
|
+
|
|
1237
|
+
return { live, archived, declined }
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1128
1240
|
/** What one blocker citation produced, since a row can be neither settled nor open. */
|
|
1129
1241
|
interface CitedResult {
|
|
1130
1242
|
readonly findings: readonly Finding[]
|
|
@@ -1288,17 +1400,29 @@ async function checkParked(
|
|
|
1288
1400
|
const findings: Finding[] = []
|
|
1289
1401
|
const untested: Untested[] = []
|
|
1290
1402
|
const running = rows.filter((row) => row.group === 'Run now')
|
|
1403
|
+
const folders = await readLabelFolders(root)
|
|
1291
1404
|
|
|
1292
1405
|
for (const row of rows) {
|
|
1293
1406
|
if (row.group === 'Run now') continue
|
|
1294
1407
|
|
|
1295
1408
|
const subject = row.stem ?? row.label
|
|
1296
1409
|
const cell = row.waiting ?? ''
|
|
1297
|
-
const
|
|
1410
|
+
const linked = citedStem(cell)
|
|
1411
|
+
const bare = linked ? undefined : bareLabels(cell, folders)
|
|
1412
|
+
const cited = linked ? [linked] : bare?.kind === 'stems' ? bare.stems : []
|
|
1298
1413
|
const contested = readPaths(cell)
|
|
1299
1414
|
|
|
1300
|
-
if (
|
|
1301
|
-
|
|
1415
|
+
if (bare?.kind === 'ambiguous') {
|
|
1416
|
+
untested.push({
|
|
1417
|
+
group: row.group,
|
|
1418
|
+
subject,
|
|
1419
|
+
message: `waits on ${bare.label}, which labels more than one task, so the blocker is not read.`,
|
|
1420
|
+
})
|
|
1421
|
+
continue
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (cited.length > 0) {
|
|
1425
|
+
const result = await checkCitedTasks(
|
|
1302
1426
|
row.group,
|
|
1303
1427
|
subject,
|
|
1304
1428
|
cited,
|
|
@@ -1324,7 +1448,7 @@ async function checkParked(
|
|
|
1324
1448
|
})
|
|
1325
1449
|
}
|
|
1326
1450
|
|
|
1327
|
-
if (
|
|
1451
|
+
if (cited.length === 0 && contested.length === 0) {
|
|
1328
1452
|
untested.push({
|
|
1329
1453
|
group: row.group,
|
|
1330
1454
|
subject,
|
|
@@ -1337,6 +1461,79 @@ async function checkParked(
|
|
|
1337
1461
|
return { findings, untested }
|
|
1338
1462
|
}
|
|
1339
1463
|
|
|
1464
|
+
/**
|
|
1465
|
+
* Reports a `Waiting on` cell rendering past the cap. A verb refuses to write
|
|
1466
|
+
* one, so a cell this long is a hand edit, which is why it moves the exit code.
|
|
1467
|
+
*/
|
|
1468
|
+
function checkCellWidth(rows: readonly BoardRow[]): Finding[] {
|
|
1469
|
+
return rows.flatMap((row) => {
|
|
1470
|
+
const width = row.waiting ? renderedLength(row.waiting) : 0
|
|
1471
|
+
if (width <= WAITING_LIMIT) return []
|
|
1472
|
+
|
|
1473
|
+
return [
|
|
1474
|
+
{
|
|
1475
|
+
kind: 'cell-long' as const,
|
|
1476
|
+
group: row.group,
|
|
1477
|
+
subject: subjectOf(row),
|
|
1478
|
+
message: `carries a Waiting on cell rendering ${width} characters, past the ${WAITING_LIMIT} a board cell holds.`,
|
|
1479
|
+
},
|
|
1480
|
+
]
|
|
1481
|
+
})
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Reports a `Touches` cell that differs from what the task's plans derive. A
|
|
1486
|
+
* verb writes the derivation on every call, so a difference is a hand edit or a
|
|
1487
|
+
* plan that changed since the last write. An empty derivation is left alone,
|
|
1488
|
+
* since a row with no live plan or no declared file is `plan-absent`,
|
|
1489
|
+
* `plan-unresolved`, or `touches-unstated` to report, not stale.
|
|
1490
|
+
*/
|
|
1491
|
+
async function checkStaleTouches(
|
|
1492
|
+
rows: readonly BoardRow[],
|
|
1493
|
+
root: string,
|
|
1494
|
+
): Promise<Finding[]> {
|
|
1495
|
+
const found = await Promise.all(
|
|
1496
|
+
rows.map(async (row): Promise<Finding | undefined> => {
|
|
1497
|
+
if (row.touchesText === undefined || !row.stem) return undefined
|
|
1498
|
+
|
|
1499
|
+
const { touches } = await deriveCells(root, {
|
|
1500
|
+
stem: row.stem,
|
|
1501
|
+
label: row.label,
|
|
1502
|
+
})
|
|
1503
|
+
if (touches === '' || touches === row.touchesText) return undefined
|
|
1504
|
+
|
|
1505
|
+
return {
|
|
1506
|
+
kind: 'touches-stale',
|
|
1507
|
+
group: row.group,
|
|
1508
|
+
subject: row.stem,
|
|
1509
|
+
message: `states ${row.touchesText || 'no file'}, and its plan derives ${touches}. A board verb rewrites the cell.`,
|
|
1510
|
+
}
|
|
1511
|
+
}),
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
return found.filter((finding): finding is Finding => finding !== undefined)
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* The claims a group name makes about a row, being a stated plan and file set
|
|
1519
|
+
* under `## Run now`, a live plan under `## Up next`, and none under
|
|
1520
|
+
* `## Needs a plan`. The board verbs read these same findings off the board
|
|
1521
|
+
* they are about to write, so a placement and a validation cannot disagree on
|
|
1522
|
+
* what a group allows.
|
|
1523
|
+
*/
|
|
1524
|
+
export async function groupRuleFindings(
|
|
1525
|
+
rows: readonly BoardRow[],
|
|
1526
|
+
root: string,
|
|
1527
|
+
): Promise<Finding[]> {
|
|
1528
|
+
const dir = tasksDir(root)
|
|
1529
|
+
|
|
1530
|
+
return [
|
|
1531
|
+
...checkPlans(rows, dir, root),
|
|
1532
|
+
...(await checkGroupClaims(rows, dir, root)),
|
|
1533
|
+
...checkTouches(rows),
|
|
1534
|
+
]
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1340
1537
|
function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
|
|
1341
1538
|
return { ok: false, reason, message }
|
|
1342
1539
|
}
|
|
@@ -1397,10 +1594,10 @@ export async function validateBoard(
|
|
|
1397
1594
|
const findings = [
|
|
1398
1595
|
...shapeFindings,
|
|
1399
1596
|
...checkMapping(rows, backlog, dir),
|
|
1400
|
-
...
|
|
1597
|
+
...(await groupRuleFindings(rows, root)),
|
|
1401
1598
|
...(await checkPlanAgreement(rows, dir, root)),
|
|
1402
|
-
...(await
|
|
1403
|
-
...
|
|
1599
|
+
...(await checkStaleTouches(rows, root)),
|
|
1600
|
+
...checkCellWidth(rows),
|
|
1404
1601
|
...checkOrdering(rows),
|
|
1405
1602
|
...parked.findings,
|
|
1406
1603
|
]
|
package/src/tooling/stamp.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { writeChainStamp } from '@/sync/stamp'
|
|
4
|
-
import { isWholeStack, type Manifest } from '@/tooling/manifest'
|
|
3
|
+
import { readStamp, stampedChain, writeChainStamp } from '@/sync/stamp'
|
|
4
|
+
import { isWholeStack, loadManifest, type Manifest } from '@/tooling/manifest'
|
|
5
5
|
import { readPackage } from '@/tooling/package'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -17,7 +17,9 @@ export function isWorkspaceRoot(target: string): boolean {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Records the chain an install resolved, and returns
|
|
20
|
+
* Records the chain an install resolved, and returns the names it wrote, or
|
|
21
|
+
* nothing when it wrote none. The names are the merged list, which can carry
|
|
22
|
+
* stacks an earlier sync recorded. A
|
|
21
23
|
* workspace root is the single refusal: one chain recorded there would be a
|
|
22
24
|
* guess at what its packages hold, and the report reading tooling as unmeasured
|
|
23
25
|
* is the true answer rather than a clean one.
|
|
@@ -27,16 +29,58 @@ export async function recordToolingChain(
|
|
|
27
29
|
target: string,
|
|
28
30
|
chain: readonly Manifest[],
|
|
29
31
|
now: Date,
|
|
30
|
-
): Promise<
|
|
31
|
-
if (isWorkspaceRoot(target)) return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
now,
|
|
32
|
+
): Promise<string[] | undefined> {
|
|
33
|
+
if (isWorkspaceRoot(target)) return undefined
|
|
34
|
+
|
|
35
|
+
const merged = mergeAddOns(
|
|
36
|
+
toolkitRoot,
|
|
37
|
+
stampedChain(readStamp(target), 'tooling'),
|
|
38
|
+
chain,
|
|
38
39
|
)
|
|
39
|
-
|
|
40
|
+
await writeChainStamp(target, { domain: 'tooling', toolkitRoot }, merged, now)
|
|
41
|
+
return merged
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* An add-on stands alone beside a core chain, so syncing one must not displace
|
|
46
|
+
* the chain it layers onto. Base extends nothing and still heads a core chain.
|
|
47
|
+
*/
|
|
48
|
+
export function isAddOn(manifest: Manifest | undefined): boolean {
|
|
49
|
+
return (
|
|
50
|
+
manifest !== undefined &&
|
|
51
|
+
manifest.parent === undefined &&
|
|
52
|
+
manifest.name !== 'base'
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Merges what this sync resolved into what an earlier one recorded. The core
|
|
58
|
+
* chain is the one just synced, or the recorded one when only add-ons synced.
|
|
59
|
+
* Add-ons accumulate after it, so a later stackless report measures them all.
|
|
60
|
+
*/
|
|
61
|
+
function mergeAddOns(
|
|
62
|
+
toolkitRoot: string,
|
|
63
|
+
recorded: readonly string[],
|
|
64
|
+
chain: readonly Manifest[],
|
|
65
|
+
): string[] {
|
|
66
|
+
const synced = chain.filter(isWholeStack)
|
|
67
|
+
const syncedCore = synced.filter((m) => !isAddOn(m)).map((m) => m.name)
|
|
68
|
+
const syncedAddOns = synced.filter(isAddOn).map((m) => m.name)
|
|
69
|
+
|
|
70
|
+
const recordedManifests = recorded.map((name) => ({
|
|
71
|
+
name,
|
|
72
|
+
manifest: loadManifest(toolkitRoot, name),
|
|
73
|
+
}))
|
|
74
|
+
const recordedCore = recordedManifests
|
|
75
|
+
.filter(({ manifest }) => !isAddOn(manifest))
|
|
76
|
+
.map(({ name }) => name)
|
|
77
|
+
const recordedAddOns = recordedManifests
|
|
78
|
+
.filter(({ manifest }) => isAddOn(manifest))
|
|
79
|
+
.map(({ name }) => name)
|
|
80
|
+
|
|
81
|
+
const core = syncedCore.length > 0 ? syncedCore : recordedCore
|
|
82
|
+
const addOns = [...new Set([...recordedAddOns, ...syncedAddOns])]
|
|
83
|
+
return [...core, ...addOns]
|
|
40
84
|
}
|
|
41
85
|
|
|
42
86
|
/**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises'
|
|
2
|
+
import { belongsTo } from '@/listeners/belongs'
|
|
3
|
+
import type { ListenerReader } from '@/listeners/machine'
|
|
4
|
+
|
|
5
|
+
/** What a stop took, or would take on a dry run, from one worktree. */
|
|
6
|
+
export interface StoppedServer {
|
|
7
|
+
readonly path: string
|
|
8
|
+
readonly pids: readonly number[]
|
|
9
|
+
readonly ports: readonly number[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `unread` is a machine with no way to list sockets, which a reader must tell
|
|
14
|
+
* apart from `read` with nothing stopped, since the first says nothing about
|
|
15
|
+
* what is still listening.
|
|
16
|
+
*/
|
|
17
|
+
export type StopReport =
|
|
18
|
+
| { readonly kind: 'read'; readonly stopped: readonly StoppedServer[] }
|
|
19
|
+
| { readonly kind: 'unread' }
|
|
20
|
+
|
|
21
|
+
export interface StopOptions {
|
|
22
|
+
readonly reader: ListenerReader
|
|
23
|
+
/** Sends SIGTERM and says whether it landed, so a pid that already exited is not counted. */
|
|
24
|
+
readonly kill?: (pid: number) => boolean
|
|
25
|
+
readonly dryRun: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sigterm(pid: number): boolean {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 'SIGTERM')
|
|
31
|
+
return true
|
|
32
|
+
} catch {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A process cwd reads back resolved, so a path held through a symlink has to be too. */
|
|
38
|
+
async function resolved(path: string): Promise<string> {
|
|
39
|
+
return realpath(path).catch(() => path)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stops every server listening from inside one of the worktrees about to be
|
|
44
|
+
* removed, so none outlives the directory it serves. Each path is tested by the
|
|
45
|
+
* nesting rule `canon pr local` uses, so a listener in the main checkout or in
|
|
46
|
+
* a sibling under `.claude/worktrees/` is never signaled. The cwd is read
|
|
47
|
+
* against the given paths and never against `process.cwd()`, which a run from
|
|
48
|
+
* inside another worktree would otherwise make the only match.
|
|
49
|
+
*/
|
|
50
|
+
export async function stopListeners(
|
|
51
|
+
paths: readonly string[],
|
|
52
|
+
opts: StopOptions,
|
|
53
|
+
): Promise<StopReport> {
|
|
54
|
+
const listeners = await opts.reader.listListeners()
|
|
55
|
+
if (listeners === undefined) return { kind: 'unread' }
|
|
56
|
+
|
|
57
|
+
const kill = opts.kill ?? sigterm
|
|
58
|
+
const roots = await Promise.all(paths.map(resolved))
|
|
59
|
+
const cwdByPid = new Map<number, string | undefined>()
|
|
60
|
+
const stopped: StoppedServer[] = []
|
|
61
|
+
|
|
62
|
+
for (const [index, root] of roots.entries()) {
|
|
63
|
+
const pids = new Set<number>()
|
|
64
|
+
const ports = new Set<number>()
|
|
65
|
+
for (const { pid, port } of listeners) {
|
|
66
|
+
if (!cwdByPid.has(pid)) cwdByPid.set(pid, await opts.reader.cwdOf(pid))
|
|
67
|
+
const cwd = cwdByPid.get(pid)
|
|
68
|
+
if (cwd === undefined || !belongsTo(cwd, root)) continue
|
|
69
|
+
pids.add(pid)
|
|
70
|
+
ports.add(port)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const landed = [...pids]
|
|
74
|
+
.sort((a, b) => a - b)
|
|
75
|
+
.filter((pid) => opts.dryRun || kill(pid))
|
|
76
|
+
if (landed.length === 0) continue
|
|
77
|
+
const landedSet = new Set(landed)
|
|
78
|
+
stopped.push({
|
|
79
|
+
path: paths[index] ?? root,
|
|
80
|
+
pids: landed,
|
|
81
|
+
ports: [
|
|
82
|
+
...new Set(
|
|
83
|
+
listeners
|
|
84
|
+
.filter(({ pid, port }) => landedSet.has(pid) && ports.has(port))
|
|
85
|
+
.map(({ port }) => port),
|
|
86
|
+
),
|
|
87
|
+
].sort((a, b) => a - b),
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { kind: 'read', stopped }
|
|
92
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* an edit to one string here and a re-capture, not another pass through the
|
|
8
8
|
* pick loop with an operator in it.
|
|
9
9
|
*
|
|
10
|
-
* `canon:
|
|
10
|
+
* `canon:design-identity` writes the picked composition into the `.card`
|
|
11
11
|
* element below and leaves everything around it alone. What ships is a working
|
|
12
12
|
* placeholder, so the route serves before any pick has been taken.
|
|
13
13
|
*
|
|
@@ -91,9 +91,9 @@ In `canon/context/development/ci.md`, the Typecheck row asserts `` `astro check`
|
|
|
91
91
|
|
|
92
92
|
## Social card route
|
|
93
93
|
|
|
94
|
-
The social card is a route the project keeps rather than a page a render writes and deletes. Re-rendering it after a rename is an edit to one string and a re-capture, not another pass through the `
|
|
94
|
+
The social card is a route the project keeps rather than a page a render writes and deletes. Re-rendering it after a rename is an edit to one string and a re-capture, not another pass through the `design-identity` pick loop.
|
|
95
95
|
|
|
96
|
-
`canon:
|
|
96
|
+
`canon:design-identity` writes the picked composition into the route and captures it through the server below.
|
|
97
97
|
|
|
98
98
|
### What ships
|
|
99
99
|
|
|
@@ -11,14 +11,18 @@ on:
|
|
|
11
11
|
types: [submitted, edited]
|
|
12
12
|
workflow_dispatch:
|
|
13
13
|
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
14
17
|
jobs:
|
|
15
18
|
phase-label-gate:
|
|
16
19
|
name: 🏷️ Phase Label Gate
|
|
17
20
|
runs-on: ubuntu-latest
|
|
18
|
-
|
|
21
|
+
# Dependabot bodies carry upstream release notes that read as phase labels
|
|
22
|
+
if: github.event.pull_request.user.login != 'dependabot[bot]' && (github.event_name == 'workflow_dispatch' || github.event.pull_request.base.ref == 'main')
|
|
19
23
|
steps:
|
|
20
24
|
- name: Checkout
|
|
21
|
-
uses: actions/checkout@
|
|
25
|
+
uses: actions/checkout@v7
|
|
22
26
|
|
|
23
27
|
- name: Setup Bun
|
|
24
28
|
uses: oven-sh/setup-bun@v2
|
|
@@ -22,38 +22,18 @@ const subjectFirstWordCase = (parsed) => {
|
|
|
22
22
|
]
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const CO_AUTHOR_TRAILER = /^co-authored-by:/i
|
|
26
|
-
const CLAUDE_OR_ANTHROPIC = /\b(claude|anthropic)\b/i
|
|
27
|
-
|
|
28
|
-
// Reads raw rather than footer, since the parser files a lone trailer under
|
|
29
|
-
// body and leaves footer null.
|
|
30
|
-
const noClaudeCoAuthor = (parsed) => {
|
|
31
|
-
const offending = (parsed.raw ?? '')
|
|
32
|
-
.split('\n')
|
|
33
|
-
.find(
|
|
34
|
-
(line) => CO_AUTHOR_TRAILER.test(line) && CLAUDE_OR_ANTHROPIC.test(line),
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
return [
|
|
38
|
-
offending === undefined,
|
|
39
|
-
`remove the co-author trailer crediting Claude or Anthropic: ${offending}`,
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
25
|
const config = {
|
|
44
26
|
extends: ['@commitlint/config-conventional'],
|
|
45
27
|
plugins: [
|
|
46
28
|
{
|
|
47
29
|
rules: {
|
|
48
30
|
'subject-first-word-case': subjectFirstWordCase,
|
|
49
|
-
'no-claude-co-author': noClaudeCoAuthor,
|
|
50
31
|
},
|
|
51
32
|
},
|
|
52
33
|
],
|
|
53
34
|
rules: {
|
|
54
35
|
'subject-case': [0],
|
|
55
36
|
'subject-first-word-case': [2, 'always'],
|
|
56
|
-
'no-claude-co-author': [2, 'always'],
|
|
57
37
|
'header-max-length': [2, 'always', 72],
|
|
58
38
|
'scope-case': [2, 'always', 'lower-case'],
|
|
59
39
|
'subject-full-stop': [2, 'never', '.'],
|
|
@@ -17,7 +17,7 @@ Two configs fix how output looks and how commits read.
|
|
|
17
17
|
- `.oxfmtrc.json` sets `semi: false`, `singleQuote: true`, `printWidth: 80`, and `sortPackageJson: false`, keeping the output Prettier wrote.
|
|
18
18
|
- The `astro` stack repeats those options in its own copy, so copy a change here into it.
|
|
19
19
|
- `commitlint.config.js` is an ESM default export extending `@commitlint/config-conventional`. It sets `header-max-length: 72`, `scope-case: lower-case`, and `subject-full-stop: never`.
|
|
20
|
-
- Its `subject-case` rule is off
|
|
20
|
+
- Its `subject-case` rule is off.
|
|
21
21
|
- `.husky/` holds `pre-commit`, `commit-msg`, `pre-push`, `post-merge`, and `post-rewrite`.
|
|
22
22
|
|
|
23
23
|
### Verify runner
|
|
@@ -6,13 +6,16 @@ on:
|
|
|
6
6
|
- main
|
|
7
7
|
workflow_dispatch:
|
|
8
8
|
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
9
12
|
jobs:
|
|
10
13
|
static-checks:
|
|
11
14
|
name: 🛡️ Static Checks
|
|
12
15
|
runs-on: ubuntu-latest
|
|
13
16
|
steps:
|
|
14
17
|
- name: Checkout
|
|
15
|
-
uses: actions/checkout@
|
|
18
|
+
uses: actions/checkout@v7
|
|
16
19
|
|
|
17
20
|
- name: Setup Bun
|
|
18
21
|
uses: oven-sh/setup-bun@v2
|
|
@@ -57,11 +57,11 @@ The `# Claude` group holds three entries, being `.canon/`, `.claude/worktrees/`,
|
|
|
57
57
|
|
|
58
58
|
## CLI
|
|
59
59
|
|
|
60
|
-
| Command | What it does
|
|
61
|
-
| -------------------- |
|
|
62
|
-
| `canon claude init` | Seeds `.claude/` workflow docs, updates `.gitignore`
|
|
63
|
-
| `canon claude sync` | Reconciles `.gitignore` against the manifest. Reports seeded file status.
|
|
64
|
-
| `canon claude setup` | Installs user-level config to `~/.claude/` once per machine: statusline
|
|
60
|
+
| Command | What it does |
|
|
61
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `canon claude init` | Seeds `.claude/` workflow docs, updates `.gitignore` |
|
|
63
|
+
| `canon claude sync` | Reconciles `.gitignore` against the manifest. Reports seeded file status. |
|
|
64
|
+
| `canon claude setup` | Installs user-level config to `~/.claude/` once per machine: statusline and the permissions allow and deny lists, and removes the empty attribution pair an earlier setup wrote. |
|
|
65
65
|
|
|
66
66
|
## CLAUDE.md
|
|
67
67
|
|
|
@@ -85,11 +85,17 @@ The seed at `tooling/claude/seeds/.claude/settings.json` ships hooks that pair w
|
|
|
85
85
|
- A PostToolUse hook pairs with `standards-audit.sh`.
|
|
86
86
|
- Project settings layer over user settings, so a project file carries only what is project-specific.
|
|
87
87
|
|
|
88
|
+
### Session defaults
|
|
89
|
+
|
|
90
|
+
The seed also sets two defaults every session in the target runs under.
|
|
91
|
+
|
|
92
|
+
- `permissions.deny` holds `Agent` and `Task`, so no session spawns a subagent. Parallel work runs as separate background sessions instead.
|
|
93
|
+
- `autoMemoryEnabled` is `false`. A person turns it back on for themselves in `settings.local.json`.
|
|
94
|
+
- `canon migrate session-defaults` adds both to a target seeded before them, since sync never rewrites an existing settings file.
|
|
95
|
+
|
|
88
96
|
### User template
|
|
89
97
|
|
|
90
98
|
The template at `tooling/claude/user/settings.template.json` is installed once per machine by `canon claude setup`.
|
|
91
99
|
|
|
92
|
-
- It sets `attribution.commit` and `attribution.pr` to empty strings to suppress co-author tags.
|
|
93
100
|
- It allows `Bash(bun run *)` under `permissions.allow`.
|
|
94
101
|
- Its `permissions.deny` holds `Read(**/.env)` and `Read(**/.env.*)`, blocking accidental secret exposure across any project.
|
|
95
|
-
- The deprecated `includeCoAuthoredBy` key is not used.
|