@erclx/canon 5.75.0 → 5.76.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 +11 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/design-identity/REQUIREMENT.md +1 -1
- package/claude/skills/design-identity/SKILL.md +13 -4
- package/claude/skills/design-identity/references/card-route.md +11 -4
- package/claude/skills/git-issue/REQUIREMENT.md +4 -2
- package/claude/skills/git-issue/SKILL.md +32 -3
- package/claude/skills/git-issue/references/issue.md +7 -0
- package/claude/skills/issue-triage/REQUIREMENT.md +35 -0
- package/claude/skills/issue-triage/SKILL.md +130 -0
- package/claude/skills/role-orchestrator/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +15 -15
- package/claude/skills/role-orchestrator/scripts/poll.ts +211 -96
- package/claude/skills/role-orchestrator/scripts/repo.ts +53 -1
- package/docs/agents/migrate.md +10 -10
- package/docs/migrations/by-verb.md +8 -4
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/skill-map.md +1 -0
- package/package.json +1 -1
- package/src/capture/render.ts +30 -5
- package/src/claude/cases/git.ts +5 -0
- package/src/claude/cases/workflow.ts +4 -0
- package/src/commands/capture.ts +38 -1
- package/src/commands/claude.ts +4 -0
- package/src/markdown/scope.ts +2 -0
- package/src/migrate/session-defaults.ts +36 -8
- package/tooling/claude/seeds/.claude/settings.json +4 -0
package/src/capture/render.ts
CHANGED
|
@@ -34,10 +34,13 @@ const FONT_PROBE_SIZE = 72
|
|
|
34
34
|
const FONT_PROBE_TEXT = 'canon capture 0123456789'
|
|
35
35
|
const ABSENT_FAMILY = '__canon_absent_family__'
|
|
36
36
|
|
|
37
|
+
export type ColorScheme = 'light' | 'dark'
|
|
38
|
+
|
|
37
39
|
export interface CaptureOptions {
|
|
38
40
|
selector: string
|
|
39
41
|
outDir?: string
|
|
40
42
|
width?: number
|
|
43
|
+
colorScheme?: ColorScheme
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export type CaptureResult =
|
|
@@ -60,9 +63,7 @@ export async function captureSources(
|
|
|
60
63
|
const browser = await chromium.launch()
|
|
61
64
|
try {
|
|
62
65
|
return await Promise.all(
|
|
63
|
-
sources.map((source) =>
|
|
64
|
-
captureOne(browser, source, options.selector, options.width),
|
|
65
|
-
),
|
|
66
|
+
sources.map((source) => captureOne(browser, source, options)),
|
|
66
67
|
)
|
|
67
68
|
} finally {
|
|
68
69
|
await browser.close()
|
|
@@ -76,8 +77,7 @@ export async function captureSources(
|
|
|
76
77
|
async function captureOne(
|
|
77
78
|
browser: Browser,
|
|
78
79
|
source: CaptureSource,
|
|
79
|
-
selector:
|
|
80
|
-
width: number | undefined,
|
|
80
|
+
{ selector, width, colorScheme }: CaptureOptions,
|
|
81
81
|
): Promise<CaptureResult> {
|
|
82
82
|
/**
|
|
83
83
|
* A viewport rather than a wrapper, since a media query answers to the
|
|
@@ -90,11 +90,13 @@ async function captureOne(
|
|
|
90
90
|
...(width === undefined
|
|
91
91
|
? {}
|
|
92
92
|
: { viewport: { width, height: DEFAULT_VIEWPORT_HEIGHT } }),
|
|
93
|
+
...(colorScheme === undefined ? {} : { colorScheme }),
|
|
93
94
|
})
|
|
94
95
|
try {
|
|
95
96
|
await page.goto(
|
|
96
97
|
source.kind === 'url' ? source.url : pathToFileURL(source.htmlPath).href,
|
|
97
98
|
)
|
|
99
|
+
if (colorScheme !== undefined) await applyColorScheme(page, colorScheme)
|
|
98
100
|
const element = page.locator(selector).first()
|
|
99
101
|
if ((await element.count()) === 0) {
|
|
100
102
|
return failed(source, `no element matched ${selector}`)
|
|
@@ -177,6 +179,29 @@ function sourceIdentifier(source: CaptureSource): string {
|
|
|
177
179
|
return source.kind === 'url' ? source.url : source.htmlPath
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Writes the theme the three switches a stylesheet reads, being the media
|
|
184
|
+
* query, a `data-theme` attribute, and a `light` or `dark` class on the root,
|
|
185
|
+
* which is the set Frameboard's theme toggle writes on every frame. The media
|
|
186
|
+
* query alone leaves a stylesheet keyed on the attribute in its default set,
|
|
187
|
+
* which is dark for one `canon design css` emits.
|
|
188
|
+
*
|
|
189
|
+
* Written after the load event, so a page script restoring a stored theme on
|
|
190
|
+
* load has already run and cannot overwrite it.
|
|
191
|
+
*/
|
|
192
|
+
async function applyColorScheme(
|
|
193
|
+
page: Page,
|
|
194
|
+
scheme: ColorScheme,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
await page.evaluate((theme) => {
|
|
197
|
+
const root = document.documentElement
|
|
198
|
+
root.setAttribute('data-theme', theme)
|
|
199
|
+
root.style.colorScheme = theme
|
|
200
|
+
root.classList.remove('light', 'dark')
|
|
201
|
+
root.classList.add(theme)
|
|
202
|
+
}, scheme)
|
|
203
|
+
}
|
|
204
|
+
|
|
180
205
|
function failed(source: CaptureSource, reason: string): CaptureResult {
|
|
181
206
|
return { status: 'failed', source: sourceIdentifier(source), reason }
|
|
182
207
|
}
|
package/src/claude/cases/git.ts
CHANGED
|
@@ -22,6 +22,11 @@ export const GIT_CASES: readonly SkillCase[] = [
|
|
|
22
22
|
prompt: 'Turn this bug into a GitHub issue and file it.',
|
|
23
23
|
expect: 'git-issue',
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
prompt:
|
|
27
|
+
'Log this bug, but add it to the open issue if one already covers it.',
|
|
28
|
+
expect: 'git-issue',
|
|
29
|
+
},
|
|
25
30
|
{
|
|
26
31
|
prompt: 'Close #42 without merging, #57 supersedes it.',
|
|
27
32
|
expect: 'git-pr-close',
|
|
@@ -96,6 +96,10 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
96
96
|
'Our backlog only ever grows, measure each row and suggest which to decline.',
|
|
97
97
|
expect: 'backlog-triage',
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
prompt: 'File the open issues on this repo into an intake folder.',
|
|
101
|
+
expect: 'issue-triage',
|
|
102
|
+
},
|
|
99
103
|
// Rot across documents nobody changed is what this reads, rather than a
|
|
100
104
|
// stated rule broken on a given line. It is also not `target-check`, which
|
|
101
105
|
// asks whether a document exists and holds its sections rather than whether
|
package/src/commands/capture.ts
CHANGED
|
@@ -31,6 +31,9 @@ const DEFAULT_SOURCE = 'assets'
|
|
|
31
31
|
*/
|
|
32
32
|
type Renderer = typeof import('@/capture/render')
|
|
33
33
|
type CaptureResult = Awaited<ReturnType<Renderer['captureSources']>>[number]
|
|
34
|
+
type ColorScheme = import('@/capture/render').ColorScheme
|
|
35
|
+
|
|
36
|
+
const COLOR_SCHEMES: readonly ColorScheme[] = ['light', 'dark']
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* Holds wiring only. Every browser reference sits behind a dynamic import,
|
|
@@ -58,6 +61,10 @@ export function register(program: Command): void {
|
|
|
58
61
|
'--width <px>',
|
|
59
62
|
'Viewport width in pixels, so a media query resolves at that width',
|
|
60
63
|
)
|
|
64
|
+
.option(
|
|
65
|
+
'--color-scheme <scheme>',
|
|
66
|
+
'Theme to render in, light or dark; defaults to whichever theme the page renders unset',
|
|
67
|
+
)
|
|
61
68
|
.addHelpText(
|
|
62
69
|
'after',
|
|
63
70
|
[
|
|
@@ -70,12 +77,21 @@ export function register(program: Command): void {
|
|
|
70
77
|
'font-family. --width takes one whole number and sets the viewport, so',
|
|
71
78
|
'run it once per breakpoint with its own --out.',
|
|
72
79
|
'',
|
|
80
|
+
'--color-scheme sets prefers-color-scheme, a data-theme attribute, and',
|
|
81
|
+
'a light or dark class on the root, so a stylesheet keyed on any of the',
|
|
82
|
+
'three renders that theme. Run it once per scheme with its own --out.',
|
|
83
|
+
'',
|
|
73
84
|
].join('\n'),
|
|
74
85
|
)
|
|
75
86
|
.action(
|
|
76
87
|
async (
|
|
77
88
|
source: string,
|
|
78
|
-
opts: {
|
|
89
|
+
opts: {
|
|
90
|
+
out?: string
|
|
91
|
+
selector?: string
|
|
92
|
+
width?: string
|
|
93
|
+
colorScheme?: string
|
|
94
|
+
},
|
|
79
95
|
): Promise<void> => {
|
|
80
96
|
/**
|
|
81
97
|
* Refused rather than defaulted, and refused ahead of every other
|
|
@@ -102,6 +118,15 @@ export function register(program: Command): void {
|
|
|
102
118
|
return
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
const colorScheme = parseColorScheme(opts.colorScheme)
|
|
122
|
+
if (colorScheme === null) {
|
|
123
|
+
frameError(
|
|
124
|
+
`--color-scheme takes ${COLOR_SCHEMES.join(' or ')}, got ${JSON.stringify(opts.colorScheme)}.`,
|
|
125
|
+
)
|
|
126
|
+
process.exitCode = 1
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
const isUrl = isUrlSource(source)
|
|
106
131
|
const sourcePath = isUrl ? source : resolve(process.cwd(), source)
|
|
107
132
|
if (!isUrl && !existsSync(sourcePath)) {
|
|
@@ -117,6 +142,7 @@ export function register(program: Command): void {
|
|
|
117
142
|
results = await renderer.captureSources(sourcePath, {
|
|
118
143
|
selector,
|
|
119
144
|
width,
|
|
145
|
+
colorScheme,
|
|
120
146
|
outDir: opts.out ? resolve(process.cwd(), opts.out) : undefined,
|
|
121
147
|
})
|
|
122
148
|
} catch (error) {
|
|
@@ -160,6 +186,17 @@ function parseWidth(raw: string | undefined): number | undefined {
|
|
|
160
186
|
return Number.isInteger(value) && value >= 1 ? value : Number.NaN
|
|
161
187
|
}
|
|
162
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Undefined for an absent flag and `null` for a value naming no scheme, so the
|
|
191
|
+
* caller can tell no scheme from a bad one.
|
|
192
|
+
*/
|
|
193
|
+
function parseColorScheme(
|
|
194
|
+
raw: string | undefined,
|
|
195
|
+
): ColorScheme | undefined | null {
|
|
196
|
+
if (raw === undefined) return undefined
|
|
197
|
+
return COLOR_SCHEMES.find((scheme) => scheme === raw) ?? null
|
|
198
|
+
}
|
|
199
|
+
|
|
163
200
|
/**
|
|
164
201
|
* Closes an open frame around a failure that stopped the whole run rather than
|
|
165
202
|
* one source. Two of them are setup states rather than defects and each names
|
package/src/commands/claude.ts
CHANGED
|
@@ -713,6 +713,10 @@ async function runSync(target: string): Promise<number> {
|
|
|
713
713
|
logWarn(
|
|
714
714
|
'settings.json lacks the merge deny entries. Run `canon migrate session-defaults --write`',
|
|
715
715
|
)
|
|
716
|
+
} else if (defaults.kind === 'planned' && defaults.plan.lacksPushAllow) {
|
|
717
|
+
logWarn(
|
|
718
|
+
'settings.json lacks the lease push allow. Run `canon migrate session-defaults --write`',
|
|
719
|
+
)
|
|
716
720
|
}
|
|
717
721
|
|
|
718
722
|
logStep('Scanning .gitignore')
|
package/src/markdown/scope.ts
CHANGED
|
@@ -260,6 +260,8 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
260
260
|
'claude/skills/git-worktree/REQUIREMENT.md',
|
|
261
261
|
'claude/skills/git-worktree/SKILL.md',
|
|
262
262
|
'claude/skills/git-worktree/references/cleanup.md',
|
|
263
|
+
'claude/skills/issue-triage/REQUIREMENT.md',
|
|
264
|
+
'claude/skills/issue-triage/SKILL.md',
|
|
263
265
|
'claude/skills/markdown-craft/REQUIREMENT.md',
|
|
264
266
|
'claude/skills/markdown-craft/SKILL.md',
|
|
265
267
|
'claude/skills/markdown-craft/references/markdown.md',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Adds the session defaults the seeded `.claude/settings.json` carries to a
|
|
3
3
|
* target that was seeded before them: the subagent deny, the memory default,
|
|
4
|
-
* the merge deny entries, and the plan guard's
|
|
5
|
-
* project holds the guard script.
|
|
4
|
+
* the merge deny entries, the lease push allow entries, and the plan guard's
|
|
5
|
+
* PreToolUse wiring where the project holds the guard script.
|
|
6
6
|
*
|
|
7
7
|
* `canon claude init` copies a seed only when the file is absent, so a target
|
|
8
8
|
* holding its own settings never sees a later seed change. This verb adds each
|
|
@@ -29,6 +29,17 @@ export const MERGE_DENIED: readonly string[] = [
|
|
|
29
29
|
'Bash(gh pr merge:*)',
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The two lease pushes a worker runs after rebasing onto a merged pull request,
|
|
34
|
+
* each pushing the current branch to its own name. An allow rule resolves
|
|
35
|
+
* before auto mode's classifier, so each entry is exact: a wildcard would also
|
|
36
|
+
* approve a refspec, a remote, or a flag that reaches the default branch.
|
|
37
|
+
*/
|
|
38
|
+
export const PUSH_ALLOWED: readonly string[] = [
|
|
39
|
+
'Bash(git push --force-with-lease)',
|
|
40
|
+
'Bash(git push --force-with-lease origin HEAD)',
|
|
41
|
+
]
|
|
42
|
+
|
|
32
43
|
/** The command the seed wires, as the seeded settings spell it. */
|
|
33
44
|
export const PLAN_GUARD_COMMAND =
|
|
34
45
|
'"$CLAUDE_PROJECT_DIR"/.claude/hooks/plan-guard.sh'
|
|
@@ -61,6 +72,8 @@ export interface SessionDefaultsPlan {
|
|
|
61
72
|
readonly predates: boolean
|
|
62
73
|
/** True when the deny list lacks any merge entry, whatever else it holds. */
|
|
63
74
|
readonly lacksMergeDeny: boolean
|
|
75
|
+
/** True when the allow list lacks any lease push entry. */
|
|
76
|
+
readonly lacksPushAllow: boolean
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
export type SessionDefaultsRead =
|
|
@@ -92,12 +105,20 @@ export function planSessionDefaults(
|
|
|
92
105
|
const next: SettingsRecord = { ...current }
|
|
93
106
|
const permissions = isRecord(current.permissions) ? current.permissions : {}
|
|
94
107
|
const deny = Array.isArray(permissions.deny) ? permissions.deny : []
|
|
108
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : []
|
|
95
109
|
|
|
96
110
|
const subagentsMissing = DENIED_TOOLS.filter((tool) => !deny.includes(tool))
|
|
97
111
|
const mergeMissing = MERGE_DENIED.filter((entry) => !deny.includes(entry))
|
|
98
112
|
const missing = [...subagentsMissing, ...mergeMissing]
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
const allowMissing = PUSH_ALLOWED.filter((entry) => !allow.includes(entry))
|
|
114
|
+
|
|
115
|
+
const nextPermissions: SettingsRecord = { ...permissions }
|
|
116
|
+
if (missing.length > 0) nextPermissions.deny = [...deny, ...missing]
|
|
117
|
+
if (allowMissing.length > 0) {
|
|
118
|
+
nextPermissions.allow = [...allow, ...allowMissing]
|
|
119
|
+
}
|
|
120
|
+
if (missing.length + allowMissing.length > 0) {
|
|
121
|
+
next.permissions = nextPermissions
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
const memoryAbsent = !('autoMemoryEnabled' in current)
|
|
@@ -111,6 +132,7 @@ export function planSessionDefaults(
|
|
|
111
132
|
|
|
112
133
|
const keys: SettingsKey[] = [
|
|
113
134
|
{ label: 'permissions.deny', changed: missing.length > 0 },
|
|
135
|
+
{ label: 'permissions.allow', changed: allowMissing.length > 0 },
|
|
114
136
|
{ label: 'autoMemoryEnabled', changed: memoryAbsent },
|
|
115
137
|
{ label: 'hooks.PreToolUse', changed: preToolUse !== undefined },
|
|
116
138
|
]
|
|
@@ -123,6 +145,7 @@ export function planSessionDefaults(
|
|
|
123
145
|
subagentsMissing.length === DENIED_TOOLS.length &&
|
|
124
146
|
mergeMissing.length === MERGE_DENIED.length,
|
|
125
147
|
lacksMergeDeny: mergeMissing.length > 0,
|
|
148
|
+
lacksPushAllow: allowMissing.length > 0,
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
151
|
|
|
@@ -222,10 +245,15 @@ export async function readSessionDefaults(
|
|
|
222
245
|
if (permissions !== undefined && !isRecord(permissions)) {
|
|
223
246
|
return { kind: 'refused', reason: `${path}: permissions is not an object` }
|
|
224
247
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
248
|
+
for (const list of ['deny', 'allow'] as const) {
|
|
249
|
+
if (
|
|
250
|
+
permissions?.[list] !== undefined &&
|
|
251
|
+
!Array.isArray(permissions[list])
|
|
252
|
+
) {
|
|
253
|
+
return {
|
|
254
|
+
kind: 'refused',
|
|
255
|
+
reason: `${path}: permissions.${list} is not a list`,
|
|
256
|
+
}
|
|
229
257
|
}
|
|
230
258
|
}
|
|
231
259
|
|