@erclx/canon 5.32.0 → 5.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +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/SKILL.md +1 -1
- package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
- package/claude/skills/{canvas → canvas-board}/SKILL.md +2 -2
- package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
- 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 +2 -2
- package/{standards → claude/skills/create-rule/references}/rule.md +12 -5
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/{standards → claude/skills/create-skill/references}/skill.md +17 -14
- 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-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-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 +1 -1
- package/claude/skills/review-ui/references/post.md +1 -1
- 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 +23 -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/references/detect.md +1 -1
- 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/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 +1 -1
- package/docs/agents/commands.md +3 -2
- package/docs/agents/context-audit-checks.md +4 -4
- package/docs/agents/index.md +3 -3
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +6 -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/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/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 +1 -1
- 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/renames.toml +0 -11
- package/governance/rules/canon/context.md +13 -0
- package/governance/rules/claude/skill.md +17 -0
- package/governance/stacks/base.toml +2 -4
- package/package.json +4 -4
- package/scripts/core/install-check.sh +3 -3
- package/scripts/lib/ui.sh +0 -159
- package/src/audits/catalog.ts +0 -1
- 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/claude.ts +1 -1
- package/src/commands/gov.ts +2 -15
- package/src/commands/labels.ts +326 -7
- package/src/commands/migrate.ts +6 -1
- package/src/commands/pr.ts +1 -1
- 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/gate/measures.ts +1 -1
- package/src/gate/sequencer.ts +56 -1
- package/src/gate/stages.ts +1 -7
- package/src/gov/install.ts +2 -3
- package/src/gov/stacks.ts +1 -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/paths.ts +4 -4
- 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 +5 -44
- 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/claude/skills/{canvas → canvas-board}/references/canvas.md +0 -0
- /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
|
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
|
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.
|