@erclx/canon 5.25.0 → 5.27.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 +4 -3
- 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 +14 -1
- 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/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +9 -21
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- 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/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +6 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +23 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +3 -1
- 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 +3 -1
- package/claude/skills/draft-ready/SKILL.md +2 -0
- package/claude/skills/draft-screencast/SKILL.md +11 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +33 -20
- 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-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +14 -6
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +21 -8
- 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/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -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/SKILL.md +3 -7
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- 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 +2 -2
- 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/SKILL.md +3 -1
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/SKILL.md +5 -14
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
- 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-watch.md +26 -0
- package/claude/skills/role-planner/SKILL.md +2 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +2 -0
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +5 -1
- package/claude/skills/search-craft/references/adopted.md +30 -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/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -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 +4 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- 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 +3 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +24 -6
- package/docs/agents/capture.md +6 -6
- package/docs/agents/commands.md +5 -5
- package/docs/agents/index.md +5 -5
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +38 -15
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +29 -5
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +11 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/operating-model.md +5 -6
- package/docs/workflow/parallel-features.md +23 -2
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +3 -34
- 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-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +6 -162
- package/scripts/sandbox-hook.sh +99 -0
- package/src/audits/run.ts +1 -1
- package/src/canvas/address.ts +12 -3
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +398 -0
- 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 +5 -1
- package/src/canvas/client/inspector/fields.css +547 -17
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/values.ts +166 -1
- package/src/canvas/client/inspector.tsx +236 -73
- package/src/canvas/client/selection.tsx +197 -0
- package/src/canvas/client/shell.css +104 -12
- package/src/canvas/client/state.ts +69 -9
- package/src/canvas/client/surface.tsx +147 -22
- package/src/canvas/content.ts +50 -1
- package/src/canvas/edit.ts +37 -4
- 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 +59 -17
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +2 -2
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +2 -2
- package/src/claude/skills-audit.ts +96 -14
- 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/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +179 -2
- package/src/commands/claude.ts +94 -11
- package/src/commands/design.ts +142 -2
- package/src/commands/feedback.ts +3 -3
- package/src/commands/gov.ts +1 -1
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +105 -58
- package/src/commands/records.ts +89 -7
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +103 -11
- package/src/commands/sessions.ts +1 -1
- package/src/commands/slides.ts +31 -77
- package/src/commands/sync.ts +1 -1
- package/src/commands/tasks.ts +2 -2
- package/src/commands/teach.ts +1 -1
- package/src/commands/transcripts.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/audit.ts +1 -1
- package/src/context/citations.ts +1 -1
- package/src/context/classify/extract.ts +2 -2
- package/src/design/project-fonts.ts +392 -0
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +2 -2
- package/src/{client-commands.ts → gate/client-commands.ts} +1 -1
- package/src/gate/measures.ts +53 -13
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +9 -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 +2 -2
- package/src/gov/install.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +3 -3
- 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/labels/audit.ts +1 -1
- package/src/markdown/ceiling.ts +1 -1
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +96 -43
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +2 -1
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- 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 +128 -0
- package/src/sandbox/provision.ts +607 -0
- package/src/sandbox/tree.ts +99 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +2 -2
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- 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 +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/reverse.ts +1 -1
- package/src/sync/seeds-report.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/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/trunk.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +2 -2
- package/src/tooling/subfolder.ts +1 -1
- package/src/web/readme-citations.ts +2 -2
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +3 -2
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +2 -0
- package/standards/wiki.md +12 -3
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
- package/scripts/lib/sandbox-dispatch.sh +0 -190
- package/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.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/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
- /package/src/{target.ts → targets/validate.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.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
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The bash entry a scenario's hooks run through. Scenarios and the library they
|
|
9
|
+
* call stay bash, so this contract is the whole of what the TypeScript harness
|
|
10
|
+
* knows about one: which hooks it declares and what `use_config` exports.
|
|
11
|
+
*/
|
|
12
|
+
const HOOK_SCRIPT = join(PROJECT_ROOT, 'scripts', 'sandbox-hook.sh')
|
|
13
|
+
|
|
14
|
+
const ANCHOR_RECORD = '@anchor='
|
|
15
|
+
|
|
16
|
+
export interface Probe {
|
|
17
|
+
readonly exports: Readonly<Record<string, string>>
|
|
18
|
+
readonly isAnchor: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ProbeResult =
|
|
22
|
+
| ({ readonly ok: true } & Probe)
|
|
23
|
+
| { readonly ok: false; readonly status: number }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reads the NUL-separated `NAME=value` pairs the probe prints. NUL rather than a
|
|
27
|
+
* newline, since an exported value can hold one. The anchor record closes every
|
|
28
|
+
* well-formed probe, so output missing it is a probe that did not finish.
|
|
29
|
+
*/
|
|
30
|
+
export function parseProbe(stdout: string): Probe {
|
|
31
|
+
const { exports, anchor } = parsePairs(stdout)
|
|
32
|
+
if (anchor === undefined)
|
|
33
|
+
throw new Error('the probe printed no anchor record')
|
|
34
|
+
|
|
35
|
+
return { exports, isAnchor: anchor === '1' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parsePairs(text: string): {
|
|
39
|
+
exports: Record<string, string>
|
|
40
|
+
anchor: string | undefined
|
|
41
|
+
} {
|
|
42
|
+
const exports: Record<string, string> = {}
|
|
43
|
+
let anchor: string | undefined
|
|
44
|
+
|
|
45
|
+
for (const record of text.split('\0')) {
|
|
46
|
+
if (record.startsWith(ANCHOR_RECORD)) {
|
|
47
|
+
anchor = record.slice(ANCHOR_RECORD.length)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
const split = record.indexOf('=')
|
|
51
|
+
if (split > 0) exports[record.slice(0, split)] = record.slice(split + 1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { exports, anchor }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Runs `use_config` and `use_anchor` in a child and returns what they exported.
|
|
59
|
+
* Stderr is inherited by default, so a hook that fails narrates into the same
|
|
60
|
+
* frame the harness writes. A non-zero status passes through rather than being
|
|
61
|
+
* flattened, since the harness exits with it.
|
|
62
|
+
*/
|
|
63
|
+
export function probeScenario(
|
|
64
|
+
file: string,
|
|
65
|
+
env: NodeJS.ProcessEnv,
|
|
66
|
+
stderr: 'inherit' | 'pipe' = 'inherit',
|
|
67
|
+
): ProbeResult {
|
|
68
|
+
const result = spawnSync('bash', [HOOK_SCRIPT, 'probe', file], {
|
|
69
|
+
env,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
stdio: ['ignore', 'pipe', stderr],
|
|
72
|
+
})
|
|
73
|
+
const status = result.status ?? 1
|
|
74
|
+
if (status !== 0) return { ok: false, status }
|
|
75
|
+
|
|
76
|
+
return { ok: true, ...parseProbe(result.stdout) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How `stage_setup` ended. The retired dispatcher ran it in its own shell, so
|
|
81
|
+
* an `exit` there ended provisioning through the frame-closing trap, and an
|
|
82
|
+
* `exec` replaced the harness outright, leaving its program's frame as the last
|
|
83
|
+
* one written. The harness reproduces both from this.
|
|
84
|
+
*/
|
|
85
|
+
export type StageEnding = 'returned' | 'exited' | 'replaced'
|
|
86
|
+
|
|
87
|
+
export interface StageResult {
|
|
88
|
+
readonly status: number
|
|
89
|
+
readonly ending: StageEnding
|
|
90
|
+
readonly exports: Readonly<Record<string, string>>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const EXITED_RECORD = '@exited'
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Runs `stage_setup` inside the tree with every stream inherited, so a scenario
|
|
97
|
+
* prompting through `select_or_route_scenario` still reaches a terminal and its
|
|
98
|
+
* narration lands in order with the harness's own.
|
|
99
|
+
*
|
|
100
|
+
* The exports `stage_setup` changed come back through a file rather than a
|
|
101
|
+
* pipe. Stdout belongs to the scenario, and a background process a scenario
|
|
102
|
+
* starts would hold an extra pipe open past the child's exit.
|
|
103
|
+
*/
|
|
104
|
+
export function stageScenario(
|
|
105
|
+
file: string,
|
|
106
|
+
env: NodeJS.ProcessEnv,
|
|
107
|
+
stdio: 'inherit' | 'ignore' = 'inherit',
|
|
108
|
+
): StageResult {
|
|
109
|
+
const scratch = mkdtempSync(join(tmpdir(), 'canon-sandbox-hook-'))
|
|
110
|
+
const report = join(scratch, 'exports')
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const result = spawnSync('bash', [HOOK_SCRIPT, 'stage', file, report], {
|
|
114
|
+
env,
|
|
115
|
+
stdio,
|
|
116
|
+
})
|
|
117
|
+
const status = result.status ?? 1
|
|
118
|
+
if (!existsSync(report)) return { status, ending: 'replaced', exports: {} }
|
|
119
|
+
|
|
120
|
+
const text = readFileSync(report, 'utf8')
|
|
121
|
+
if (text.split('\0').includes(EXITED_RECORD))
|
|
122
|
+
return { status, ending: 'exited', exports: {} }
|
|
123
|
+
|
|
124
|
+
return { status, ending: 'returned', exports: parsePairs(text).exports }
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
127
|
+
}
|
|
128
|
+
}
|