@erclx/canon 4.87.0 → 4.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +1 -1
- package/claude/skills/docs-fold/SKILL.md +4 -4
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +8 -8
- package/claude/skills/git-split/SKILL.md +19 -19
- package/claude/skills/memory-capture/SKILL.md +2 -2
- package/claude/skills/memory-review/SKILL.md +2 -2
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/review-address/SKILL.md +11 -11
- package/claude/skills/review-pr/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
- package/claude/skills/role-orchestrator/scripts/poll.sh +1 -1
- package/claude/skills/teach-workspace/SKILL.md +2 -2
- package/claude/skills/ui-test/REQUIREMENT.md +1 -1
- package/claude/skills/ui-test/SKILL.md +2 -2
- package/docs/agents/commands.md +2 -1
- package/docs/agents/records.md +27 -0
- package/docs/agents/sandbox.md +1 -1
- package/docs/workflow/ai-workflow.md +4 -2
- package/governance/rules/core/055-scratch.md +1 -0
- package/package.json +1 -1
- package/scripts/tooling/verify.sh +2 -2
- package/src/claude/skills-headings.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/records.ts +159 -0
- package/src/migrate/record-layout.ts +2 -2
- package/src/migrate/scratch-evidence.ts +3 -5
- package/src/records/prune.ts +488 -0
- package/src/records/size.ts +24 -1
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -2
- package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -2
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +2 -2
- package/tooling/claude/seeds/CLAUDE.md +2 -6
package/src/commands/migrate.ts
CHANGED
|
@@ -1164,7 +1164,7 @@ export function register(program: Command): void {
|
|
|
1164
1164
|
' review/{design,board,slides,diagrams}/ -> tmp/render/<kind>/',
|
|
1165
1165
|
' review/references/ -> picks/references/',
|
|
1166
1166
|
' review/branch/review-<slug>.md -> review/branch-<slug>.md',
|
|
1167
|
-
' review/ui-checklist-<slug>.md -> tmp/ui-checklist/<slug>.md',
|
|
1167
|
+
' review/ui-checklist-<slug>.md -> tmp/handoff/ui-checklist/<slug>.md',
|
|
1168
1168
|
' review/evidence/<slug>/ -> picks/<slug>/ or evidence/<nn>-<slug>/',
|
|
1169
1169
|
'',
|
|
1170
1170
|
'An evidence folder is a pick when it directly holds an arm-<id>',
|
package/src/commands/records.ts
CHANGED
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
isOrdinalKind,
|
|
11
11
|
ORDINAL_KINDS,
|
|
12
12
|
} from '@/records/ordinal'
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_OLDER_THAN_DAYS,
|
|
15
|
+
type PruneReport,
|
|
16
|
+
type PruneUnit,
|
|
17
|
+
pruneScratch,
|
|
18
|
+
} from '@/records/prune'
|
|
13
19
|
import {
|
|
14
20
|
type FolderSize,
|
|
15
21
|
formatBytes,
|
|
@@ -50,6 +56,9 @@ const EXIT_MIGRATABLE = 2
|
|
|
50
56
|
/** Returned when `--claim` loses every retry to a collision. */
|
|
51
57
|
const EXIT_CONTENDED = 2
|
|
52
58
|
|
|
59
|
+
/** Returned when candidates exist and `--write` was not passed. */
|
|
60
|
+
const EXIT_PRUNABLE = 2
|
|
61
|
+
|
|
53
62
|
interface ValidateCommandOptions {
|
|
54
63
|
readonly json?: boolean
|
|
55
64
|
readonly root?: string
|
|
@@ -61,6 +70,11 @@ interface MigrateCommandOptions extends ValidateCommandOptions {
|
|
|
61
70
|
readonly write?: boolean
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
interface PruneCommandOptions extends ValidateCommandOptions {
|
|
74
|
+
readonly write?: boolean
|
|
75
|
+
readonly olderThan?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
interface OrdinalCommandOptions extends ValidateCommandOptions {
|
|
65
79
|
readonly claim?: boolean
|
|
66
80
|
}
|
|
@@ -233,6 +247,55 @@ export function register(program: Command): void {
|
|
|
233
247
|
process.exitCode = await runSize(opts)
|
|
234
248
|
})
|
|
235
249
|
|
|
250
|
+
records
|
|
251
|
+
.command('prune-tmp')
|
|
252
|
+
.description('Report scratch nobody has touched in a while, and delete it')
|
|
253
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
254
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
255
|
+
.option('--write', 'Delete every candidate the report lists')
|
|
256
|
+
.option(
|
|
257
|
+
'--older-than <days>',
|
|
258
|
+
`Age in days a unit's newest file must clear to be offered (default ${DEFAULT_OLDER_THAN_DAYS})`,
|
|
259
|
+
)
|
|
260
|
+
.option('--root <path>', 'Project root, defaulting to the main worktree')
|
|
261
|
+
.addHelpText(
|
|
262
|
+
'after',
|
|
263
|
+
[
|
|
264
|
+
'',
|
|
265
|
+
'A candidate is a unit whose newest file is older than --older-than:',
|
|
266
|
+
' a tmp/<slug>/ folder, a folder one level inside runs/, render/, or',
|
|
267
|
+
' pr/, or a single marker file inside hooks/<hook>/. A unit with no',
|
|
268
|
+
' files is offered whatever its age.',
|
|
269
|
+
'',
|
|
270
|
+
'tmp/handoff/ and tmp/pr/poll/ are never offered, since a reader deletes',
|
|
271
|
+
'a handoff itself and a poll baseline is live state. The pre-split names',
|
|
272
|
+
'the same two folders replaced (memory-routing/, teach-promotion/,',
|
|
273
|
+
'ui-checklist/, pr-poll/) are skipped the same way rather than offered',
|
|
274
|
+
'as ordinary slugs.',
|
|
275
|
+
'',
|
|
276
|
+
'Exit codes:',
|
|
277
|
+
' 0 nothing to prune, or --write deleted every candidate',
|
|
278
|
+
' 1 refused, with the reason on stderr or in the JSON record, or a',
|
|
279
|
+
' delete failed',
|
|
280
|
+
' 2 candidates exist and --write was not passed',
|
|
281
|
+
'',
|
|
282
|
+
'It writes nothing until --write is passed, matching canon records',
|
|
283
|
+
'migrate: a session record has no history to undo a wrong delete from.',
|
|
284
|
+
'It reads mtime like canon records size, so a machine restored by canon',
|
|
285
|
+
'records pull reads everything as new, which fails safe by offering',
|
|
286
|
+
'nothing.',
|
|
287
|
+
'',
|
|
288
|
+
'Examples:',
|
|
289
|
+
' canon records prune-tmp',
|
|
290
|
+
' canon records prune-tmp --write',
|
|
291
|
+
' canon records prune-tmp --older-than 30 --json',
|
|
292
|
+
'',
|
|
293
|
+
].join('\n'),
|
|
294
|
+
)
|
|
295
|
+
.action(async (opts: PruneCommandOptions) => {
|
|
296
|
+
process.exitCode = await runPruneTmp(opts)
|
|
297
|
+
})
|
|
298
|
+
|
|
236
299
|
records
|
|
237
300
|
.command('push')
|
|
238
301
|
.description(
|
|
@@ -390,6 +453,102 @@ function reportSize(outcome: Extract<SizeOutcome, { ok: true }>): void {
|
|
|
390
453
|
outro()
|
|
391
454
|
}
|
|
392
455
|
|
|
456
|
+
async function runPruneTmp(opts: PruneCommandOptions): Promise<number> {
|
|
457
|
+
const emitJson = opts.json ?? false
|
|
458
|
+
const olderThan = Number(opts.olderThan ?? DEFAULT_OLDER_THAN_DAYS)
|
|
459
|
+
|
|
460
|
+
if (!Number.isFinite(olderThan) || olderThan <= 0) {
|
|
461
|
+
return reportRefusal(
|
|
462
|
+
'canon records prune-tmp',
|
|
463
|
+
{
|
|
464
|
+
reason: 'bad-older-than',
|
|
465
|
+
message: `--older-than must be a positive number of days, got ${opts.olderThan}.`,
|
|
466
|
+
},
|
|
467
|
+
emitJson,
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
472
|
+
const write = opts.write ?? false
|
|
473
|
+
const outcome = await pruneScratch(root, olderThan, write)
|
|
474
|
+
|
|
475
|
+
if (!outcome.ok)
|
|
476
|
+
return reportRefusal('canon records prune-tmp', outcome, emitJson)
|
|
477
|
+
|
|
478
|
+
return reportPrune(outcome, emitJson)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function pruneRow(entry: PruneUnit): string[] {
|
|
482
|
+
return [
|
|
483
|
+
entry.path,
|
|
484
|
+
String(entry.files),
|
|
485
|
+
formatBytes(entry.bytes),
|
|
486
|
+
entry.newest ?? '',
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function pruneExitCode(outcome: PruneReport): number {
|
|
491
|
+
if (outcome.failed.length > 0) return 1
|
|
492
|
+
if (outcome.candidates.length > 0 && outcome.deleted.length === 0)
|
|
493
|
+
return EXIT_PRUNABLE
|
|
494
|
+
return 0
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function reportPrune(outcome: PruneReport, emitJson: boolean): number {
|
|
498
|
+
const exit = pruneExitCode(outcome)
|
|
499
|
+
|
|
500
|
+
if (emitJson) {
|
|
501
|
+
process.stdout.write(`${JSON.stringify(outcome)}\n`)
|
|
502
|
+
return exit
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
intro('canon records prune-tmp')
|
|
506
|
+
|
|
507
|
+
if (outcome.candidates.length === 0) {
|
|
508
|
+
logStep('Prunable')
|
|
509
|
+
logInfo(`none, older than ${plural(outcome.olderThan, 'day')}`)
|
|
510
|
+
} else {
|
|
511
|
+
logStep(outcome.deleted.length > 0 ? 'Deleted' : 'Prunable')
|
|
512
|
+
|
|
513
|
+
const headers = ['path', 'files', 'size', 'newest']
|
|
514
|
+
const rows = outcome.candidates.map(pruneRow)
|
|
515
|
+
const widths = headers.map((header, column) =>
|
|
516
|
+
columnWidth(
|
|
517
|
+
header,
|
|
518
|
+
rows.map((row) => row[column]),
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
const render = (cells: readonly string[]): string =>
|
|
522
|
+
cells
|
|
523
|
+
.map((cell, column) =>
|
|
524
|
+
column === 0
|
|
525
|
+
? cell.padEnd(widths[column])
|
|
526
|
+
: cell.padStart(widths[column]),
|
|
527
|
+
)
|
|
528
|
+
.join(' ')
|
|
529
|
+
.trimEnd()
|
|
530
|
+
|
|
531
|
+
pipeOutput([render(headers), ...rows.map(render)].join('\n'))
|
|
532
|
+
|
|
533
|
+
if (outcome.deleted.length === 0) logInfo('Re-run with --write to delete.')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (outcome.skipped.length > 0) {
|
|
537
|
+
logStep('Skipped')
|
|
538
|
+
for (const entry of outcome.skipped)
|
|
539
|
+
logInfo(`${entry.path}: ${entry.reason}`)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (outcome.failed.length > 0) {
|
|
543
|
+
logStep('Failed')
|
|
544
|
+
for (const entry of outcome.failed)
|
|
545
|
+
logWarn(`${entry.path}: ${entry.message}`)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
outro()
|
|
549
|
+
return exit
|
|
550
|
+
}
|
|
551
|
+
|
|
393
552
|
async function runPush(opts: BackupCommandOptions): Promise<number> {
|
|
394
553
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
395
554
|
const outcome = await pushRecords(root)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* feedback moves to `.canon/feedback/`, option captures to `.canon/picks/`,
|
|
7
7
|
* claim-backing folders to a numbered `.canon/evidence/`, renders rebuilt from
|
|
8
8
|
* committed sources to `.canon/tmp/render/`, a branch report flattens to
|
|
9
|
-
* `review/branch-<slug>.md`, and a flat checklist joins the `tmp/ui-checklist/`
|
|
9
|
+
* `review/branch-<slug>.md`, and a flat checklist joins the `tmp/handoff/ui-checklist/`
|
|
10
10
|
* handoff folder. `canon records push` carries every top-level `.canon/`
|
|
11
11
|
* entry except `EXCLUDED_ENTRIES`, so the new root folders are backed with no
|
|
12
12
|
* list edit, and `tmp/render/` is deliberately not.
|
|
@@ -120,7 +120,7 @@ export const RECORD_LAYOUT_MOVES: readonly RecordLayoutMove[] = [
|
|
|
120
120
|
kind: 'files',
|
|
121
121
|
from: ['review'],
|
|
122
122
|
prefix: 'ui-checklist-',
|
|
123
|
-
to: [SCRATCH, 'ui-checklist'],
|
|
123
|
+
to: [SCRATCH, 'handoff', 'ui-checklist'],
|
|
124
124
|
renamed: '',
|
|
125
125
|
prune: false,
|
|
126
126
|
},
|
|
@@ -48,11 +48,9 @@ import { recordDir, SCRATCH } from '@/record-root'
|
|
|
48
48
|
* `verify-astro` and `verify-vite-react` pass the same test and are excluded
|
|
49
49
|
* by name: both are scaffolds a command generates rather than records a
|
|
50
50
|
* session wrote, and each is 100+ MB, which the review remote is not sized
|
|
51
|
-
* for. `ablation`, `
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* a skill body names each of those paths, so moving one needs a code change
|
|
55
|
-
* first rather than a promotion.
|
|
51
|
+
* for. `ablation`, `runs`, `memory-archive`, `hooks`, `pr`, and `handoff`
|
|
52
|
+
* fail the second clause: a script or a skill body names each of those
|
|
53
|
+
* paths, so moving one needs a code change first rather than a promotion.
|
|
56
54
|
*/
|
|
57
55
|
export const PROMOTED_FOLDERS: readonly string[] = [
|
|
58
56
|
'hero-probe',
|