@erclx/canon 5.68.0 → 5.69.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 +19 -0
- package/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +5 -5
- package/claude/skills/app-drive/SKILL.md +14 -4
- package/claude/skills/app-drive/references/drive-record.md +12 -1
- package/claude/skills/canon-cli/SKILL.md +2 -2
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
- package/claude/skills/canon-feedback/SKILL.md +1 -1
- package/claude/skills/design-identity/REQUIREMENT.md +4 -4
- package/claude/skills/design-identity/SKILL.md +7 -7
- package/claude/skills/{pick-by-eye → design-pick}/REQUIREMENT.md +6 -6
- package/claude/skills/{pick-by-eye → design-pick}/SKILL.md +18 -10
- package/claude/skills/{pick-by-eye → design-pick}/references/board.md +17 -11
- package/claude/skills/design-pick/references/design-config.md +33 -0
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +2 -2
- package/claude/skills/design-taste/SKILL.md +3 -3
- package/claude/skills/design-taste/references/preflight.md +1 -1
- package/claude/skills/git-issue/REQUIREMENT.md +1 -1
- package/claude/skills/git-issue/references/issue.md +1 -1
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/plan-feature/references/plan.md +21 -6
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/pr-evidence/references/evidence.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +3 -3
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-planning-wait.md +24 -0
- package/claude/skills/role-planner/SKILL.md +4 -2
- package/claude/skills/role-worker/SKILL.md +5 -6
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/references/receipt-sweep.md +1 -1
- package/claude/skills/{ux-audit → ui-audit}/REQUIREMENT.md +3 -3
- package/claude/skills/{ux-audit → ui-audit}/SKILL.md +7 -7
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-craft/SKILL.md +1 -1
- package/claude/skills/{ui-record → ui-film}/REQUIREMENT.md +2 -2
- package/claude/skills/{ui-record → ui-film}/SKILL.md +2 -2
- package/claude/skills/{ui-record → ui-film}/references/config.md +3 -3
- package/claude/skills/{ui-record → ui-film}/references/handoff.md +4 -4
- package/claude/skills/{ui-record → ui-film}/references/pr-clip.md +5 -5
- package/claude/skills/{ui-record → ui-film}/references/take-craft.md +1 -1
- package/claude/skills/{ux-measure → ui-measure}/REQUIREMENT.md +3 -3
- package/claude/skills/{ux-measure → ui-measure}/SKILL.md +6 -6
- package/docs/agents/audits.md +1 -1
- package/docs/agents/migrate.md +13 -13
- package/docs/target/migrations.md +12 -10
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -1
- package/docs/workflow/skill-map-on-request.md +1 -2
- package/docs/workflow/skill-map.md +9 -10
- package/docs/workflow/visual-design-workflow.md +6 -1
- package/governance/ledger/claude/background.md +26 -0
- package/governance/rules/claude/background.md +19 -0
- package/governance/stacks/base.toml +1 -0
- package/package.json +4 -4
- package/scripts/ci/install-playwright-deps.sh +52 -0
- package/src/claude/cases/authoring.ts +2 -2
- package/src/claude/cases/misc.ts +1 -1
- package/src/claude/cases/setup.ts +0 -5
- package/src/claude/cases/workflow.ts +6 -6
- package/src/commands/fixtures/gh.sh +64 -0
- package/src/commands/gov.ts +33 -3
- package/src/commands/migrate.ts +12 -2
- package/src/commands/tasks.ts +183 -0
- package/src/gate/stages.ts +2 -2
- package/src/gov/audit.ts +48 -11
- package/src/gov/detect.ts +1 -1
- package/src/markdown/scope.ts +9 -14
- package/src/migrate/record-layout.ts +5 -3
- package/src/migrate/skill-names.ts +21 -4
- package/src/records/validate.ts +1 -0
- package/src/sandbox/headless/dispatch-shim.sh +15 -0
- package/src/sandbox/headless/dispatch.ts +12 -14
- package/src/sandbox/stub-gh.sh +26 -0
- package/src/sandbox/stub-remote.ts +8 -29
- package/src/sync/engine.ts +1 -1
- package/src/take/camera.ts +2 -2
- package/src/take/film.ts +20 -2
- package/src/take/pointer.ts +2 -1
- package/src/take/stepper.ts +19 -5
- package/src/take/zoom/NOTICE.md +1 -1
- package/src/tasks/launch-wait.ts +160 -0
- package/tooling/claude/reference.md +1 -1
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +0 -34
- package/claude/skills/canon-feedback-triage/SKILL.md +0 -98
- package/claude/skills/plan-ready/REQUIREMENT.md +0 -35
- package/claude/skills/plan-ready/SKILL.md +0 -136
- package/claude/skills/plan-ready/references/assembly.md +0 -75
- package/claude/skills/plan-ready/references/ready.md +0 -128
- /package/claude/skills/{pick-by-eye → design-pick}/references/live-arms.md +0 -0
package/src/commands/tasks.ts
CHANGED
|
@@ -3,6 +3,13 @@ import type { Command } from 'commander'
|
|
|
3
3
|
import { execa } from 'execa'
|
|
4
4
|
import { gitEnv } from '@/git/env'
|
|
5
5
|
import { type AnswersOutcome, planAnswers } from '@/tasks/answers'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_LAUNCH_WAIT_SECONDS,
|
|
8
|
+
type LaunchOutcome,
|
|
9
|
+
type LaunchWait,
|
|
10
|
+
readLaunchable,
|
|
11
|
+
waitForLaunchable,
|
|
12
|
+
} from '@/tasks/launch-wait'
|
|
6
13
|
import {
|
|
7
14
|
type BoardOutcome,
|
|
8
15
|
type BoardRefused,
|
|
@@ -112,6 +119,12 @@ interface AnswersCommandOptions {
|
|
|
112
119
|
readonly root?: string
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
interface LaunchableCommandOptions {
|
|
123
|
+
readonly json?: boolean
|
|
124
|
+
readonly root?: string
|
|
125
|
+
readonly wait?: string | true
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
interface BranchCommandOptions {
|
|
116
129
|
readonly json?: boolean
|
|
117
130
|
readonly root?: string
|
|
@@ -414,6 +427,72 @@ export function register(program: Command): void {
|
|
|
414
427
|
process.exitCode = await runAnswers(plan, opts)
|
|
415
428
|
})
|
|
416
429
|
|
|
430
|
+
tasks
|
|
431
|
+
.command('launchable')
|
|
432
|
+
.description(
|
|
433
|
+
'Report whether a row has a linked plan the dispatch may launch',
|
|
434
|
+
)
|
|
435
|
+
.argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation')
|
|
436
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
437
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
438
|
+
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
439
|
+
.option(
|
|
440
|
+
'--wait [seconds]',
|
|
441
|
+
`Hold until the row is launchable, default ${DEFAULT_LAUNCH_WAIT_SECONDS}`,
|
|
442
|
+
)
|
|
443
|
+
.addHelpText(
|
|
444
|
+
'after',
|
|
445
|
+
[
|
|
446
|
+
'',
|
|
447
|
+
"Resolves the task's Plan: line and runs the plan-answers gate on the",
|
|
448
|
+
'plan it names, in one read. It keys on the line rather than on the',
|
|
449
|
+
'plan file, since a planner links a plan only after it validates, so a',
|
|
450
|
+
'plan still being written never reads as landed. It answers planning',
|
|
451
|
+
'alone: the branch and claim checks stay in the dispatch runbook.',
|
|
452
|
+
'',
|
|
453
|
+
'The record carries task, plan, launchable, and open, the last three',
|
|
454
|
+
'named as plan-answers names them. Read `reason`:',
|
|
455
|
+
' unlinked the task carries no Plan: line yet',
|
|
456
|
+
' open the plan still holds a question for the operator',
|
|
457
|
+
' launchable the plan waits on nobody',
|
|
458
|
+
'',
|
|
459
|
+
'`--wait [seconds]` re-reads every 30 seconds until the row is',
|
|
460
|
+
'launchable or the deadline passes. `reason` then reads `launchable`',
|
|
461
|
+
'or `timeout`, the record carries the last reading, and',
|
|
462
|
+
'`waitedSeconds` is added. A refusal ends the wait at once. A wait',
|
|
463
|
+
'past 600 seconds outlasts a foreground shell call, so run it in the',
|
|
464
|
+
'background and act on the wake.',
|
|
465
|
+
'',
|
|
466
|
+
'Refusals, read off `reason`:',
|
|
467
|
+
' no-board the project carries no .canon/tasks/',
|
|
468
|
+
' no-match no task carries that stem exactly',
|
|
469
|
+
' ambiguous several tasks start with the name, so pass one in full',
|
|
470
|
+
' several-plans the Plan: line links more than one plan',
|
|
471
|
+
' no-plan the Plan: line names no file',
|
|
472
|
+
' archived the Plan: line points into the plans archive',
|
|
473
|
+
' bad-input the Plan: line carries an empty target',
|
|
474
|
+
' bad-wait --wait takes a whole number of seconds above zero',
|
|
475
|
+
'',
|
|
476
|
+
'Exit codes:',
|
|
477
|
+
' 0 the row is launchable',
|
|
478
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
479
|
+
' 2 the row is unlinked or open, or the wait reached its deadline',
|
|
480
|
+
'',
|
|
481
|
+
'It reports and never writes. Branch on `reason` rather than on the',
|
|
482
|
+
'exit code, which a shell function wrapping canon can flatten to zero.',
|
|
483
|
+
'',
|
|
484
|
+
'Examples:',
|
|
485
|
+
' canon tasks launchable v28.1-trigger-escalation',
|
|
486
|
+
' canon tasks launchable v28.1-trigger-escalation --json',
|
|
487
|
+
' canon tasks launchable v28.1-trigger-escalation --wait --json',
|
|
488
|
+
' canon tasks launchable v28.1-trigger-escalation --wait 900 --json',
|
|
489
|
+
'',
|
|
490
|
+
].join('\n'),
|
|
491
|
+
)
|
|
492
|
+
.action(async (task: string, opts: LaunchableCommandOptions) => {
|
|
493
|
+
process.exitCode = await runLaunchable(task, opts)
|
|
494
|
+
})
|
|
495
|
+
|
|
417
496
|
tasks
|
|
418
497
|
.command('plan-branch')
|
|
419
498
|
.description('Derive the branch name a dispatch and a worker both take')
|
|
@@ -1827,6 +1906,110 @@ function reportAnswers(
|
|
|
1827
1906
|
return EXIT_FINDINGS
|
|
1828
1907
|
}
|
|
1829
1908
|
|
|
1909
|
+
const BAD_WAIT = '--wait takes a whole number of seconds above zero.'
|
|
1910
|
+
|
|
1911
|
+
interface BadWait {
|
|
1912
|
+
readonly ok: false
|
|
1913
|
+
readonly reason: 'bad-wait'
|
|
1914
|
+
readonly message: string
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
async function runLaunchable(
|
|
1918
|
+
task: string,
|
|
1919
|
+
opts: LaunchableCommandOptions,
|
|
1920
|
+
): Promise<number> {
|
|
1921
|
+
const emitJson = opts.json ?? false
|
|
1922
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
1923
|
+
|
|
1924
|
+
let waitSeconds: number | undefined
|
|
1925
|
+
if (opts.wait === true) {
|
|
1926
|
+
waitSeconds = DEFAULT_LAUNCH_WAIT_SECONDS
|
|
1927
|
+
} else if (opts.wait !== undefined) {
|
|
1928
|
+
if (!/^\d+$/.test(opts.wait) || Number(opts.wait) === 0) {
|
|
1929
|
+
const refused: BadWait = {
|
|
1930
|
+
ok: false,
|
|
1931
|
+
reason: 'bad-wait',
|
|
1932
|
+
message: BAD_WAIT,
|
|
1933
|
+
}
|
|
1934
|
+
return reportLaunchable(refused, emitJson, root)
|
|
1935
|
+
}
|
|
1936
|
+
waitSeconds = Number(opts.wait)
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const outcome =
|
|
1940
|
+
waitSeconds === undefined
|
|
1941
|
+
? await readLaunchable(root, task)
|
|
1942
|
+
: await waitForLaunchable(root, task, {
|
|
1943
|
+
waitSeconds,
|
|
1944
|
+
clock: {
|
|
1945
|
+
now: () => Date.now(),
|
|
1946
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
1947
|
+
},
|
|
1948
|
+
})
|
|
1949
|
+
|
|
1950
|
+
return reportLaunchable(outcome, emitJson, root)
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function reportLaunchable(
|
|
1954
|
+
outcome: LaunchOutcome | LaunchWait | BadWait,
|
|
1955
|
+
emitJson: boolean,
|
|
1956
|
+
root: string,
|
|
1957
|
+
): number {
|
|
1958
|
+
if (!outcome.ok) {
|
|
1959
|
+
intro('canon tasks launchable')
|
|
1960
|
+
logStep('Refused')
|
|
1961
|
+
logError(outcome.message)
|
|
1962
|
+
outro()
|
|
1963
|
+
|
|
1964
|
+
if (emitJson) {
|
|
1965
|
+
process.stdout.write(
|
|
1966
|
+
`${JSON.stringify({
|
|
1967
|
+
ok: false,
|
|
1968
|
+
reason: outcome.reason,
|
|
1969
|
+
message: outcome.message,
|
|
1970
|
+
...('waitedSeconds' in outcome && {
|
|
1971
|
+
waitedSeconds: outcome.waitedSeconds,
|
|
1972
|
+
}),
|
|
1973
|
+
root,
|
|
1974
|
+
})}\n`,
|
|
1975
|
+
)
|
|
1976
|
+
}
|
|
1977
|
+
return 1
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const code = outcome.launchable ? 0 : EXIT_FINDINGS
|
|
1981
|
+
|
|
1982
|
+
if (emitJson) {
|
|
1983
|
+
process.stdout.write(`${JSON.stringify({ ...outcome, root })}\n`)
|
|
1984
|
+
return code
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
intro('canon tasks launchable')
|
|
1988
|
+
logStep(outcome.task)
|
|
1989
|
+
|
|
1990
|
+
if (outcome.plan === undefined) {
|
|
1991
|
+
logWarn('carries no Plan: line yet, so no plan has landed.')
|
|
1992
|
+
} else if (outcome.launchable) {
|
|
1993
|
+
logInfo(`${outcome.plan} waits on nobody, so the dispatch may launch it.`)
|
|
1994
|
+
} else {
|
|
1995
|
+
for (const question of outcome.open) {
|
|
1996
|
+
logWarn(`${question.label} ${question.why}`)
|
|
1997
|
+
}
|
|
1998
|
+
logError(`${outcome.plan} still waits on the operator.`)
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if ('waitedSeconds' in outcome) {
|
|
2002
|
+
logInfo(
|
|
2003
|
+
outcome.reason === 'timeout'
|
|
2004
|
+
? `not launchable within ${outcome.waitedSeconds}s`
|
|
2005
|
+
: `launchable after ${outcome.waitedSeconds}s`,
|
|
2006
|
+
)
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
outro()
|
|
2010
|
+
return code
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1830
2013
|
async function runBranch(
|
|
1831
2014
|
plan: string,
|
|
1832
2015
|
opts: BranchCommandOptions,
|
package/src/gate/stages.ts
CHANGED
|
@@ -402,9 +402,9 @@ export const STAGES: readonly Stage[] = [
|
|
|
402
402
|
checks: [
|
|
403
403
|
{
|
|
404
404
|
kind: 'cli',
|
|
405
|
-
argv: ['gov', 'audit', '--gate'],
|
|
405
|
+
argv: ['gov', 'audit', '--gate', '--touched'],
|
|
406
406
|
failure:
|
|
407
|
-
'A rule in a gated folder is past a shape limit or short of its ledger. Run bun src/cli.ts gov audit.',
|
|
407
|
+
'A rule in a gated folder is past a shape limit or short of its ledger. Run bun src/cli.ts gov audit --touched.',
|
|
408
408
|
},
|
|
409
409
|
],
|
|
410
410
|
},
|
package/src/gov/audit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { join, normalize, relative, resolve, sep } from 'node:path'
|
|
4
4
|
import { listRuleFiles } from '@/gov/payload'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -57,6 +57,19 @@ export const PRACTICE_FOLDERS: readonly string[] = [
|
|
|
57
57
|
'tooling',
|
|
58
58
|
]
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The folders whose rules record house decisions. Their ledger is checked
|
|
62
|
+
* wherever one exists and owed only by a rule the branch touched, since the
|
|
63
|
+
* reasoning behind an older bullet was never written down and no backfill
|
|
64
|
+
* recovers it.
|
|
65
|
+
*/
|
|
66
|
+
export const CORE_FOLDERS: readonly string[] = [
|
|
67
|
+
'canon',
|
|
68
|
+
'claude',
|
|
69
|
+
'code',
|
|
70
|
+
'writing',
|
|
71
|
+
]
|
|
72
|
+
|
|
60
73
|
const SHIPPED_RULES = join('governance', 'rules')
|
|
61
74
|
const LEDGERS = join('governance', 'ledger')
|
|
62
75
|
|
|
@@ -316,29 +329,41 @@ export function compareLedger(
|
|
|
316
329
|
.filter((entry) => entry.bullets !== entry.rows)
|
|
317
330
|
}
|
|
318
331
|
|
|
319
|
-
/**
|
|
320
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Where a rule's ledger lives and whether its absence is a finding, or nothing
|
|
334
|
+
* when its folder carries none.
|
|
335
|
+
*/
|
|
336
|
+
function ledgerFor(
|
|
337
|
+
file: string,
|
|
338
|
+
touched: ReadonlySet<string>,
|
|
339
|
+
): { path: string; owed: boolean } | undefined {
|
|
321
340
|
const prefix = `${SHIPPED_RULES}${sep}`
|
|
322
341
|
if (!file.startsWith(prefix)) return undefined
|
|
323
342
|
const rest = file.slice(prefix.length)
|
|
324
343
|
const folder = rest.split(sep)[0]
|
|
325
|
-
|
|
326
|
-
return
|
|
344
|
+
const path = join(LEDGERS, rest)
|
|
345
|
+
if (PRACTICE_FOLDERS.includes(folder)) return { path, owed: true }
|
|
346
|
+
if (CORE_FOLDERS.includes(folder)) return { path, owed: touched.has(file) }
|
|
347
|
+
return undefined
|
|
327
348
|
}
|
|
328
349
|
|
|
329
350
|
async function readLedger(
|
|
330
351
|
root: string,
|
|
331
352
|
file: string,
|
|
332
353
|
groups: readonly HeadingCount[],
|
|
354
|
+
touched: ReadonlySet<string>,
|
|
333
355
|
): Promise<LedgerReading> {
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
356
|
+
const ledger = ledgerFor(file, touched)
|
|
357
|
+
if (ledger === undefined) return { status: 'not-required' }
|
|
358
|
+
const { path } = ledger
|
|
336
359
|
|
|
337
360
|
let text: string
|
|
338
361
|
try {
|
|
339
362
|
text = await readFile(resolve(root, path), 'utf8')
|
|
340
363
|
} catch {
|
|
341
|
-
return
|
|
364
|
+
return ledger.owed
|
|
365
|
+
? { status: 'missing', path }
|
|
366
|
+
: { status: 'not-required' }
|
|
342
367
|
}
|
|
343
368
|
|
|
344
369
|
return {
|
|
@@ -352,14 +377,22 @@ async function readLedger(
|
|
|
352
377
|
* Measures every rule this tree authors, or the files named.
|
|
353
378
|
*
|
|
354
379
|
* A tree holding `governance/rules/` is a canon checkout, read across both
|
|
355
|
-
* authored corpora with a ledger asked of each outside-practice rule
|
|
356
|
-
*
|
|
380
|
+
* authored corpora with a ledger asked of each outside-practice rule and of
|
|
381
|
+
* each core rule in `touched`, and checked for any core rule carrying one.
|
|
382
|
+
* Any other tree is a target, read at `.claude/rules/project/` alone with no
|
|
357
383
|
* ledger asked, since ledgers never ship.
|
|
384
|
+
*
|
|
385
|
+
* `touched` holds paths relative to the root, and leaving it out reads no core
|
|
386
|
+
* rule as touched.
|
|
358
387
|
*/
|
|
359
388
|
export async function readRuleAudit(
|
|
360
389
|
root: string,
|
|
361
390
|
paths: readonly string[] = [],
|
|
391
|
+
options: { readonly touched?: readonly string[] } = {},
|
|
362
392
|
): Promise<RuleAuditReport> {
|
|
393
|
+
const touched = new Set(
|
|
394
|
+
(options.touched ?? []).map((path) => normalize(path)),
|
|
395
|
+
)
|
|
363
396
|
const scope = existsSync(resolve(root, SHIPPED_RULES)) ? 'canon' : 'target'
|
|
364
397
|
const corpus = scope === 'canon' ? CANON_CORPUS : TARGET_CORPUS
|
|
365
398
|
|
|
@@ -421,7 +454,11 @@ export async function readRuleAudit(
|
|
|
421
454
|
for (const abs of files) {
|
|
422
455
|
const file = relative(root, abs)
|
|
423
456
|
const { groups, ...shape } = measureRule(await readFile(abs, 'utf8'))
|
|
424
|
-
rules.push({
|
|
457
|
+
rules.push({
|
|
458
|
+
file,
|
|
459
|
+
...shape,
|
|
460
|
+
ledger: await readLedger(root, file, groups, touched),
|
|
461
|
+
})
|
|
425
462
|
}
|
|
426
463
|
|
|
427
464
|
return { kind: 'measured', scope, corpus, rules }
|
package/src/gov/detect.ts
CHANGED
|
@@ -253,7 +253,7 @@ function isLiteral(pattern: string): boolean {
|
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
255
|
* A literal pattern is also tested on disk, since evidence such as
|
|
256
|
-
* `.canon/
|
|
256
|
+
* `.canon/board` sits in a folder a target's own `.gitignore` excludes, and
|
|
257
257
|
* the git listing never carries it.
|
|
258
258
|
*/
|
|
259
259
|
function matchEntry(
|
package/src/markdown/scope.ts
CHANGED
|
@@ -136,8 +136,6 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
136
136
|
'claude/skills/build-in-slices/references/adopted.md',
|
|
137
137
|
'claude/skills/canon-cli/REQUIREMENT.md',
|
|
138
138
|
'claude/skills/canon-cli/SKILL.md',
|
|
139
|
-
'claude/skills/canon-feedback-triage/REQUIREMENT.md',
|
|
140
|
-
'claude/skills/canon-feedback-triage/SKILL.md',
|
|
141
139
|
'claude/skills/canon-feedback/REQUIREMENT.md',
|
|
142
140
|
'claude/skills/canon-feedback/SKILL.md',
|
|
143
141
|
'claude/skills/canon-feedback/references/template.md',
|
|
@@ -202,10 +200,10 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
202
200
|
'claude/skills/document-repair/REQUIREMENT.md',
|
|
203
201
|
'claude/skills/document-repair/SKILL.md',
|
|
204
202
|
'claude/skills/document-repair/references/batches.md',
|
|
205
|
-
'claude/skills/pick
|
|
206
|
-
'claude/skills/pick
|
|
207
|
-
'claude/skills/pick
|
|
208
|
-
'claude/skills/pick
|
|
203
|
+
'claude/skills/design-pick/REQUIREMENT.md',
|
|
204
|
+
'claude/skills/design-pick/SKILL.md',
|
|
205
|
+
'claude/skills/design-pick/references/board.md',
|
|
206
|
+
'claude/skills/design-pick/references/live-arms.md',
|
|
209
207
|
'claude/skills/write-doc/REQUIREMENT.md',
|
|
210
208
|
'claude/skills/write-doc/SKILL.md',
|
|
211
209
|
'claude/skills/write-doc/references/context.md',
|
|
@@ -229,10 +227,6 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
229
227
|
'claude/skills/design-identity/REQUIREMENT.md',
|
|
230
228
|
'claude/skills/design-identity/SKILL.md',
|
|
231
229
|
'claude/skills/design-identity/references/card-route.md',
|
|
232
|
-
'claude/skills/plan-ready/REQUIREMENT.md',
|
|
233
|
-
'claude/skills/plan-ready/SKILL.md',
|
|
234
|
-
'claude/skills/plan-ready/references/assembly.md',
|
|
235
|
-
'claude/skills/plan-ready/references/ready.md',
|
|
236
230
|
'claude/skills/git-branch/REQUIREMENT.md',
|
|
237
231
|
'claude/skills/git-branch/SKILL.md',
|
|
238
232
|
'claude/skills/git-branch/references/branch.md',
|
|
@@ -391,10 +385,10 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
391
385
|
'claude/skills/test-first/references/adopted.md',
|
|
392
386
|
'claude/skills/ui-checklist/REQUIREMENT.md',
|
|
393
387
|
'claude/skills/ui-checklist/SKILL.md',
|
|
394
|
-
'claude/skills/
|
|
395
|
-
'claude/skills/
|
|
396
|
-
'claude/skills/
|
|
397
|
-
'claude/skills/
|
|
388
|
+
'claude/skills/ui-audit/REQUIREMENT.md',
|
|
389
|
+
'claude/skills/ui-audit/SKILL.md',
|
|
390
|
+
'claude/skills/ui-measure/REQUIREMENT.md',
|
|
391
|
+
'claude/skills/ui-measure/SKILL.md',
|
|
398
392
|
'claude/skills/write-human/REQUIREMENT.md',
|
|
399
393
|
'claude/skills/write-human/SKILL.md',
|
|
400
394
|
'claude/skills/write-human/references/adopted.md',
|
|
@@ -404,6 +398,7 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
404
398
|
'claude/skills/youtube-transcripts/SKILL.md',
|
|
405
399
|
'.claude/skills/internal-claude/REQUIREMENT.md',
|
|
406
400
|
'.claude/skills/internal-docs/REQUIREMENT.md',
|
|
401
|
+
'.claude/skills/internal-feedback-triage/REQUIREMENT.md',
|
|
407
402
|
'.claude/skills/internal-governance/REQUIREMENT.md',
|
|
408
403
|
'.claude/skills/internal-rule-audit/REQUIREMENT.md',
|
|
409
404
|
'.claude/skills/internal-sandbox-check/REQUIREMENT.md',
|
|
@@ -88,8 +88,9 @@ const RENDER = [SCRATCH, 'render'] as const
|
|
|
88
88
|
* Every move this migration makes, as record-relative paths.
|
|
89
89
|
*
|
|
90
90
|
* The first two rows are batch 2's. The rest are batch 3's, in the order the
|
|
91
|
-
* intake's review-folder pass listed them
|
|
92
|
-
*
|
|
91
|
+
* intake's review-folder pass listed them, then the design board, moved to
|
|
92
|
+
* the `.canon/board` folder Frameboard names. A later batch appends here
|
|
93
|
+
* rather than adding a second table.
|
|
93
94
|
*/
|
|
94
95
|
export const RECORD_LAYOUT_MOVES: readonly RecordLayoutMove[] = [
|
|
95
96
|
{ kind: 'folder', from: ['review', 'memory'], to: ['memory', 'review'] },
|
|
@@ -130,6 +131,7 @@ export const RECORD_LAYOUT_MOVES: readonly RecordLayoutMove[] = [
|
|
|
130
131
|
picks: ['picks'],
|
|
131
132
|
evidence: ['evidence'],
|
|
132
133
|
},
|
|
134
|
+
{ kind: 'folder', from: ['canvas'], to: ['board'] },
|
|
133
135
|
]
|
|
134
136
|
|
|
135
137
|
function resolveRecord(root: string, segments: readonly string[]): string {
|
|
@@ -346,7 +348,7 @@ function entriesOf(dir: string, isDirectory: boolean): string[] {
|
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
/**
|
|
349
|
-
* What `pick
|
|
351
|
+
* What `design-pick` Step 6 writes into the folder it archives a choice to:
|
|
350
352
|
* a capture per canvas frame, named `arm-<id>`. A design handoff marks a pick
|
|
351
353
|
* folder an earlier skill wrote. A folder carrying neither is evidence.
|
|
352
354
|
*/
|
|
@@ -54,6 +54,15 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
|
|
|
54
54
|
* target on the oldest spelling lands in one pass rather than two.
|
|
55
55
|
* `draft-ready` alone keys on its namespaced forms below rather than here.
|
|
56
56
|
*
|
|
57
|
+
* Four skills then moved into the family their job belongs to. `pick-by-eye`
|
|
58
|
+
* was the one step of the design flow without the `design-` prefix, so it
|
|
59
|
+
* became `design-pick`. `ui-record` filmed footage while "record" already
|
|
60
|
+
* named the design record, so it became `ui-film`. `ux-audit` and
|
|
61
|
+
* `ux-measure` were the only `ux-` skills beside the `ui-` ones, so they
|
|
62
|
+
* became `ui-audit` and `ui-measure`. `draft-and-pick`, `claude-ux-audit`,
|
|
63
|
+
* and `claude-ux-measure` are retargeted onto the new names for the same
|
|
64
|
+
* reason `claude-ui-test` was.
|
|
65
|
+
*
|
|
57
66
|
* Every name takes two words. Ten of these would have landed as a bare single
|
|
58
67
|
* word under a plain strip, and a bare word such as `review` or `docs` is a
|
|
59
68
|
* substring of ordinary prose with no token left for a later sweep to find.
|
|
@@ -92,8 +101,10 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
92
101
|
'claude-teach': 'teach-workspace',
|
|
93
102
|
'claude-ui-test': 'ui-checklist',
|
|
94
103
|
'ui-test': 'ui-checklist',
|
|
95
|
-
'claude-ux-audit': '
|
|
96
|
-
'claude-ux-measure': '
|
|
104
|
+
'claude-ux-audit': 'ui-audit',
|
|
105
|
+
'claude-ux-measure': 'ui-measure',
|
|
106
|
+
'ux-audit': 'ui-audit',
|
|
107
|
+
'ux-measure': 'ui-measure',
|
|
97
108
|
'claude-worker': 'role-worker',
|
|
98
109
|
'claude-worktree': 'session-worktree',
|
|
99
110
|
'canon-screencast': 'script-screencast',
|
|
@@ -110,8 +121,10 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
110
121
|
'draft-figure': 'draw-figure',
|
|
111
122
|
'draft-identity': 'design-identity',
|
|
112
123
|
'draft-slides': 'make-deck',
|
|
113
|
-
'draft-and-pick': 'pick
|
|
124
|
+
'draft-and-pick': 'design-pick',
|
|
114
125
|
'draft-screencast': 'script-screencast',
|
|
126
|
+
'pick-by-eye': 'design-pick',
|
|
127
|
+
'ui-record': 'ui-film',
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
/**
|
|
@@ -190,6 +203,10 @@ export const RENAMED_DRAFT_POINTERS: readonly string[] = [
|
|
|
190
203
|
* what shipped or what a session ran under whatever name was current then, so
|
|
191
204
|
* rewriting one makes it testify to a release or a run that never happened.
|
|
192
205
|
*
|
|
206
|
+
* A replay fixture is excluded on the same argument. It records a real
|
|
207
|
+
* project's tree as that project held it, so a rewrite changes what the replay
|
|
208
|
+
* proves.
|
|
209
|
+
*
|
|
193
210
|
* The record archives need no entry here. They are gitignored, so the tracked
|
|
194
211
|
* listing every sweep reads never reaches them.
|
|
195
212
|
*/
|
|
@@ -197,7 +214,7 @@ export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
|
|
|
197
214
|
replacements: { ...SKILL_NAME_MAP, ...SKILL_CITATION_MAP },
|
|
198
215
|
keepMarker: 'canon-keep-retired',
|
|
199
216
|
wholeToken: true,
|
|
200
|
-
excludedPrefixes: ['scripts/eval/result-'],
|
|
217
|
+
excludedPrefixes: ['scripts/eval/result-', 'sandbox/fixtures/replay/'],
|
|
201
218
|
excludedPaths: [
|
|
202
219
|
'CHANGELOG.md',
|
|
203
220
|
'src/migrate/skill-names.ts',
|
package/src/records/validate.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run by the claude wrapper installDispatchShim writes in src/sandbox/headless/dispatch.ts.
|
|
3
|
+
# $1 is the real claude binary, and everything after it is the caller's arguments.
|
|
4
|
+
real="$1"
|
|
5
|
+
shift
|
|
6
|
+
|
|
7
|
+
for arg in "$@"; do
|
|
8
|
+
case "$arg" in
|
|
9
|
+
--bg | --background)
|
|
10
|
+
echo "canon sandbox harness: refusing $arg. A sandbox run may not dispatch a background session, so this shim stands first on PATH in place of the real claude binary. Drive the refusal the fixture asks for, or run this outside the harness." >&2
|
|
11
|
+
exit 64
|
|
12
|
+
;;
|
|
13
|
+
esac
|
|
14
|
+
done
|
|
15
|
+
exec "$real" "$@"
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
} from 'node:fs'
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
const DISPATCH_SHIM = fileURLToPath(
|
|
14
|
+
new URL('dispatch-shim.sh', import.meta.url),
|
|
15
|
+
)
|
|
11
16
|
|
|
12
17
|
/*
|
|
13
18
|
* The bound on a nested background dispatch, and the recording behind it.
|
|
@@ -31,9 +36,11 @@ import { join } from 'node:path'
|
|
|
31
36
|
* The prevention. Writes a `claude` that refuses a background dispatch by flag
|
|
32
37
|
* name and delegates everything else, for the runner to place first on the PATH
|
|
33
38
|
* of the session it spawns. It stays a bash file, since it stands in for a
|
|
34
|
-
* binary any shell in the session may exec.
|
|
39
|
+
* binary any shell in the session may exec. The refusal lives in
|
|
40
|
+
* `dispatch-shim.sh`, so the shell gate lints it, and the `claude` written here
|
|
41
|
+
* is a two-line wrapper that runs it with the real binary as its first argument.
|
|
35
42
|
*
|
|
36
|
-
* The real binary is baked into the
|
|
43
|
+
* The real binary is baked into the wrapper rather than passed through the
|
|
37
44
|
* environment. An env var carrying it would sit in the spawned session's own
|
|
38
45
|
* environment, which hands any arm the exact string that walks around the shim.
|
|
39
46
|
*
|
|
@@ -46,22 +53,13 @@ import { join } from 'node:path'
|
|
|
46
53
|
* backstops.
|
|
47
54
|
*/
|
|
48
55
|
export function installDispatchShim(dir: string, real: string): void {
|
|
49
|
-
const
|
|
56
|
+
const wrapper = [
|
|
50
57
|
'#!/usr/bin/env bash',
|
|
51
|
-
|
|
52
|
-
'for arg in "$@"; do',
|
|
53
|
-
' case "$arg" in',
|
|
54
|
-
' --bg | --background)',
|
|
55
|
-
' echo "canon sandbox harness: refusing $arg. A sandbox run may not dispatch a background session, so this shim stands first on PATH in place of the real claude binary. Drive the refusal the fixture asks for, or run this outside the harness." >&2',
|
|
56
|
-
' exit 64',
|
|
57
|
-
' ;;',
|
|
58
|
-
' esac',
|
|
59
|
-
'done',
|
|
60
|
-
`exec ${shellQuote(real)} "$@"`,
|
|
58
|
+
`exec bash ${shellQuote(DISPATCH_SHIM)} ${shellQuote(real)} "$@"`,
|
|
61
59
|
'',
|
|
62
60
|
].join('\n')
|
|
63
61
|
|
|
64
|
-
writeFileSync(join(dir, 'claude'),
|
|
62
|
+
writeFileSync(join(dir, 'claude'), wrapper, 'utf8')
|
|
65
63
|
chmodSync(join(dir, 'claude'), 0o755)
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# stub gh
|
|
3
|
+
dir="$STUB_GH_DIR"
|
|
4
|
+
line="$(printf '%q ' "$@")"
|
|
5
|
+
line="${line% }"
|
|
6
|
+
printf '%s\n' "$line" >>"$dir/gh.log"
|
|
7
|
+
|
|
8
|
+
case "$1" in
|
|
9
|
+
api) exit 0 ;;
|
|
10
|
+
pr)
|
|
11
|
+
case "$2" in
|
|
12
|
+
create)
|
|
13
|
+
count="$(cat "$dir/pr-count" 2>/dev/null || echo 0)"
|
|
14
|
+
count=$((count + 1))
|
|
15
|
+
echo "$count" >"$dir/pr-count"
|
|
16
|
+
echo "https://github.com/$GITHUB_ORG/$STUB_GH_REPO/pull/$count"
|
|
17
|
+
exit 0
|
|
18
|
+
;;
|
|
19
|
+
close | list) exit 0 ;;
|
|
20
|
+
esac
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
printf '%s\n' "$line" >>"$dir/gh-unknown.log"
|
|
25
|
+
echo "stub gh: unknown call: $line" >&2
|
|
26
|
+
exit 1
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
|
+
copyFileSync,
|
|
4
5
|
existsSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
readdirSync,
|
|
7
8
|
readFileSync,
|
|
8
9
|
rmSync,
|
|
9
|
-
writeFileSync,
|
|
10
10
|
} from 'node:fs'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
12
13
|
|
|
13
14
|
const STUB_ORG = 'stub-org'
|
|
14
15
|
const ANCHOR_REPO = 'canon-sandbox'
|
|
@@ -26,34 +27,11 @@ const PINNED_DATE = '2026-01-01T00:00:00Z'
|
|
|
26
27
|
* Answers the `gh` calls the git scenarios make and fails on any other, so a new
|
|
27
28
|
* call reaches a reader as a red arm rather than a silent pass. Every call is
|
|
28
29
|
* logged as one line, quoted so an argument holding a space stays one argument.
|
|
30
|
+
* The pull request URL it prints reads `GITHUB_ORG` and `STUB_GH_REPO` from the
|
|
31
|
+
* environment `createStubRemote` returns, so it follows `STUB_ORG` and
|
|
32
|
+
* `ANCHOR_REPO`.
|
|
29
33
|
*/
|
|
30
|
-
const STUB_GH =
|
|
31
|
-
# stub gh
|
|
32
|
-
dir="$STUB_GH_DIR"
|
|
33
|
-
line="$(printf '%q ' "$@")"
|
|
34
|
-
line="\${line% }"
|
|
35
|
-
printf '%s\\n' "$line" >>"$dir/gh.log"
|
|
36
|
-
|
|
37
|
-
case "$1" in
|
|
38
|
-
api) exit 0 ;;
|
|
39
|
-
pr)
|
|
40
|
-
case "$2" in
|
|
41
|
-
create)
|
|
42
|
-
count="$(cat "$dir/pr-count" 2>/dev/null || echo 0)"
|
|
43
|
-
count=$((count + 1))
|
|
44
|
-
echo "$count" >"$dir/pr-count"
|
|
45
|
-
echo "https://github.com/${STUB_ORG}/${ANCHOR_REPO}/pull/$count"
|
|
46
|
-
exit 0
|
|
47
|
-
;;
|
|
48
|
-
close | list) exit 0 ;;
|
|
49
|
-
esac
|
|
50
|
-
;;
|
|
51
|
-
esac
|
|
52
|
-
|
|
53
|
-
printf '%s\\n' "$line" >>"$dir/gh-unknown.log"
|
|
54
|
-
echo "stub gh: unknown call: $line" >&2
|
|
55
|
-
exit 1
|
|
56
|
-
`
|
|
34
|
+
const STUB_GH = fileURLToPath(new URL('stub-gh.sh', import.meta.url))
|
|
57
35
|
|
|
58
36
|
/**
|
|
59
37
|
* Builds a local bare repository and a stub `gh` under `stubDir` and returns the
|
|
@@ -75,13 +53,14 @@ export function createStubRemote(stubDir: string): Record<string, string> {
|
|
|
75
53
|
)
|
|
76
54
|
|
|
77
55
|
const gh = join(bin, 'gh')
|
|
78
|
-
|
|
56
|
+
copyFileSync(STUB_GH, gh)
|
|
79
57
|
chmodSync(gh, 0o755)
|
|
80
58
|
|
|
81
59
|
return {
|
|
82
60
|
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
83
61
|
STUB_GH_DIR: stubDir,
|
|
84
62
|
GITHUB_ORG: STUB_ORG,
|
|
63
|
+
STUB_GH_REPO: ANCHOR_REPO,
|
|
85
64
|
GIT_CONFIG_COUNT: '1',
|
|
86
65
|
GIT_CONFIG_KEY_0: `url.${remotes}/.insteadOf`,
|
|
87
66
|
GIT_CONFIG_VALUE_0: `https://github.com/${STUB_ORG}/`,
|