@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/records/validate.ts
CHANGED
|
@@ -22,7 +22,6 @@ export const RECORD_KINDS = [
|
|
|
22
22
|
'plans',
|
|
23
23
|
'groundwork',
|
|
24
24
|
'intake',
|
|
25
|
-
'memory',
|
|
26
25
|
'standards',
|
|
27
26
|
'teach',
|
|
28
27
|
] as const
|
|
@@ -60,7 +59,6 @@ export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
|
|
|
60
59
|
export const FINDING_KINDS = [
|
|
61
60
|
'name-malformed',
|
|
62
61
|
'title-missing',
|
|
63
|
-
'title-is-slug',
|
|
64
62
|
'section-missing',
|
|
65
63
|
'scope-unanchored',
|
|
66
64
|
'entry-unreasoned',
|
|
@@ -72,7 +70,6 @@ export const FINDING_KINDS = [
|
|
|
72
70
|
'state-missing',
|
|
73
71
|
'closing-partial',
|
|
74
72
|
'item-incomplete',
|
|
75
|
-
'category-mismatch',
|
|
76
73
|
'operator-call-phrasing',
|
|
77
74
|
'batch-unsplit',
|
|
78
75
|
'source-unquoted',
|
|
@@ -81,22 +78,12 @@ export const FINDING_KINDS = [
|
|
|
81
78
|
|
|
82
79
|
export type FindingKind = (typeof FINDING_KINDS)[number]
|
|
83
80
|
|
|
84
|
-
/**
|
|
85
|
-
* The transforms `migrate.ts` carries. Most finding kinds have none, since a
|
|
86
|
-
* transform is only safe where the old shape is recoverable from the file
|
|
87
|
-
* itself, so this stays optional on `Finding` rather than required.
|
|
88
|
-
*/
|
|
89
|
-
export const FINDING_REMEDIES = ['category-from-name'] as const
|
|
90
|
-
|
|
91
|
-
export type FindingRemedy = (typeof FINDING_REMEDIES)[number]
|
|
92
|
-
|
|
93
81
|
export interface Finding {
|
|
94
82
|
readonly kind: FindingKind
|
|
95
83
|
/** The record the finding sits in, relative to the validated folder. */
|
|
96
84
|
readonly record: string
|
|
97
85
|
readonly subject: string
|
|
98
86
|
readonly message: string
|
|
99
|
-
readonly remedy?: FindingRemedy
|
|
100
87
|
}
|
|
101
88
|
|
|
102
89
|
export interface ValidateReport {
|
|
@@ -214,9 +201,8 @@ function finding(
|
|
|
214
201
|
record: string,
|
|
215
202
|
subject: string,
|
|
216
203
|
message: string,
|
|
217
|
-
remedy?: FindingRemedy,
|
|
218
204
|
): Finding {
|
|
219
|
-
return { kind, record, subject, message
|
|
205
|
+
return { kind, record, subject, message }
|
|
220
206
|
}
|
|
221
207
|
|
|
222
208
|
async function listMarkdown(dir: string): Promise<string[]> {
|
|
@@ -934,169 +920,6 @@ async function checkWorkspace(dir: string, slug: string): Promise<Finding[]> {
|
|
|
934
920
|
]
|
|
935
921
|
}
|
|
936
922
|
|
|
937
|
-
const MEMORY_INDEX = 'index.md'
|
|
938
|
-
const MEMORY_FIELDS = ['title', 'description', 'category'] as const
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* The filename prefix and the `category` field are one fact in two spellings,
|
|
942
|
-
* so the map is the whole type list and the comparison against it is what
|
|
943
|
-
* catches a prefix outside the set, a field disagreeing with the prefix, and a
|
|
944
|
-
* casing drift that would open a second group in the catalog.
|
|
945
|
-
*/
|
|
946
|
-
export const CATEGORY_BY_TYPE = {
|
|
947
|
-
feedback: 'Feedback',
|
|
948
|
-
project: 'Project',
|
|
949
|
-
user: 'User',
|
|
950
|
-
reference: 'Reference',
|
|
951
|
-
} as const
|
|
952
|
-
|
|
953
|
-
export type MemoryType = keyof typeof CATEGORY_BY_TYPE
|
|
954
|
-
|
|
955
|
-
const MEMORY_TYPES = Object.keys(CATEGORY_BY_TYPE) as readonly MemoryType[]
|
|
956
|
-
|
|
957
|
-
export const MEMORY_NAME = /^([a-z]+)-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/
|
|
958
|
-
|
|
959
|
-
/** The two markers a rule-bearing body carries, on top of the rule line itself. */
|
|
960
|
-
const MEMORY_MARKERS = ['**Why:**', '**How to apply:**'] as const
|
|
961
|
-
|
|
962
|
-
export function memoryType(value: string): MemoryType | undefined {
|
|
963
|
-
return MEMORY_TYPES.find((type) => type === value)
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
/**
|
|
967
|
-
* The one missing-field shape `migrate.ts` can repair: `category` alone,
|
|
968
|
-
* recoverable from the same filename prefix `checkMemory` already read it
|
|
969
|
-
* from. `title` and `description` are prose nobody wrote down, so a finding
|
|
970
|
-
* naming either carries no remedy.
|
|
971
|
-
*/
|
|
972
|
-
function memoryRemedy(
|
|
973
|
-
missing: readonly string[],
|
|
974
|
-
named: MemoryType | undefined,
|
|
975
|
-
): FindingRemedy | undefined {
|
|
976
|
-
return named && missing.length === 1 && missing[0] === 'category'
|
|
977
|
-
? 'category-from-name'
|
|
978
|
-
: undefined
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
export function checkMemory(name: string, text: string): Finding[] {
|
|
982
|
-
const findings: Finding[] = []
|
|
983
|
-
const match = MEMORY_NAME.exec(name)
|
|
984
|
-
const named = match ? memoryType(match[1]) : undefined
|
|
985
|
-
|
|
986
|
-
if (!named) {
|
|
987
|
-
findings.push(
|
|
988
|
-
finding(
|
|
989
|
-
'name-malformed',
|
|
990
|
-
name,
|
|
991
|
-
name,
|
|
992
|
-
`is not named <type>-<slug>.md with a type of ${MEMORY_TYPES.join(', ')}.`,
|
|
993
|
-
),
|
|
994
|
-
)
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const frontmatter = parseFrontmatter(text)
|
|
998
|
-
const missing = MEMORY_FIELDS.filter(
|
|
999
|
-
(field) => !readField(frontmatter, field),
|
|
1000
|
-
)
|
|
1001
|
-
|
|
1002
|
-
if (missing.length > 0) {
|
|
1003
|
-
findings.push(
|
|
1004
|
-
finding(
|
|
1005
|
-
'frontmatter-incomplete',
|
|
1006
|
-
name,
|
|
1007
|
-
name,
|
|
1008
|
-
`carries no ${missing.join(' and no ')}.`,
|
|
1009
|
-
memoryRemedy(missing, named),
|
|
1010
|
-
),
|
|
1011
|
-
)
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Its own kind rather than `title-missing`, which means an absent heading on a
|
|
1015
|
-
// plan. One kind covering both leaves a caller filtering the JSON unable to
|
|
1016
|
-
// tell a record with no title from one whose title is its own slug.
|
|
1017
|
-
if (readField(frontmatter, 'title') === name.replace(/\.md$/, '')) {
|
|
1018
|
-
findings.push(
|
|
1019
|
-
finding(
|
|
1020
|
-
'title-is-slug',
|
|
1021
|
-
name,
|
|
1022
|
-
name,
|
|
1023
|
-
'is titled with its own filename, so the catalog renders a slug where the rule belongs.',
|
|
1024
|
-
),
|
|
1025
|
-
)
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
const category = readField(frontmatter, 'category')
|
|
1029
|
-
|
|
1030
|
-
// Reported against the prefix alone. A name the prefix rule already failed
|
|
1031
|
-
// has no type to compare against, and reporting it twice names one defect as
|
|
1032
|
-
// two.
|
|
1033
|
-
if (named && category && category !== CATEGORY_BY_TYPE[named]) {
|
|
1034
|
-
findings.push(
|
|
1035
|
-
finding(
|
|
1036
|
-
'category-mismatch',
|
|
1037
|
-
name,
|
|
1038
|
-
category,
|
|
1039
|
-
`is not ${CATEGORY_BY_TYPE[named]}, which the filename prefix declares.`,
|
|
1040
|
-
),
|
|
1041
|
-
)
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
return [
|
|
1045
|
-
...findings,
|
|
1046
|
-
...checkMemoryBody(
|
|
1047
|
-
name,
|
|
1048
|
-
text.slice(frontmatter?.raw.length ?? 0),
|
|
1049
|
-
named ?? category,
|
|
1050
|
-
),
|
|
1051
|
-
]
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
/**
|
|
1055
|
-
* A `user` or `reference` entry is a single sentence by design, so the markers
|
|
1056
|
-
* are checked only where a rule is being stated. The type is read off the
|
|
1057
|
-
* prefix, falling back to the category so a misnamed file is still checked
|
|
1058
|
-
* against the shape it claims.
|
|
1059
|
-
*/
|
|
1060
|
-
function checkMemoryBody(
|
|
1061
|
-
name: string,
|
|
1062
|
-
text: string,
|
|
1063
|
-
claimed: string | undefined,
|
|
1064
|
-
): Finding[] {
|
|
1065
|
-
const type = claimed && memoryType(claimed.toLowerCase())
|
|
1066
|
-
if (type !== 'feedback' && type !== 'project') return []
|
|
1067
|
-
|
|
1068
|
-
const body = linesOutsideFences(text).filter((line) => line.trim().length > 0)
|
|
1069
|
-
|
|
1070
|
-
const findings: Finding[] = []
|
|
1071
|
-
const opening = body[0]
|
|
1072
|
-
|
|
1073
|
-
if (!opening || MEMORY_MARKERS.some((marker) => opening.startsWith(marker))) {
|
|
1074
|
-
findings.push(
|
|
1075
|
-
finding(
|
|
1076
|
-
'section-missing',
|
|
1077
|
-
name,
|
|
1078
|
-
'the rule line',
|
|
1079
|
-
'is absent, so the entry carries a rationale with no rule to apply.',
|
|
1080
|
-
),
|
|
1081
|
-
)
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
for (const marker of MEMORY_MARKERS) {
|
|
1085
|
-
if (!body.some((line) => line.startsWith(marker))) {
|
|
1086
|
-
findings.push(
|
|
1087
|
-
finding(
|
|
1088
|
-
'section-missing',
|
|
1089
|
-
name,
|
|
1090
|
-
marker,
|
|
1091
|
-
`is required on a ${type} entry and the body carries no such line.`,
|
|
1092
|
-
),
|
|
1093
|
-
)
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
return findings
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
923
|
const STANDARD_INDEX = 'index.md'
|
|
1101
924
|
const STANDARD_FIELDS = ['title', 'description'] as const
|
|
1102
925
|
|
|
@@ -1225,7 +1048,7 @@ export function checkStandard(name: string, text: string): Finding[] {
|
|
|
1225
1048
|
*/
|
|
1226
1049
|
const FOLDER_CHECK: Readonly<
|
|
1227
1050
|
Record<
|
|
1228
|
-
Exclude<RecordKind, 'plans' | '
|
|
1051
|
+
Exclude<RecordKind, 'plans' | 'standards'>,
|
|
1229
1052
|
(dir: string, slug: string) => Promise<Finding[]>
|
|
1230
1053
|
>
|
|
1231
1054
|
> = {
|
|
@@ -1279,15 +1102,6 @@ export async function validateRecords(
|
|
|
1279
1102
|
|
|
1280
1103
|
if (kind === 'plans') return validateFiles(dir, kind, checkPlan)
|
|
1281
1104
|
|
|
1282
|
-
if (kind === 'memory') {
|
|
1283
|
-
return validateFiles(
|
|
1284
|
-
dir,
|
|
1285
|
-
kind,
|
|
1286
|
-
checkMemory,
|
|
1287
|
-
(file) => file === MEMORY_INDEX,
|
|
1288
|
-
)
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
1105
|
// The walk stays flat, matching install and the catalog.
|
|
1292
1106
|
if (kind === 'standards') {
|
|
1293
1107
|
return validateFiles(
|
package/src/sandbox/census.ts
CHANGED
|
@@ -71,7 +71,7 @@ export function listSkills(root: string): string[] {
|
|
|
71
71
|
*
|
|
72
72
|
* `<category>-<command>` is the rule `canon/context/sandbox/coverage/census.md` states, and it
|
|
73
73
|
* alone pairs 29 of 54 skills. The bare `<command>` fallback is what reaches the
|
|
74
|
-
* rest: `claude/target-setup.
|
|
74
|
+
* rest: `claude/target-setup.ts` drives the `target-setup` skill, not a `claude-target-setup` that
|
|
75
75
|
* does not exist. Stating one spelling and shipping two is what let the audit
|
|
76
76
|
* report a paired skill as unpaired.
|
|
77
77
|
*
|
package/src/sandbox/coverage.ts
CHANGED
|
@@ -44,13 +44,9 @@ function listScenarios(sandboxDir: string): ScenarioCoverage[] {
|
|
|
44
44
|
const scenarios: ScenarioCoverage[] = []
|
|
45
45
|
|
|
46
46
|
for (const category of listCategories(sandboxDir)) {
|
|
47
|
-
|
|
48
|
-
const { scenarios: files, ambiguous } = listScenarioFiles(
|
|
49
|
-
join(sandboxDir, category),
|
|
50
|
-
)
|
|
51
|
-
const commands = [...files.map((f) => f.command), ...ambiguous].sort()
|
|
47
|
+
const { scenarios: files } = listScenarioFiles(join(sandboxDir, category))
|
|
52
48
|
|
|
53
|
-
for (const command of
|
|
49
|
+
for (const { command } of files) {
|
|
54
50
|
scenarios.push({ category, command, armed: [] })
|
|
55
51
|
}
|
|
56
52
|
}
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
listCategories,
|
|
23
23
|
listScenarioFiles,
|
|
24
24
|
loadScenario,
|
|
25
|
-
type ScenarioKind,
|
|
26
25
|
} from '@/sandbox/scenario'
|
|
27
26
|
import { createStubRemote, readStubManifest } from '@/sandbox/stub-remote'
|
|
28
27
|
|
|
@@ -153,28 +152,6 @@ export function armKey(arm: Arm): string {
|
|
|
153
152
|
return arm.arm === undefined ? scenario : `${scenario}/${arm.arm}`
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
/**
|
|
157
|
-
* Reads the quoted arguments of `select_or_route_scenario`, joining a call that
|
|
158
|
-
* continues across backslash-newline pairs. The first argument is the prompt and
|
|
159
|
-
* the rest are arm names. A call this cannot see returns no arms, which the
|
|
160
|
-
* coverage cross-check in `enumerateArms` turns into an error rather than a
|
|
161
|
-
* smaller list.
|
|
162
|
-
*/
|
|
163
|
-
export function parseArmNames(source: string): string[] {
|
|
164
|
-
const start = source.search(/select_or_route_scenario\b/)
|
|
165
|
-
if (start === -1) return []
|
|
166
|
-
|
|
167
|
-
const call = source
|
|
168
|
-
.slice(start)
|
|
169
|
-
.replace(/\\\n/g, ' ')
|
|
170
|
-
.split('\n')[0]
|
|
171
|
-
?.replace(/^select_or_route_scenario/, '')
|
|
172
|
-
|
|
173
|
-
return [...(call ?? '').matchAll(/"([^"]*)"/g)]
|
|
174
|
-
.map((match) => match[1] ?? '')
|
|
175
|
-
.slice(1)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
155
|
function matchesTarget(
|
|
179
156
|
category: string,
|
|
180
157
|
command: string,
|
|
@@ -199,25 +176,14 @@ function matchesArm(arm: Arm, targets: readonly string[]): boolean {
|
|
|
199
176
|
}
|
|
200
177
|
|
|
201
178
|
/**
|
|
202
|
-
* Reads a scenario's arm names and anchor flag
|
|
203
|
-
*
|
|
179
|
+
* Reads a scenario's arm names and anchor flag off its module's own keys, which
|
|
180
|
+
* is what its stage routes on.
|
|
204
181
|
*/
|
|
205
182
|
function readArms(
|
|
206
183
|
file: string,
|
|
207
|
-
kind: ScenarioKind,
|
|
208
184
|
):
|
|
209
185
|
| { ok: true; anchor: boolean; names: string[] }
|
|
210
186
|
| { ok: false; error: string } {
|
|
211
|
-
if (kind === 'sh') {
|
|
212
|
-
const source = readFileSync(file, 'utf8')
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
ok: true,
|
|
216
|
-
anchor: /^use_anchor\s*\(\)/m.test(source),
|
|
217
|
-
names: parseArmNames(source),
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
187
|
try {
|
|
222
188
|
const definition = loadScenario(file)
|
|
223
189
|
|
|
@@ -250,17 +216,12 @@ export function enumerateArms(
|
|
|
250
216
|
const coverage = collectCoverage(root)
|
|
251
217
|
|
|
252
218
|
for (const category of listCategories(sandboxDir)) {
|
|
253
|
-
const { scenarios
|
|
254
|
-
join(sandboxDir, category),
|
|
255
|
-
)
|
|
256
|
-
for (const command of ambiguous)
|
|
257
|
-
if (matchesTarget(category, command, targets))
|
|
258
|
-
errors.push(`${category}:${command} exists as both .sh and .ts`)
|
|
219
|
+
const { scenarios } = listScenarioFiles(join(sandboxDir, category))
|
|
259
220
|
|
|
260
|
-
for (const { command, file
|
|
221
|
+
for (const { command, file } of scenarios) {
|
|
261
222
|
if (!matchesTarget(category, command, targets)) continue
|
|
262
223
|
|
|
263
|
-
const declared = readArms(file
|
|
224
|
+
const declared = readArms(file)
|
|
264
225
|
if (!declared.ok) {
|
|
265
226
|
errors.push(`${category}:${command} did not load: ${declared.error}`)
|
|
266
227
|
continue
|
|
@@ -740,13 +701,16 @@ function canonVersion(): string {
|
|
|
740
701
|
}
|
|
741
702
|
|
|
742
703
|
/**
|
|
743
|
-
* Adds
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
704
|
+
* Adds a detached worktree of `ref` at `path`, so it carries full history for
|
|
705
|
+
* the scenarios that read it, and returns a remover that is safe to call twice.
|
|
706
|
+
* `node_modules` is linked from the working checkout, since the copy runs the
|
|
707
|
+
* same CLI and carries no install of its own.
|
|
747
708
|
*/
|
|
748
|
-
function
|
|
749
|
-
|
|
709
|
+
export function addDetachedWorktree(
|
|
710
|
+
root: string,
|
|
711
|
+
path: string,
|
|
712
|
+
ref: string,
|
|
713
|
+
): () => void {
|
|
750
714
|
execFileSync('git', ['worktree', 'add', '--detach', path, ref], {
|
|
751
715
|
cwd: root,
|
|
752
716
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
@@ -800,7 +764,11 @@ export async function runEquivalence(
|
|
|
800
764
|
|
|
801
765
|
let removeBaseline: () => void
|
|
802
766
|
try {
|
|
803
|
-
removeBaseline =
|
|
767
|
+
removeBaseline = addDetachedWorktree(
|
|
768
|
+
options.root,
|
|
769
|
+
join(workDir, 'base'),
|
|
770
|
+
options.base,
|
|
771
|
+
)
|
|
804
772
|
} catch (error) {
|
|
805
773
|
// An unknown ref must not leave an empty scratch folder behind.
|
|
806
774
|
if (options.out === undefined)
|
package/src/sandbox/expect.ts
CHANGED
|
@@ -354,7 +354,7 @@ function checkWriteScope(
|
|
|
354
354
|
// none, and without this the declaration vanishes from the verdict entirely:
|
|
355
355
|
// no result, no skipped entry, and no contribution to the unchecked count that
|
|
356
356
|
// exists to surface exactly this. The `undefined` branch above cannot stand in,
|
|
357
|
-
// since `run
|
|
357
|
+
// since `canon sandbox run` always passes `--writes` and `readPathList` returns `[]` for an
|
|
358
358
|
// empty file. An arm whose output escaped the snapshot reads as a clean run,
|
|
359
359
|
// which is the vacuous pass the harness exists to remove.
|
|
360
360
|
if (writes.length === 0) {
|
|
@@ -377,7 +377,7 @@ function checkWriteScope(
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
/**
|
|
380
|
-
* `run
|
|
380
|
+
* `canon sandbox run` watches two toolkit roots for a write to shared session scratch
|
|
381
381
|
* during a run, and reports every one it finds as an unattributed escape with
|
|
382
382
|
* no arm able to fail on it. An arm whose skill legitimately reaches outside
|
|
383
383
|
* the sandbox tree declares the destinations here, and the harness asserts
|
|
@@ -388,8 +388,8 @@ function checkWriteScope(
|
|
|
388
388
|
* nothing skips rather than passing on a fabricated zero. An escape-scope
|
|
389
389
|
* declaration exists to bound a side effect nothing requires, so zero escapes
|
|
390
390
|
* is the outcome a correct run produces and reports as a pass outright,
|
|
391
|
-
* provided a watched root held something to watch. `
|
|
392
|
-
* returns an empty manifest both when a watch ran clean and when none of the
|
|
391
|
+
* provided a watched root held something to watch. `snapshotRoot` in
|
|
392
|
+
* `src/sandbox/headless/watch.ts` returns an empty manifest both when a watch ran clean and when none of the
|
|
393
393
|
* four watched directories existed under a root, and the two produce the same
|
|
394
394
|
* empty `escapes` list. `watched` is what tells them apart: a run that had
|
|
395
395
|
* nothing to watch reports unmeasured rather than passing on a diff it never
|
package/src/sandbox/provision.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
writeFileSync,
|
|
9
9
|
} from 'node:fs'
|
|
10
10
|
import { basename, dirname, join, relative } from 'node:path'
|
|
11
|
-
import { probeScenario, stageScenario } from '@/sandbox/hooks'
|
|
12
11
|
import {
|
|
13
12
|
FIXTURE_SUFFIX,
|
|
14
13
|
FIXTURES_DIR,
|
|
@@ -34,9 +33,8 @@ import {
|
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
35
|
* Provisions one scenario into the sandbox tree, and resets or cleans that tree.
|
|
37
|
-
* A
|
|
38
|
-
*
|
|
39
|
-
* here, in the order the retired bash dispatcher held.
|
|
36
|
+
* A scenario's arm runs in process through `scenario.ts`, and everything
|
|
37
|
+
* around it runs here, in the order the retired bash dispatcher held.
|
|
40
38
|
*
|
|
41
39
|
* Narration is part of the contract. `canon sandbox equivalence` compares the
|
|
42
40
|
* log line for line against the bash harness, so every message, its frame
|
|
@@ -403,36 +401,22 @@ interface Scenario {
|
|
|
403
401
|
|
|
404
402
|
/**
|
|
405
403
|
* Reads what a scenario declares ahead of provisioning: its config exports and
|
|
406
|
-
* whether it stages from the anchor
|
|
407
|
-
*
|
|
404
|
+
* whether it stages from the anchor, from its module, which is also what its
|
|
405
|
+
* stage runs.
|
|
408
406
|
*/
|
|
409
407
|
function declareScenario(
|
|
410
408
|
root: string,
|
|
411
409
|
scenario: Scenario,
|
|
412
410
|
env: NodeJS.ProcessEnv,
|
|
413
|
-
):
|
|
414
|
-
| { kind: 'sh'; file: string; isAnchor: boolean }
|
|
415
|
-
| { kind: 'ts'; definition: ScenarioDefinition; isAnchor: boolean } {
|
|
411
|
+
): { definition: ScenarioDefinition; isAnchor: boolean } {
|
|
416
412
|
const name = `${scenario.category}/${scenario.command}`
|
|
417
413
|
const resolved = resolveScenarioFile(
|
|
418
414
|
join(root, 'sandbox'),
|
|
419
415
|
scenario.category,
|
|
420
416
|
scenario.command,
|
|
421
417
|
)
|
|
422
|
-
if (!resolved.ok && resolved.reason === 'ambiguous')
|
|
423
|
-
fail(
|
|
424
|
-
`Sandbox script ${name} exists as both .sh and .ts. Delete the one the port replaced.`,
|
|
425
|
-
)
|
|
426
418
|
if (!resolved.ok) fail(`Sandbox script not found: ${name}`)
|
|
427
419
|
|
|
428
|
-
if (resolved.kind === 'sh') {
|
|
429
|
-
const probe = probeScenario(resolved.file, env)
|
|
430
|
-
if (!probe.ok) throw new Halt(probe.status)
|
|
431
|
-
Object.assign(env, probe.exports)
|
|
432
|
-
|
|
433
|
-
return { kind: 'sh', file: resolved.file, isAnchor: probe.isAnchor }
|
|
434
|
-
}
|
|
435
|
-
|
|
436
420
|
let definition: ScenarioDefinition
|
|
437
421
|
try {
|
|
438
422
|
definition = loadScenario(resolved.file)
|
|
@@ -443,7 +427,7 @@ function declareScenario(
|
|
|
443
427
|
}
|
|
444
428
|
Object.assign(env, scenarioExports(definition, root, env))
|
|
445
429
|
|
|
446
|
-
return {
|
|
430
|
+
return { definition, isAnchor: definition.anchor === true }
|
|
447
431
|
}
|
|
448
432
|
|
|
449
433
|
async function provisionScenario(
|
|
@@ -481,21 +465,14 @@ async function provisionScenario(
|
|
|
481
465
|
// A scenario that execs a verb as its last step hands the run to that verb,
|
|
482
466
|
// whose own frame is the last one written, so nothing follows it here. An
|
|
483
467
|
// in-process arm writes its exports straight into `env`.
|
|
484
|
-
const staged =
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
dir: sandbox,
|
|
491
|
-
env,
|
|
492
|
-
arm: scenario.arm,
|
|
493
|
-
})),
|
|
494
|
-
exports: {},
|
|
495
|
-
}
|
|
468
|
+
const staged = await stageScenarioInProcess(declared.definition, {
|
|
469
|
+
root,
|
|
470
|
+
dir: sandbox,
|
|
471
|
+
env,
|
|
472
|
+
arm: scenario.arm,
|
|
473
|
+
})
|
|
496
474
|
if (staged.ending === 'replaced') throw new Halt(staged.status, false)
|
|
497
475
|
if (staged.ending === 'exited') throw new Halt(staged.status)
|
|
498
|
-
Object.assign(env, staged.exports)
|
|
499
476
|
|
|
500
477
|
const isSkipAutoCommit = (env.SANDBOX_SKIP_AUTO_COMMIT ?? '') !== ''
|
|
501
478
|
const isSkipInject = (env.SANDBOX_SKIP_SKILL_INJECT ?? '') !== ''
|
|
@@ -602,7 +579,7 @@ function parseTarget(target: string): { category: string; command: string } {
|
|
|
602
579
|
}
|
|
603
580
|
|
|
604
581
|
export interface SandboxRequest {
|
|
605
|
-
/** False where a caller already opened the frame, as `run
|
|
582
|
+
/** False where a caller already opened the frame, as `canon sandbox run` and the picker do. */
|
|
606
583
|
readonly isHeader: boolean
|
|
607
584
|
/** `<category>:<command>`, `reset`, or `clean`. */
|
|
608
585
|
readonly target: string
|