@erclx/aitk 0.43.0 → 0.45.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/claude-autoship/SKILL.md +15 -10
- package/claude/skills/claude-docs/SKILL.md +16 -2
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +14 -4
- package/claude/skills/claude-memory-capture/SKILL.md +45 -16
- package/claude/skills/claude-memory-review/REQUIREMENT.md +8 -2
- package/claude/skills/claude-memory-review/SKILL.md +41 -25
- package/claude/skills/claude-orchestrate/SKILL.md +5 -0
- package/claude/skills/git-ship/SKILL.md +14 -11
- package/claude/skills/session-resume/SKILL.md +2 -2
- package/docs/agents/commands.md +36 -34
- package/docs/agents/index.md +2 -1
- package/docs/agents/indexes.md +3 -1
- package/docs/agents/scripting.md +4 -3
- package/docs/agents/skills-audit.md +55 -0
- package/docs/agents/tasks.md +33 -1
- package/docs/ai-workflow.md +5 -2
- package/package.json +1 -1
- package/scripts/core/verify.sh +8 -0
- package/snippets/claude/orchestrator-sweep.md +2 -0
- package/src/claude/seeds.ts +1 -0
- package/src/claude/skills-audit.ts +215 -0
- package/src/claude/skills-list.ts +3 -3
- package/src/commands/claude.ts +283 -5
- package/src/commands/context.ts +1 -4
- package/src/commands/tasks.ts +113 -0
- package/src/tasks/archive.ts +5 -3
- package/src/tasks/validate.ts +401 -0
- package/src/ui.ts +5 -0
- package/standards/tasks.md +5 -0
- package/tooling/claude/reference.md +11 -1
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +60 -0
- package/tooling/claude/seeds/.claude/memory/index.md +8 -0
- package/tooling/claude/seeds/.claude/settings.json +4 -0
- package/tooling/claude/seeds/CLAUDE.md +3 -0
package/src/commands/claude.ts
CHANGED
|
@@ -12,6 +12,15 @@ import {
|
|
|
12
12
|
type Seed,
|
|
13
13
|
} from '@/claude/seeds'
|
|
14
14
|
import { listSeeds, readSeedContents } from '@/claude/seeds-list'
|
|
15
|
+
import {
|
|
16
|
+
auditExitCode,
|
|
17
|
+
auditSkills,
|
|
18
|
+
CORPORA,
|
|
19
|
+
DESCRIPTION_LIMIT,
|
|
20
|
+
REQUIREMENT_SECTIONS,
|
|
21
|
+
type SkillFinding,
|
|
22
|
+
type SkillsAudit,
|
|
23
|
+
} from '@/claude/skills-audit'
|
|
15
24
|
import { listSkills } from '@/claude/skills-list'
|
|
16
25
|
import {
|
|
17
26
|
planSettings,
|
|
@@ -24,6 +33,7 @@ import { execScript, PROJECT_ROOT } from '@/exec'
|
|
|
24
33
|
import { isDirectory, resolveTarget } from '@/target'
|
|
25
34
|
import { injectGitignore, pruneGitignore } from '@/tooling/inject'
|
|
26
35
|
import {
|
|
36
|
+
frameError,
|
|
27
37
|
intro,
|
|
28
38
|
isNonInteractive,
|
|
29
39
|
logAdd,
|
|
@@ -32,6 +42,8 @@ import {
|
|
|
32
42
|
logStep,
|
|
33
43
|
logWarn,
|
|
34
44
|
outro,
|
|
45
|
+
pipeOutput,
|
|
46
|
+
plural,
|
|
35
47
|
select,
|
|
36
48
|
} from '@/ui'
|
|
37
49
|
|
|
@@ -49,12 +61,17 @@ interface SkillsListOptions {
|
|
|
49
61
|
readonly names?: boolean
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
interface SkillsAuditOptions {
|
|
65
|
+
readonly json?: boolean
|
|
66
|
+
readonly requirementsOnly?: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
const SEEDED_FILES: readonly string[] = [
|
|
53
70
|
'ARCHITECTURE.md',
|
|
54
71
|
'REQUIREMENTS.md',
|
|
55
72
|
'DESIGN.md',
|
|
56
73
|
]
|
|
57
|
-
const SEEDED_DIRS: readonly string[] = ['tasks', 'wireframes']
|
|
74
|
+
const SEEDED_DIRS: readonly string[] = ['memory', 'tasks', 'wireframes']
|
|
58
75
|
const USER_DIR = join('tooling', 'claude', 'user')
|
|
59
76
|
const STATUSLINE = 'statusline-command.sh'
|
|
60
77
|
|
|
@@ -139,15 +156,15 @@ export function register(program: Command): void {
|
|
|
139
156
|
|
|
140
157
|
const skills = claude
|
|
141
158
|
.command('skills')
|
|
142
|
-
.description('Plugin skill catalog (list)')
|
|
143
|
-
.argument('[subcommand]', "
|
|
159
|
+
.description('Plugin skill catalog (list, audit)')
|
|
160
|
+
.argument('[subcommand]', "One of 'list' or 'audit'")
|
|
144
161
|
.helpOption('-h, --help', 'Show this help message')
|
|
145
162
|
.action((subcommand: string | undefined) => {
|
|
146
163
|
intro('aitk claude')
|
|
147
164
|
logError(
|
|
148
165
|
subcommand === undefined
|
|
149
|
-
? "Missing subcommand. Use 'list'."
|
|
150
|
-
: `Unknown subcommand: ${subcommand}. Use 'list'.`,
|
|
166
|
+
? "Missing subcommand. Use 'list' or 'audit'."
|
|
167
|
+
: `Unknown subcommand: ${subcommand}. Use 'list' or 'audit'.`,
|
|
151
168
|
)
|
|
152
169
|
outro()
|
|
153
170
|
process.exitCode = 1
|
|
@@ -172,6 +189,41 @@ export function register(program: Command): void {
|
|
|
172
189
|
.action((opts: SkillsListOptions) => {
|
|
173
190
|
process.exitCode = runSkillsList(opts)
|
|
174
191
|
})
|
|
192
|
+
|
|
193
|
+
skills
|
|
194
|
+
.command('audit')
|
|
195
|
+
.description(
|
|
196
|
+
'Report both skill corpora against the mechanical rules in standards/skill.md',
|
|
197
|
+
)
|
|
198
|
+
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
199
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
200
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
201
|
+
.option(
|
|
202
|
+
'--requirements-only',
|
|
203
|
+
'Run the gating requirement-presence check alone',
|
|
204
|
+
)
|
|
205
|
+
.addHelpText(
|
|
206
|
+
'after',
|
|
207
|
+
[
|
|
208
|
+
'',
|
|
209
|
+
'Exit codes:',
|
|
210
|
+
' 0 the audit completed with every skill carrying a requirement',
|
|
211
|
+
' 1 refused, with the reason on stderr',
|
|
212
|
+
' 2 a skill folder carries no REQUIREMENT.md',
|
|
213
|
+
'',
|
|
214
|
+
'Only a missing REQUIREMENT.md sets a failing exit code. Name, description,',
|
|
215
|
+
'folder, and requirement-section findings are advisory.',
|
|
216
|
+
'',
|
|
217
|
+
'Examples:',
|
|
218
|
+
' aitk claude skills audit',
|
|
219
|
+
' aitk claude skills audit --json',
|
|
220
|
+
' aitk claude skills audit --requirements-only',
|
|
221
|
+
'',
|
|
222
|
+
].join('\n'),
|
|
223
|
+
)
|
|
224
|
+
.action(async (path: string | undefined, opts: SkillsAuditOptions) => {
|
|
225
|
+
process.exitCode = await runSkillsAudit(path, opts)
|
|
226
|
+
})
|
|
175
227
|
}
|
|
176
228
|
|
|
177
229
|
function succeed(message: string): number {
|
|
@@ -413,3 +465,229 @@ function runSkillsList(opts: SkillsListOptions): number {
|
|
|
413
465
|
outro()
|
|
414
466
|
return 0
|
|
415
467
|
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Measures the tree at the cwd rather than the toolkit root the catalog reads,
|
|
471
|
+
* so a linked worktree audits its own branch instead of reporting on `main`. A
|
|
472
|
+
* target carrying `.claude/skills/` alone is in scope for the same reason.
|
|
473
|
+
*/
|
|
474
|
+
async function runSkillsAudit(
|
|
475
|
+
path: string | undefined,
|
|
476
|
+
opts: SkillsAuditOptions,
|
|
477
|
+
): Promise<number> {
|
|
478
|
+
const root = resolve(path ?? process.cwd())
|
|
479
|
+
const gateOnly = opts.requirementsOnly ?? false
|
|
480
|
+
const report = await auditSkills(root)
|
|
481
|
+
|
|
482
|
+
if (report.corpora.length === 0) {
|
|
483
|
+
return refuseAudit(
|
|
484
|
+
`No skill corpus under ${root}. Looked for ${CORPORA.join(' and ')}.`,
|
|
485
|
+
gateOnly,
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (gateOnly) {
|
|
490
|
+
reportRequirementGate(report)
|
|
491
|
+
} else {
|
|
492
|
+
intro('aitk claude skills audit')
|
|
493
|
+
reportScope(report)
|
|
494
|
+
reportRequirements(report)
|
|
495
|
+
reportFrontmatter(report)
|
|
496
|
+
reportFolder(report)
|
|
497
|
+
reportRequirementShape(report)
|
|
498
|
+
reportUnmeasured()
|
|
499
|
+
outro()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (opts.json) {
|
|
503
|
+
process.stdout.write(
|
|
504
|
+
`${JSON.stringify({
|
|
505
|
+
root,
|
|
506
|
+
corpora: report.corpora.map((corpus) => ({
|
|
507
|
+
path: corpus.rel,
|
|
508
|
+
skills: corpus.skills,
|
|
509
|
+
})),
|
|
510
|
+
skills: report.skills,
|
|
511
|
+
findings: {
|
|
512
|
+
missingRequirement: report.missingRequirement,
|
|
513
|
+
nameMismatch: report.nameMismatch,
|
|
514
|
+
missingDescription: report.missingDescription,
|
|
515
|
+
longDescription: report.longDescription,
|
|
516
|
+
readme: report.readme,
|
|
517
|
+
folderName: report.folderName,
|
|
518
|
+
requirementSections: report.requirementSections,
|
|
519
|
+
},
|
|
520
|
+
checkpoints: {
|
|
521
|
+
descriptionLimit: DESCRIPTION_LIMIT,
|
|
522
|
+
requirementSections: REQUIREMENT_SECTIONS,
|
|
523
|
+
corpora: CORPORA,
|
|
524
|
+
},
|
|
525
|
+
})}\n`,
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return auditExitCode(report)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function refuseAudit(message: string, gateOnly: boolean): number {
|
|
533
|
+
if (gateOnly) {
|
|
534
|
+
frameError(message)
|
|
535
|
+
return 1
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
intro('aitk claude skills audit')
|
|
539
|
+
logStep('Refused')
|
|
540
|
+
logWarn(message)
|
|
541
|
+
outro()
|
|
542
|
+
return 1
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Prints nothing when every skill carries a requirement.
|
|
547
|
+
*
|
|
548
|
+
* `--requirements-only` is what `verify.sh` runs on every push, and that script
|
|
549
|
+
* pipes a stage's whole output into its own frame. A passing gate that printed
|
|
550
|
+
* its frame would nest one inside the other on every contributor's push.
|
|
551
|
+
*/
|
|
552
|
+
function reportRequirementGate(report: SkillsAudit): void {
|
|
553
|
+
const missing = report.missingRequirement
|
|
554
|
+
if (missing.length === 0) return
|
|
555
|
+
|
|
556
|
+
intro('aitk claude skills audit')
|
|
557
|
+
logError(
|
|
558
|
+
missing.length === 1
|
|
559
|
+
? '1 skill folder carries no REQUIREMENT.md'
|
|
560
|
+
: `${missing.length} skill folders carry no REQUIREMENT.md`,
|
|
561
|
+
)
|
|
562
|
+
pipeOutput(missing.join('\n'))
|
|
563
|
+
outro()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function reportFindings(findings: readonly SkillFinding[]): void {
|
|
567
|
+
pipeOutput(
|
|
568
|
+
findings.map((found) => `${found.rel} ${found.detail}`).join('\n'),
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Names each corpus that resolved, since a corpus the tree does not carry is
|
|
574
|
+
* skipped silently and a count taken over one of the two reads as the whole.
|
|
575
|
+
*/
|
|
576
|
+
function reportScope(report: SkillsAudit): void {
|
|
577
|
+
logStep('Scope')
|
|
578
|
+
for (const corpus of report.corpora) {
|
|
579
|
+
logInfo(`${corpus.rel}: ${plural(corpus.skills, 'skill')}`)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function reportRequirements(report: SkillsAudit): void {
|
|
584
|
+
logStep('Requirements')
|
|
585
|
+
logInfo('Every skill folder carries REQUIREMENT.md beside SKILL.md.')
|
|
586
|
+
logInfo('This is the only measure here that fails a run.')
|
|
587
|
+
|
|
588
|
+
if (report.missingRequirement.length === 0) {
|
|
589
|
+
logInfo('Every skill carries one.')
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
logWarn(`${plural(report.missingRequirement.length, 'skill')} without one`)
|
|
594
|
+
pipeOutput(report.missingRequirement.join('\n'))
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function reportFrontmatter(report: SkillsAudit): void {
|
|
598
|
+
logStep('Frontmatter')
|
|
599
|
+
logInfo(
|
|
600
|
+
`name matches the folder, and description is present and under ${DESCRIPTION_LIMIT} characters.`,
|
|
601
|
+
)
|
|
602
|
+
logInfo('A body whose frontmatter does not parse reads as declaring neither.')
|
|
603
|
+
|
|
604
|
+
const findings =
|
|
605
|
+
report.nameMismatch.length +
|
|
606
|
+
report.missingDescription.length +
|
|
607
|
+
report.longDescription.length
|
|
608
|
+
if (findings === 0) {
|
|
609
|
+
logInfo('Every body declares both fields.')
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (report.nameMismatch.length > 0) {
|
|
614
|
+
logWarn(
|
|
615
|
+
`${plural(report.nameMismatch.length, 'body')} whose name does not match its folder`,
|
|
616
|
+
)
|
|
617
|
+
reportFindings(report.nameMismatch)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (report.missingDescription.length > 0) {
|
|
621
|
+
logWarn(
|
|
622
|
+
`${plural(report.missingDescription.length, 'body')} without a description`,
|
|
623
|
+
)
|
|
624
|
+
pipeOutput(report.missingDescription.join('\n'))
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (report.longDescription.length > 0) {
|
|
628
|
+
logWarn(
|
|
629
|
+
`${plural(report.longDescription.length, 'description')} past the ${DESCRIPTION_LIMIT}-character ceiling`,
|
|
630
|
+
)
|
|
631
|
+
reportFindings(report.longDescription)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function reportFolder(report: SkillsAudit): void {
|
|
636
|
+
logStep('Folder')
|
|
637
|
+
logInfo(
|
|
638
|
+
'No README.md inside a skill folder, and a folder name in kebab-case carrying no capital or underscore.',
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
if (report.readme.length === 0 && report.folderName.length === 0) {
|
|
642
|
+
logInfo('Every folder conforms.')
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (report.readme.length > 0) {
|
|
647
|
+
logWarn(`${plural(report.readme.length, 'folder')} carrying a README.md`)
|
|
648
|
+
pipeOutput(report.readme.join('\n'))
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (report.folderName.length > 0) {
|
|
652
|
+
logWarn(
|
|
653
|
+
`${plural(report.folderName.length, 'folder name')} outside kebab-case`,
|
|
654
|
+
)
|
|
655
|
+
pipeOutput(report.folderName.join('\n'))
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function reportRequirementShape(report: SkillsAudit): void {
|
|
660
|
+
logStep('Requirement shape')
|
|
661
|
+
logInfo(
|
|
662
|
+
`Each REQUIREMENT.md declares ${REQUIREMENT_SECTIONS.join(' and ')}, matched at any heading level.`,
|
|
663
|
+
)
|
|
664
|
+
logInfo(
|
|
665
|
+
'A folder carrying no requirement is reported above rather than counted twice here.',
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if (report.requirementSections.length === 0) {
|
|
669
|
+
logInfo('Every requirement declares both.')
|
|
670
|
+
return
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
logWarn(
|
|
674
|
+
`${plural(report.requirementSections.length, 'requirement')} short a declared section`,
|
|
675
|
+
)
|
|
676
|
+
reportFindings(report.requirementSections)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Stated on every run, including the run where everything above passed. A
|
|
681
|
+
* report that lists only what it measured reads as a verdict on the standard
|
|
682
|
+
* rather than on the half of it a parser can reach.
|
|
683
|
+
*/
|
|
684
|
+
function reportUnmeasured(): void {
|
|
685
|
+
logStep('Unmeasured')
|
|
686
|
+
logInfo(
|
|
687
|
+
'The standard states rules no parser reads, and a pass above says nothing about them.',
|
|
688
|
+
)
|
|
689
|
+
logInfo(
|
|
690
|
+
'Whether each Must traces to a stated gap, whether a gap reads as an observed failure rather than an intent, and whether a description routes.',
|
|
691
|
+
)
|
|
692
|
+
logInfo('The 5,000-word body ceiling is mechanical and still absent here.')
|
|
693
|
+
}
|
package/src/commands/context.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
logWarn,
|
|
32
32
|
outro,
|
|
33
33
|
pipeOutput,
|
|
34
|
+
plural,
|
|
34
35
|
} from '@/ui'
|
|
35
36
|
|
|
36
37
|
/** Returned when an unresolved citation is found, which is the gating check. */
|
|
@@ -253,10 +254,6 @@ function reportGate(report: ScannedCitations): void {
|
|
|
253
254
|
outro()
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
function plural(count: number, noun: string): string {
|
|
257
|
-
return `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
258
|
-
}
|
|
259
|
-
|
|
260
257
|
/**
|
|
261
258
|
* Names the resolved path of every audited folder, plus the requested names
|
|
262
259
|
* that resolved nowhere.
|
package/src/commands/tasks.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { relative } from 'node:path'
|
|
|
2
2
|
import { $ } from 'bun'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { type ArchiveOutcome, archiveTask } from '@/tasks/archive'
|
|
5
|
+
import {
|
|
6
|
+
type Finding,
|
|
7
|
+
type ValidateOutcome,
|
|
8
|
+
validateBoard,
|
|
9
|
+
} from '@/tasks/validate'
|
|
5
10
|
import {
|
|
6
11
|
intro,
|
|
7
12
|
logAdd,
|
|
@@ -9,16 +14,25 @@ import {
|
|
|
9
14
|
logInfo,
|
|
10
15
|
logRemove,
|
|
11
16
|
logStep,
|
|
17
|
+
logWarn,
|
|
12
18
|
outro,
|
|
13
19
|
pipeOutput,
|
|
14
20
|
} from '@/ui'
|
|
15
21
|
|
|
22
|
+
/** Returned when the board carries a finding, which is the gating result. */
|
|
23
|
+
const EXIT_FINDINGS = 2
|
|
24
|
+
|
|
16
25
|
interface ArchiveCommandOptions {
|
|
17
26
|
readonly json?: boolean
|
|
18
27
|
readonly pullRequest?: string
|
|
19
28
|
readonly root?: string
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
interface ValidateCommandOptions {
|
|
32
|
+
readonly json?: boolean
|
|
33
|
+
readonly root?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
/**
|
|
23
37
|
* The board is shared scratch at the main worktree root, and `git worktree
|
|
24
38
|
* list` puts that root first. A pull inside a linked worktree fires the same
|
|
@@ -71,6 +85,105 @@ export function register(program: Command): void {
|
|
|
71
85
|
.action(async (task: string | undefined, opts: ArchiveCommandOptions) => {
|
|
72
86
|
process.exitCode = await runArchive(task, opts)
|
|
73
87
|
})
|
|
88
|
+
|
|
89
|
+
tasks
|
|
90
|
+
.command('validate')
|
|
91
|
+
.description(
|
|
92
|
+
'Report what each board row claims against what the tree holds',
|
|
93
|
+
)
|
|
94
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
95
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
96
|
+
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
97
|
+
.addHelpText(
|
|
98
|
+
'after',
|
|
99
|
+
[
|
|
100
|
+
'',
|
|
101
|
+
'Checks:',
|
|
102
|
+
' every Run now row points at a plan file that resolves',
|
|
103
|
+
' every row maps to a task file and every task file to a row',
|
|
104
|
+
' no task carries more than one row',
|
|
105
|
+
' no two Run now rows touch the same file',
|
|
106
|
+
'',
|
|
107
|
+
'Exit codes:',
|
|
108
|
+
' 0 every check passed',
|
|
109
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
110
|
+
' 2 the board carries at least one finding',
|
|
111
|
+
'',
|
|
112
|
+
'It reports and never writes. A row is a claim about readiness, so a',
|
|
113
|
+
'session fixes the row the report names.',
|
|
114
|
+
'',
|
|
115
|
+
'Examples:',
|
|
116
|
+
' aitk tasks validate',
|
|
117
|
+
' aitk tasks validate --json',
|
|
118
|
+
'',
|
|
119
|
+
].join('\n'),
|
|
120
|
+
)
|
|
121
|
+
.action(async (opts: ValidateCommandOptions) => {
|
|
122
|
+
process.exitCode = await runValidate(opts)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runValidate(opts: ValidateCommandOptions): Promise<number> {
|
|
127
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
128
|
+
const outcome = await validateBoard(root)
|
|
129
|
+
|
|
130
|
+
return reportValidation(outcome, opts.json ?? false, root)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reportValidation(
|
|
134
|
+
outcome: ValidateOutcome,
|
|
135
|
+
emitJson: boolean,
|
|
136
|
+
root: string,
|
|
137
|
+
): number {
|
|
138
|
+
if (!outcome.ok) {
|
|
139
|
+
if (emitJson) {
|
|
140
|
+
process.stdout.write(
|
|
141
|
+
`${JSON.stringify({ ok: false, reason: outcome.reason, message: outcome.message })}\n`,
|
|
142
|
+
)
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
intro('aitk tasks validate')
|
|
147
|
+
logStep('Refused')
|
|
148
|
+
logError(outcome.message)
|
|
149
|
+
outro()
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!emitJson) {
|
|
154
|
+
intro('aitk tasks validate')
|
|
155
|
+
logStep('Board')
|
|
156
|
+
logInfo(
|
|
157
|
+
`${outcome.rows} row(s) across the readiness groups, ${outcome.tasks} task file(s)`,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
logStep(outcome.findings.length === 0 ? 'Clean' : 'Findings')
|
|
161
|
+
if (outcome.findings.length === 0) {
|
|
162
|
+
logInfo('every row resolves, maps one to one, and touches its own files')
|
|
163
|
+
} else {
|
|
164
|
+
for (const finding of outcome.findings) logWarn(describe(finding))
|
|
165
|
+
}
|
|
166
|
+
outro()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (emitJson) {
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
`${JSON.stringify({
|
|
172
|
+
ok: true,
|
|
173
|
+
root,
|
|
174
|
+
rows: outcome.rows,
|
|
175
|
+
tasks: outcome.tasks,
|
|
176
|
+
findings: outcome.findings,
|
|
177
|
+
})}\n`,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return outcome.findings.length > 0 ? EXIT_FINDINGS : 0
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function describe(finding: Finding): string {
|
|
185
|
+
const scope = finding.group ? `${finding.group}: ` : ''
|
|
186
|
+
return `${scope}${finding.subject} ${finding.message}`
|
|
74
187
|
}
|
|
75
188
|
|
|
76
189
|
async function runArchive(
|
package/src/tasks/archive.ts
CHANGED
|
@@ -8,10 +8,12 @@ const ARCHIVE_DIR = join('.claude', '.tmp', 'task-archive')
|
|
|
8
8
|
const PLANS_DIR = join('.claude', 'plans')
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Siblings that sit on the board without being tasks: the generated index, the
|
|
12
|
+
* hand-maintained ordering, and the pre-compaction session map. `validate`
|
|
13
|
+
* reads the same list, so neither verb can count a sibling as a task the other
|
|
14
|
+
* does not.
|
|
13
15
|
*/
|
|
14
|
-
const RESERVED_STEMS = ['index', 'priority'] as const
|
|
16
|
+
export const RESERVED_STEMS = ['index', 'priority', 'session'] as const
|
|
15
17
|
|
|
16
18
|
export const ARCHIVE_REFUSALS = [
|
|
17
19
|
'no-board',
|