@iceinvein/agent-skills 0.2.0 → 0.4.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 (68) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/index.js +14 -10
  3. package/package.json +1 -1
  4. package/skills/index.json +4 -4
  5. package/skills/migrate/README.md +35 -23
  6. package/skills/migrate/SKILL.md +75 -15
  7. package/skills/migrate/bin/migrate.ts +90 -0
  8. package/skills/migrate/docs/architecture.md +61 -26
  9. package/skills/migrate/docs/reference.md +53 -8
  10. package/skills/migrate/fixtures/fake-gh.ts +113 -0
  11. package/skills/migrate/fixtures/flow-target/docs/WORK.md +12 -0
  12. package/skills/migrate/fixtures/flow-target/docs/modernisation/capability-map/.gitkeep +0 -0
  13. package/skills/migrate/fixtures/flow-target/tools/flow/src/cli.ts +156 -0
  14. package/skills/migrate/package.json +1 -1
  15. package/skills/migrate/references/phases/adjudicate.md +161 -0
  16. package/skills/migrate/references/phases/handoff.md +220 -0
  17. package/skills/migrate/references/phases/probe.md +2 -2
  18. package/skills/migrate/references/phases/queue.md +21 -14
  19. package/skills/migrate/references/run-ops.md +17 -13
  20. package/skills/migrate/scripts/__tests__/adapter-flow.test.ts +290 -0
  21. package/skills/migrate/scripts/__tests__/adapter-github.test.ts +232 -0
  22. package/skills/migrate/scripts/__tests__/adapter-markdown.test.ts +183 -0
  23. package/skills/migrate/scripts/__tests__/adjudicate.test.ts +332 -0
  24. package/skills/migrate/scripts/__tests__/assumptions.test.ts +179 -0
  25. package/skills/migrate/scripts/__tests__/coverage.test.ts +192 -0
  26. package/skills/migrate/scripts/__tests__/e2e-express.test.ts +167 -7
  27. package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +9 -4
  28. package/skills/migrate/scripts/__tests__/forecast.test.ts +280 -0
  29. package/skills/migrate/scripts/__tests__/gates-handoff.test.ts +309 -0
  30. package/skills/migrate/scripts/__tests__/handoff-cmd.test.ts +308 -0
  31. package/skills/migrate/scripts/__tests__/handoff-order.test.ts +156 -0
  32. package/skills/migrate/scripts/adapters/flow.ts +280 -0
  33. package/skills/migrate/scripts/adapters/github.ts +260 -0
  34. package/skills/migrate/scripts/adapters/markdown.ts +175 -0
  35. package/skills/migrate/scripts/adjudicate-cmd.ts +243 -0
  36. package/skills/migrate/scripts/assumptions.ts +188 -0
  37. package/skills/migrate/scripts/check.ts +119 -320
  38. package/skills/migrate/scripts/coverage-cmd.ts +86 -0
  39. package/skills/migrate/scripts/coverage.ts +151 -0
  40. package/skills/migrate/scripts/dates.ts +17 -0
  41. package/skills/migrate/scripts/forecast-cmd.ts +124 -0
  42. package/skills/migrate/scripts/forecast.ts +264 -0
  43. package/skills/migrate/scripts/gates/adjudication.ts +30 -0
  44. package/skills/migrate/scripts/gates/census.ts +107 -0
  45. package/skills/migrate/scripts/gates/citations.ts +11 -0
  46. package/skills/migrate/scripts/gates/context.ts +76 -0
  47. package/skills/migrate/scripts/gates/coverage.ts +22 -0
  48. package/skills/migrate/scripts/gates/deltas.ts +15 -0
  49. package/skills/migrate/scripts/gates/handoff.ts +145 -0
  50. package/skills/migrate/scripts/gates/leaks.ts +11 -0
  51. package/skills/migrate/scripts/gates/parity.ts +15 -0
  52. package/skills/migrate/scripts/gates/queue.ts +9 -0
  53. package/skills/migrate/scripts/gates/refs.ts +97 -0
  54. package/skills/migrate/scripts/gates/run-state.ts +67 -0
  55. package/skills/migrate/scripts/gates/source.ts +28 -0
  56. package/skills/migrate/scripts/handoff-cmd.ts +186 -0
  57. package/skills/migrate/scripts/handoff.ts +330 -0
  58. package/skills/migrate/scripts/paths.ts +4 -0
  59. package/skills/migrate/scripts/types.ts +43 -0
  60. package/skills/migrate/scripts/validate.ts +12 -0
  61. package/skills/migrate/skill.json +2 -2
  62. package/skills/migrate/templates/forecast-assumptions.md +59 -0
  63. package/skills/sluice/SKILL.md +20 -7
  64. package/skills/sluice/references/deep-channel.md +20 -0
  65. package/skills/sluice/references/finish.md +4 -2
  66. package/skills/sluice/references/meter.md +38 -0
  67. package/skills/sluice/scripts/run-stats.sh +236 -0
  68. package/skills/sluice/skill.json +4 -3
@@ -0,0 +1,192 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { computeCoverage, renderCoverage } from '../coverage.ts'
3
+ import type { HandoffFile } from '../handoff.ts'
4
+ import type { Requirement, Throughput } from '../types.ts'
5
+
6
+ function req(id: string, cap: string, confidence: Requirement['confidence']): Requirement {
7
+ return {
8
+ id,
9
+ cap,
10
+ requirement: `requirement ${id}`,
11
+ actors: 'User',
12
+ objects: 'Thing',
13
+ rules: 'none',
14
+ origin: 'intended',
15
+ confidence,
16
+ citations: [],
17
+ parity: { kind: 'rubric', level: 'high' },
18
+ batch: 'b-1',
19
+ }
20
+ }
21
+
22
+ const CONFIRMED: Requirement['confidence'] = { kind: 'confirmed' }
23
+ const INFERRED: Requirement['confidence'] = { kind: 'inferred' }
24
+
25
+ const REQS = [
26
+ req('UM-001', 'user-management', CONFIRMED),
27
+ req('UM-002', 'user-management', CONFIRMED),
28
+ req('BI-001', 'billing', CONFIRMED),
29
+ req('BI-002', 'billing', INFERRED),
30
+ req('BI-003', 'billing', INFERRED),
31
+ ]
32
+
33
+ const HANDOFF: HandoffFile = {
34
+ version: 1,
35
+ adapter: 'markdown',
36
+ items: [
37
+ {
38
+ key: 'billing',
39
+ title: 'Billing',
40
+ frs: ['BI-001', 'BI-002', 'BI-003'],
41
+ dependsOn: [],
42
+ weight: 3,
43
+ },
44
+ {
45
+ key: 'user-management',
46
+ title: 'User management',
47
+ frs: ['UM-001', 'UM-002'],
48
+ dependsOn: ['billing'],
49
+ weight: 2,
50
+ },
51
+ ],
52
+ refs: {},
53
+ basis: { confirmed: 3, emitted: 5, order: ['billing', 'user-management'] },
54
+ }
55
+
56
+ const through = (completions: Throughput['completions']): Throughput => ({
57
+ completions,
58
+ basis: 'markdown roadmap checkboxes, dated in file',
59
+ })
60
+
61
+ test('the denominator is confirmed requirements only, with exclusions reported', () => {
62
+ const r = computeCoverage({
63
+ requirements: REQS,
64
+ handoff: HANDOFF,
65
+ throughput: through([
66
+ { fr: 'UM-001', doneAt: '2026-08-12' },
67
+ { fr: 'BI-001', doneAt: '2026-08-11' },
68
+ ]),
69
+ })
70
+ // Five requirements exist; three are confirmed; two of those are built.
71
+ expect(r.confirmed).toBe(3)
72
+ expect(r.built).toBe(2)
73
+ expect(r.nonConfirmed).toEqual([{ slug: 'billing', count: 2 }])
74
+ })
75
+
76
+ test('a completion for a non-confirmed requirement does not inflate the figure', () => {
77
+ // BI-002 is inferred, so it is outside the denominator. Reporting it as
78
+ // complete must not push built above the confirmed total it sits over.
79
+ const r = computeCoverage({
80
+ requirements: REQS,
81
+ handoff: HANDOFF,
82
+ throughput: through([
83
+ { fr: 'BI-001', doneAt: '2026-08-11' },
84
+ { fr: 'BI-002', doneAt: '2026-08-11' },
85
+ ]),
86
+ })
87
+ expect(r.built).toBe(1)
88
+ expect(r.confirmed).toBe(3)
89
+ expect(r.unknown).toEqual([])
90
+ })
91
+
92
+ test('capabilities are reported in the emitted dependency order', () => {
93
+ const r = computeCoverage({ requirements: REQS, handoff: HANDOFF, throughput: through([]) })
94
+ expect(r.caps.map((c) => c.slug)).toEqual(['billing', 'user-management'])
95
+ })
96
+
97
+ test('an undated completion counts as built and is reported as undated', () => {
98
+ const r = computeCoverage({
99
+ requirements: REQS,
100
+ handoff: HANDOFF,
101
+ throughput: through([{ fr: 'UM-001', doneAt: null }]),
102
+ })
103
+ expect(r.built).toBe(1)
104
+ expect(r.undated).toBe(1)
105
+ expect(renderCoverage(r)).toContain('undated: 1 completion(s)')
106
+ })
107
+
108
+ test('a completion naming an unknown requirement is collected, not counted', () => {
109
+ const r = computeCoverage({
110
+ requirements: REQS,
111
+ handoff: HANDOFF,
112
+ throughput: through([
113
+ { fr: 'UM-001', doneAt: '2026-08-12' },
114
+ { fr: 'ZZ-999', doneAt: '2026-08-12' },
115
+ ]),
116
+ })
117
+ expect(r.unknown).toEqual(['ZZ-999'])
118
+ expect(r.built).toBe(1)
119
+ })
120
+
121
+ test('the rendered report names its evidence and marks a finished capability', () => {
122
+ const r = computeCoverage({
123
+ requirements: REQS,
124
+ handoff: HANDOFF,
125
+ throughput: through([
126
+ { fr: 'UM-001', doneAt: '2026-08-12' },
127
+ { fr: 'UM-002', doneAt: '2026-08-13' },
128
+ ]),
129
+ })
130
+ expect(renderCoverage(r)).toBe(
131
+ [
132
+ 'built 2/3 confirmed requirements (67%)',
133
+ 'evidence: markdown roadmap checkboxes, dated in file',
134
+ 'excluded: 2 non-confirmed (billing 2)',
135
+ '',
136
+ 'billing 0/1',
137
+ 'user-management 2/2 done',
138
+ ].join('\n'),
139
+ )
140
+ })
141
+
142
+ test('a store with no confirmed requirements reports zero rather than dividing by zero', () => {
143
+ const r = computeCoverage({
144
+ requirements: [req('BI-002', 'billing', INFERRED)],
145
+ handoff: { ...HANDOFF, basis: { confirmed: 0, emitted: 1, order: ['billing'] } },
146
+ throughput: through([]),
147
+ })
148
+ expect(r.confirmed).toBe(0)
149
+ expect(renderCoverage(r)).toContain('built 0/0 confirmed requirements (0%)')
150
+ })
151
+
152
+ test('a capability the emitted order omits is counted and named as stale', () => {
153
+ // A stale handoff.json used to narrow both numerator and denominator
154
+ // silently, reporting 100% while confirmed requirements sat unbuilt in a
155
+ // capability that appeared nowhere in the output.
156
+ const r = computeCoverage({
157
+ requirements: REQS,
158
+ handoff: { ...HANDOFF, basis: { confirmed: 3, emitted: 5, order: ['user-management'] } },
159
+ throughput: through([
160
+ { fr: 'UM-001', doneAt: '2026-08-12' },
161
+ { fr: 'UM-002', doneAt: '2026-08-13' },
162
+ ]),
163
+ })
164
+ expect(r.confirmed).toBe(3)
165
+ expect(r.built).toBe(2)
166
+ expect(r.stale).toEqual(['billing'])
167
+ expect(r.caps.map((c) => c.slug)).toEqual(['user-management', 'billing'])
168
+ expect(renderCoverage(r)).toContain('stale: 1 capability(ies) not in the emitted work (billing)')
169
+ })
170
+
171
+ test('the percentage never contradicts the fraction beside it', () => {
172
+ const at = (built: number, confirmed: number): string => {
173
+ const reqs = Array.from({ length: confirmed }, (_, i) => req(`X-${i}`, 'billing', CONFIRMED))
174
+ return renderCoverage(
175
+ computeCoverage({
176
+ requirements: reqs,
177
+ handoff: {
178
+ ...HANDOFF,
179
+ items: [],
180
+ basis: { confirmed, emitted: confirmed, order: ['billing'] },
181
+ },
182
+ throughput: through(
183
+ Array.from({ length: built }, (_, i) => ({ fr: `X-${i}`, doneAt: '2026-08-12' })),
184
+ ),
185
+ }),
186
+ )
187
+ }
188
+ expect(at(199, 200)).toContain('built 199/200 confirmed requirements (99%)')
189
+ expect(at(1, 250)).toContain('built 1/250 confirmed requirements (1%)')
190
+ expect(at(200, 200)).toContain('(100%)')
191
+ expect(at(0, 200)).toContain('(0%)')
192
+ })
@@ -755,7 +755,7 @@ function readTomlString(text: string, key: string): string {
755
755
  return match[1]
756
756
  }
757
757
 
758
- test('contract-only run driven probe through queue, ending green at check --phase queue', async () => {
758
+ test('contract-only run driven probe through handoff, ending green at a plain check', async () => {
759
759
  const groundTruthPath = join(source, 'GROUND-TRUTH.md')
760
760
  const rows = await parseGroundTruth(groundTruthPath)
761
761
  expect(rows.length).toBeGreaterThan(0)
@@ -1012,8 +1012,8 @@ test('contract-only run driven probe through queue, ending green at check --phas
1012
1012
 
1013
1013
  // 10. Phase 5, queue. Every item this run owed was filed in the pass that
1014
1014
  // named it, so this phase closes on the status flip, exactly as queue.md
1015
- // says: closing is not "the queue is empty", since nothing in this milestone
1016
- // adjudicates an item.
1015
+ // says: closing is not "the queue is empty", since phase 5 itself never
1016
+ // adjudicates an item. Phase 6, below, is where they get decided.
1017
1017
  const listed = await migrate(['queue', 'list', '--open'])
1018
1018
  expect(listed.code).toBe(0)
1019
1019
  expect(listed.out).toContain(`${Object.keys(QUEUE_ITEMS).length + 1} item(s)`)
@@ -1025,12 +1025,13 @@ test('contract-only run driven probe through queue, ending green at check --phas
1025
1025
  expect(green.out).not.toContain('Violations')
1026
1026
  expect(green.code).toBe(0)
1027
1027
 
1028
- // 12. Plain `migrate check` gates every phase through handoff, and fails on
1029
- // exactly the two that have no verb in this milestone. The violation count
1030
- // is what makes "exactly" an assertion rather than a hope.
1028
+ // 12. Plain `migrate check` gates every phase through handoff. A run that
1029
+ // stops at the queue fails it on three distinct fronts, and naming each one
1030
+ // is what makes this an assertion rather than a hope: the two phases still
1031
+ // pending, every queue item nobody has ruled on, and the handoff that never
1032
+ // emitted anything.
1031
1033
  const full = await migrate(['check'])
1032
1034
  expect(full.code).toBe(1)
1033
- expect(full.out).toContain('Violations (2):')
1034
1035
  expect(full.out).toContain(' run-state:')
1035
1036
  expect(full.out).toContain(
1036
1037
  ' phase adjudicate is pending; every phase through handoff must be done',
@@ -1038,6 +1039,10 @@ test('contract-only run driven probe through queue, ending green at check --phas
1038
1039
  expect(full.out).toContain(
1039
1040
  ' phase handoff is pending; every phase through handoff must be done',
1040
1041
  )
1042
+ expect(full.out).toContain(' adjudication:')
1043
+ expect(full.out).toContain('is still open; every queue item needs a ruling before handoff')
1044
+ expect(full.out).toContain(' handoff:')
1045
+ expect(full.out).toContain('no handoff.json in the store')
1041
1046
 
1042
1047
  // 13. The terminus assertion in step 11 is load-bearing, shown by mutation
1043
1048
  // rather than asserted: nulling every requirement's parity plan is a phase-4
@@ -1090,4 +1095,159 @@ test('contract-only run driven probe through queue, ending green at check --phas
1090
1095
  expect(afterRemoval.out).toContain(
1091
1096
  `lens census for ${mutatedSurface} claims in_ledger 0 + added ${claimed} = ${claimed} element(s) in the ledger, but elements.jsonl has ${actual}`,
1092
1097
  )
1098
+ await writeFile(elementsPath, elementsText)
1099
+ expect((await migrate(['check', '--phase', 'queue'])).code).toBe(0)
1100
+
1101
+ // 15. Phase 6, adjudicate. The review sheet first, because that is how the
1102
+ // phase is meant to be worked: one pass over every open item with its
1103
+ // recommendation in view, rather than opening four files.
1104
+ const sheet = await migrate(['adjudicate'])
1105
+ expect(sheet.code).toBe(0)
1106
+ expect(sheet.out).toContain(`${Object.keys(QUEUE_ITEMS).length + 1} open`)
1107
+ for (const id of [...Object.keys(QUEUE_ITEMS), QUEUE_ID]) {
1108
+ expect(sheet.out).toContain(`${id} [`)
1109
+ }
1110
+
1111
+ const RULINGS: Record<string, string> = {
1112
+ 'q-express-user-list-source':
1113
+ 'the empty list is the real behaviour; the table stays unread and unmapped',
1114
+ 'q-express-mailer-delivery-unobservable':
1115
+ 'accept the rubric at moderate; delivery is unobservable from the source alone',
1116
+ 'q-express-users-table-unwired': 'the table is in scope and stays mapped to UD-002',
1117
+ [QUEUE_ID]: 'the scaffold row is enumeration noise and is skipped by name',
1118
+ }
1119
+ for (const [id, ruling] of Object.entries(RULINGS)) {
1120
+ const ruled = await migrate(['adjudicate', id, '--ruling', ruling])
1121
+ expect(ruled.code).toBe(0)
1122
+ expect(ruled.out).toContain('open -> adjudicated')
1123
+ }
1124
+ // Re-ruling one of them refuses without --force, so an owner's recorded
1125
+ // decision cannot be replaced by a re-run that meant no harm.
1126
+ const reruled = await migrate(['adjudicate', QUEUE_ID, '--ruling', 'something else'])
1127
+ expect(reruled.code).toBe(1)
1128
+ expect(reruled.err).toContain('--force')
1129
+
1130
+ expect((await migrate(['adjudicate'])).out).toContain('0 open')
1131
+ expect((await migrate(['phase', 'adjudicate', '--status', 'done'])).code).toBe(0)
1132
+
1133
+ // 16. Phase 7, handoff. UD-006 still carries a `queued` confidence, and its
1134
+ // queue item is now adjudicated, so it no longer blocks: that is the whole
1135
+ // point of measuring blockers against open items rather than against the
1136
+ // confidence field.
1137
+ const dry = await migrate(['handoff', '--dry-run'])
1138
+ expect(dry.code).toBe(0)
1139
+ expect(dry.out).toContain('plan:')
1140
+ expect(dry.out).toContain('nothing written')
1141
+ expect(await Bun.file(join(storeDir, 'handoff.json')).exists()).toBe(false)
1142
+
1143
+ const emitted = await migrate(['handoff'])
1144
+ expect(emitted.code).toBe(0)
1145
+ expect(emitted.out).toContain('adapter markdown')
1146
+
1147
+ const handoffFile = JSON.parse(await readFile(join(storeDir, 'handoff.json'), 'utf8'))
1148
+ expect(handoffFile.basis.emitted).toBe(REQUIREMENTS.length)
1149
+ // Every capability the seam declared reached a work item.
1150
+ expect(handoffFile.items).toHaveLength(CAPABILITIES.length)
1151
+ const roadmapPath = join(target, 'docs', 'migrate', 'roadmap.md')
1152
+ const roadmap = await readFile(roadmapPath, 'utf8')
1153
+ for (const r of REQUIREMENTS) expect(roadmap).toContain(`- [ ] ${r.id} `)
1154
+
1155
+ expect((await migrate(['phase', 'handoff', '--status', 'done'])).code).toBe(0)
1156
+
1157
+ // 17. The milestone's acceptance proof. Plain `migrate check`, with no
1158
+ // --phase, gating every phase through handoff, exits 0. This is the first
1159
+ // time in the project's history that the unbounded gate can pass at all.
1160
+ const complete = await migrate(['check'])
1161
+ expect(complete.out).not.toContain('Violations')
1162
+ expect(complete.code).toBe(0)
1163
+
1164
+ // 18. Coverage, read back through the adapter that emitted the work. Two
1165
+ // boxes ticked by hand, one dated and one not, which is what an owner
1166
+ // actually does to a roadmap.
1167
+ const built = ['UD-001', 'UD-002']
1168
+ let ticked = roadmap
1169
+ ticked = ticked.replace(`- [ ] ${built[0]} `, `- [x] ${built[0]} <!-- done:2026-08-10 --> `)
1170
+ ticked = ticked.replace(`- [ ] ${built[1]} `, `- [x] ${built[1]} <!-- done:2026-08-12 --> `)
1171
+ await writeFile(roadmapPath, ticked)
1172
+
1173
+ const coverage = await migrate(['coverage'])
1174
+ expect(coverage.code).toBe(0)
1175
+ expect(coverage.out).toContain('evidence: markdown roadmap checkboxes, dated in file')
1176
+ // The fixture carries two non-confirmed requirements, UD-006 (`queued`) and
1177
+ // one inferred. Both sit outside the confirmed denominator and are reported
1178
+ // as exclusions rather than counted against delivery: parity is a promise
1179
+ // about behaviour the run confirmed.
1180
+ expect(coverage.out).toContain(
1181
+ 'excluded: 2 non-confirmed (user-directory 1, welcome-notification 1)',
1182
+ )
1183
+ expect(coverage.out).toMatch(/built 2\/\d+ confirmed requirements/)
1184
+
1185
+ // Re-running handoff after the boxes were ticked must not erase them. This
1186
+ // is the data-loss regression the markdown adapter exists to avoid, checked
1187
+ // here against a real store rather than a unit fixture.
1188
+ expect((await migrate(['handoff'])).code).toBe(0)
1189
+ const afterRerun = await readFile(roadmapPath, 'utf8')
1190
+ expect(afterRerun).toContain(`- [x] ${built[0]} <!-- done:2026-08-10 --> `)
1191
+ expect(afterRerun).toContain(`- [x] ${built[1]} <!-- done:2026-08-12 --> `)
1192
+
1193
+ // 19. Forecast. It refuses before the owner has attested anything, which is
1194
+ // the difference between a projection and a guess.
1195
+ const unattested = await migrate(['forecast'])
1196
+ expect(unattested.code).toBe(1)
1197
+ expect(unattested.err).toContain('forecast-assumptions.md')
1198
+
1199
+ await writeFile(
1200
+ join(storeDir, 'forecast-assumptions.md'),
1201
+ [
1202
+ '---',
1203
+ 'attestedBy: e2e',
1204
+ 'attestedDate: 2026-08-13',
1205
+ '---',
1206
+ '',
1207
+ '## Territories',
1208
+ '',
1209
+ '| capability | territory |',
1210
+ '| --- | --- |',
1211
+ ...CAPABILITIES.map((c) => `| ${c.slug} | established |`),
1212
+ '',
1213
+ '## Multipliers',
1214
+ '',
1215
+ '| territory | multiplier |',
1216
+ '| --- | --- |',
1217
+ '| established | 1.0 |',
1218
+ '',
1219
+ '## Scenarios',
1220
+ '',
1221
+ '| label | rate | streams | tax | note |',
1222
+ '| --- | --- | --- | --- | --- |',
1223
+ '| steady | as-is | 1 | 0 | measured |',
1224
+ '| target | 2 | 1 | 0 | owner target |',
1225
+ '',
1226
+ '## Caveats',
1227
+ '',
1228
+ '- The fixture is not a real campaign.',
1229
+ '',
1230
+ ].join('\n'),
1231
+ )
1232
+
1233
+ const forecast = await migrate(['forecast'])
1234
+ expect(forecast.code).toBe(0)
1235
+ expect(forecast.out).toContain('attested by e2e on 2026-08-13')
1236
+ // Two dated completions is exactly the minimum, so the measured rows
1237
+ // project; the target row is labelled as owner-attested either way.
1238
+ expect(forecast.out).toContain('target (owner-attested, nothing measures this)')
1239
+ expect(forecast.out).toContain('The fixture is not a real campaign.')
1240
+
1241
+ // 20. The acceptance proof is load-bearing, shown by mutation: reopening one
1242
+ // queue item must break the plain check that step 17 asserted, and only on
1243
+ // the adjudication gate.
1244
+ const reopenPath = join(storeDir, 'queue', `${QUEUE_ID}.md`)
1245
+ const ruledText = await readFile(reopenPath, 'utf8')
1246
+ await writeFile(reopenPath, ruledText.replace('status: adjudicated', 'status: open'))
1247
+ const reopened = await migrate(['check'])
1248
+ expect(reopened.code).toBe(1)
1249
+ expect(reopened.out).toContain(' adjudication:')
1250
+ expect(reopened.out).toContain(QUEUE_ID)
1251
+ await writeFile(reopenPath, ruledText)
1252
+ expect((await migrate(['check'])).code).toBe(0)
1093
1253
  })
@@ -1208,12 +1208,13 @@ test('aspnet recipe run driven probe through queue, ending green at check --phas
1208
1208
  expect(green.out).not.toContain('Violations')
1209
1209
  expect(green.code).toBe(0)
1210
1210
 
1211
- // 13. Plain `migrate check` gates every phase through handoff, and fails on
1212
- // exactly the two that have no verb in this milestone. The violation count
1213
- // is what makes "exactly" an assertion rather than a hope.
1211
+ // 13. Plain `migrate check` gates every phase through handoff. A run that
1212
+ // stops at the queue fails it on three distinct fronts, and naming each one
1213
+ // is what makes this an assertion rather than a hope: the two phases still
1214
+ // pending, every queue item nobody has ruled on, and the handoff that never
1215
+ // emitted anything.
1214
1216
  const full = await migrate(['check'])
1215
1217
  expect(full.code).toBe(1)
1216
- expect(full.out).toContain('Violations (2):')
1217
1218
  expect(full.out).toContain(' run-state:')
1218
1219
  expect(full.out).toContain(
1219
1220
  ' phase adjudicate is pending; every phase through handoff must be done',
@@ -1221,6 +1222,10 @@ test('aspnet recipe run driven probe through queue, ending green at check --phas
1221
1222
  expect(full.out).toContain(
1222
1223
  ' phase handoff is pending; every phase through handoff must be done',
1223
1224
  )
1225
+ expect(full.out).toContain(' adjudication:')
1226
+ expect(full.out).toContain('is still open; every queue item needs a ruling before handoff')
1227
+ expect(full.out).toContain(' handoff:')
1228
+ expect(full.out).toContain('no handoff.json in the store')
1224
1229
 
1225
1230
  // 14. The terminus assertion in step 12 is load-bearing, shown by mutation
1226
1231
  // rather than asserted: nulling every requirement's parity plan is a phase-4