@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.
Files changed (210) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +1 -1
  3. package/claude/skills/auto-ship/references/worktree-name.md +1 -1
  4. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  5. package/claude/skills/backlog-triage/SKILL.md +1 -1
  6. package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
  7. package/claude/skills/canon-cli/SKILL.md +75 -32
  8. package/claude/skills/canon-feedback/SKILL.md +1 -1
  9. package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
  10. package/claude/skills/{canvas → canvas-board}/SKILL.md +2 -2
  11. package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
  12. package/claude/skills/context-fold/REQUIREMENT.md +25 -33
  13. package/claude/skills/context-fold/SKILL.md +25 -70
  14. package/claude/skills/context-fold/references/classify.md +3 -3
  15. package/claude/skills/context-fold/references/completion-report.md +4 -4
  16. package/claude/skills/context-fold/references/context-refresh.md +5 -5
  17. package/{standards → claude/skills/context-fold/references}/context.md +16 -5
  18. package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
  19. package/claude/skills/create-rule/SKILL.md +2 -2
  20. package/{standards → claude/skills/create-rule/references}/rule.md +12 -5
  21. package/claude/skills/create-skill/SKILL.md +2 -2
  22. package/{standards → claude/skills/create-skill/references}/skill.md +17 -14
  23. package/claude/skills/design-taste/REQUIREMENT.md +3 -3
  24. package/claude/skills/design-taste/SKILL.md +1 -1
  25. package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
  26. package/claude/skills/docs-sync/SKILL.md +3 -3
  27. package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
  28. package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
  29. package/claude/skills/draft-and-pick/SKILL.md +5 -5
  30. package/claude/skills/draft-doc/SKILL.md +6 -6
  31. package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
  32. package/claude/skills/draft-figure/SKILL.md +2 -2
  33. package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
  34. package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
  35. package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
  36. package/claude/skills/draft-identity/SKILL.md +2 -2
  37. package/claude/skills/draft-ready/SKILL.md +1 -1
  38. package/claude/skills/git-branch/SKILL.md +1 -1
  39. package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
  40. package/claude/skills/git-commit/SKILL.md +2 -2
  41. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  42. package/claude/skills/git-followup/SKILL.md +1 -1
  43. package/claude/skills/git-issue/SKILL.md +1 -1
  44. package/claude/skills/git-pr/SKILL.md +13 -13
  45. package/claude/skills/git-pr/references/evidence.md +2 -2
  46. package/claude/skills/git-pr/references/final-command.md +38 -14
  47. package/claude/skills/git-pr/references/labels.md +5 -1
  48. package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
  49. package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
  50. package/claude/skills/git-pr/references/task-number.md +1 -1
  51. package/claude/skills/git-ship/SKILL.md +15 -14
  52. package/claude/skills/git-split/SKILL.md +3 -3
  53. package/claude/skills/git-stage/SKILL.md +1 -1
  54. package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
  55. package/claude/skills/review-address/SKILL.md +1 -1
  56. package/claude/skills/review-address/references/post-review-findings.md +1 -1
  57. package/claude/skills/review-pr/REQUIREMENT.md +1 -1
  58. package/claude/skills/review-pr/SKILL.md +3 -3
  59. package/claude/skills/review-pr/references/body-variants.md +2 -2
  60. package/claude/skills/review-pr/references/key-changes.md +3 -3
  61. package/claude/skills/review-pr/references/post.md +4 -2
  62. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  63. package/claude/skills/review-ui/SKILL.md +1 -1
  64. package/claude/skills/review-ui/references/post.md +1 -1
  65. package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
  66. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  67. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
  68. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
  69. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +23 -1
  70. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
  71. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
  72. package/claude/skills/role-planner/REQUIREMENT.md +2 -1
  73. package/claude/skills/role-planner/SKILL.md +13 -2
  74. package/claude/skills/role-reviewer/SKILL.md +1 -1
  75. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  76. package/claude/skills/role-worker/SKILL.md +9 -1
  77. package/claude/skills/session-map/SKILL.md +1 -1
  78. package/claude/skills/session-resume/REQUIREMENT.md +2 -2
  79. package/claude/skills/session-worktree/SKILL.md +5 -5
  80. package/claude/skills/target-setup/references/detect.md +1 -1
  81. package/claude/skills/task-board/REQUIREMENT.md +7 -12
  82. package/claude/skills/task-board/SKILL.md +5 -17
  83. package/claude/skills/task-board/references/tasks.md +2 -2
  84. package/claude/skills/task-complete/REQUIREMENT.md +47 -0
  85. package/claude/skills/task-complete/SKILL.md +86 -0
  86. package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
  87. package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
  88. package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
  89. package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
  90. package/claude/skills/teach-workspace/SKILL.md +8 -4
  91. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
  92. package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
  93. package/claude/skills/teach-workspace/references/promotion.md +3 -3
  94. package/claude/skills/ui-checklist/SKILL.md +1 -1
  95. package/claude/skills/ux-audit/SKILL.md +1 -1
  96. package/claude/skills/ux-measure/SKILL.md +1 -1
  97. package/docs/agents/canvas.md +1 -1
  98. package/docs/agents/commands.md +3 -2
  99. package/docs/agents/context-audit-checks.md +4 -4
  100. package/docs/agents/index.md +3 -3
  101. package/docs/agents/indexes.md +1 -1
  102. package/docs/agents/install-and-sync.md +6 -11
  103. package/docs/agents/key-changes.md +5 -5
  104. package/docs/agents/label-coverage.md +11 -0
  105. package/docs/agents/migrate.md +1 -1
  106. package/docs/agents/records-push.md +2 -2
  107. package/docs/agents/records-stale.md +12 -47
  108. package/docs/agents/records-validate.md +3 -7
  109. package/docs/agents/records.md +5 -27
  110. package/docs/agents/rule-citations.md +1 -1
  111. package/docs/agents/sandbox.md +4 -4
  112. package/docs/agents/skills-audit.md +1 -1
  113. package/docs/agents/tasks-plans.md +2 -2
  114. package/docs/agents/tasks.md +2 -2
  115. package/docs/target/migrations.md +10 -2
  116. package/docs/target/projects.md +1 -1
  117. package/docs/target/sync.md +1 -1
  118. package/docs/workflow/operating-model.md +2 -2
  119. package/docs/workflow/parallel-features.md +7 -7
  120. package/docs/workflow/skill-map-on-request.md +9 -9
  121. package/docs/workflow/skill-map.md +12 -11
  122. package/docs/workflow/visual-design-workflow.md +2 -2
  123. package/governance/renames.toml +0 -11
  124. package/governance/rules/canon/context.md +13 -0
  125. package/governance/rules/claude/skill.md +17 -0
  126. package/governance/stacks/base.toml +2 -4
  127. package/package.json +4 -4
  128. package/scripts/core/install-check.sh +3 -3
  129. package/scripts/lib/ui.sh +0 -159
  130. package/src/audits/catalog.ts +0 -1
  131. package/src/claude/cases/misc.ts +1 -1
  132. package/src/claude/cases/setup.ts +4 -0
  133. package/src/claude/cases/workflow.ts +6 -1
  134. package/src/claude/seeds.ts +1 -1
  135. package/src/claude/skills-audit.ts +2 -2
  136. package/src/cli/help.ts +3 -1
  137. package/src/cli/ui.ts +2 -3
  138. package/src/commands/claude.ts +1 -1
  139. package/src/commands/gov.ts +2 -15
  140. package/src/commands/labels.ts +326 -7
  141. package/src/commands/migrate.ts +6 -1
  142. package/src/commands/pr.ts +1 -1
  143. package/src/commands/records.ts +13 -399
  144. package/src/commands/sandbox.ts +3 -6
  145. package/src/commands/tasks.ts +3 -3
  146. package/src/context/audit.ts +5 -5
  147. package/src/context/folders.ts +1 -1
  148. package/src/design/budget.ts +1 -1
  149. package/src/design/components.ts +1 -1
  150. package/src/design/document.ts +1 -1
  151. package/src/design/regen.ts +1 -1
  152. package/src/design/render.ts +1 -1
  153. package/src/gate/measures.ts +1 -1
  154. package/src/gate/sequencer.ts +56 -1
  155. package/src/gate/stages.ts +1 -7
  156. package/src/gov/install.ts +2 -3
  157. package/src/gov/stacks.ts +1 -1
  158. package/src/hooks/post-merge.ts +1 -1
  159. package/src/labels/apply.ts +39 -0
  160. package/src/labels/format.ts +3 -3
  161. package/src/labels/phase.ts +3 -3
  162. package/src/labels/sections.ts +43 -0
  163. package/src/migrate/skill-names.ts +20 -1
  164. package/src/pr/paths.ts +4 -4
  165. package/src/records/backup.ts +16 -6
  166. package/src/records/stale-canonical.ts +31 -1
  167. package/src/records/validate.ts +2 -188
  168. package/src/sandbox/census.ts +1 -1
  169. package/src/sandbox/coverage.ts +2 -6
  170. package/src/sandbox/equivalence.ts +5 -44
  171. package/src/sandbox/expect.ts +4 -4
  172. package/src/sandbox/provision.ts +13 -36
  173. package/src/sandbox/scenario.ts +26 -48
  174. package/src/sandbox/tree.ts +2 -6
  175. package/src/standards/read.ts +14 -1
  176. package/src/sync/git.ts +1 -1
  177. package/src/tasks/archive.ts +5 -5
  178. package/src/tasks/branch.ts +3 -3
  179. package/src/tasks/label.ts +1 -1
  180. package/src/tasks/record.ts +2 -2
  181. package/src/tasks/validate.ts +1 -1
  182. package/standards/index.md +0 -13
  183. package/standards/standard.md +1 -7
  184. package/tooling/base/reference.md +1 -1
  185. package/tooling/base/seeds/.github/pull_request_template.md +2 -2
  186. package/tooling/claude/reference.md +1 -1
  187. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
  188. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
  189. package/tooling/claude/seeds/.claude/settings.json +9 -0
  190. package/governance/rules/standards/context.md +0 -14
  191. package/governance/rules/standards/design.md +0 -12
  192. package/governance/rules/standards/readme.md +0 -12
  193. package/governance/rules/standards/rule.md +0 -13
  194. package/governance/rules/standards/skill.md +0 -21
  195. package/governance/rules/standards/standard.md +0 -13
  196. package/scripts/config.sh +0 -11
  197. package/scripts/lib/gov.sh +0 -14
  198. package/scripts/lib/sandbox-fixtures.sh +0 -191
  199. package/scripts/lib/sandbox-git.sh +0 -125
  200. package/scripts/lib/sandbox-path.sh +0 -50
  201. package/scripts/sandbox-hook.sh +0 -99
  202. package/src/gov/standard-rules.ts +0 -179
  203. package/src/records/migrate.ts +0 -78
  204. package/src/records/stale.ts +0 -243
  205. package/src/sandbox/hooks.ts +0 -128
  206. /package/claude/skills/{canvas → canvas-board}/references/canvas.md +0 -0
  207. /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
  208. /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
  209. /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
  210. /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
@@ -1,191 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- # Fixture files carry this suffix so the repository's own checks ignore them.
4
- # A stored `index.md.fixture` is not an `index.md`, so the index walker, prettier,
5
- # shfmt, and shellcheck all skip it and the bytes reach the sandbox unmodified.
6
- FIXTURE_SUFFIX=".fixture"
7
-
8
- # The tree mirrors the scenario path, so `category` and `scenario` together match
9
- # `sandbox/<category>/<scenario>.sh`. Both segments are load-bearing:
10
- # four scenario basenames repeat across categories (`claude`, `docs`, `review`,
11
- # `sync`), and `docs` is also a category of its own.
12
- fixture_stage_dir() {
13
- local category="$1"
14
- local scenario="$2"
15
- local arm="$3"
16
- local stage="$4"
17
- echo "$PROJECT_ROOT/sandbox/fixtures/$category/$scenario/$arm/$stage"
18
- }
19
-
20
- list_fixture_files() {
21
- local source_dir="$1"
22
- find "$source_dir" -type f -name "*$FIXTURE_SUFFIX" -print0 | sort -z
23
- }
24
-
25
- count_fixture_files() {
26
- local source_dir="$1"
27
- if [ ! -d "$source_dir" ]; then
28
- echo 0
29
- return 0
30
- fi
31
- find "$source_dir" -type f -name "*$FIXTURE_SUFFIX" | wc -l
32
- }
33
-
34
- # The copy loops match on the suffix, so a file without one would be skipped in
35
- # silence and the scenario would provision a tree it was never meant to have.
36
- assert_fixtures_suffixed() {
37
- local source_dir="$1"
38
- [ -d "$source_dir" ] || return 0
39
-
40
- local stray
41
- stray="$(find "$source_dir" -type f -not -name "*$FIXTURE_SUFFIX" | sort | tr '\n' ' ')"
42
- [ -z "$stray" ] || log_error "Fixture missing the $FIXTURE_SUFFIX suffix: $stray"
43
- }
44
-
45
- fixture_target_path() {
46
- local source_dir="$1"
47
- local source_file="$2"
48
- local relative="${source_file#"$source_dir"/}"
49
- echo "${relative%"$FIXTURE_SUFFIX"}"
50
- }
51
-
52
- create_from_fixtures() {
53
- local source_dir="$1"
54
- [ -d "$source_dir" ] || return 0
55
-
56
- local source_file target
57
- while IFS= read -r -d '' source_file; do
58
- target="$(fixture_target_path "$source_dir" "$source_file")"
59
- mkdir -p "$(dirname "$target")"
60
- cp "$source_file" "$target"
61
- done < <(list_fixture_files "$source_dir")
62
- }
63
-
64
- # An append fixture extends a file the anchor or the injected seeds already
65
- # provide. A missing target means that upstream shape changed, so fail rather
66
- # than create the file and silently alter what the scenario provisions.
67
- append_from_fixtures() {
68
- local source_dir="$1"
69
- [ -d "$source_dir" ] || return 0
70
-
71
- local source_file target
72
- while IFS= read -r -d '' source_file; do
73
- target="$(fixture_target_path "$source_dir" "$source_file")"
74
- if [ ! -f "$target" ]; then
75
- log_error "Append fixture has no target: $target"
76
- fi
77
- cat "$source_file" >>"$target"
78
- done < <(list_fixture_files "$source_dir")
79
- }
80
-
81
- # Stages the first `count` markdown files the toolkit ships for a domain into
82
- # `dest`, flattening any source subfolder. Source rather than fixture tree,
83
- # because an arm modelling a real install wants the files a target actually
84
- # received and a copy under `fixtures/` would drift from them silently.
85
- #
86
- # The flattening is load-bearing rather than incidental. Both `detectUnmigrated`
87
- # and the sync engine match a target file to its source by basename against the
88
- # flat domain root, so a file lifted out of a source subfolder such as `bundled/`
89
- # has no flat sibling and reads as project-authored. An arm staging one claims a
90
- # drift or an unmigrated domain it did not stage.
91
- stage_toolkit_markdown() {
92
- local src="$1"
93
- local dest="$2"
94
- local count="$3"
95
-
96
- mkdir -p "$dest"
97
- while IFS= read -r file; do
98
- cp "$file" "$dest/$(basename "$file")"
99
- done < <(find "$src" -maxdepth 1 -type f -name "*.md" ! -name "index.md" | sort | head -n "$count")
100
- }
101
-
102
- # The newest top-level path history records a deletion under that the toolkit no
103
- # longer ships. Read from history rather than hardcoded, so an arm stages a root
104
- # the walk will actually recognize instead of a name that has since come back.
105
- # `prompts` is preferred because it is the case measured in a real target, and
106
- # any other dropped root exercises the same walk.
107
- pick_dropped_root() {
108
- local preferred="prompts"
109
- local first=""
110
- local candidate
111
-
112
- while IFS= read -r candidate; do
113
- [ -e "$PROJECT_ROOT/$candidate" ] && continue
114
- [ "$candidate" = "$preferred" ] && {
115
- echo "$preferred"
116
- return 0
117
- }
118
- [ -n "$first" ] || first="$candidate"
119
- done < <(git -C "$PROJECT_ROOT" log --all --diff-filter=D --name-only --format= |
120
- awk -F/ 'NF > 1 { print $1 }' | sort -u)
121
-
122
- echo "$first"
123
- }
124
-
125
- # Restores one file's exact published bytes from the commit before it was
126
- # deleted. Content is what the attribution matches on, so a file written by hand
127
- # would report unattributed and an arm would assert the wrong verdict.
128
- #
129
- # Both reads take the whole listing through a process substitution rather than a
130
- # pipeline ending in an early exit. `set -o pipefail` is on, and a `grep -m 1`
131
- # that matches the first line closes the pipe while git is still writing, so the
132
- # substitution returns git's SIGPIPE status and the arm fails on a listing it
133
- # actually read.
134
- restore_dropped_file() {
135
- local root="$1"
136
- local rel="" commit="" line
137
-
138
- while IFS= read -r line; do
139
- case "$line" in
140
- "$root"/*)
141
- rel="$line"
142
- break
143
- ;;
144
- esac
145
- done < <(git -C "$PROJECT_ROOT" log --all --diff-filter=D --name-only \
146
- --format= -- "$root/")
147
-
148
- [ -n "$rel" ] || return 1
149
-
150
- while IFS= read -r line; do
151
- commit="$line"
152
- break
153
- done < <(git -C "$PROJECT_ROOT" log --all --diff-filter=D --format=%H -- "$rel")
154
-
155
- [ -n "$commit" ] || return 1
156
-
157
- mkdir -p "$(dirname "$rel")"
158
- git -C "$PROJECT_ROOT" show "$commit^:$rel" >"$rel" || return 1
159
-
160
- echo "$rel"
161
- }
162
-
163
- # Stages one step of a scenario arm into the sandbox working directory.
164
- # Scenarios call this once per step so their own git operations stay between
165
- # the steps, where they are visible.
166
- stage_fixtures() {
167
- local category="$1"
168
- local scenario="$2"
169
- local arm="$3"
170
- local stage="$4"
171
-
172
- local stage_dir
173
- stage_dir="$(fixture_stage_dir "$category" "$scenario" "$arm" "$stage")"
174
-
175
- if [ ! -d "$stage_dir" ]; then
176
- log_error "Fixture stage not found: $category/$scenario/$arm/$stage"
177
- fi
178
-
179
- assert_fixtures_suffixed "$stage_dir/create"
180
- assert_fixtures_suffixed "$stage_dir/append"
181
-
182
- local staged
183
- staged=$(($(count_fixture_files "$stage_dir/create") + \
184
- $(count_fixture_files "$stage_dir/append")))
185
- if [ "$staged" -eq 0 ]; then
186
- log_error "Fixture stage provisions nothing: $category/$scenario/$arm/$stage"
187
- fi
188
-
189
- create_from_fixtures "$stage_dir/create"
190
- append_from_fixtures "$stage_dir/append"
191
- }
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- # The sandbox reaches GitHub over HTTPS with the gh token so a headless run needs
4
- # no SSH key and no agent. `gh auth login` authenticates the CLI but leaves git
5
- # itself without a credential, so the harness supplies the helper rather than
6
- # assuming the machine carries one.
7
- SANDBOX_GIT_CREDENTIAL_HELPER='!gh auth git-credential'
8
-
9
- # Call from the main shell before any scenario runs. sandbox_anchor_url is used
10
- # inside command substitutions, where log_error would exit only the subshell and
11
- # leave the caller building an empty remote, so the run has to stop here instead.
12
- require_sandbox_anchor_config() {
13
- if [ -z "${GITHUB_ORG:-}" ]; then
14
- log_error "GITHUB_ORG is empty. Export GITHUB_ORG=<org>, or run from a clone whose remote.origin.url points at GitHub."
15
- fi
16
- }
17
-
18
- resolve_sandbox_anchor_repo() {
19
- printf '%s\n' "${1:-${ANCHOR_REPO:-}}"
20
- }
21
-
22
- sandbox_anchor_url() {
23
- local repo_name
24
- repo_name="$(resolve_sandbox_anchor_repo "$@")"
25
-
26
- if [ -z "$repo_name" ] || [ -z "${GITHUB_ORG:-}" ]; then
27
- log_error "sandbox_anchor_url needs GITHUB_ORG and a repository name. Call require_sandbox_anchor_config first."
28
- fi
29
-
30
- printf 'https://github.com/%s/%s.git\n' "$GITHUB_ORG" "$repo_name"
31
- }
32
-
33
- # credential.helper is multi-valued and git concatenates it across system, global,
34
- # and local, trying each in order until one returns a credential. A plain set
35
- # leaves an operator's existing helper ahead of this one, so a stale token wins
36
- # and gh never runs. The empty value resets the inherited list first.
37
- configure_sandbox_git_credentials() {
38
- git config credential.helper ""
39
- git config --add credential.helper "$SANDBOX_GIT_CREDENTIAL_HELPER"
40
- }
41
-
42
- resolve_sandbox_git_identity() {
43
- SANDBOX_GIT_NAME="${SANDBOX_GIT_NAME:-$(git config --global user.name 2>/dev/null || true)}"
44
- SANDBOX_GIT_EMAIL="${SANDBOX_GIT_EMAIL:-$(git config --global user.email 2>/dev/null || true)}"
45
-
46
- : "${SANDBOX_GIT_NAME:=canon-sandbox}"
47
- : "${SANDBOX_GIT_EMAIL:=sandbox@example.com}"
48
-
49
- export SANDBOX_GIT_NAME SANDBOX_GIT_EMAIL
50
- }
51
-
52
- configure_sandbox_git_identity() {
53
- resolve_sandbox_git_identity
54
- git config user.name "$SANDBOX_GIT_NAME"
55
- git config user.email "$SANDBOX_GIT_EMAIL"
56
- }
57
-
58
- # Every scenario that needs a remote points at the same throwaway repository, so
59
- # the name lives here rather than in each one.
60
- SANDBOX_ANCHOR_REPO="canon-sandbox"
61
-
62
- # A scenario calls this from its own use_anchor hook rather than this file
63
- # defining the hook. sandbox-hook.sh keys off `type -t use_anchor`, so
64
- # declaring it here would hand an anchor to the scenarios that source this file
65
- # for the identity helpers alone.
66
- use_sandbox_anchor() {
67
- export ANCHOR_REPO="${1:-$SANDBOX_ANCHOR_REPO}"
68
- }
69
-
70
- # gh answers an absent repository and an unreachable host with the same exit
71
- # status, so the 404 is the only thing separating them. Anything else is a
72
- # network or credential fault, where creating a repository would be the wrong
73
- # answer and would fail for the same reason the read did.
74
- ensure_sandbox_anchor_repo() {
75
- local repo_name gh_error
76
-
77
- repo_name="$(resolve_sandbox_anchor_repo "$@")"
78
-
79
- if [ -z "$repo_name" ] || [ -z "${GITHUB_ORG:-}" ]; then
80
- log_error "ensure_sandbox_anchor_repo needs GITHUB_ORG and a repository name. Call require_sandbox_anchor_config first."
81
- fi
82
-
83
- if gh_error="$(gh api "repos/${GITHUB_ORG}/${repo_name}" --silent 2>&1)"; then
84
- return 0
85
- fi
86
-
87
- case "$gh_error" in
88
- *"HTTP 404"*) ;;
89
- *) log_error "Cannot reach ${GITHUB_ORG}/${repo_name}: ${gh_error}" ;;
90
- esac
91
-
92
- # An absent anchor is nearly always a wrong GITHUB_ORG or a rename nobody
93
- # performed, and provisioning stages a fixture rather than cloning while every
94
- # scenario force-pushes to main, so a repository created here would carry all
95
- # nine to a pass against something that is not the anchor. Refusing is the safe
96
- # default and creating is the opt-in, the shape `canon tooling sync --write`
97
- # already sets. `canon records push` refuses outright for the reason
98
- # `canon/context/development/records.md` records, so the two still differ.
99
- # The sibling SANDBOX_ flags are presence tests, so any non-empty value turns
100
- # them on. This one allowlists instead, because a presence test would have
101
- # SANDBOX_ANCHOR_CREATE=false provisioning a repository. Both spellings are
102
- # accepted so the `true` those siblings are set to does not land on a refusal.
103
- case "${SANDBOX_ANCHOR_CREATE:-}" in
104
- 1 | true) ;;
105
- *) log_error "${GITHUB_ORG}/${repo_name} does not exist. Check GITHUB_ORG and whether a rename is pending, then create it with 'gh repo create ${GITHUB_ORG}/${repo_name} --private' or re-run with SANDBOX_ANCHOR_CREATE=true." ;;
106
- esac
107
-
108
- log_warn "${GITHUB_ORG}/${repo_name} does not exist and SANDBOX_ANCHOR_CREATE is set. Creating it as private."
109
- if ! gh_error="$(gh repo create "${GITHUB_ORG}/${repo_name}" --private 2>&1)"; then
110
- log_error "Could not create ${GITHUB_ORG}/${repo_name}: ${gh_error}"
111
- fi
112
- log_info "Created ${GITHUB_ORG}/${repo_name} as a private repository."
113
- }
114
-
115
- # A remote is useless without an author, so the scenarios that reach one always
116
- # configure both. configure_sandbox_git_identity stays callable on its own for
117
- # the scenarios that never push. The probe runs first so an absent remote is
118
- # reported before the scenario stages anything. The remove keeps this idempotent
119
- # against a sandbox tree that already carries an origin.
120
- configure_sandbox_anchor_remote() {
121
- ensure_sandbox_anchor_repo "$@"
122
- configure_sandbox_git_identity
123
- git remote remove origin 2>/dev/null || true
124
- git remote add origin "$(sandbox_anchor_url "$@")"
125
- }
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- # The sandbox tree lives outside the toolkit worktree. `canon sandbox run`
4
- # sets cwd to it for `claude -p`, and every `CLAUDE.md` between that cwd and the
5
- # filesystem root loads into the session. Under the repository the toolkit's own
6
- # instructions join that chain beside the seeded copy the scenario installed,
7
- # both carry the rule sending shared session scratch to the main worktree root,
8
- # and nothing decides which root wins. A session picking the toolkit writes its
9
- # output where no manifest reads it, so the run reports success while the verdict
10
- # reports no writes at all. The retired eval runner kept its fixture outside the
11
- # repository for the same reason.
12
- #
13
- # Mints a short random per-run identifier the first time it is asked for, then
14
- # holds it in CANON_SANDBOX_RUN_ID for the rest of this process. A direct call
15
- # (not `$(...)`) exports it into the caller's own shell. A call already carrying
16
- # the variable, inherited from a parent such as `canon sandbox run`, reuses it
17
- # rather than minting a new one, which is what keeps a hook on the parent's tree.
18
- #
19
- # Twin of `mintSandboxRunId` in `src/sandbox/tree.ts`.
20
- mint_sandbox_run_id() {
21
- if [ -z "${CANON_SANDBOX_RUN_ID:-}" ]; then
22
- CANON_SANDBOX_RUN_ID="$(date +%s)-$$-$RANDOM"
23
- fi
24
- export CANON_SANDBOX_RUN_ID
25
- }
26
-
27
- # The base every per-run tree nests under, with no run id appended. Split out
28
- # of `resolve_sandbox_dir` so a caller that needs to recognize any run's tree,
29
- # such as `require_project_root` in `scripts/lib/ui.sh`, tests against this
30
- # prefix instead of a single resolved path that changes on every call.
31
- sandbox_dir_prefix() {
32
- printf '%s/canon/sandbox\n' "${XDG_STATE_HOME:-$HOME/.local/state}"
33
- }
34
-
35
- # Twin of `sandboxTree` in `src/sandbox/tree.ts`. The exec boundary rules out
36
- # a shared constant, so a change to the default lands on both sides.
37
- #
38
- # A bare fall-through with no per-run component is one path per machine, so
39
- # two sessions resolving the default at once would provision over each other
40
- # with neither told. `mint_sandbox_run_id` is what makes two such sessions
41
- # land on two different trees rather than one.
42
- resolve_sandbox_dir() {
43
- if [ -n "${CANON_SANDBOX_DIR:-}" ]; then
44
- printf '%s\n' "$CANON_SANDBOX_DIR"
45
- return 0
46
- fi
47
-
48
- mint_sandbox_run_id
49
- printf '%s-%s\n' "$(sandbox_dir_prefix)" "$CANON_SANDBOX_RUN_ID"
50
- }
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env bash
2
- # No `-u`. Scenarios ran under the retired dispatcher without it and read unset
3
- # variables as empty, so turning it on here changes what they provision.
4
- set -e
5
- set -o pipefail
6
-
7
- # Runs one scenario's hooks for the provisioning order in
8
- # `src/sandbox/provision.ts`. Scenarios and the library they call stay bash, so
9
- # the harness reaches them through this entry and nothing else.
10
- #
11
- # probe <scenario> runs `use_config` and `use_anchor` when declared
12
- # and prints every export they changed, closed by an
13
- # `@anchor=<0|1>` record
14
- # stage <scenario> <file> re-runs both hooks, runs `stage_setup` inside the
15
- # tree, and writes the exports it changed to <file>,
16
- # or an `@exited` record when the shell exits first
17
- #
18
- # Pairs are NUL-separated `NAME=value`, since a value can hold a newline. The
19
- # retired dispatcher ran `stage_setup` in its own shell, so an export, an `exit`,
20
- # or an `exec` there reached the harness, and the report carries all three: an
21
- # `exec` skips the EXIT trap and leaves no report at all. The stage re-runs the
22
- # hooks rather than receiving the probe's pairs, since they only export.
23
-
24
- SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
25
- PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
26
- export PROJECT_ROOT
27
-
28
- # Globals carry a prefix, since the scenario sourced below shares this shell.
29
- _hook_die() {
30
- echo "sandbox-hook: $1" >&2
31
- exit 2
32
- }
33
- _hook_mode="${1:-}"
34
- _hook_scenario="${2:-}"
35
- _hook_report="${3:-}"
36
-
37
- [[ "$_hook_mode" == "probe" || "$_hook_mode" == "stage" ]] || _hook_die "mode must be probe or stage, got: ${_hook_mode:-<empty>}"
38
- [ -f "$_hook_scenario" ] || _hook_die "no scenario file at: ${_hook_scenario:-<empty>}"
39
- [ "$_hook_mode" = "probe" ] || [ -n "$_hook_report" ] || _hook_die "stage needs a file to report its exports to"
40
- [ "$_hook_mode" = "probe" ] || trap 'printf "@exited\0" >"$_hook_report"' EXIT
41
-
42
- declare -A _hook_before
43
- _hook_snapshot() {
44
- local name
45
- _hook_before=()
46
- for name in $(compgen -e); do
47
- _hook_before[$name]="${!name}"
48
- done
49
- }
50
-
51
- # The shell moves PWD and OLDPWD itself on every `cd`, and neither is state.
52
- _hook_changed() {
53
- local name
54
- for name in $(compgen -e); do
55
- [[ "$name" == "PWD" || "$name" == "OLDPWD" ]] && continue
56
- if [[ ! -v _hook_before[$name] ]] || [ "${_hook_before[$name]}" != "${!name}" ]; then
57
- printf '%s=%s\0' "$name" "${!name}"
58
- fi
59
- done
60
- }
61
-
62
- # Taken before `config.sh`, so a `GITHUB_ORG` it derives from the remote reaches
63
- # the harness as an export like any other.
64
- _hook_snapshot
65
-
66
- source "$PROJECT_ROOT/scripts/config.sh"
67
- source "$PROJECT_ROOT/scripts/lib/ui.sh"
68
- source "$PROJECT_ROOT/scripts/lib/sandbox-path.sh"
69
- source "$PROJECT_ROOT/scripts/lib/sandbox-git.sh"
70
- source "$PROJECT_ROOT/scripts/lib/sandbox-fixtures.sh"
71
-
72
- # Unexported, as the dispatcher held them, so a scenario reads both while a
73
- # child it spawns sees neither. Shellcheck cannot see the scenario reading them.
74
- # shellcheck disable=SC2034
75
- SANDBOX="$(resolve_sandbox_dir)" SANDBOX_DIR="$PROJECT_ROOT/sandbox"
76
-
77
- # shellcheck source=/dev/null
78
- source "$_hook_scenario"
79
-
80
- _hook_declared() { [[ "$(type -t "$1")" == "function" ]]; }
81
- _hook_run() {
82
- if _hook_declared use_config; then use_config; fi
83
- if _hook_declared use_anchor; then use_anchor; fi
84
- }
85
-
86
- if [ "$_hook_mode" = "probe" ]; then
87
- # Stdout carries the pairs alone, so a hook that prints is moved off it.
88
- _hook_run >&2
89
- _hook_changed
90
- if _hook_declared use_anchor; then printf '@anchor=1\0'; else printf '@anchor=0\0'; fi
91
- exit 0
92
- fi
93
-
94
- _hook_run
95
- _hook_snapshot
96
- cd "$SANDBOX"
97
- stage_setup
98
- trap - EXIT
99
- _hook_changed >"$_hook_report"
@@ -1,179 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
- import { mkdir, rm, writeFile } from 'node:fs/promises'
3
- import { basename, join, relative } from 'node:path'
4
- import { listRuleSourcePaths, rulesSourceDir } from '@/gov/install'
5
- import { parseFrontmatter } from '@/indexes/frontmatter'
6
-
7
- const STANDARDS_REL = 'standards'
8
-
9
- /** The band the generated rules ship in, named for what they point at. */
10
- export const STANDARD_RULES_BAND = 'standards'
11
-
12
- export type StandardRulesResult =
13
- | { readonly ok: true; readonly written: readonly string[] }
14
- | { readonly ok: false; readonly reason: string }
15
-
16
- interface StandardRule {
17
- readonly name: string
18
- readonly sourceRel: string
19
- readonly title: string
20
- readonly description: string
21
- readonly paths: readonly string[]
22
- readonly bullets: readonly string[]
23
- }
24
-
25
- export function standardRulesDir(root: string): string {
26
- return join(rulesSourceDir(root), STANDARD_RULES_BAND)
27
- }
28
-
29
- /**
30
- * Generates one path-scoped rule per standard whose frontmatter carries
31
- * `paths:`. The standard owns the glob and the operative bullets, so the rule
32
- * is output, committed beside the authored bands so every reader of
33
- * `governance/rules/` sees it as an ordinary file.
34
- *
35
- * Every standard is read and checked before anything is written, so a refusal
36
- * leaves the previous output whole rather than half cleared.
37
- */
38
- export async function regenStandardRules(
39
- root: string,
40
- ): Promise<StandardRulesResult> {
41
- const standardsDir = join(root, STANDARDS_REL)
42
- if (!existsSync(standardsDir)) return { ok: true, written: [] }
43
-
44
- const reserved = reservedNames(root)
45
- const rules: StandardRule[] = []
46
-
47
- for (const file of readdirSync(standardsDir).sort()) {
48
- if (!file.endsWith('.md')) continue
49
- const read = readStandard(root, file, reserved)
50
- if (typeof read === 'string') return { ok: false, reason: read }
51
- if (read !== undefined) rules.push(read)
52
- }
53
-
54
- const dir = standardRulesDir(root)
55
- await rm(dir, { recursive: true, force: true })
56
- if (rules.length > 0) await mkdir(dir, { recursive: true })
57
-
58
- const written: string[] = []
59
- for (const rule of rules) {
60
- const dest = join(dir, `${rule.name}.md`)
61
- await writeFile(dest, renderStandardRule(rule))
62
- written.push(relative(root, dest))
63
- }
64
-
65
- return { ok: true, written }
66
- }
67
-
68
- /**
69
- * Band folders and authored rule basenames. A bare-word rule name equal to a
70
- * band would install the whole band through `--add`, and one equal to an
71
- * authored rule would shadow it in the name index.
72
- */
73
- function reservedNames(root: string): {
74
- readonly bands: ReadonlySet<string>
75
- readonly rules: ReadonlySet<string>
76
- } {
77
- const bands = new Set<string>([STANDARD_RULES_BAND])
78
- const rules = new Set<string>()
79
-
80
- for (const rel of listRuleSourcePaths(root)) {
81
- const segments = rel.split('/')
82
- if (segments.length > 1) bands.add(segments[0])
83
- if (segments[0] === STANDARD_RULES_BAND) continue
84
- rules.add(basename(rel, '.md'))
85
- }
86
-
87
- return { bands, rules }
88
- }
89
-
90
- /** A rule to render, `undefined` for a standard with no `paths:`, or a reason. */
91
- function readStandard(
92
- root: string,
93
- file: string,
94
- reserved: ReturnType<typeof reservedNames>,
95
- ): StandardRule | string | undefined {
96
- const sourceRel = `${STANDARDS_REL}/${file}`
97
- const fields = parseFrontmatter(
98
- readFileSync(join(root, STANDARDS_REL, file), 'utf8'),
99
- )?.fields
100
- if (fields === undefined || fields.paths === undefined) return undefined
101
-
102
- const name = basename(file, '.md')
103
- if (!isStringList(fields.paths)) {
104
- return `${sourceRel}: paths must be a list of strings`
105
- }
106
- if (fields.rule !== undefined && !isStringList(fields.rule)) {
107
- return `${sourceRel}: rule must be a list of strings`
108
- }
109
- if (reserved.bands.has(name)) {
110
- return `${sourceRel}: name collides with a rule band folder`
111
- }
112
- if (reserved.rules.has(name)) {
113
- return `${sourceRel}: name collides with an authored rule`
114
- }
115
-
116
- return {
117
- name,
118
- sourceRel,
119
- title: typeof fields.title === 'string' ? fields.title : name,
120
- description:
121
- typeof fields.description === 'string' ? fields.description : '',
122
- paths: fields.paths,
123
- bullets: fields.rule ?? [],
124
- }
125
- }
126
-
127
- function isStringList(value: unknown): value is string[] {
128
- return (
129
- Array.isArray(value) && value.every((entry) => typeof entry === 'string')
130
- )
131
- }
132
-
133
- function renderStandardRule(rule: StandardRule): string {
134
- const scope = rule.description.replace(/\.$/, '')
135
- const pointer = [
136
- `Follow the ${rule.name} standard for ${lowerFirst(scope)}.`,
137
- 'It is the single source.',
138
- `Read it with \`canon standards ${rule.name}\`.`,
139
- ].join(' ')
140
-
141
- return [
142
- '---',
143
- `# Generated by \`canon gov regen\` from ${rule.sourceRel}. Edit that frontmatter, not this file.`,
144
- `description: ${yamlScalar(rule.description)}`,
145
- 'paths:',
146
- ...rule.paths.map((glob) => ` - ${yamlQuoted(glob)}`),
147
- '---',
148
- '',
149
- `# ${rule.title.replace(/\breference$/, 'standards')}`,
150
- '',
151
- '## Authority',
152
- '',
153
- ...[pointer, ...rule.bullets].map((bullet) => `- ${bullet}`),
154
- '',
155
- ].join('\n')
156
- }
157
-
158
- function lowerFirst(text: string): string {
159
- return text.charAt(0).toLowerCase() + text.slice(1)
160
- }
161
-
162
- /** Plain when YAML would read it back unchanged, single-quoted otherwise. */
163
- function yamlScalar(text: string): string {
164
- const isPlain =
165
- text !== '' &&
166
- !/^[-?:,[\]{}#&*!|>'"%@`\s]/.test(text) &&
167
- !/: |\s#|:$|\s$/.test(text)
168
- return isPlain ? text : yamlQuoted(text)
169
- }
170
-
171
- /**
172
- * Quoted the way Prettier's `singleQuote` leaves YAML, so the drift check
173
- * never fights the formatter: single quotes unless the text holds one and
174
- * double quotes would need no escape.
175
- */
176
- function yamlQuoted(text: string): string {
177
- if (text.includes("'") && !/["\\]/.test(text)) return `"${text}"`
178
- return `'${text.replaceAll("'", "''")}'`
179
- }