@erclx/aitk 0.44.0 → 0.46.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 (38) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/claude-autoship/SKILL.md +15 -10
  3. package/claude/skills/claude-docs/SKILL.md +16 -2
  4. package/claude/skills/claude-memory-capture/REQUIREMENT.md +14 -4
  5. package/claude/skills/claude-memory-capture/SKILL.md +45 -16
  6. package/claude/skills/claude-memory-review/REQUIREMENT.md +8 -2
  7. package/claude/skills/claude-memory-review/SKILL.md +41 -25
  8. package/claude/skills/claude-orchestrate/SKILL.md +5 -0
  9. package/claude/skills/claude-seed-sync/REQUIREMENT.md +3 -1
  10. package/claude/skills/claude-seed-sync/SKILL.md +16 -4
  11. package/claude/skills/git-ship/SKILL.md +14 -11
  12. package/claude/skills/session-resume/SKILL.md +2 -2
  13. package/claude/skills/toolkit-operator/SKILL.md +18 -1
  14. package/docs/agents/commands.md +36 -35
  15. package/docs/agents/index.md +1 -0
  16. package/docs/agents/indexes.md +3 -1
  17. package/docs/agents/install-and-sync.md +46 -0
  18. package/docs/agents/scripting.md +4 -3
  19. package/docs/agents/skills-audit.md +55 -0
  20. package/docs/ai-workflow.md +3 -2
  21. package/docs/target-projects.md +5 -1
  22. package/package.json +1 -1
  23. package/scripts/core/verify.sh +8 -0
  24. package/src/claude/seeds.ts +7 -1
  25. package/src/claude/skills-audit.ts +215 -0
  26. package/src/claude/skills-list.ts +3 -3
  27. package/src/commands/claude.ts +283 -5
  28. package/src/commands/context.ts +1 -4
  29. package/src/commands/sync.ts +54 -1
  30. package/src/sync/check.ts +71 -0
  31. package/src/sync/layout.ts +139 -0
  32. package/src/sync/seeds-report.ts +111 -0
  33. package/src/ui.ts +5 -0
  34. package/tooling/claude/reference.md +11 -1
  35. package/tooling/claude/seeds/.claude/hooks/memory-index.sh +60 -0
  36. package/tooling/claude/seeds/.claude/memory/index.md +8 -0
  37. package/tooling/claude/seeds/.claude/settings.json +4 -0
  38. package/tooling/claude/seeds/CLAUDE.md +3 -0
@@ -17,9 +17,9 @@ export interface SkillListing {
17
17
  * The folder name wins over the frontmatter `name` when they disagree, because
18
18
  * Claude Code invokes a skill by its directory.
19
19
  *
20
- * `requirement` reports whether the folder carries `REQUIREMENT.md`. Coverage is
21
- * selective by design, so a false is not a gap to close and this field carries no
22
- * reason for one. The toolkit records those separately from the catalog.
20
+ * `requirement` reports whether the folder carries `REQUIREMENT.md`. Every skill
21
+ * is meant to carry one, so a false is a gap rather than a recorded exemption.
22
+ * `aitk claude skills audit` is what fails on it, across both corpora.
23
23
  */
24
24
  export function listSkills(root: string): SkillListing[] {
25
25
  const skillsRoot = join(root, 'claude', 'skills')
@@ -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]', "Only 'list' is supported")
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
+ }
@@ -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.
@@ -95,6 +95,16 @@ async function runCheck(target: string, options: SyncOptions): Promise<number> {
95
95
  function renderCheck(report: CheckReport): void {
96
96
  intro('aitk sync --check')
97
97
 
98
+ if (!report.managed) {
99
+ logStep('Not a toolkit project')
100
+ logWarn('No .claude/ directory and no CLAUDE.md at the target.')
101
+ logInfo(
102
+ 'Run `aitk init` to install, or /aitk:setup-init to resolve a stack.',
103
+ )
104
+ outro()
105
+ return
106
+ }
107
+
98
108
  for (const domain of report.domains) {
99
109
  logStep(domain.domain)
100
110
 
@@ -123,14 +133,33 @@ function renderCheck(report: CheckReport): void {
123
133
  }
124
134
  }
125
135
 
136
+ for (const entry of report.unmigrated) {
137
+ logStep(`${entry.domain} (not migrated)`)
138
+ logWarn(
139
+ `${entry.files} files at ${entry.rootPath}/, nothing at ${entry.installPath}/`,
140
+ )
141
+ logInfo('Run /aitk:migration-standards to relocate them.')
142
+ }
143
+
144
+ renderSeeds(report)
145
+
146
+ if (report.superseded.length > 0) {
147
+ logStep('Superseded by a newer layout')
148
+ for (const entry of report.superseded) {
149
+ logWarn(`${entry.rel} (replaced by ${entry.replacedBy}/)`)
150
+ }
151
+ logInfo('Move the content yourself. No sync command touches these.')
152
+ }
153
+
126
154
  if (report.newSkills.length > 0) {
127
155
  logStep('New skills, no sync needed')
128
156
  for (const name of report.newSkills) logInfo(name)
129
157
  }
130
158
 
131
159
  outro()
160
+ const unmigrated = report.unmigrated.map((entry) => entry.domain)
132
161
  const uncovered = STAMP_DOMAINS.filter(
133
- (domain) => !report.covers.includes(domain),
162
+ (domain) => !report.covers.includes(domain) && !unmigrated.includes(domain),
134
163
  )
135
164
  const unstamped =
136
165
  uncovered.length === 0 ? '' : `Unstamped: ${uncovered.join(', ')}. `
@@ -139,6 +168,30 @@ function renderCheck(report: CheckReport): void {
139
168
  )
140
169
  }
141
170
 
171
+ /**
172
+ * Seeds print their own section because no sync command applies them. A `stale`
173
+ * seed is safe to take whole and a `drifted` one holds edits, which is the split
174
+ * `claude-seed-sync` reads to decide what needs a section-level merge.
175
+ */
176
+ function renderSeeds(report: CheckReport): void {
177
+ const notable = report.seeds.entries.filter(
178
+ (entry) => entry.state !== 'matching',
179
+ )
180
+
181
+ if (notable.length === 0) return
182
+
183
+ logStep('seeds')
184
+ if (report.seeds.historyUnavailable) {
185
+ logWarn('This toolkit has no git history. Drift below is unattributed.')
186
+ }
187
+
188
+ for (const entry of notable) {
189
+ logWarn(`${entry.rel} (${entry.state})`)
190
+ }
191
+
192
+ logInfo('Run /aitk:claude-seed-sync to reconcile these section by section.')
193
+ }
194
+
142
195
  async function runSync(target: string): Promise<number> {
143
196
  intro('aitk sync')
144
197
 
package/src/sync/check.ts CHANGED
@@ -1,8 +1,16 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { join } from 'node:path'
2
3
  import { execa } from 'execa'
3
4
  import { createGovAdapter } from '@/gov/adapter'
4
5
  import { createSnippetsAdapter } from '@/snippets/adapter'
5
6
  import { planSync, type ScanEntry, type SyncAdapter } from '@/sync/engine'
7
+ import {
8
+ collectSuperseded,
9
+ detectUnmigrated,
10
+ type SupersededEntry,
11
+ type UnmigratedDomain,
12
+ } from '@/sync/layout'
13
+ import { buildSeedsReport, type SeedsReport } from '@/sync/seeds-report'
6
14
  import {
7
15
  readStamp,
8
16
  STAMP_DOMAINS,
@@ -68,7 +76,16 @@ export interface UpstreamCommit {
68
76
 
69
77
  export interface CheckReport {
70
78
  readonly covers: readonly StampDomain[]
79
+ /** False when the target is not a toolkit project, so every section stays empty. */
80
+ readonly managed: boolean
71
81
  readonly domains: readonly DomainReport[]
82
+ /**
83
+ * Reported beside the domains rather than as one of them, because seeds carry
84
+ * no stamp and produce no change. See `@/sync/seeds-report`.
85
+ */
86
+ readonly seeds: SeedsReport
87
+ readonly superseded: readonly SupersededEntry[]
88
+ readonly unmigrated: readonly UnmigratedDomain[]
72
89
  readonly newSkills: readonly string[]
73
90
  }
74
91
 
@@ -78,6 +95,33 @@ export function installedStampDomains(target: string): StampDomain[] {
78
95
  )
79
96
  }
80
97
 
98
+ /**
99
+ * Whether the target is a toolkit-managed project at all. Seeds are enumerated
100
+ * from the source rather than from what a target installed, so without this gate
101
+ * a directory the toolkit has never touched reports every seed as `missing` and
102
+ * routes to a skill that reconciles section by section. `installedStampDomains`
103
+ * gates the three scanned domains the same way, which is why they stay quiet on
104
+ * the same directory.
105
+ *
106
+ * An unmigrated domain counts as a marker in its own right. `detectUnmigrated`
107
+ * fires only on root files whose basename the toolkit ships, so it firing proves
108
+ * the toolkit installed here before the layout moved under `.claude/`. Reading
109
+ * only the markers would report such a target as unmanaged while the same report
110
+ * carried its unmigrated domain, and a consumer reading the JSON would route to
111
+ * the relocation while the rendered half routed to install.
112
+ */
113
+ export function isManagedTarget(
114
+ target: string,
115
+ unmigrated: readonly UnmigratedDomain[],
116
+ ): boolean {
117
+ if (unmigrated.length > 0) return true
118
+
119
+ return (
120
+ isDirectory(join(target, '.claude')) ||
121
+ existsSync(join(target, 'CLAUDE.md'))
122
+ )
123
+ }
124
+
81
125
  export function countStates(entries: readonly ScanEntry[]): StateCounts {
82
126
  return {
83
127
  matching: count(entries, 'matching'),
@@ -93,8 +137,16 @@ export function countStates(entries: readonly ScanEntry[]): StateCounts {
93
137
  * Whether the target has diverged from the toolkit in a way a sync could close.
94
138
  * Orphaned files are excluded: a project-authored rule never converges, and
95
139
  * counting it would leave `--exit-code` failing forever with no remedy.
140
+ *
141
+ * An unmigrated domain counts, because running the relocation closes it. A
142
+ * superseded artifact does not, for the same reason orphaned files do not: only
143
+ * the user can move content they wrote, so failing a job on it leaves the job
144
+ * red with no mechanical remedy. Seeds are excluded on the same grounds, since
145
+ * every seed a project edits would otherwise fail the check forever.
96
146
  */
97
147
  export function hasDrift(report: CheckReport): boolean {
148
+ if (report.unmigrated.length > 0) return true
149
+
98
150
  return report.domains.some(
99
151
  (domain) =>
100
152
  domain.counts.stale +
@@ -126,9 +178,28 @@ export async function buildCheckReport(
126
178
  .map((domain) => domain.commit)
127
179
  .filter((commit): commit is string => commit !== undefined)
128
180
 
181
+ const unmigrated = detectUnmigrated(toolkitRoot, target)
182
+ const managed = isManagedTarget(target, unmigrated)
183
+
184
+ if (!managed) {
185
+ return {
186
+ covers: [],
187
+ managed,
188
+ domains: [],
189
+ seeds: { entries: [], historyUnavailable: false },
190
+ superseded: [],
191
+ unmigrated: [],
192
+ newSkills: [],
193
+ }
194
+ }
195
+
129
196
  return {
130
197
  covers: stamp?.covers ?? [],
198
+ managed,
131
199
  domains,
200
+ seeds: buildSeedsReport(toolkitRoot, target),
201
+ superseded: collectSuperseded(target),
202
+ unmigrated,
132
203
  newSkills: await readNewSkills(toolkitRoot, anchors),
133
204
  }
134
205
  }