@erclx/canon 5.26.0 → 5.28.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 +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +2 -0
- package/claude/skills/auto-ship/SKILL.md +3 -11
- package/claude/skills/auto-ship/references/review-findings.md +20 -0
- package/claude/skills/backlog-triage/SKILL.md +2 -0
- package/claude/skills/bash-cli-script/SKILL.md +2 -0
- package/claude/skills/build-in-slices/SKILL.md +2 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/canon-feedback/SKILL.md +2 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
- package/claude/skills/canon-operator/SKILL.md +2 -0
- package/claude/skills/canon-rollout/SKILL.md +3 -11
- package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
- package/claude/skills/canvas/SKILL.md +13 -0
- package/claude/skills/ci-workflow/SKILL.md +2 -0
- package/claude/skills/code-craft/SKILL.md +2 -0
- package/claude/skills/codebase-layout/SKILL.md +2 -0
- package/claude/skills/context-fold/SKILL.md +3 -17
- package/claude/skills/context-fold/references/completion-report.md +26 -0
- package/claude/skills/create-rule/SKILL.md +2 -0
- package/claude/skills/create-skill/SKILL.md +2 -0
- package/claude/skills/create-standard/SKILL.md +2 -0
- package/claude/skills/decision-escalate/SKILL.md +2 -0
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +2 -0
- package/claude/skills/design-extract/SKILL.md +2 -0
- package/claude/skills/design-taste/SKILL.md +2 -0
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/SKILL.md +2 -0
- package/claude/skills/draft-and-pick/SKILL.md +2 -0
- package/claude/skills/draft-doc/SKILL.md +2 -0
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +2 -0
- package/claude/skills/draft-identity/SKILL.md +2 -0
- package/claude/skills/draft-ready/SKILL.md +5 -1
- package/claude/skills/draft-screencast/SKILL.md +2 -0
- package/claude/skills/draft-slides/SKILL.md +2 -0
- package/claude/skills/git-branch/SKILL.md +2 -0
- package/claude/skills/git-commit/SKILL.md +2 -0
- package/claude/skills/git-followup/SKILL.md +2 -0
- package/claude/skills/git-issue/SKILL.md +2 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +7 -7
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +2 -0
- package/claude/skills/git-stage/SKILL.md +2 -0
- package/claude/skills/git-worktree/SKILL.md +2 -0
- package/claude/skills/index-lookup/SKILL.md +2 -0
- package/claude/skills/markdown-propose/SKILL.md +2 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/plan-feature/SKILL.md +2 -0
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/plan-intake/SKILL.md +2 -0
- package/claude/skills/plan-intake-answer/SKILL.md +2 -0
- package/claude/skills/project-commands/SKILL.md +2 -0
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +2 -0
- package/claude/skills/repo-metadata/SKILL.md +2 -0
- package/claude/skills/restate-plainly/SKILL.md +2 -0
- package/claude/skills/review-address/REQUIREMENT.md +4 -2
- package/claude/skills/review-address/SKILL.md +19 -14
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
- package/claude/skills/review-branch/SKILL.md +2 -0
- package/claude/skills/review-craft/SKILL.md +2 -0
- package/claude/skills/review-craft/references/ui.md +6 -6
- package/claude/skills/review-pr/SKILL.md +4 -8
- package/claude/skills/review-pr/references/body-variants.md +52 -0
- package/claude/skills/review-pr/references/key-changes.md +16 -0
- package/claude/skills/review-pr/references/post.md +15 -43
- package/claude/skills/review-ui/REQUIREMENT.md +2 -2
- package/claude/skills/review-ui/SKILL.md +16 -12
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +13 -3
- package/claude/skills/review-ui/references/tick.md +22 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
- package/claude/skills/role-orchestrator/SKILL.md +31 -42
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +13 -3
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +6 -34
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +12 -23
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
- package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +4 -2
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +3 -1
- package/claude/skills/role-worker/SKILL.md +5 -2
- package/claude/skills/search-craft/SKILL.md +2 -0
- package/claude/skills/seed-sync/SKILL.md +2 -0
- package/claude/skills/session-compact/SKILL.md +2 -0
- package/claude/skills/session-map/SKILL.md +2 -0
- package/claude/skills/session-relay/SKILL.md +2 -0
- package/claude/skills/session-resume/SKILL.md +2 -0
- package/claude/skills/session-worktree/SKILL.md +3 -11
- package/claude/skills/session-worktree/references/install-command.md +20 -0
- package/claude/skills/sketch-design/SKILL.md +2 -0
- package/claude/skills/systematic-debugging/SKILL.md +2 -0
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +2 -0
- package/claude/skills/task-board/SKILL.md +2 -0
- package/claude/skills/teach-workspace/SKILL.md +5 -15
- package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
- package/claude/skills/teach-workspace/references/revisit.md +22 -0
- package/claude/skills/test-craft/SKILL.md +2 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/ui-checklist/SKILL.md +3 -1
- package/claude/skills/ux-audit/SKILL.md +2 -0
- package/claude/skills/ux-measure/SKILL.md +2 -0
- package/claude/skills/ux-walkthrough/SKILL.md +2 -0
- package/claude/skills/video-craft/SKILL.md +2 -0
- package/claude/skills/write-human/SKILL.md +2 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +24 -8
- package/docs/agents/capture.md +5 -5
- package/docs/agents/commands.md +2 -2
- package/docs/agents/gate.md +3 -1
- package/docs/agents/index.md +5 -4
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +47 -23
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/pr-tick.md +72 -0
- package/docs/agents/sandbox.md +30 -6
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +2 -1
- package/docs/agents/tasks-plans.md +1 -1
- package/docs/agents/tasks-validate.md +18 -18
- package/docs/agents/tasks.md +1 -1
- package/docs/workflow/operating-model.md +15 -21
- package/docs/workflow/parallel-features.md +24 -3
- package/docs/workflow/skill-map.md +3 -1
- package/docs/workflow/zshrc-aliases.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.ts +1 -1
- package/scripts/lib/sandbox-path.sh +6 -162
- package/src/audits/baseline.ts +1 -1
- package/src/audits/run.ts +2 -2
- package/src/canvas/address.ts +12 -3
- package/src/canvas/capture.ts +1 -1
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +201 -171
- package/src/canvas/client/inspector/color-inputs.tsx +139 -0
- package/src/canvas/client/inspector/color-picker.tsx +668 -0
- package/src/canvas/client/inspector/color-space.ts +240 -0
- package/src/canvas/client/inspector/field.tsx +66 -3
- package/src/canvas/client/inspector/fields.css +473 -6
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/token-select.tsx +109 -0
- package/src/canvas/client/inspector/values.ts +117 -3
- package/src/canvas/client/inspector.tsx +311 -15
- package/src/canvas/client/keys.ts +54 -0
- package/src/canvas/client/selection.tsx +199 -0
- package/src/canvas/client/shell.css +210 -12
- package/src/canvas/client/state.ts +165 -14
- package/src/canvas/client/surface.tsx +341 -39
- package/src/canvas/client/tools.tsx +64 -0
- package/src/canvas/content.ts +51 -2
- package/src/canvas/edit.ts +49 -6
- package/src/canvas/editing.ts +203 -0
- package/src/canvas/global.d.ts +15 -0
- package/src/canvas/server.ts +149 -12
- package/src/canvas/tokens.ts +60 -18
- package/src/census/count.ts +1 -1
- package/src/claude/seeds.ts +3 -3
- package/src/claude/skills-audit.ts +28 -0
- package/src/claude/skills-drift.ts +1 -1
- package/src/claude/skills-families.ts +36 -0
- package/src/claude/skills-list.ts +76 -15
- package/src/claude/skills-reach.ts +1 -1
- package/src/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +180 -3
- package/src/commands/claude.ts +70 -12
- package/src/commands/design.ts +145 -5
- package/src/commands/docs.ts +1 -1
- package/src/commands/feedback.ts +2 -2
- package/src/commands/gate.ts +9 -2
- package/src/commands/gov.ts +3 -3
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +2 -2
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +2 -2
- package/src/commands/pr.ts +288 -65
- package/src/commands/records.ts +1 -1
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +64 -15
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +1 -1
- package/src/commands/sync.ts +2 -2
- package/src/commands/tasks.ts +21 -4
- package/src/commands/teach.ts +1 -1
- package/src/commands/tooling.ts +1 -1
- package/src/commands/transcripts.ts +2 -2
- package/src/commands/upgrade.ts +1 -1
- package/src/commands/wiki.ts +1 -1
- package/src/commands/worktrees.ts +1 -1
- package/src/comments/scan.ts +1 -1
- package/src/comments/trend.ts +1 -1
- package/src/context/architecture.ts +1 -1
- package/src/context/citations.ts +3 -3
- package/src/context/classify/extract.ts +3 -3
- package/src/context/folders.ts +2 -2
- package/src/context/requirements.ts +1 -1
- package/src/design/project-fonts.ts +392 -0
- package/src/docs/read.ts +1 -1
- package/src/exec.ts +1 -1
- package/src/feedback/github.ts +2 -2
- package/src/gate/measures.ts +48 -10
- package/src/gate/sequencer.ts +56 -4
- package/src/gate/stages.ts +17 -0
- package/src/gate/suite-lock.ts +214 -0
- package/src/{git-files.ts → git/files.ts} +2 -2
- package/src/{git-ignore.ts → git/ignore.ts} +1 -1
- package/src/{worktree.ts → git/worktree.ts} +1 -1
- package/src/gov/citations.ts +1 -1
- package/src/gov/install.ts +1 -1
- package/src/gov/superseded.ts +1 -1
- package/src/gov/test-order.ts +2 -2
- package/src/help.ts +1 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/indexes/walk.ts +1 -1
- package/src/intake/folder.ts +1 -1
- package/src/labels/audit.ts +1 -1
- package/src/labels/map.ts +1 -1
- package/src/labels/phase.ts +3 -3
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/records.ts +11 -11
- package/src/migrate/scratch-evidence.ts +1 -1
- package/src/migrate/surface-roots.ts +6 -6
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +250 -59
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/pr/tick.ts +56 -0
- package/src/process/harness.ts +1 -1
- package/src/records/backup.ts +2 -2
- package/src/records/ordinal.ts +1 -1
- package/src/records/prune.ts +1 -1
- package/src/records/size.ts +1 -1
- package/src/records/stale-canonical.ts +2 -2
- package/src/records/stale.ts +1 -1
- package/src/records/validate.ts +2 -2
- package/src/{project-root.ts → roots/project.ts} +1 -1
- package/src/{record-root.ts → roots/record.ts} +1 -1
- package/src/sandbox/coverage.ts +11 -12
- package/src/sandbox/equivalence.ts +57 -26
- package/src/sandbox/headless/dispatch.ts +264 -0
- package/src/sandbox/headless/run.ts +653 -0
- package/src/sandbox/headless/watch.ts +145 -0
- package/src/sandbox/hooks.ts +1 -1
- package/src/sandbox/provision.ts +90 -29
- package/src/sandbox/scenario.ts +588 -0
- package/src/sandbox/tree.ts +4 -8
- package/src/secrets/scan.ts +1 -1
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/live.ts +4 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +2 -2
- package/src/slides/decks.ts +1 -1
- package/src/slides/package/fonts.ts +6 -2
- package/src/standards/audit.ts +2 -2
- package/src/standards/read.ts +1 -1
- package/src/sync/check.ts +2 -2
- package/src/sync/engine.ts +3 -3
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/layout.ts +2 -2
- package/src/sync/reverse.ts +1 -1
- package/src/sync/stamp.ts +1 -1
- package/src/sync/target.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/targets/sweep.ts +2 -2
- package/src/{target.ts → targets/validate.ts} +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/label.ts +1 -1
- package/src/tasks/reach.ts +2 -2
- package/src/tasks/trunk.ts +1 -1
- package/src/tasks/validate.ts +38 -14
- package/src/teach/nav.ts +1 -1
- package/src/teach/workspace.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/read.ts +1 -1
- package/src/tooling/scan.ts +1 -1
- package/src/tooling/subfolder.ts +1 -1
- package/src/version/installed.ts +1 -1
- package/src/version/skew.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/board.md +3 -3
- package/standards/canvas.md +2 -1
- package/standards/plan.md +13 -7
- package/standards/skill.md +1 -0
- package/standards/wiki.md +12 -3
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
- package/tsconfig.json +1 -1
- package/scripts/lib/sandbox-dispatch.sh +0 -190
- /package/src/{client-commands.ts → gate/client-commands.ts} +0 -0
- /package/src/{git-base.ts → git/base.ts} +0 -0
- /package/src/{git-env.ts → git/env.ts} +0 -0
- /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
- /package/src/{surface-root.ts → roots/surface.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { join, relative } from 'node:path'
|
|
5
|
+
|
|
6
|
+
/** Path to content hash, one entry per regular file. */
|
|
7
|
+
export type Manifest = ReadonlyMap<string, string>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hashes every regular file under `base`, keyed by its path relative to it, for
|
|
11
|
+
* the top-level entries `keep` admits. A symlink is neither followed nor
|
|
12
|
+
* hashed, matching `find -type f`.
|
|
13
|
+
*/
|
|
14
|
+
function hashFiles(base: string, keep: (name: string) => boolean): Manifest {
|
|
15
|
+
const manifest = new Map<string, string>()
|
|
16
|
+
const pending = readdirSync(base, { withFileTypes: true })
|
|
17
|
+
.filter((entry) => keep(entry.name))
|
|
18
|
+
.map((entry) => ({ entry, dir: base }))
|
|
19
|
+
|
|
20
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
21
|
+
const path = join(next.dir, next.entry.name)
|
|
22
|
+
if (next.entry.isDirectory()) {
|
|
23
|
+
for (const entry of readdirSync(path, { withFileTypes: true }))
|
|
24
|
+
pending.push({ entry, dir: path })
|
|
25
|
+
} else if (next.entry.isFile()) {
|
|
26
|
+
const hash = createHash('sha1').update(readFileSync(path)).digest('hex')
|
|
27
|
+
manifest.set(relative(base, path), hash)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new Map([...manifest].sort(([a], [b]) => (a < b ? -1 : 1)))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Records a hash per file so the post-run comparison can name what the session
|
|
36
|
+
* wrote. A diff against `refs/sandbox/baseline` cannot stand in for this: an arm
|
|
37
|
+
* that sets SANDBOX_SKIP_AUTO_COMMIT leaves its last stage uncommitted, so the
|
|
38
|
+
* fixtures the harness staged would read as session writes.
|
|
39
|
+
*/
|
|
40
|
+
export function snapshotTree(dir: string): Manifest {
|
|
41
|
+
return hashFiles(dir, (name) => name !== '.git')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reports both sides of a change, so a deletion is a write. Reporting only the
|
|
46
|
+
* after side would let a file removed outside the declared scope produce no
|
|
47
|
+
* assertion at all, which is the one way this check stayed weaker than the
|
|
48
|
+
* permission scoping it replaced. A modified file differs on both sides and
|
|
49
|
+
* reports once.
|
|
50
|
+
*/
|
|
51
|
+
export function writesBetween(before: Manifest, after: Manifest): string[] {
|
|
52
|
+
const paths = new Set([...before.keys(), ...after.keys()])
|
|
53
|
+
|
|
54
|
+
return [...paths]
|
|
55
|
+
.filter((path) => before.get(path) !== after.get(path))
|
|
56
|
+
.sort()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The roots a run must not write shared session scratch to. `snapshotTree`
|
|
61
|
+
* reads the sandbox alone, so before this a session that resolved that scratch
|
|
62
|
+
* against a toolkit root wrote where no manifest looked: the run reported
|
|
63
|
+
* success, the write list came back empty, and `write_scope` had nothing to
|
|
64
|
+
* assert against.
|
|
65
|
+
*
|
|
66
|
+
* It makes an escape visible rather than impossible. A session can write to a
|
|
67
|
+
* home directory, to a sibling worktree, or to any path outside these roots, and
|
|
68
|
+
* none of that is watched. The standing limits in
|
|
69
|
+
* `canon/context/sandbox/overview.md` carry what stays invisible.
|
|
70
|
+
*
|
|
71
|
+
* A linked worktree is the normal place to develop this repository, and the
|
|
72
|
+
* rule an escaping session follows sends scratch to the main root rather than to
|
|
73
|
+
* the worktree it was launched from, so the main root joins the list whenever it
|
|
74
|
+
* differs.
|
|
75
|
+
*/
|
|
76
|
+
export function escapeRoots(projectRoot: string): string[] {
|
|
77
|
+
const listing = spawnSync('git', ['worktree', 'list', '--porcelain'], {
|
|
78
|
+
cwd: projectRoot,
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
})
|
|
81
|
+
const line = (listing.stdout ?? '')
|
|
82
|
+
.split('\n')
|
|
83
|
+
.find((l) => l.startsWith('worktree '))
|
|
84
|
+
const mainRoot = line?.slice('worktree '.length)
|
|
85
|
+
|
|
86
|
+
return mainRoot === undefined || mainRoot === '' || mainRoot === projectRoot
|
|
87
|
+
? [projectRoot]
|
|
88
|
+
: [projectRoot, mainRoot]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The four directories the seed's shared-scratch rule names, and the whole of
|
|
93
|
+
* where an escape lands. A session that resolves that rule against a toolkit
|
|
94
|
+
* root writes here and nowhere else, so watching these rather than the whole
|
|
95
|
+
* root costs no detection.
|
|
96
|
+
*
|
|
97
|
+
* It buys the difference between a signal and noise. Nothing distinguishes the
|
|
98
|
+
* spawned session's writes from the operator's own, so a watch over the whole
|
|
99
|
+
* root reports ordinary editing during a run as an escape. Measured 2026-08-02:
|
|
100
|
+
* three runs against the relocated sandbox all wrote their report to the right
|
|
101
|
+
* place, and two of the three still reported an escape under the wide watch,
|
|
102
|
+
* naming a context entry being edited in the session that launched them.
|
|
103
|
+
*
|
|
104
|
+
* The scope stated for an arm author: `escapeRoots` times two, these times four.
|
|
105
|
+
* This watch reaches nothing past those eight destinations, so a nested run's
|
|
106
|
+
* own escape is invisible here whether or not an arm declares `escape_scope`. A
|
|
107
|
+
* nested background dispatch reports through `sessions` instead, from
|
|
108
|
+
* `dispatch.ts`, so the key defined against this file-write watch keeps one
|
|
109
|
+
* meaning. An arm whose skill is meant to reach past the sandbox tree declares
|
|
110
|
+
* `escape_scope` in its `expect.toml`, which bounds a legitimate write rather
|
|
111
|
+
* than the run. Read `canon/context/sandbox/isolation.md` and
|
|
112
|
+
* `canon/context/sandbox/coverage/workflow-arms.md` before writing a claim past
|
|
113
|
+
* that.
|
|
114
|
+
*/
|
|
115
|
+
export const ESCAPE_SCRATCH_DIRS = [
|
|
116
|
+
'.canon/plans',
|
|
117
|
+
'.canon/review',
|
|
118
|
+
'.canon/memory',
|
|
119
|
+
'.canon/tasks',
|
|
120
|
+
] as const
|
|
121
|
+
|
|
122
|
+
export interface RootSnapshot {
|
|
123
|
+
readonly manifest: Manifest
|
|
124
|
+
/**
|
|
125
|
+
* The root held at least one of the four directories. A root with none of
|
|
126
|
+
* them contributes no manifest and no diff, which reads identically to a watch
|
|
127
|
+
* that ran clean, so `checkEscapeScope` needs this to tell "watched and clean"
|
|
128
|
+
* from "nothing to watch" apart.
|
|
129
|
+
*/
|
|
130
|
+
readonly isWatched: boolean
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function snapshotRoot(dir: string): RootSnapshot {
|
|
134
|
+
const targets = ESCAPE_SCRATCH_DIRS.filter((sub) =>
|
|
135
|
+
existsSync(join(dir, sub)),
|
|
136
|
+
)
|
|
137
|
+
if (targets.length === 0) return { manifest: new Map(), isWatched: false }
|
|
138
|
+
|
|
139
|
+
const manifest = new Map<string, string>()
|
|
140
|
+
for (const sub of targets)
|
|
141
|
+
for (const [path, hash] of hashFiles(join(dir, sub), () => true))
|
|
142
|
+
manifest.set(`${sub}/${path}`, hash)
|
|
143
|
+
|
|
144
|
+
return { manifest, isWatched: true }
|
|
145
|
+
}
|
package/src/sandbox/hooks.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
2
2
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
|
-
import { PROJECT_ROOT } from '@/project
|
|
5
|
+
import { PROJECT_ROOT } from '@/roots/project'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The bash entry a scenario's hooks run through. Scenarios and the library they
|
package/src/sandbox/provision.ts
CHANGED
|
@@ -4,13 +4,21 @@ import {
|
|
|
4
4
|
copyFileSync,
|
|
5
5
|
existsSync,
|
|
6
6
|
mkdirSync,
|
|
7
|
-
readdirSync,
|
|
8
7
|
rmSync,
|
|
9
|
-
statSync,
|
|
10
8
|
writeFileSync,
|
|
11
9
|
} from 'node:fs'
|
|
12
10
|
import { basename, dirname, join, relative } from 'node:path'
|
|
13
11
|
import { probeScenario, stageScenario } from '@/sandbox/hooks'
|
|
12
|
+
import {
|
|
13
|
+
FIXTURE_SUFFIX,
|
|
14
|
+
FIXTURES_DIR,
|
|
15
|
+
listFixtureFiles,
|
|
16
|
+
loadScenario,
|
|
17
|
+
resolveScenarioFile,
|
|
18
|
+
type ScenarioDefinition,
|
|
19
|
+
scenarioExports,
|
|
20
|
+
stageScenarioInProcess,
|
|
21
|
+
} from '@/sandbox/scenario'
|
|
14
22
|
import { assertSandboxDirSafe, sandboxRoots, sandboxTree } from '@/sandbox/tree'
|
|
15
23
|
import {
|
|
16
24
|
intro,
|
|
@@ -26,16 +34,16 @@ import {
|
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
36
|
* Provisions one scenario into the sandbox tree, and resets or cleans that tree.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
37
|
+
* A bash scenario's hooks run through `sandbox-hook.sh` and a TypeScript one's
|
|
38
|
+
* arm runs in process through `scenario.ts`, and everything between them runs
|
|
39
|
+
* here, in the order the retired bash dispatcher held.
|
|
31
40
|
*
|
|
32
41
|
* Narration is part of the contract. `canon sandbox equivalence` compares the
|
|
33
42
|
* log line for line against the bash harness, so every message, its frame
|
|
34
43
|
* glyph, and its position in the order stays as that harness wrote it.
|
|
35
44
|
*/
|
|
36
45
|
|
|
37
|
-
const ANCHOR_FIXTURE_PARTS = ['sandbox',
|
|
38
|
-
const FIXTURE_SUFFIX = '.fixture'
|
|
46
|
+
const ANCHOR_FIXTURE_PARTS = ['sandbox', FIXTURES_DIR, 'anchor', 'create']
|
|
39
47
|
const SKILL_PATHSPEC = 'claude/skills/**/SKILL.md'
|
|
40
48
|
|
|
41
49
|
/**
|
|
@@ -98,15 +106,6 @@ function finish(message: string): void {
|
|
|
98
106
|
process.stderr.write(`\n${GREEN}${message}${NC}\n`)
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
function listFixtureFiles(dir: string): string[] {
|
|
102
|
-
if (!existsSync(dir)) return []
|
|
103
|
-
|
|
104
|
-
return (readdirSync(dir, { recursive: true, encoding: 'utf8' }) as string[])
|
|
105
|
-
.map((path) => join(dir, path))
|
|
106
|
-
.filter((path) => statSync(path).isFile())
|
|
107
|
-
.sort()
|
|
108
|
-
}
|
|
109
|
-
|
|
110
109
|
function stageAnchorTree(
|
|
111
110
|
root: string,
|
|
112
111
|
sandbox: string,
|
|
@@ -273,12 +272,22 @@ function setupAssets(
|
|
|
273
272
|
* tree, so a scenario exercises the branch's skill rather than the installed
|
|
274
273
|
* plugin's. The merge base against `origin/main` comes first, so a local `main`
|
|
275
274
|
* trailing the remote does not pull in skills other merged branches changed.
|
|
275
|
+
* A headless run skips it, since a copied body alone shadows the plugin's whole
|
|
276
|
+
* skill folder and its relative citations then resolve to nothing.
|
|
276
277
|
*/
|
|
277
278
|
export function injectChangedSkills(
|
|
278
279
|
root: string,
|
|
279
280
|
sandbox: string,
|
|
280
281
|
isSkipAutoCommit: boolean,
|
|
282
|
+
isSkipInject = false,
|
|
281
283
|
): string[] {
|
|
284
|
+
if (isSkipInject) {
|
|
285
|
+
logInfo(
|
|
286
|
+
'Skipped dev skill injection: the run resolves skills through --plugin-dir',
|
|
287
|
+
)
|
|
288
|
+
return []
|
|
289
|
+
}
|
|
290
|
+
|
|
282
291
|
const base = (
|
|
283
292
|
readGit(root, ['merge-base', 'HEAD', 'origin/main']) ??
|
|
284
293
|
readGit(root, ['merge-base', 'HEAD', 'main']) ??
|
|
@@ -392,12 +401,57 @@ interface Scenario {
|
|
|
392
401
|
readonly arm: string | undefined
|
|
393
402
|
}
|
|
394
403
|
|
|
395
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Reads what a scenario declares ahead of provisioning: its config exports and
|
|
406
|
+
* whether it stages from the anchor. A bash scenario answers through the probe,
|
|
407
|
+
* and a TypeScript one from its module, which is also what its stage runs.
|
|
408
|
+
*/
|
|
409
|
+
function declareScenario(
|
|
410
|
+
root: string,
|
|
411
|
+
scenario: Scenario,
|
|
412
|
+
env: NodeJS.ProcessEnv,
|
|
413
|
+
):
|
|
414
|
+
| { kind: 'sh'; file: string; isAnchor: boolean }
|
|
415
|
+
| { kind: 'ts'; definition: ScenarioDefinition; isAnchor: boolean } {
|
|
416
|
+
const name = `${scenario.category}/${scenario.command}`
|
|
417
|
+
const resolved = resolveScenarioFile(
|
|
418
|
+
join(root, 'sandbox'),
|
|
419
|
+
scenario.category,
|
|
420
|
+
scenario.command,
|
|
421
|
+
)
|
|
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
|
+
if (!resolved.ok) fail(`Sandbox script not found: ${name}`)
|
|
427
|
+
|
|
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
|
+
let definition: ScenarioDefinition
|
|
437
|
+
try {
|
|
438
|
+
definition = loadScenario(resolved.file)
|
|
439
|
+
} catch (error) {
|
|
440
|
+
fail(
|
|
441
|
+
`Could not load scenario ${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
Object.assign(env, scenarioExports(definition, root, env))
|
|
445
|
+
|
|
446
|
+
return { kind: 'ts', definition, isAnchor: definition.anchor === true }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function provisionScenario(
|
|
396
450
|
root: string,
|
|
397
451
|
sandbox: string,
|
|
398
452
|
scenario: Scenario,
|
|
399
453
|
baseEnv: NodeJS.ProcessEnv,
|
|
400
|
-
): void {
|
|
454
|
+
): Promise<void> {
|
|
401
455
|
const env: NodeJS.ProcessEnv = { ...baseEnv }
|
|
402
456
|
if (scenario.arm !== undefined) {
|
|
403
457
|
env.SANDBOX_SCENARIO = scenario.arm
|
|
@@ -405,13 +459,7 @@ function provisionScenario(
|
|
|
405
459
|
}
|
|
406
460
|
|
|
407
461
|
const sandboxDir = join(root, 'sandbox')
|
|
408
|
-
const
|
|
409
|
-
if (!existsSync(file) || !statSync(file).isFile())
|
|
410
|
-
fail(`Sandbox script not found: ${scenario.category}/${scenario.command}`)
|
|
411
|
-
|
|
412
|
-
const probe = probeScenario(file, env)
|
|
413
|
-
if (!probe.ok) throw new Halt(probe.status)
|
|
414
|
-
Object.assign(env, probe.exports)
|
|
462
|
+
const declared = declareScenario(root, scenario, env)
|
|
415
463
|
|
|
416
464
|
if (!existsSync(sandboxDir))
|
|
417
465
|
fail(`Sandbox directory not found at: ${sandboxDir}`)
|
|
@@ -425,20 +473,33 @@ function provisionScenario(
|
|
|
425
473
|
}
|
|
426
474
|
|
|
427
475
|
logStep(`Provisioning ${scenario.category}:${scenario.command}`)
|
|
428
|
-
if (
|
|
476
|
+
if (declared.isAnchor) stageAnchorTree(root, sandbox, env)
|
|
429
477
|
else initEmptyTree(sandbox, env)
|
|
430
478
|
configureCredentials(sandbox, env)
|
|
431
479
|
setupAssets(root, sandbox, env)
|
|
432
480
|
|
|
433
481
|
// A scenario that execs a verb as its last step hands the run to that verb,
|
|
434
|
-
// whose own frame is the last one written, so nothing follows it here.
|
|
435
|
-
|
|
482
|
+
// whose own frame is the last one written, so nothing follows it here. An
|
|
483
|
+
// in-process arm writes its exports straight into `env`.
|
|
484
|
+
const staged =
|
|
485
|
+
declared.kind === 'sh'
|
|
486
|
+
? stageScenario(declared.file, env)
|
|
487
|
+
: {
|
|
488
|
+
...(await stageScenarioInProcess(declared.definition, {
|
|
489
|
+
root,
|
|
490
|
+
dir: sandbox,
|
|
491
|
+
env,
|
|
492
|
+
arm: scenario.arm,
|
|
493
|
+
})),
|
|
494
|
+
exports: {},
|
|
495
|
+
}
|
|
436
496
|
if (staged.ending === 'replaced') throw new Halt(staged.status, false)
|
|
437
497
|
if (staged.ending === 'exited') throw new Halt(staged.status)
|
|
438
498
|
Object.assign(env, staged.exports)
|
|
439
499
|
|
|
440
500
|
const isSkipAutoCommit = (env.SANDBOX_SKIP_AUTO_COMMIT ?? '') !== ''
|
|
441
|
-
|
|
501
|
+
const isSkipInject = (env.SANDBOX_SKIP_SKILL_INJECT ?? '') !== ''
|
|
502
|
+
injectChangedSkills(root, sandbox, isSkipAutoCommit, isSkipInject)
|
|
442
503
|
must(commitScenarioChanges(sandbox, isSkipAutoCommit, env))
|
|
443
504
|
tagBaseline(sandbox, env)
|
|
444
505
|
|
|
@@ -579,7 +640,7 @@ export async function runSandbox(
|
|
|
579
640
|
}
|
|
580
641
|
|
|
581
642
|
const { category, command } = parseTarget(request.target)
|
|
582
|
-
provisionScenario(
|
|
643
|
+
await provisionScenario(
|
|
583
644
|
root,
|
|
584
645
|
sandbox,
|
|
585
646
|
{ category, command, arm: request.arm },
|