arkgate 4.8.19 → 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 (85) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/README.md +107 -117
  3. package/bin/ark-check-runtime.mjs +26 -4
  4. package/bin/ark-layer-match.mjs +236 -12
  5. package/bin/ark.mjs +6 -2
  6. package/bin/lib/analysis-engine.mjs +13 -6
  7. package/bin/lib/config-contract.mjs +6 -0
  8. package/bin/lib/diagnostic-catalog.mjs +1 -0
  9. package/bin/lib/doctor-advisories.mjs +22 -1
  10. package/bin/lib/doctor-human.mjs +11 -9
  11. package/bin/lib/doctor-plan.mjs +17 -14
  12. package/bin/lib/enforcement-honesty.mjs +30 -2
  13. package/bin/lib/first-run-help.mjs +7 -1
  14. package/bin/lib/html-report-advisories.mjs +8 -0
  15. package/bin/lib/html-report-depth.mjs +12 -7
  16. package/bin/lib/invariant-coverage.mjs +406 -54
  17. package/bin/lib/managed-upgrade.mjs +59 -14
  18. package/bin/lib/package-manager.mjs +5 -2
  19. package/bin/lib/physical-cohesion.mjs +68 -51
  20. package/bin/lib/pilot-loop.mjs +183 -37
  21. package/bin/lib/reshape-decisions.mjs +2 -2
  22. package/bin/lib/rules-under-contract.mjs +40 -4
  23. package/bin/lib/start-install-recovery.mjs +156 -0
  24. package/bin/lib/start-preview.mjs +0 -54
  25. package/bin/lib/upgrade-behavior-changes.mjs +83 -0
  26. package/bin/lib/upgrade-command.mjs +69 -31
  27. package/bin/lib/upgrade-outcome.mjs +359 -0
  28. package/bin/lib/violations.mjs +109 -0
  29. package/dist/{configTypes-VD0qcubY.d.ts → configTypes-CTELo87-.d.ts} +9 -0
  30. package/dist/{diagnosticCatalog-D2v0ASN2.d.ts → diagnosticCatalog-B7BXlet6.d.ts} +79 -5
  31. package/dist/eslint/index.cjs +4 -4
  32. package/dist/eslint/index.d.ts +35 -4
  33. package/dist/eslint/index.js +4 -4
  34. package/dist/index.cjs +25 -18
  35. package/dist/index.d.ts +13 -4
  36. package/dist/index.js +27 -20
  37. package/dist/nestjs/index.cjs +9 -9
  38. package/dist/nestjs/index.d.ts +3 -3
  39. package/dist/nestjs/index.js +9 -9
  40. package/dist/runtime/index.cjs +18 -11
  41. package/dist/runtime/index.d.ts +6 -6
  42. package/dist/runtime/index.js +18 -11
  43. package/dist/{types-D5GT5ZT8.d.ts → types-fQ9-KrBj.d.ts} +1 -1
  44. package/dist/{types-BSzRy2X1.d.ts → types-yDNEQ603.d.ts} +1 -1
  45. package/docs/README.md +4 -3
  46. package/docs/agent-guide.md +15 -7
  47. package/docs/brownfield-adoption.md +4 -3
  48. package/docs/configuration.md +46 -8
  49. package/docs/develop.md +1 -1
  50. package/docs/diagnostics.md +12 -2
  51. package/docs/package-surface.md +4 -3
  52. package/docs/product-voice.md +11 -3
  53. package/docs/use.md +1 -1
  54. package/package.json +3 -3
  55. package/schemas/ark.config.schema.json +14 -0
  56. package/server.json +2 -2
  57. package/templates/agent-skills/README.md +1 -1
  58. package/templates/agent-skills/ark-adopt/SKILL.md +18 -1
  59. package/templates/agent-skills/ark-architect/SKILL.md +2 -0
  60. package/templates/agent-skills/ark-autopilot/SKILL.md +6 -2
  61. package/templates/agent-skills/ark-contract/SKILL.md +2 -0
  62. package/templates/agent-skills/ark-coverage/SKILL.md +3 -1
  63. package/templates/agent-skills/ark-explain/SKILL.md +3 -1
  64. package/templates/agent-skills/ark-explore/SKILL.md +15 -1
  65. package/templates/agent-skills/ark-fix/SKILL.md +2 -0
  66. package/templates/agent-skills/ark-loop/SKILL.md +2 -0
  67. package/templates/agent-skills/ark-order/SKILL.md +2 -0
  68. package/templates/agent-skills/ark-place/SKILL.md +2 -0
  69. package/templates/agent-skills/ark-runtime/SKILL.md +2 -0
  70. package/templates/agent-skills/ark-think/SKILL.md +2 -0
  71. package/templates/agent-skills/ark-upgrade/SKILL.md +25 -10
  72. package/templates/skills/ark-adopt.md +18 -1
  73. package/templates/skills/ark-architect.md +2 -0
  74. package/templates/skills/ark-autopilot.md +6 -2
  75. package/templates/skills/ark-contract.md +2 -0
  76. package/templates/skills/ark-coverage.md +3 -1
  77. package/templates/skills/ark-explain.md +3 -1
  78. package/templates/skills/ark-explore.md +15 -1
  79. package/templates/skills/ark-fix.md +2 -0
  80. package/templates/skills/ark-loop.md +2 -0
  81. package/templates/skills/ark-order.md +2 -0
  82. package/templates/skills/ark-place.md +2 -0
  83. package/templates/skills/ark-runtime.md +2 -0
  84. package/templates/skills/ark-think.md +2 -0
  85. package/templates/skills/ark-upgrade.md +25 -10
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Classified next steps when start / start --apply package install fails.
3
+ *
4
+ * Callers render `formatStartPackageInstallFailure`. Classification, Age window,
5
+ * pin-aware npx, and AgeExclude-must-be-in-config stay behind this module.
6
+ */
7
+ import { spawnSync } from 'node:child_process';
8
+ import { arkPackageRecoveryCommand } from './package-manager.mjs';
9
+
10
+ const AGE_MARK = /ERR_PNPM_NO_MATURE_MATCHING_VERSION|minimumReleaseAge/i;
11
+ const AGE_EXCLUDE_LINE =
12
+ 'To add it locally later: put arkgate on minimumReleaseAgeExclude in pnpm config or pnpm-workspace.yaml (a CLI flag is not enough), or wait until the release is old enough.';
13
+ const BIN_NOTE =
14
+ '`arkgate-check` is a command in the arkgate package — not its own npm package.';
15
+
16
+ /**
17
+ * @param {unknown} hostOutput
18
+ * @returns {string[]}
19
+ */
20
+ function hostOutputTail(hostOutput) {
21
+ if (!hostOutput || !String(hostOutput).trim()) return [];
22
+ return String(hostOutput)
23
+ .split('\n')
24
+ .map((line) => line.trimEnd())
25
+ .filter((line) => line.trim())
26
+ .slice(-6);
27
+ }
28
+
29
+ /**
30
+ * Exact npm version for `npx --package=arkgate@<pin>` when Age blocked add.
31
+ * @param {string|undefined} packageVersion
32
+ * @param {string|undefined} installCommand
33
+ * @returns {string|null}
34
+ */
35
+ export function resolveStartInstallPackagePin(packageVersion, installCommand) {
36
+ const fromArg = String(packageVersion ?? '')
37
+ .trim()
38
+ .replace(/^arkgate@/i, '')
39
+ .replace(/^\^/, '')
40
+ .replace(/^v/i, '');
41
+ if (/^\d+\.\d+\.\d+/.test(fromArg)) return fromArg.match(/^\d+\.\d+\.\d+/)?.[0] ?? null;
42
+ const fromCmd = String(installCommand ?? '').match(/arkgate@\^?v?(\d+\.\d+\.\d+)/i);
43
+ return fromCmd?.[1] ?? null;
44
+ }
45
+
46
+ /**
47
+ * @typedef {{ kind: 'pnpm-age', ageWindow: string, headline: string }} PnpmAgeInstallFailure
48
+ * @typedef {{ kind: 'generic', hostTail: string[] }} GenericInstallFailure
49
+ * @typedef {PnpmAgeInstallFailure | GenericInstallFailure} ClassifiedStartInstallFailure
50
+ */
51
+
52
+ /**
53
+ * Classify captured package-manager stdout+stderr. Age is one class.
54
+ * @param {unknown} hostOutput
55
+ * @returns {ClassifiedStartInstallFailure}
56
+ */
57
+ export function classifyStartInstallFailure(hostOutput) {
58
+ const text = String(hostOutput ?? '');
59
+ if (AGE_MARK.test(text)) {
60
+ const days = text.match(/(\d+)\s*days?/i)?.[1];
61
+ const ageWindow = days ? `${days} days` : 'a cooling-off window';
62
+ const headline = days
63
+ ? `This repo waits before trusting new npm packages (pnpm Age — ${days} days).`
64
+ : 'This repo waits before trusting new npm packages (pnpm Age).';
65
+ return { kind: 'pnpm-age', ageWindow, headline };
66
+ }
67
+ return { kind: 'generic', hostTail: hostOutputTail(text) };
68
+ }
69
+
70
+ /**
71
+ * Plain Age headline, or null. Kept for the #268 one-liner tests.
72
+ * @param {unknown} hostOutput
73
+ * @returns {string|null}
74
+ */
75
+ export function explainPnpmMaturityBlock(hostOutput) {
76
+ const classified = classifyStartInstallFailure(hostOutput);
77
+ return classified.kind === 'pnpm-age' ? classified.headline : null;
78
+ }
79
+
80
+ /**
81
+ * @typedef {{
82
+ * kind: 'pnpm-age' | 'generic',
83
+ * ageWindow?: string,
84
+ * primaryCommand: string,
85
+ * replayInstallCommand: string | null,
86
+ * lines: string[],
87
+ * }} StartInstallRecovery
88
+ */
89
+
90
+ /**
91
+ * Typed next-step for a failed start package install.
92
+ * Age never replays the doomed add; generic still names the exact command.
93
+ * @param {{
94
+ * exitStatus: number,
95
+ * installCommand: string,
96
+ * hostOutput?: string,
97
+ * packageVersion?: string,
98
+ * }} input
99
+ * @returns {StartInstallRecovery}
100
+ */
101
+ export function startInstallRecovery(input) {
102
+ const exitStatus = input?.exitStatus ?? 1;
103
+ const installCommand = input?.installCommand ?? '';
104
+ const classified = classifyStartInstallFailure(input?.hostOutput);
105
+ const pin = resolveStartInstallPackagePin(input?.packageVersion, installCommand);
106
+ const honesty = `Package install failed (exit ${exitStatus}). Setup files are written; the local command is not installed yet.`;
107
+ if (classified.kind === 'pnpm-age') {
108
+ const doctor = arkPackageRecoveryCommand(
109
+ 'arkgate-check',
110
+ '--doctor',
111
+ pin ? `arkgate@${pin}` : undefined
112
+ );
113
+ return {
114
+ kind: 'pnpm-age',
115
+ ageWindow: classified.ageWindow,
116
+ primaryCommand: doctor,
117
+ replayInstallCommand: null,
118
+ lines: [classified.headline, honesty, ` ${doctor}`, BIN_NOTE, AGE_EXCLUDE_LINE],
119
+ };
120
+ }
121
+ const doctor = arkPackageRecoveryCommand('arkgate-check', '--doctor');
122
+ return {
123
+ kind: 'generic',
124
+ primaryCommand: installCommand,
125
+ replayInstallCommand: installCommand,
126
+ lines: [...classified.hostTail, honesty, ` ${installCommand}`, ` ${doctor}`, BIN_NOTE],
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Red next-step when post-apply package install fails.
132
+ * Host files may already be written; local bins may be missing.
133
+ * @param {{
134
+ * exitStatus: number,
135
+ * installCommand: string,
136
+ * hostOutput?: string,
137
+ * packageVersion?: string,
138
+ * }} input
139
+ */
140
+ export function formatStartPackageInstallFailure(input) {
141
+ return startInstallRecovery(input).lines.join('\n');
142
+ }
143
+
144
+ /** Pipe the package manager so recovery can hide maturity internals. */
145
+ export function runStartPackageInstall(command, commandArgs, cwd) {
146
+ const result = spawnSync(command, commandArgs, {
147
+ cwd,
148
+ stdio: ['ignore', 'pipe', 'pipe'],
149
+ encoding: 'utf8',
150
+ });
151
+ return {
152
+ status: result.status ?? 1,
153
+ stdout: result.stdout ?? '',
154
+ stderr: result.stderr ?? '',
155
+ };
156
+ }
@@ -5,7 +5,6 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import {
7
7
  arkCommand,
8
- arkPackageRecoveryCommand,
9
8
  buildArchitectureRecommendation,
10
9
  START_APPLY_REFUSE_FOOTER,
11
10
  } from '../ark-shared.mjs';
@@ -218,59 +217,6 @@ function commands(root, args, helpers) {
218
217
  return result;
219
218
  }
220
219
 
221
- /**
222
- * Plain one-liner for pnpm minimumReleaseAge / ERR_PNPM_NO_MATURE_MATCHING_VERSION.
223
- * Host dumps stay out of the recovery — stranger needs one next step.
224
- */
225
- export function explainPnpmMaturityBlock(hostOutput) {
226
- const text = String(hostOutput ?? '');
227
- if (!/ERR_PNPM_NO_MATURE_MATCHING_VERSION|minimumReleaseAge/i.test(text)) return null;
228
- const days = text.match(/(\d+)\s*days?/i)?.[1];
229
- const window = days ? `${days} days` : 'a cooling-off window';
230
- return `This repo blocks packages younger than ${window} — pin an older release or exclude arkgate temporarily.`;
231
- }
232
-
233
- /** Pipe the package manager so recovery can hide maturity internals. */
234
- export function runStartPackageInstall(command, commandArgs, cwd) {
235
- const result = spawnSync(command, commandArgs, {
236
- cwd,
237
- stdio: ['ignore', 'pipe', 'pipe'],
238
- encoding: 'utf8',
239
- });
240
- return {
241
- status: result.status ?? 1,
242
- stdout: result.stdout ?? '',
243
- stderr: result.stderr ?? '',
244
- };
245
- }
246
-
247
- /**
248
- * Red next-step when post-apply package install fails.
249
- * Host files may already be written; local bins may be missing.
250
- */
251
- export function formatStartPackageInstallFailure({ exitStatus, installCommand, hostOutput }) {
252
- const doctor = arkPackageRecoveryCommand('arkgate-check', '--doctor');
253
- const maturity = explainPnpmMaturityBlock(hostOutput);
254
- const lines = [];
255
- if (maturity) {
256
- lines.push(maturity);
257
- } else if (hostOutput && String(hostOutput).trim()) {
258
- const tail = String(hostOutput)
259
- .split('\n')
260
- .map((line) => line.trimEnd())
261
- .filter((line) => line.trim())
262
- .slice(-6);
263
- if (tail.length) lines.push(...tail);
264
- }
265
- lines.push(
266
- `Package install failed (exit ${exitStatus}). Setup files are written; the local command is not installed yet.`,
267
- ` ${installCommand}`,
268
- ` ${doctor}`,
269
- '`arkgate-check` is a command in the arkgate package — not its own npm package.'
270
- );
271
- return lines.join('\n');
272
- }
273
-
274
220
  /**
275
221
  * @param {object} preview
276
222
  * @param {{ applying?: boolean }} [options] when applying=true, do not claim “no files were changed”
@@ -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
  }