@erclx/canon 5.38.0 → 5.39.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/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +12 -9
- package/claude/skills/auto-ship/SKILL.md +43 -23
- package/claude/skills/backlog-triage/SKILL.md +62 -53
- package/claude/skills/backlog-triage/references/apply.md +69 -0
- package/claude/skills/build-in-slices/SKILL.md +8 -4
- package/claude/skills/canon-cli/SKILL.md +5 -1
- package/claude/skills/canon-feedback/SKILL.md +18 -8
- package/claude/skills/canon-feedback-triage/SKILL.md +10 -3
- package/claude/skills/canvas-board/SKILL.md +36 -17
- package/claude/skills/ci-workflow/SKILL.md +5 -4
- package/claude/skills/code-craft/SKILL.md +10 -6
- package/claude/skills/codebase-layout/SKILL.md +3 -5
- package/claude/skills/context-fold/REQUIREMENT.md +4 -4
- package/claude/skills/context-fold/SKILL.md +42 -23
- package/claude/skills/context-fold/references/context-refresh.md +4 -3
- package/claude/skills/context-fold/references/context.md +79 -64
- package/claude/skills/create-rule/SKILL.md +7 -3
- package/claude/skills/create-skill/SKILL.md +27 -7
- package/claude/skills/create-skill/references/skill-reference.md +41 -17
- package/claude/skills/decision-escalate/SKILL.md +8 -5
- package/claude/skills/deploy-app/SKILL.md +1 -1
- package/claude/skills/deprecation-migration/SKILL.md +12 -4
- package/claude/skills/design-taste/SKILL.md +17 -11
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -0
- package/claude/skills/docs-sync/SKILL.md +17 -6
- package/claude/skills/docs-sync/references/docs.md +54 -21
- package/claude/skills/docs-sync/references/readme.md +32 -8
- package/claude/skills/document-health/SKILL.md +3 -4
- package/claude/skills/draft-and-pick/SKILL.md +73 -47
- package/claude/skills/draft-doc/SKILL.md +9 -4
- package/claude/skills/draft-doc/references/context.md +4 -4
- package/claude/skills/draft-doc/references/docs.md +1 -0
- package/claude/skills/draft-doc/references/wiki.md +3 -2
- package/claude/skills/draft-figure/SKILL.md +9 -7
- package/claude/skills/draft-identity/SKILL.md +24 -52
- package/claude/skills/draft-identity/references/card-route.md +46 -0
- package/claude/skills/draft-ready/SKILL.md +19 -7
- package/claude/skills/draft-screencast/SKILL.md +6 -112
- package/claude/skills/draft-screencast/references/beats-template.md +119 -0
- package/claude/skills/draft-slides/SKILL.md +7 -4
- package/claude/skills/git-branch/SKILL.md +3 -3
- package/claude/skills/git-commit/SKILL.md +5 -3
- package/claude/skills/git-followup/SKILL.md +16 -9
- package/claude/skills/git-issue/SKILL.md +6 -5
- package/claude/skills/git-pr/SKILL.md +56 -44
- package/claude/skills/git-ship/SKILL.md +43 -34
- package/claude/skills/git-ship/references/ci-watch.md +31 -0
- package/claude/skills/git-split/SKILL.md +28 -100
- package/claude/skills/git-split/references/independent.md +40 -0
- package/claude/skills/git-split/references/stacked.md +73 -0
- package/claude/skills/git-stage/SKILL.md +9 -7
- package/claude/skills/git-worktree/SKILL.md +27 -49
- package/claude/skills/git-worktree/references/cleanup.md +55 -0
- package/claude/skills/markdown-craft/SKILL.md +2 -2
- package/claude/skills/markdown-craft/references/markdown.md +82 -53
- package/claude/skills/plan-feature/SKILL.md +52 -46
- package/claude/skills/plan-feature/references/batches.md +46 -0
- package/claude/skills/plan-groundwork/SKILL.md +60 -74
- package/claude/skills/plan-groundwork/references/output.md +48 -0
- package/claude/skills/plan-intake/SKILL.md +28 -21
- package/claude/skills/plan-intake-answer/SKILL.md +14 -13
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +31 -11
- package/claude/skills/review-address/SKILL.md +76 -158
- package/claude/skills/review-address/references/rebase-only.md +34 -0
- package/claude/skills/review-craft/SKILL.md +27 -11
- package/claude/skills/review-pr/SKILL.md +91 -40
- package/claude/skills/review-ui/SKILL.md +62 -37
- package/claude/skills/review-ui/references/hosted-preview.md +30 -0
- package/claude/skills/role-orchestrator/SKILL.md +105 -74
- package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
- package/claude/skills/role-planner/SKILL.md +66 -64
- package/claude/skills/role-reviewer/SKILL.md +57 -46
- package/claude/skills/role-worker/SKILL.md +68 -63
- package/claude/skills/search-craft/SKILL.md +7 -5
- package/claude/skills/session-compact/SKILL.md +16 -11
- package/claude/skills/session-map/SKILL.md +31 -10
- package/claude/skills/session-relay/SKILL.md +32 -65
- package/claude/skills/session-resume/SKILL.md +18 -13
- package/claude/skills/session-worktree/SKILL.md +70 -42
- package/claude/skills/systematic-debugging/SKILL.md +6 -1
- package/claude/skills/target-check/SKILL.md +13 -6
- package/claude/skills/target-setup/SKILL.md +39 -27
- package/claude/skills/task-board/SKILL.md +68 -60
- package/claude/skills/task-board/references/create-report.md +36 -0
- package/claude/skills/task-complete/SKILL.md +20 -18
- package/claude/skills/teach-workspace/SKILL.md +61 -33
- package/claude/skills/test-craft/SKILL.md +2 -1
- package/claude/skills/test-first/SKILL.md +15 -9
- package/claude/skills/ui-checklist/SKILL.md +18 -16
- package/claude/skills/ux-audit/SKILL.md +9 -10
- package/claude/skills/ux-measure/SKILL.md +22 -18
- package/claude/skills/video-craft/SKILL.md +15 -7
- package/claude/skills/write-human/SKILL.md +9 -1
- package/claude/skills/youtube-transcripts/SKILL.md +2 -1
- package/docs/agents/context-audit-checks.md +19 -1
- package/docs/agents/context-audit.md +2 -2
- package/docs/agents/driver.md +1 -1
- package/docs/agents/hooks.md +21 -1
- package/docs/agents/index.md +10 -14
- package/docs/agents/indexes.md +25 -29
- package/docs/agents/install-and-sync.md +124 -235
- package/docs/agents/markdown-audit-density.md +39 -0
- package/docs/agents/markdown-audit-weight.md +9 -7
- package/docs/agents/markdown-audit.md +18 -14
- package/docs/agents/migrate.md +41 -16
- package/docs/agents/output-shape.md +60 -45
- package/docs/agents/overview.md +36 -12
- package/docs/agents/routing.md +1 -1
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/skills-reach.md +1 -1
- package/docs/agents/targets.md +25 -77
- package/docs/target/migrations.md +1 -1
- package/docs/target/projects.md +2 -2
- package/docs/workflow/ai-workflow.md +2 -2
- package/docs/workflow/parallel-features.md +2 -2
- package/docs/workflow/skill-map.md +1 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/package.json +1 -1
- package/src/commands/claude.ts +1 -1
- package/src/commands/context.ts +106 -7
- package/src/commands/hooks.ts +63 -0
- package/src/commands/markdown.ts +169 -32
- package/src/context/audit.ts +256 -5
- package/src/context/classify/prompts.ts +1 -1
- package/src/context/gate.ts +23 -7
- package/src/context/shape-gated.ts +28 -0
- package/src/design/document.ts +1 -1
- package/src/design/tokens.ts +1 -1
- package/src/gate/measures.ts +15 -190
- package/src/gate/stages.ts +7 -23
- package/src/hooks/reply-links.ts +110 -0
- package/src/markdown/density.ts +258 -0
- package/src/markdown/gate.ts +57 -18
- package/src/markdown/reference-shape.ts +103 -0
- package/src/markdown/scope.ts +174 -0
- package/src/markdown/structure.ts +9 -59
- package/src/shipped/references.ts +1 -1
- package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
- package/tooling/claude/seeds/.claude/settings.json +4 -0
- package/docs/agents/commands.md +0 -154
- package/docs/agents/docs.md +0 -27
- package/docs/agents/drift-surfaces.md +0 -201
- package/docs/agents/gov-detect.md +0 -47
- package/docs/agents/scripting.md +0 -142
- package/src/markdown/growth.ts +0 -43
package/src/markdown/gate.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { ColonFinding } from '@/markdown/colons'
|
|
2
|
+
import type { DensityFinding } from '@/markdown/density'
|
|
2
3
|
import type { HeadingCaseFinding } from '@/markdown/heading-case'
|
|
3
4
|
import type { LinkFinding } from '@/markdown/links'
|
|
5
|
+
import type { ReferenceShapeFinding } from '@/markdown/reference-shape'
|
|
4
6
|
import type { BanFinding } from '@/markdown/scan'
|
|
5
|
-
import
|
|
7
|
+
import { GATED_PATHS } from '@/markdown/scope'
|
|
6
8
|
import type { StructureReport } from '@/markdown/structure'
|
|
7
9
|
|
|
10
|
+
export interface GatedDensityFinding extends DensityFinding {
|
|
11
|
+
readonly gating: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GatedReferenceShapeFinding extends ReferenceShapeFinding {
|
|
15
|
+
readonly gating: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export interface GateInput {
|
|
9
19
|
/** Every ban hit across every file measured, flattened. */
|
|
10
20
|
readonly bans: readonly BanFinding[]
|
|
@@ -13,10 +23,11 @@ export interface GateInput {
|
|
|
13
23
|
/**
|
|
14
24
|
* Every structural measure the run made, read by nothing here.
|
|
15
25
|
*
|
|
16
|
-
* Naming it is what makes the split checkable:
|
|
17
|
-
*
|
|
18
|
-
* contributor to route around the stage.
|
|
19
|
-
*
|
|
26
|
+
* Naming it is what makes the split checkable: depth weight, cadence, and the
|
|
27
|
+
* sentence count are judgments a reader settles, and a push failing on one
|
|
28
|
+
* teaches a contributor to route around the stage. The bullet and paragraph
|
|
29
|
+
* weight a reader settles here are the advisory half, and the same numbers
|
|
30
|
+
* gate through `density` below, which knows the path each one sits on.
|
|
20
31
|
*/
|
|
21
32
|
readonly structure: readonly StructureReport[]
|
|
22
33
|
/**
|
|
@@ -33,25 +44,53 @@ export interface GateInput {
|
|
|
33
44
|
*/
|
|
34
45
|
readonly headingCase: readonly HeadingCaseFinding[]
|
|
35
46
|
/**
|
|
36
|
-
* Every
|
|
47
|
+
* Every density finding, each marked with whether its path gates.
|
|
37
48
|
*
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
40
|
-
* groundwork
|
|
41
|
-
*
|
|
49
|
+
* The mark is decided by the caller from `src/markdown/scope.ts`, since only
|
|
50
|
+
* the caller knows the path and whether the tree is a canon checkout. A plan,
|
|
51
|
+
* a groundwork record, and a rule carry no density finding at all, and an
|
|
52
|
+
* in-scope path under no gated prefix carries one marked `gating: false`.
|
|
53
|
+
*/
|
|
54
|
+
readonly density: readonly GatedDensityFinding[]
|
|
55
|
+
/**
|
|
56
|
+
* Every reference shape finding, each marked with whether its path gates.
|
|
57
|
+
* Only a page whose frontmatter carries `kind: reference` has any.
|
|
42
58
|
*/
|
|
43
|
-
readonly
|
|
59
|
+
readonly referenceShape: readonly GatedReferenceShapeFinding[]
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
/**
|
|
47
63
|
* Whether the audit found something that should fail the caller.
|
|
48
64
|
*
|
|
49
|
-
* A banned character and a relative link resolving to
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
65
|
+
* A banned character and a relative link resolving to nothing on disk are both
|
|
66
|
+
* facts rather than a threshold, so either gates unconditionally and there is
|
|
67
|
+
* no widened mode to reach for. A density finding is a fact too, a count
|
|
68
|
+
* against a fixed number, and it gates wherever the caller marked its path as
|
|
69
|
+
* gated, and so does a reference shape finding. The standards decide what
|
|
70
|
+
* counts as banned, which keeps this answering how many rather than which.
|
|
71
|
+
*/
|
|
72
|
+
export function isGating({
|
|
73
|
+
bans,
|
|
74
|
+
links,
|
|
75
|
+
density,
|
|
76
|
+
referenceShape,
|
|
77
|
+
}: GateInput): boolean {
|
|
78
|
+
return (
|
|
79
|
+
bans.length > 0 ||
|
|
80
|
+
links.length > 0 ||
|
|
81
|
+
density.some((finding) => finding.gating) ||
|
|
82
|
+
referenceShape.some((finding) => finding.gating)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a reference shape finding on this path fails the run, under the
|
|
88
|
+
* prefixes density gates on. A page is opted in by its own marker, so the
|
|
89
|
+
* density scope and the always-gated bullet run have nothing to add here.
|
|
54
90
|
*/
|
|
55
|
-
export function
|
|
56
|
-
|
|
91
|
+
export function referenceShapeGates(
|
|
92
|
+
rel: string,
|
|
93
|
+
gated: readonly string[] = GATED_PATHS,
|
|
94
|
+
): boolean {
|
|
95
|
+
return gated.some((prefix) => rel.startsWith(prefix))
|
|
57
96
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { bodyLines } from '@/markdown/scan'
|
|
2
|
+
import { TABLE_ROW } from '@/markdown/structure'
|
|
3
|
+
|
|
4
|
+
/** The H2 sections a reference page may hold below its opening. */
|
|
5
|
+
export const REFERENCE_SECTIONS: readonly string[] = [
|
|
6
|
+
'Output',
|
|
7
|
+
'Refusals',
|
|
8
|
+
'See also',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export type ReferenceShapeKind =
|
|
12
|
+
| 'extra-section'
|
|
13
|
+
| 'section-order'
|
|
14
|
+
| 'subsection'
|
|
15
|
+
| 'flag-table'
|
|
16
|
+
|
|
17
|
+
export interface ReferenceShapeFinding {
|
|
18
|
+
readonly kind: ReferenceShapeKind
|
|
19
|
+
readonly line: number
|
|
20
|
+
/** The heading text, or the first header cell of a flag table. */
|
|
21
|
+
readonly text: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n?/
|
|
25
|
+
const MARKER = /^kind:\s*(["']?)reference\1\s*$/m
|
|
26
|
+
const HEADING = /^(#{2,3})\s+(.*?)(?:\s+#+)?\s*$/
|
|
27
|
+
const SEPARATOR_ROW = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A first header cell naming flags or options, read with its code span and
|
|
31
|
+
* emphasis stripped. Only the first column is read, so a refusal table headed
|
|
32
|
+
* `Reason` stays out of reach whatever its later columns say.
|
|
33
|
+
*/
|
|
34
|
+
const FLAG_HEADER = /^(?:flags?|options?)$/i
|
|
35
|
+
|
|
36
|
+
/** Whether the page's frontmatter opts it into the reference shape. */
|
|
37
|
+
export function isReferencePage(source: string): boolean {
|
|
38
|
+
const frontmatter = FRONTMATTER.exec(source)
|
|
39
|
+
return frontmatter !== null && MARKER.test(frontmatter[1])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every breach of the reference shape on a page marked `kind: reference`, in
|
|
44
|
+
* line order, and nothing on an unmarked page.
|
|
45
|
+
*
|
|
46
|
+
* The shape is an opening holding the command, then `## Output`,
|
|
47
|
+
* `## Refusals`, and `## See also`, each at most once and in that order, though
|
|
48
|
+
* a page may leave one out, with no H3 and no table of flags, since
|
|
49
|
+
* `--help` already prints them. The check reads headings and table headers
|
|
50
|
+
* outside fences and leaves the opening to a reader.
|
|
51
|
+
*/
|
|
52
|
+
export function referenceShapeFindings(
|
|
53
|
+
source: string,
|
|
54
|
+
): ReferenceShapeFinding[] {
|
|
55
|
+
if (!isReferencePage(source)) return []
|
|
56
|
+
|
|
57
|
+
const found: ReferenceShapeFinding[] = []
|
|
58
|
+
const lines = bodyLines(source)
|
|
59
|
+
let lastSection = -1
|
|
60
|
+
|
|
61
|
+
for (const [index, line] of lines.entries()) {
|
|
62
|
+
if (line.fenced) continue
|
|
63
|
+
|
|
64
|
+
const heading = HEADING.exec(line.text)
|
|
65
|
+
if (heading) {
|
|
66
|
+
const [, marks, text] = heading
|
|
67
|
+
if (marks.length === 3) {
|
|
68
|
+
found.push({ kind: 'subsection', line: line.number, text })
|
|
69
|
+
} else {
|
|
70
|
+
const position = REFERENCE_SECTIONS.indexOf(text)
|
|
71
|
+
if (position === -1) {
|
|
72
|
+
found.push({ kind: 'extra-section', line: line.number, text })
|
|
73
|
+
} else if (position <= lastSection) {
|
|
74
|
+
found.push({ kind: 'section-order', line: line.number, text })
|
|
75
|
+
} else {
|
|
76
|
+
lastSection = position
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const previous = lines[index - 1]
|
|
83
|
+
const next = lines[index + 1]
|
|
84
|
+
const opensTable =
|
|
85
|
+
TABLE_ROW.test(line.text) &&
|
|
86
|
+
!(previous && !previous.fenced && TABLE_ROW.test(previous.text)) &&
|
|
87
|
+
next !== undefined &&
|
|
88
|
+
SEPARATOR_ROW.test(next.text)
|
|
89
|
+
if (!opensTable) continue
|
|
90
|
+
|
|
91
|
+
const first = firstCell(line.text)
|
|
92
|
+
if (FLAG_HEADER.test(first)) {
|
|
93
|
+
found.push({ kind: 'flag-table', line: line.number, text: first })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return found
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function firstCell(row: string): string {
|
|
101
|
+
const cells = row.trim().replace(/^\|/, '').split('|')
|
|
102
|
+
return (cells[0] ?? '').replace(/[`*_]/g, '').trim()
|
|
103
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { DensityKind } from '@/markdown/density'
|
|
2
|
+
|
|
3
|
+
/** The surfaces the retired bullet-stacks stage held, per `ALWAYS_GATED`. */
|
|
4
|
+
const BULLET_RUN_SCOPE = [
|
|
5
|
+
/^canon\/context\/.+\.md$/,
|
|
6
|
+
/^docs\/.+\.md$/,
|
|
7
|
+
/^(?:claude|\.claude)\/skills\/[^/]+\/SKILL\.md$/,
|
|
8
|
+
/^(?:claude|\.claude)\/skills\/[^/]+\/references\/.+\.md$/,
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The tracked documents the density limits hold: the context entries, the docs
|
|
13
|
+
* pages, the skill bodies and references of both skill trees, the tooling
|
|
14
|
+
* seeds and stack references, the wiki, and the two root guides.
|
|
15
|
+
*
|
|
16
|
+
* Rules stay out, since a rule is one imperative per bullet. So do
|
|
17
|
+
* `CLAUDE.md`, the requirement and design records, a seed carrying one of
|
|
18
|
+
* those three names, the changelog, the examples, and every `.canon/` record,
|
|
19
|
+
* each with a shape and cap of its own. Named here and nowhere else, so the
|
|
20
|
+
* audit, the gate stage reading its exit, and their tests read one scope.
|
|
21
|
+
*/
|
|
22
|
+
const DENSITY_SCOPE = [
|
|
23
|
+
...BULLET_RUN_SCOPE,
|
|
24
|
+
/^tooling\/[^/]+\/seeds\/(?:.+\/)?(?!(?:CLAUDE|REQUIREMENTS|DESIGN)\.md$)[^/]+\.md$/,
|
|
25
|
+
/^tooling\/[^/]+\/reference\.md$/,
|
|
26
|
+
/^wiki\/.+\.md$/,
|
|
27
|
+
/^(?:README|CONTRIBUTING)\.md$/,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The path prefixes whose density findings fail a run, each added by the
|
|
32
|
+
* rewrite batch that brought its folders to zero.
|
|
33
|
+
*
|
|
34
|
+
* It describes this repository's rewrite progress and nothing about a target's
|
|
35
|
+
* corpus, which is why the audit reads it only inside a canon checkout.
|
|
36
|
+
*/
|
|
37
|
+
export const GATED_PATHS: readonly string[] = [
|
|
38
|
+
'canon/context/claude-plugin/',
|
|
39
|
+
'canon/context/claude-internal/',
|
|
40
|
+
'canon/context/features/',
|
|
41
|
+
'canon/context/cli/',
|
|
42
|
+
'canon/context/sandbox/',
|
|
43
|
+
'canon/context/governance/',
|
|
44
|
+
'canon/context/tooling/',
|
|
45
|
+
'canon/context/web.md',
|
|
46
|
+
'canon/context/context-model.md',
|
|
47
|
+
'canon/context/design.md',
|
|
48
|
+
'canon/context/index.md',
|
|
49
|
+
'claude/skills/api-design/SKILL.md',
|
|
50
|
+
'claude/skills/auto-ship/SKILL.md',
|
|
51
|
+
'claude/skills/backlog-triage/SKILL.md',
|
|
52
|
+
'claude/skills/backlog-triage/references/apply.md',
|
|
53
|
+
'claude/skills/build-in-slices/SKILL.md',
|
|
54
|
+
'claude/skills/canon-cli/SKILL.md',
|
|
55
|
+
'claude/skills/canon-feedback-triage/SKILL.md',
|
|
56
|
+
'claude/skills/canon-feedback/SKILL.md',
|
|
57
|
+
'claude/skills/canvas-board/SKILL.md',
|
|
58
|
+
'claude/skills/ci-workflow/SKILL.md',
|
|
59
|
+
'claude/skills/code-craft/SKILL.md',
|
|
60
|
+
'claude/skills/codebase-layout/SKILL.md',
|
|
61
|
+
'claude/skills/context-fold/SKILL.md',
|
|
62
|
+
'claude/skills/create-rule/SKILL.md',
|
|
63
|
+
'claude/skills/create-skill/SKILL.md',
|
|
64
|
+
'claude/skills/decision-escalate/SKILL.md',
|
|
65
|
+
'claude/skills/deploy-app/SKILL.md',
|
|
66
|
+
'claude/skills/deprecation-migration/SKILL.md',
|
|
67
|
+
'claude/skills/design-taste/SKILL.md',
|
|
68
|
+
'claude/skills/docs-sync/SKILL.md',
|
|
69
|
+
'claude/skills/document-health/SKILL.md',
|
|
70
|
+
'claude/skills/draft-and-pick/SKILL.md',
|
|
71
|
+
'claude/skills/draft-doc/SKILL.md',
|
|
72
|
+
'claude/skills/draft-figure/SKILL.md',
|
|
73
|
+
'claude/skills/draft-identity/SKILL.md',
|
|
74
|
+
'claude/skills/draft-identity/references/card-route.md',
|
|
75
|
+
'claude/skills/draft-ready/SKILL.md',
|
|
76
|
+
'claude/skills/draft-screencast/SKILL.md',
|
|
77
|
+
'claude/skills/draft-screencast/references/beats-template.md',
|
|
78
|
+
'claude/skills/draft-slides/SKILL.md',
|
|
79
|
+
'claude/skills/git-branch/SKILL.md',
|
|
80
|
+
'claude/skills/git-commit/SKILL.md',
|
|
81
|
+
'claude/skills/git-followup/SKILL.md',
|
|
82
|
+
'claude/skills/git-issue/SKILL.md',
|
|
83
|
+
'claude/skills/git-pr/SKILL.md',
|
|
84
|
+
'claude/skills/git-ship/SKILL.md',
|
|
85
|
+
'claude/skills/git-ship/references/ci-watch.md',
|
|
86
|
+
'claude/skills/git-split/SKILL.md',
|
|
87
|
+
'claude/skills/git-split/references/independent.md',
|
|
88
|
+
'claude/skills/git-split/references/stacked.md',
|
|
89
|
+
'claude/skills/git-stage/SKILL.md',
|
|
90
|
+
'claude/skills/git-worktree/SKILL.md',
|
|
91
|
+
'claude/skills/git-worktree/references/cleanup.md',
|
|
92
|
+
'claude/skills/markdown-craft/SKILL.md',
|
|
93
|
+
'claude/skills/plan-feature/SKILL.md',
|
|
94
|
+
'claude/skills/plan-feature/references/batches.md',
|
|
95
|
+
'claude/skills/plan-groundwork/SKILL.md',
|
|
96
|
+
'claude/skills/plan-groundwork/references/output.md',
|
|
97
|
+
'claude/skills/plan-intake-answer/SKILL.md',
|
|
98
|
+
'claude/skills/plan-intake/SKILL.md',
|
|
99
|
+
'claude/skills/read-frames/SKILL.md',
|
|
100
|
+
'claude/skills/record-screencast/SKILL.md',
|
|
101
|
+
'claude/skills/review-address/SKILL.md',
|
|
102
|
+
'claude/skills/review-address/references/rebase-only.md',
|
|
103
|
+
'claude/skills/review-craft/SKILL.md',
|
|
104
|
+
'claude/skills/review-pr/SKILL.md',
|
|
105
|
+
'claude/skills/review-ui/SKILL.md',
|
|
106
|
+
'claude/skills/review-ui/references/hosted-preview.md',
|
|
107
|
+
'claude/skills/role-orchestrator/SKILL.md',
|
|
108
|
+
'claude/skills/role-orchestrator/references/orchestrator-filing.md',
|
|
109
|
+
'claude/skills/role-planner/SKILL.md',
|
|
110
|
+
'claude/skills/role-reviewer/SKILL.md',
|
|
111
|
+
'claude/skills/role-worker/SKILL.md',
|
|
112
|
+
'claude/skills/search-craft/SKILL.md',
|
|
113
|
+
'claude/skills/session-compact/SKILL.md',
|
|
114
|
+
'claude/skills/session-map/SKILL.md',
|
|
115
|
+
'claude/skills/session-relay/SKILL.md',
|
|
116
|
+
'claude/skills/session-resume/SKILL.md',
|
|
117
|
+
'claude/skills/session-worktree/SKILL.md',
|
|
118
|
+
'claude/skills/systematic-debugging/SKILL.md',
|
|
119
|
+
'claude/skills/target-check/SKILL.md',
|
|
120
|
+
'claude/skills/target-setup/SKILL.md',
|
|
121
|
+
'claude/skills/task-board/SKILL.md',
|
|
122
|
+
'claude/skills/task-board/references/create-report.md',
|
|
123
|
+
'claude/skills/task-complete/SKILL.md',
|
|
124
|
+
'claude/skills/teach-workspace/SKILL.md',
|
|
125
|
+
'claude/skills/test-craft/SKILL.md',
|
|
126
|
+
'claude/skills/test-first/SKILL.md',
|
|
127
|
+
'claude/skills/ui-checklist/SKILL.md',
|
|
128
|
+
'claude/skills/ux-audit/SKILL.md',
|
|
129
|
+
'claude/skills/ux-measure/SKILL.md',
|
|
130
|
+
'claude/skills/video-craft/SKILL.md',
|
|
131
|
+
'claude/skills/write-human/SKILL.md',
|
|
132
|
+
'claude/skills/youtube-transcripts/SKILL.md',
|
|
133
|
+
'docs/agents/indexes.md',
|
|
134
|
+
'docs/agents/install-and-sync.md',
|
|
135
|
+
'docs/agents/migrate.md',
|
|
136
|
+
'docs/agents/output-shape.md',
|
|
137
|
+
'docs/agents/overview.md',
|
|
138
|
+
'docs/agents/targets.md',
|
|
139
|
+
'canon/context/development/',
|
|
140
|
+
'canon/context/ci/',
|
|
141
|
+
'canon/context/scripts/',
|
|
142
|
+
'.claude/skills/internal-',
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The limit that gates before any folder is listed, and the surfaces it gates
|
|
147
|
+
* on. A run of more than 5 plain bullets failed the push in the context
|
|
148
|
+
* entries, the docs pages, and the skill bodies and references through a stage
|
|
149
|
+
* of its own before this measure existed, and waiting on `GATED_PATHS` would
|
|
150
|
+
* reopen that hole. The seeds, the stack references, the wiki, and the root
|
|
151
|
+
* guides were never held to it, and seven runs in the stack references would
|
|
152
|
+
* fail the push on the day this lands, so those wait for their batch.
|
|
153
|
+
*/
|
|
154
|
+
const ALWAYS_GATED: readonly DensityKind[] = ['bullet-run']
|
|
155
|
+
|
|
156
|
+
/** Whether a path takes density findings at all. */
|
|
157
|
+
export function inDensityScope(rel: string): boolean {
|
|
158
|
+
return DENSITY_SCOPE.some((pattern) => pattern.test(rel))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Whether a density finding of this kind on this path fails the run. */
|
|
162
|
+
export function densityGates(
|
|
163
|
+
rel: string,
|
|
164
|
+
kind: DensityKind,
|
|
165
|
+
gated: readonly string[] = GATED_PATHS,
|
|
166
|
+
): boolean {
|
|
167
|
+
if (!inDensityScope(rel)) return false
|
|
168
|
+
if (
|
|
169
|
+
ALWAYS_GATED.includes(kind) &&
|
|
170
|
+
BULLET_RUN_SCOPE.some((pattern) => pattern.test(rel))
|
|
171
|
+
)
|
|
172
|
+
return true
|
|
173
|
+
return gated.some((prefix) => rel.startsWith(prefix))
|
|
174
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BodyLine, visibleText } from '@/markdown/scan'
|
|
2
2
|
|
|
3
|
-
const HEADING = /^#{1,6}\s/
|
|
3
|
+
export const HEADING = /^#{1,6}\s/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A section marker holding its line alone, which is the signpost a document
|
|
@@ -65,10 +65,10 @@ const BOLD_LINE = /^\*\*([^*]+)\*\*\s*$/
|
|
|
65
65
|
const SPAN_LABEL = /^`[^`]+`$/
|
|
66
66
|
const MARKER_WIDTH = 20
|
|
67
67
|
|
|
68
|
-
const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
|
|
69
|
-
const TABLE_ROW = /^\s*\|/
|
|
68
|
+
export const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
|
|
69
|
+
export const TABLE_ROW = /^\s*\|/
|
|
70
70
|
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
71
|
-
const BLOCKQUOTE = /^\s*>/
|
|
71
|
+
export const BLOCKQUOTE = /^\s*>/
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* A sentence ends on terminal punctuation followed by the start of another
|
|
@@ -109,8 +109,8 @@ const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“([]*[A-Z]|`)|\s*$)/g
|
|
|
109
109
|
export const CHECKPOINTS = {
|
|
110
110
|
run: 40,
|
|
111
111
|
peerBullet: 130,
|
|
112
|
-
bullet:
|
|
113
|
-
paragraph:
|
|
112
|
+
bullet: 200,
|
|
113
|
+
paragraph: 350,
|
|
114
114
|
sentences: 4,
|
|
115
115
|
renderWidth: 80,
|
|
116
116
|
cadence: 3,
|
|
@@ -336,7 +336,7 @@ function isTableRun(run: readonly BodyLine[]): boolean {
|
|
|
336
336
|
* what lets a linked path reach the span test at all, since the markup around
|
|
337
337
|
* it would fail the pattern the destination is still attached.
|
|
338
338
|
*/
|
|
339
|
-
function isSectionMarker(text: string): boolean {
|
|
339
|
+
export function isSectionMarker(text: string): boolean {
|
|
340
340
|
const marker = text.match(BOLD_LINE)
|
|
341
341
|
if (!marker) return false
|
|
342
342
|
|
|
@@ -438,11 +438,7 @@ interface FoldedBullet extends BulletFinding {
|
|
|
438
438
|
readonly text: string
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
/**
|
|
442
|
-
* The walk `heavyBullets` reports from, keeping each past-checkpoint bullet's
|
|
443
|
-
* text so `longBlocks` can ask whether a base copy of it exists. One walk
|
|
444
|
-
* rather than two, for the reason `paragraphBlocks` is shared.
|
|
445
|
-
*/
|
|
441
|
+
/** The walk `heavyBullets` reports from. */
|
|
446
442
|
function foldBullets(
|
|
447
443
|
lines: readonly BodyLine[],
|
|
448
444
|
checkpoints: Checkpoints,
|
|
@@ -579,10 +575,7 @@ function paragraphText(block: readonly BodyLine[]): string {
|
|
|
579
575
|
* paragraph rule whenever the bullet rule was changed, and an author reading
|
|
580
576
|
* the sentence cap would find no weight rule to read at all.
|
|
581
577
|
*
|
|
582
|
-
*
|
|
583
|
-
* one population, sharing a median near 170 characters with no gap behind
|
|
584
|
-
* either candidate. They are separate checkpoints regardless, so either moves
|
|
585
|
-
* without dragging the other.
|
|
578
|
+
* They are separate checkpoints, so either moves without dragging the other.
|
|
586
579
|
*/
|
|
587
580
|
export function heavyParagraphs(
|
|
588
581
|
lines: readonly BodyLine[],
|
|
@@ -606,49 +599,6 @@ export function heavyParagraphs(
|
|
|
606
599
|
return findings
|
|
607
600
|
}
|
|
608
601
|
|
|
609
|
-
export interface LongBlock {
|
|
610
|
-
readonly kind: 'paragraph' | 'bullet'
|
|
611
|
-
readonly line: number
|
|
612
|
-
readonly characters: number
|
|
613
|
-
/** The block's source lines folded into one, as the paragraph and bullet folds read it. */
|
|
614
|
-
readonly text: string
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* The paragraphs past the paragraph checkpoint and the top-level bullets past
|
|
619
|
-
* the bullet checkpoint, each with its text.
|
|
620
|
-
*
|
|
621
|
-
* Weight alone, never the sentence cap, since the growth stage asks about the
|
|
622
|
-
* two numbers the standard states in characters. The definitions are the
|
|
623
|
-
* audit's own, so a block this names and a block the weight report names are
|
|
624
|
-
* one set.
|
|
625
|
-
*/
|
|
626
|
-
export function longBlocks(
|
|
627
|
-
lines: readonly BodyLine[],
|
|
628
|
-
checkpoints: Checkpoints,
|
|
629
|
-
): LongBlock[] {
|
|
630
|
-
const blocks: LongBlock[] = []
|
|
631
|
-
|
|
632
|
-
for (const block of paragraphBlocks(lines)) {
|
|
633
|
-
const text = paragraphText(block)
|
|
634
|
-
const characters = visibleText(text).length
|
|
635
|
-
if (characters > checkpoints.paragraph) {
|
|
636
|
-
blocks.push({
|
|
637
|
-
kind: 'paragraph',
|
|
638
|
-
line: block[0].number,
|
|
639
|
-
characters,
|
|
640
|
-
text,
|
|
641
|
-
})
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
for (const bullet of foldBullets(lines, checkpoints)) {
|
|
646
|
-
blocks.push({ kind: 'bullet', ...bullet })
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return blocks.sort((a, b) => a.line - b.line)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
602
|
/** Punctuation either side of a word, so an opener is compared on its letters. */
|
|
653
603
|
const WORD_EDGE = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu
|
|
654
604
|
|
|
@@ -132,7 +132,7 @@ const SAME_REPOSITORY =
|
|
|
132
132
|
* `canon/context/` carries no equivalent pattern. The same resolution test
|
|
133
133
|
* cannot separate a path every scaffolded project holds, such as
|
|
134
134
|
* `canon/context/index.md`, from this repository's own domain entry, such
|
|
135
|
-
* as `canon/context/context-model
|
|
135
|
+
* as `canon/context/context-model.md`, since both resolve here and only the
|
|
136
136
|
* second is a defect. That is a semantic read no pattern makes, so it stays
|
|
137
137
|
* with the rule and the review checklist rather than joining this reader.
|
|
138
138
|
*/
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
# A reply with no link is the ordinary turn, so it leaves before anything
|
|
13
|
+
# spawns. The payload holds the reply JSON-escaped, and `](` survives the
|
|
14
|
+
# escaping untouched, so a shell test on the raw payload is enough. This is the
|
|
15
|
+
# one quiet exit: every later failure to measure reports instead.
|
|
16
|
+
case "$input" in
|
|
17
|
+
*']('*) ;;
|
|
18
|
+
*) exit 0 ;;
|
|
19
|
+
esac
|
|
20
|
+
|
|
21
|
+
# A failure to measure exits 1 rather than 2. Exit 2 forces the session to
|
|
22
|
+
# continue, and a hook that cannot read `stop_hook_active` would then block
|
|
23
|
+
# every continuation too.
|
|
24
|
+
unread() {
|
|
25
|
+
printf 'Reply-links: the final reply was not checked. %s\n' "$1" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
command -v jq >/dev/null 2>&1 || unread 'Found no `jq` on PATH.'
|
|
30
|
+
|
|
31
|
+
event=$(printf '%s' "$input" | jq -r '.hook_event_name // empty' 2>/dev/null)
|
|
32
|
+
[ -n "$event" ] || unread 'The payload did not parse.'
|
|
33
|
+
[ "$event" = "Stop" ] || exit 0
|
|
34
|
+
|
|
35
|
+
# The forced continuation after a block fires this hook again with the field
|
|
36
|
+
# true. Passing it is what makes the block fire once, so the corrected links
|
|
37
|
+
# are not checked a second time.
|
|
38
|
+
stop_hook_active=$(printf '%s' "$input" | jq -r '.stop_hook_active // false')
|
|
39
|
+
[ "$stop_hook_active" = "true" ] && exit 0
|
|
40
|
+
|
|
41
|
+
reply=$(printf '%s' "$input" | jq -r '.last_assistant_message // empty')
|
|
42
|
+
case "$reply" in
|
|
43
|
+
*']('*) ;;
|
|
44
|
+
*) exit 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
# The verb resolves a relative link against the main worktree root, read from
|
|
48
|
+
# the directory the session stands in, so the payload's cwd is named rather than
|
|
49
|
+
# inherited from wherever the hook was spawned.
|
|
50
|
+
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty')
|
|
51
|
+
[ -n "$cwd" ] && [ -d "$cwd" ] || unread 'The payload carried no working directory that exists.'
|
|
52
|
+
|
|
53
|
+
# A project has no checkout of the CLI, so this resolves the installed binary
|
|
54
|
+
# alone, and a machine without it says so rather than passing the reply.
|
|
55
|
+
if command -v canon >/dev/null 2>&1; then
|
|
56
|
+
record=$(cd "$cwd" && printf '%s' "$reply" | canon hooks reply-links --json 2>/dev/null)
|
|
57
|
+
else
|
|
58
|
+
unread 'Found no `canon` binary on PATH. Install one with `bun add -g @erclx/canon`.'
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# The record decides rather than the exit code. A binary predating the verb
|
|
62
|
+
# exits 1 on an unknown command, which is the code a finding also leaves, and
|
|
63
|
+
# it writes no record.
|
|
64
|
+
ok=$(printf '%s' "$record" | jq -r 'if .ok == true then "true" elif .ok == false then "false" else empty end' 2>/dev/null)
|
|
65
|
+
case "$ok" in
|
|
66
|
+
true) exit 0 ;;
|
|
67
|
+
false)
|
|
68
|
+
printf '%s\n' "$(printf '%s' "$record" | jq -r '.message')" >&2
|
|
69
|
+
exit 2
|
|
70
|
+
;;
|
|
71
|
+
*) unread 'The link check returned no record. An installed `canon` that predates `canon hooks reply-links` does this.' ;;
|
|
72
|
+
esac
|
|
@@ -77,13 +77,24 @@ headings=$(printf '%s' "$record" |
|
|
|
77
77
|
colons=$(printf '%s' "$record" |
|
|
78
78
|
jq -r --arg new "$new" '.entries[]?.colons[]? | (.lead // (.text[:.column] | sub("^.*\\s"; ""))) as $lead | select($new | contains($lead + ":")) | ":\(.line):\(.column + 1) \(.text)"' 2>/dev/null)
|
|
79
79
|
|
|
80
|
+
# Density findings are handed back whole, gating or not, since the verb only
|
|
81
|
+
# returns them for a path in its scope and a finding the push does not fail on
|
|
82
|
+
# yet is the one a rewrite has to clear before its folder is gated.
|
|
83
|
+
density=$(printf '%s' "$record" |
|
|
84
|
+
jq -r '.entries[]?.density[]? | ":\(.line) \(.kind) " + (if .limit == null then "opens with neither a paragraph nor a table" else "\(.value) past \(.limit)" end) + (if .gating then " gating" else "" end)' 2>/dev/null)
|
|
85
|
+
|
|
86
|
+
# A page marked `kind: reference` carries reference shape findings, handed back
|
|
87
|
+
# whole the same way, since a gated one fails the push like a density finding.
|
|
88
|
+
shape=$(printf '%s' "$record" |
|
|
89
|
+
jq -r '.entries[]?.referenceShape[]? | ":\(.line) \(.kind) \(.text)" + (if .gating then " gating" else "" end)' 2>/dev/null)
|
|
90
|
+
|
|
80
91
|
# A set the verb shipped empty measures nothing and would report a clean file.
|
|
81
92
|
# Reading the findings alone turns that narrowed check into a pass, so the field
|
|
82
93
|
# is read beside them.
|
|
83
94
|
empty=$(printf '%s' "$record" |
|
|
84
95
|
jq -r '[.bans.emptySets[]?] | join(", ")' 2>/dev/null)
|
|
85
96
|
|
|
86
|
-
[ -z "$hits" ] && [ -z "$headings" ] && [ -z "$colons" ] && [ -z "$unread" ] && [ -z "$empty" ] && exit 0
|
|
97
|
+
[ -z "$hits" ] && [ -z "$headings" ] && [ -z "$colons" ] && [ -z "$density" ] && [ -z "$shape" ] && [ -z "$unread" ] && [ -z "$empty" ] && exit 0
|
|
87
98
|
|
|
88
99
|
nl=$'\n'
|
|
89
100
|
msg=""
|
|
@@ -111,4 +122,14 @@ if [ -n "$colons" ]; then
|
|
|
111
122
|
msg="${msg:+$msg$nl}$found"
|
|
112
123
|
fi
|
|
113
124
|
|
|
125
|
+
if [ -n "$density" ]; then
|
|
126
|
+
found=$(printf 'Standards-audit: %s breaks the markdown density limits, being a 350-character paragraph, 2 paragraphs in a row, a 200-character bullet, 5 plain bullets in a row, no H4, and a paragraph or table opening every H3. Split each block at its seam rather than trimming words to pass. A finding marked gating fails the push.\n%s' "$file" "$density")
|
|
127
|
+
msg="${msg:+$msg$nl}$found"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [ -n "$shape" ]; then
|
|
131
|
+
found=$(printf 'Standards-audit: %s is marked kind: reference and breaks its shape, being an opening, then Output, Refusals, and See also, each at most once and in that order, no H3, and no flag table. Point at the command'"'"'s own --help for flags. A finding marked gating fails the push.\n%s' "$file" "$shape")
|
|
132
|
+
msg="${msg:+$msg$nl}$found"
|
|
133
|
+
fi
|
|
134
|
+
|
|
114
135
|
jq -nc --arg msg "$msg" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|