@iceinvein/agent-skills 0.3.0 → 0.5.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 (67) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/skills/index.json +3 -3
  4. package/skills/migrate/README.md +35 -23
  5. package/skills/migrate/SKILL.md +75 -15
  6. package/skills/migrate/bin/migrate.ts +90 -0
  7. package/skills/migrate/docs/architecture.md +61 -26
  8. package/skills/migrate/docs/reference.md +53 -8
  9. package/skills/migrate/fixtures/fake-gh.ts +113 -0
  10. package/skills/migrate/fixtures/flow-target/docs/WORK.md +12 -0
  11. package/skills/migrate/fixtures/flow-target/docs/modernisation/capability-map/.gitkeep +0 -0
  12. package/skills/migrate/fixtures/flow-target/tools/flow/src/cli.ts +156 -0
  13. package/skills/migrate/package.json +1 -1
  14. package/skills/migrate/references/phases/adjudicate.md +161 -0
  15. package/skills/migrate/references/phases/handoff.md +220 -0
  16. package/skills/migrate/references/phases/probe.md +2 -2
  17. package/skills/migrate/references/phases/queue.md +21 -14
  18. package/skills/migrate/references/run-ops.md +17 -13
  19. package/skills/migrate/scripts/__tests__/adapter-flow.test.ts +290 -0
  20. package/skills/migrate/scripts/__tests__/adapter-github.test.ts +232 -0
  21. package/skills/migrate/scripts/__tests__/adapter-markdown.test.ts +183 -0
  22. package/skills/migrate/scripts/__tests__/adjudicate.test.ts +332 -0
  23. package/skills/migrate/scripts/__tests__/assumptions.test.ts +179 -0
  24. package/skills/migrate/scripts/__tests__/coverage.test.ts +192 -0
  25. package/skills/migrate/scripts/__tests__/e2e-express.test.ts +167 -7
  26. package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +9 -4
  27. package/skills/migrate/scripts/__tests__/forecast.test.ts +280 -0
  28. package/skills/migrate/scripts/__tests__/gates-handoff.test.ts +309 -0
  29. package/skills/migrate/scripts/__tests__/handoff-cmd.test.ts +308 -0
  30. package/skills/migrate/scripts/__tests__/handoff-order.test.ts +156 -0
  31. package/skills/migrate/scripts/adapters/flow.ts +280 -0
  32. package/skills/migrate/scripts/adapters/github.ts +260 -0
  33. package/skills/migrate/scripts/adapters/markdown.ts +175 -0
  34. package/skills/migrate/scripts/adjudicate-cmd.ts +243 -0
  35. package/skills/migrate/scripts/assumptions.ts +188 -0
  36. package/skills/migrate/scripts/check.ts +119 -320
  37. package/skills/migrate/scripts/coverage-cmd.ts +86 -0
  38. package/skills/migrate/scripts/coverage.ts +151 -0
  39. package/skills/migrate/scripts/dates.ts +17 -0
  40. package/skills/migrate/scripts/forecast-cmd.ts +124 -0
  41. package/skills/migrate/scripts/forecast.ts +264 -0
  42. package/skills/migrate/scripts/gates/adjudication.ts +30 -0
  43. package/skills/migrate/scripts/gates/census.ts +107 -0
  44. package/skills/migrate/scripts/gates/citations.ts +11 -0
  45. package/skills/migrate/scripts/gates/context.ts +76 -0
  46. package/skills/migrate/scripts/gates/coverage.ts +22 -0
  47. package/skills/migrate/scripts/gates/deltas.ts +15 -0
  48. package/skills/migrate/scripts/gates/handoff.ts +145 -0
  49. package/skills/migrate/scripts/gates/leaks.ts +11 -0
  50. package/skills/migrate/scripts/gates/parity.ts +15 -0
  51. package/skills/migrate/scripts/gates/queue.ts +9 -0
  52. package/skills/migrate/scripts/gates/refs.ts +97 -0
  53. package/skills/migrate/scripts/gates/run-state.ts +67 -0
  54. package/skills/migrate/scripts/gates/source.ts +28 -0
  55. package/skills/migrate/scripts/handoff-cmd.ts +186 -0
  56. package/skills/migrate/scripts/handoff.ts +330 -0
  57. package/skills/migrate/scripts/paths.ts +4 -0
  58. package/skills/migrate/scripts/types.ts +43 -0
  59. package/skills/migrate/scripts/validate.ts +12 -0
  60. package/skills/migrate/skill.json +2 -2
  61. package/skills/migrate/templates/forecast-assumptions.md +59 -0
  62. package/skills/sluice/SKILL.md +24 -7
  63. package/skills/sluice/references/deep-channel.md +197 -43
  64. package/skills/sluice/references/meter.md +7 -0
  65. package/skills/sluice/references/review.md +15 -7
  66. package/skills/sluice/scripts/run-stats.sh +22 -3
  67. package/skills/sluice/skill.json +1 -1
package/README.md CHANGED
@@ -28,7 +28,7 @@ Auto-detects Claude Code, Cursor, Codex, or Gemini CLI. Use `--tool claude` to t
28
28
 
29
29
  | Skill | What it does |
30
30
  |-------|--------------|
31
- | **migrate** | Walks a legacy codebase through probe, enumerate, seam, extract, parity, and queue: measured surface coverage, an empirically derived capability seam, cited requirements, a parity plan, and a batch decision queue. Bundles a Bun CLI that enforces the coverage arithmetic and phase ordering instead of trusting it. |
31
+ | **migrate** | Walks a legacy codebase through probe, enumerate, seam, extract, parity, queue, adjudicate and handoff: measured surface coverage, an empirically derived capability seam, cited requirements, a parity plan, a batch decision queue, and work items emitted to markdown, GitHub or a flow target with coverage and forecast read back. Bundles a Bun CLI that enforces the coverage arithmetic and phase ordering instead of trusting it. |
32
32
 
33
33
  ### Code Architecture
34
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
package/skills/index.json CHANGED
@@ -225,9 +225,9 @@
225
225
  },
226
226
  {
227
227
  "name": "migrate",
228
- "description": "Source-agnostic legacy migration mapping. Walks a legacy codebase through probe, enumerate, seam, extract, parity, and queue, building an auditable requirements ledger with mandatory citations and a `migrate check` gate in place of self-reported completeness. Use when the user asks to migrate, re-specify, replatform, or map a legacy system onto a new stack, or to resume, check, or report on a mapping run already under way.",
228
+ "description": "Source-agnostic legacy migration mapping. Walks a legacy codebase through probe, enumerate, seam, extract, parity, queue, adjudicate and handoff, building an auditable requirements ledger with mandatory citations and a `migrate check` gate in place of self-reported completeness. Use when the user asks to migrate, re-specify, replatform, or map a legacy system onto a new stack, to hand mapped requirements to a delivery team, or to resume, check, report, or forecast a mapping run already under way.",
229
229
  "type": "prompt",
230
- "version": "0.2.0"
230
+ "version": "0.3.0"
231
231
  },
232
232
  {
233
233
  "name": "module-secret-auditor",
@@ -283,7 +283,7 @@
283
283
  "name": "sluice",
284
284
  "description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries seven rules as one-liners in the router and the full treatment in references read only on friction, and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what every dispatched agent cost. Claude Code only; conflicts with the superpowers plugin.",
285
285
  "type": "prompt",
286
- "version": "0.1.1"
286
+ "version": "0.2.0"
287
287
  },
288
288
  {
289
289
  "name": "temporal-coupling-detector",
@@ -8,9 +8,9 @@ directions per lens, derives a capability seam empirically rather than by
8
8
  guesswork, extracts cited functional requirements, plans parity against the
9
9
  source, and routes every ambiguity to a batch decision queue for a human to
10
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.
11
+ are enforced by a bundled Bun CLI instead of being self-reported. Adjudicate
12
+ and handoff complete the walkthrough, and `migrate coverage` and `migrate
13
+ forecast` read delivery back through the medium handoff emitted into.
14
14
 
15
15
  ## Using it
16
16
 
@@ -31,14 +31,16 @@ that phase is current so a run never pays for prose it does not need yet.
31
31
  | 3 | Extract | `references/phases/extract.md` | `requirements.jsonl`, attribute/rule-sweep/closer census records, terminal element dispositions |
32
32
  | 4 | Parity | `references/phases/parity.md` | `deltas.jsonl` and a parity plan on every non-queued requirement |
33
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
34
+ | 6 | Adjudicate | `references/phases/adjudicate.md` | A ruling on every queue item, recorded in its own frontmatter |
35
+ | 7 | Handoff | `references/phases/handoff.md` | Work items in a delivery medium, plus `handoff.json` recording what was emitted |
36
+
37
+ A run goes probe through handoff, and plain `migrate check`, with no
38
+ `--phase`, gates every one of them: its exit 0 is what "the migration is
39
+ mapped" means. `migrate check --phase <p>` is the mid-run form, and the two
40
+ gates that describe phases 6 and 7 are phase-scoped so they stay silent
41
+ until the checked terminus reaches them. Once handoff has run, `migrate
42
+ coverage` and `migrate forecast` read delivery back through the same adapter
43
+ that emitted the work. `references/run-ops.md` covers what applies across every phase
42
44
  rather than any one of them: subagent dispatch, the batch-checkpoint
43
45
  discipline, and what happens when two agents contend for the store lock.
44
46
 
@@ -69,10 +71,11 @@ directory. See `references/recipes/README.md` for the exact file shape.
69
71
  migrate check --phase <current-phase>
70
72
  ```
71
73
 
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.
74
+ bounds the run-state gate at that phase, and skips the two gates that
75
+ describe phases the terminus has not reached (`adjudication` and `handoff`).
76
+ The remaining nine always read the whole store, so a coverage or census gap
77
+ past your current phase still fails on its own gate regardless of `--phase`.
78
+ Citations are checked by default; pass `--no-citations` to skip that gate.
76
79
 
77
80
  ## Fixtures
78
81
 
@@ -92,12 +95,14 @@ end:
92
95
 
93
96
  `scripts/__tests__/e2e-express.test.ts` and
94
97
  `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.
98
+ temp directory and drive the real CLI as a subprocess through `init`,
99
+ `import`, `census`, `phase`, `queue add`, `queue list`, and `check`,
100
+ reconciling every row against the fixture's ground truth.
101
+ `e2e-webforms.test.ts` runs probe through queue against the `aspnet` recipe.
102
+ `e2e-express.test.ts` carries on past it, through `adjudicate`, `handoff`,
103
+ `coverage` and `forecast`, and closes on plain `migrate check` at exit 0:
104
+ the whole point of the unbounded gate is that some real run has to be able
105
+ to reach it.
101
106
 
102
107
  Both show gates in both directions. Failing before they pass: the mid-run check
103
108
  after enumerate names the three closer records extract has not written yet, and
@@ -110,7 +115,7 @@ asserts the gate that should catch it does.
110
115
 
111
116
  - **[docs/reference.md](docs/reference.md)** is what you need to drive the CLI:
112
117
  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
118
+ their grammars, what each of the twelve gates enforces, and the exit-code
114
119
  convention. Ships with the installed skill.
115
120
  - **[docs/architecture.md](docs/architecture.md)** is for working on the skill
116
121
  itself: the module map, the rule that decides what belongs in the CLI rather
@@ -133,6 +138,8 @@ The store lives at `.migrate/` in the target repo and is committed.
133
138
  | `.migrate/phases.json` | object | per-phase status, batches, resume pointers |
134
139
  | `.migrate/seam.md` | prose | validator scripts and their raw output |
135
140
  | `.migrate/parity-basis.md` | prose | runnable-versus-source-only detection evidence |
141
+ | `.migrate/handoff.json` | What handoff emitted, the refs that make a re-run idempotent, and the forecast basis |
142
+ | `.migrate/forecast-assumptions.md` | Owner-attested forecast inputs; `migrate forecast` refuses without it |
136
143
  | `.migrate/queue/q-<slug>.md` | prose | evidence, options, recommendation |
137
144
  | `.migrate/.env` | secrets | runtime-lens credentials, gitignored |
138
145
  | `docs/migrate/*.md` | generated | human-readable views, written by `migrate report` |
@@ -151,6 +158,10 @@ The store lives at `.migrate/` in the target repo and is committed.
151
158
  | `migrate check [--phase <p>] [--no-citations] [--leaks]` | Runs the gates |
152
159
  | `migrate status` | Phase state, counts, resume pointer |
153
160
  | `migrate reset --phase <phase>` | Clears one phase's derived rows and returns it to `pending` |
161
+ | `migrate adjudicate [<id>] [--ruling <text>] [--force]` | Prints the review sheet, or records one ruling |
162
+ | `migrate handoff [--adapter <name>] [--dry-run]` | Emits the requirements as work items |
163
+ | `migrate coverage [--adapter <name>]` | Built versus confirmed, read back through the adapter |
164
+ | `migrate forecast [--adapter <name>]` | Projects remaining work from measured throughput |
154
165
  | `migrate report [--out <dir>]` | Renders markdown views |
155
166
 
156
167
  Run `migrate --help` for the same list from the CLI itself.
@@ -163,7 +174,8 @@ list at the same time. `import` and `census` touch `phases.json` incidentally,
163
174
  each moving a phase to `running` (unless it is already `done`) when they record
164
175
  a batch. Nothing else writes it at all.
165
176
 
166
- A lock failure on `import`, `census`, `phase --status`, or `reset` exits `3`;
177
+ A lock failure on `import`, `census`, `phase --status`, `reset`, `adjudicate`
178
+ or `handoff` exits `3`;
167
179
  pass `--force-unlock` once you have confirmed no other agent is actually
168
180
  writing.
169
181
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: migrate
3
- description: Source-agnostic legacy migration mapping. Walks a legacy codebase through probe, enumerate, seam, extract, parity, and queue, building an auditable requirements ledger with mandatory citations and a `migrate check` gate in place of self-reported completeness. Use when the user asks to migrate, re-specify, replatform, or map a legacy system onto a new stack, or to resume, check, or report on a mapping run already under way.
3
+ description: Source-agnostic legacy migration mapping. Walks a legacy codebase through probe, enumerate, seam, extract, parity, queue, adjudicate and handoff, building an auditable requirements ledger with mandatory citations and a `migrate check` gate in place of self-reported completeness. Use when the user asks to migrate, re-specify, replatform, or map a legacy system onto a new stack, to hand mapped requirements to a delivery team, or to resume, check, report, or forecast a mapping run already under way.
4
4
  ---
5
5
 
6
6
  # migrate
@@ -129,29 +129,89 @@ migrate phase queue --status done
129
129
 
130
130
  ### 6. Adjudicate
131
131
 
132
- A run stops at the queue in this version of the tool: `adjudicate` has no verb
133
- yet, so nothing here can move a queue item's status past `open`. `migrate
134
- status` and `migrate queue list` are the terminus; adjudication arrives with
135
- its verb in the next milestone.
132
+ Produces a ruling on every queue item: `status: adjudicated` plus a `ruling`
133
+ line, written into the item's own frontmatter.
134
+
135
+ Read `references/phases/adjudicate.md` before dispatching anything.
136
+
137
+ ```
138
+ migrate adjudicate
139
+ migrate adjudicate <id> --ruling "<text>"
140
+ migrate import <elements|reqs|deltas> <batch.json>
141
+ migrate phase adjudicate --status done
142
+ ```
143
+
144
+ Run `migrate adjudicate` with no arguments first: it prints the review sheet,
145
+ severity first, each item carrying the first line of its recommendation, which
146
+ is what makes one pass over the whole queue possible.
147
+
148
+ The verb writes queue frontmatter and nothing else. A ruling's consequence in
149
+ the store (an element's disposition, a requirement's confidence or parity) goes
150
+ through `migrate import`, exactly as in phases 3 and 4, so the row files keep
151
+ one writer and one validation path. The command prints that next step on every
152
+ success because it is easy to believe the ruling did it for you.
136
153
 
137
154
  ### 7. Handoff
138
155
 
139
- `handoff` has no verb yet either, for the same reason. `migrate status` and
140
- `migrate queue list` remain the terminus; handoff arrives with its verb in the
141
- next milestone.
156
+ Produces the requirements as work items in whatever the delivery team uses,
157
+ one work item per capability in dependency order, plus `.migrate/handoff.json`
158
+ recording what was emitted and the forecast basis.
159
+
160
+ Read `references/phases/handoff.md` before dispatching anything.
161
+
162
+ ```
163
+ migrate handoff --dry-run
164
+ migrate handoff [--adapter <markdown|github|flow>]
165
+ migrate phase handoff --status done
166
+ ```
167
+
168
+ Three adapters: `markdown` (the default, a roadmap plus a file per capability),
169
+ `github` (a milestone per capability, an issue per requirement), and `flow`
170
+ (a Nexus `stack` target's capability map, and `docs/WORK.md` where it exists).
171
+
172
+ Handoff refuses while anything is unresolved and names every blocker at once:
173
+ the gate with citations and leaks both on, any queue item still open, and any
174
+ requirement blocked by one. Blocked means a `queued` confidence or a sub-high
175
+ `rubric` parity whose queue item is **still open**; once that item is
176
+ adjudicated the requirement stops blocking, even though its confidence field
177
+ still reads `queued`.
178
+
179
+ `--dry-run` writes nothing at all, `handoff.json` included, so it is safe to
180
+ run against a store you are still working on.
181
+
182
+ ## Reading progress back
183
+
184
+ Once handoff has run, these two re-read delivery through the same adapter that
185
+ emitted the work. They are the only part of this tool meant to be run
186
+ repeatedly after the mapping run ends.
187
+
188
+ ```
189
+ migrate coverage
190
+ migrate forecast
191
+ ```
192
+
193
+ `coverage` divides built by **confirmed** requirements, reports the
194
+ non-confirmed exclusions separately, and names the evidence it read. `forecast`
195
+ needs an owner-attested `.migrate/forecast-assumptions.md`, copied from
196
+ `templates/forecast-assumptions.md`, and refuses without one; it labels every
197
+ scenario as measured or as an owner target so an aspiration never reads as a
198
+ fact.
142
199
 
143
200
  ## Checking as you go
144
201
 
145
202
  Run `migrate check --phase <current>` after every batch. It bounds the
146
- run-state gate at that phase; the other nine gates always read the whole
147
- store, so a coverage or census gap past your current phase still fails on its
148
- own gate regardless of `--phase`.
203
+ run-state gate at that phase; the nine gates that are neither run-state nor
204
+ phase-scoped always read the whole store, so a coverage or census gap past
205
+ your current phase still fails on its own gate regardless of `--phase`.
149
206
 
150
207
  Run plain `migrate check` only when claiming the whole migration is complete:
151
- with no `--phase`, it gates every phase through `handoff`. In this version
152
- that cannot pass, because `adjudicate` and `handoff` have no verbs to complete
153
- them. `migrate check --phase queue` is the practical terminus for this
154
- milestone; its exit 0 is what "done, for now" means.
208
+ with no `--phase`, it gates every phase through `handoff`, and its exit 0 is
209
+ what "the migration is mapped" means.
210
+
211
+ Two of the twelve gates are phase-scoped. `adjudication` and `handoff` describe
212
+ phases 6 and 7, so they stay silent until the checked terminus reaches them;
213
+ that is what keeps `migrate check --phase queue` usable for a whole mid-run
214
+ campaign instead of red from the first batch.
155
215
 
156
216
  ```
157
217
  migrate check --phase queue
@@ -18,12 +18,20 @@ Subcommands:
18
18
  queue add <file.md> Add a queue item
19
19
  queue list [--open] List queue items, severity first
20
20
  queue show <id> Print one queue item
21
+ adjudicate [<id>] [--ruling <text>] [--force] [--force-unlock]
22
+ Print the review sheet, or record one ruling
21
23
  check [--phase <p>] [--no-citations] [--leaks]
22
24
  Run the gates; without --phase, exit 0 means
23
25
  the whole migration is complete
24
26
  status Phase state, counts, resume pointer
25
27
  reset --phase <phase> [--force-unlock]
26
28
  Clear one phase's derived rows
29
+ handoff [--adapter <name>] [--dry-run] [--force-unlock]
30
+ Emit the ratified requirements as work items
31
+ coverage [--adapter <name>] Built versus confirmed, read back through
32
+ the adapter that emitted the work
33
+ forecast [--adapter <name>] Project remaining work from measured
34
+ throughput and attested assumptions
27
35
  report [--out <dir>] Render markdown views
28
36
  --version Print the migrate version
29
37
  --help Show this message`
@@ -166,6 +174,38 @@ const HANDLERS: Record<string, Handler> = {
166
174
  const { runQueue } = await import('../scripts/queue-cmd.ts')
167
175
  return runQueue({ root, args })
168
176
  },
177
+ adjudicate: async (args) => {
178
+ const ruling = readFlag(args, '--ruling')
179
+ if (ruling.error) {
180
+ process.stderr.write(`adjudicate: ${ruling.error}\n`)
181
+ return 2
182
+ }
183
+ // Same rule as `phase`: a flag sitting in the first positional slot leaves
184
+ // the id undefined, which would silently downgrade a write to a listing
185
+ // nobody asked for. Any write-intent flag with no id to apply it to is
186
+ // that mistake, so it is refused rather than serviced.
187
+ const id = args[0]?.startsWith('--') ? undefined : args[0]
188
+ if (!id && (ruling.value !== undefined || args.includes('--force'))) {
189
+ process.stderr.write(
190
+ 'adjudicate: want <id> before --ruling/--force, as in `adjudicate q-x --ruling "..."`\n',
191
+ )
192
+ return 2
193
+ }
194
+ const { findStoreRoot } = await import('../scripts/paths.ts')
195
+ const root = await findStoreRoot(process.cwd())
196
+ if (!root) {
197
+ process.stderr.write('adjudicate: no .migrate store found above the cwd\n')
198
+ return 2
199
+ }
200
+ const { runAdjudicate } = await import('../scripts/adjudicate-cmd.ts')
201
+ return runAdjudicate({
202
+ root,
203
+ ...(id ? { id } : {}),
204
+ ...(ruling.value !== undefined ? { ruling: ruling.value } : {}),
205
+ ...(args.includes('--force') ? { force: true } : {}),
206
+ ...(args.includes('--force-unlock') ? { forceUnlock: true } : {}),
207
+ })
208
+ },
169
209
  check: async (args) => {
170
210
  const phase = readFlag(args, '--phase')
171
211
  if (phase.error) {
@@ -227,6 +267,56 @@ const HANDLERS: Record<string, Handler> = {
227
267
  const { runReset } = await import('../scripts/reset-cmd.ts')
228
268
  return runReset({ root, phase, ...(forceUnlock ? { forceUnlock: true } : {}) })
229
269
  },
270
+ handoff: async (args) => {
271
+ const adapter = readFlag(args, '--adapter')
272
+ if (adapter.error) {
273
+ process.stderr.write(`handoff: ${adapter.error}\n`)
274
+ return 2
275
+ }
276
+ const { findStoreRoot } = await import('../scripts/paths.ts')
277
+ const root = await findStoreRoot(process.cwd())
278
+ if (!root) {
279
+ process.stderr.write('handoff: no .migrate store found above the cwd\n')
280
+ return 2
281
+ }
282
+ const { runHandoff } = await import('../scripts/handoff-cmd.ts')
283
+ return runHandoff({
284
+ root,
285
+ ...(adapter.value ? { adapter: adapter.value } : {}),
286
+ ...(args.includes('--dry-run') ? { dryRun: true } : {}),
287
+ ...(args.includes('--force-unlock') ? { forceUnlock: true } : {}),
288
+ })
289
+ },
290
+ coverage: async (args) => {
291
+ const adapter = readFlag(args, '--adapter')
292
+ if (adapter.error) {
293
+ process.stderr.write(`coverage: ${adapter.error}\n`)
294
+ return 2
295
+ }
296
+ const { findStoreRoot } = await import('../scripts/paths.ts')
297
+ const root = await findStoreRoot(process.cwd())
298
+ if (!root) {
299
+ process.stderr.write('coverage: no .migrate store found above the cwd\n')
300
+ return 2
301
+ }
302
+ const { runCoverage } = await import('../scripts/coverage-cmd.ts')
303
+ return runCoverage({ root, ...(adapter.value ? { adapter: adapter.value } : {}) })
304
+ },
305
+ forecast: async (args) => {
306
+ const adapter = readFlag(args, '--adapter')
307
+ if (adapter.error) {
308
+ process.stderr.write(`forecast: ${adapter.error}\n`)
309
+ return 2
310
+ }
311
+ const { findStoreRoot } = await import('../scripts/paths.ts')
312
+ const root = await findStoreRoot(process.cwd())
313
+ if (!root) {
314
+ process.stderr.write('forecast: no .migrate store found above the cwd\n')
315
+ return 2
316
+ }
317
+ const { runForecast } = await import('../scripts/forecast-cmd.ts')
318
+ return runForecast({ root, ...(adapter.value ? { adapter: adapter.value } : {}) })
319
+ },
230
320
  report: async (args) => {
231
321
  const result = readFlag(args, '--out')
232
322
  if (result.error) {
@@ -7,9 +7,9 @@ file formats and gate behaviour, see [reference.md](reference.md).
7
7
 
8
8
  Three parts, one rule that decides which part gets a given piece of work.
9
9
 
10
- - **The skill** (`SKILL.md`, plus the six phase manuals under
11
- `references/phases/` and the cross-cutting `references/run-ops.md`, all
12
- landed in Milestone 2) holds judgment: what to look for in a legacy codebase,
10
+ - **The skill** (`SKILL.md`, plus the eight phase manuals under
11
+ `references/phases/` and the cross-cutting `references/run-ops.md`) holds
12
+ judgment: what to look for in a legacy codebase,
13
13
  how to decide a requirement is confirmed rather than inferred, when to
14
14
  escalate to the queue.
15
15
  - **The CLI** (`bin/`, `scripts/`) holds anything that must not be
@@ -40,14 +40,22 @@ scripts/
40
40
  config.ts config.toml load and write, TOML escaping
41
41
  store.ts JSONL read, atomic write, id upsert, file readers
42
42
  lock.ts store lock: serialises the read-modify-write in import,
43
- census, phase --status and reset
43
+ census, phase --status, reset, adjudicate and handoff
44
44
  phases.ts phases.json state and committed batches
45
45
  validate.ts per-row shape validation shared by import and check
46
46
  census.ts census kinds, balance and bounds invariants, subject identity
47
47
  citations.ts resolves src refs against the source tree
48
48
  leaks.ts scans artifacts and git history for env values
49
49
  queue.ts queue item parsing and grammar
50
- check.ts composes the ten gates into a violation list
50
+ check.ts loads the store once, runs GATE_ORDER, applies the
51
+ phase-scope rule; holds no gate logic itself
52
+ gates/context.ts GateContext: everything a gate may read
53
+ gates/<gate>.ts one module per gate, twelve of them
54
+ handoff.ts adapter contract, dependency order, preflight, handoff.json
55
+ adapters/*.ts markdown, github, flow: one per delivery medium
56
+ coverage.ts built over confirmed, per capability
57
+ assumptions.ts parses and validates forecast-assumptions.md
58
+ forecast.ts measured velocities, demand, scenario projection
51
59
  report.ts markdown rendering
52
60
  *-cmd.ts one per subcommand; argument handling and orchestration
53
61
  ```
@@ -109,7 +117,7 @@ and `init` shipped exactly that bug in Milestone 2.
109
117
  renames, and cleans up the temp file on failure without masking the original
110
118
  error. A fixed temp name was tried first and lost data under concurrent writes.
111
119
 
112
- **The read-modify-write around a store file is lock-serialised.** Four commands
120
+ **The read-modify-write around a store file is lock-serialised.** Six commands
113
121
  do one. `import` and `census` each read a whole store file, upsert or replace
114
122
  rows, and rewrite the whole file; both also commit a batch into `phases.json`,
115
123
  via `recordBatch`, inside the same lock. `phase --status` does its own
@@ -127,7 +135,7 @@ one rename can still discard the other's rows.
127
135
  section must not take it. That is why `savePhases` and `recordBatch` are
128
136
  lock-free while `setPhaseStatus` is not, and why `reset-cmd.ts` calls
129
137
  `savePhases` rather than `setPhaseStatus` to move its phase back to `pending`.
130
- Check this before adding a call inside any of the four.
138
+ Check this before adding a call inside any of them.
131
139
 
132
140
  `census-cmd.ts` orders its two writes deliberately: it commits the batch into
133
141
  `phases.json` first, and writes `census.jsonl` second. If the process is
@@ -139,7 +147,7 @@ orphan census row would be worse: a record naming a batch that was never
139
147
  actually committed, which is exactly the mismatch the run-state gate exists to
140
148
  catch.
141
149
 
142
- `lock.ts`'s `withStoreLock` wraps each of these four write paths in one lock
150
+ `lock.ts`'s `withStoreLock` wraps each of these write paths in one lock
143
151
  file for the whole store (`.migrate/.lock`, `O_EXCL` create, bounded retry with
144
152
  backoff). It distinguishes a lock file that is merely absent or momentarily
145
153
  empty (never counted against a corruption budget) from one that is genuinely
@@ -180,20 +188,43 @@ store.
180
188
 
181
189
  ### Add a gate
182
190
 
183
- Gates live in `check.ts` and push `{ gate, message }` onto one list.
184
-
185
- 1. Add the gate name to `GATE_ORDER`, which fixes its position in the report.
186
- 2. Push violations that name the specific offending row, path or id. An
191
+ Gates live one per module under `scripts/gates/`, each exporting
192
+ `export const gate: Gate` over the shared `GateContext` that `check.ts` builds
193
+ once per run. A gate is a pure function of that record: it may not read the
194
+ store itself, which is what keeps the store parsed exactly once no matter how
195
+ many gates consult it, and what stops a gate depending on another having run
196
+ first. Add the name to `GATE_ORDER`, the module to `GATES`, and, only if the
197
+ gate describes work a mid-run terminus has not reached, an entry to
198
+ `GATE_PHASE`.
199
+
200
+ 1. Write `scripts/gates/<name>.ts` exporting `export const gate: Gate`, and
201
+ add the name to `GATE_ORDER`, which fixes its position in the report, and
202
+ the module to `GATES`.
203
+ 2. Return violations that name the specific offending row, path or id. An
187
204
  aggregate "check failed" is never acceptable; the message is what an agent
188
205
  acts on without a human.
189
- 3. If the gate is expensive, make it opt-in behind a flag like `--leaks`, and
190
- have `check-cmd.ts` pass it through. If it is cheap enough to want on by
191
- default instead, follow citations: on unless the caller passes
192
- `--no-citations`, so an orchestrator does not have to remember to ask for
193
- it.
194
- 4. Add tests for both directions. A gate that produces false failures is worse
206
+ 3. If the gate is expensive, make it opt-in behind a flag like `--leaks`, read
207
+ off `GateContext`, and have `check-cmd.ts` pass it through. If it is cheap
208
+ enough to want on by default instead, follow citations: on unless the caller
209
+ passes `--no-citations`.
210
+ 4. Only if the gate describes work a mid-run terminus has not reached, add an
211
+ entry to `GATE_PHASE`. This is a narrow escape hatch, not the norm: nine of
212
+ the twelve gates read the whole store regardless of `--phase`, and a tenth,
213
+ run-state, is bounded by the terminus rather than skipped. A defect is a
214
+ defect whenever it is found.
215
+ 5. Add tests for both directions. A gate that produces false failures is worse
195
216
  than no gate, because it makes `check` ignorable.
196
217
 
218
+ ### Add a handoff adapter
219
+
220
+ `scripts/adapters/<name>.ts` exporting an `Adapter`, registered in the
221
+ `ADAPTERS` map in `handoff-cmd.ts`, `coverage-cmd.ts` and `forecast-cmd.ts`.
222
+ An adapter reaches the world only through `HandoffInput`'s `root`, `gitBin` and
223
+ `ghBin`, which is what makes `github` testable against `fixtures/fake-gh.ts`
224
+ and `flow` against `fixtures/flow-target/`. `apply()` must be idempotent, and
225
+ must not destroy progress the medium is carrying: the markdown adapter reads
226
+ its own roadmap before regenerating it for exactly that reason.
227
+
197
228
  ### Add a census kind
198
229
 
199
230
  1. Add the variant to the `Census` union in `types.ts`.
@@ -219,7 +250,7 @@ This is the main source-genericity lever: a COBOL source declares `programs`,
219
250
  `bun run typecheck` (tsc). All three must be clean.
220
251
 
221
252
  **A test that passes against a broken implementation is treated as a defect**,
222
- not a minor style issue. Two shipped during this milestone and both were caught
253
+ not a minor style issue. Two shipped during Milestone 2 and both were caught
223
254
  only by mutation. When you add a regression test, verify it fails against the
224
255
  unfixed code: revert the source file with
225
256
  `git checkout <sha> -- <path>`, run the test, observe the failure, then restore
@@ -254,13 +285,17 @@ too:
254
285
  surfaces, three elements) and shows the gate failing on unaccounted elements
255
286
  before it passes. That arc is the point: a test that only demonstrates the
256
287
  passing state would be worth much less.
257
- - `e2e-express.test.ts` and `e2e-webforms.test.ts` drive the whole of each
258
- fixture probe through queue: `init`, `import`, `census`, `phase`, `queue
259
- add`, `queue list`, and `check`, ending green at `migrate check --phase
260
- queue` and then asserting plain `migrate check` fails on exactly `adjudicate`
261
- and `handoff`. Both parse `GROUND-TRUTH.md` for their element rows rather
262
- than hand-copying them, so fixture and test cannot drift, and both close on a
263
- mutation showing the terminus assertion is load-bearing.
288
+ - `e2e-webforms.test.ts` drives the whole of `tiny-webforms` probe through
289
+ queue: `init`, `import`, `census`, `phase`, `queue add`, `queue list`, and
290
+ `check`, ending green at `migrate check --phase queue`.
291
+ - `e2e-express.test.ts` carries on past that terminus through `adjudicate`,
292
+ `handoff`, `coverage` and `forecast`, and closes on plain `migrate check` at
293
+ exit 0. Some real run has to be able to reach the unbounded gate, or it
294
+ asserts nothing.
295
+
296
+ Both parse `GROUND-TRUTH.md` for their element rows rather than hand-copying
297
+ them, so fixture and test cannot drift, and both close on a mutation showing the
298
+ terminus assertion is load-bearing.
264
299
 
265
300
  When you change a fixture, the tests that read it will tell you; when you
266
301
  change a manual the tests follow (`seam.md`'s clustering procedure,