arkgate 2.6.0 → 2.6.1

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.
@@ -224,6 +224,10 @@ export function applyFrameworkLayoutOverlays(config, root) {
224
224
  'src/services/**',
225
225
  'src/use-cases/**',
226
226
  'src/lib/**',
227
+ // Common Next "app core" bags (API clients, hooks, auth, stores) — not UI routes.
228
+ // Without these, monorepos like */src/core/** stay ungoverned and produce false greens.
229
+ 'src/core/**',
230
+ '**/core/**',
227
231
  'src/actions/**',
228
232
  'src/**/actions.ts',
229
233
  'src/**/actions.tsx',
@@ -244,6 +248,20 @@ export function applyFrameworkLayoutOverlays(config, root) {
244
248
  'src/lib/prisma/**',
245
249
  'src/server/db/**',
246
250
  ]);
251
+ // Demo assets, generated public output, and tool configs are not architecture surface.
252
+ const nextExcludes = [
253
+ '**/public/**',
254
+ '**/*.config.js',
255
+ '**/*.config.ts',
256
+ '**/*.config.mjs',
257
+ '**/playwright*.ts',
258
+ '**/eslint.config.*',
259
+ '**/postcss.config.*',
260
+ '**/prettier.config.*',
261
+ '**/rstest.config.*',
262
+ '**/scripts/**',
263
+ ];
264
+ next.exclude = [...new Set([...(next.exclude ?? []), ...nextExcludes])];
247
265
  next.frameworkOverlay = next.frameworkOverlay
248
266
  ? `${next.frameworkOverlay}+next`
249
267
  : 'next';
@@ -369,6 +387,9 @@ export {
369
387
  layerForFile,
370
388
  layerForRelativePath,
371
389
  isEdgeDenied,
390
+ DEFAULT_GENERATED_FILE_GLOBS,
391
+ scanExcludePatterns,
392
+ isScanExcludedRelative,
372
393
  } from './ark-layer-match.mjs';
373
394
 
374
395
  function normalizePrefix(prefix) {
@@ -719,7 +740,15 @@ function readPackageJson(root) {
719
740
  }
720
741
  }
721
742
 
722
- /** Workspace roots from package.json workspaces and pnpm-workspace.yaml (no YAML dependency). */
743
+ /**
744
+ * Workspace / multi-package roots for monorepo include.
745
+ * Sources (universal — no project-specific names):
746
+ * 1. package.json workspaces + pnpm-workspace.yaml
747
+ * 2. rush.json projectFolder top segments (JSONC-tolerant)
748
+ * 3. lerna.json packages globs
749
+ * 4. If still empty: conventional multi-package top-level dirs that exist and
750
+ * contain at least one package.json (packages, apps, plugins, services, …)
751
+ */
723
752
  export function detectWorkspaces(root) {
724
753
  const dirs = new Set();
725
754
  const addGlob = (glob) => {
@@ -727,9 +756,16 @@ export function detectWorkspaces(root) {
727
756
  const beforeStar = glob.split('*')[0].replace(/\/+$/, '');
728
757
  if (beforeStar && beforeStar !== '.') dirs.add(normalizeRel(beforeStar));
729
758
  };
759
+ const addTop = (rel) => {
760
+ if (typeof rel !== 'string') return;
761
+ const top = rel.split(/[/\\]/).filter(Boolean)[0];
762
+ if (top && top !== '.') dirs.add(normalizeRel(top));
763
+ };
764
+
730
765
  const pkg = readPackageJson(root);
731
766
  const ws = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
732
767
  if (Array.isArray(ws)) ws.forEach(addGlob);
768
+
733
769
  const pnpmFile = path.join(root, 'pnpm-workspace.yaml');
734
770
  if (fs.existsSync(pnpmFile)) {
735
771
  let inPackages = false;
@@ -744,9 +780,183 @@ export function detectWorkspaces(root) {
744
780
  if (item) addGlob(item[1].trim());
745
781
  }
746
782
  }
783
+
784
+ // Rush monorepos often have no root package.json workspaces — only rush.json.
785
+ const rushFile = path.join(root, 'rush.json');
786
+ if (fs.existsSync(rushFile)) {
787
+ try {
788
+ let text = fs.readFileSync(rushFile, 'utf8');
789
+ text = text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
790
+ const rush = JSON.parse(text);
791
+ for (const project of rush.projects || []) {
792
+ if (typeof project?.projectFolder === 'string') addTop(project.projectFolder);
793
+ }
794
+ } catch {
795
+ /* ignore malformed rush.json */
796
+ }
797
+ }
798
+
799
+ // Lerna packages globs.
800
+ const lernaFile = path.join(root, 'lerna.json');
801
+ if (fs.existsSync(lernaFile)) {
802
+ try {
803
+ const lerna = JSON.parse(fs.readFileSync(lernaFile, 'utf8'));
804
+ if (Array.isArray(lerna.packages)) lerna.packages.forEach(addGlob);
805
+ } catch {
806
+ /* ignore */
807
+ }
808
+ }
809
+
810
+ // Conventional multi-package roots when no explicit workspace manifest listed roots.
811
+ if (dirs.size === 0) {
812
+ const conventional = [
813
+ 'packages',
814
+ 'apps',
815
+ 'plugins',
816
+ 'services',
817
+ 'server',
818
+ 'servers',
819
+ 'server-plugins',
820
+ 'libs',
821
+ 'lib',
822
+ 'modules',
823
+ 'foundations',
824
+ 'pods',
825
+ 'models',
826
+ 'clients',
827
+ 'sdks',
828
+ 'tools',
829
+ 'tooling',
830
+ 'desktop',
831
+ 'common',
832
+ ];
833
+ for (const name of conventional) {
834
+ const abs = path.join(root, name);
835
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) continue;
836
+ if (dirContainsPackageJson(abs, 2)) dirs.add(name);
837
+ }
838
+ }
839
+
747
840
  return [...dirs];
748
841
  }
749
842
 
843
+ /** True if dir (or a child within maxDepth) has a package.json — multi-package root signal. */
844
+ function dirContainsPackageJson(dir, maxDepth) {
845
+ try {
846
+ if (fs.existsSync(path.join(dir, 'package.json'))) return true;
847
+ if (maxDepth <= 0) return false;
848
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
849
+ if (!entry.isDirectory()) continue;
850
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
851
+ if (dirContainsPackageJson(path.join(dir, entry.name), maxDepth - 1)) return true;
852
+ }
853
+ } catch {
854
+ return false;
855
+ }
856
+ return false;
857
+ }
858
+
859
+ const SKIP_DIR_NAMES = new Set([
860
+ 'node_modules',
861
+ 'dist',
862
+ 'build',
863
+ 'coverage',
864
+ '.git',
865
+ '.next',
866
+ '.turbo',
867
+ '.cache',
868
+ 'vendor',
869
+ '__pycache__',
870
+ ]);
871
+
872
+ function dirHasTsSources(dir, maxDepth) {
873
+ try {
874
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
875
+ if (entry.isFile() && /\.(tsx?|jsx?|mts|cts)$/i.test(entry.name)) return true;
876
+ if (!entry.isDirectory()) continue;
877
+ if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
878
+ if (maxDepth > 0 && dirHasTsSources(path.join(dir, entry.name), maxDepth - 1)) return true;
879
+ }
880
+ } catch {
881
+ return false;
882
+ }
883
+ return false;
884
+ }
885
+
886
+ /**
887
+ * Discover package roots that contain TypeScript/JS sources (polyglot-safe).
888
+ * Returns relative paths (e.g. remotion-composer, packages/ui). Caps depth/count
889
+ * to avoid scanning huge trees. Universal — no project-specific names.
890
+ */
891
+ export function detectTsPackageRoots(root, options = {}) {
892
+ const maxDepth = options.maxDepth ?? 3;
893
+ const maxRoots = options.maxRoots ?? 40;
894
+ const found = [];
895
+
896
+ const visit = (abs, rel, depth) => {
897
+ if (found.length >= maxRoots || depth > maxDepth) return;
898
+ let entries;
899
+ try {
900
+ entries = fs.readdirSync(abs, { withFileTypes: true });
901
+ } catch {
902
+ return;
903
+ }
904
+ const hasPkg = entries.some((e) => e.isFile() && e.name === 'package.json');
905
+ if (hasPkg && rel && dirHasTsSources(abs, 3)) {
906
+ found.push(rel.split(path.sep).join('/'));
907
+ // Do not descend into nested packages under an already-selected package root
908
+ // unless maxDepth allows and we want monorepo packages/* children — still scan children
909
+ // for nested packages (packages/foo).
910
+ }
911
+ if (depth >= maxDepth) return;
912
+ for (const entry of entries) {
913
+ if (!entry.isDirectory()) continue;
914
+ if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
915
+ // Skip agent skill asset trees (not app code).
916
+ if (entry.name === 'skills' || entry.name === 'templates' || entry.name === 'fixtures') continue;
917
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
918
+ visit(path.join(abs, entry.name), childRel, depth + 1);
919
+ }
920
+ };
921
+
922
+ visit(root, '', 0);
923
+ // Also treat root itself if it has package.json + TS
924
+ if (fs.existsSync(path.join(root, 'package.json')) && dirHasTsSources(root, 2)) {
925
+ if (!found.includes('.')) {
926
+ // Prefer explicit '.' only when no nested packages found
927
+ if (found.length === 0) found.push('.');
928
+ }
929
+ }
930
+ return [...new Set(found)].sort();
931
+ }
932
+
933
+ /**
934
+ * Merge workspace globs with TS package roots. If workspaces alone would miss
935
+ * all TS (or are empty), fill from detectTsPackageRoots.
936
+ */
937
+ export function resolveIncludeRoots(root) {
938
+ const workspaces = detectWorkspaces(root);
939
+ const tsRoots = detectTsPackageRoots(root);
940
+ if (workspaces.length === 0) {
941
+ if (tsRoots.length > 0) return tsRoots.filter((r) => r !== '.');
942
+ return [];
943
+ }
944
+ // Workspaces present: keep them, add TS package roots not covered by a workspace prefix
945
+ const merged = new Set(workspaces);
946
+ for (const tr of tsRoots) {
947
+ if (tr === '.') continue;
948
+ const covered = workspaces.some(
949
+ (w) => tr === w || tr.startsWith(`${w}/`) || w.startsWith(`${tr}/`)
950
+ );
951
+ if (!covered) merged.add(tr.split('/')[0]); // top segment if nested
952
+ // Prefer full package path when it's a direct child
953
+ if (!tr.includes('/') || workspaces.includes(tr.split('/')[0])) {
954
+ if (fs.existsSync(path.join(root, tr, 'package.json'))) merged.add(tr);
955
+ }
956
+ }
957
+ return [...merged];
958
+ }
959
+
750
960
  function isSourceFile(name) {
751
961
  return /\.(tsx?|jsx?|mjsx?|cjsx?|mts|cts)$/i.test(name);
752
962
  }
@@ -822,6 +1032,47 @@ function countTsxFiles(files) {
822
1032
  return files.filter((f) => /\.(tsx|jsx)$/i.test(f)).length;
823
1033
  }
824
1034
 
1035
+ /**
1036
+ * Merge root + nested package.json dependencies (frontend/, packages/*, apps/*, …).
1037
+ * Hosts that only put `arkgate` at the monorepo root and Next under frontend/ must
1038
+ * still detect nextFramework so layout overlays (src/core/**) apply on day one.
1039
+ */
1040
+ export function collectAggregatedDeps(root) {
1041
+ const deps = {};
1042
+ const mergePkg = (pkg) => {
1043
+ if (!pkg || typeof pkg !== 'object') return;
1044
+ for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
1045
+ const block = pkg[key];
1046
+ if (block && typeof block === 'object') Object.assign(deps, block);
1047
+ }
1048
+ };
1049
+ mergePkg(readPackageJson(root));
1050
+ let entries = [];
1051
+ try {
1052
+ entries = fs.readdirSync(root, { withFileTypes: true });
1053
+ } catch {
1054
+ return deps;
1055
+ }
1056
+ for (const entry of entries) {
1057
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
1058
+ const childRoot = path.join(root, entry.name);
1059
+ mergePkg(readPackageJson(childRoot));
1060
+ // One level under conventional multi-package roots
1061
+ if (['packages', 'apps', 'services', 'plugins', 'packages-internal'].includes(entry.name)) {
1062
+ try {
1063
+ for (const sub of fs.readdirSync(childRoot, { withFileTypes: true })) {
1064
+ if (sub.isDirectory() && !sub.name.startsWith('.')) {
1065
+ mergePkg(readPackageJson(path.join(childRoot, sub.name)));
1066
+ }
1067
+ }
1068
+ } catch {
1069
+ /* ignore */
1070
+ }
1071
+ }
1072
+ }
1073
+ return deps;
1074
+ }
1075
+
825
1076
  /**
826
1077
  * Collect deterministic repo shape signals for architecture archetype scoring.
827
1078
  * Vendor packages may appear in toolHints only — never as the primary label.
@@ -830,7 +1081,20 @@ export function collectRepoShapeSignals(root) {
830
1081
  const pkg = readPackageJson(root);
831
1082
  const workspaceDirs = detectWorkspaces(root);
832
1083
  const workspaces = workspaceDirs.length > 0;
833
- const srcDirs = ['src', 'lib', 'api', 'packages', 'apps'].filter((d) =>
1084
+ // Include frontend/web/client common Next monorepo app folders (deer-flow-style).
1085
+ const candidateScanDirs = [
1086
+ 'src',
1087
+ 'lib',
1088
+ 'api',
1089
+ 'packages',
1090
+ 'apps',
1091
+ 'frontend',
1092
+ 'web',
1093
+ 'client',
1094
+ 'app',
1095
+ ...workspaceDirs,
1096
+ ];
1097
+ const srcDirs = [...new Set(candidateScanDirs)].filter((d) =>
834
1098
  fs.existsSync(path.join(root, d))
835
1099
  );
836
1100
  const scanRoots = srcDirs.length > 0 ? srcDirs.map((d) => path.join(root, d)) : [root];
@@ -838,13 +1102,14 @@ export function collectRepoShapeSignals(root) {
838
1102
  const sourceFileCount = sourceFiles.length;
839
1103
  const tinyTree = sourceFileCount < 3;
840
1104
 
841
- const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
1105
+ // Nested package.json deps (not root-only) critical for monorepo Next under frontend/
1106
+ const deps = collectAggregatedDeps(root);
842
1107
  const hasUiFramework = Object.keys(deps).some((name) =>
843
1108
  /^(react|react-dom|vue|svelte|preact|solid-js)$/i.test(name.split('/')[0])
844
1109
  );
845
1110
  const srcUiFiles = sourceFiles.filter((file) => {
846
1111
  const rel = path.relative(root, file).split(path.sep).join('/');
847
- return rel.startsWith('src/') && /\.(tsx|jsx)$/i.test(file);
1112
+ return (rel.includes('/src/') || rel.startsWith('src/')) && /\.(tsx|jsx)$/i.test(file);
848
1113
  });
849
1114
 
850
1115
  const topNames = new Set(srcDirs.flatMap((d) => listTopLevelDirNames(root, d)));
@@ -860,7 +1125,8 @@ export function collectRepoShapeSignals(root) {
860
1125
  const rel = path.relative(root, file).split(path.sep).join('/');
861
1126
  return (
862
1127
  /(^|\/)next\.config\./.test(rel) ||
863
- /(^|\/)app\/.*\/page\.(t|j)sx?$/.test(rel) ||
1128
+ // app/page.tsx OR app/dashboard/page.tsx (middle segment optional)
1129
+ /(^|\/)app\/(?:.*\/)?page\.(t|j)sx?$/.test(rel) ||
864
1130
  /(^|\/)pages\/.+\.(t|j)sx?$/.test(rel)
865
1131
  );
866
1132
  });
@@ -1127,7 +1393,15 @@ export function scoreArchetypes(signals, playbook) {
1127
1393
  const rawConfidence = top.score / top.maxPositive;
1128
1394
  const margin =
1129
1395
  second && second.score > 0 ? (top.score - second.score) / Math.max(top.score, 1) : 0.25;
1130
- const confidence = Math.min(1, Math.max(0.1, rawConfidence * 0.7 + margin * 0.3));
1396
+ let confidence = Math.min(1, Math.max(0.1, rawConfidence * 0.7 + margin * 0.3));
1397
+ // Thin / zero TS surface: never present a high-confidence archetype as the sole answer.
1398
+ const thinTs =
1399
+ !signals.sourceFileCount ||
1400
+ signals.sourceFileCount < 8 ||
1401
+ signals.tinyTree;
1402
+ if (thinTs) {
1403
+ confidence = Math.min(confidence, 0.28);
1404
+ }
1131
1405
 
1132
1406
  return {
1133
1407
  ranked: scored,
@@ -1135,6 +1409,13 @@ export function scoreArchetypes(signals, playbook) {
1135
1409
  label: top.label,
1136
1410
  preset: top.preset,
1137
1411
  confidence: Math.round(confidence * 1000) / 1000,
1412
+ ...(thinTs
1413
+ ? {
1414
+ thinTsSurface: true,
1415
+ caution:
1416
+ 'TypeScript/JS surface is thin or missing — treat the archetype as a weak hint. Prefer ark-check --suggest-include / --adopt-contract on the real package roots before scaffolding.',
1417
+ }
1418
+ : {}),
1138
1419
  phases: top.phases,
1139
1420
  analogy: top.analogy,
1140
1421
  antiPatterns: top.antiPatterns,
@@ -1169,6 +1450,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
1169
1450
  label: result.label,
1170
1451
  preset: result.preset,
1171
1452
  confidence: result.confidence,
1453
+ ...(result.thinTsSurface ? { thinTsSurface: true, caution: result.caution } : {}),
1172
1454
  phases: result.phases,
1173
1455
  adoptInOrder,
1174
1456
  analogy: result.analogy,
@@ -1334,6 +1616,13 @@ export function formatArchitectureRecommendationHuman(recommendation) {
1334
1616
  lines.push('');
1335
1617
  lines.push(`Archetype: ${recommendation.archetype} — ${recommendation.label}`);
1336
1618
  lines.push(`Preset: ${recommendation.preset} (confidence ${recommendation.confidence})`);
1619
+ if (recommendation.thinTsSurface) {
1620
+ lines.push('');
1621
+ lines.push(
1622
+ `⚠ Thin TypeScript surface (${recommendation.signals?.sourceFileCount ?? 0} files) — confidence is capped. Do not treat this as a firm shape.`
1623
+ );
1624
+ if (recommendation.caution) lines.push(recommendation.caution);
1625
+ }
1337
1626
  if (recommendation.runnerUp?.id) {
1338
1627
  lines.push(
1339
1628
  `Runner-up: ${recommendation.runnerUp.id}${recommendation.runnerUp.label ? ` (${recommendation.runnerUp.label})` : ''}`
package/bin/ark.mjs CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  buildArchitectureRecommendation,
11
11
  detectPackageManager,
12
12
  detectWorkspaces,
13
+ resolveIncludeRoots,
14
+ detectTsPackageRoots,
13
15
  INIT_WIZARD_CHOICES,
14
16
  isValidArchetypeId,
15
17
  mapWizardChoiceToArchetype,
@@ -345,16 +347,32 @@ async function start(args) {
345
347
  if (!fs.existsSync(configPath)) {
346
348
  const initArgs = ['--root', root, '--init'];
347
349
  const preset = archetype ? resolveArchetypePreset(archetype).preset : undefined;
350
+ const includeRoots = resolveIncludeRoots(root);
351
+ const tsPackages = detectTsPackageRoots(root);
348
352
  const workspaces = detectWorkspaces(root);
349
353
  const looksLikeMonorepo =
354
+ includeRoots.length > 0 ||
355
+ tsPackages.length > 0 ||
350
356
  workspaces.length > 0 ||
357
+ fs.existsSync(path.join(root, 'rush.json')) ||
358
+ fs.existsSync(path.join(root, 'pnpm-workspace.yaml')) ||
359
+ fs.existsSync(path.join(root, 'lerna.json')) ||
351
360
  fs.existsSync(path.join(root, 'apps')) ||
352
361
  fs.existsSync(path.join(root, 'packages'));
353
- // Mature multi-package trees must NOT get a thin src/** starter (0 files → false green).
354
- // Prefer the monorepo preset so include roots match apps/packages/tooling.
355
- if (looksLikeMonorepo && (rec?.mature || workspaces.length > 0)) {
356
- initArgs.push('--preset', 'monorepo');
357
- console.log(' Multi-package layout detected — using monorepo profile.');
362
+ // Mature multi-package / nested-TS trees must NOT get a thin src/** starter (0 files).
363
+ if (looksLikeMonorepo && (rec?.mature || includeRoots.length > 0 || tsPackages.length > 0)) {
364
+ // UI-heavy TS packages (Remotion/Vite) prefer ui-surface patterns when recommend says so.
365
+ const useUi =
366
+ rec?.preset === 'feature-sliced' ||
367
+ rec?.archetype === 'frontend-surface' ||
368
+ (tsPackages.length > 0 && includeRoots.length === 0 && !rec?.mature);
369
+ initArgs.push('--preset', useUi && tsPackages.length <= 3 ? 'ui-surface' : 'monorepo');
370
+ const shown = includeRoots.length > 0 ? includeRoots : tsPackages;
371
+ console.log(
372
+ shown.length > 0
373
+ ? ` Multi-package / TS package layout detected — profile include: ${shown.join(', ')}.`
374
+ : ' Multi-package layout detected — using monorepo profile.'
375
+ );
358
376
  } else if (!rec?.mature && preset) {
359
377
  initArgs.push('--preset', preset);
360
378
  }
@@ -363,7 +381,12 @@ async function start(args) {
363
381
  } else {
364
382
  console.log(' Found an existing ark.config.json — keeping it.');
365
383
  }
366
- runArkCheck(['--root', root, '--install-agent-gates'], { cwd: root });
384
+ {
385
+ const gateArgs = ['--root', root, '--install-agent-gates'];
386
+ if (args.tools) gateArgs.push('--tools', args.tools);
387
+ if (args.force) gateArgs.push('--force');
388
+ runArkCheck(gateArgs, { cwd: root });
389
+ }
367
390
 
368
391
  // 4) Show the plan: what's safe to auto-fix vs what needs a decision.
369
392
  console.log('');
@@ -406,49 +429,36 @@ async function start(args) {
406
429
  planOk = false;
407
430
  }
408
431
 
409
- // 5) Plain-language wrap-up — three operating modes, one contract.
410
- // suggest = greenfield shape proposal; adapt = match real layout / raise coverage;
411
- // enforce = contract actually governs code and gates stay on.
432
+ // 5) Plain-language wrap-up — one next step, status light only.
433
+ // Modes are detected (Suggest/Adapt/Enforce), not user-picked settings.
412
434
  console.log('');
413
435
  if (mode === 'enforce' && planOk) {
414
- console.log('Done — Ark is in ENFORCE mode: your contract governs the code and the gates stay on.');
436
+ console.log('Done — status: ENFORCE (gates can honestly protect you).');
415
437
  console.log('What happens now:');
416
438
  console.log(' • Every edit is checked (in CI and, if wired, at write time).');
417
- console.log(' • Carry out remaining plan steps with /ark-autopilot (safe auto + your approvals).');
418
439
  } else if (mode === 'suggest') {
419
- console.log('Done — Ark is in SUGGEST mode: a starting shape is installed; enforcement grows as you add layers.');
440
+ console.log('Done — status: SUGGEST (starting shape installed; expand as you grow).');
420
441
  console.log('What happens now:');
421
- console.log(' • Gates are on for whatever the contract already matches.');
422
- console.log(' • Expand coverage as you create real layer folders (see the plan above).');
423
442
  if (governedPercent != null) {
424
- console.log(` • Right now Ark governs ~${governedPercent}% of in-scope files — low is normal on a fresh scaffold.`);
443
+ console.log(` • Ark governs ~${governedPercent}% of in-scope files — low is normal on a fresh scaffold.`);
425
444
  }
426
- console.log(' • When you want the agent to drive the plan: /ark-autopilot');
427
445
  } else {
428
- console.log('Done — Ark is in ADAPT mode: config is in place, but the contract still needs to match your real layout.');
446
+ console.log('Done — status: ADAPT (contract still aligning with your real layout).');
429
447
  console.log('What happens now:');
430
448
  if (governedPercent != null) {
431
449
  console.log(
432
- ` • Governed coverage is ~${governedPercent}% — a "clean" plan with low coverage checks almost nothing.`
450
+ ` • Governed ~${governedPercent}% — a "clean" plan with low coverage checks almost nothing.`
433
451
  );
434
452
  }
435
- console.log(` • See what is unmatched: ${arkCommand(root, 'ark-check', '--coverage')}`);
436
- console.log(' • On a mature repo, prefer /ark-adopt over forcing a starter preset.');
437
- console.log(' • Drive fixes with /ark-autopilot once the contract matches; until then ENFORCE is not honest.');
438
- }
439
- console.log(` • Re-run the plan anytime: ${arkCommand(root, 'ark-check', '--plan')}`);
440
- console.log(` • Full project check: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
441
- console.log(` • Adoption health: ${arkCommand(root, 'ark-check', '--doctor')}`);
442
- console.log(` • Update Ark later: ${arkCommand(root, 'ark', 'upgrade')}`);
443
- if (fs.existsSync(path.join(root, '.ark-baseline.json'))) {
444
- console.log(
445
- ' • Baseline file present — keep empty for ratchet-from-clean, or freeze debt with --update-baseline.'
446
- );
447
- } else {
448
- console.log(
449
- ' • No baseline yet (fine on clean trees). Adopting dirty code? freeze with --update-baseline.'
450
- );
451
453
  }
454
+ console.log('');
455
+ console.log('Next (the only flow you need):');
456
+ console.log(' 1. In your agent: /ark-autopilot');
457
+ console.log(' → origin report, adoption, plan, safe fixes, leave gates on.');
458
+ console.log(` 2. Status anytime: ${arkCommand(root, 'ark-check', '--doctor')}`);
459
+ console.log(` 3. After edits: ${arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config')}`);
460
+ console.log('');
461
+ console.log('Optional later: --plan · --coverage · /ark-fix · /ark-place · ark upgrade');
452
462
 
453
463
  // 6) First architecture report — freezes an origin snapshot under .ark/reports/
454
464
  // so later --report runs can show evolution. Idempotent: origin is written only once.