@erclx/canon 5.41.0 → 5.43.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/api-design/references/adopted.md +34 -14
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
- package/claude/skills/auto-ship/SKILL.md +24 -4
- package/claude/skills/auto-ship/references/draft-mark.md +32 -6
- package/claude/skills/auto-ship/references/failure-recovery.md +15 -13
- package/claude/skills/auto-ship/references/tier-failures.md +31 -10
- package/claude/skills/auto-ship/references/ui-checklist.md +9 -4
- package/claude/skills/auto-ship/references/worktree-name.md +9 -3
- package/claude/skills/build-in-slices/references/adopted.md +35 -21
- package/claude/skills/canvas-board/references/canvas.md +17 -6
- package/claude/skills/canvas-board/references/design.md +26 -7
- 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/code-craft/references/adopted.md +58 -20
- package/claude/skills/codebase-layout/references/adopted.md +49 -16
- package/claude/skills/codebase-layout/references/frontend.md +11 -4
- package/claude/skills/codebase-layout/references/tests.md +4 -2
- package/claude/skills/context-fold/references/classify.md +10 -15
- package/claude/skills/context-fold/references/completion-report.md +11 -9
- package/claude/skills/context-fold/references/context-refresh.md +26 -9
- package/claude/skills/context-fold/references/unusable-baseline.md +5 -3
- package/claude/skills/create-rule/REQUIREMENT.md +5 -0
- package/claude/skills/create-rule/SKILL.md +10 -1
- package/claude/skills/create-rule/references/checklist.md +26 -0
- package/claude/skills/create-rule/references/rule.md +98 -20
- package/claude/skills/create-skill/SKILL.md +1 -0
- package/claude/skills/create-skill/references/skill-disclosure.md +10 -2
- package/claude/skills/create-skill/references/skill-frontmatter.md +43 -0
- package/claude/skills/create-skill/references/skill-paths.md +7 -4
- package/claude/skills/create-skill/references/skill-practice.md +6 -3
- package/claude/skills/create-skill/references/skill-requirement.md +4 -2
- package/claude/skills/create-skill/references/skill.md +67 -44
- package/claude/skills/decision-escalate/SKILL.md +1 -2
- package/claude/skills/deploy-app/references/vercel.md +2 -1
- package/claude/skills/deprecation-migration/references/adopted.md +44 -25
- package/claude/skills/design-taste/references/adopted.md +9 -1
- package/claude/skills/design-taste/references/craft.md +3 -1
- package/claude/skills/design-taste/references/kinds.md +8 -7
- package/claude/skills/design-taste/references/preflight.md +12 -0
- package/claude/skills/design-taste/references/systems.md +7 -6
- package/claude/skills/design-taste/references/tells.md +24 -25
- package/claude/skills/design-taste/references/vocabulary.md +7 -5
- package/claude/skills/document-health/references/axes.md +55 -30
- package/claude/skills/draft-and-pick/references/live-arms.md +2 -2
- package/claude/skills/draft-doc/references/context.md +4 -3
- package/claude/skills/draft-doc/references/docs.md +6 -3
- package/claude/skills/draft-doc/references/readme/agent-facing.md +6 -0
- package/claude/skills/draft-doc/references/readme/application.md +8 -2
- package/claude/skills/draft-doc/references/readme/cli.md +5 -1
- package/claude/skills/draft-doc/references/readme/library.md +4 -0
- package/claude/skills/draft-doc/references/readme/plugin.md +9 -1
- package/claude/skills/draft-doc/references/readme.md +30 -13
- package/claude/skills/draft-doc/references/requirements.md +17 -4
- package/claude/skills/draft-doc/references/wiki-kind.md +20 -10
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/references/architecture-set.md +7 -3
- package/claude/skills/draft-figure/references/figures.md +8 -6
- package/claude/skills/draft-figure/references/hand-drawn.md +3 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +19 -9
- package/claude/skills/draft-figure/references/mermaid.md +10 -5
- package/claude/skills/draft-ready/references/ready.md +22 -15
- package/claude/skills/git-branch/references/branch.md +11 -1
- package/claude/skills/git-branch/references/slug.md +17 -5
- package/claude/skills/git-commit/references/commit.md +9 -5
- package/claude/skills/git-commit/references/versioning.md +23 -17
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +5 -6
- package/claude/skills/git-issue/references/issue.md +29 -16
- package/claude/skills/git-pr/REQUIREMENT.md +1 -5
- package/claude/skills/git-pr/SKILL.md +11 -22
- package/claude/skills/git-pr/references/final-command.md +21 -8
- package/claude/skills/git-pr/references/labels.md +45 -28
- package/claude/skills/git-pr/references/pr.md +29 -9
- package/claude/skills/git-pr/references/publish.md +42 -28
- package/claude/skills/git-pr/references/task-number.md +20 -8
- package/claude/skills/git-ship/SKILL.md +1 -1
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/plan-feature/references/plan-lifecycle.md +10 -3
- package/claude/skills/plan-feature/references/plan.md +58 -21
- package/claude/skills/plan-groundwork/references/groundwork-spikes.md +4 -1
- package/claude/skills/plan-groundwork/references/groundwork.md +14 -16
- package/claude/skills/plan-intake/references/intake.md +29 -24
- package/claude/skills/pr-evidence/REQUIREMENT.md +40 -0
- package/claude/skills/pr-evidence/SKILL.md +53 -0
- package/claude/skills/pr-evidence/references/evidence.md +150 -0
- package/claude/skills/review-address/references/post-review-findings.md +10 -3
- package/claude/skills/review-address/references/rebase-conflicts.md +15 -7
- package/claude/skills/review-address/references/rest-reads.md +3 -1
- package/claude/skills/review-craft/references/adopted.md +35 -18
- package/claude/skills/review-craft/references/security.md +42 -7
- package/claude/skills/review-craft/references/ui.md +23 -8
- package/claude/skills/review-pr/SKILL.md +6 -6
- package/claude/skills/review-pr/references/body-variants.md +29 -9
- package/claude/skills/review-pr/references/close-out.md +20 -5
- package/claude/skills/review-pr/references/key-changes.md +27 -3
- package/claude/skills/review-pr/references/post.md +83 -31
- package/claude/skills/review-pr/references/scope.md +54 -19
- package/claude/skills/review-ui/references/close-out.md +7 -5
- package/claude/skills/review-ui/references/driving.md +24 -4
- package/claude/skills/review-ui/references/post.md +43 -15
- package/claude/skills/role-orchestrator/SKILL.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +67 -17
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +80 -40
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +52 -15
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +48 -26
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +12 -13
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +72 -36
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +16 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +64 -24
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +80 -26
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +41 -18
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +19 -7
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +22 -12
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +15 -6
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +25 -7
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +34 -8
- package/claude/skills/role-orchestrator/scripts/poll.ts +16 -24
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +3 -3
- package/claude/skills/role-worker/REQUIREMENT.md +3 -0
- package/claude/skills/role-worker/SKILL.md +34 -2
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +2 -2
- package/claude/skills/search-craft/references/adopted.md +18 -6
- package/claude/skills/session-map/references/session.md +26 -7
- package/claude/skills/session-worktree/references/main-root-writes.md +25 -8
- package/claude/skills/systematic-debugging/references/adopted.md +30 -16
- package/claude/skills/target-check/references/domains.md +39 -11
- package/claude/skills/target-setup/references/detect.md +80 -65
- package/claude/skills/target-setup/references/indexes.md +8 -6
- package/claude/skills/task-board/references/board.md +86 -30
- package/claude/skills/task-board/references/decline.md +29 -13
- package/claude/skills/task-board/references/tasks-decline.md +12 -5
- package/claude/skills/task-board/references/tasks.md +65 -33
- package/claude/skills/task-complete/references/archive.md +41 -17
- package/claude/skills/task-complete/references/receipt-sweep.md +23 -9
- package/claude/skills/task-complete/references/unusable-baseline.md +10 -3
- package/claude/skills/teach-workspace/references/glossary.md +6 -2
- package/claude/skills/teach-workspace/references/lesson-build.md +44 -16
- package/claude/skills/teach-workspace/references/lesson-craft.md +12 -7
- package/claude/skills/teach-workspace/references/pedagogy.md +6 -3
- package/claude/skills/teach-workspace/references/promotion-fold.md +17 -11
- package/claude/skills/teach-workspace/references/promotion.md +31 -8
- package/claude/skills/teach-workspace/references/revisit.md +8 -2
- package/claude/skills/teach-workspace/references/teach.md +17 -9
- package/claude/skills/test-craft/references/adopted.md +48 -14
- package/claude/skills/test-craft/references/component.md +7 -4
- package/claude/skills/test-craft/references/e2e.md +25 -7
- package/claude/skills/test-craft/references/unit.md +3 -1
- package/claude/skills/test-first/references/adopted.md +12 -4
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/video-craft/references/adopted.md +62 -18
- package/claude/skills/write-human/references/adopted.md +21 -8
- package/docs/agents/audits.md +1 -0
- package/docs/agents/migrate.md +11 -8
- package/docs/target/migrations.md +11 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map.md +1 -0
- package/governance/detect.toml +4 -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/tooling/ci-workflow.md +6 -39
- package/governance/rules/ui/design-reference.md +37 -0
- package/governance/rules/writing/markdown.md +2 -1
- package/package.json +6 -6
- package/src/audits/catalog.ts +42 -0
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/settings.ts +8 -4
- package/src/commands/claude.ts +11 -1
- package/src/commands/gov.ts +198 -0
- package/src/commands/migrate.ts +121 -2
- package/src/commands/pr.ts +146 -105
- package/src/commands/tooling.ts +3 -3
- package/src/commands/worktrees.ts +49 -0
- package/src/gate/stages.ts +15 -0
- package/src/gov/audit.ts +440 -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 +144 -0
- package/src/migrate/session-defaults.ts +138 -0
- package/src/pr/lift.ts +6 -3
- package/src/pr/local.ts +4 -69
- package/src/pr/review-state.ts +182 -0
- package/src/sync/check.ts +15 -0
- package/src/tooling/stamp.ts +56 -12
- package/src/worktrees/stop.ts +92 -0
- 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/settings.json +4 -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/git-pr/references/evidence.md +0 -124
package/src/audits/catalog.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ReachRefusal } from '@/claude/skills-reach'
|
|
|
4
4
|
import type { ContextAuditRefusal } from '@/context/audit'
|
|
5
5
|
import type { SweepVerdict } from '@/context/classify/patterns'
|
|
6
6
|
import type { AuditRefusal } from '@/deps/audit'
|
|
7
|
+
import type { RuleAuditRefusal } from '@/gov/audit'
|
|
7
8
|
import type { RestatedRefusal } from '@/gov/restated'
|
|
8
9
|
import type { LabelAuditRefusal } from '@/labels/audit'
|
|
9
10
|
import type { ValidateRefusal as RecordRefusal } from '@/records/validate'
|
|
@@ -412,6 +413,33 @@ function restatedCounts(record: unknown): Record<string, number> | undefined {
|
|
|
412
413
|
})
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Counts rules rather than findings, since a rule past four limits is one
|
|
418
|
+
* rewrite, and keeps the two ledger findings apart because a missing ledger and
|
|
419
|
+
* a short one are fixed by different passes.
|
|
420
|
+
*/
|
|
421
|
+
function ruleCounts(record: unknown): Record<string, number> | undefined {
|
|
422
|
+
const rules = asObject(record)?.rules
|
|
423
|
+
if (!Array.isArray(rules)) return undefined
|
|
424
|
+
|
|
425
|
+
let shapeRules = 0
|
|
426
|
+
let missingLedgers = 0
|
|
427
|
+
let ledgerMismatches = 0
|
|
428
|
+
|
|
429
|
+
for (const raw of rules) {
|
|
430
|
+
const rule = asObject(raw)
|
|
431
|
+
const findings = lengthOf(rule?.findings)
|
|
432
|
+
const ledger = asObject(rule?.ledger)
|
|
433
|
+
if (findings === undefined || ledger === undefined) return undefined
|
|
434
|
+
|
|
435
|
+
if (findings > 0) shapeRules += 1
|
|
436
|
+
if (ledger.status === 'missing') missingLedgers += 1
|
|
437
|
+
if ((lengthOf(ledger.mismatches) ?? 0) > 0) ledgerMismatches += 1
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { shapeRules, missingLedgers, ledgerMismatches }
|
|
441
|
+
}
|
|
442
|
+
|
|
415
443
|
function findingsOnly(record: unknown): Record<string, number> | undefined {
|
|
416
444
|
const root = asObject(record)
|
|
417
445
|
if (root === undefined) return undefined
|
|
@@ -681,6 +709,20 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
681
709
|
] satisfies RestatedRefusal[],
|
|
682
710
|
counts: restatedCounts,
|
|
683
711
|
},
|
|
712
|
+
{
|
|
713
|
+
id: 'rules',
|
|
714
|
+
label: 'Rule shape and ledgers',
|
|
715
|
+
argv: ['gov', 'audit', '--json'],
|
|
716
|
+
// Reports rather than gates. The corpus starts far from zero, and the
|
|
717
|
+
// `rule-shape` stage already fails a push on each folder once its rewrite
|
|
718
|
+
// lists it as gated, so gating the whole set here would fail every push.
|
|
719
|
+
gatingExits: [],
|
|
720
|
+
corpus: 'tracked',
|
|
721
|
+
// A target that authors no rule of its own has no corpus here, which is an
|
|
722
|
+
// absence rather than a break, on the same test the restated sweep takes.
|
|
723
|
+
absentReasons: ['no-rules'] satisfies RuleAuditRefusal[],
|
|
724
|
+
counts: ruleCounts,
|
|
725
|
+
},
|
|
684
726
|
{
|
|
685
727
|
id: 'counts',
|
|
686
728
|
label: 'Self-stated catalog counts',
|
|
@@ -81,6 +81,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
81
81
|
'Open the preview for that pull request and click through each box on its visual checklist, then post what happened.',
|
|
82
82
|
expect: 'review-ui',
|
|
83
83
|
},
|
|
84
|
+
{
|
|
85
|
+
prompt:
|
|
86
|
+
'I pushed another commit to the open PR. Update the screenshot comparison on it so the reviewer sees the current head.',
|
|
87
|
+
expect: 'pr-evidence',
|
|
88
|
+
},
|
|
84
89
|
{
|
|
85
90
|
prompt:
|
|
86
91
|
'The screencast draft is finished. Turn it into an actual video now.',
|
package/src/claude/settings.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { dirname } from 'node:path'
|
|
|
5
5
|
const DEFAULT_INDENT = ' '
|
|
6
6
|
const INDENT = /^([ \t]+)\S/m
|
|
7
7
|
|
|
8
|
+
// The pair setup once wrote to suppress Claude's attribution. Only this exact
|
|
9
|
+
// value is retracted, since anything else is a person's own choice.
|
|
10
|
+
const RETIRED_ATTRIBUTION = { commit: '', pr: '' }
|
|
11
|
+
|
|
8
12
|
export type SettingsRecord = Record<string, unknown>
|
|
9
13
|
|
|
10
14
|
export interface SettingsKey {
|
|
@@ -107,10 +111,10 @@ export function planSettings(
|
|
|
107
111
|
}
|
|
108
112
|
keys.push({ label: 'statusLine', changed: statusLineChanged })
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
if (equals(next.attribution, RETIRED_ATTRIBUTION)) {
|
|
115
|
+
delete next.attribution
|
|
116
|
+
keys.push({ label: 'attribution', changed: true })
|
|
117
|
+
}
|
|
114
118
|
|
|
115
119
|
const permissions = asRecord(next.permissions)
|
|
116
120
|
const merged: SettingsRecord = { ...permissions }
|
package/src/commands/claude.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
serializeSettings,
|
|
70
70
|
writeSettings,
|
|
71
71
|
} from '@/claude/settings'
|
|
72
|
+
import { readSessionDefaults } from '@/migrate/session-defaults'
|
|
72
73
|
import { copyPreservingMode } from '@/sync/copy'
|
|
73
74
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
|
|
74
75
|
import { recordDir } from '@/roots/record'
|
|
@@ -255,7 +256,7 @@ export function register(program: Command): void {
|
|
|
255
256
|
|
|
256
257
|
claude
|
|
257
258
|
.command('setup')
|
|
258
|
-
.description('One-shot user-level config (statusline,
|
|
259
|
+
.description('One-shot user-level config (statusline, permissions)')
|
|
259
260
|
.argument('[dest]', 'User config directory', join(homedir(), '.claude'))
|
|
260
261
|
.helpOption('-h, --help', 'Show this help message')
|
|
261
262
|
.action(async (dest: string) => {
|
|
@@ -663,6 +664,15 @@ async function runSync(target: string): Promise<number> {
|
|
|
663
664
|
else logWarn(`${name}/ missing. Run \`canon claude init\``)
|
|
664
665
|
}
|
|
665
666
|
|
|
667
|
+
// Sync never rewrites an existing settings file, so a target seeded before
|
|
668
|
+
// the session defaults learns of them only here.
|
|
669
|
+
const defaults = await readSessionDefaults(resolved, PROJECT_ROOT)
|
|
670
|
+
if (defaults.kind === 'planned' && defaults.plan.predates) {
|
|
671
|
+
logWarn(
|
|
672
|
+
'settings.json lacks the session defaults. Run `canon migrate session-defaults --write`',
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
666
676
|
logStep('Scanning .gitignore')
|
|
667
677
|
const chain = claudeChain(PROJECT_ROOT)
|
|
668
678
|
const pruned = await pruneGitignore(chain, resolved)
|
package/src/commands/gov.ts
CHANGED
|
@@ -32,6 +32,16 @@ import {
|
|
|
32
32
|
SEED_REL,
|
|
33
33
|
SHIPPED_SKILLS_REL,
|
|
34
34
|
} from '@/gov/restated'
|
|
35
|
+
import {
|
|
36
|
+
auditExit,
|
|
37
|
+
hasFinding,
|
|
38
|
+
PRACTICE_FOLDERS,
|
|
39
|
+
readRuleAudit,
|
|
40
|
+
RULE_GATED_FOLDERS,
|
|
41
|
+
RULE_LIMITS,
|
|
42
|
+
type RuleAuditReport,
|
|
43
|
+
type RuleReading,
|
|
44
|
+
} from '@/gov/audit'
|
|
35
45
|
import {
|
|
36
46
|
CITATION_MARKER,
|
|
37
47
|
type CitationReport,
|
|
@@ -123,6 +133,12 @@ interface CountsOptions {
|
|
|
123
133
|
readonly json?: boolean
|
|
124
134
|
}
|
|
125
135
|
|
|
136
|
+
interface AuditOptions {
|
|
137
|
+
readonly root?: string
|
|
138
|
+
readonly json?: boolean
|
|
139
|
+
readonly gate?: boolean
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
/** What a reader does about each way the sweep produced no reading. */
|
|
127
143
|
const RESTATED_REFUSALS: Record<RestatedRefusal, string> = {
|
|
128
144
|
'no-instructions': `No ${INSTRUCTIONS_REL} here, or it carries no bullet, so there is no instruction corpus to sweep.`,
|
|
@@ -573,6 +589,188 @@ export function register(program: Command): void {
|
|
|
573
589
|
.action((opts: RestatedOptions) => {
|
|
574
590
|
process.exitCode = runRestated(opts)
|
|
575
591
|
})
|
|
592
|
+
|
|
593
|
+
gov
|
|
594
|
+
.command('audit')
|
|
595
|
+
.description(
|
|
596
|
+
'Report every rule past a shape limit, and in this repository every outside-practice rule missing a source ledger',
|
|
597
|
+
)
|
|
598
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
599
|
+
.argument(
|
|
600
|
+
'[paths...]',
|
|
601
|
+
'Rule files or folders of rules to read, defaulting to the corpus',
|
|
602
|
+
)
|
|
603
|
+
.option('--root <path>', 'Tree to read, defaulting to the cwd')
|
|
604
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
605
|
+
.option(
|
|
606
|
+
'--gate',
|
|
607
|
+
'Set 2 only for a finding in a gated folder, which the push stage reads',
|
|
608
|
+
)
|
|
609
|
+
.addHelpText(
|
|
610
|
+
'after',
|
|
611
|
+
[
|
|
612
|
+
'',
|
|
613
|
+
'Limits, each a finding one past its number:',
|
|
614
|
+
` words ${RULE_LIMITS.words} words in the body`,
|
|
615
|
+
` bullets ${RULE_LIMITS.bullets} bullets in the file`,
|
|
616
|
+
` h2 ${RULE_LIMITS.h2} H2 headings`,
|
|
617
|
+
` perH2 ${RULE_LIMITS.perH2} bullets under one H2, the text above the first H2 a group`,
|
|
618
|
+
` chars ${RULE_LIMITS.chars} characters in one bullet, continuation lines folded in`,
|
|
619
|
+
` sentences ${RULE_LIMITS.sentences} sentences in one bullet`,
|
|
620
|
+
' emphasis any CRITICAL, IMPORTANT, MUST, ALWAYS, NEVER, or "you must"',
|
|
621
|
+
' outside a code span',
|
|
622
|
+
'',
|
|
623
|
+
'Corpus:',
|
|
624
|
+
' A tree holding governance/rules/ reads it and internal/rules/, and',
|
|
625
|
+
` asks a ledger of each rule under ${PRACTICE_FOLDERS.join(', ')}`,
|
|
626
|
+
' at governance/ledger/<folder>/<slug>.md, one table row per bullet',
|
|
627
|
+
' under a heading of the same name.',
|
|
628
|
+
' Any other tree reads .claude/rules/project/ alone and asks no ledger.',
|
|
629
|
+
' .claude/rules/canon/ is fixed at the toolkit source and never read',
|
|
630
|
+
' unless named. A named path resolves against --root and must sit',
|
|
631
|
+
' inside it.',
|
|
632
|
+
'',
|
|
633
|
+
`Gated folders: ${RULE_GATED_FOLDERS.length === 0 ? 'none yet' : RULE_GATED_FOLDERS.join(', ')}`,
|
|
634
|
+
'',
|
|
635
|
+
'Exit codes:',
|
|
636
|
+
' 0 no finding, or under --gate no finding in a gated folder',
|
|
637
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
638
|
+
' 2 at least one finding, or under --gate one in a gated folder',
|
|
639
|
+
'',
|
|
640
|
+
'Reasons:',
|
|
641
|
+
' no-rules the tree holds no rule corpus to read, or a named folder holds no rule',
|
|
642
|
+
' no-path a named path does not exist',
|
|
643
|
+
' outside-root a named path sits outside --root',
|
|
644
|
+
'',
|
|
645
|
+
'Examples:',
|
|
646
|
+
' canon gov audit',
|
|
647
|
+
' canon gov audit .claude/rules/project/api.md',
|
|
648
|
+
' canon gov audit --json',
|
|
649
|
+
'',
|
|
650
|
+
].join('\n'),
|
|
651
|
+
)
|
|
652
|
+
.action(async (paths: string[], opts: AuditOptions) => {
|
|
653
|
+
process.exitCode = await runAudit(paths, opts)
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Reports by default and gates only under `--gate`, and there only for the
|
|
659
|
+
* folders `RULE_GATED_FOLDERS` lists. The corpus starts far from zero, so the
|
|
660
|
+
* push stage fails on a folder once its rewrite has brought it there.
|
|
661
|
+
*/
|
|
662
|
+
async function runAudit(
|
|
663
|
+
paths: readonly string[],
|
|
664
|
+
opts: AuditOptions,
|
|
665
|
+
): Promise<number> {
|
|
666
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
667
|
+
const report = await readRuleAudit(root, paths)
|
|
668
|
+
const emitJson = opts.json ?? false
|
|
669
|
+
|
|
670
|
+
if (report.kind === 'unreadable') {
|
|
671
|
+
intro('canon gov audit')
|
|
672
|
+
logStep('Refused')
|
|
673
|
+
logError(report.message)
|
|
674
|
+
outro()
|
|
675
|
+
|
|
676
|
+
if (emitJson) {
|
|
677
|
+
process.stdout.write(
|
|
678
|
+
`${JSON.stringify({ ok: false, reason: report.reason, message: report.message })}\n`,
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return 1
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
reportAudit(report, root, paths.length > 0)
|
|
686
|
+
|
|
687
|
+
if (emitJson) {
|
|
688
|
+
process.stdout.write(
|
|
689
|
+
`${JSON.stringify({
|
|
690
|
+
ok: true,
|
|
691
|
+
root,
|
|
692
|
+
limits: RULE_LIMITS,
|
|
693
|
+
gated: RULE_GATED_FOLDERS,
|
|
694
|
+
...report,
|
|
695
|
+
})}\n`,
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return auditExit(report, {
|
|
700
|
+
gate: opts.gate ?? false,
|
|
701
|
+
gated: RULE_GATED_FOLDERS,
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function describeLedger(reading: RuleReading): string[] {
|
|
706
|
+
const { ledger } = reading
|
|
707
|
+
if (ledger.status === 'missing') {
|
|
708
|
+
return [`${reading.file}: no ledger at ${ledger.path}`]
|
|
709
|
+
}
|
|
710
|
+
if (ledger.status !== 'present') return []
|
|
711
|
+
return ledger.mismatches.map(
|
|
712
|
+
(mismatch) =>
|
|
713
|
+
`${ledger.path}: ${mismatch.heading ?? '(above the first H2)'} has ${mismatch.rows} row(s) for ${mismatch.bullets} bullet(s)`,
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function reportAudit(
|
|
718
|
+
report: Extract<RuleAuditReport, { kind: 'measured' }>,
|
|
719
|
+
root: string,
|
|
720
|
+
named: boolean,
|
|
721
|
+
): void {
|
|
722
|
+
intro('canon gov audit')
|
|
723
|
+
|
|
724
|
+
const shaped = report.rules.filter((reading) => reading.findings.length > 0)
|
|
725
|
+
const ledgers = report.rules.flatMap(describeLedger)
|
|
726
|
+
|
|
727
|
+
logStep('Corpus')
|
|
728
|
+
logInfo(
|
|
729
|
+
named
|
|
730
|
+
? `${report.rules.length} named rule(s) in ${root}`
|
|
731
|
+
: `${report.rules.length} rule(s) under ${report.corpus.join(' and ')} in ${root}`,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
logStep('Shape')
|
|
735
|
+
if (shaped.length === 0) {
|
|
736
|
+
logInfo('every rule is within the six limits and carries no emphasis word')
|
|
737
|
+
} else {
|
|
738
|
+
for (const reading of shaped) {
|
|
739
|
+
for (const finding of reading.findings) {
|
|
740
|
+
const at =
|
|
741
|
+
finding.line === undefined
|
|
742
|
+
? reading.file
|
|
743
|
+
: `${reading.file}:${finding.line}`
|
|
744
|
+
logWarn(
|
|
745
|
+
finding.limit === 'emphasis'
|
|
746
|
+
? `${at}: emphasis word`
|
|
747
|
+
: `${at}: ${finding.limit} ${finding.count} past ${finding.max}`,
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
logStep('Ledgers')
|
|
754
|
+
if (report.scope === 'target') {
|
|
755
|
+
logInfo('not read, since ledgers live in the toolkit and never ship')
|
|
756
|
+
} else if (ledgers.length === 0) {
|
|
757
|
+
logInfo('every outside-practice rule has a ledger row per bullet')
|
|
758
|
+
} else {
|
|
759
|
+
for (const line of ledgers) logWarn(line)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const failing = report.rules.filter(hasFinding).length
|
|
763
|
+
logStep('Summary')
|
|
764
|
+
logInfo(
|
|
765
|
+
`${failing} of ${report.rules.length} rule(s) carry a finding: ${shaped.length} with a shape finding, ${ledgers.length} ledger finding(s)`,
|
|
766
|
+
)
|
|
767
|
+
logInfo(
|
|
768
|
+
RULE_GATED_FOLDERS.length === 0
|
|
769
|
+
? 'no folder is gated yet, so --gate passes every finding'
|
|
770
|
+
: `gated: ${RULE_GATED_FOLDERS.join(', ')}`,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
outro()
|
|
576
774
|
}
|
|
577
775
|
|
|
578
776
|
/**
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { listRepositoryFiles } from '@/git/files'
|
|
5
5
|
import { indexSourceRules } from '@/gov/adapter'
|
|
@@ -25,6 +25,11 @@ import {
|
|
|
25
25
|
type RuleLayoutPlan,
|
|
26
26
|
walkFlatRules,
|
|
27
27
|
} from '@/migrate/rule-layout'
|
|
28
|
+
import {
|
|
29
|
+
applySessionDefaults,
|
|
30
|
+
readSessionDefaults,
|
|
31
|
+
type SessionDefaultsRead,
|
|
32
|
+
} from '@/migrate/session-defaults'
|
|
28
33
|
import {
|
|
29
34
|
SKILL_CITATION_MAP,
|
|
30
35
|
SKILL_NAME_MAP,
|
|
@@ -50,7 +55,7 @@ import {
|
|
|
50
55
|
} from '@/migrate/scratch-evidence'
|
|
51
56
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
|
|
52
57
|
import { readStamp, stampedHashes } from '@/sync/stamp'
|
|
53
|
-
import { logError, logInfo, logStep, logWarn, plural } from '@/cli/ui'
|
|
58
|
+
import { logAdd, logError, logInfo, logStep, logWarn, plural } from '@/cli/ui'
|
|
54
59
|
|
|
55
60
|
interface SweepOptions {
|
|
56
61
|
readonly json?: boolean
|
|
@@ -959,6 +964,81 @@ function toRuleLayoutRecord(
|
|
|
959
964
|
}
|
|
960
965
|
}
|
|
961
966
|
|
|
967
|
+
interface SessionDefaultsOptions {
|
|
968
|
+
readonly json?: boolean
|
|
969
|
+
readonly write?: boolean
|
|
970
|
+
readonly root?: string
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Adds the seeded session defaults to a target settings file that predates
|
|
975
|
+
* them. A missing file is nothing to do, since `canon claude init` seeds the
|
|
976
|
+
* whole file with the defaults already in it.
|
|
977
|
+
*/
|
|
978
|
+
async function runSessionDefaults(
|
|
979
|
+
opts: SessionDefaultsOptions,
|
|
980
|
+
): Promise<number> {
|
|
981
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
982
|
+
|
|
983
|
+
const mismatch = checkoutMismatchWarning(process.cwd())
|
|
984
|
+
if (mismatch !== undefined) logWarn(mismatch)
|
|
985
|
+
|
|
986
|
+
const read = await readSessionDefaults(root, PROJECT_ROOT)
|
|
987
|
+
|
|
988
|
+
if (opts.json) {
|
|
989
|
+
process.stdout.write(
|
|
990
|
+
`${JSON.stringify(toSessionDefaultsRecord(read, opts.write))}\n`,
|
|
991
|
+
)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (read.kind === 'refused') {
|
|
995
|
+
logError(read.reason)
|
|
996
|
+
return 1
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (read.kind === 'absent') {
|
|
1000
|
+
logInfo(`No ${read.path}, so there is nothing to migrate.`)
|
|
1001
|
+
return 0
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
for (const key of read.plan.keys) {
|
|
1005
|
+
if (key.changed) logAdd(key.label)
|
|
1006
|
+
else logInfo(key.label)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (!read.plan.changed) {
|
|
1010
|
+
logInfo('Session defaults already present.')
|
|
1011
|
+
return 0
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (!opts.write) {
|
|
1015
|
+
logWarn('Nothing was written. Pass --write to apply this plan.')
|
|
1016
|
+
return 2
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
await applySessionDefaults(read)
|
|
1020
|
+
logStep(`Wrote ${read.path}.`)
|
|
1021
|
+
return 0
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function toSessionDefaultsRecord(
|
|
1025
|
+
read: SessionDefaultsRead,
|
|
1026
|
+
wrote: boolean | undefined,
|
|
1027
|
+
): unknown {
|
|
1028
|
+
if (read.kind === 'refused') {
|
|
1029
|
+
return { ok: false, wrote: false, reason: read.reason }
|
|
1030
|
+
}
|
|
1031
|
+
if (read.kind === 'absent') {
|
|
1032
|
+
return { ok: true, wrote: false, path: read.path, keys: [] }
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
ok: true,
|
|
1036
|
+
wrote: wrote === true && read.plan.changed,
|
|
1037
|
+
path: read.path,
|
|
1038
|
+
keys: read.plan.keys,
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
962
1042
|
export function register(program: Command): void {
|
|
963
1043
|
const migrate = program
|
|
964
1044
|
.command('migrate')
|
|
@@ -1322,4 +1402,43 @@ export function register(program: Command): void {
|
|
|
1322
1402
|
.action(async (opts: RuleLayoutOptions) => {
|
|
1323
1403
|
process.exitCode = await runRuleLayout(opts)
|
|
1324
1404
|
})
|
|
1405
|
+
|
|
1406
|
+
migrate
|
|
1407
|
+
.command('session-defaults')
|
|
1408
|
+
.description('Add the seeded session defaults to a target settings file')
|
|
1409
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
1410
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
1411
|
+
.option('--write', 'Apply the plan rather than reporting it')
|
|
1412
|
+
.option(
|
|
1413
|
+
'--root <path>',
|
|
1414
|
+
'Project root, defaulting to the working directory',
|
|
1415
|
+
)
|
|
1416
|
+
.addHelpText(
|
|
1417
|
+
'after',
|
|
1418
|
+
[
|
|
1419
|
+
'',
|
|
1420
|
+
'Adds Agent and Task to permissions.deny in .claude/settings.json,',
|
|
1421
|
+
'appending to the list without reordering it, and sets',
|
|
1422
|
+
'autoMemoryEnabled to false only when the key is absent. A deny entry',
|
|
1423
|
+
'is never removed and an existing autoMemoryEnabled is never changed.',
|
|
1424
|
+
'',
|
|
1425
|
+
'A missing settings file is nothing to do. A file that is not valid',
|
|
1426
|
+
'JSON, or whose permissions or deny has an unexpected shape, is',
|
|
1427
|
+
'refused by name and never partly written.',
|
|
1428
|
+
'',
|
|
1429
|
+
'Exit codes:',
|
|
1430
|
+
' 0 nothing to add, or --write applied the plan',
|
|
1431
|
+
' 1 refused: the toolkit root, or an unreadable settings file',
|
|
1432
|
+
' 2 a plan exists and --write was not passed',
|
|
1433
|
+
'',
|
|
1434
|
+
'Examples:',
|
|
1435
|
+
' canon migrate session-defaults',
|
|
1436
|
+
' canon migrate session-defaults --write',
|
|
1437
|
+
' canon migrate session-defaults --json',
|
|
1438
|
+
'',
|
|
1439
|
+
].join('\n'),
|
|
1440
|
+
)
|
|
1441
|
+
.action(async (opts: SessionDefaultsOptions) => {
|
|
1442
|
+
process.exitCode = await runSessionDefaults(opts)
|
|
1443
|
+
})
|
|
1325
1444
|
}
|