arkgate 4.8.18 → 4.8.20

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 (74) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +5 -6
  3. package/bin/ark-check-runtime.mjs +23 -0
  4. package/bin/ark-layer-match.mjs +141 -9
  5. package/bin/ark-mcp-runtime.mjs +3 -3
  6. package/bin/ark-shared.mjs +187 -16
  7. package/bin/ark.mjs +23 -19
  8. package/bin/lib/analysis-engine.mjs +6 -6
  9. package/bin/lib/ci-and-commands.mjs +2 -2
  10. package/bin/lib/config-contract.mjs +1 -0
  11. package/bin/lib/doctor-advisories.mjs +2 -1
  12. package/bin/lib/doctor-human.mjs +2 -0
  13. package/bin/lib/doctor-next-actions.mjs +35 -0
  14. package/bin/lib/doctor-plan.mjs +8 -8
  15. package/bin/lib/enforcement-honesty.mjs +30 -2
  16. package/bin/lib/gate-files.mjs +22 -1
  17. package/bin/lib/host-support-matrix.mjs +1 -1
  18. package/bin/lib/html-report-depth.mjs +6 -2
  19. package/bin/lib/install-migrate.mjs +3 -4
  20. package/bin/lib/invariant-coverage.mjs +44 -13
  21. package/bin/lib/managed-upgrade.mjs +30 -5
  22. package/bin/lib/mcp-adoption.mjs +4 -1
  23. package/bin/lib/package-manager.mjs +5 -2
  24. package/bin/lib/physical-cohesion.mjs +68 -51
  25. package/bin/lib/presets.mjs +14 -14
  26. package/bin/lib/reshape-decisions.mjs +2 -2
  27. package/bin/lib/rules-under-contract.mjs +40 -4
  28. package/bin/lib/scan-files.mjs +3 -26
  29. package/bin/lib/skill-install.mjs +3 -2
  30. package/bin/lib/start-install-recovery.mjs +156 -0
  31. package/bin/lib/start-preview.mjs +17 -16
  32. package/bin/lib/write-path-detect.mjs +3 -2
  33. package/dist/{configTypes-VD0qcubY.d.ts → configTypes-VVknqcTJ.d.ts} +2 -0
  34. package/dist/{diagnosticCatalog-BNxKdcN4.d.ts → diagnosticCatalog-BPYWNyJR.d.ts} +9 -3
  35. package/dist/eslint/index.cjs +4 -4
  36. package/dist/eslint/index.d.ts +12 -4
  37. package/dist/eslint/index.js +4 -4
  38. package/dist/index.cjs +18 -18
  39. package/dist/index.d.ts +8 -4
  40. package/dist/index.js +20 -20
  41. package/dist/nestjs/index.cjs +5 -5
  42. package/dist/nestjs/index.d.ts +3 -3
  43. package/dist/nestjs/index.js +5 -5
  44. package/dist/runtime/index.cjs +10 -10
  45. package/dist/runtime/index.d.ts +6 -6
  46. package/dist/runtime/index.js +10 -10
  47. package/dist/{types-BSzRy2X1.d.ts → types-Cs-46DRu.d.ts} +1 -1
  48. package/dist/{types-D5GT5ZT8.d.ts → types-p_fCp6P-.d.ts} +1 -1
  49. package/docs/README.md +1 -1
  50. package/docs/agent-guide.md +3 -1
  51. package/docs/ai-gates.md +7 -3
  52. package/docs/configuration.md +20 -5
  53. package/docs/develop.md +1 -2
  54. package/docs/package-surface.md +2 -2
  55. package/docs/use.md +1 -2
  56. package/package.json +3 -3
  57. package/schemas/ark.config.schema.json +6 -0
  58. package/server.json +2 -2
  59. package/templates/agent-skills/README.md +1 -1
  60. package/templates/agent-skills/ark-adopt/SKILL.md +15 -1
  61. package/templates/agent-skills/ark-autopilot/SKILL.md +2 -0
  62. package/templates/agent-skills/ark-coverage/SKILL.md +1 -1
  63. package/templates/agent-skills/ark-explain/SKILL.md +1 -1
  64. package/templates/agent-skills/ark-explore/SKILL.md +13 -1
  65. package/templates/agent-skills/ark-place/SKILL.md +2 -0
  66. package/templates/agent-skills/ark-upgrade/SKILL.md +5 -3
  67. package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
  68. package/templates/skills/ark-adopt.md +15 -1
  69. package/templates/skills/ark-autopilot.md +2 -0
  70. package/templates/skills/ark-coverage.md +1 -1
  71. package/templates/skills/ark-explain.md +1 -1
  72. package/templates/skills/ark-explore.md +13 -1
  73. package/templates/skills/ark-place.md +2 -0
  74. package/templates/skills/ark-upgrade.md +5 -3
@@ -875,12 +875,61 @@ const SKIP_DIR_NAMES = new Set([
875
875
  '__pycache__',
876
876
  ]);
877
877
 
878
+ /**
879
+ * Non-product trees — same convention as the governed check walk
880
+ * (`isSkippedSourceDir` in scan-files). Start/recommend/discovery must not
881
+ * treat playground, fixtures, scaffolds, or tests as production architecture.
882
+ */
883
+ export const NON_PRODUCT_SOURCE_DIR_NAMES = new Set([
884
+ 'bench',
885
+ 'benches',
886
+ 'benchmark',
887
+ 'benchmarks',
888
+ 'docs',
889
+ 'documentation',
890
+ 'example',
891
+ 'examples',
892
+ 'fixture',
893
+ 'fixtures',
894
+ 'testdata',
895
+ 'playground',
896
+ 'scaffold',
897
+ 'scaffolds',
898
+ '__tests__',
899
+ '__mocks__',
900
+ 'e2e',
901
+ 'test',
902
+ 'tests',
903
+ ]);
904
+
905
+ export function isNonProductSourceDirName(name) {
906
+ return NON_PRODUCT_SOURCE_DIR_NAMES.has(name);
907
+ }
908
+
909
+ export function isNonProductRelativePath(relPath) {
910
+ return String(relPath)
911
+ .split(/[/\\]/)
912
+ .filter(Boolean)
913
+ .some((token) => NON_PRODUCT_SOURCE_DIR_NAMES.has(token.toLowerCase()));
914
+ }
915
+
916
+ /** Check-walk skip: vendor/build plus non-product fixture trees. */
917
+ export function isSkippedSourceDir(name) {
918
+ return (
919
+ name === 'node_modules' ||
920
+ name === 'dist' ||
921
+ name === 'coverage' ||
922
+ NON_PRODUCT_SOURCE_DIR_NAMES.has(name)
923
+ );
924
+ }
925
+
878
926
  function dirHasTsSources(dir, maxDepth) {
879
927
  try {
880
928
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
881
929
  if (entry.isFile() && /\.(tsx?|jsx?|mts|cts)$/i.test(entry.name)) return true;
882
930
  if (!entry.isDirectory()) continue;
883
931
  if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
932
+ if (isSkippedSourceDir(entry.name)) continue;
884
933
  if (maxDepth > 0 && dirHasTsSources(path.join(dir, entry.name), maxDepth - 1)) return true;
885
934
  }
886
935
  } catch {
@@ -918,8 +967,14 @@ export function detectTsPackageRoots(root, options = {}) {
918
967
  for (const entry of entries) {
919
968
  if (!entry.isDirectory()) continue;
920
969
  if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
921
- // Skip agent skill asset trees (not app code).
922
- if (entry.name === 'skills' || entry.name === 'templates' || entry.name === 'fixtures') continue;
970
+ // Skip agent skill asset trees and non-product fixture/playground/test trees.
971
+ if (
972
+ entry.name === 'skills' ||
973
+ entry.name === 'templates' ||
974
+ isSkippedSourceDir(entry.name)
975
+ ) {
976
+ continue;
977
+ }
923
978
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
924
979
  visit(path.join(abs, entry.name), childRel, depth + 1);
925
980
  }
@@ -944,7 +999,7 @@ export function resolveIncludeRoots(root) {
944
999
  const workspaces = detectWorkspaces(root);
945
1000
  const tsRoots = detectTsPackageRoots(root);
946
1001
  if (workspaces.length === 0) {
947
- if (tsRoots.length > 0) return tsRoots.filter((r) => r !== '.');
1002
+ if (tsRoots.length > 0) return tsRoots.filter((r) => r !== '.' && !isNonProductRelativePath(r));
948
1003
  return [];
949
1004
  }
950
1005
  // Workspaces present: keep them, add TS package roots not covered by a workspace prefix
@@ -960,7 +1015,7 @@ export function resolveIncludeRoots(root) {
960
1015
  if (fs.existsSync(path.join(root, tr, 'package.json'))) merged.add(tr);
961
1016
  }
962
1017
  }
963
- return [...merged];
1018
+ return [...merged].filter((entry) => !isNonProductRelativePath(entry));
964
1019
  }
965
1020
 
966
1021
  function isSourceFile(name) {
@@ -982,18 +1037,96 @@ function readJsonc(file) {
982
1037
 
983
1038
  function packageRole(rel, pkg) {
984
1039
  const tokens = rel.toLowerCase().split('/');
985
- if (tokens.some((token) => ['docs', 'documentation', 'website', 'examples', 'example', 'tests', 'test', 'fixtures'].includes(token))) {
986
- return tokens.some((token) => token.startsWith('doc') || token === 'website')
987
- ? 'docs'
988
- : tokens.some((token) => token.startsWith('test') || token === 'fixtures')
989
- ? 'test'
990
- : 'example';
1040
+ if (tokens.some((token) => ['docs', 'documentation', 'website'].includes(token))) {
1041
+ return 'docs';
1042
+ }
1043
+ if (
1044
+ tokens.some((token) =>
1045
+ [
1046
+ 'tests',
1047
+ 'test',
1048
+ 'fixtures',
1049
+ 'fixture',
1050
+ 'testdata',
1051
+ 'e2e',
1052
+ '__tests__',
1053
+ 'playground',
1054
+ 'scaffold',
1055
+ 'scaffolds',
1056
+ ].includes(token)
1057
+ )
1058
+ ) {
1059
+ return 'test';
1060
+ }
1061
+ if (tokens.some((token) => ['examples', 'example'].includes(token))) {
1062
+ return 'example';
991
1063
  }
992
1064
  if (pkg?.bin) return 'cli';
993
1065
  if (pkg?.exports || pkg?.main || pkg?.module || pkg?.types || pkg?.typings) return 'library';
994
1066
  return 'application';
995
1067
  }
996
1068
 
1069
+ /** Path tokens already treated as UI app folders in discoverRepoUnits (root scan). */
1070
+ export const UI_PACKAGE_PATH_TOKENS = new Set(['web', 'frontend', 'client', 'ui']);
1071
+
1072
+ const UI_PACKAGE_DEP_NAMES =
1073
+ /^(react|react-dom|vue|svelte|preact|solid-js|next|@angular\/core|@sveltejs\/kit)$/i;
1074
+
1075
+ /** True when a relative source path sits under a conventional UI package folder. */
1076
+ export function sourcePathLooksLikeUi(relPath) {
1077
+ return String(relPath ?? '')
1078
+ .toLowerCase()
1079
+ .split(/[/\\]/)
1080
+ .filter((token) => token && token !== '.')
1081
+ .some((token) => UI_PACKAGE_PATH_TOKENS.has(token));
1082
+ }
1083
+
1084
+ /**
1085
+ * Application/CLI unit that is a UI surface (folder name or UI framework deps).
1086
+ * Used so monorepo start does not park `web/**` on ApplicationOrchestration.
1087
+ */
1088
+ export function isUiPackageUnit(unit) {
1089
+ if (!unit || typeof unit !== 'object') return false;
1090
+ if (sourcePathLooksLikeUi(unit.root)) return true;
1091
+ const deps = unit.productionDeps ?? {};
1092
+ return Object.keys(deps).some((name) => UI_PACKAGE_DEP_NAMES.test(String(name).split('/')[0]));
1093
+ }
1094
+
1095
+ /** Source root of an application unit that should land on Presentation, not Application. */
1096
+ export function isUiApplicationSource(unit, sourceRoot) {
1097
+ if (!unit) return sourcePathLooksLikeUi(sourceRoot);
1098
+ const base =
1099
+ unit.root === '.'
1100
+ ? sourceRoot
1101
+ : sourceRoot === '.' || !sourceRoot
1102
+ ? unit.root
1103
+ : `${unit.root}/${sourceRoot}`;
1104
+ return sourcePathLooksLikeUi(base) || isUiPackageUnit(unit);
1105
+ }
1106
+
1107
+ /**
1108
+ * Monorepo start globs for package units by role. `uiMode: 'ui'` keeps only UI
1109
+ * application roots (web/, frontend/, react/vue/svelte); `'not-ui'` drops them
1110
+ * so they are not parked on ApplicationOrchestration (#288).
1111
+ */
1112
+ export function monorepoRoleSourceGlobs(units, roles, { root, domainScoped = false, uiMode } = {}) {
1113
+ if (!root || !Array.isArray(units)) return [];
1114
+ return units
1115
+ .filter((unit) => roles.includes(unit.role) && !(domainScoped && unit.root === '.'))
1116
+ .flatMap((unit) =>
1117
+ (unit.sourceRoots ?? []).map((sourceRoot) => {
1118
+ const base =
1119
+ unit.root === '.' ? sourceRoot : sourceRoot === '.' ? unit.root : `${unit.root}/${sourceRoot}`;
1120
+ if (!base || base === '.') return null;
1121
+ const uiSource = isUiApplicationSource(unit, sourceRoot);
1122
+ if (uiMode === 'ui' && !uiSource) return null;
1123
+ if (uiMode === 'not-ui' && uiSource) return null;
1124
+ return `${base}/**`;
1125
+ })
1126
+ )
1127
+ .filter((pattern) => pattern && (!domainScoped || /^(packages|libs)\//.test(pattern)));
1128
+ }
1129
+
997
1130
  function entrypointDirs(pkg) {
998
1131
  const values = [];
999
1132
  const add = (value) => {
@@ -1025,9 +1158,18 @@ export function discoverRepoUnits(root) {
1025
1158
  for (const name of ['tsconfig.json', 'jsconfig.json']) {
1026
1159
  const config = readJsonc(path.join(abs, name));
1027
1160
  const rootDir = config?.compilerOptions?.rootDir;
1028
- if (typeof rootDir === 'string') roots.add(normalizeRel(rootDir));
1161
+ if (typeof rootDir === 'string' && !/\.(json|jsonc)$/i.test(rootDir)) {
1162
+ const normalizedRootDir = normalizeRel(rootDir);
1163
+ if (!isNonProductRelativePath(normalizedRootDir)) roots.add(normalizedRootDir);
1164
+ }
1029
1165
  for (const ref of config?.references ?? []) {
1030
- if (typeof ref?.path === 'string') roots.add(normalizeRel(ref.path).replace(/^\.\//, ''));
1166
+ if (typeof ref?.path !== 'string') continue;
1167
+ const relRef = normalizeRel(ref.path).replace(/^\.\//, '');
1168
+ // Project references often point at tsconfig.app.json — that is not a source root.
1169
+ if (!relRef || relRef === '.' || /\.(json|jsonc)$/i.test(relRef)) continue;
1170
+ if (isNonProductRelativePath(relRef)) continue;
1171
+ const absRef = path.join(abs, relRef);
1172
+ if (fs.statSync(absRef, { throwIfNoEntry: false })?.isDirectory()) roots.add(relRef);
1031
1173
  }
1032
1174
  }
1033
1175
  for (const dir of entrypointDirs(pkg)) {
@@ -1067,6 +1209,7 @@ function walkSourceFiles(dir, files = [], depth = 0) {
1067
1209
  // `.github/workflows/` CI dir must not read as an app "workflows"/saga signal, and Ark's
1068
1210
  // own installed `.claude`/`.codex` dirs must not perturb a re-run's recommendation.
1069
1211
  if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) continue;
1212
+ if (isSkippedSourceDir(entry.name)) continue;
1070
1213
  walkSourceFiles(path.join(dir, entry.name), files, depth + 1);
1071
1214
  }
1072
1215
  return files;
@@ -1105,6 +1248,7 @@ function dirExistsAnywhere(root, names) {
1105
1248
  // Skip node_modules/dist and ALL dot-dirs so `.github/workflows/` (CI YAML) can't be read
1106
1249
  // as an app "workflows"/saga signal, and Ark's own `.claude`/`.codex` don't self-perturb.
1107
1250
  if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) continue;
1251
+ if (isSkippedSourceDir(entry.name)) continue;
1108
1252
  if (names.has(entry.name)) return true;
1109
1253
  const child = rel === '.' ? entry.name : `${rel}/${entry.name}`;
1110
1254
  if (child.split('/').length < 8) queue.push(child);
@@ -1139,7 +1283,14 @@ export function collectRepoShapeSignals(root) {
1139
1283
  const pkg = readPackageJson(root);
1140
1284
  const repoUnits = discoverRepoUnits(root);
1141
1285
  const workspaceDirs = detectWorkspaces(root);
1142
- const workspaces = workspaceDirs.length > 0;
1286
+ // Manifest presence is workspace evidence even when globs parse to no dirs
1287
+ // (pnpm-workspace.yaml without package.json#workspaces is the common case).
1288
+ const workspaces =
1289
+ workspaceDirs.length > 0 ||
1290
+ isPnpmWorkspaceRoot(root) ||
1291
+ isNpmYarnWorkspaceRoot(root) ||
1292
+ fs.existsSync(path.join(root, 'rush.json')) ||
1293
+ fs.existsSync(path.join(root, 'lerna.json'));
1143
1294
  // Include frontend/web/client — common Next monorepo app folders.
1144
1295
  const candidateScanDirs = repoUnits
1145
1296
  .filter((unit) => !['docs', 'example', 'test'].includes(unit.role))
@@ -1362,7 +1513,10 @@ export function collectRepoShapeSignals(root) {
1362
1513
  }
1363
1514
 
1364
1515
  const SIGNAL_WHY = {
1365
- workspaces: (signals) => `workspace roots declared (${signals.workspaceDirs.join(', ')})`,
1516
+ workspaces: (signals) =>
1517
+ signals.workspaceDirs?.length
1518
+ ? `workspace roots declared (${signals.workspaceDirs.join(', ')})`
1519
+ : 'workspace manifest present (pnpm-workspace.yaml, package.json workspaces, rush, or lerna)',
1366
1520
  tinyTree: (signals) => `few source files (${signals.sourceFileCount})`,
1367
1521
  ui: () => 'UI directories or multiple TSX files present',
1368
1522
  uiHeavy: () => 'substantial UI surface (multiple TSX files)',
@@ -1397,8 +1551,24 @@ const SIGNAL_WHY = {
1397
1551
  expressLike: () => 'HTTP framework dependency (express/fastify/hono/…)',
1398
1552
  };
1399
1553
 
1554
+ /**
1555
+ * Playbook labels that claim "without UI" are only honest when uiHeavy is false.
1556
+ * Mirror of the #282 workspace negative-copy overlay: do not invent a new
1557
+ * archetype — rewrite the one-minute label so it cannot contradict the sensors.
1558
+ */
1559
+ export function honestArchetypeLabel(label, signals) {
1560
+ if (typeof label !== 'string' || !signals?.uiHeavy) return label;
1561
+ if (!/without UI/i.test(label)) return label;
1562
+ if (signals.workspaces && signals.apiSurface) return 'API-heavy monorepo with UI packages';
1563
+ if (signals.apiSurface) return 'API-heavy repository with a UI surface';
1564
+ return 'Repository with a UI surface';
1565
+ }
1566
+
1400
1567
  const NEGATIVE_SIGNAL_WHY = {
1401
- workspaces: () => 'not a workspace monorepo (penalized for this shape)',
1568
+ workspaces: (signals) =>
1569
+ signals.workspaces
1570
+ ? `workspace monorepo (${signals.workspaceDirs?.join(', ') || 'manifest present'}; this shape prefers a single package)`
1571
+ : 'not a workspace monorepo (penalized for this shape)',
1402
1572
  cli: () => 'CLI bin entry present (penalized for this shape)',
1403
1573
  ui: () => 'UI directories present (penalized for this shape)',
1404
1574
  uiHeavy: () => 'heavy UI surface (penalized for this shape)',
@@ -1582,7 +1752,7 @@ export function scoreArchetypes(signals, playbook) {
1582
1752
  );
1583
1753
  scored.push({
1584
1754
  id,
1585
- label: def.label,
1755
+ label: honestArchetypeLabel(def.label, signals),
1586
1756
  preset: resolvePreset(def, signals),
1587
1757
  score,
1588
1758
  maxPositive: maxPositive || 1,
@@ -1718,6 +1888,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
1718
1888
  })),
1719
1889
  workspaces: signals.workspaces,
1720
1890
  ui: signals.ui,
1891
+ uiHeavy: signals.uiHeavy,
1721
1892
  apiSurface: signals.apiSurface,
1722
1893
  persistence: signals.persistence,
1723
1894
  jobs: signals.jobs,
package/bin/ark.mjs CHANGED
@@ -26,10 +26,13 @@ import {
26
26
  applyStartPreview,
27
27
  emitStartSetupBudgetRefuse,
28
28
  evaluateStartSetupBudgetGate,
29
- formatStartPackageInstallFailure,
30
29
  planStart,
31
30
  renderStartPreview,
32
31
  } from './lib/start-preview.mjs';
32
+ import {
33
+ formatStartPackageInstallFailure,
34
+ runStartPackageInstall,
35
+ } from './lib/start-install-recovery.mjs';
33
36
  import { runUpgradeCommand } from './lib/upgrade-command.mjs';
34
37
  import { detectActiveAgentHost } from './lib/skill-install.mjs';
35
38
  import { loadArkConfigContract } from './lib/config-contract.mjs';
@@ -211,11 +214,6 @@ function cliVersion() {
211
214
 
212
215
  // packageInstallArgv is imported from ark-shared (workspace-aware -w / -W).
213
216
 
214
- function runCommand(command, commandArgs, cwd) {
215
- const result = spawnSync(command, commandArgs, { cwd, stdio: 'inherit', encoding: 'utf8' });
216
- return result.status ?? 1;
217
- }
218
-
219
217
  function runArkCheck(args, options = {}) {
220
218
  const result = spawnSync(process.execPath, [arkCheck, ...args], {
221
219
  cwd: options.cwd,
@@ -282,13 +280,16 @@ export function ensureProjectArkgateDependency(root, opts = {}) {
282
280
  }
283
281
  const pinned = pinArkgateDevDependency(root);
284
282
  let installStatus = null;
283
+ let hostOutput = '';
285
284
  // Only run the package manager after a successful pin change — avoid surprise
286
285
  // network on every start when arkgate is already listed.
287
286
  if (runPm && pinned.changed) {
288
287
  const [command, commandArgs] = packageInstallArgv(root, pinned.version);
289
- installStatus = runCommand(command, commandArgs, root);
288
+ const install = runStartPackageInstall(command, commandArgs, root);
289
+ installStatus = install.status;
290
+ hostOutput = `${install.stdout}\n${install.stderr}`;
290
291
  }
291
- return { pinned, installStatus };
292
+ return { pinned, installStatus, hostOutput };
292
293
  }
293
294
 
294
295
  async function resolveArchetypeInteractive(rl, root) {
@@ -503,16 +504,17 @@ async function start(args) {
503
504
  const installCommand = `${command} ${commandArgs.join(' ')}`;
504
505
  if (!args.json) console.log(`Installing package: ${installCommand}`);
505
506
  // Keep stdout clean for --json consumers (package managers are chatty on stdout).
506
- const status = args.json
507
- ? (spawnSync(command, commandArgs, {
508
- cwd: args.root,
509
- stdio: ['ignore', 'pipe', 'pipe'],
510
- encoding: 'utf8',
511
- }).status ?? 1)
512
- : runCommand(command, commandArgs, args.root);
513
- if (status !== 0) {
514
- console.error(formatStartPackageInstallFailure({ exitStatus: status, installCommand }));
515
- return status;
507
+ const install = runStartPackageInstall(command, commandArgs, args.root);
508
+ if (install.status !== 0) {
509
+ console.error(
510
+ formatStartPackageInstallFailure({
511
+ exitStatus: install.status,
512
+ installCommand,
513
+ hostOutput: `${install.stdout}\n${install.stderr}`,
514
+ packageVersion: cliVersion(),
515
+ })
516
+ );
517
+ return install.status;
516
518
  }
517
519
  }
518
520
  }
@@ -572,7 +574,7 @@ async function start(args) {
572
574
  // 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
573
575
  // Default install=true; only --no-install skips. (installExplicit tracks user override for copy.)
574
576
  if (args.install && fs.existsSync(path.join(root, 'package.json'))) {
575
- const { pinned, installStatus } = ensureProjectArkgateDependency(root, {
577
+ const { pinned, installStatus, hostOutput } = ensureProjectArkgateDependency(root, {
576
578
  install: true,
577
579
  runPackageManager: !args.skipPackageManager,
578
580
  });
@@ -584,6 +586,8 @@ async function start(args) {
584
586
  formatStartPackageInstallFailure({
585
587
  exitStatus: installStatus,
586
588
  installCommand: `${command} ${commandArgs.join(' ')}`,
589
+ hostOutput,
590
+ packageVersion: cliVersion(),
587
591
  })
588
592
  );
589
593
  }