arkgate 4.8.20 → 4.8.21

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 (75) hide show
  1. package/CHANGELOG.md +124 -5
  2. package/README.md +107 -117
  3. package/bin/ark-check-runtime.mjs +3 -4
  4. package/bin/ark-layer-match.mjs +110 -18
  5. package/bin/lib/analysis-engine.mjs +13 -6
  6. package/bin/lib/config-contract.mjs +5 -0
  7. package/bin/lib/diagnostic-catalog.mjs +1 -0
  8. package/bin/lib/doctor-advisories.mjs +20 -0
  9. package/bin/lib/doctor-human.mjs +11 -9
  10. package/bin/lib/doctor-plan.mjs +15 -14
  11. package/bin/lib/first-run-help.mjs +7 -1
  12. package/bin/lib/html-report-advisories.mjs +8 -0
  13. package/bin/lib/html-report-depth.mjs +9 -8
  14. package/bin/lib/invariant-coverage.mjs +396 -75
  15. package/bin/lib/managed-upgrade.mjs +59 -14
  16. package/bin/lib/pilot-loop.mjs +183 -37
  17. package/bin/lib/upgrade-behavior-changes.mjs +83 -0
  18. package/bin/lib/upgrade-command.mjs +69 -31
  19. package/bin/lib/upgrade-outcome.mjs +359 -0
  20. package/bin/lib/violations.mjs +109 -0
  21. package/dist/{configTypes-VVknqcTJ.d.ts → configTypes-CTELo87-.d.ts} +7 -0
  22. package/dist/{diagnosticCatalog-BPYWNyJR.d.ts → diagnosticCatalog-B7BXlet6.d.ts} +73 -5
  23. package/dist/eslint/index.cjs +4 -4
  24. package/dist/eslint/index.d.ts +26 -3
  25. package/dist/eslint/index.js +4 -4
  26. package/dist/index.cjs +24 -17
  27. package/dist/index.d.ts +9 -4
  28. package/dist/index.js +25 -18
  29. package/dist/nestjs/index.cjs +9 -9
  30. package/dist/nestjs/index.d.ts +3 -3
  31. package/dist/nestjs/index.js +9 -9
  32. package/dist/runtime/index.cjs +18 -11
  33. package/dist/runtime/index.d.ts +6 -6
  34. package/dist/runtime/index.js +18 -11
  35. package/dist/{types-p_fCp6P-.d.ts → types-fQ9-KrBj.d.ts} +1 -1
  36. package/dist/{types-Cs-46DRu.d.ts → types-yDNEQ603.d.ts} +1 -1
  37. package/docs/README.md +4 -3
  38. package/docs/agent-guide.md +15 -7
  39. package/docs/brownfield-adoption.md +4 -3
  40. package/docs/configuration.md +34 -11
  41. package/docs/diagnostics.md +12 -2
  42. package/docs/package-surface.md +3 -2
  43. package/docs/product-voice.md +11 -3
  44. package/package.json +1 -1
  45. package/schemas/ark.config.schema.json +8 -0
  46. package/server.json +2 -2
  47. package/templates/agent-skills/README.md +1 -1
  48. package/templates/agent-skills/ark-adopt/SKILL.md +5 -2
  49. package/templates/agent-skills/ark-architect/SKILL.md +2 -0
  50. package/templates/agent-skills/ark-autopilot/SKILL.md +6 -4
  51. package/templates/agent-skills/ark-contract/SKILL.md +2 -0
  52. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  53. package/templates/agent-skills/ark-explain/SKILL.md +2 -0
  54. package/templates/agent-skills/ark-explore/SKILL.md +4 -2
  55. package/templates/agent-skills/ark-fix/SKILL.md +2 -0
  56. package/templates/agent-skills/ark-loop/SKILL.md +2 -0
  57. package/templates/agent-skills/ark-order/SKILL.md +2 -0
  58. package/templates/agent-skills/ark-place/SKILL.md +2 -0
  59. package/templates/agent-skills/ark-runtime/SKILL.md +2 -0
  60. package/templates/agent-skills/ark-think/SKILL.md +2 -0
  61. package/templates/agent-skills/ark-upgrade/SKILL.md +20 -7
  62. package/templates/skills/ark-adopt.md +5 -2
  63. package/templates/skills/ark-architect.md +2 -0
  64. package/templates/skills/ark-autopilot.md +6 -4
  65. package/templates/skills/ark-contract.md +2 -0
  66. package/templates/skills/ark-coverage.md +2 -0
  67. package/templates/skills/ark-explain.md +2 -0
  68. package/templates/skills/ark-explore.md +4 -2
  69. package/templates/skills/ark-fix.md +2 -0
  70. package/templates/skills/ark-loop.md +2 -0
  71. package/templates/skills/ark-order.md +2 -0
  72. package/templates/skills/ark-place.md +2 -0
  73. package/templates/skills/ark-runtime.md +2 -0
  74. package/templates/skills/ark-think.md +2 -0
  75. package/templates/skills/ark-upgrade.md +20 -7
@@ -120,35 +120,39 @@ export function extractionCardFromBet(bet, preferredFiles) {
120
120
  }
121
121
 
122
122
  /**
123
- * Select **one** next pilot from pattern bets (or build bets from smells).
123
+ * Viable pattern bets, best pilot first (same rank as the historical selector).
124
+ * Mechanical-safe claims and non-production god-module paths are dropped.
124
125
  * @param {object[] | null | undefined} patternBets
125
- * @param {{ designSmells?: object[] }} [options]
126
- * @returns {null | ReturnType<typeof extractionCardFromBet>}
126
+ * @param {object[] | null | undefined} [designSmells]
127
127
  */
128
- export function selectNextPilot(patternBets, options = {}) {
128
+ function rankedPatternBets(patternBets, designSmells) {
129
129
  let bets = Array.isArray(patternBets) ? [...patternBets] : [];
130
- if (bets.length === 0 && Array.isArray(options.designSmells) && options.designSmells.length) {
131
- bets = buildPatternBetsFromSmells(options.designSmells);
130
+ if (bets.length === 0 && Array.isArray(designSmells) && designSmells.length) {
131
+ bets = buildPatternBetsFromSmells(designSmells);
132
132
  }
133
- if (bets.length === 0) return null;
134
-
135
- let best = null;
136
- let bestScore = -Infinity;
133
+ const ranked = [];
137
134
  for (let i = 0; i < bets.length; i++) {
138
135
  const bet = bets[i];
139
136
  if (!bet || bet.neverMechanicalSafe === false) continue;
140
- // Skip anything that claims mechanical-safe (honesty).
141
137
  if (bet.class === 'mechanical-safe') continue;
142
138
  const files = pilotFilesForBet(bet);
143
139
  if (files === null) continue;
144
- const sc = scoreBet(bet, i, files);
145
- if (sc > bestScore) {
146
- bestScore = sc;
147
- best = { bet, files };
148
- }
140
+ ranked.push({ bet, files, score: scoreBet(bet, i, files) });
149
141
  }
150
- if (!best) return null;
151
- return extractionCardFromBet(best.bet, best.files);
142
+ ranked.sort((a, b) => b.score - a.score);
143
+ return ranked;
144
+ }
145
+
146
+ /**
147
+ * Select **one** next pilot from pattern bets (or build bets from smells).
148
+ * @param {object[] | null | undefined} patternBets
149
+ * @param {{ designSmells?: object[] }} [options]
150
+ * @returns {null | ReturnType<typeof extractionCardFromBet>}
151
+ */
152
+ export function selectNextPilot(patternBets, options = {}) {
153
+ const ranked = rankedPatternBets(patternBets, options.designSmells);
154
+ if (ranked.length === 0) return null;
155
+ return extractionCardFromBet(ranked[0].bet, ranked[0].files);
152
156
  }
153
157
 
154
158
  /**
@@ -174,15 +178,159 @@ export function formatExtractionCard(card) {
174
178
  }
175
179
 
176
180
  /**
177
- * Doctor/plan JSON summary of the active pilot loop step.
181
+ * One proposed pilot. The loop does not know designWeak or which sensor
182
+ * produced the card — only this list.
183
+ *
184
+ * @typedef {'pattern-bet' | 'reshape'} PilotSource
185
+ * @typedef {{
186
+ * source: PilotSource,
187
+ * target: string,
188
+ * move: string,
189
+ * moveSample: Array<string | { from: string, to: string }>,
190
+ * successSignal: string,
191
+ * killSwitch: string,
192
+ * bet?: object,
193
+ * files?: string[],
194
+ * doNot?: string[],
195
+ * }} PilotCandidate
196
+ */
197
+
198
+ /**
199
+ * @param {object} bet
200
+ * @param {string[]} files
201
+ * @returns {PilotCandidate}
202
+ */
203
+ function candidateFromBet(bet, files) {
204
+ const target =
205
+ (files && files[0]) ||
206
+ (typeof bet.pilot === 'string' ? bet.pilot : null) ||
207
+ 'src/**';
208
+ return {
209
+ source: 'pattern-bet',
210
+ target,
211
+ move:
212
+ typeof bet.fix === 'string' && bet.fix.trim()
213
+ ? bet.fix.trim()
214
+ : 'Apply one bounded extraction for this smell on pilot paths only',
215
+ moveSample: files || [],
216
+ successSignal:
217
+ typeof bet.successSignal === 'string'
218
+ ? bet.successSignal
219
+ : 'Smell evidence paths cleared on pilot without weakening the contract',
220
+ killSwitch:
221
+ typeof bet.killSwitch === 'string'
222
+ ? bet.killSwitch
223
+ : 'If pilot increases edge violations without design clarity, stop and re-map with /ark-explore',
224
+ bet,
225
+ files,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * @param {PilotCandidate} candidate
231
+ * @returns {null | ReturnType<typeof extractionCardFromBet>}
232
+ */
233
+ function extractionCardFromCandidate(candidate) {
234
+ if (!candidate || typeof candidate.target !== 'string' || candidate.target.length === 0) {
235
+ return null;
236
+ }
237
+ if (candidate.source === 'pattern-bet' && candidate.bet) {
238
+ return extractionCardFromBet(candidate.bet, candidate.files);
239
+ }
240
+ const sample = Array.isArray(candidate.moveSample) ? candidate.moveSample : [];
241
+ const evidence = sample
242
+ .map((entry) => (typeof entry === 'string' ? entry : entry?.from))
243
+ .filter((entry) => typeof entry === 'string' && entry.length > 0)
244
+ .slice(0, 8);
245
+ const reshape = candidate.source === 'reshape';
246
+ return {
247
+ id: PILOT_LOOP_ID,
248
+ patternBetId: reshape ? `reshape:${candidate.target}` : `pattern-b:${candidate.target}`,
249
+ smellId: reshape ? 'physical-cohesion' : 'unknown',
250
+ pilot: candidate.target,
251
+ pilotTarget: candidate.target,
252
+ evidence,
253
+ move: candidate.move,
254
+ doNot:
255
+ Array.isArray(candidate.doNot) && candidate.doNot.length
256
+ ? [...candidate.doNot]
257
+ : [...DEFAULT_DO_NOT],
258
+ successSignal: candidate.successSignal,
259
+ killSwitch: candidate.killSwitch,
260
+ neverMechanicalSafe: true,
261
+ class: 'judgment',
262
+ loopStep: 'one-pilot',
263
+ reDoctor: 'ark-check --doctor --json',
264
+ rePlan: 'ark-check --plan --json',
265
+ next: reshape
266
+ ? '/ark-loop (one reshape pilot) | re-doctor after pilot'
267
+ : '/ark-autopilot (one cluster / one B pilot) | re-doctor after pilot',
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Normalize design-weak pattern bets and a proposed reshape pilot into one list.
273
+ * Pattern bets stay in selector rank (best first) and are omitted unless
274
+ * design fitness is design-weak, so that path keeps today's card. A reshape
275
+ * card is appended when physical cohesion proposes one. Doctor builds this
276
+ * list after advisories, so the loop never depends on call order.
277
+ *
178
278
  * @param {{
179
279
  * designWeak?: boolean,
180
280
  * patternBets?: object[],
181
281
  * designSmells?: object[],
182
- * }} opts
282
+ * }} [designFitness]
283
+ * @param {{
284
+ * physicalCohesion?: {
285
+ * reshapePilot?: { proposed?: boolean, nextPilot?: object | null },
286
+ * },
287
+ * }} [advisories]
288
+ * @returns {PilotCandidate[]}
183
289
  */
184
- export function summarizePilotLoop(opts = {}) {
185
- const designWeak = opts.designWeak === true;
290
+ export function collectPilotCandidates(designFitness = {}, advisories = {}) {
291
+ /** @type {PilotCandidate[]} */
292
+ const candidates = [];
293
+ if (designFitness?.designWeak === true) {
294
+ for (const ranked of rankedPatternBets(designFitness.patternBets, designFitness.designSmells)) {
295
+ candidates.push(candidateFromBet(ranked.bet, ranked.files));
296
+ }
297
+ }
298
+ const reshape = advisories?.physicalCohesion?.reshapePilot;
299
+ const card = reshape?.proposed === true ? reshape.nextPilot : null;
300
+ if (card && typeof card.pilotTarget === 'string' && card.pilotTarget.length > 0) {
301
+ candidates.push({
302
+ source: 'reshape',
303
+ target: card.pilotTarget,
304
+ move:
305
+ typeof card.move === 'string' && card.move.trim()
306
+ ? card.move.trim()
307
+ : 'Consolidate one anchor only, then re-doctor before the next card',
308
+ moveSample: Array.isArray(card.moveSample) ? card.moveSample : [],
309
+ successSignal:
310
+ typeof card.successSignal === 'string'
311
+ ? card.successSignal
312
+ : 're-run doctor: the cluster count drops, layer and slice id unchanged, card still proposed',
313
+ killSwitch:
314
+ typeof card.killSwitch === 'string'
315
+ ? card.killSwitch
316
+ : 'revert this move set; nothing else was touched',
317
+ ...(Array.isArray(card.doNot) ? { doNot: card.doNot } : {}),
318
+ });
319
+ }
320
+ return candidates;
321
+ }
322
+
323
+ /**
324
+ * Doctor/plan JSON summary. Active iff `candidates.length > 0`.
325
+ * Activates exactly one candidate (ADR 0010 one-pilot rule). The rest stay
326
+ * queued. No designWeak parameter.
327
+ *
328
+ * @param {PilotCandidate[]} [candidates]
329
+ */
330
+ export function summarizePilotLoop(candidates = []) {
331
+ const list = (Array.isArray(candidates) ? candidates : []).filter(
332
+ (candidate) => candidate && typeof candidate.target === 'string' && candidate.target.length > 0
333
+ );
186
334
  // Same forbid bits as DESIGN_WEAK_HONESTY_FLAGS (both auto-apply aliases).
187
335
  const forbid = {
188
336
  multiPilotBatchForbidden: true,
@@ -190,55 +338,53 @@ export function summarizePilotLoop(opts = {}) {
190
338
  autoApplyPlanBForbidden: true,
191
339
  };
192
340
 
193
- if (!designWeak) {
341
+ if (list.length === 0) {
194
342
  return {
195
343
  active: false,
196
344
  id: PILOT_LOOP_ID,
197
- reason: 'not-design-weak',
345
+ reason: 'no-pilot-candidates',
198
346
  oneAtATime: true,
199
347
  neverMechanicalSafe: true,
200
348
  ...forbid,
201
349
  };
202
350
  }
203
351
 
204
- const nextPilot = selectNextPilot(opts.patternBets, {
205
- designSmells: opts.designSmells,
206
- });
207
- if (!nextPilot) {
352
+ // One pilot, not the list. Later candidates wait for a re-doctor.
353
+ const extractionCard = extractionCardFromCandidate(list[0]);
354
+ if (!extractionCard) {
208
355
  return {
209
356
  active: false,
210
357
  id: PILOT_LOOP_ID,
211
- reason: 'no-pattern-bets',
358
+ reason: 'no-pilot-candidates',
212
359
  oneAtATime: true,
213
360
  neverMechanicalSafe: true,
214
361
  ...forbid,
215
362
  };
216
363
  }
217
364
 
218
- const remaining = Array.isArray(opts.patternBets) ? opts.patternBets.length : 0;
219
- const queued = Math.max(0, remaining - 1);
220
-
365
+ const queued = list.length - 1;
221
366
  return {
222
367
  active: true,
223
368
  id: PILOT_LOOP_ID,
224
369
  oneAtATime: true,
225
370
  neverMechanicalSafe: true,
226
371
  ...forbid,
227
- remainingBets: remaining,
228
- // Remaining pattern bets are a queue, never concurrent pilots.
372
+ source: list[0].source,
373
+ remainingBets: list.length,
229
374
  queuedBets: queued,
230
375
  ...(queued > 0
231
376
  ? {
232
- queueNote: `${queued} additional pattern bet(s) stay queued — run the single nextPilot only, then re-doctor before selecting another.`,
377
+ queueNote: `${queued} additional pilot candidate(s) stay queued — run the single nextPilot only, then re-doctor before selecting another.`,
233
378
  }
234
379
  : {}),
235
- nextPilot,
380
+ nextPilot: extractionCard,
381
+ extractionCard,
236
382
  instruction:
237
383
  'Apply ONE pilot from nextPilot (extraction card), then re-doctor. ' +
238
384
  'Do not multi-pilot batch. patternBets never mechanical-safe. ' +
239
385
  'Never silent auto-apply of plan B. ' +
240
386
  'Success = reduced smell evidence on pilot paths; residual outside pilot may remain.',
241
- cardText: formatExtractionCard(nextPilot),
387
+ cardText: formatExtractionCard(extractionCard),
242
388
  };
243
389
  }
244
390
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Per-version upgrade behavior changes.
3
+ *
4
+ * This table is the package source for migration notes. CHANGELOG.md must
5
+ * carry the same sentences (Unreleased / 4.8.21, including the 4.8.20
6
+ * coverage.symbol note). Post-upgrade verification prints these lines so an
7
+ * agent reads them before editing code.
8
+ *
9
+ * Shipped with the package (`bin/lib` is in the published `files` list).
10
+ */
11
+
12
+ /** @typedef {{ version: string, note: string }} UpgradeBehaviorChange */
13
+
14
+ /** @type {readonly UpgradeBehaviorChange[]} */
15
+ export const UPGRADE_BEHAVIOR_CHANGES = Object.freeze([
16
+ Object.freeze({
17
+ version: '4.8.20',
18
+ note: 'coverage.symbol now requires a real declaration (some green repos turn red)',
19
+ }),
20
+ Object.freeze({
21
+ version: '4.8.21',
22
+ note: 'a bare mention of an invariant id no longer counts as coverage (only describe/it titles or declarations, incl. type/interface/enum)',
23
+ }),
24
+ Object.freeze({
25
+ version: '4.8.21',
26
+ note: "upgrade exit code 3 = applied but red. Scripts comparing against 1 must switch to 'non-zero'",
27
+ }),
28
+ ]);
29
+
30
+ /**
31
+ * @param {string} version
32
+ * @returns {number[]}
33
+ */
34
+ function versionParts(version) {
35
+ return String(version)
36
+ .replace(/^v/, '')
37
+ .split('.')
38
+ .map((part) => {
39
+ const match = /^(\d+)/.exec(part);
40
+ return match ? Number(match[1]) : 0;
41
+ });
42
+ }
43
+
44
+ /**
45
+ * @param {string} left
46
+ * @param {string} right
47
+ * @returns {-1|0|1}
48
+ */
49
+ export function compareUpgradeVersions(left, right) {
50
+ const a = versionParts(left);
51
+ const b = versionParts(right);
52
+ const length = Math.max(a.length, b.length);
53
+ for (let index = 0; index < length; index += 1) {
54
+ const delta = (a[index] ?? 0) - (b[index] ?? 0);
55
+ if (delta !== 0) return delta < 0 ? -1 : 1;
56
+ }
57
+ return 0;
58
+ }
59
+
60
+ /**
61
+ * @param {UpgradeBehaviorChange} row
62
+ * @returns {string}
63
+ */
64
+ export function formatBehaviorChange(row) {
65
+ return `${row.version}: ${row.note}`;
66
+ }
67
+
68
+ /**
69
+ * Lines from the shipped table.
70
+ * Omit `upTo` to return every note in this package (the post-upgrade default:
71
+ * the build that implements a note must show it, even before the version bump).
72
+ * Pass `upTo` to keep notes whose version is less than or equal to that release.
73
+ *
74
+ * @param {string} [upTo]
75
+ * @returns {string[]}
76
+ */
77
+ export function behaviorChangeLines(upTo) {
78
+ const rows =
79
+ typeof upTo === 'string' && upTo.trim()
80
+ ? UPGRADE_BEHAVIOR_CHANGES.filter((row) => compareUpgradeVersions(row.version, upTo) <= 0)
81
+ : UPGRADE_BEHAVIOR_CHANGES;
82
+ return rows.map((row) => formatBehaviorChange(row));
83
+ }
@@ -10,6 +10,13 @@ import {
10
10
  formatPackageInstallDecisionHuman,
11
11
  } from '../ark-shared.mjs';
12
12
  import { describePackageVersionDualTruth } from './field-install.mjs';
13
+ import {
14
+ exitCodeFor,
15
+ formatBlockedAssets,
16
+ formatPostUpgradeHuman,
17
+ runPostUpgradeVerification,
18
+ upgradeOutcome,
19
+ } from './upgrade-outcome.mjs';
13
20
  import { __packageRoot } from './gate-files.mjs';
14
21
  import {
15
22
  applyManagedUpgrade,
@@ -316,15 +323,13 @@ export function buildUpgradeNextCommand(args, planDigest) {
316
323
  return arkCommand(args.root, 'arkgate', argsStr);
317
324
  }
318
325
 
319
- function verify(root, json, arkCheck, runArkCheck) {
320
- const args = ['--root', root, '--config', 'ark.config.json', '--strict-merge'];
321
- if (!json) return { exitCode: runArkCheck(args, { cwd: root }) };
322
- const result = spawnSync(process.execPath, [arkCheck, ...args, '--json'], {
323
- cwd: root,
324
- stdio: ['ignore', 'pipe', 'pipe'],
325
- encoding: 'utf8',
326
- });
327
- return { exitCode: result.status ?? 1, stderr: result.stderr?.trim() || undefined };
326
+ function stampBlockedNextCommands(plan, args) {
327
+ if ((plan?.summary?.blocked ?? 0) === 0) return plan;
328
+ const command = buildUpgradeNextCommand({ ...args, acceptConflicts: true }, plan.planDigest);
329
+ for (const asset of plan.assets ?? []) {
330
+ if (asset.blocked) asset.nextCommand = command;
331
+ }
332
+ return plan;
328
333
  }
329
334
 
330
335
  function runProjectLocalCli({ root, script, argv }) {
@@ -544,11 +549,14 @@ export function runUpgradeCommand(args, dependencies) {
544
549
  }).status ?? 1;
545
550
  }
546
551
 
547
- const plan = planManagedUpgrade(root, {
548
- tools: args.tools,
549
- acceptConflicts: args.acceptConflicts,
550
- refreshSkills: args.refreshSkills === true,
551
- });
552
+ const plan = stampBlockedNextCommands(
553
+ planManagedUpgrade(root, {
554
+ tools: args.tools,
555
+ acceptConflicts: args.acceptConflicts,
556
+ refreshSkills: args.refreshSkills === true,
557
+ }),
558
+ args
559
+ );
552
560
  if (!args.apply) {
553
561
  const wouldWrite = plan.summary?.wouldWrite ?? 0;
554
562
  const blocked = plan.summary?.blocked ?? 0;
@@ -577,11 +585,14 @@ export function runUpgradeCommand(args, dependencies) {
577
585
  );
578
586
  } else {
579
587
  renderManagedUpgrade(plan, {
580
- next: needsApply
581
- ? args.install
582
- ? `Update the package and recompute this preview with: ${command}`
583
- : `Apply the exact preview with: ${command}`
584
- : undefined,
588
+ next:
589
+ blocked > 0
590
+ ? undefined
591
+ : needsApply
592
+ ? args.install
593
+ ? `Update the package and recompute this preview with: ${command}`
594
+ : `Apply the exact preview with: ${command}`
595
+ : undefined,
585
596
  skillDrift,
586
597
  hostSelection: hostHonesty,
587
598
  });
@@ -600,54 +611,80 @@ export function runUpgradeCommand(args, dependencies) {
600
611
  } catch (error) {
601
612
  const message = error instanceof Error ? error.message : String(error);
602
613
  console.error(message);
603
- return 2;
614
+ return exitCodeFor(upgradeOutcome({ error }));
604
615
  }
605
616
  if (applied.blocked) {
617
+ stampBlockedNextCommands(applied, args);
606
618
  const command = buildUpgradeNextCommand(
607
619
  { ...args, acceptConflicts: true },
608
620
  applied.planDigest
609
621
  );
622
+ const refusal = formatBlockedAssets(applied, { refused: true }).join('\n');
623
+ console.error(refusal);
610
624
  if (args.json) {
611
625
  console.log(
612
626
  managedUpgradeJson(applied, {
613
627
  blocked: true,
628
+ outcome: 'blocked',
614
629
  reasonCode: applied.reasonCode ?? 'managed-consent-required',
615
630
  nextCommand: command,
616
631
  })
617
632
  );
618
633
  } else {
619
- renderManagedUpgrade(applied, {
620
- next: 'Preview again with --accept-conflicts, then use that preview\'s exact next command.',
621
- });
634
+ renderManagedUpgrade(applied, { refused: true });
622
635
  }
623
- return 1;
636
+ return exitCodeFor(upgradeOutcome({ blocked: true }));
624
637
  }
625
638
  if (applied.nothingToApply && !applied.applied) {
626
- if (args.json) console.log(JSON.stringify(applied, null, 2));
639
+ const idle = upgradeOutcome({ nothingToApply: true, applied: false });
640
+ if (args.json) console.log(JSON.stringify({ ...applied, outcome: idle.kind }, null, 2));
627
641
  else {
628
642
  renderManagedUpgrade(applied);
629
643
  console.log('No managed content writes pending.');
630
644
  }
631
- return 0;
645
+ return exitCodeFor(idle);
632
646
  }
633
- const verification = args.strict
634
- ? { mode: 'strict-merge', ...verify(root, args.json, dependencies.arkCheck, dependencies.runArkCheck) }
635
- : { mode: 'skipped', exitCode: 0 };
647
+ const postUpgrade = runPostUpgradeVerification(root, {
648
+ strict: args.strict,
649
+ arkCheck: dependencies.arkCheck,
650
+ packageVersion: dependencies.cliVersion,
651
+ ...(dependencies.checkResult ? { checkResult: dependencies.checkResult } : {}),
652
+ ...(typeof dependencies.spawnArchitectureCheck === 'function'
653
+ ? { checkResult: dependencies.spawnArchitectureCheck(root) }
654
+ : {}),
655
+ });
656
+ const outcome = upgradeOutcome({ applied: true, postUpgrade });
657
+ const verification = {
658
+ mode: postUpgrade.mode,
659
+ exitCode: postUpgrade.exitCode,
660
+ ...(postUpgrade.stderr ? { stderr: postUpgrade.stderr } : {}),
661
+ };
636
662
  const dualTruth = describePackageVersionDualTruth(root);
637
663
  const skillDrift = buildSkillDriftSummary(applied);
638
664
  const hostHonesty = buildHostSelectionHonesty(applied);
639
665
  // FX05: post-upgrade verification block (advisory, notAScore).
640
666
  const postUpgradeChecks = buildPostUpgradeChecks(root, {
641
667
  cliVersion: dependencies.cliVersion,
642
- verification,
668
+ verification: {
669
+ ...verification,
670
+ verdict: postUpgrade.verdict,
671
+ failing: postUpgrade.failing,
672
+ },
643
673
  dualTruth,
644
674
  });
675
+ const postUpgradePayload = {
676
+ verdict: postUpgrade.verdict,
677
+ failing: postUpgrade.failing,
678
+ behaviorChanges: postUpgrade.behaviorChanges,
679
+ };
645
680
  if (args.json) {
646
681
  console.log(
647
682
  JSON.stringify(
648
683
  {
649
684
  ...applied,
685
+ outcome: outcome.kind,
650
686
  verification,
687
+ postUpgrade: postUpgradePayload,
651
688
  skillDrift,
652
689
  hostSelection: hostHonesty,
653
690
  postUpgradeChecks,
@@ -673,6 +710,7 @@ export function runUpgradeCommand(args, dependencies) {
673
710
  );
674
711
  } else {
675
712
  renderManagedUpgrade(applied, { skillDrift, hostSelection: hostHonesty });
713
+ for (const line of formatPostUpgradeHuman(postUpgrade)) console.log(line);
676
714
  if (!args.strict) console.log('Architecture verification skipped (--no-strict).');
677
715
  if (args.install === false || dualTruth.dualTruth) {
678
716
  console.log(
@@ -690,5 +728,5 @@ export function runUpgradeCommand(args, dependencies) {
690
728
  console.log(` [note] mcp: ${postUpgradeChecks.mcpNote}`);
691
729
  }
692
730
  }
693
- return verification.exitCode;
731
+ return exitCodeFor(outcome);
694
732
  }