@iceinvein/agent-skills 0.1.39 → 0.2.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 (148) hide show
  1. package/README.md +18 -2
  2. package/dist/cli/index.js +105 -28
  3. package/package.json +1 -1
  4. package/skills/index.json +14 -2
  5. package/skills/magpie/README.md +2 -1
  6. package/skills/magpie/SKILL.md +138 -45
  7. package/skills/magpie/bin/magpie.ts +43 -0
  8. package/skills/magpie/fixtures/example-pr/brief.json +18 -0
  9. package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
  10. package/skills/magpie/fixtures/fake-gh.sh +14 -0
  11. package/skills/magpie/package.json +1 -1
  12. package/skills/magpie/references/peer-review.md +7 -2
  13. package/skills/magpie/references/scout.md +90 -0
  14. package/skills/magpie/references/specialists.md +117 -6
  15. package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
  16. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
  17. package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
  18. package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
  19. package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
  20. package/skills/magpie/scripts/__tests__/gh.test.ts +90 -0
  21. package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
  22. package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
  23. package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
  24. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
  25. package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
  26. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +173 -1
  27. package/skills/magpie/scripts/__tests__/render-findings.test.ts +151 -1
  28. package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
  29. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
  30. package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
  31. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +207 -7
  32. package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
  33. package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
  34. package/skills/magpie/scripts/diff-chunks.ts +28 -0
  35. package/skills/magpie/scripts/findings-files.ts +32 -0
  36. package/skills/magpie/scripts/gh.ts +68 -14
  37. package/skills/magpie/scripts/git-diff.ts +111 -0
  38. package/skills/magpie/scripts/path-filter.ts +9 -5
  39. package/skills/magpie/scripts/preview-cmd.ts +11 -1
  40. package/skills/magpie/scripts/refresh.ts +32 -3
  41. package/skills/magpie/scripts/render-cmd.ts +35 -11
  42. package/skills/magpie/scripts/render-findings.ts +78 -2
  43. package/skills/magpie/scripts/render-progress.ts +6 -1
  44. package/skills/magpie/scripts/setup-cmd.ts +38 -1
  45. package/skills/magpie/scripts/shard.ts +171 -0
  46. package/skills/magpie/scripts/status-cmd.ts +7 -4
  47. package/skills/magpie/scripts/types.ts +50 -0
  48. package/skills/magpie/skill.json +2 -2
  49. package/skills/magpie/templates/styles.css +66 -0
  50. package/skills/migrate/README.md +194 -0
  51. package/skills/migrate/SKILL.md +197 -0
  52. package/skills/migrate/bin/migrate +15 -0
  53. package/skills/migrate/bin/migrate.ts +309 -0
  54. package/skills/migrate/biome.json +35 -0
  55. package/skills/migrate/bun.lock +24 -0
  56. package/skills/migrate/docs/architecture.md +294 -0
  57. package/skills/migrate/docs/reference.md +590 -0
  58. package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
  59. package/skills/migrate/fixtures/tiny-express/app.js +29 -0
  60. package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
  61. package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
  62. package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
  63. package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
  64. package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
  65. package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
  66. package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
  67. package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
  68. package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
  69. package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
  70. package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
  71. package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
  72. package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
  73. package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
  74. package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
  75. package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
  76. package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
  77. package/skills/migrate/install.sh +68 -0
  78. package/skills/migrate/package.json +17 -0
  79. package/skills/migrate/references/phases/enumerate.md +291 -0
  80. package/skills/migrate/references/phases/extract.md +652 -0
  81. package/skills/migrate/references/phases/parity.md +275 -0
  82. package/skills/migrate/references/phases/probe.md +135 -0
  83. package/skills/migrate/references/phases/queue.md +242 -0
  84. package/skills/migrate/references/phases/seam.md +416 -0
  85. package/skills/migrate/references/recipes/README.md +116 -0
  86. package/skills/migrate/references/recipes/aspnet.md +287 -0
  87. package/skills/migrate/references/run-ops.md +280 -0
  88. package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
  89. package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
  90. package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
  91. package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
  92. package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
  93. package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
  94. package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
  95. package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
  96. package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
  97. package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
  98. package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
  99. package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
  100. package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
  101. package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
  102. package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
  103. package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
  104. package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
  105. package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
  106. package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
  107. package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
  108. package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
  109. package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
  110. package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
  111. package/skills/migrate/scripts/census-cmd.ts +109 -0
  112. package/skills/migrate/scripts/census.ts +342 -0
  113. package/skills/migrate/scripts/check-cmd.ts +24 -0
  114. package/skills/migrate/scripts/check.ts +376 -0
  115. package/skills/migrate/scripts/citations.ts +92 -0
  116. package/skills/migrate/scripts/config.ts +237 -0
  117. package/skills/migrate/scripts/ids.ts +31 -0
  118. package/skills/migrate/scripts/import-cmd.ts +141 -0
  119. package/skills/migrate/scripts/init-cmd.ts +118 -0
  120. package/skills/migrate/scripts/leaks.ts +184 -0
  121. package/skills/migrate/scripts/lock.ts +188 -0
  122. package/skills/migrate/scripts/paths.ts +103 -0
  123. package/skills/migrate/scripts/phase-cmd.ts +63 -0
  124. package/skills/migrate/scripts/phases.ts +113 -0
  125. package/skills/migrate/scripts/queue-cmd.ts +98 -0
  126. package/skills/migrate/scripts/queue.ts +258 -0
  127. package/skills/migrate/scripts/report-cmd.ts +47 -0
  128. package/skills/migrate/scripts/report.ts +131 -0
  129. package/skills/migrate/scripts/reset-cmd.ts +120 -0
  130. package/skills/migrate/scripts/status-cmd.ts +52 -0
  131. package/skills/migrate/scripts/store.ts +159 -0
  132. package/skills/migrate/scripts/types.ts +137 -0
  133. package/skills/migrate/scripts/validate.ts +221 -0
  134. package/skills/migrate/skill.json +33 -0
  135. package/skills/migrate/templates/config.toml +27 -0
  136. package/skills/migrate/templates/queue-item.md +17 -0
  137. package/skills/migrate/tsconfig.json +18 -0
  138. package/skills/migrate/uninstall.sh +31 -0
  139. package/skills/sluice/SKILL.md +82 -0
  140. package/skills/sluice/references/deep-channel.md +94 -0
  141. package/skills/sluice/references/finish.md +35 -0
  142. package/skills/sluice/references/intent.md +29 -0
  143. package/skills/sluice/references/review.md +42 -0
  144. package/skills/sluice/references/root-cause.md +38 -0
  145. package/skills/sluice/references/show-or-say.md +36 -0
  146. package/skills/sluice/references/test-first.md +35 -0
  147. package/skills/sluice/references/verify.md +26 -0
  148. package/skills/sluice/skill.json +32 -0
@@ -0,0 +1,171 @@
1
+ import { mkdir, readdir, rm, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { type FileChunk, splitFileChunks } from './diff-chunks.ts'
4
+
5
+ export const DEFAULT_SHARD_BUDGET = 6000
6
+ export const DEFAULT_SHARD_MAX_FILES = 80
7
+
8
+ export type ShardEntry = { id: number; path: string; files: string[]; lines: number }
9
+ export type ShardManifest = {
10
+ budget: number
11
+ maxFiles: number
12
+ totalFiles: number
13
+ totalLines: number
14
+ shards: ShardEntry[]
15
+ }
16
+
17
+ /**
18
+ * The directory a file is grouped under, capped at two segments. Cohesion is
19
+ * not cosmetic: the architecture and code-smells focuses look for duplication,
20
+ * boundary violations, and shotgun surgery, and splitting a module across
21
+ * shards hides exactly those.
22
+ */
23
+ export function groupKey(path: string): string {
24
+ const parts = path.split('/')
25
+ if (parts.length <= 1) return '.'
26
+ return parts.slice(0, Math.min(2, parts.length - 1)).join('/')
27
+ }
28
+
29
+ export function planShards(chunks: FileChunk[], budget: number, maxFiles: number): FileChunk[][] {
30
+ const groups = new Map<string, FileChunk[]>()
31
+ for (const c of chunks) {
32
+ const key = groupKey(c.path)
33
+ const existing = groups.get(key)
34
+ if (existing) existing.push(c)
35
+ else groups.set(key, [c])
36
+ }
37
+
38
+ const shards: FileChunk[][] = []
39
+ let current: FileChunk[] = []
40
+ let currentLines = 0
41
+ const flush = () => {
42
+ if (current.length > 0) {
43
+ shards.push(current)
44
+ current = []
45
+ currentLines = 0
46
+ }
47
+ }
48
+
49
+ for (const members of groups.values()) {
50
+ const groupLines = members.reduce((n, c) => n + c.lines, 0)
51
+ // Start a fresh shard rather than straddle a group across a boundary, when
52
+ // the whole group could fit on its own.
53
+ if (
54
+ current.length > 0 &&
55
+ (currentLines + groupLines > budget || current.length + members.length > maxFiles)
56
+ ) {
57
+ flush()
58
+ }
59
+ for (const c of members) {
60
+ if (
61
+ current.length > 0 &&
62
+ (currentLines + c.lines > budget || current.length + 1 > maxFiles)
63
+ ) {
64
+ flush()
65
+ }
66
+ current.push(c)
67
+ currentLines += c.lines
68
+ }
69
+ }
70
+ flush()
71
+ return shards
72
+ }
73
+
74
+ /** Read `$RUN_DIR/diff.patch`, tolerating only its absence. A missing
75
+ * diff.patch means "nothing to shard"; anything else (permission error,
76
+ * disk error, EISDIR, ...) is a genuine failure and must propagate rather
77
+ * than silently turn into an empty manifest. */
78
+ async function readDiffPatch(runDir: string): Promise<string> {
79
+ try {
80
+ return await Bun.file(join(runDir, 'diff.patch')).text()
81
+ } catch (err) {
82
+ const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined
83
+ if (code === 'ENOENT') return ''
84
+ throw err
85
+ }
86
+ }
87
+
88
+ /** Delete `shard-<n>.patch` files left by an earlier split. Re-splitting 7 shards
89
+ * into 4 would otherwise leave `shard-5.patch` .. `shard-7.patch` on disk: the
90
+ * manifest is authoritative for dispatch, but a stale patch makes the directory
91
+ * claim coverage the manifest does not have, and a resume that reads shard ids
92
+ * off disk would review a file set that no longer matches its id. Only the shard
93
+ * patches are touched: `manifest.json` stays in place until it is overwritten at
94
+ * the end (so there is never a window with no manifest), and `diff.patch` lives
95
+ * outside this directory and is what a single-shard manifest points at. */
96
+ async function clearStaleShardPatches(shardsDir: string): Promise<void> {
97
+ let entries: string[]
98
+ try {
99
+ entries = await readdir(shardsDir)
100
+ } catch {
101
+ return
102
+ }
103
+ for (const name of entries) {
104
+ if (/^shard-\d+\.patch$/.test(name)) await rm(join(shardsDir, name), { force: true })
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Split `$RUN_DIR/diff.patch` into budgeted shards. `diff.patch` is never
110
+ * modified: shards are views over it, which is what keeps the critic's diff
111
+ * excerpts, dedupe's evidence check, and the report's rendering working
112
+ * unchanged.
113
+ *
114
+ * Re-splitting an already-sharded run invalidates any
115
+ * `findings/<focus>.shard-<n>.json` from the previous split: the same shard id
116
+ * now names a different file set. Stage 4's resume rule keys on those files, so
117
+ * the caller must delete them before re-dispatching.
118
+ */
119
+ export async function shardDiff(
120
+ runDir: string,
121
+ opts: { budget?: number; maxFiles?: number } = {},
122
+ ): Promise<ShardManifest> {
123
+ const budget = opts.budget ?? DEFAULT_SHARD_BUDGET
124
+ const maxFiles = opts.maxFiles ?? DEFAULT_SHARD_MAX_FILES
125
+ const diff = await readDiffPatch(runDir)
126
+ const chunks = splitFileChunks(diff)
127
+ const totalLines = chunks.reduce((n, c) => n + c.lines, 0)
128
+ const shardsDir = join(runDir, 'shards')
129
+ await clearStaleShardPatches(shardsDir)
130
+ await mkdir(shardsDir, { recursive: true })
131
+
132
+ const planned = planShards(chunks, budget, maxFiles)
133
+ let shards: ShardEntry[]
134
+ if (planned.length === 0) {
135
+ shards = []
136
+ } else if (planned.length === 1) {
137
+ // Single-shard passthrough: stage 4 takes the same path it took on 0.9.0.
138
+ const only = planned[0] ?? []
139
+ shards = [
140
+ {
141
+ id: 1,
142
+ path: 'diff.patch',
143
+ files: only.map((c) => c.path),
144
+ lines: only.reduce((n, c) => n + c.lines, 0),
145
+ },
146
+ ]
147
+ } else {
148
+ shards = []
149
+ for (const [i, members] of planned.entries()) {
150
+ const id = i + 1
151
+ const rel = `shards/shard-${id}.patch`
152
+ await writeFile(join(runDir, rel), members.map((c) => c.text).join(''))
153
+ shards.push({
154
+ id,
155
+ path: rel,
156
+ files: members.map((c) => c.path),
157
+ lines: members.reduce((n, c) => n + c.lines, 0),
158
+ })
159
+ }
160
+ }
161
+
162
+ const manifest: ShardManifest = {
163
+ budget,
164
+ maxFiles,
165
+ totalFiles: chunks.length,
166
+ totalLines,
167
+ shards,
168
+ }
169
+ await writeFile(join(shardsDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
170
+ return manifest
171
+ }
@@ -1,7 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
 
4
- const ORDER = [
4
+ /** The resume ladder. Exported so `skill-lint` can assert that the diagnostic
5
+ * stage names SKILL.md logs (`filter`, `tests-check`, `shard-coverage`, ...) stay
6
+ * out of it: a diagnostic entry that lands on this list would move `next`. */
7
+ export const ORDER = [
5
8
  'setup',
6
9
  'context',
7
10
  'specialists',
@@ -15,9 +18,9 @@ const ORDER = [
15
18
  export type StatusResult = {
16
19
  /**
17
20
  * Highest stage the log says is behind us. A stage logged `skipped` counts:
18
- * `context` has no work in the pipeline and is always logged that way, so
19
- * treating it as unfinished would send a resume back to a stage that has no
20
- * step to run.
21
+ * `context` logs `skipped` when the scout produced no brief, and the pipeline
22
+ * has nothing to go back for, so treating it as unfinished would send a resume
23
+ * to a stage with no remaining work.
21
24
  */
22
25
  lastCompleted: (typeof ORDER)[number] | null
23
26
  next: (typeof ORDER)[number] | 'cleanup'
@@ -332,3 +332,53 @@ export type PrFileEntry = {
332
332
  export function isSuggestion(f: ReviewFinding): boolean {
333
333
  return f.risk.action === 'consider' || f.risk.action === 'optional'
334
334
  }
335
+
336
+ export type BriefSubsystem = {
337
+ name: string
338
+ role: string
339
+ }
340
+
341
+ /** Scout-produced PR summary. Written to `$RUN_DIR/brief.json` by the context stage. */
342
+ export type PrBrief = {
343
+ purpose: string
344
+ changes: string[]
345
+ subsystems: BriefSubsystem[]
346
+ watchItems: string[]
347
+ unclear: string[]
348
+ }
349
+
350
+ function briefStrings(raw: unknown): string[] {
351
+ if (!Array.isArray(raw)) return []
352
+ return raw
353
+ .filter((v): v is string => typeof v === 'string')
354
+ .map((v) => v.trim())
355
+ .filter((v) => v.length > 0)
356
+ }
357
+
358
+ /**
359
+ * Lenient by design, unlike `parseFinding`. A subagent-authored brief that came
360
+ * back malformed must degrade the report header to absent, not throw away an
361
+ * otherwise-complete review at render time.
362
+ */
363
+ export function parseBrief(raw: unknown): PrBrief | null {
364
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
365
+ const r = raw as Record<string, unknown>
366
+ const purpose = typeof r.purpose === 'string' ? r.purpose.trim() : ''
367
+ if (purpose.length === 0) return null
368
+ const subsystems: BriefSubsystem[] = Array.isArray(r.subsystems)
369
+ ? (r.subsystems as unknown[]).flatMap((entry) => {
370
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return []
371
+ const e = entry as Record<string, unknown>
372
+ const name = typeof e.name === 'string' ? e.name.trim() : ''
373
+ if (name.length === 0) return []
374
+ return [{ name, role: typeof e.role === 'string' ? e.role.trim() : '' }]
375
+ })
376
+ : []
377
+ return {
378
+ purpose,
379
+ changes: briefStrings(r.changes),
380
+ subsystems,
381
+ watchItems: briefStrings(r.watchItems),
382
+ unclear: briefStrings(r.unclear),
383
+ }
384
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "magpie",
3
- "version": "0.8.0",
4
- "description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec (falling back to a Claude second opinion when codex is unavailable), and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request.",
3
+ "version": "0.10.0",
4
+ "description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec (falling back to a Claude second opinion when codex is unavailable), and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request. Splits oversized diffs into budgeted shards and rebuilds the diff from the local clone when gh pr diff refuses it.",
5
5
  "author": "iceinvein",
6
6
  "type": "prompt",
7
7
  "tools": [
@@ -183,6 +183,11 @@ button {
183
183
  color: var(--ink);
184
184
  }
185
185
 
186
+ .diff-source {
187
+ font-size: 0.78rem;
188
+ opacity: 0.7;
189
+ }
190
+
186
191
  .header-spacer {
187
192
  flex: 1;
188
193
  }
@@ -1638,3 +1643,64 @@ code.inline-code {
1638
1643
  border-color: var(--line);
1639
1644
  }
1640
1645
  }
1646
+
1647
+ /* ---- PR brief header ---- */
1648
+ .pr-brief {
1649
+ margin: 0;
1650
+ padding: 10px 16px 12px;
1651
+ background: var(--card);
1652
+ border-bottom: 1px solid var(--line);
1653
+ color: var(--ink);
1654
+ }
1655
+
1656
+ .brief-summary {
1657
+ cursor: pointer;
1658
+ font-size: 12px;
1659
+ font-weight: 600;
1660
+ letter-spacing: 0.02em;
1661
+ text-transform: uppercase;
1662
+ color: var(--ink-muted);
1663
+ }
1664
+
1665
+ .brief-body {
1666
+ padding-top: 8px;
1667
+ max-width: 92ch;
1668
+ }
1669
+
1670
+ .brief-purpose {
1671
+ margin: 0;
1672
+ font-size: 14px;
1673
+ line-height: 1.5;
1674
+ }
1675
+
1676
+ .brief-changes {
1677
+ margin: 8px 0 0;
1678
+ padding-left: 18px;
1679
+ font-size: 13px;
1680
+ line-height: 1.5;
1681
+ color: var(--ink-muted);
1682
+ }
1683
+
1684
+ .brief-subsystems,
1685
+ .brief-issues {
1686
+ display: flex;
1687
+ flex-wrap: wrap;
1688
+ gap: 6px;
1689
+ margin-top: 10px;
1690
+ }
1691
+
1692
+ .brief-chip,
1693
+ .brief-issue {
1694
+ padding: 2px 8px;
1695
+ border: 1px solid var(--line);
1696
+ border-radius: var(--radius-sm);
1697
+ font-family: var(--font-mono);
1698
+ font-size: 11px;
1699
+ color: var(--ink-muted);
1700
+ }
1701
+
1702
+ .brief-issue {
1703
+ border-color: var(--accent);
1704
+ color: var(--accent);
1705
+ text-decoration: none;
1706
+ }
@@ -0,0 +1,194 @@
1
+ # migrate
2
+
3
+ Source-agnostic legacy migration mapping. `migrate` walks a legacy codebase
4
+ through probe, enumerate, seam, extract, parity, and queue, building an
5
+ auditable requirements ledger with mandatory citations instead of a
6
+ self-reported one. It enumerates the legacy surface from two independent
7
+ directions per lens, derives a capability seam empirically rather than by
8
+ guesswork, extracts cited functional requirements, plans parity against the
9
+ source, and routes every ambiguity to a batch decision queue for a human to
10
+ adjudicate. The coverage arithmetic, citation resolution, and phase ordering
11
+ are enforced by a bundled Bun CLI instead of being self-reported. Two more
12
+ phases, adjudicate and handoff, complete the walkthrough but ship no CLI verb
13
+ yet; see the phases table below.
14
+
15
+ ## Using it
16
+
17
+ Invoke `/migrate` in a target repo that is a git working copy, pointed at a
18
+ read-only checkout of the legacy source. The skill (`SKILL.md`) is the
19
+ walkthrough: it names, phase by phase, what to read, what to dispatch, and
20
+ what `migrate` command closes that phase out. Each phase's manual under
21
+ `references/phases/` carries the actual judgment calls, loaded only when
22
+ that phase is current so a run never pays for prose it does not need yet.
23
+
24
+ ### The phases
25
+
26
+ | # | Phase | Manual | What it produces |
27
+ |---|---|---|---|
28
+ | 0 | Probe | `references/phases/probe.md` | `.migrate/config.toml` (detected source stack, basis, target profile) and `.migrate/parity-basis.md` (the detection evidence, hand-written) |
29
+ | 1 | Enumerate | `references/phases/enumerate.md` | `elements.jsonl` and a lens census record per surface |
30
+ | 2 | Seam | `references/phases/seam.md` | `capabilities.jsonl`, `seam.json`, `seam.md`: the capability partition and its evidence |
31
+ | 3 | Extract | `references/phases/extract.md` | `requirements.jsonl`, attribute/rule-sweep/closer census records, terminal element dispositions |
32
+ | 4 | Parity | `references/phases/parity.md` | `deltas.jsonl` and a parity plan on every non-queued requirement |
33
+ | 5 | Queue | `references/phases/queue.md` | Queue items carrying evidence, options and a recommendation for anything ambiguous |
34
+ | 6 | Adjudicate | none yet | No verb ships in this version; `migrate status` and `migrate queue list` are the terminus |
35
+ | 7 | Handoff | none yet | Same as adjudicate: no verb yet |
36
+
37
+ A run in this version stops at the queue. `adjudicate` and `handoff` have no
38
+ CLI verbs to complete them, so `migrate check --phase queue` is the
39
+ practical terminus: its exit 0 is what "done, for now" means. Plain `migrate
40
+ check` gates every phase through `handoff` and cannot pass yet for the same
41
+ reason. `references/run-ops.md` covers what applies across every phase
42
+ rather than any one of them: subagent dispatch, the batch-checkpoint
43
+ discipline, and what happens when two agents contend for the store lock.
44
+
45
+ ### Recipes
46
+
47
+ Enumerate reads the source stack `probe` detected and looks for a matching
48
+ file in `references/recipes/`, one file per stack family
49
+ (`references/recipes/aspnet.md` covers `aspnet-webforms`, `aspnet-mvc`, and
50
+ `aspnet-webapi`). A recipe answers one narrow question: for each declared
51
+ surface type, at least two independent directions for enumerating it and the
52
+ probe command that realises each one. Nothing else; the lens contract itself
53
+ lives once in `enumerate.md`, and a recipe does not restate it, carry
54
+ classification rules, or gate anything.
55
+
56
+ If no file matches the detected stack, that is contract-only mode: a
57
+ supported path, not a degraded one. The enumerating agent derives its own two
58
+ directions per surface, and the census gates them exactly as it would a
59
+ recipe's.
60
+
61
+ **Adding a stack is one new file in `references/recipes/` and no edit
62
+ anywhere else.** `SKILL.md`, the phase manuals, and the CLI never name an
63
+ individual stack; they only read `[source].stack` and look in that
64
+ directory. See `references/recipes/README.md` for the exact file shape.
65
+
66
+ ## Checking as you go
67
+
68
+ ```
69
+ migrate check --phase <current-phase>
70
+ ```
71
+
72
+ bounds the run-state gate at that phase; the other nine gates always read
73
+ the whole store, so a coverage or census gap past your current phase still
74
+ fails on its own gate regardless of `--phase`. Citations are checked by
75
+ default; pass `--no-citations` to skip that gate.
76
+
77
+ ## Fixtures
78
+
79
+ Two fixtures, each with a committed `GROUND-TRUTH.md`, drive the skill end to
80
+ end:
81
+
82
+ - **`fixtures/tiny-express/`**: a small Express/Node app, twelve elements
83
+ across all eight default surfaces. Its stack (`express`) matches no file
84
+ in `references/recipes/`, so it proves the contract-only path: enumerate
85
+ deriving its own two directions per surface with no recipe to lean on, and
86
+ the census gating them exactly the same as it would a recipe's.
87
+ - **`fixtures/tiny-webforms/`**: a small ASP.NET Web Forms app, sixteen
88
+ elements across the same eight surfaces. Its stack (`aspnet-webforms`)
89
+ matches `references/recipes/aspnet.md`, so it is that recipe's first run
90
+ against committed code, not just the throwaway trees it was written
91
+ against.
92
+
93
+ `scripts/__tests__/e2e-express.test.ts` and
94
+ `scripts/__tests__/e2e-webforms.test.ts` copy the respective fixture to a
95
+ temp directory and drive the real CLI as a subprocess, probe through queue,
96
+ through `init`, `import`, `census`, `phase`, `queue add`, `queue list`, and
97
+ `check`, reconciling every row against the fixture's ground truth. Both end at
98
+ `migrate check --phase queue` on exit 0, and then show plain `migrate check`
99
+ failing on exactly `adjudicate` and `handoff`, the two phases with no verb in
100
+ this version.
101
+
102
+ Both show gates in both directions. Failing before they pass: the mid-run check
103
+ after enumerate names the three closer records extract has not written yet, and
104
+ the `deltas` gate names the sanctioned difference each run files unsigned
105
+ before an owner signs it. Failing after they pass: each run closes green, then
106
+ mutates the store (nulling every parity plan, then removing an element row) and
107
+ asserts the gate that should catch it does.
108
+
109
+ ## Documentation
110
+
111
+ - **[docs/reference.md](docs/reference.md)** is what you need to drive the CLI:
112
+ the batch-file and census formats with worked examples, the row schemas and
113
+ their grammars, what each of the ten gates enforces, and the exit-code
114
+ convention. Ships with the installed skill.
115
+ - **[docs/architecture.md](docs/architecture.md)** is for working on the skill
116
+ itself: the module map, the rule that decides what belongs in the CLI rather
117
+ than the prompt, how to add a gate or a surface type, the testing
118
+ conventions, and the known limits.
119
+
120
+ ## Store layout
121
+
122
+ The store lives at `.migrate/` in the target repo and is committed.
123
+
124
+ | Path | Shape | Holds |
125
+ |---|---|---|
126
+ | `.migrate/config.toml` | declarative | source pointer and scope, detected source stack, target profile, surface-type set, closer set, handoff adapter |
127
+ | `.migrate/elements.jsonl` | rows | surface ledger |
128
+ | `.migrate/requirements.jsonl` | rows | functional requirements and their dispositions |
129
+ | `.migrate/capabilities.jsonl` | rows | the seam partition |
130
+ | `.migrate/seam.json` | object | run-level seam metadata: validators run, modularity, status |
131
+ | `.migrate/deltas.jsonl` | rows | sanctioned delta catalog |
132
+ | `.migrate/census.jsonl` | rows | one accounting record per lens run |
133
+ | `.migrate/phases.json` | object | per-phase status, batches, resume pointers |
134
+ | `.migrate/seam.md` | prose | validator scripts and their raw output |
135
+ | `.migrate/parity-basis.md` | prose | runnable-versus-source-only detection evidence |
136
+ | `.migrate/queue/q-<slug>.md` | prose | evidence, options, recommendation |
137
+ | `.migrate/.env` | secrets | runtime-lens credentials, gitignored |
138
+ | `docs/migrate/*.md` | generated | human-readable views, written by `migrate report` |
139
+
140
+ ## CLI
141
+
142
+ | Command | Does |
143
+ |---|---|
144
+ | `migrate init --source <path> --scope <text> --name <target>` | Writes `.migrate/config.toml` |
145
+ | `migrate import <elements\|reqs\|deltas> <batch.json>` | Validated bulk append to the store |
146
+ | `migrate census <record.json>` | Records a lens accounting record |
147
+ | `migrate phase [<name>] [--status <s>]` | Prints phase state, or sets one phase's status to any value you name |
148
+ | `migrate queue add <file.md>` | Adds a queue item |
149
+ | `migrate queue list [--open]` | Lists queue items, severity first |
150
+ | `migrate queue show <id>` | Prints one queue item |
151
+ | `migrate check [--phase <p>] [--no-citations] [--leaks]` | Runs the gates |
152
+ | `migrate status` | Phase state, counts, resume pointer |
153
+ | `migrate reset --phase <phase>` | Clears one phase's derived rows and returns it to `pending` |
154
+ | `migrate report [--out <dir>]` | Renders markdown views |
155
+
156
+ Run `migrate --help` for the same list from the CLI itself.
157
+
158
+ **Four commands write a phase's status, each to a different extent.** `phase
159
+ --status <s>` is the only one that writes any value you ask for, and the only
160
+ one whose whole purpose is that write. `reset --phase <p>` also writes it
161
+ directly, but only ever to `pending`, and it empties that phase's `batches`
162
+ list at the same time. `import` and `census` touch `phases.json` incidentally,
163
+ each moving a phase to `running` (unless it is already `done`) when they record
164
+ a batch. Nothing else writes it at all.
165
+
166
+ A lock failure on `import`, `census`, `phase --status`, or `reset` exits `3`;
167
+ pass `--force-unlock` once you have confirmed no other agent is actually
168
+ writing.
169
+
170
+ ## Install
171
+
172
+ ```
173
+ bunx @iceinvein/agent-skills install migrate -g
174
+ ```
175
+
176
+ This skill ships in two parts: the prompt (`SKILL.md`) and a companion Bun
177
+ CLI (`bin` + `scripts`). The agent-skills installer writes both into
178
+ `~/.claude/skills/migrate/` and then runs the bundled `install.sh` as a
179
+ postinstall step, which symlinks `bin/migrate` onto your PATH (preferring
180
+ `/usr/local/bin`, falling back to `~/.local/bin`). Removing the skill with
181
+ `agent-skills remove migrate -g` runs `uninstall.sh` first to undo the PATH
182
+ symlink.
183
+
184
+ If you cloned this repo and want to run from source, you can also invoke
185
+ `./install.sh` directly: it does the PATH-link step against the local source
186
+ tree.
187
+
188
+ ## Development
189
+
190
+ ```
191
+ bun test # Run all tests
192
+ bun run lint # Biome check
193
+ bun run typecheck # tsc --noEmit
194
+ ```