@erclx/canon 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/REQUIREMENT.md +51 -0
- package/claude/skills/api-design/SKILL.md +102 -0
- package/claude/skills/api-design/references/adopted.md +40 -0
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
- package/claude/skills/codebase-layout/SKILL.md +2 -1
- package/claude/skills/review-ui/REQUIREMENT.md +3 -0
- package/claude/skills/review-ui/SKILL.md +15 -1
- package/claude/skills/review-ui/references/driving.md +3 -3
- package/claude/skills/review-ui/references/post.md +16 -3
- package/claude/skills/role-orchestrator/SKILL.md +2 -2
- package/claude/skills/systematic-debugging/REQUIREMENT.md +14 -0
- package/claude/skills/systematic-debugging/SKILL.md +45 -17
- package/claude/skills/systematic-debugging/references/adopted.md +44 -0
- package/docs/agents/capture.md +2 -2
- package/docs/agents/commands.md +1 -1
- package/docs/agents/index.md +2 -2
- package/docs/agents/pr-evidence.md +48 -1
- package/docs/agents/scripting.md +0 -3
- package/docs/agents/tasks-plans.md +8 -5
- package/docs/agents/worktrees.md +16 -1
- package/docs/workflow/skill-map.md +2 -1
- package/package.json +1 -1
- package/scripts/core/regen-agent-fixture.sh +3 -3
- package/scripts/core/regen-hero.ts +119 -0
- package/scripts/core/regen-web-favicon.ts +1 -1
- package/src/capture/frame-values.ts +359 -0
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +2 -0
- package/src/commands/gate.ts +3 -2
- package/src/commands/pr.ts +78 -6
- package/src/commands/tasks.ts +12 -2
- package/src/commands/tooling.ts +2 -2
- package/src/commands/worktrees.ts +37 -0
- package/src/counts/catalogs.ts +3 -3
- package/src/design/tokens.ts +1 -1
- package/src/gate/sequencer.ts +0 -20
- package/src/gate/stages.ts +1 -2
- package/src/help.ts +0 -1
- package/src/pr/evidence.ts +35 -0
- package/src/tasks/archive.ts +27 -0
- package/src/tasks/record.ts +25 -2
- package/src/worktrees/bare-flag.ts +83 -0
- package/standards/tasks.md +2 -2
- package/scripts/core/regen-hero.sh +0 -419
- package/scripts/core/repair-bare-flag.sh +0 -19
- package/scripts/lib/worktree.sh +0 -20
- package/scripts/tooling/create.sh +0 -109
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Fills every assets/captures/*.html.tmpl from the CLI catalogs and the design
|
|
3
|
-
# source, writing the .html beside each one.
|
|
4
|
-
#
|
|
5
|
-
# The name says hero because the hero was the only template when it was written
|
|
6
|
-
# and every citation of it across docs/, canon/context/, and src/ spells that
|
|
7
|
-
# name. Renaming the file is a sweep across roughly twenty surfaces for a
|
|
8
|
-
# cosmetic gain, so the file keeps its name and the loop below covers whatever
|
|
9
|
-
# templates the folder holds.
|
|
10
|
-
#
|
|
11
|
-
# The palette arrives the same way the counts do. `canon design css
|
|
12
|
-
# --no-components` emits the custom properties from src/design/tokens.ts and
|
|
13
|
-
# they land on the {{TOKENS}} placeholder, so neither frame carries its own copy
|
|
14
|
-
# of a hex value and a token moved at the source moves both captures. The
|
|
15
|
-
# component half is left out on purpose: it is a scrollbar and a status marker,
|
|
16
|
-
# and a static capture frame renders neither.
|
|
17
|
-
#
|
|
18
|
-
# A frame a README shows in both themes fills once more against the light roles
|
|
19
|
-
# and writes `<name>-light.html` beside the dark one, so each theme is its own
|
|
20
|
-
# capture set with its own stamp.
|
|
21
|
-
#
|
|
22
|
-
# Only the HTML regenerates here. The PNG beside it is a chromium render whose
|
|
23
|
-
# bytes move with the browser version, so asserting it in verify.sh would fail
|
|
24
|
-
# on a machine whose chromium differs rather than on a stale count. Rebuild the
|
|
25
|
-
# images with `canon capture assets/captures --selector .window --out assets/evidence`
|
|
26
|
-
# after this script reports a change. The markup and the image sit in two
|
|
27
|
-
# folders, so that run reads the sources here and sends every PNG and stamp to
|
|
28
|
-
# assets/evidence/, where the documents point and where `canon pr evidence`
|
|
29
|
-
# compares a changed image. The selector has no default, since
|
|
30
|
-
# the element a capture crops to belongs to the page rather than to the command,
|
|
31
|
-
# and `.window` is the class this repository's own sources declare.
|
|
32
|
-
# That capture also writes a .stamp beside each PNG, which records the digest of
|
|
33
|
-
# the markup it rendered and is what the Hero stage compares, so a frame's three
|
|
34
|
-
# files commit together. The frame carries no version. `package.json` is bumped on main by the release
|
|
35
|
-
# tooling, so embedding it drifts every open branch on the next release and the
|
|
36
|
-
# stage then fails for work that touched nothing.
|
|
37
|
-
#
|
|
38
|
-
# Clone-only. `canon capture` ships now, but this script reads the repository's
|
|
39
|
-
# own catalogs, which a registry install does not carry.
|
|
40
|
-
set -e
|
|
41
|
-
set -o pipefail
|
|
42
|
-
|
|
43
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
45
|
-
|
|
46
|
-
ASSET_DIR="$PROJECT_ROOT/assets/captures"
|
|
47
|
-
LISTED=10
|
|
48
|
-
|
|
49
|
-
# `--check` fills every template into a temp folder and discards it, so a renamed
|
|
50
|
-
# featured skill, an empty catalog, or an unresolved placeholder still fails a
|
|
51
|
-
# branch while no frame file changes. Frames are refreshed after a merge rather
|
|
52
|
-
# than committed by each branch, so a branch asks whether the regen can run and
|
|
53
|
-
# never whether its output is current.
|
|
54
|
-
OUT_DIR="$ASSET_DIR"
|
|
55
|
-
case "${1:-}" in
|
|
56
|
-
'') ;;
|
|
57
|
-
--check) OUT_DIR="$(mktemp -d)" ;;
|
|
58
|
-
*)
|
|
59
|
-
echo "regen-hero: unknown argument $1" >&2
|
|
60
|
-
exit 1
|
|
61
|
-
;;
|
|
62
|
-
esac
|
|
63
|
-
|
|
64
|
-
# `bun src/cli.ts` rather than `canon`, since a globally linked binary resolves to
|
|
65
|
-
# the main checkout no matter which worktree is running.
|
|
66
|
-
catalog() {
|
|
67
|
-
(cd "$PROJECT_ROOT" && CANON_NON_INTERACTIVE=1 bun src/cli.ts "$@" --json 2>/dev/null)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
TEMPLATES=("$ASSET_DIR"/*.html.tmpl)
|
|
71
|
-
if [ ! -f "${TEMPLATES[0]}" ]; then
|
|
72
|
-
echo "regen-hero: no templates under $ASSET_DIR" >&2
|
|
73
|
-
exit 1
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
# Not routed through `catalog`, since the design emitter writes CSS on stdout
|
|
77
|
-
# rather than a `--json` record.
|
|
78
|
-
TOKEN_CSS="$(cd "$PROJECT_ROOT" && CANON_NON_INTERACTIVE=1 bun src/cli.ts design css --no-components)"
|
|
79
|
-
if [ -z "$TOKEN_CSS" ]; then
|
|
80
|
-
echo "regen-hero: the design source emitted nothing, refusing to write an unstyled frame" >&2
|
|
81
|
-
exit 1
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
# The commands have no `--json` catalog of their own, so the count comes from
|
|
85
|
-
# `canon gov counts`, which already reads the registration block in the CLI
|
|
86
|
-
# entry point to judge a document stating its own command count. Reading that
|
|
87
|
-
# figure here rather than re-deriving it with a second regex is what keeps the
|
|
88
|
-
# two readings of `src/cli.ts` from drifting apart unwatched, which is the
|
|
89
|
-
# defect `canon gov counts` itself exists to catch.
|
|
90
|
-
# `gov counts` exits non-zero on an ordinary finding, unlike every `list` verb
|
|
91
|
-
# below, so its output is read past that under `set -e` rather than aborting
|
|
92
|
-
# a hero rebuild for a stale count somewhere unrelated in the tree.
|
|
93
|
-
COUNTS_JSON="$(catalog gov counts || true)"
|
|
94
|
-
SKILLS_JSON="$(catalog claude skills list)"
|
|
95
|
-
GOV_JSON="$(catalog gov list)"
|
|
96
|
-
STANDARDS_JSON="$(catalog standards list)"
|
|
97
|
-
TOOLING_JSON="$(catalog tooling list)"
|
|
98
|
-
|
|
99
|
-
for payload in "$COUNTS_JSON" "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$TOOLING_JSON"; do
|
|
100
|
-
if [ -z "$payload" ]; then
|
|
101
|
-
echo "regen-hero: a catalog returned nothing, refusing to write a zeroed hero" >&2
|
|
102
|
-
exit 1
|
|
103
|
-
fi
|
|
104
|
-
done
|
|
105
|
-
|
|
106
|
-
# The catalogs reach the eval as files rather than as environment entries. Linux
|
|
107
|
-
# caps a single env string at 128KB, and the standards payload crossed it, which
|
|
108
|
-
# fails the exec with E2BIG before any stage can report a stale count. A file
|
|
109
|
-
# path is bounded whatever the catalogs grow to.
|
|
110
|
-
PAYLOAD_DIR="$(mktemp -d)"
|
|
111
|
-
trap 'rm -rf "$PAYLOAD_DIR"; [ "$OUT_DIR" = "$ASSET_DIR" ] || rm -rf "$OUT_DIR"' EXIT
|
|
112
|
-
|
|
113
|
-
printf '%s' "$COUNTS_JSON" >"$PAYLOAD_DIR/counts.json"
|
|
114
|
-
printf '%s' "$SKILLS_JSON" >"$PAYLOAD_DIR/skills.json"
|
|
115
|
-
printf '%s' "$GOV_JSON" >"$PAYLOAD_DIR/gov.json"
|
|
116
|
-
printf '%s' "$STANDARDS_JSON" >"$PAYLOAD_DIR/standards.json"
|
|
117
|
-
printf '%s' "$TOOLING_JSON" >"$PAYLOAD_DIR/tooling.json"
|
|
118
|
-
printf '%s' "$TOKEN_CSS" >"$PAYLOAD_DIR/tokens.css"
|
|
119
|
-
|
|
120
|
-
export PAYLOAD_DIR
|
|
121
|
-
export ASSET_DIR OUT_DIR LISTED PROJECT_ROOT
|
|
122
|
-
|
|
123
|
-
bun --eval '
|
|
124
|
-
const { readFileSync, readdirSync } = require("node:fs")
|
|
125
|
-
|
|
126
|
-
const { PAYLOAD_DIR, ASSET_DIR, OUT_DIR, LISTED, PROJECT_ROOT } = process.env
|
|
127
|
-
|
|
128
|
-
const payload = (name) => readFileSync(PAYLOAD_DIR + "/" + name + ".json", "utf8")
|
|
129
|
-
|
|
130
|
-
// Indented to the depth a rule inside the `<style>` block sits at, so the
|
|
131
|
-
// emitted file passes the same formatter every other committed asset does.
|
|
132
|
-
const tokenCss = readFileSync(PAYLOAD_DIR + "/tokens.css", "utf8")
|
|
133
|
-
.trimEnd()
|
|
134
|
-
.split("\n")
|
|
135
|
-
.map((line) => (line === "" ? "" : " " + line))
|
|
136
|
-
.join("\n")
|
|
137
|
-
|
|
138
|
-
const COUNTS_JSON = payload("counts")
|
|
139
|
-
const SKILLS_JSON = payload("skills")
|
|
140
|
-
const GOV_JSON = payload("gov")
|
|
141
|
-
const STANDARDS_JSON = payload("standards")
|
|
142
|
-
const TOOLING_JSON = payload("tooling")
|
|
143
|
-
|
|
144
|
-
const listed = Number(LISTED)
|
|
145
|
-
const skills = JSON.parse(SKILLS_JSON).skills.map((entry) => entry.name)
|
|
146
|
-
const gov = JSON.parse(GOV_JSON)
|
|
147
|
-
// Rule names carry a numeric prefix that orders the load, not the identity a
|
|
148
|
-
// reader knows them by, so the frame shows the slug alone.
|
|
149
|
-
const slug = (entry) => entry.name.replace(/^\d+-/, "")
|
|
150
|
-
const rules = gov.rules.map(slug)
|
|
151
|
-
// A rule no stack names is opt-in behind `--add`, so featuring one advertises
|
|
152
|
-
// an entry no ordinary `canon gov install` delivers. The column samples the
|
|
153
|
-
// stack-reached subset while its count and remainder describe the whole catalog.
|
|
154
|
-
const stacked = new Set(gov.stacks.flatMap((stack) => stack.rules))
|
|
155
|
-
const deliveredRules = gov.rules.filter((entry) => stacked.has(entry.name)).map(slug)
|
|
156
|
-
const standards = JSON.parse(STANDARDS_JSON).standards.map((entry) => entry.name)
|
|
157
|
-
const toolingStacks = JSON.parse(TOOLING_JSON).stacks
|
|
158
|
-
const commandCount = JSON.parse(COUNTS_JSON).catalogs.commands
|
|
159
|
-
if (!commandCount) {
|
|
160
|
-
console.error("regen-hero: gov counts read no commands catalog, refusing to write a zeroed hero")
|
|
161
|
-
process.exit(1)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const MARK_SVG = (await Bun.file(`${PROJECT_ROOT}/assets/brand/mark.svg`).text()).trim()
|
|
165
|
-
if (!MARK_SVG) {
|
|
166
|
-
console.error("regen-hero: assets/brand/mark.svg read empty, refusing to write a hero with no mark")
|
|
167
|
-
process.exit(1)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const accentMatch = tokenCss.match(/--color-accent:\s*(#[0-9a-fA-F]{3,8})/)
|
|
171
|
-
if (!accentMatch) {
|
|
172
|
-
console.error("regen-hero: token css carries no --color-accent value, refusing to write a colorless favicon")
|
|
173
|
-
process.exit(1)
|
|
174
|
-
}
|
|
175
|
-
const faviconSvg = MARK_SVG.replace(/<!--[\s\S]*?-->/, "").trim().replaceAll("currentColor", accentMatch[1])
|
|
176
|
-
const FAVICON = `data:image/svg+xml,${encodeURIComponent(faviconSvg)}`
|
|
177
|
-
|
|
178
|
-
const escape = (value) =>
|
|
179
|
-
value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
180
|
-
|
|
181
|
-
// Even spacing across the sorted catalog rather than its first N, so a column
|
|
182
|
-
// whose names share a prefix does not read as a narrower catalog than the one
|
|
183
|
-
// that ships. Rules and standards sample. Skills do not, because the even step
|
|
184
|
-
// lands on the entries closest to commodity and on none of the workflow the
|
|
185
|
-
// toolkit exists to carry.
|
|
186
|
-
const sample = (names) => {
|
|
187
|
-
if (names.length <= listed) return names
|
|
188
|
-
const step = names.length / listed
|
|
189
|
-
return Array.from({ length: listed }, (_, i) => names[Math.floor(i * step)])
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// The first seven are the method and exist nowhere else. The last three are
|
|
193
|
-
// recognizable on sight and are what keeps a column of chosen names from
|
|
194
|
-
// reading as an all-`claude-` catalog, which is the failure the sampler above
|
|
195
|
-
// was written against.
|
|
196
|
-
const FEATURED_SKILLS = [
|
|
197
|
-
"plan-feature",
|
|
198
|
-
"plan-groundwork",
|
|
199
|
-
"plan-intake",
|
|
200
|
-
"role-orchestrator",
|
|
201
|
-
"auto-ship",
|
|
202
|
-
"review-pr",
|
|
203
|
-
"task-board",
|
|
204
|
-
"git-ship",
|
|
205
|
-
"target-setup",
|
|
206
|
-
"systematic-debugging",
|
|
207
|
-
]
|
|
208
|
-
|
|
209
|
-
// A chosen name is maintained by hand where a sample never goes stale, so a
|
|
210
|
-
// rename has to fail the run rather than quietly shrink the column. This is
|
|
211
|
-
// what makes the trade acceptable.
|
|
212
|
-
//
|
|
213
|
-
// The length is asserted because the `+N more` figure below counts down from
|
|
214
|
-
// `listed` rather than from what this returns, so a list of any other length
|
|
215
|
-
// renders a remainder that is wrong by the difference.
|
|
216
|
-
const featured = (names, chosen) => {
|
|
217
|
-
if (chosen.length !== listed) {
|
|
218
|
-
console.error(`regen-hero: ${chosen.length} featured skills, expected ${listed}`)
|
|
219
|
-
process.exit(1)
|
|
220
|
-
}
|
|
221
|
-
const catalog = new Set(names)
|
|
222
|
-
const missing = chosen.filter((name) => !catalog.has(name))
|
|
223
|
-
if (missing.length > 0) {
|
|
224
|
-
console.error(`regen-hero: featured skills missing from the catalog: ${missing.join(", ")}`)
|
|
225
|
-
process.exit(1)
|
|
226
|
-
}
|
|
227
|
-
return chosen
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const markup = (names) =>
|
|
231
|
-
names
|
|
232
|
-
.map((name) => ` <div class="entry">${escape(name)}</div>`)
|
|
233
|
-
.join("\n")
|
|
234
|
-
|
|
235
|
-
const entries = (names) => markup(sample(names))
|
|
236
|
-
|
|
237
|
-
const remaining = (names) => String(Math.max(0, names.length - listed))
|
|
238
|
-
|
|
239
|
-
// An empty array is well-formed JSON, so the shell guard on an empty payload
|
|
240
|
-
// does not catch a catalog whose tree went missing. A zeroed count would ship a
|
|
241
|
-
// frame claiming the domain has nothing in it.
|
|
242
|
-
for (const [label, list] of [
|
|
243
|
-
["skills", skills], ["rules", rules], ["standards", standards],
|
|
244
|
-
["tooling stacks", toolingStacks], ["gov stacks", gov.stacks],
|
|
245
|
-
["stack-delivered rules", deliveredRules],
|
|
246
|
-
]) {
|
|
247
|
-
if (list.length === 0) {
|
|
248
|
-
console.error(`regen-hero: the ${label} catalog is empty, refusing to write a zeroed hero`)
|
|
249
|
-
process.exit(1)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// A terminal frame renders lines into a `<pre>` where the hero renders names
|
|
254
|
-
// into divs, so the two cannot share a value however similar the catalog read
|
|
255
|
-
// behind them looks. Padding happens before escaping, since an entity is longer
|
|
256
|
-
// than the character it replaces and would push a column out of line.
|
|
257
|
-
const pad = (value, width) => value + " ".repeat(Math.max(0, width - value.length))
|
|
258
|
-
const frameRow = (cells) => `<span class="frame">│</span> <span class="ok">✓</span> ${cells}`
|
|
259
|
-
|
|
260
|
-
// The frame sets `white-space: pre` at a fixed window width, so an overlong
|
|
261
|
-
// cell is clipped by the window edge rather than wrapped. Truncating here is
|
|
262
|
-
// what keeps the longest row inside the capture, and the ellipsis is what stops
|
|
263
|
-
// a clipped value from reading as the whole value.
|
|
264
|
-
const GLOB_WIDTH = 44
|
|
265
|
-
const clip = (value, width) =>
|
|
266
|
-
value.length <= width ? value : `${value.slice(0, width - 1)}…`
|
|
267
|
-
|
|
268
|
-
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`
|
|
269
|
-
|
|
270
|
-
const govStackRows = gov.stacks
|
|
271
|
-
.map((stack) =>
|
|
272
|
-
frameRow(
|
|
273
|
-
`<span class="name">${escape(pad(stack.name, 17))}</span>` +
|
|
274
|
-
`<span class="muted">${escape(pad(plural(stack.rules.length, "rule"), 11))}</span>` +
|
|
275
|
-
`<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : "")}</span>`,
|
|
276
|
-
),
|
|
277
|
-
)
|
|
278
|
-
.join("\n")
|
|
279
|
-
|
|
280
|
-
// Sampled from the stack-reached subset for the reason the hero column is, and
|
|
281
|
-
// showing each rule beside the glob that loads it rather than its description,
|
|
282
|
-
// since the glob is the mechanism a reader cannot otherwise see.
|
|
283
|
-
const deliveredEntries = gov.rules.filter((entry) => stacked.has(entry.name))
|
|
284
|
-
const govRuleRows = sample(deliveredEntries)
|
|
285
|
-
.map((entry) => {
|
|
286
|
-
const globs = [entry.paths ?? []].flat()
|
|
287
|
-
// A rule carrying no glob is not unscoped, it loads every session, and
|
|
288
|
-
// saying so is the contrast that makes the column mean anything.
|
|
289
|
-
const scope = globs.length > 0 ? clip(globs.join(" "), GLOB_WIDTH) : "every session"
|
|
290
|
-
return frameRow(
|
|
291
|
-
`<span class="name">${escape(pad(slug(entry), 24))}</span>` +
|
|
292
|
-
`<span class="domain">${escape(pad(`[${entry.domain}]`, 12))}</span>` +
|
|
293
|
-
`<span class="muted">${escape(scope)}</span>`,
|
|
294
|
-
)
|
|
295
|
-
})
|
|
296
|
-
.join("\n")
|
|
297
|
-
|
|
298
|
-
// A standard either governs an artifact by path or is opened by name when a
|
|
299
|
-
// session decides it needs it. Rendering the first and naming the second is the
|
|
300
|
-
// contrast, since nothing else in the catalog tells a reader that the corpus
|
|
301
|
-
// installs into no project and is read through a verb instead.
|
|
302
|
-
const standardEntries = JSON.parse(STANDARDS_JSON).standards
|
|
303
|
-
const standardRows = sample(standardEntries)
|
|
304
|
-
.map((entry) => {
|
|
305
|
-
const applies = [entry.appliesTo ?? []].flat()
|
|
306
|
-
const governs =
|
|
307
|
-
applies.length > 0 ? clip(applies.join(" "), GLOB_WIDTH) : "read by name"
|
|
308
|
-
return frameRow(
|
|
309
|
-
`<span class="name">${escape(pad(entry.name, 18))}</span>` +
|
|
310
|
-
`<span class="muted">${escape(governs)}</span>`,
|
|
311
|
-
)
|
|
312
|
-
})
|
|
313
|
-
.join("\n")
|
|
314
|
-
|
|
315
|
-
// A tooling stack is counted rather than listed, because what it lays down is
|
|
316
|
-
// dev dependencies, run scripts, and ignore groups rather than named entries a
|
|
317
|
-
// reader would recognize. The inheritance is the part worth showing, since it
|
|
318
|
-
// is why a stack carrying two of its own arrives with far more than two.
|
|
319
|
-
const toolingStackRows = toolingStacks
|
|
320
|
-
.map((stack) =>
|
|
321
|
-
frameRow(
|
|
322
|
-
`<span class="name">${escape(pad(stack.name, 14))}</span>` +
|
|
323
|
-
`<span class="muted">${escape(pad(plural(stack.devDeps, "dep"), 10))}</span>` +
|
|
324
|
-
`<span class="muted">${escape(pad(plural(stack.scripts, "script"), 12))}</span>` +
|
|
325
|
-
`<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : "")}</span>`,
|
|
326
|
-
),
|
|
327
|
-
)
|
|
328
|
-
.join("\n")
|
|
329
|
-
|
|
330
|
-
for (const [label, rows] of [
|
|
331
|
-
["governance stack rows", govStackRows],
|
|
332
|
-
["governance rule rows", govRuleRows],
|
|
333
|
-
["standard rows", standardRows],
|
|
334
|
-
["tooling stack rows", toolingStackRows],
|
|
335
|
-
]) {
|
|
336
|
-
if (rows === "") {
|
|
337
|
-
console.error(`regen-hero: the ${label} rendered empty, refusing to write a blank frame`)
|
|
338
|
-
process.exit(1)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const values = {
|
|
343
|
-
TOKENS: tokenCss,
|
|
344
|
-
GOV_STACK_ROWS: govStackRows,
|
|
345
|
-
GOV_RULE_ROWS: govRuleRows,
|
|
346
|
-
STANDARD_ROWS: standardRows,
|
|
347
|
-
TOOLING_STACK_ROWS: toolingStackRows,
|
|
348
|
-
SKILL_COUNT: String(skills.length),
|
|
349
|
-
RULE_COUNT: String(rules.length),
|
|
350
|
-
STANDARD_COUNT: String(standards.length),
|
|
351
|
-
GOV_STACK_COUNT: String(gov.stacks.length),
|
|
352
|
-
TOOLING_STACK_COUNT: String(toolingStacks.length),
|
|
353
|
-
COMMAND_COUNT: String(commandCount),
|
|
354
|
-
MARK_SVG,
|
|
355
|
-
FAVICON,
|
|
356
|
-
SKILL_ENTRIES: markup(featured(skills, FEATURED_SKILLS)),
|
|
357
|
-
RULE_ENTRIES: entries(deliveredRules),
|
|
358
|
-
STANDARD_ENTRIES: entries(standards),
|
|
359
|
-
SKILL_MORE: remaining(skills),
|
|
360
|
-
RULE_MORE: remaining(rules),
|
|
361
|
-
STANDARD_MORE: remaining(standards),
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Every template takes the same value map, so one carrying no count placeholder
|
|
365
|
-
// simply resolves none of them. What a template must not do is name a
|
|
366
|
-
// placeholder nobody fills, which the unresolved check below catches per file.
|
|
367
|
-
const templates = readdirSync(ASSET_DIR)
|
|
368
|
-
.filter((name) => name.endsWith(".html.tmpl"))
|
|
369
|
-
.sort()
|
|
370
|
-
|
|
371
|
-
if (templates.length === 0) {
|
|
372
|
-
console.error(`regen-hero: no templates under ${ASSET_DIR}`)
|
|
373
|
-
process.exit(1)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// The frames a README serves in both themes. The emitter already carries a
|
|
377
|
-
// `--color-light-*` role beside each `--color-*` one, so the light frame is the
|
|
378
|
-
// same template with the plain roles pointed at the light ones rather than a
|
|
379
|
-
// second copy of any hex value. The hero is a landing-page frame no README
|
|
380
|
-
// shows, so it stays dark.
|
|
381
|
-
const LIGHT_TEMPLATES = new Set(["install.html.tmpl"])
|
|
382
|
-
|
|
383
|
-
const lightRoles = [...tokenCss.matchAll(/--color-light-([a-z-]+):/g)].map((match) => match[1])
|
|
384
|
-
if (lightRoles.length === 0) {
|
|
385
|
-
console.error("regen-hero: token css carries no --color-light-* roles, refusing to write a light frame")
|
|
386
|
-
process.exit(1)
|
|
387
|
-
}
|
|
388
|
-
const lightTokenCss = [
|
|
389
|
-
tokenCss,
|
|
390
|
-
"",
|
|
391
|
-
" :root {",
|
|
392
|
-
...lightRoles.map((role) => ` --color-${role}: var(--color-light-${role});`),
|
|
393
|
-
" --frame-shadow: rgb(0 0 0 / 16%);",
|
|
394
|
-
" }",
|
|
395
|
-
].join("\n")
|
|
396
|
-
|
|
397
|
-
const frames = templates.flatMap((template) => {
|
|
398
|
-
const base = template.replace(/\.html\.tmpl$/, "")
|
|
399
|
-
const dark = { template, out: `${base}.html`, tokens: tokenCss }
|
|
400
|
-
return LIGHT_TEMPLATES.has(template)
|
|
401
|
-
? [dark, { template, out: `${base}-light.html`, tokens: lightTokenCss }]
|
|
402
|
-
: [dark]
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
for (const { template, out, tokens } of frames) {
|
|
406
|
-
let html = await Bun.file(ASSET_DIR + "/" + template).text()
|
|
407
|
-
for (const [key, value] of Object.entries({ ...values, TOKENS: tokens })) {
|
|
408
|
-
html = html.replaceAll(`{{${key}}}`, value)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const unresolved = html.match(/{{[A-Z_]+}}/g)
|
|
412
|
-
if (unresolved) {
|
|
413
|
-
console.error(`regen-hero: ${template} carries unresolved placeholders ${[...new Set(unresolved)].join(", ")}`)
|
|
414
|
-
process.exit(1)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await Bun.write(OUT_DIR + "/" + out, html)
|
|
418
|
-
}
|
|
419
|
-
'
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Repairs `core.bare`, which Claude Code's worktree entry leaves set in the
|
|
3
|
-
# shared config with nothing restoring it. The flag strands the main worktree
|
|
4
|
-
# and breaks the git reads that scope a verification run, so this runs ahead of
|
|
5
|
-
# every stage rather than as one of them.
|
|
6
|
-
#
|
|
7
|
-
# The rule itself lives in `scripts/lib/worktree.sh`, the one bash function in
|
|
8
|
-
# this repository under test, so this file is the invocation and never a second
|
|
9
|
-
# copy of the guard that spares a genuinely bare repository.
|
|
10
|
-
set -e
|
|
11
|
-
set -o pipefail
|
|
12
|
-
|
|
13
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
15
|
-
|
|
16
|
-
source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
17
|
-
source "$PROJECT_ROOT/scripts/lib/worktree.sh"
|
|
18
|
-
|
|
19
|
-
repair_bare_flag "$PROJECT_ROOT"
|
package/scripts/lib/worktree.sh
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
# Claude Code's worktree entry writes core.bare into the parent repository's
|
|
4
|
-
# shared config and its exit never restores it, stranding every later command in
|
|
5
|
-
# the main worktree. Sourced by verify.sh, which runs the repair before any stage
|
|
6
|
-
# because the flag breaks the git reads that scope the run.
|
|
7
|
-
|
|
8
|
-
repair_bare_flag() {
|
|
9
|
-
local root="${1:-$PROJECT_ROOT}"
|
|
10
|
-
local common_dir
|
|
11
|
-
[ "$(git -C "$root" config --get core.bare 2>/dev/null || echo false)" = true ] || return 0
|
|
12
|
-
|
|
13
|
-
common_dir=$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 0
|
|
14
|
-
# A genuinely bare repository keeps its objects at the root and has no .git
|
|
15
|
-
# directory, so it fails this test and keeps its flag.
|
|
16
|
-
[ "$(basename "$common_dir")" = ".git" ] || return 0
|
|
17
|
-
|
|
18
|
-
git -C "$root" config core.bare false
|
|
19
|
-
log_warn "Repaired core.bare, which worktree entry left set. Recovery is 'git config core.bare false'."
|
|
20
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(dirname "$(dirname "$SCRIPT_DIR")")}"
|
|
7
|
-
|
|
8
|
-
source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
9
|
-
source "$PROJECT_ROOT/scripts/lib/tooling.sh"
|
|
10
|
-
trap close_timeline EXIT
|
|
11
|
-
|
|
12
|
-
show_help() {
|
|
13
|
-
echo -e "${GREY}┌${NC}"
|
|
14
|
-
log_step "Tooling create usage"
|
|
15
|
-
echo -e "${GREY}│${NC} ${WHITE}Usage:${NC} canon tooling create [stack]"
|
|
16
|
-
echo -e "${GREY}│${NC}"
|
|
17
|
-
echo -e "${GREY}│${NC} Creates a new stack with stub manifest, reference, configs, and seeds."
|
|
18
|
-
echo -e "${GREY}│${NC}"
|
|
19
|
-
echo -e "${GREY}│${NC} ${WHITE}Arguments:${NC}"
|
|
20
|
-
echo -e "${GREY}│${NC} stack Name of the new stack to create"
|
|
21
|
-
echo -e "${GREY}│${NC}"
|
|
22
|
-
echo -e "${GREY}│${NC} ${WHITE}Options:${NC}"
|
|
23
|
-
echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
|
|
24
|
-
echo -e "${GREY}└${NC}"
|
|
25
|
-
exit 0
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
main() {
|
|
29
|
-
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
|
30
|
-
show_help
|
|
31
|
-
fi
|
|
32
|
-
|
|
33
|
-
local stack="$1"
|
|
34
|
-
|
|
35
|
-
echo -e "${GREY}┌${NC}"
|
|
36
|
-
echo -e "${GREY}│${NC} ${WHITE}canon tooling create${NC}"
|
|
37
|
-
|
|
38
|
-
if [ -z "$stack" ]; then
|
|
39
|
-
ask "Stack name?" "stack"
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
if [ -z "$stack" ]; then
|
|
43
|
-
log_error "Stack name is required"
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
if is_tooling_stack_excluded "$stack"; then
|
|
47
|
-
log_error "Reserved name: $stack. Managed by \`canon claude\`, not \`canon tooling\`."
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
local dest="$PROJECT_ROOT/tooling/$stack"
|
|
51
|
-
|
|
52
|
-
if [ -d "$dest" ]; then
|
|
53
|
-
log_error "Stack already exists: $stack"
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
log_step "Creating"
|
|
57
|
-
log_add "tooling/$stack/configs/"
|
|
58
|
-
log_add "tooling/$stack/seeds/"
|
|
59
|
-
log_add "tooling/$stack/manifest.toml"
|
|
60
|
-
log_add "tooling/$stack/reference.md"
|
|
61
|
-
|
|
62
|
-
select_option "Create stack at tooling/$stack?" "Yes" "No"
|
|
63
|
-
|
|
64
|
-
if [ "$SELECTED_OPTION" = "No" ]; then
|
|
65
|
-
log_warn "Cancelled"
|
|
66
|
-
exit 0
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
log_step "Applying"
|
|
70
|
-
|
|
71
|
-
mkdir -p "$dest/configs"
|
|
72
|
-
log_add "tooling/$stack/configs/"
|
|
73
|
-
|
|
74
|
-
mkdir -p "$dest/seeds"
|
|
75
|
-
log_add "tooling/$stack/seeds/"
|
|
76
|
-
|
|
77
|
-
cat >"$dest/manifest.toml" <<EOF
|
|
78
|
-
[stack]
|
|
79
|
-
name = "$stack"
|
|
80
|
-
extends = ""
|
|
81
|
-
runtime = ""
|
|
82
|
-
scaffold = ""
|
|
83
|
-
|
|
84
|
-
[dependencies.dev]
|
|
85
|
-
packages = []
|
|
86
|
-
|
|
87
|
-
[scripts]
|
|
88
|
-
"script-key" = "command --flag"
|
|
89
|
-
|
|
90
|
-
[gitignore]
|
|
91
|
-
"# $stack" = ["pattern/", ".file"]
|
|
92
|
-
EOF
|
|
93
|
-
log_add "tooling/$stack/manifest.toml"
|
|
94
|
-
cat >"$dest/reference.md" <<EOF
|
|
95
|
-
# Tooling $stack reference
|
|
96
|
-
|
|
97
|
-
## Overview
|
|
98
|
-
|
|
99
|
-
[One or two sentences: what this stack provides and its purpose.]
|
|
100
|
-
|
|
101
|
-
EOF
|
|
102
|
-
log_add "tooling/$stack/reference.md"
|
|
103
|
-
|
|
104
|
-
trap - EXIT
|
|
105
|
-
echo -e "${GREY}└${NC}\n"
|
|
106
|
-
echo -e "${GREEN}✓ Stack created${NC}"
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
main "$@"
|