@erclx/canon 5.32.0 → 5.34.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/REQUIREMENT.md +1 -2
- package/claude/skills/api-design/SKILL.md +2 -3
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/auto-ship/SKILL.md +1 -1
- package/claude/skills/auto-ship/references/worktree-name.md +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +1 -1
- package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
- package/claude/skills/canon-cli/SKILL.md +75 -32
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
- package/claude/skills/canon-feedback/SKILL.md +11 -29
- package/claude/skills/canon-feedback/references/template.md +53 -0
- package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
- package/claude/skills/{canvas → canvas-board}/SKILL.md +6 -3
- package/claude/skills/{canvas → canvas-board}/references/canvas.md +26 -3
- package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +0 -1
- package/claude/skills/context-fold/REQUIREMENT.md +25 -33
- package/claude/skills/context-fold/SKILL.md +25 -70
- package/claude/skills/context-fold/references/classify.md +3 -3
- package/claude/skills/context-fold/references/completion-report.md +4 -4
- package/claude/skills/context-fold/references/context-refresh.md +5 -5
- package/{standards → claude/skills/context-fold/references}/context.md +16 -5
- package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
- package/claude/skills/create-rule/SKILL.md +3 -3
- package/{standards → claude/skills/create-rule/references}/rule.md +15 -6
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/{standards → claude/skills/create-skill/references}/skill.md +18 -15
- package/claude/skills/design-taste/REQUIREMENT.md +3 -3
- package/claude/skills/design-taste/SKILL.md +1 -1
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
- package/claude/skills/docs-sync/SKILL.md +3 -3
- package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
- package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
- package/claude/skills/draft-and-pick/SKILL.md +5 -5
- package/claude/skills/draft-doc/SKILL.md +6 -6
- package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
- package/claude/skills/draft-figure/SKILL.md +2 -2
- package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
- package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
- package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
- package/claude/skills/draft-identity/SKILL.md +2 -2
- package/claude/skills/draft-ready/SKILL.md +1 -1
- package/claude/skills/git-branch/SKILL.md +1 -1
- package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
- package/claude/skills/git-commit/SKILL.md +2 -2
- package/claude/skills/git-followup/REQUIREMENT.md +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-issue/references/issue.md +4 -1
- package/claude/skills/git-pr/SKILL.md +13 -13
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-pr/references/final-command.md +38 -14
- package/claude/skills/git-pr/references/labels.md +5 -1
- package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
- package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
- package/claude/skills/git-pr/references/task-number.md +1 -1
- package/claude/skills/git-ship/SKILL.md +15 -14
- package/claude/skills/git-split/SKILL.md +3 -3
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/review-address/references/post-review-findings.md +1 -1
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -1
- package/claude/skills/review-pr/SKILL.md +3 -3
- package/claude/skills/review-pr/references/body-variants.md +2 -2
- package/claude/skills/review-pr/references/key-changes.md +3 -3
- package/claude/skills/review-pr/references/post.md +4 -2
- package/claude/skills/review-ui/REQUIREMENT.md +1 -1
- package/claude/skills/review-ui/SKILL.md +8 -6
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +53 -17
- package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +32 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
- package/claude/skills/role-planner/REQUIREMENT.md +2 -1
- package/claude/skills/role-planner/SKILL.md +13 -2
- package/claude/skills/role-reviewer/SKILL.md +1 -1
- package/claude/skills/role-worker/REQUIREMENT.md +1 -0
- package/claude/skills/role-worker/SKILL.md +9 -1
- package/claude/skills/session-map/SKILL.md +1 -1
- package/claude/skills/session-resume/REQUIREMENT.md +2 -2
- package/claude/skills/session-worktree/SKILL.md +5 -5
- package/claude/skills/target-setup/SKILL.md +10 -4
- package/claude/skills/target-setup/references/detect.md +20 -3
- package/claude/skills/task-board/REQUIREMENT.md +7 -12
- package/claude/skills/task-board/SKILL.md +5 -17
- package/claude/skills/task-board/references/tasks.md +2 -2
- package/claude/skills/task-complete/REQUIREMENT.md +47 -0
- package/claude/skills/task-complete/SKILL.md +86 -0
- package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
- package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
- package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
- package/claude/skills/teach-workspace/SKILL.md +8 -4
- package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
- package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
- package/claude/skills/teach-workspace/references/promotion.md +3 -3
- package/claude/skills/test-craft/REQUIREMENT.md +2 -3
- package/claude/skills/test-craft/SKILL.md +15 -3
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/ux-measure/SKILL.md +1 -1
- package/docs/agents/canvas.md +6 -2
- package/docs/agents/commands.md +4 -3
- package/docs/agents/context-audit-checks.md +4 -4
- package/docs/agents/gov-detect.md +47 -0
- package/docs/agents/index.md +7 -4
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +10 -11
- package/docs/agents/key-changes.md +5 -5
- package/docs/agents/label-coverage.md +11 -0
- package/docs/agents/migrate.md +1 -1
- package/docs/agents/pr-evidence-layout.md +70 -0
- package/docs/agents/pr-evidence.md +8 -9
- package/docs/agents/pr-frames.md +7 -2
- package/docs/agents/pr-lift.md +132 -0
- package/docs/agents/records-push.md +2 -2
- package/docs/agents/records-stale.md +12 -47
- package/docs/agents/records-validate.md +3 -7
- package/docs/agents/records.md +5 -27
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/sandbox.md +4 -4
- package/docs/agents/scripting.md +2 -1
- package/docs/agents/skills-audit.md +1 -1
- package/docs/agents/tasks-plans.md +2 -2
- package/docs/agents/tasks.md +2 -2
- package/docs/target/migrations.md +10 -2
- package/docs/target/projects.md +4 -4
- package/docs/target/sync.md +1 -1
- package/docs/workflow/operating-model.md +2 -2
- package/docs/workflow/parallel-features.md +7 -7
- package/docs/workflow/skill-map-on-request.md +9 -9
- package/docs/workflow/skill-map.md +12 -11
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +201 -0
- package/governance/renames.toml +0 -11
- package/governance/rules/canon/context.md +13 -0
- package/governance/rules/claude/skill.md +17 -0
- package/governance/rules/code/000-code.md +1 -13
- package/governance/rules/code/010-testing.md +2 -30
- package/governance/rules/ui/400-ui.md +1 -1
- package/governance/rules/ui/480-canvas.md +13 -0
- package/governance/stacks/base.toml +2 -4
- package/package.json +4 -4
- package/scripts/core/install-check.sh +3 -3
- package/scripts/core/regen-hero.ts +0 -6
- package/scripts/lib/ui.sh +0 -168
- package/src/audits/catalog.ts +0 -1
- package/src/canvas/arrange.ts +66 -0
- package/src/canvas/capture.ts +20 -6
- package/src/canvas/content.ts +39 -0
- package/src/canvas/fit.ts +238 -0
- package/src/canvas/tokens.ts +28 -10
- package/src/capture/frame-values.ts +2 -32
- package/src/claude/cases/misc.ts +1 -1
- package/src/claude/cases/setup.ts +4 -0
- package/src/claude/cases/workflow.ts +6 -1
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +2 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/ui.ts +2 -3
- package/src/commands/canvas.ts +179 -1
- package/src/commands/claude.ts +1 -1
- package/src/commands/gov.ts +146 -16
- package/src/commands/labels.ts +326 -7
- package/src/commands/migrate.ts +6 -1
- package/src/commands/pr.ts +377 -2
- package/src/commands/records.ts +13 -399
- package/src/commands/sandbox.ts +3 -6
- package/src/commands/tasks.ts +3 -3
- package/src/context/audit.ts +5 -5
- package/src/context/folders.ts +1 -1
- package/src/design/budget.ts +1 -1
- package/src/design/components.ts +1 -1
- package/src/design/document.ts +1 -1
- package/src/design/regen.ts +1 -1
- package/src/design/render.ts +1 -1
- package/src/feedback/body.ts +26 -5
- package/src/gate/measures.ts +59 -45
- package/src/gate/sequencer.ts +56 -1
- package/src/gate/stages.ts +4 -11
- package/src/gov/detect.ts +301 -0
- package/src/gov/install.ts +2 -3
- package/src/gov/list.ts +23 -2
- package/src/gov/stacks.ts +10 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/labels/apply.ts +39 -0
- package/src/labels/format.ts +3 -3
- package/src/labels/phase.ts +3 -3
- package/src/labels/sections.ts +43 -0
- package/src/migrate/skill-names.ts +20 -1
- package/src/pr/evidence.ts +218 -45
- package/src/pr/frames.ts +23 -3
- package/src/pr/lift.ts +418 -0
- package/src/pr/paths.ts +4 -4
- package/src/pr/rest.ts +3 -0
- package/src/pr/review-scope.ts +76 -0
- package/src/records/backup.ts +16 -6
- package/src/records/stale-canonical.ts +31 -1
- package/src/records/validate.ts +2 -188
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/coverage.ts +2 -6
- package/src/sandbox/equivalence.ts +19 -51
- package/src/sandbox/expect.ts +4 -4
- package/src/sandbox/provision.ts +13 -36
- package/src/sandbox/scenario.ts +26 -48
- package/src/sandbox/tree.ts +2 -6
- package/src/standards/read.ts +14 -1
- package/src/sync/git.ts +1 -1
- package/src/tasks/archive.ts +5 -5
- package/src/tasks/branch.ts +3 -3
- package/src/tasks/label.ts +1 -1
- package/src/tasks/record.ts +2 -2
- package/src/tasks/validate.ts +1 -1
- package/standards/index.md +0 -13
- package/standards/standard.md +1 -7
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/pull_request_template.md +2 -2
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
- package/tooling/claude/seeds/.claude/settings.json +9 -0
- package/governance/rules/standards/context.md +0 -14
- package/governance/rules/standards/design.md +0 -12
- package/governance/rules/standards/readme.md +0 -12
- package/governance/rules/standards/rule.md +0 -13
- package/governance/rules/standards/skill.md +0 -21
- package/governance/rules/standards/standard.md +0 -13
- package/scripts/config.sh +0 -11
- package/scripts/lib/gov.sh +0 -14
- package/scripts/lib/sandbox-fixtures.sh +0 -191
- package/scripts/lib/sandbox-git.sh +0 -125
- package/scripts/lib/sandbox-path.sh +0 -50
- package/scripts/sandbox-hook.sh +0 -99
- package/src/gov/standard-rules.ts +0 -179
- package/src/records/migrate.ts +0 -78
- package/src/records/stale.ts +0 -243
- package/src/sandbox/hooks.ts +0 -128
- /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
- /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
- /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
package/src/sandbox/scenario.ts
CHANGED
|
@@ -42,15 +42,13 @@ export const FIXTURE_SUFFIX = '.fixture'
|
|
|
42
42
|
/** A scenario whose only arm carries this name routes none, as a bash file with no picker did. */
|
|
43
43
|
export const DEFAULT_ARM_NAME = 'default'
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** The picker's message unless a scenario names another. */
|
|
46
46
|
const ROUTE_PROMPT = 'Which scenario?'
|
|
47
47
|
|
|
48
48
|
/** Every scenario that sets the anchor pushes to this one throwaway repository. */
|
|
49
49
|
export const SANDBOX_ANCHOR_REPO = 'canon-sandbox'
|
|
50
50
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
export type ScenarioKind = 'ts' | 'sh'
|
|
51
|
+
const EXTENSION = '.ts'
|
|
54
52
|
|
|
55
53
|
export type ArmSetup = (ctx: StageContext) => void | Promise<void>
|
|
56
54
|
|
|
@@ -60,12 +58,12 @@ export interface ScenarioDefinition {
|
|
|
60
58
|
/** Stages the tree from the anchor fixture and names the throwaway remote. */
|
|
61
59
|
readonly anchor?: boolean
|
|
62
60
|
/**
|
|
63
|
-
* Stages and narrates
|
|
64
|
-
*
|
|
61
|
+
* Stages and narrates ahead of the picker, so the picker's echo lands after
|
|
62
|
+
* it on the frame. Runs before the arm is
|
|
65
63
|
* routed, which leaves `ctx.arm` undefined.
|
|
66
64
|
*/
|
|
67
65
|
readonly prepare?: ArmSetup
|
|
68
|
-
/** The picker's message, for a
|
|
66
|
+
/** The picker's message, for a scenario that asks something other than the default. */
|
|
69
67
|
readonly prompt?: string
|
|
70
68
|
/** In routing order, so the first arm is the one a headless caller gets. */
|
|
71
69
|
readonly arms: Readonly<Record<string, ArmSetup>>
|
|
@@ -78,43 +76,27 @@ export function scenario(definition: ScenarioDefinition): ScenarioDefinition {
|
|
|
78
76
|
export interface ScenarioFile {
|
|
79
77
|
readonly command: string
|
|
80
78
|
readonly file: string
|
|
81
|
-
readonly kind: ScenarioKind
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
/**
|
|
85
|
-
* Lists the scenarios in one category folder, one per stem. A
|
|
86
|
-
*
|
|
87
|
-
*
|
|
82
|
+
* Lists the scenarios in one category folder, one per `.ts` stem. A file in any
|
|
83
|
+
* other form is ignored rather than refused, so a hand-written leftover does not
|
|
84
|
+
* break the listing.
|
|
88
85
|
*/
|
|
89
86
|
export function listScenarioFiles(categoryDir: string): {
|
|
90
87
|
scenarios: ScenarioFile[]
|
|
91
|
-
ambiguous: string[]
|
|
92
88
|
} {
|
|
93
|
-
const
|
|
89
|
+
const scenarios: ScenarioFile[] = []
|
|
94
90
|
for (const entry of readdirSync(categoryDir, { withFileTypes: true })) {
|
|
95
|
-
if (!entry.isFile()) continue
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const command = entry.name.slice(0, -kind.length)
|
|
99
|
-
const found = byStem.get(command) ?? []
|
|
100
|
-
found.push({
|
|
101
|
-
command,
|
|
91
|
+
if (!entry.isFile() || !entry.name.endsWith(EXTENSION)) continue
|
|
92
|
+
scenarios.push({
|
|
93
|
+
command: entry.name.slice(0, -EXTENSION.length),
|
|
102
94
|
file: join(categoryDir, entry.name),
|
|
103
|
-
kind: kind === '.ts' ? 'ts' : 'sh',
|
|
104
95
|
})
|
|
105
|
-
byStem.set(command, found)
|
|
106
96
|
}
|
|
97
|
+
scenarios.sort((a, b) => a.command.localeCompare(b.command))
|
|
107
98
|
|
|
108
|
-
|
|
109
|
-
const ambiguous: string[] = []
|
|
110
|
-
for (const [command, found] of [...byStem].sort(([a], [b]) =>
|
|
111
|
-
a.localeCompare(b),
|
|
112
|
-
)) {
|
|
113
|
-
if (found.length > 1) ambiguous.push(command)
|
|
114
|
-
else if (found[0] !== undefined) scenarios.push(found[0])
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { scenarios, ambiguous }
|
|
99
|
+
return { scenarios }
|
|
118
100
|
}
|
|
119
101
|
|
|
120
102
|
/** Every category folder under `sandbox/`, fixtures excluded. */
|
|
@@ -129,21 +111,18 @@ export function listCategories(sandboxDir: string): string[] {
|
|
|
129
111
|
|
|
130
112
|
export type ResolvedScenario =
|
|
131
113
|
| ({ readonly ok: true } & ScenarioFile)
|
|
132
|
-
| { readonly ok: false; readonly reason: 'missing'
|
|
114
|
+
| { readonly ok: false; readonly reason: 'missing' }
|
|
133
115
|
|
|
134
116
|
export function resolveScenarioFile(
|
|
135
117
|
sandboxDir: string,
|
|
136
118
|
category: string,
|
|
137
119
|
command: string,
|
|
138
120
|
): ResolvedScenario {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (file === undefined) return { ok: false, reason: 'missing' }
|
|
145
|
-
|
|
146
|
-
return { ok: true, command, file, kind: file.endsWith('.ts') ? 'ts' : 'sh' }
|
|
121
|
+
const file = join(sandboxDir, category, `${command}${EXTENSION}`)
|
|
122
|
+
if (!existsSync(file) || !statSync(file).isFile())
|
|
123
|
+
return { ok: false, reason: 'missing' }
|
|
124
|
+
|
|
125
|
+
return { ok: true, command, file }
|
|
147
126
|
}
|
|
148
127
|
|
|
149
128
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -190,7 +169,7 @@ export function armNames(definition: ScenarioDefinition): string[] {
|
|
|
190
169
|
return names.length === 1 && names[0] === DEFAULT_ARM_NAME ? [] : names
|
|
191
170
|
}
|
|
192
171
|
|
|
193
|
-
/**
|
|
172
|
+
/** Derives the organization from the toolkit's own remote unless `GITHUB_ORG` names one. */
|
|
194
173
|
function deriveGithubOrg(root: string, env: NodeJS.ProcessEnv): string {
|
|
195
174
|
if ((env.GITHUB_ORG ?? '') !== '') return env.GITHUB_ORG ?? ''
|
|
196
175
|
|
|
@@ -209,8 +188,8 @@ function deriveGithubOrg(root: string, env: NodeJS.ProcessEnv): string {
|
|
|
209
188
|
}
|
|
210
189
|
|
|
211
190
|
/**
|
|
212
|
-
* What
|
|
213
|
-
*
|
|
191
|
+
* What provisioning reads off a scenario: its config, the derived organization,
|
|
192
|
+
* and the anchor's repository name. These land in the one
|
|
214
193
|
* environment provisioning passes to every child, the seed and gov installs
|
|
215
194
|
* included.
|
|
216
195
|
*/
|
|
@@ -447,8 +426,7 @@ export function createStageContext(options: ContextOptions): StageContext {
|
|
|
447
426
|
|
|
448
427
|
/**
|
|
449
428
|
* `gh` answers an absent repository and an unreachable host with one status,
|
|
450
|
-
* so the 404 alone separates them. Creating is the opt-in
|
|
451
|
-
* `ensure_sandbox_anchor_repo` in `scripts/lib/sandbox-git.sh` records.
|
|
429
|
+
* so the 404 alone separates them. Creating is the opt-in.
|
|
452
430
|
*/
|
|
453
431
|
const ensureAnchorRepo = (org: string, repo: string): void => {
|
|
454
432
|
const probe = spawn(
|
|
@@ -503,7 +481,7 @@ export function createStageContext(options: ContextOptions): StageContext {
|
|
|
503
481
|
git('remote', 'add', 'origin', `https://github.com/${org}/${repo}.git`)
|
|
504
482
|
}
|
|
505
483
|
|
|
506
|
-
/**
|
|
484
|
+
/** Copies a fixture folder into the tree. */
|
|
507
485
|
const fixtures = (
|
|
508
486
|
category: string,
|
|
509
487
|
command: string,
|
|
@@ -602,7 +580,7 @@ export function createStageContext(options: ContextOptions): StageContext {
|
|
|
602
580
|
}
|
|
603
581
|
|
|
604
582
|
/**
|
|
605
|
-
* Picks the arm
|
|
583
|
+
* Picks the arm: a routed name first,
|
|
606
584
|
* echoed on the frame, then the first arm for a headless caller, then a prompt.
|
|
607
585
|
*/
|
|
608
586
|
async function routeArm(
|
package/src/sandbox/tree.ts
CHANGED
|
@@ -8,11 +8,9 @@ import { join } from 'node:path'
|
|
|
8
8
|
* in `CANON_SANDBOX_RUN_ID` for the rest of this process, so a script that
|
|
9
9
|
* spawns a child inheriting `process.env` — `canon sandbox run` calling
|
|
10
10
|
* `canon sandbox` to provision and then `canon sandbox check` — resolves the same tree in every one
|
|
11
|
-
* of them, as do the
|
|
11
|
+
* of them, as do the installer children provisioning spawns. A process
|
|
12
12
|
* that already carries the variable, inherited from such a parent, reuses it
|
|
13
13
|
* rather than minting a new one.
|
|
14
|
-
*
|
|
15
|
-
* Twin of `mint_sandbox_run_id` in `scripts/lib/sandbox-path.sh`.
|
|
16
14
|
*/
|
|
17
15
|
export function mintSandboxRunId(): string {
|
|
18
16
|
const existing = process.env.CANON_SANDBOX_RUN_ID
|
|
@@ -28,9 +26,7 @@ export function mintSandboxRunId(): string {
|
|
|
28
26
|
* per-run default is unit-testable on its own rather than only through the
|
|
29
27
|
* command's registration.
|
|
30
28
|
*
|
|
31
|
-
*
|
|
32
|
-
* boundary rules out a shared constant, so a change to the default lands on
|
|
33
|
-
* both sides. The fall-through used to be one path per machine, so two
|
|
29
|
+
* The fall-through used to be one path per machine, so two
|
|
34
30
|
* sessions each resolving the default at once provisioned over each other
|
|
35
31
|
* with neither told; `mintSandboxRunId` gives the path a per-run component
|
|
36
32
|
* instead, which is what makes two such sessions land on two different trees.
|
package/src/standards/read.ts
CHANGED
|
@@ -60,8 +60,21 @@ export function standardRoots(root: string): StandardRoot[] {
|
|
|
60
60
|
*/
|
|
61
61
|
const MOVED_STANDARDS: Readonly<Record<string, string>> = {
|
|
62
62
|
board: 'claude/skills/task-board/references/board.md',
|
|
63
|
+
context: 'claude/skills/context-fold/references/context.md',
|
|
64
|
+
design: 'claude/skills/canvas-board/references/design.md',
|
|
65
|
+
docs: 'claude/skills/docs-sync/references/docs.md',
|
|
66
|
+
figures: 'claude/skills/draft-figure/references/figures.md',
|
|
67
|
+
readme: 'claude/skills/docs-sync/references/readme.md',
|
|
68
|
+
rule: 'claude/skills/create-rule/references/rule.md',
|
|
69
|
+
skill: 'claude/skills/create-skill/references/skill.md',
|
|
70
|
+
branch: 'claude/skills/git-branch/references/branch.md',
|
|
71
|
+
commit: 'claude/skills/git-commit/references/commit.md',
|
|
72
|
+
pr: 'claude/skills/git-pr/references/pr.md',
|
|
73
|
+
publish: 'claude/skills/git-pr/references/publish.md',
|
|
74
|
+
slug: 'claude/skills/git-branch/references/slug.md',
|
|
75
|
+
versioning: 'claude/skills/git-commit/references/versioning.md',
|
|
63
76
|
markdown: 'claude/skills/markdown-craft/references/markdown.md',
|
|
64
|
-
canvas: 'claude/skills/canvas/references/canvas.md',
|
|
77
|
+
canvas: 'claude/skills/canvas-board/references/canvas.md',
|
|
65
78
|
glossary: 'claude/skills/teach-workspace/references/glossary.md',
|
|
66
79
|
groundwork: 'claude/skills/plan-groundwork/references/groundwork.md',
|
|
67
80
|
'groundwork-spikes':
|
package/src/sync/git.ts
CHANGED
package/src/tasks/archive.ts
CHANGED
|
@@ -578,7 +578,7 @@ function isBulletFor(line: string, target: string): boolean {
|
|
|
578
578
|
|
|
579
579
|
/**
|
|
580
580
|
* Resolves the `Plan:` target against the board and against the project root
|
|
581
|
-
* both, which is how `
|
|
581
|
+
* both, which is how `task-complete` reads the same line. It accepts `../plans/x.md`
|
|
582
582
|
* and `.canon/plans/x.md` as one file, so a gate reading only the first form
|
|
583
583
|
* would pass the second and strand the plan this exists to protect.
|
|
584
584
|
*
|
|
@@ -633,7 +633,7 @@ function atOneRoot(path: string, plans: string[], root: string): string {
|
|
|
633
633
|
/**
|
|
634
634
|
* Names the other live tasks whose `Plan:` line lands on the same file through
|
|
635
635
|
* any of its links. This is
|
|
636
|
-
* the rule `
|
|
636
|
+
* the rule the `post-merge` hook applies before it archives a plan, held here so one
|
|
637
637
|
* question has one implementation: a plan another live task still cites is a
|
|
638
638
|
* plan the sweep is correct to leave, and a guard that read the folder instead
|
|
639
639
|
* refused every task sharing one plan and deadlocked the board against the
|
|
@@ -697,7 +697,7 @@ export type CitationOutcome = PlanCitations | ArchiveRefused
|
|
|
697
697
|
|
|
698
698
|
/**
|
|
699
699
|
* Answers where one task's plan sits and who else holds it, which is the whole
|
|
700
|
-
* of the last-live-citation rule.
|
|
700
|
+
* of the last-live-citation rule. The archive reads this rather than scanning
|
|
701
701
|
* the board itself, so the sweep that moves a plan and the gate that refuses a
|
|
702
702
|
* task archive cannot drift into disagreeing about which plan is free.
|
|
703
703
|
*
|
|
@@ -897,7 +897,7 @@ async function resolveStem(
|
|
|
897
897
|
|
|
898
898
|
/**
|
|
899
899
|
* Archives one task as a single unit: the move, the ordering-row removal, and
|
|
900
|
-
* the index regen. The hook and `task-
|
|
900
|
+
* the index regen. The hook and `task-complete` both call this, so every gate
|
|
901
901
|
* refuses rather than reports. A caller with nobody watching cannot act on a
|
|
902
902
|
* warning, and two callers gating differently is the drift this exists to stop.
|
|
903
903
|
*/
|
|
@@ -918,7 +918,7 @@ export async function archiveTask(
|
|
|
918
918
|
const from = join(dir, `${stem}.md`)
|
|
919
919
|
const text = await readFile(from, 'utf8')
|
|
920
920
|
|
|
921
|
-
// `
|
|
921
|
+
// `task-complete` ticks outcomes at ship time, before the merge, so an earlier
|
|
922
922
|
// slice merging after the last slice shipped would find every box ticked
|
|
923
923
|
// while the last slice is still open. Only the last number closes the task.
|
|
924
924
|
const last = readPullRequest(text).at(-1)
|
package/src/tasks/branch.ts
CHANGED
|
@@ -14,10 +14,10 @@ const MARKDOWN = '.md'
|
|
|
14
14
|
*/
|
|
15
15
|
export const PLAN_BRANCH_TYPE = 'feat'
|
|
16
16
|
|
|
17
|
-
/** The description cap in `
|
|
17
|
+
/** The description cap in `claude/skills/git-branch/references/branch.md`, in kebab-separated words. */
|
|
18
18
|
export const DESCRIPTION_WORD_CAP = 4
|
|
19
19
|
|
|
20
|
-
/** The branch length cap in `
|
|
20
|
+
/** The branch length cap in `claude/skills/git-branch/references/branch.md`, in characters. */
|
|
21
21
|
export const BRANCH_LENGTH_CAP = 50
|
|
22
22
|
|
|
23
23
|
export interface PlanBranch {
|
|
@@ -48,7 +48,7 @@ function slugOf(path: string): string {
|
|
|
48
48
|
* of them name. It is the one derivation, so the collision check and the
|
|
49
49
|
* worktree entry it gates cannot hold two answers for one plan.
|
|
50
50
|
*
|
|
51
|
-
* Conformance covers both caps `
|
|
51
|
+
* Conformance covers both caps `claude/skills/git-branch/references/branch.md` states, being the word
|
|
52
52
|
* count of the description and the length of the whole branch. A slug is a
|
|
53
53
|
* plan's own filename rather than a name anyone chose for a branch, so a plan
|
|
54
54
|
* can name a branch this refuses to grade as conforming, and reporting that is
|
package/src/tasks/label.ts
CHANGED
|
@@ -185,7 +185,7 @@ async function reserveLabel(root: string, label: string): Promise<boolean> {
|
|
|
185
185
|
* the task file after this returns, so a reservation with no file behind it is
|
|
186
186
|
* the ordinary state for a few seconds, and a scan blind to it would offer the
|
|
187
187
|
* same label to every retry. Reservations are never expired: a claim that died
|
|
188
|
-
* leaves a gap, and `
|
|
188
|
+
* leaves a gap, and `claude/skills/git-commit/references/versioning.md` permits gaps.
|
|
189
189
|
*/
|
|
190
190
|
export async function claimLabel(
|
|
191
191
|
root: string,
|
package/src/tasks/record.ts
CHANGED
|
@@ -420,7 +420,7 @@ async function openTask(
|
|
|
420
420
|
* resolve the board root in-process.
|
|
421
421
|
*
|
|
422
422
|
* `branches` names the recording branch and every name it carried before a
|
|
423
|
-
* rename, since `git-ship` renames a branch after `
|
|
423
|
+
* rename, since `git-ship` renames a branch after `task-complete` has already
|
|
424
424
|
* written the marker under the old name. Each comes off the `Pending branch:`
|
|
425
425
|
* line even when the number was already listed, so a rerun repairs a stale
|
|
426
426
|
* marker.
|
|
@@ -504,7 +504,7 @@ export async function recordPlan(
|
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
/**
|
|
507
|
-
* Marks the named outcomes `[x]` in place. `
|
|
507
|
+
* Marks the named outcomes `[x]` in place. `task-complete` runs this against a
|
|
508
508
|
* board it can read and cannot edit from a linked worktree, and the positions
|
|
509
509
|
* come from the read it already made.
|
|
510
510
|
*
|
package/src/tasks/validate.ts
CHANGED
|
@@ -597,7 +597,7 @@ async function listTaskStems(dir: string): Promise<string[]> {
|
|
|
597
597
|
|
|
598
598
|
/**
|
|
599
599
|
* Resolves a pointer against the board and against the project root both, the
|
|
600
|
-
* way `
|
|
600
|
+
* way `task-complete` reads the same line. A fragment is dropped first, since an
|
|
601
601
|
* anchor is part of the link and never part of the path.
|
|
602
602
|
*/
|
|
603
603
|
function resolves(target: string, dir: string, root: string): boolean {
|
package/standards/index.md
CHANGED
|
@@ -7,17 +7,4 @@ subtitle: Reference docs for consistent authoring across the toolkit and target
|
|
|
7
7
|
|
|
8
8
|
Reference docs for consistent authoring across the toolkit and target projects.
|
|
9
9
|
|
|
10
|
-
- [Branch reference](branch.md): Branch naming format and type conventions
|
|
11
|
-
- [Commit reference](commit.md): Commit message format and type conventions
|
|
12
|
-
- [Context entry reference](context.md): Shape and content rules for canon/context/<domain>.md entries
|
|
13
|
-
- [Design reference](design.md): Shape and content rules for canon/DESIGN.md
|
|
14
|
-
- [Docs reference](docs.md): Reader and jurisdiction, frontmatter, page structure, what a page links out to, the diagram permission, and when a category earns a subfolder
|
|
15
|
-
- [Figure reference](figures.md): When a figure earns its place, the render-first policy for a graph-shaped subject, freehand SVG as the escape hatch, and its wrapping, color, and accessibility rules
|
|
16
|
-
- [Pull request reference](pr.md): Pull request title and body conventions
|
|
17
|
-
- [Publish reference](publish.md): Scan an author runs against finished text, the cross-reference form each destination takes, and the response to an unreadable source
|
|
18
|
-
- [Readme reference](readme.md): Readme voice, structure, and content conventions
|
|
19
|
-
- [Governance rule reference](rule.md): Rule frontmatter, body shape, and voice for .claude/rules files
|
|
20
|
-
- [Claude skill reference](skill.md): Skill structure and authoring rules for Claude Code
|
|
21
|
-
- [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
|
|
22
10
|
- [Standard reference](standard.md): Shape and content rules for authoring a standard
|
|
23
|
-
- [Versioning reference](versioning.md): Phase label vs semver discipline across tasks, PRs, reviews, issues, commits, and tags
|
package/standards/standard.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Standard reference
|
|
3
3
|
description: Shape and content rules for authoring a standard
|
|
4
|
-
paths:
|
|
5
|
-
- 'standards/**/*.md'
|
|
6
|
-
rule:
|
|
7
|
-
- 'Read it before writing or editing a standard. Do not work the shape from memory.'
|
|
8
4
|
---
|
|
9
5
|
|
|
10
6
|
# Standard reference
|
|
@@ -68,9 +64,7 @@ A standard failing these questions is non-conforming even when it satisfies ever
|
|
|
68
64
|
|
|
69
65
|
- Start the file with a frontmatter block carrying at least `title` and `description`. A consuming surface may require more.
|
|
70
66
|
- `title`: names the doc type in sentence case, suffixed `reference` (`Commit reference`, `Branch reference`)
|
|
71
|
-
- `description`: one line naming what the standard covers. It becomes the index link label on install
|
|
72
|
-
- `paths` (optional): the globs where the standard applies, one per entry, copied verbatim into the rule `canon gov regen` generates for it. Omit it when no path should route to the standard, and no rule is generated.
|
|
73
|
-
- `rule` (optional): operative bullets the generated rule carries after its pointer bullet, one per entry, each a full sentence. Quote an entry holding a backtick, a colon, or a leading `-`.
|
|
67
|
+
- `description`: one line naming what the standard covers. It becomes the index link label on install.
|
|
74
68
|
|
|
75
69
|
## Structure
|
|
76
70
|
|
|
@@ -29,7 +29,7 @@ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and n
|
|
|
29
29
|
- `.lintstagedrc`: the glob map below.
|
|
30
30
|
- `.prettierignore`: created empty. Projects add their own entries.
|
|
31
31
|
- `.github/workflows/verify.yml`: runs on pull requests targeting `main` and on `workflow_dispatch`. A project with more than one language root adds its own jobs here.
|
|
32
|
-
- `.github/pull_request_template.md`: `## Summary`, `## Key
|
|
32
|
+
- `.github/pull_request_template.md`: `## Summary`, `## Key changes`, `## Technical context`, `## Testing`. A project adds the checklist lines its own review needs.
|
|
33
33
|
- `canon/context/`: extend the `ci` and `development` entries with project-specific commands, workflows, or deploy steps. Canonical rationale stays in this reference.
|
|
34
34
|
|
|
35
35
|
## Tool pairing
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<Action Verb> <Direct Object> to <Result>.
|
|
4
4
|
|
|
5
|
-
## Key
|
|
5
|
+
## Key changes
|
|
6
6
|
|
|
7
7
|
- <Verb> <specific component/file/function> (<reason if non-obvious>)
|
|
8
8
|
- <Verb> <specific component/file/function>
|
|
9
9
|
|
|
10
|
-
<!-- ## Technical
|
|
10
|
+
<!-- ## Technical context (optional, omit for docs/config/trivial changes) -->
|
|
11
11
|
<!-- - <Why this approach was taken, not what changed> -->
|
|
12
12
|
|
|
13
13
|
## Testing
|
|
@@ -21,7 +21,7 @@ canon/
|
|
|
21
21
|
|
|
22
22
|
.canon/
|
|
23
23
|
├── tasks/ ← seeded. One file per task plus a generated index.md, local scratch only. `archive/` holds the tasks that shipped.
|
|
24
|
-
├── plans/ ← execution detail for multi-step tasks. `feature-*.md` entries
|
|
24
|
+
├── plans/ ← execution detail for multi-step tasks. `feature-*.md` entries moved into `archive/` by `canon tasks archive` when the `post-merge` hook archives their task.
|
|
25
25
|
├── review/ ← one subfolder per producer. `branch/` no longer written, `design/` for canon design render.
|
|
26
26
|
└── tmp/ ← ephemeral scratch space, safe to delete without loss
|
|
27
27
|
```
|
|
@@ -24,8 +24,9 @@ IFS= read -r -d '' -t 2 input
|
|
|
24
24
|
# its edit path prints a URL as well and is not an opening.
|
|
25
25
|
command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
|
|
26
26
|
stdout=$(printf '%s' "$input" | jq -r '.tool_response.stdout // empty')
|
|
27
|
+
is_hand_opened=false
|
|
27
28
|
case "$command" in
|
|
28
|
-
*"gh pr create"*) ;;
|
|
29
|
+
*"gh pr create"*) is_hand_opened=true ;;
|
|
29
30
|
*)
|
|
30
31
|
printf '%s\n' "$stdout" | grep -qx 'opened=true' || exit 0
|
|
31
32
|
;;
|
|
@@ -57,4 +58,11 @@ session=$(printf '%s' "$input" | jq -r '.session_id // "unknown"')
|
|
|
57
58
|
printf -- '- %s session=%s pr=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$session" "$url" >>"$log_dir/log.md"
|
|
58
59
|
|
|
59
60
|
msg="Pull request $url just opened. If this session holds a worker's channel obligation, announce it to the controller now, per role-worker."
|
|
61
|
+
# A pull request opened by hand skipped git-pr, so nothing has labelled it.
|
|
62
|
+
# The verb is named only where the installed canon carries it, since a skill
|
|
63
|
+
# can reach a target ahead of the release that ships the verb.
|
|
64
|
+
if [ "$is_hand_opened" = true ] && canon labels apply --help >/dev/null 2>&1; then
|
|
65
|
+
msg="$msg Run canon labels apply ${url##*/} to label it."
|
|
66
|
+
fi
|
|
67
|
+
|
|
60
68
|
jq -nc --arg msg "$msg" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
4
|
+
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
5
|
+
# rather than `timeout cat`, which macOS does not ship.
|
|
6
|
+
IFS= read -r -d '' -t 2 input
|
|
7
|
+
[ -n "$input" ] || {
|
|
8
|
+
printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Every Bash call reaches this line, so the command is read in one jq pass and
|
|
13
|
+
# matched with a shell pattern before anything else costs a process.
|
|
14
|
+
fields=$(printf '%s' "$input" | jq -r 'if .tool_name == "Bash" then [(.cwd // ""), (.tool_input.command // "")] | @json else empty end')
|
|
15
|
+
[ -n "$fields" ] || exit 0
|
|
16
|
+
|
|
17
|
+
cmd=$(printf '%s' "$fields" | jq -r '.[1]')
|
|
18
|
+
case "$cmd" in
|
|
19
|
+
*'gh api'*) ;;
|
|
20
|
+
*) exit 0 ;;
|
|
21
|
+
esac
|
|
22
|
+
|
|
23
|
+
# A command that only mentions a post is not one: a heredoc writing a review
|
|
24
|
+
# body that quotes the command, or a search whose pattern holds it. The string
|
|
25
|
+
# is cut into the commands it runs, with heredoc bodies dropped and quoted text
|
|
26
|
+
# left whole, and only a command that starts with `gh api` is read further.
|
|
27
|
+
# Splitting on the separators outside quotes is what keeps `echo 'a; gh api'`
|
|
28
|
+
# one command rather than two.
|
|
29
|
+
IFS= read -r -d '' splitter <<'AWK'
|
|
30
|
+
{ s = s (NR > 1 ? "\n" : "") $0 }
|
|
31
|
+
function emit() {
|
|
32
|
+
gsub(/\n/, " ", seg)
|
|
33
|
+
sub(/^[ \t]+/, "", seg)
|
|
34
|
+
if (seg != "") print seg
|
|
35
|
+
seg = ""
|
|
36
|
+
}
|
|
37
|
+
END {
|
|
38
|
+
n = length(s); q = ""; seg = ""; nt = 0; i = 1
|
|
39
|
+
while (i <= n) {
|
|
40
|
+
c = substr(s, i, 1)
|
|
41
|
+
if (q == "'") { seg = seg c; if (c == "'") q = ""; i++; continue }
|
|
42
|
+
if (q == "\"") {
|
|
43
|
+
if (c == "\\") { seg = seg c substr(s, i + 1, 1); i += 2; continue }
|
|
44
|
+
if (c == "\"") q = ""
|
|
45
|
+
seg = seg c; i++; continue
|
|
46
|
+
}
|
|
47
|
+
if (c == "\\") { seg = seg c substr(s, i + 1, 1); i += 2; continue }
|
|
48
|
+
if (c == "'" || c == "\"") { q = c; seg = seg c; i++; continue }
|
|
49
|
+
if (c == "#" && (seg == "" || substr(seg, length(seg), 1) ~ /[ \t]/)) {
|
|
50
|
+
while (i <= n && substr(s, i, 1) != "\n") i++
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
if (c == "<" && substr(s, i, 2) == "<<" && substr(s, i, 3) != "<<<") {
|
|
54
|
+
j = i + 2; dash = 0
|
|
55
|
+
if (substr(s, j, 1) == "-") { dash = 1; j++ }
|
|
56
|
+
while (substr(s, j, 1) ~ /[ \t]/) j++
|
|
57
|
+
oq = substr(s, j, 1)
|
|
58
|
+
if (oq == "'" || oq == "\"") j++
|
|
59
|
+
tag = ""
|
|
60
|
+
while (substr(s, j, 1) ~ /[A-Za-z0-9_]/) { tag = tag substr(s, j, 1); j++ }
|
|
61
|
+
if (oq == "'" || oq == "\"") j++
|
|
62
|
+
if (tag != "") { nt++; tags[nt] = tag; dashes[nt] = dash; i = j; continue }
|
|
63
|
+
}
|
|
64
|
+
if (c == "\n") {
|
|
65
|
+
emit(); i++
|
|
66
|
+
for (t = 1; t <= nt; t++) {
|
|
67
|
+
while (i <= n) {
|
|
68
|
+
e = index(substr(s, i), "\n")
|
|
69
|
+
if (e == 0) { line = substr(s, i); i = n + 1 } else { line = substr(s, i, e - 1); i += e }
|
|
70
|
+
if (dashes[t]) sub(/^\t+/, "", line)
|
|
71
|
+
if (line == tags[t]) break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
nt = 0; continue
|
|
75
|
+
}
|
|
76
|
+
if (c == ";" || c == "|" || c == "(" || c == ")" || c == "`" || c == "&") {
|
|
77
|
+
if (c == "&" && (substr(seg, length(seg), 1) ~ /[<>]/ || substr(s, i + 1, 1) == ">")) { seg = seg c; i++; continue }
|
|
78
|
+
emit(); i++; continue
|
|
79
|
+
}
|
|
80
|
+
seg = seg c; i++
|
|
81
|
+
}
|
|
82
|
+
emit()
|
|
83
|
+
}
|
|
84
|
+
AWK
|
|
85
|
+
|
|
86
|
+
# A block reaches the session through stderr, and stdout stays empty so nothing
|
|
87
|
+
# unframed lands in the hook protocol.
|
|
88
|
+
deny() {
|
|
89
|
+
printf '%s\n' "$1" >&2
|
|
90
|
+
exit 2
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# The thread a review or a comment lands on: a review post or edit, and an issue
|
|
94
|
+
# or pull request comment post or edit. A pull request's inline comments are left
|
|
95
|
+
# out, since no shipped skill posts one.
|
|
96
|
+
seg="[^/[:space:]'\"]+"
|
|
97
|
+
end="(['\" ?]|\$)"
|
|
98
|
+
endpoint="(pulls/$seg/reviews(/$seg)?|issues/$seg/comments|issues/comments/$seg)$end"
|
|
99
|
+
starts_gh_api="^([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*((command|exec)[[:space:]]+)?gh[[:space:]]+api([[:space:]]|\$)"
|
|
100
|
+
body_inline="[[:space:]](-f|-F|--field|--raw-field)[[:space:]=]*['\"]?body="
|
|
101
|
+
body_file="[[:space:]](-F|--field)[[:space:]=]*['\"]?body=@([^'\"[:space:]]+)"
|
|
102
|
+
|
|
103
|
+
# Reads one command already known to start with `gh api` and returns when it
|
|
104
|
+
# has nothing to scan or the scan came back clean.
|
|
105
|
+
check() {
|
|
106
|
+
local call=$1 method='' file='' cwd out code warnings
|
|
107
|
+
|
|
108
|
+
[[ $call =~ $endpoint ]] || return 0
|
|
109
|
+
|
|
110
|
+
# gh sends POST when a field flag is present and no method is named, and the
|
|
111
|
+
# explicit method wins when one is. A call naming neither is a read.
|
|
112
|
+
if [[ $call =~ (-X|--method)[[:space:]=]*([A-Za-z]+) ]]; then
|
|
113
|
+
method=$(printf '%s' "${BASH_REMATCH[2]}" | tr '[:lower:]' '[:upper:]')
|
|
114
|
+
elif [[ $call =~ [[:space:]](-f|-F|--field|--raw-field|--input)([[:space:]=]|$) ]]; then
|
|
115
|
+
method=POST
|
|
116
|
+
fi
|
|
117
|
+
case "$method" in
|
|
118
|
+
POST | PATCH | PUT) ;;
|
|
119
|
+
*) return 0 ;;
|
|
120
|
+
esac
|
|
121
|
+
|
|
122
|
+
if [[ $call =~ $body_file ]]; then
|
|
123
|
+
file=${BASH_REMATCH[2]}
|
|
124
|
+
elif [[ $call =~ $body_inline ]]; then
|
|
125
|
+
deny 'This review or comment post passes its body inline, so the label scan cannot read it. Write the body to a file in an earlier call and pass it as -F body=@<file>.'
|
|
126
|
+
else
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
[ "$file" != '-' ] || deny 'This post reads its body from stdin, so the label scan cannot read it. Write the body to a file in an earlier call and pass it as -F body=@<file>.'
|
|
131
|
+
|
|
132
|
+
if ! command -v canon >/dev/null 2>&1; then
|
|
133
|
+
jq -nc '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:"The label scan did not run on this review or comment post, since canon is not on PATH. Run canon labels scan --body-file on the body once it is."}}'
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# The payload's own cwd is the directory a relative path was written against.
|
|
138
|
+
# The project dir and this process's directory can each be a different worktree.
|
|
139
|
+
cwd=$(printf '%s' "$fields" | jq -r '.[0]')
|
|
140
|
+
[ -n "$cwd" ] || cwd=${CLAUDE_PROJECT_DIR:-.}
|
|
141
|
+
case "$file" in
|
|
142
|
+
/*) ;;
|
|
143
|
+
*) file="$cwd/$file" ;;
|
|
144
|
+
esac
|
|
145
|
+
|
|
146
|
+
# A body written by the same call does not exist when this hook runs, so the
|
|
147
|
+
# post would go out unread by the scan. Blocking is the only verdict that holds.
|
|
148
|
+
[ -f "$file" ] || deny "The body file $file does not exist yet, so the label scan cannot read it. Write the body in an earlier call, then post it."
|
|
149
|
+
|
|
150
|
+
# No --title: a review body carries none, and a title would grade it as an
|
|
151
|
+
# ordinary pull request body.
|
|
152
|
+
out=$(cd "$cwd" && canon labels scan --body-file "$file" 2>&1)
|
|
153
|
+
code=$?
|
|
154
|
+
[ "$code" -ne 0 ] || return 0
|
|
155
|
+
|
|
156
|
+
warnings=$(printf '%s\n' "$out" | grep -F '!')
|
|
157
|
+
[ -n "$warnings" ] || warnings=$out
|
|
158
|
+
if [ "$code" -eq 2 ]; then
|
|
159
|
+
deny "The label scan found text this post would publish. Fix the body file and post again.
|
|
160
|
+
$warnings"
|
|
161
|
+
fi
|
|
162
|
+
deny "The label scan could not grade $file (exit $code). Fix what it reports and post again.
|
|
163
|
+
$warnings"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
commands=$(printf '%s' "$cmd" | awk "$splitter")
|
|
167
|
+
while IFS= read -r line; do
|
|
168
|
+
[[ $line =~ $starts_gh_api ]] || continue
|
|
169
|
+
check "$line"
|
|
170
|
+
done <<<"$commands"
|
|
171
|
+
exit 0
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scratch-guard.sh"
|
|
28
28
|
}
|
|
29
29
|
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Bash",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/review-post-scan.sh"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
30
39
|
}
|
|
31
40
|
],
|
|
32
41
|
"PostToolUse": [
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
# Generated by `canon gov regen` from standards/context.md. Edit that frontmatter, not this file.
|
|
3
|
-
description: Shape and content rules for canon/context/<domain>.md entries
|
|
4
|
-
paths:
|
|
5
|
-
- 'canon/context/**'
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Context entry standards
|
|
9
|
-
|
|
10
|
-
## Authority
|
|
11
|
-
|
|
12
|
-
- Follow the context standard for shape and content rules for canon/context/<domain>.md entries. It is the single source. Read it with `canon standards context`.
|
|
13
|
-
- Before non-trivial work in a domain, read its `canon/context/<domain>.md` entry. Use `canon/context/index.md` to find it.
|
|
14
|
-
- Leave the entry conforming when work in its domain changes what it describes.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
# Generated by `canon gov regen` from standards/design.md. Edit that frontmatter, not this file.
|
|
3
|
-
description: Shape and content rules for canon/DESIGN.md
|
|
4
|
-
paths:
|
|
5
|
-
- 'canon/DESIGN.md'
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Design standards
|
|
9
|
-
|
|
10
|
-
## Authority
|
|
11
|
-
|
|
12
|
-
- Follow the design standard for shape and content rules for canon/DESIGN.md. It is the single source. Read it with `canon standards design`.
|