@erclx/canon 5.53.0 → 5.55.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/CHANGELOG.md +18 -0
- package/README.md +10 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -5
- package/claude/skills/create-skill/SKILL.md +1 -1
- package/claude/skills/design-identity/REQUIREMENT.md +2 -2
- package/claude/skills/design-identity/SKILL.md +6 -6
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +4 -4
- package/claude/skills/design-record/references/folder.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/document-health/SKILL.md +6 -6
- package/claude/skills/document-repair/SKILL.md +5 -5
- package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/SKILL.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
- package/claude/skills/pick-by-eye/SKILL.md +34 -41
- package/claude/skills/pick-by-eye/references/board.md +42 -0
- package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/target-check/SKILL.md +6 -8
- package/claude/skills/target-check/references/domains.md +1 -4
- package/claude/skills/target-setup/SKILL.md +8 -8
- package/claude/skills/target-update/SKILL.md +5 -5
- package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
- package/claude/skills/teach-craft/SKILL.md +32 -1
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
- package/claude/skills/teach-craft/references/pedagogy.md +29 -2
- package/docs/agents/install-and-sync.md +2 -10
- package/docs/agents/output-shape.md +11 -10
- package/docs/target/migrations.md +8 -3
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -1
- package/docs/workflow/skill-map.md +0 -1
- package/docs/workflow/visual-design-workflow.md +1 -4
- package/governance/detect.toml +1 -6
- package/governance/renames.toml +0 -1
- package/package.json +2 -5
- package/scripts/core/install-check.sh +1 -1
- package/scripts/lib/ui.sh +12 -65
- package/src/audits/run.ts +1 -1
- package/src/claude/cases/misc.ts +0 -5
- package/src/claude/routing.ts +1 -1
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli/help.ts +0 -3
- package/src/cli/run.ts +5 -6
- package/src/cli/ui.ts +87 -28
- package/src/cli.ts +6 -2
- package/src/commands/claude.ts +2 -3
- package/src/commands/design.ts +20 -324
- package/src/commands/gate.ts +5 -3
- package/src/commands/gov.ts +11 -21
- package/src/commands/labels.ts +1 -1
- package/src/commands/pr.ts +1 -1
- package/src/commands/sync.ts +5 -6
- package/src/commands/tooling.ts +5 -8
- package/src/commands/transcripts.ts +6 -11
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +4 -20
- package/src/gov/audit.ts +1 -6
- package/src/hooks/post-merge.ts +2 -5
- package/src/markdown/scope.ts +2 -4
- package/src/process/harness.ts +3 -4
- package/src/sandbox/equivalence.ts +0 -1
- package/src/sandbox/provision.ts +8 -13
- package/src/sandbox/scenario.ts +6 -12
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/engine.ts +3 -8
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/src/sync/workflow.ts +3 -5
- package/src/tooling/verify-stack.ts +1 -1
- package/tooling/base/configs/.husky/post-merge +6 -10
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +1 -1
- package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
- package/claude/skills/canvas-board/SKILL.md +0 -143
- package/claude/skills/canvas-board/references/canvas.md +0 -170
- package/governance/rules/ui/canvas.md +0 -13
- package/scripts/core/check-color-source.sh +0 -51
- package/src/canvas/address.ts +0 -227
- package/src/canvas/arrange.ts +0 -66
- package/src/canvas/capture.ts +0 -391
- package/src/canvas/client/app.tsx +0 -188
- package/src/canvas/client/inspector/align-grid.tsx +0 -162
- package/src/canvas/client/inspector/color-field.tsx +0 -398
- package/src/canvas/client/inspector/color-inputs.tsx +0 -139
- package/src/canvas/client/inspector/color-picker.tsx +0 -668
- package/src/canvas/client/inspector/color-space.ts +0 -240
- package/src/canvas/client/inspector/field.tsx +0 -263
- package/src/canvas/client/inspector/fields.css +0 -763
- package/src/canvas/client/inspector/section.tsx +0 -27
- package/src/canvas/client/inspector/size-field.tsx +0 -69
- package/src/canvas/client/inspector/token-select.tsx +0 -109
- package/src/canvas/client/inspector/values.ts +0 -297
- package/src/canvas/client/inspector.tsx +0 -692
- package/src/canvas/client/keys.ts +0 -71
- package/src/canvas/client/layers.tsx +0 -118
- package/src/canvas/client/main.tsx +0 -71
- package/src/canvas/client/pages-panel.tsx +0 -212
- package/src/canvas/client/panel-handle.tsx +0 -90
- package/src/canvas/client/selection.tsx +0 -199
- package/src/canvas/client/shell.css +0 -762
- package/src/canvas/client/state.ts +0 -792
- package/src/canvas/client/surface.tsx +0 -803
- package/src/canvas/client/theme-panel.tsx +0 -55
- package/src/canvas/client/tools.tsx +0 -153
- package/src/canvas/content.ts +0 -781
- package/src/canvas/edit.ts +0 -575
- package/src/canvas/editing.ts +0 -203
- package/src/canvas/fit.ts +0 -238
- package/src/canvas/global.d.ts +0 -15
- package/src/canvas/history.ts +0 -116
- package/src/canvas/index.html +0 -20
- package/src/canvas/server.ts +0 -766
- package/src/canvas/tokens.ts +0 -221
- package/src/commands/canvas.ts +0 -1097
- package/src/design/adapter.ts +0 -59
- package/src/design/base.css +0 -203
- package/src/design/brand-favicon.ts +0 -18
- package/src/design/neutral.ts +0 -127
- package/src/design/project-fonts.ts +0 -392
package/src/design/regen.ts
CHANGED
|
@@ -1,42 +1,20 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
-
import { buildDesignCss } from '@/design/css'
|
|
4
3
|
import { renderDesignDocument } from '@/design/document'
|
|
5
|
-
import { NEUTRAL_TOKENS } from '@/design/neutral'
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* The
|
|
13
|
-
* installs, and neither is edited by hand. That is the cost the operator took
|
|
14
|
-
* on when the tokens moved into a module: two artifacts where the record used
|
|
15
|
-
* to be one, and a regen step that has to run or both go stale silently. The
|
|
16
|
-
* gate stage is what keeps silently out of it.
|
|
6
|
+
* The record the token module renders, written by `canon design regen` and
|
|
7
|
+
* asserted for drift by the `design` stage of `bun run check`. It is never
|
|
8
|
+
* edited by hand. That is the cost the operator took on when the tokens moved
|
|
9
|
+
* into a module: a regen step that has to run or the record goes stale
|
|
10
|
+
* silently. The gate stage is what keeps silently out of it.
|
|
17
11
|
*/
|
|
18
12
|
|
|
19
13
|
/** The rendered record, at the path `claude/skills/design-record/references/design.md` fixes for it. */
|
|
20
14
|
export const DESIGN_DOCUMENT = join('canon', 'DESIGN.md')
|
|
21
15
|
|
|
22
|
-
/**
|
|
23
|
-
* The base stylesheet a target installs. It sits inside `src/` because that is
|
|
24
|
-
* the folder the published package ships, and beside the module that writes it
|
|
25
|
-
* because a generated file kept away from its generator is one nobody thinks to
|
|
26
|
-
* regenerate.
|
|
27
|
-
*/
|
|
28
|
-
export const DESIGN_BASE_CSS = join('src', 'design', 'base.css')
|
|
29
|
-
|
|
30
|
-
const BASE_BANNER = [
|
|
31
|
-
'Generated by `canon design regen` from src/design/neutral.ts. Do not edit.',
|
|
32
|
-
'This is the base half of a design install. A target overrides a value in',
|
|
33
|
-
'.claude/design/project/, which the three-way merge leaves alone, so this',
|
|
34
|
-
'file changes freely and nothing a project wrote lives in it.',
|
|
35
|
-
].join('\n ')
|
|
36
|
-
|
|
37
16
|
export interface RegenResult {
|
|
38
17
|
readonly documentPath: string
|
|
39
|
-
readonly cssPath: string
|
|
40
18
|
}
|
|
41
19
|
|
|
42
20
|
function write(root: string, rel: string, body: string): string {
|
|
@@ -49,10 +27,5 @@ function write(root: string, rel: string, body: string): string {
|
|
|
49
27
|
export function regenDesign(root: string): RegenResult {
|
|
50
28
|
return {
|
|
51
29
|
documentPath: write(root, DESIGN_DOCUMENT, renderDesignDocument()),
|
|
52
|
-
cssPath: write(
|
|
53
|
-
root,
|
|
54
|
-
DESIGN_BASE_CSS,
|
|
55
|
-
buildDesignCss(NEUTRAL_TOKENS, { banner: BASE_BANNER }),
|
|
56
|
-
),
|
|
57
30
|
}
|
|
58
31
|
}
|
package/src/design/render.ts
CHANGED
|
@@ -259,7 +259,7 @@ ${previewChrome()}
|
|
|
259
259
|
</head>
|
|
260
260
|
<body>
|
|
261
261
|
<h1>Design tokens</h1>
|
|
262
|
-
<p class="note">Generated from <code>canon/DESIGN.md</code> by <code>
|
|
262
|
+
<p class="note">Generated from <code>canon/DESIGN.md</code> by <code>bun run web:previews</code>. Token preview only, not a screen mock.</p>${summary}
|
|
263
263
|
${sections.join('\n')}
|
|
264
264
|
</body>
|
|
265
265
|
</html>
|
package/src/design/tokens.ts
CHANGED
|
@@ -84,8 +84,9 @@ const MONO = 'Noto Sans Mono, DejaVu Sans Mono, monospace'
|
|
|
84
84
|
* The proportional stack every role but `code` declares. Geist is the face the
|
|
85
85
|
* visual-direction track set throughout, embedded through `FONT_FACES`.
|
|
86
86
|
*
|
|
87
|
-
* The stack stops at two names and a generic because
|
|
88
|
-
* written by `
|
|
87
|
+
* The stack stops at two names and a generic because
|
|
88
|
+
* `web/src/styles/tokens.css` is written by `bun run web:tokens` and formatted
|
|
89
|
+
* by oxfmt, and a declaration
|
|
89
90
|
* past 80 columns is wrapped by the second and flattened by the first. Every
|
|
90
91
|
* emitted line stays inside the width so the two writers never disagree.
|
|
91
92
|
*/
|
package/src/gate/sequencer.ts
CHANGED
package/src/gate/stages.ts
CHANGED
|
@@ -119,12 +119,6 @@ const TESTS_SCOPE = new RegExp(
|
|
|
119
119
|
].join('|'),
|
|
120
120
|
)
|
|
121
121
|
|
|
122
|
-
const script = (name: string, failure: string): Check => ({
|
|
123
|
-
kind: 'command',
|
|
124
|
-
argv: ['bash', `scripts/core/${name}`],
|
|
125
|
-
failure,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
122
|
/**
|
|
129
123
|
* Every stage the gate runs, in the order it runs them.
|
|
130
124
|
*
|
|
@@ -212,10 +206,10 @@ export const STAGES: readonly Stage[] = [
|
|
|
212
206
|
},
|
|
213
207
|
{
|
|
214
208
|
// `canon/DESIGN.md`, the web stylesheet, and the tab icon are written
|
|
215
|
-
// from `src/design/tokens.ts`,
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
209
|
+
// from `src/design/tokens.ts`, and none is edited by hand. Three
|
|
210
|
+
// artifacts from one token source is the cost of the token move, and a
|
|
211
|
+
// render step that has to run is only safe while something fails when it
|
|
212
|
+
// did not, which is this. The record and the worked example are
|
|
219
213
|
// then counted against the prose budget, which the drift asserts cannot
|
|
220
214
|
// see since a regenerated paragraph agrees with its source.
|
|
221
215
|
id: 'design',
|
|
@@ -232,12 +226,6 @@ export const STAGES: readonly Stage[] = [
|
|
|
232
226
|
failure:
|
|
233
227
|
'The design record drifted from the token source. Run bun run check and commit canon/DESIGN.md.',
|
|
234
228
|
},
|
|
235
|
-
{
|
|
236
|
-
kind: 'drift',
|
|
237
|
-
pathspec: 'src/design/base.css',
|
|
238
|
-
failure:
|
|
239
|
-
'The base stylesheet drifted from the token source. Run bun run check and commit src/design/base.css.',
|
|
240
|
-
},
|
|
241
229
|
{
|
|
242
230
|
kind: 'command',
|
|
243
231
|
argv: ['bun', 'run', 'web:tokens'],
|
|
@@ -536,10 +524,6 @@ export const STAGES: readonly Stage[] = [
|
|
|
536
524
|
argv: ['bun', 'run', 'check:shell'],
|
|
537
525
|
failure: 'Shell check failed',
|
|
538
526
|
},
|
|
539
|
-
script(
|
|
540
|
-
'check-color-source.sh',
|
|
541
|
-
'A color escape is defined outside scripts/lib/ui.sh.',
|
|
542
|
-
),
|
|
543
527
|
],
|
|
544
528
|
success: 'Shell check passed',
|
|
545
529
|
},
|
package/src/gov/audit.ts
CHANGED
|
@@ -45,13 +45,8 @@ export const RULE_GATED_FOLDERS: readonly string[] = [
|
|
|
45
45
|
/**
|
|
46
46
|
* Rule files in a gated folder whose findings `--gate` still passes, one path
|
|
47
47
|
* each and never a folder, so a new rule beside one is gated the day it lands.
|
|
48
|
-
*
|
|
49
|
-
* `ui/canvas.md` carries a shape finding and no ledger, and v149.2 removes the
|
|
50
|
-
* rule, so this entry goes with it.
|
|
51
48
|
*/
|
|
52
|
-
export const RULE_GATE_EXEMPT: readonly string[] = [
|
|
53
|
-
join('governance', 'rules', 'ui', 'canvas.md'),
|
|
54
|
-
]
|
|
49
|
+
export const RULE_GATE_EXEMPT: readonly string[] = []
|
|
55
50
|
|
|
56
51
|
/** The folders whose rules claim outside practice, so each carries a ledger. */
|
|
57
52
|
export const PRACTICE_FOLDERS: readonly string[] = [
|
package/src/hooks/post-merge.ts
CHANGED
|
@@ -359,11 +359,8 @@ export async function runPostMerge(input: PostMergeInput): Promise<void> {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
if (!isSkipped('CANON_SKIP_UPGRADE')) {
|
|
362
|
-
await step(
|
|
363
|
-
|
|
364
|
-
at([...STEP_VERBS.upgrade, '--json'], { CANON_NON_INTERACTIVE: '1' }),
|
|
365
|
-
(result, fields) =>
|
|
366
|
-
moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
|
|
362
|
+
await step(input, at([...STEP_VERBS.upgrade, '--json']), (result, fields) =>
|
|
363
|
+
moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
|
|
367
364
|
)
|
|
368
365
|
}
|
|
369
366
|
|
package/src/markdown/scope.ts
CHANGED
|
@@ -136,9 +136,6 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
136
136
|
'claude/skills/canon-feedback/REQUIREMENT.md',
|
|
137
137
|
'claude/skills/canon-feedback/SKILL.md',
|
|
138
138
|
'claude/skills/canon-feedback/references/template.md',
|
|
139
|
-
'claude/skills/canvas-board/REQUIREMENT.md',
|
|
140
|
-
'claude/skills/canvas-board/SKILL.md',
|
|
141
|
-
'claude/skills/canvas-board/references/canvas.md',
|
|
142
139
|
'claude/skills/ci-workflow/REQUIREMENT.md',
|
|
143
140
|
'claude/skills/ci-workflow/SKILL.md',
|
|
144
141
|
'claude/skills/ci-workflow/references/workflows.md',
|
|
@@ -203,6 +200,7 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
203
200
|
'claude/skills/document-repair/references/batches.md',
|
|
204
201
|
'claude/skills/pick-by-eye/REQUIREMENT.md',
|
|
205
202
|
'claude/skills/pick-by-eye/SKILL.md',
|
|
203
|
+
'claude/skills/pick-by-eye/references/board.md',
|
|
206
204
|
'claude/skills/pick-by-eye/references/live-arms.md',
|
|
207
205
|
'claude/skills/write-doc/REQUIREMENT.md',
|
|
208
206
|
'claude/skills/write-doc/SKILL.md',
|
|
@@ -375,6 +373,7 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
375
373
|
'claude/skills/task-complete/references/unusable-baseline.md',
|
|
376
374
|
'claude/skills/teach-craft/REQUIREMENT.md',
|
|
377
375
|
'claude/skills/teach-craft/SKILL.md',
|
|
376
|
+
'claude/skills/teach-craft/references/adopted.md',
|
|
378
377
|
'claude/skills/teach-craft/references/lesson-craft.md',
|
|
379
378
|
'claude/skills/teach-craft/references/pedagogy.md',
|
|
380
379
|
'claude/skills/test-craft/REQUIREMENT.md',
|
|
@@ -399,7 +398,6 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
399
398
|
'claude/skills/write-human/references/machine-tells.md',
|
|
400
399
|
'claude/skills/youtube-transcripts/REQUIREMENT.md',
|
|
401
400
|
'claude/skills/youtube-transcripts/SKILL.md',
|
|
402
|
-
'.claude/skills/internal-canvas/REQUIREMENT.md',
|
|
403
401
|
'.claude/skills/internal-claude/REQUIREMENT.md',
|
|
404
402
|
'.claude/skills/internal-docs/REQUIREMENT.md',
|
|
405
403
|
'.claude/skills/internal-governance/REQUIREMENT.md',
|
package/src/process/harness.ts
CHANGED
|
@@ -90,8 +90,7 @@ export function snapshotStateDir(): string {
|
|
|
90
90
|
*
|
|
91
91
|
* A git hook exports `GIT_DIR`, which would resolve a fixture's git-aware
|
|
92
92
|
* reads against this checkout instead of the temporary directory a case
|
|
93
|
-
* builds, so every spawn drops the `GIT_` prefix
|
|
94
|
-
* flag every case needs to avoid a picker blocking on stdin.
|
|
93
|
+
* builds, so every spawn drops the `GIT_` prefix.
|
|
95
94
|
*
|
|
96
95
|
* `CANON_STATE_DIR` and `CANON_SANDBOX_DIR` get the same treatment as `GIT_DIR`,
|
|
97
96
|
* each pointed at a folder under the case's own `cwd` rather than dropped,
|
|
@@ -99,7 +98,8 @@ export function snapshotStateDir(): string {
|
|
|
99
98
|
* `HOME` to this machine's real `~/.local/state/canon`. `stateDir()` and
|
|
100
99
|
* `sandboxTree()` resolve the same three ways and share that parent, so both
|
|
101
100
|
* overrides move together. A case explicitly passing its own value through
|
|
102
|
-
* `options.env` still wins
|
|
101
|
+
* `options.env` still wins. The child's stdin is a pipe rather than a
|
|
102
|
+
* terminal, so every case runs headless.
|
|
103
103
|
*
|
|
104
104
|
* The `stateDir()` snapshot before and after the spawn is what actually
|
|
105
105
|
* catches an escape past that redirection, since a default can be wrong in a
|
|
@@ -125,7 +125,6 @@ export function runCli(
|
|
|
125
125
|
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
126
126
|
env: {
|
|
127
127
|
...inherited,
|
|
128
|
-
CANON_NON_INTERACTIVE: '1',
|
|
129
128
|
CANON_STATE_DIR: join(options.cwd, '.canon-state'),
|
|
130
129
|
CANON_SANDBOX_DIR: join(options.cwd, '.canon-state', 'sandbox'),
|
|
131
130
|
...options.env,
|
package/src/sandbox/provision.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
logStep,
|
|
28
28
|
logWarn,
|
|
29
29
|
outro,
|
|
30
|
-
|
|
30
|
+
outroSuccess,
|
|
31
31
|
select,
|
|
32
32
|
} from '@/cli/ui'
|
|
33
33
|
|
|
@@ -99,9 +99,7 @@ function readGit(cwd: string, args: string[]): string | undefined {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function finish(message: string): void {
|
|
102
|
-
|
|
103
|
-
outro()
|
|
104
|
-
process.stderr.write(`\n${GREEN}${message}${NC}\n`)
|
|
102
|
+
outroSuccess(message)
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
function stageAnchorTree(
|
|
@@ -210,7 +208,7 @@ function install(
|
|
|
210
208
|
env: NodeJS.ProcessEnv,
|
|
211
209
|
): void {
|
|
212
210
|
const result = spawnSync('bun', [join(root, 'src', 'cli.ts'), ...args], {
|
|
213
|
-
env
|
|
211
|
+
env,
|
|
214
212
|
encoding: 'utf8',
|
|
215
213
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
216
214
|
})
|
|
@@ -432,10 +430,7 @@ async function provisionScenario(
|
|
|
432
430
|
baseEnv: NodeJS.ProcessEnv,
|
|
433
431
|
): Promise<void> {
|
|
434
432
|
const env: NodeJS.ProcessEnv = { ...baseEnv }
|
|
435
|
-
if (scenario.arm !== undefined)
|
|
436
|
-
env.SANDBOX_SCENARIO = scenario.arm
|
|
437
|
-
env.CANON_NON_INTERACTIVE = '1'
|
|
438
|
-
}
|
|
433
|
+
if (scenario.arm !== undefined) env.SANDBOX_SCENARIO = scenario.arm
|
|
439
434
|
|
|
440
435
|
const sandboxDir = join(root, 'sandbox')
|
|
441
436
|
const declared = declareScenario(root, scenario, env)
|
|
@@ -475,14 +470,14 @@ async function provisionScenario(
|
|
|
475
470
|
must(commitScenarioChanges(sandbox, isSkipAutoCommit, env))
|
|
476
471
|
tagBaseline(sandbox, env)
|
|
477
472
|
|
|
478
|
-
finish('
|
|
473
|
+
finish('Sandbox Ready')
|
|
479
474
|
}
|
|
480
475
|
|
|
481
476
|
function cleanSandbox(sandbox: string): void {
|
|
482
477
|
logStep('Removing sandbox')
|
|
483
478
|
rmSync(sandbox, { recursive: true, force: true })
|
|
484
479
|
logRemove(sandbox)
|
|
485
|
-
finish('
|
|
480
|
+
finish('Sandbox clean')
|
|
486
481
|
}
|
|
487
482
|
|
|
488
483
|
function isAtBaseline(sandbox: string): boolean {
|
|
@@ -520,7 +515,7 @@ async function resetSandbox(sandbox: string): Promise<void> {
|
|
|
520
515
|
|
|
521
516
|
if (isAtBaseline(sandbox)) {
|
|
522
517
|
logInfo('At baseline')
|
|
523
|
-
finish('
|
|
518
|
+
finish('Sandbox at baseline')
|
|
524
519
|
return
|
|
525
520
|
}
|
|
526
521
|
|
|
@@ -556,7 +551,7 @@ async function resetSandbox(sandbox: string): Promise<void> {
|
|
|
556
551
|
must(run('git', ['checkout-index', '-a', '-f'], quiet))
|
|
557
552
|
}
|
|
558
553
|
logInfo('Sandbox reset to baseline')
|
|
559
|
-
finish('
|
|
554
|
+
finish('Sandbox reset complete')
|
|
560
555
|
}
|
|
561
556
|
|
|
562
557
|
/**
|
package/src/sandbox/scenario.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { createRequire } from 'node:module'
|
|
14
14
|
import { dirname, join, relative } from 'node:path'
|
|
15
15
|
import {
|
|
16
|
+
isNonInteractive,
|
|
16
17
|
logError,
|
|
17
18
|
logInfo,
|
|
18
19
|
logStep,
|
|
@@ -580,8 +581,9 @@ export function createStageContext(options: ContextOptions): StageContext {
|
|
|
580
581
|
}
|
|
581
582
|
|
|
582
583
|
/**
|
|
583
|
-
* Picks the arm: a routed name first,
|
|
584
|
-
*
|
|
584
|
+
* Picks the arm: a routed name first, echoed on the frame, then a prompt. A
|
|
585
|
+
* headless caller naming no arm is refused rather than handed the first one,
|
|
586
|
+
* since a verdict on an arm nobody chose reads as a verdict on the scenario.
|
|
585
587
|
*/
|
|
586
588
|
async function routeArm(
|
|
587
589
|
names: readonly string[],
|
|
@@ -600,17 +602,9 @@ async function routeArm(
|
|
|
600
602
|
const routed = env.SANDBOX_SCENARIO ?? ''
|
|
601
603
|
if (routed !== '') return echo(routed)
|
|
602
604
|
|
|
603
|
-
|
|
604
|
-
// `is_non_interactive` read the shell it ran in.
|
|
605
|
-
const isHeadless =
|
|
606
|
-
env.CANON_NON_INTERACTIVE === '1' ||
|
|
607
|
-
// canon-keep-retired
|
|
608
|
-
env.AITK_NON_INTERACTIVE === '1'
|
|
609
|
-
if (isHeadless) return echo(names[0] ?? DEFAULT_ARM_NAME)
|
|
610
|
-
|
|
611
|
-
if (!process.stdin.isTTY) {
|
|
605
|
+
if (isNonInteractive()) {
|
|
612
606
|
logError(
|
|
613
|
-
`${prompt} requires a TTY. Pass
|
|
607
|
+
`${prompt} requires a TTY. Pass the arm as the scenario argument, one of: ${names.join(', ')}.`,
|
|
614
608
|
)
|
|
615
609
|
throw new ScenarioStop(1, 'exited')
|
|
616
610
|
}
|
|
@@ -15,8 +15,7 @@ import { isMarked } from '@/sweep/exempt-marker'
|
|
|
15
15
|
* number resolves.
|
|
16
16
|
*
|
|
17
17
|
* That is also what keeps the check a prose pattern rather than a parser.
|
|
18
|
-
* `src/design/
|
|
19
|
-
* `#191512`, which no width or boundary rule separates from a pull request
|
|
18
|
+
* `src/design/tokens.ts` carries twenty values shaped `#191512`, which no width or boundary rule separates from a pull request
|
|
20
19
|
* number, so telling them apart would need the line's syntactic position.
|
|
21
20
|
*/
|
|
22
21
|
export const SHIPPED_CORPORA = [
|
package/src/sync/check.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { basename, join, sep } from 'node:path'
|
|
3
3
|
import { execa } from 'execa'
|
|
4
4
|
import { gitEnv } from '@/git/env'
|
|
5
|
-
import { createDesignAdapter, DESIGN_INSTALL_DIR } from '@/design/adapter'
|
|
6
5
|
import { createGovAdapter } from '@/gov/adapter'
|
|
7
6
|
import { successorResolver } from '@/gov/renames'
|
|
8
7
|
import { lookupRules } from '@/gov/install'
|
|
@@ -46,17 +45,11 @@ import { readSkew, type SkewReport } from '@/version/skew'
|
|
|
46
45
|
* being one of these, because `src/tooling/` never calls `planSync`, so the
|
|
47
46
|
* three lookups below have no entry to offer it. Standards and snippets left
|
|
48
47
|
* the list with their install channels: nothing writes either corpus into a
|
|
49
|
-
* target, so there is no installed copy to attribute.
|
|
50
|
-
*
|
|
51
|
-
* Design joins as a scanned domain rather than a stamp-only one because its
|
|
52
|
-
* base file is attributed the same way a rule is, and its install marker is
|
|
53
|
-
* what keeps it off a target that never asked for it: a project with no
|
|
54
|
-
* `.claude/design/` is never scanned, so design values arrive on an install
|
|
55
|
-
* rather than on the next sync.
|
|
48
|
+
* target, so there is no installed copy to attribute. Design left with its
|
|
49
|
+
* install verb for the same reason.
|
|
56
50
|
*/
|
|
57
51
|
export const SCANNED_DOMAINS = [
|
|
58
52
|
'governance',
|
|
59
|
-
'design',
|
|
60
53
|
] as const satisfies readonly StampDomain[]
|
|
61
54
|
|
|
62
55
|
export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
|
|
@@ -68,17 +61,14 @@ export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
|
|
|
68
61
|
*/
|
|
69
62
|
const SYNCED_SOURCES: Record<ScannedDomain, string> = {
|
|
70
63
|
governance: 'governance/rules/',
|
|
71
|
-
design: 'src/design/',
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
const ADAPTERS: Record<ScannedDomain, (root: string) => SyncAdapter> = {
|
|
75
67
|
governance: createGovAdapter,
|
|
76
|
-
design: createDesignAdapter,
|
|
77
68
|
}
|
|
78
69
|
|
|
79
70
|
const INSTALL_MARKERS: Record<ScannedDomain, readonly string[]> = {
|
|
80
71
|
governance: ['.claude', 'rules', 'canon'],
|
|
81
|
-
design: DESIGN_INSTALL_DIR.split(sep),
|
|
82
72
|
}
|
|
83
73
|
|
|
84
74
|
export interface StateCounts {
|
|
@@ -215,33 +205,18 @@ export interface CheckReport {
|
|
|
215
205
|
readonly skew: SkewReport
|
|
216
206
|
}
|
|
217
207
|
|
|
218
|
-
/**
|
|
219
|
-
* Scanned domains a target takes deliberately rather than by being managed.
|
|
220
|
-
*
|
|
221
|
-
* An absent one is a choice, so it is never reported as unstamped. Governance
|
|
222
|
-
* wants the opposite reading, since a managed target without it has yet to
|
|
223
|
-
* install what every project is expected to carry, and naming it is the only
|
|
224
|
-
* place that shows up.
|
|
225
|
-
*/
|
|
226
|
-
export const OPT_IN_DOMAINS: readonly ScannedDomain[] = ['design']
|
|
227
|
-
|
|
228
208
|
/**
|
|
229
209
|
* Scanned domains this target should have stamped and has not, which is the
|
|
230
210
|
* one line a domain nobody installed ever appears on.
|
|
231
211
|
*
|
|
232
212
|
* An unmigrated domain is excluded because the relocation is its remedy rather
|
|
233
|
-
* than a sync
|
|
234
|
-
* because there is nothing there to stamp and the sync it would name refuses.
|
|
213
|
+
* than a sync.
|
|
235
214
|
*/
|
|
236
215
|
export function uncoveredDomains(report: CheckReport): ScannedDomain[] {
|
|
237
216
|
const unmigrated = new Set(report.unmigrated.map((entry) => entry.domain))
|
|
238
|
-
const installed = new Set(report.domains.map((entry) => entry.domain))
|
|
239
217
|
|
|
240
218
|
return SCANNED_DOMAINS.filter(
|
|
241
|
-
(domain) =>
|
|
242
|
-
!report.covers.includes(domain) &&
|
|
243
|
-
!unmigrated.has(domain) &&
|
|
244
|
-
(!OPT_IN_DOMAINS.includes(domain) || installed.has(domain)),
|
|
219
|
+
(domain) => !report.covers.includes(domain) && !unmigrated.has(domain),
|
|
245
220
|
)
|
|
246
221
|
}
|
|
247
222
|
|
package/src/sync/engine.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
logStep,
|
|
27
27
|
logWarn,
|
|
28
28
|
outro,
|
|
29
|
-
|
|
29
|
+
outroSuccess,
|
|
30
30
|
select,
|
|
31
31
|
} from '@/cli/ui'
|
|
32
32
|
import { compareVersions, parseVersion } from '@/version/compare'
|
|
@@ -436,7 +436,6 @@ export async function runDomainSync(
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
const plan = planSync(adapter, resolved)
|
|
439
|
-
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
440
439
|
|
|
441
440
|
if (
|
|
442
441
|
!existsSync(adapter.installedRoot(resolved)) &&
|
|
@@ -453,8 +452,7 @@ export async function runDomainSync(
|
|
|
453
452
|
if (count === 0) {
|
|
454
453
|
await adapter.onComplete?.(resolved)
|
|
455
454
|
await recordStamp(adapter, resolved, new Date())
|
|
456
|
-
|
|
457
|
-
process.stderr.write(`${GREEN}✓ Everything up to date${NC}\n`)
|
|
455
|
+
outroSuccess('Everything up to date')
|
|
458
456
|
return 0
|
|
459
457
|
}
|
|
460
458
|
|
|
@@ -489,10 +487,7 @@ export async function runDomainSync(
|
|
|
489
487
|
await adapter.onComplete?.(resolved)
|
|
490
488
|
await recordStamp(adapter, resolved, new Date())
|
|
491
489
|
|
|
492
|
-
|
|
493
|
-
process.stderr.write(
|
|
494
|
-
`${GREEN}✓ Sync complete${NC} ${GREY}(${count} ${adapter.unit})${NC}\n`,
|
|
495
|
-
)
|
|
490
|
+
outroSuccess('Sync complete', `(${count} ${adapter.unit})`)
|
|
496
491
|
return 0
|
|
497
492
|
}
|
|
498
493
|
|
package/src/sync/stamp.ts
CHANGED
|
@@ -10,17 +10,17 @@ import { compareVersions, parseVersion } from '@/version/compare'
|
|
|
10
10
|
import { readInstalled } from '@/version/installed'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Domains the stamp can record. Governance
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* file hashes.
|
|
13
|
+
* Domains the stamp can record. Governance attributes file by file through
|
|
14
|
+
* the sync engine. Tooling runs its own inject and manifest machinery, so it
|
|
15
|
+
* records the stack chain it resolved instead and carries no file hashes.
|
|
17
16
|
*
|
|
18
17
|
* A stamp written before the standards or snippets install channel closed
|
|
19
|
-
* still carries a `standards` or `snippets` record
|
|
18
|
+
* still carries a `standards` or `snippets` record, and one written before the
|
|
19
|
+
* design install retired carries a `design` record. `isStamp` ignores the key
|
|
20
20
|
* and `sortDomains` drops it on the next write, so the target loses a domain
|
|
21
21
|
* nothing can refresh rather than losing the whole file.
|
|
22
22
|
*/
|
|
23
|
-
export const STAMP_DOMAINS = ['governance', '
|
|
23
|
+
export const STAMP_DOMAINS = ['governance', 'tooling'] as const
|
|
24
24
|
|
|
25
25
|
export type StampDomain = (typeof STAMP_DOMAINS)[number]
|
|
26
26
|
|
package/src/sync/target.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { DESIGN_INSTALL_DIR } from '@/design/adapter'
|
|
4
3
|
import { isDirectory } from '@/targets/validate'
|
|
5
4
|
|
|
6
|
-
export const SYNC_DOMAINS = ['governance', '
|
|
5
|
+
export const SYNC_DOMAINS = ['governance', 'claude'] as const
|
|
7
6
|
|
|
8
7
|
export type SyncDomain = (typeof SYNC_DOMAINS)[number]
|
|
9
8
|
|
|
@@ -14,7 +13,6 @@ export interface DomainState {
|
|
|
14
13
|
|
|
15
14
|
const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
16
15
|
governance: join('.claude', 'rules'),
|
|
17
|
-
design: DESIGN_INSTALL_DIR,
|
|
18
16
|
claude: '.claude',
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -26,7 +24,6 @@ const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
|
26
24
|
*/
|
|
27
25
|
const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
|
|
28
26
|
governance: ['.claude/rules/', '.claude/GOV.md'],
|
|
29
|
-
design: ['.claude/design/'],
|
|
30
27
|
claude: ['.gitignore'],
|
|
31
28
|
}
|
|
32
29
|
|
package/src/sync/workflow.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
logStep,
|
|
20
20
|
logWarn,
|
|
21
21
|
outro,
|
|
22
|
-
|
|
22
|
+
outroSuccess,
|
|
23
23
|
pipeOutput,
|
|
24
24
|
select,
|
|
25
25
|
} from '@/cli/ui'
|
|
@@ -75,7 +75,7 @@ export async function collectChanges(git: GitRunner): Promise<DomainChange[]> {
|
|
|
75
75
|
*
|
|
76
76
|
* This is the only toolkit code that writes to a git remote, which is why a
|
|
77
77
|
* headless run refuses it outright. `select_option` resolved to its first
|
|
78
|
-
* option
|
|
78
|
+
* option with no terminal to answer it, and the first option here was
|
|
79
79
|
* `Commit and open PR` whenever `gh` was installed, so an agent following the
|
|
80
80
|
* documented non-interactive path pushed a branch and opened a pull request on
|
|
81
81
|
* someone's repository without being asked.
|
|
@@ -193,8 +193,6 @@ function promptChoice(canOpenPullRequest: boolean): Promise<WorkflowChoice> {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
function succeed(message: string): number {
|
|
196
|
-
|
|
197
|
-
outro()
|
|
198
|
-
process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
|
|
196
|
+
outroSuccess(message)
|
|
199
197
|
return 0
|
|
200
198
|
}
|
|
@@ -27,20 +27,16 @@ if ! command -v canon >/dev/null 2>&1; then
|
|
|
27
27
|
exit 0
|
|
28
28
|
fi
|
|
29
29
|
|
|
30
|
-
# A global binary older than the verb cannot run it, and
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
30
|
+
# A global binary older than the verb cannot run it, and its owner upgrades it
|
|
31
|
+
# by hand. The probe reads the verb's own usage line rather than the exit,
|
|
32
|
+
# since an older binary answers an unknown command's `--help` with its root
|
|
33
|
+
# help and exits zero.
|
|
34
34
|
has_verb() {
|
|
35
35
|
canon hooks post-merge --help 2>/dev/null | grep -q '^Usage: canon hooks post-merge'
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
if ! has_verb; then
|
|
39
|
-
if [ -z "$CANON_SKIP_UPGRADE" ]; then
|
|
40
|
-
CANON_NON_INTERACTIVE=1 canon upgrade >/dev/null 2>&1 || true
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
38
|
if has_verb; then
|
|
45
39
|
canon hooks post-merge --root "$root" || true
|
|
40
|
+
else
|
|
41
|
+
echo 'canon is too old to run the post-merge hook. Run `canon upgrade`.' >&2
|
|
46
42
|
fi
|
|
@@ -16,7 +16,7 @@ The claude stack installs the Claude Code workflow into a project through `canon
|
|
|
16
16
|
|
|
17
17
|
- `.canon/tasks/` holds one file per task plus a generated `index.md`, local scratch only, with `archive/` holding the tasks that shipped
|
|
18
18
|
- `.canon/plans/` holds execution detail for multi-step tasks, and `canon tasks archive` moves a `feature-*.md` plan into `archive/` when the `post-merge` hook archives its task
|
|
19
|
-
- `.canon/review/` holds
|
|
19
|
+
- `.canon/review/` holds the reports a review or measurement skill writes, such as `ux-measure-<slug>.md`
|
|
20
20
|
- `.canon/tmp/` is ephemeral scratch, safe to delete without loss
|
|
21
21
|
- `.claude/worktrees/` is where `EnterWorktree` creates a linked worktree, which is why it stays under `.claude/`
|
|
22
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Design
|
|
2
2
|
|
|
3
|
-
Authoring guidance: the design standard. Values live in `<your stylesheet>`, such as `src/styles/tokens.css` or a Tailwind `@theme` block. A project with none writes
|
|
3
|
+
Authoring guidance: the design standard. Values live in `<your stylesheet>`, such as `src/styles/tokens.css` or a Tailwind `@theme` block. A project with none writes a stylesheet of its own and names it here.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|