@erclx/canon 5.54.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/create-skill/SKILL.md +1 -1
  4. package/claude/skills/document-health/SKILL.md +6 -6
  5. package/claude/skills/document-repair/SKILL.md +5 -5
  6. package/claude/skills/target-check/SKILL.md +6 -8
  7. package/claude/skills/target-check/references/domains.md +1 -4
  8. package/claude/skills/target-setup/SKILL.md +8 -8
  9. package/claude/skills/target-update/SKILL.md +5 -5
  10. package/docs/agents/install-and-sync.md +2 -2
  11. package/docs/agents/output-shape.md +11 -10
  12. package/docs/target/sync.md +1 -1
  13. package/package.json +1 -1
  14. package/scripts/core/install-check.sh +1 -1
  15. package/scripts/lib/ui.sh +12 -65
  16. package/src/audits/run.ts +1 -1
  17. package/src/claude/routing.ts +1 -1
  18. package/src/cli/run.ts +5 -6
  19. package/src/cli/ui.ts +87 -28
  20. package/src/cli.ts +6 -0
  21. package/src/commands/claude.ts +2 -3
  22. package/src/commands/design.ts +8 -13
  23. package/src/commands/gate.ts +5 -3
  24. package/src/commands/gov.ts +11 -21
  25. package/src/commands/labels.ts +1 -1
  26. package/src/commands/pr.ts +1 -1
  27. package/src/commands/sync.ts +1 -1
  28. package/src/commands/tooling.ts +5 -8
  29. package/src/commands/transcripts.ts +6 -11
  30. package/src/gate/sequencer.ts +1 -1
  31. package/src/gate/stages.ts +0 -10
  32. package/src/hooks/post-merge.ts +2 -5
  33. package/src/process/harness.ts +3 -4
  34. package/src/sandbox/equivalence.ts +0 -1
  35. package/src/sandbox/provision.ts +8 -13
  36. package/src/sandbox/scenario.ts +6 -12
  37. package/src/sync/engine.ts +3 -8
  38. package/src/sync/workflow.ts +3 -5
  39. package/src/tooling/verify-stack.ts +1 -1
  40. package/tooling/base/configs/.husky/post-merge +6 -10
  41. package/scripts/core/check-color-source.sh +0 -51
@@ -27,7 +27,7 @@ import {
27
27
  logStep,
28
28
  logWarn,
29
29
  outro,
30
- palette,
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
- const { GREEN, NC } = palette(process.stderr)
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: { ...env, CANON_NON_INTERACTIVE: '1' },
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('✓ Sandbox Ready')
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('✓ Sandbox clean')
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('✓ Sandbox at baseline')
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('✓ Sandbox reset complete')
554
+ finish('Sandbox reset complete')
560
555
  }
561
556
 
562
557
  /**
@@ -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
- * echoed on the frame, then the first arm for a headless caller, then a prompt.
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
- // Read off the scenario's environment rather than this process's, as
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 an argument or set CANON_NON_INTERACTIVE=1.`,
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
  }
@@ -26,7 +26,7 @@ import {
26
26
  logStep,
27
27
  logWarn,
28
28
  outro,
29
- palette,
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
- outro()
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
- outro()
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
 
@@ -19,7 +19,7 @@ import {
19
19
  logStep,
20
20
  logWarn,
21
21
  outro,
22
- palette,
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 under `CANON_NON_INTERACTIVE=1`, and the first option here was
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
- const { GREEN, NC } = palette(process.stderr)
197
- outro()
198
- process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
196
+ outroSuccess(message)
199
197
  return 0
200
198
  }
@@ -124,7 +124,7 @@ async function run({
124
124
  await phase(state, 'Sync', () =>
125
125
  execa('bun', [cli, 'tooling', 'sync', stack, '.', '--write'], {
126
126
  ...captured(state.dir),
127
- env: { CI: '1', CANON_NON_INTERACTIVE: '1' },
127
+ env: { CI: '1' },
128
128
  }),
129
129
  )
130
130
 
@@ -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 the verb carries the
31
- # only step that would update it, so reinstall here and probe again. The probe
32
- # reads the verb's own usage line rather than the exit, since an older binary
33
- # answers an unknown command's `--help` with its root help and exits zero.
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
@@ -1,51 +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:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
7
-
8
- SCRIPTS_ROOT="$PROJECT_ROOT/scripts"
9
- SANDBOX_ROOT="$PROJECT_ROOT/sandbox"
10
- PALETTE_SOURCE="$SCRIPTS_ROOT/lib/ui.sh"
11
-
12
- # `find` writes to stderr and returns non-zero for a missing root, inside a
13
- # process substitution whose status nothing reads. Without this the walk covers
14
- # nothing and the check reports the tree clean having never measured it.
15
- if [ ! -f "$PALETTE_SOURCE" ]; then
16
- echo "No palette source at ${PALETTE_SOURCE#"$PROJECT_ROOT/"}, color sourcing unverifiable." >&2
17
- exit 1
18
- fi
19
-
20
- # The same silence follows a root that moved, so a walked root that is gone
21
- # fails here rather than shrinking the corpus with no message.
22
- for root in "$SCRIPTS_ROOT" "$SANDBOX_ROOT"; do
23
- if [ ! -d "$root" ]; then
24
- echo "No shell root at ${root#"$PROJECT_ROOT/"}, color sourcing unverifiable." >&2
25
- exit 1
26
- fi
27
- done
28
-
29
- # Any SGR sequence in either spelling. The cursor and erase sequences an
30
- # interactive prompt writes end in a letter instead, and they run only where a
31
- # terminal already exists, so they are not what this measures.
32
- SGR='\\(033|e|x1b)\[[0-9;]*m'
33
-
34
- # Sixteen copies of one decision is what made color impossible to turn off on
35
- # the TypeScript side. A rule nothing enforces rebuilds them the next time a
36
- # script wants grey, so the escape lives in the shared library and every other
37
- # script reads the palette that library derives for its stream.
38
- offenders=""
39
- while IFS= read -r file; do
40
- [ "$file" = "$PALETTE_SOURCE" ] && continue
41
- if grep -qE "$SGR" "$file"; then
42
- offenders="$offenders ${file#"$PROJECT_ROOT/"}"$'\n'
43
- fi
44
- done < <(find "$SCRIPTS_ROOT" "$SANDBOX_ROOT" -type f -name '*.sh')
45
-
46
- if [ -n "$offenders" ]; then
47
- echo "A color escape is defined outside the shared library:" >&2
48
- printf '%s' "$offenders" >&2
49
- echo "Source scripts/lib/ui.sh and read its palette, so one answer covers every writer." >&2
50
- exit 1
51
- fi