arkgate 4.6.4 → 4.6.6

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 (72) hide show
  1. package/CHANGELOG.md +77 -2105
  2. package/README.md +11 -9
  3. package/bin/ark-check-runtime.mjs +136 -16
  4. package/bin/ark-mcp-runtime.mjs +18 -30
  5. package/bin/ark.mjs +13 -3
  6. package/bin/lib/adapter-contract.mjs +13 -9
  7. package/bin/lib/adoption-stance.mjs +104 -0
  8. package/bin/lib/agent-projection-command.mjs +18 -0
  9. package/bin/lib/agent-projection.mjs +2 -2
  10. package/bin/lib/analysis-engine.mjs +5 -5
  11. package/bin/lib/ci-and-commands.mjs +3 -3
  12. package/bin/lib/ci-merge-boundary.mjs +91 -0
  13. package/bin/lib/config-contract.mjs +2 -0
  14. package/bin/lib/design-delta.mjs +2 -2
  15. package/bin/lib/design-smells.mjs +1 -1
  16. package/bin/lib/diagnostic-catalog.mjs +6 -5
  17. package/bin/lib/doctor-advisories.mjs +2 -2
  18. package/bin/lib/doctor-next-actions.mjs +35 -5
  19. package/bin/lib/doctor-plan.mjs +164 -133
  20. package/bin/lib/enforcement-honesty.mjs +72 -0
  21. package/bin/lib/first-run-help.mjs +8 -7
  22. package/bin/lib/graph-blind.mjs +15 -6
  23. package/bin/lib/html-report-advisories.mjs +10 -2
  24. package/bin/lib/html-report.mjs +2 -2
  25. package/bin/lib/install-migrate.mjs +10 -0
  26. package/bin/lib/invariant-coverage.mjs +6 -2
  27. package/bin/lib/managed-upgrade.mjs +8 -3
  28. package/bin/lib/mcp-adoption.mjs +19 -0
  29. package/bin/lib/policy-delta-io.mjs +1 -1
  30. package/bin/lib/post-green-path.mjs +5 -1
  31. package/bin/lib/presets.mjs +22 -0
  32. package/bin/lib/product-copy.mjs +6 -3
  33. package/bin/lib/remediation.mjs +74 -10
  34. package/bin/lib/skill-install.mjs +2 -0
  35. package/bin/lib/snippet-analysis.mjs +40 -8
  36. package/bin/lib/start-preview.mjs +12 -22
  37. package/bin/lib/status-command.mjs +16 -0
  38. package/bin/lib/status-manifest.mjs +8 -2
  39. package/bin/lib/team-parliament-io.mjs +62 -2
  40. package/bin/lib/team-parliament.mjs +25 -5
  41. package/bin/lib/unavailable-analysis.mjs +1 -0
  42. package/dist/{configTypes-B8uIcLaG.d.ts → configTypes-l6XiwiC1.d.ts} +7 -0
  43. package/dist/eslint/index.cjs +3 -3
  44. package/dist/eslint/index.d.ts +1 -1
  45. package/dist/eslint/index.js +3 -3
  46. package/dist/index.cjs +26 -26
  47. package/dist/index.d.ts +20 -3
  48. package/dist/index.js +29 -29
  49. package/docs/README.md +6 -9
  50. package/docs/agent-guide.md +10 -0
  51. package/docs/ai-gates.md +12 -5
  52. package/docs/brownfield-adoption.md +7 -1
  53. package/docs/configuration.md +11 -2
  54. package/docs/develop.md +4 -2
  55. package/docs/diagnostics.md +17 -7
  56. package/docs/package-surface.md +6 -4
  57. package/docs/product-voice.md +6 -4
  58. package/docs/threat-model.md +2 -2
  59. package/docs/use.md +5 -4
  60. package/package.json +1 -1
  61. package/schemas/ark.config.schema.json +6 -0
  62. package/schemas/ark.design-delta.schema.json +1 -1
  63. package/server.json +2 -2
  64. package/templates/agent-skills/README.md +1 -1
  65. package/templates/agent-skills/ark-adopt/SKILL.md +7 -0
  66. package/templates/agent-skills/ark-explore/SKILL.md +6 -0
  67. package/templates/agent-skills/ark-place/SKILL.md +11 -4
  68. package/templates/agent-skills/ark-upgrade/SKILL.md +9 -2
  69. package/templates/skills/ark-adopt.md +7 -0
  70. package/templates/skills/ark-explore.md +6 -0
  71. package/templates/skills/ark-place.md +11 -4
  72. package/templates/skills/ark-upgrade.md +9 -2
@@ -12,10 +12,7 @@ import {
12
12
  import * as arkShared from '../ark-shared.mjs';
13
13
  import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
14
14
  import { describePackageVersionDualTruth } from './field-install.mjs';
15
- import {
16
- detectAgentHomeGaps,
17
- agentHomeConcernIsActive,
18
- } from './agent-homes.mjs';
15
+ import { detectAgentHomeGaps, agentHomeConcernIsActive } from './agent-homes.mjs';
19
16
  import { operatingModeTitle } from './product-copy.mjs';
20
17
  import { collectDoctorNextActions } from './doctor-next-actions.mjs';
21
18
  export { summarizeRulesUnderContract };
@@ -28,12 +25,12 @@ const matchingLayersForRelativePath =
28
25
  import {
29
26
  collectAdoptionGaps,
30
27
  detectSkillGaps,
31
- skillGapsForActiveHost,
32
28
  detectCodexHomeGap,
33
29
  codexConcernIsActive,
34
30
  detectWritePathCapabilities,
35
31
  missingGates,
36
32
  staleRunnerGateFiles,
33
+ skillGapsForActiveHost,
37
34
  } from './agent-gates.mjs';
38
35
  import {
39
36
  baselineOccurrenceKeys,
@@ -64,17 +61,19 @@ import {
64
61
  } from './golden-pattern.mjs';
65
62
  import { summarizePilotLoop } from './pilot-loop.mjs';
66
63
  import { computeDoctorAdvisories, printDoctorAdvisories } from './doctor-advisories.mjs';
67
- import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
64
+ import { printParseHealthSection } from './parse-health.mjs';
68
65
  import { designDeltaDoctorLines } from './design-delta.mjs';
69
66
  import { enforcementDoctorLines } from './enforcement-state.mjs';
67
+ import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
68
+ import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
69
+ import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
70
+ import { writeCiMergeBoundary } from './ci-merge-boundary.mjs';
70
71
  import {
71
- buildDoctorImprovementCompass,
72
- printImprovementCompassSection,
73
- } from './improvement-compass-doctor.mjs';
74
- import {
75
- buildDeepModuleCoachAdvisory,
76
- printDeepModuleCoachSection,
77
- } from './deep-module-coach.mjs';
72
+ classifyAdopted,
73
+ githubEvidenceForCiMergeBoundary,
74
+ readAdoptionStance,
75
+ NOT_ADOPTED_NEXT_ACTION,
76
+ } from './adoption-stance.mjs';
78
77
 
79
78
  const color = {
80
79
  green: (s) => `\x1b[32m${s}\x1b[0m`,
@@ -535,6 +534,15 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
535
534
  const adoption = collectAdoptionGaps(root, config, cov);
536
535
  // Prefer writePath from adoption (same detector); recompute only if missing (tests/stubs).
537
536
  const writePath = adoption.writePath ?? detectWritePathCapabilities(root);
537
+ let ciMergeBoundary = null;
538
+ try {
539
+ ciMergeBoundary = writeCiMergeBoundary(root, {
540
+ writePath,
541
+ github: githubEvidenceForCiMergeBoundary(adoption, writePath),
542
+ });
543
+ } catch {
544
+ ciMergeBoundary = null;
545
+ }
538
546
  const baseline = readBaseline(root, '.ark-baseline.json');
539
547
  const occurrenceKeys = baselineOccurrenceKeys(violations);
540
548
  const currentKeys = new Set(occurrenceKeys);
@@ -621,6 +629,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
621
629
  : packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT'
622
630
  ? 'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)'
623
631
  : null;
632
+ const stanceFile = readAdoptionStance(root);
633
+ const githubForBoundary = githubEvidenceForCiMergeBoundary(adoption, writePath);
634
+ const adopted = classifyAdopted({
635
+ stance: stanceFile,
636
+ github: githubForBoundary,
637
+ ci: ciMergeBoundary?.ci,
638
+ });
624
639
  const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
625
640
  computeDoctorEnforcementHonesty({
626
641
  governedPercent: cov.governed.percent,
@@ -646,12 +661,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
646
661
  : rulesUnderContract?.active === true
647
662
  ? { active: true, extraMergeTeeth: false }
648
663
  : null,
649
- primaryNextAction: postGreenPath?.action ?? dualTruthNext,
664
+ primaryNextAction:
665
+ adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
650
666
  operatingMode,
651
667
  packageInstalled,
652
668
  selfHost:
653
669
  packageVersionTruth?.selfHost === true ||
654
670
  packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
671
+ adopted,
672
+ ciMergeBoundary,
673
+ github: githubForBoundary,
674
+ adoptionStance: stanceFile,
675
+ stewardNudge: doctorAdvisories.stewardNudge,
655
676
  });
656
677
 
657
678
  // Improvement compass: projection only — never feeds ok/valid/goal.met.
@@ -677,11 +698,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
677
698
  improvementCompass,
678
699
  pilotLoop,
679
700
  });
701
+ const stewardUnfinished = Boolean(
702
+ doctorAdvisories.stewardNudge?.emptyStewardsPastGrace ||
703
+ (doctorAdvisories.stewardNudge?.needsStewards &&
704
+ (doctorAdvisories.stewardNudge?.stewardCount ?? 0) === 0)
705
+ );
680
706
 
681
707
  if (asJson) {
682
708
  (options.writeJson ?? console.log)(
683
709
  JSON.stringify(
684
710
  {
711
+ schemaVersion: '1.0',
712
+ envelope: 'doctor',
685
713
  ok: analysisComplete && (options.designDelta?.valid ?? true),
686
714
  doctor: {
687
715
  completeness,
@@ -699,11 +727,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
699
727
  ...(options.designDelta ? { designDelta: options.designDelta } : {}),
700
728
  // Q01: primary next action when Shape residual dominates (null if not design-weak).
701
729
  postGreenPath,
702
- ...(postGreenPath
703
- ? { primaryNextAction: postGreenPath.action, ...DESIGN_WEAK_HONESTY_FLAGS }
704
- : productHonesty.primaryNextAction
705
- ? { primaryNextAction: productHonesty.primaryNextAction }
706
- : {}),
730
+ adoptionStance: adopted,
731
+ ...(adopted === 'not-adopted'
732
+ ? {
733
+ primaryNextAction: NOT_ADOPTED_NEXT_ACTION,
734
+ ...(postGreenPath ? DESIGN_WEAK_HONESTY_FLAGS : {}),
735
+ }
736
+ : postGreenPath
737
+ ? { primaryNextAction: postGreenPath.action, ...DESIGN_WEAK_HONESTY_FLAGS }
738
+ : productHonesty.primaryNextAction
739
+ ? { primaryNextAction: productHonesty.primaryNextAction }
740
+ : {}),
741
+ ...(stewardUnfinished
742
+ ? { healthyFinishedForbidden: true, stewardsUnfinished: true }
743
+ : {}),
707
744
  // Q03: advisory golden for new-code placement (absent = no claim).
708
745
  goldenPattern,
709
746
  // Y06: advisory pure-layer opt-in (null when not applicable).
@@ -767,6 +804,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
767
804
  skillGaps,
768
805
  ...(agentHomeGaps.length > 0 ? { agentHomeGaps } : {}),
769
806
  staleRunnerFiles: staleRunners,
807
+ ciMergeBoundary,
770
808
  writePath: {
771
809
  activeHost: writePath.activeHost,
772
810
  support: writePath.support,
@@ -827,6 +865,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
827
865
  const line = (mark, text) => console.log(` ${mark} ${text}`);
828
866
  console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
829
867
  if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
868
+ printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line });
830
869
 
831
870
  const emptyScope = emptyScopeEarly;
832
871
  const mode = operatingMode;
@@ -835,24 +874,27 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
835
874
  // Modes are detected states, not user-picked settings. Plain-language "what you do next".
836
875
  // Never paint green (ok) under design residual — edges clean ≠ design done (product-voice).
837
876
  const modeMark =
838
- mode === 'enforce' && !designFitness.designWeak
877
+ mode === 'enforce' &&
878
+ !designFitness.designWeak &&
879
+ adopted !== 'not-adopted' &&
880
+ !stewardUnfinished
839
881
  ? ok
840
882
  : warn;
841
883
  // modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce.
842
884
  const modeHelp = {
843
- suggest:
844
- 'thin or new tree; architecture config is not yet in charge. You do not pick this light. Next: ark start (preview), then ark start --apply; re-check with --doctor.',
845
- adapt:
846
- 'config and tree still disagree, or debt is open. The write path does not fully protect you yet. You do not pick this light. Next: do doctor top action #1 (often /ark-adopt or /ark-autopilot).',
885
+ suggest: 'thin or new tree. Next: ark start --apply, then doctor.',
886
+ adapt: 'config and tree still disagree. Next: do #1.',
847
887
  enforce:
848
- 'honest coverage and clean checked imports. You arrived here; you never turn Enforce on. Next: keep the host write path and CI check; only NEW violations should fail.',
888
+ adopted === 'not-adopted'
889
+ ? 'import rules check out; merge boundary not adopted.'
890
+ : 'import rules check out. Keep host + CI.',
849
891
  };
850
- const modeTitle = operatingModeTitle(mode, designFitness.designWeak);
892
+ const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished);
851
893
  line(
852
894
  modeMark,
853
895
  `${modeTitle} — ${
854
896
  designFitness.designWeak
855
- ? 'import rules check out; design smells remain. Green is not elegant design. You do not pick this light. Next: one Shape door — /ark-explore shape-focus → plan B; apply B only with /ark-autopilot and your OK. A clean import check is not done.'
897
+ ? 'import rules check out; leftover design work remains.'
856
898
  : modeHelp[mode]
857
899
  }`
858
900
  );
@@ -897,52 +939,116 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
897
939
  safetyHasEntries,
898
940
  showNewHere,
899
941
  designFitness,
942
+ operatingMode,
943
+ adopted,
944
+ stewardNudge: doctorAdvisories.stewardNudge,
900
945
  });
901
946
  console.log('');
902
- if (isDoctorHealthyNothingToDo(designFitness, uniqueActions)) {
947
+ if (ciMergeBoundary?.ci?.state) {
948
+ line(
949
+ ciMergeBoundary.ci.state === 'required' ? ok : warn,
950
+ `CI merge: ${ciMergeBoundary.ci.state}`
951
+ );
952
+ }
953
+ if (adopted === 'advisory-only-acked') {
954
+ line(warn, 'Adoption: advisory-only ack — not a required GitHub status.');
955
+ }
956
+ if (isDoctorHealthyNothingToDo(designFitness, uniqueActions, adopted)) {
903
957
  console.log(color.green('✔ Healthy — nothing to do.'));
904
- console.log(color.dim(' Contract edges and design residual are clear. Keep write path + CI.'));
958
+ console.log(color.dim(' Keep write path + CI.'));
905
959
  } else {
906
960
  console.log(color.bold('Primary next action'));
907
961
  console.log(` 1. ${uniqueActions[0]}`);
908
- if (uniqueActions.length > 1) {
909
- console.log(color.bold(`Also (${uniqueActions.length - 1}):`));
910
- uniqueActions.slice(1).forEach((action, index) => console.log(` ${index + 2}. ${action}`));
962
+ }
963
+
964
+ console.log('');
965
+ console.log(color.bold('Coverage'));
966
+ const govMark =
967
+ emptyScope || cov.governed.percent < 50
968
+ ? bad
969
+ : cov.governed.percent >= 80
970
+ ? ok
971
+ : warn;
972
+ line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
973
+
974
+ const hostRed =
975
+ gatesMissing.length > 0 ||
976
+ Boolean(writePath.gap) ||
977
+ writePathHonesty?.softWriteHost === true;
978
+ if (hostRed) {
979
+ console.log('');
980
+ console.log(color.bold('Host / CI'));
981
+ if (writePath.activeHost) line(' ', `Active host: ${writePath.activeHost}`);
982
+ if (gatesMissing.length > 0) line(bad, `Missing gates: ${gatesMissing.join(', ')}`);
983
+ else if (writePath.gap || writePathHonesty?.softWriteHost) {
984
+ line(warn, 'Local writes are advisory; required CI is the merge boundary.');
911
985
  }
912
- if (postGreenPath) {
913
- console.log(
914
- color.dim(
915
- ` Shape residual is the primary door under ${modeTitle} — do not skill-shop explore vs coverage vs think.`
916
- )
986
+ }
987
+
988
+ const nudge = doctorAdvisories.stewardNudge;
989
+ if ((nudge?.needsStewards || nudge?.drift || nudge?.emptyStewardsPastGrace) && nudge.ask) {
990
+ console.log('');
991
+ console.log(color.bold('Stewards'));
992
+ line(warn, nudge.ask);
993
+ }
994
+
995
+ if (violations.length === 0) {
996
+ if (!analysisComplete) {
997
+ console.log('');
998
+ line(
999
+ warn,
1000
+ 'No reported violations — contract compliance is not verified until analysis is complete'
1001
+ );
1002
+ } else if (emptyScope || cov.governed.percent < 50) {
1003
+ console.log('');
1004
+ line(
1005
+ warn,
1006
+ 'No active violations — coverage is still thin, so green is not yet honest enforcement'
917
1007
  );
918
- } else {
919
- console.log(color.dim(' Doctor is the control plane: do #1 first, then re-run --doctor.'));
920
1008
  }
921
1009
  }
922
1010
 
923
- // P0-B — single honesty surface (never a score; never "all good" when residual remains).
924
- if (productHonesty) {
1011
+ if (!options.all) {
925
1012
  console.log('');
926
- console.log(color.bold('Product honesty'));
1013
+ console.log(color.dim('More: --doctor --all'));
1014
+ return;
1015
+ }
1016
+
1017
+ console.log('');
1018
+ console.log(color.dim('---'));
1019
+ console.log(color.bold('Details'));
1020
+
1021
+ if (coverageHonesty.greenIsNotEnforcement) {
1022
+ line(coverageHonesty.worseThanNoGate ? bad : warn, coverageHonesty.message);
1023
+ }
1024
+ if (cov.suggestions.length > 0) {
1025
+ line(warn, `${cov.suggestions.length} ungoverned director(y/ies) — proposals: ${arkCommand(root, 'ark-check', '--coverage')}`);
1026
+ }
1027
+ if (cov.emptyLayers.length > 0) line(warn, `Empty layers (pattern matches nothing): ${cov.emptyLayers.join(', ')}`);
1028
+ if (cov.layersWithoutRules.length > 0) line(warn, `Layers with no rule edge: ${cov.layersWithoutRules.join(', ')}`);
1029
+ if (cov.dualMembership?.count > 0) {
927
1030
  line(
928
- productHonesty.unfinished ? warn : ok,
929
- `${productHonesty.headline} — ${productHonesty.primaryMessage}`
1031
+ warn,
1032
+ `Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
930
1033
  );
931
- if (Array.isArray(productHonesty.reasonIds) && productHonesty.reasonIds.length > 0) {
932
- line(' ', color.dim(`signals: ${productHonesty.reasonIds.join(', ')} (notAScore)`));
933
- }
934
- if (rulesUnderContract?.mergePlanes?.failMergeWhen) {
935
- line(
936
- ' ',
937
- color.dim(
938
- `merge planes: ${rulesUnderContract.mergePlanes.failMergeWhen} · ${rulesUnderContract.mergePlanes.dualPlaneStamp}`
939
- )
940
- );
941
- }
942
1034
  }
1035
+ if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
943
1036
 
944
- printImprovementCompassSection(improvementCompass, { line, warn, ok, color });
945
- printDeepModuleCoachSection(deepModuleCoach, { line, warn, ok, color });
1037
+ if (packageVersionTruth?.dualTruth) {
1038
+ console.log('');
1039
+ console.log(color.bold('Package pin (dual-truth)'));
1040
+ line(warn, packageVersionTruth.note);
1041
+ } else if (packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT') {
1042
+ console.log('');
1043
+ console.log(color.bold('Package pin'));
1044
+ line(warn, packageVersionTruth.note);
1045
+ }
1046
+ if (options.configWalkedUp && options.configRoot) {
1047
+ line(
1048
+ ok,
1049
+ `Config walk-up: using monorepo root ${options.configRoot} (ark.config.json not in cwd package)`
1050
+ );
1051
+ }
946
1052
 
947
1053
  console.log('');
948
1054
  console.log(color.bold('Design fitness'));
@@ -951,7 +1057,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
951
1057
  } else {
952
1058
  line(designFitness.designWeak ? warn : warn, designFitness.label);
953
1059
  for (const smell of designSmells.slice(0, 5)) {
954
- // Q02: outcome-first (plain language); technical message stays in JSON + dim detail.
955
1060
  const outcome = smell.outcome || smell.message;
956
1061
  line(' ', `[${smell.id}] ${outcome}`);
957
1062
  if (smell.outcome && smell.message && smell.message !== smell.outcome) {
@@ -961,7 +1066,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
961
1066
  line(' ', color.dim(`evidence: ${smell.evidence.slice(0, 4).join(', ')}`));
962
1067
  }
963
1068
  }
964
- // Q04 — surface one next pilot under leftover design work.
965
1069
  if (pilotLoop?.active && pilotLoop.nextPilot) {
966
1070
  const np = pilotLoop.nextPilot;
967
1071
  line(
@@ -980,7 +1084,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
980
1084
  line(row.level === 'bad' ? bad : row.level === 'ok' ? ok : ' ', row.level === 'dim' ? color.dim(row.text) : row.text);
981
1085
  }
982
1086
 
983
- // Q03 — optional golden pattern note (advisory for new code only).
984
1087
  if (goldenPattern.present) {
985
1088
  console.log('');
986
1089
  console.log(color.bold('Golden pattern (new code)'));
@@ -999,76 +1102,11 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
999
1102
  'Fix or remove it — absence is fine; a bad file is not guidance.'
1000
1103
  );
1001
1104
  }
1002
- // Y06 — one-line pure-layer opt-in (U05 voice; never blocker).
1003
1105
  if (pureLayerOptIn) {
1004
1106
  line(' ', color.dim(pureLayerOptIn.message));
1005
1107
  }
1006
1108
 
1007
- printDoctorAdvisories(doctorAdvisories, { line, warn, color }); // advisory sections
1008
-
1009
- console.log('');
1010
- console.log(color.bold('Coverage'));
1011
- const govMark =
1012
- emptyScope || cov.governed.percent < 50
1013
- ? bad
1014
- : cov.governed.percent >= 80
1015
- ? ok
1016
- : warn;
1017
- line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
1018
- if (coverageHonesty.greenIsNotEnforcement) {
1019
- line(coverageHonesty.worseThanNoGate ? bad : warn, coverageHonesty.message);
1020
- }
1021
- if (cov.suggestions.length > 0) {
1022
- line(warn, `${cov.suggestions.length} ungoverned director(y/ies) — proposals: ${arkCommand(root, 'ark-check', '--coverage')}`);
1023
- }
1024
- if (cov.emptyLayers.length > 0) line(warn, `Empty layers (pattern matches nothing): ${cov.emptyLayers.join(', ')}`);
1025
- if (cov.layersWithoutRules.length > 0) line(warn, `Layers with no rule edge: ${cov.layersWithoutRules.join(', ')}`);
1026
- if (cov.dualMembership?.count > 0) {
1027
- line(
1028
- warn,
1029
- `Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
1030
- );
1031
- }
1032
- if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
1033
-
1034
- if (packageVersionTruth?.dualTruth) {
1035
- console.log('');
1036
- console.log(color.bold('Package pin (dual-truth)'));
1037
- line(warn, packageVersionTruth.note);
1038
- } else if (packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT') {
1039
- console.log('');
1040
- console.log(color.bold('Package pin'));
1041
- line(warn, packageVersionTruth.note);
1042
- }
1043
- if (options.configWalkedUp && options.configRoot) {
1044
- line(
1045
- ok,
1046
- `Config walk-up: using monorepo root ${options.configRoot} (ark.config.json not in cwd package)`
1047
- );
1048
- }
1049
-
1050
- if (showNewHere) {
1051
- console.log('');
1052
- console.log(color.bold('New here?'));
1053
- // Suggest residual: start → doctor only (not a competing recommend/architect curriculum).
1054
- line(ok, `Primary path: ${arkCommand(root, 'ark', 'start')} (preview) → ${arkCommand(root, 'ark', 'start --apply')} → re-run --doctor`);
1055
- if (recommendation) {
1056
- line(warn, `Sensor shape hint (not a second curriculum): ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
1057
- if (recommendation.galleryStarter) line(ok, `Gallery starter (optional): ${recommendation.galleryStarter}`);
1058
- if (recommendation.policyPack) {
1059
- line(ok, `Policy pack (optional expert): ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
1060
- }
1061
- if (recommendation.signals?.nestFramework) {
1062
- line(ok, 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)');
1063
- }
1064
- if (recommendation.signals?.monorepoTooling?.length) {
1065
- line(ok, `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`);
1066
- }
1067
- } else {
1068
- line(warn, 'Low governed coverage or fresh config — finish start, then re-run doctor before adding layers of code.');
1069
- }
1070
- line(ok, `Optional sensor detail: ${arkCommand(root, 'ark-check', '--recommend')}`);
1071
- }
1109
+ printDoctorAdvisories(doctorAdvisories, { line, warn, color });
1072
1110
 
1073
1111
  console.log('');
1074
1112
  console.log(color.bold('Violations'));
@@ -1121,7 +1159,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1121
1159
  }
1122
1160
  const enforcement = writePath.enforcementState;
1123
1161
  for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
1124
- // EH07 Repair: use support/matrix caps (inventory omits envelope-emitted on Codex).
1125
1162
  const supportCaps = writePath.support?.capabilities || {};
1126
1163
  const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
1127
1164
  const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
@@ -1182,7 +1219,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1182
1219
  codexHomeGap.stale > 0 ? `${codexHomeGap.stale} content-behind-package` : null, codexHomeGap.catalogStateReason,
1183
1220
  ].filter(Boolean);
1184
1221
  const deferred = !codexConcernIsActive();
1185
- // Deferred home debt is dim/info (not warn) so non-Codex sessions are not "incomplete".
1186
1222
  if (deferred) {
1187
1223
  line(color.dim('·'), color.dim(`Codex home skills ${parts.join(', ')} (deferred — not on Codex session)`));
1188
1224
  } else {
@@ -1209,8 +1245,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1209
1245
  if (!baseline.exists) {
1210
1246
  line(!analysisComplete || violations.length > 0 ? warn : ok, !analysisComplete ? 'No baseline — current violations were not fully evaluated' : violations.length > 0 ? 'No baseline — adopting a dirty repo? freeze with --update-baseline' : 'No baseline (nothing to freeze)');
1211
1247
  } else {
1212
- // Baseline keys are line-agnostic, so N keys can suppress ≥N violations — label as keys
1213
- // to avoid an apparent mismatch with the "frozen" violation count above.
1214
1248
  const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok;
1215
1249
  line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
1216
1250
  if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
@@ -1228,7 +1262,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1228
1262
  line(warn, `Stale runner in ${staleRunners.join(', ')}`);
1229
1263
  }
1230
1264
 
1231
- // Adoption completeness (hosts, MCP health, codex home, core optionality, origin, baseline policy)
1232
1265
  console.log('');
1233
1266
  console.log(color.bold('Adoption (separate from fitness score)'));
1234
1267
  if (adoption.gaps.length === 0 && !adoption.layerBalance) {
@@ -1238,8 +1271,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1238
1271
  );
1239
1272
  } else {
1240
1273
  for (const gap of adoption.gaps) {
1241
- // Deferred Codex-home debt (non-temp) is annotated, not a top action, when the
1242
- // session host is not Codex — fix when that host is used.
1243
1274
  const mark = gap.deferred
1244
1275
  ? color.dim('·')
1245
1276
  : gap.severity === 'warn'
@@ -10,6 +10,11 @@ import {
10
10
  HOST_SUPPORT_MATRIX,
11
11
  HOST_SUPPORT_HOSTS,
12
12
  } from './host-support-matrix.mjs';
13
+ import {
14
+ classifyAdopted,
15
+ MERGE_BOUNDARY_NOT_REQUIRED,
16
+ NOT_ADOPTED_NEXT_ACTION,
17
+ } from './adoption-stance.mjs';
13
18
 
14
19
  /** Soft = matrix hard-write false; hard-capable = matrix hard-write true. Single source of truth. */
15
20
  function hostWriteClassSets() {
@@ -380,6 +385,37 @@ export function buildProductHonesty(input = {}) {
380
385
  });
381
386
  }
382
387
 
388
+ const adopted =
389
+ typeof input.adopted === 'string'
390
+ ? input.adopted
391
+ : input.ciMergeBoundary || input.github || input.adoptionStance || input.considerMergeBoundary
392
+ ? classifyAdopted({
393
+ stance: input.adoptionStance,
394
+ github: input.github,
395
+ ci: input.ciMergeBoundary?.ci,
396
+ })
397
+ : null;
398
+ if (adopted === 'not-adopted') {
399
+ reasons.push({
400
+ id: MERGE_BOUNDARY_NOT_REQUIRED,
401
+ message:
402
+ 'Merge boundary not adopted — require a GitHub status running arkgate-check --strict-merge, or write .ark/adoption-stance.json with stance: "advisory-only".',
403
+ });
404
+ }
405
+
406
+ const emptyStewards =
407
+ input.emptyStewards === true ||
408
+ input.stewardNudge?.emptyStewardsPastGrace === true ||
409
+ (input.stewardNudge?.needsStewards === true &&
410
+ (input.stewardNudge?.stewardCount ?? 0) === 0);
411
+ if (emptyStewards) {
412
+ reasons.push({
413
+ id: 'empty-stewards',
414
+ message:
415
+ 'No stewards listed — not a finished Enforce. Name GitHub handles or emails for stewards[], or this stays Adapt-or-nudge. /ark-adopt asks; it does not invent names.',
416
+ });
417
+ }
418
+
383
419
  // Mode adapt/suggest (FG-FINISHED-ADAPT-DEBT): prefer unfinished unless the tree is
384
420
  // whole-tree green AND zero design smells AND zero blocking violations.
385
421
  // Type-only placement debt alone must not keep adapt unfinished via active-blocking.
@@ -442,6 +478,8 @@ export function buildProductHonesty(input = {}) {
442
478
  architectureReasons.find((r) => r.id === 'package-pin-absent') ||
443
479
  architectureReasons.find((r) => r.id === 'baseline-missing-with-debt') ||
444
480
  architectureReasons.find((r) => r.id === 'residual-pilot') ||
481
+ architectureReasons.find((r) => r.id === MERGE_BOUNDARY_NOT_REQUIRED) ||
482
+ architectureReasons.find((r) => r.id === 'empty-stewards') ||
445
483
  architectureReasons[0] ||
446
484
  environmentResiduals[0];
447
485
 
@@ -450,6 +488,9 @@ export function buildProductHonesty(input = {}) {
450
488
  primaryMessage =
451
489
  primary?.message ||
452
490
  'Not finished: residual honesty signals remain (violations, mode, coverage, freeze, design, package pin, or pilots).';
491
+ } else if (adopted === 'advisory-only-acked') {
492
+ primaryMessage =
493
+ 'This tree acked advisory-only in .ark/adoption-stance.json. That is not a required GitHub merge status.';
453
494
  } else if (softWriteOnly) {
454
495
  primaryMessage = `${hostLabel} local writes stay advisory/bypassable; architecture contract on this slice is ready. Hard merge boundary is a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).`;
455
496
  } else if (wholeTreeGoverned) {
@@ -468,6 +509,8 @@ export function buildProductHonesty(input = {}) {
468
509
  headline = wholeTreeGoverned
469
510
  ? `Architecture contract ready; ${hostLabel} local writes are advisory`
470
511
  : `Contract residual clear; ${hostLabel} local writes are advisory`;
512
+ } else if (!unfinished && adopted === 'advisory-only-acked') {
513
+ headline = 'Advisory-only adoption — merge status is not required';
471
514
  } else if (!unfinished) {
472
515
  headline = 'Honesty clear on residual signals';
473
516
  } else if (coverageIncomplete) {
@@ -487,6 +530,12 @@ export function buildProductHonesty(input = {}) {
487
530
  } else if (!primaryNextAction && pinAbsent) {
488
531
  primaryNextAction =
489
532
  'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)';
533
+ } else if (!primaryNextAction && adopted === 'not-adopted') {
534
+ primaryNextAction = NOT_ADOPTED_NEXT_ACTION;
535
+ } else if (!primaryNextAction && emptyStewards) {
536
+ primaryNextAction =
537
+ input.stewardNudge?.nextAction ||
538
+ '/ark-adopt (ask, then update stewards[] — do not invent names)';
490
539
  } else if (!primaryNextAction && softWriteOnly) {
491
540
  primaryNextAction =
492
541
  'Confirm the GitHub required status context name runs arkgate-check --strict-merge (or ark-check --strict-merge). Soft-write hosts stay advisory at local write; the required status is the hard merge boundary.';
@@ -523,6 +572,8 @@ export function buildProductHonesty(input = {}) {
523
572
  primaryMessage,
524
573
  primaryNextAction,
525
574
  headline,
575
+ hookGreenIsNotTreeGreen: true,
576
+ scriptedEditsBypassPreToolUse: true,
526
577
  };
527
578
  }
528
579
 
@@ -556,6 +607,12 @@ export function computeDoctorEnforcementHonesty({
556
607
  packageInstalled,
557
608
  selfHost,
558
609
  motherCli,
610
+ adopted: adoptedInput,
611
+ ciMergeBoundary,
612
+ github,
613
+ adoptionStance,
614
+ emptyStewards,
615
+ stewardNudge,
559
616
  } = {}) {
560
617
  const coverageHonesty = buildCoverageHonesty({
561
618
  percent: governedPercent,
@@ -581,6 +638,14 @@ export function computeDoctorEnforcementHonesty({
581
638
  const blockingForHonesty = Number.isFinite(Number(activeBlockingViolations))
582
639
  ? Math.max(0, Number(activeBlockingViolations))
583
640
  : Number(activeViolations) || 0;
641
+ const adopted =
642
+ typeof adoptedInput === 'string'
643
+ ? adoptedInput
644
+ : classifyAdopted({
645
+ stance: adoptionStance,
646
+ github,
647
+ ci: ciMergeBoundary?.ci,
648
+ });
584
649
  const productHonesty = buildProductHonesty({
585
650
  coverageHonesty,
586
651
  baselineHonesty,
@@ -596,6 +661,13 @@ export function computeDoctorEnforcementHonesty({
596
661
  primaryNextAction,
597
662
  operatingMode,
598
663
  activeBlockingViolations: blockingForHonesty,
664
+ adopted,
665
+ ciMergeBoundary,
666
+ github,
667
+ adoptionStance,
668
+ considerMergeBoundary: true,
669
+ emptyStewards,
670
+ stewardNudge,
599
671
  });
600
672
  return {
601
673
  coverageHonesty,
@@ -5,14 +5,11 @@
5
5
  export function setupUsage() {
6
6
  return `arkgate (alias ark) — One architecture config. One check. One coach.
7
7
 
8
- arkgate start preview what will change (no writes)
9
- arkgate start --apply write the compact contract + host router + CI
10
- arkgate-check --doctor status light + primary next action
8
+ arkgate start preview (no writes)
9
+ arkgate start --apply write host + CI setup
10
+ arkgate-check --doctor where you are + one next action
11
11
 
12
- Then session 0 in your agent: /ark-adopt
13
12
  Stuck? Run doctor. Do #1.
14
-
15
- More commands and flags: arkgate --help --all
16
13
  `;
17
14
  }
18
15
 
@@ -114,7 +111,7 @@ export function checkUsageAll() {
114
111
  'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
115
112
  ' arkgate-check --version',
116
113
  ' arkgate-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--policy-base <file> | --policy-base-ref <git-ref>] [--policy-ack <file>] [--fail-on-new-smells --base-ref <git-ref>] [--contract-diff] [--contract-session] [--changed] [--against <git-ref>] [--base <git-ref>] [--persona touch|contributor|agent|steward] [--author <id>] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
117
- ' ark-check --doctor [--json] [--resident] [--fail-on-new-smells --base-ref <git-ref>] read-only diagnosis; resident JSON falls back cold',
114
+ ' ark-check --doctor [--json] [--all] [--resident] [--fail-on-new-smells --base-ref <git-ref>] compact first screen; --all prints Details; resident JSON falls back cold',
118
115
  ' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
119
116
  ' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
120
117
  ' ark-check --rules-inventory [--json] brownfield rules inventory (AR13; deterministic candidates, not a score)',
@@ -183,6 +180,10 @@ export function checkUsageAll() {
183
180
  'transition. Weakening or judgment-required findings fail unless --policy-ack names',
184
181
  'every finding and is bound to both policy hashes. Use --policy-base/--policy-base-ref',
185
182
  'for an explicit comparison; ARK_POLICY_BASE_REF is the CI environment equivalent.',
183
+ 'The same merge profile blocks new UI business-rule files (domain-logic-in-ui) created',
184
+ 'versus that base; leftover design on existing files stays green. Missing base skips',
185
+ 'this check (does not exit 2). --fail-on-new-smells --base-ref remains the full',
186
+ 'new+worsened-on-touched-paths ratchet.',
186
187
  'Add --require-write-hook claude|grok|antigravity|cursor|codex to validate a hard local',
187
188
  'write boundary for that specific host. Codex covers complete local apply_patch only;',
188
189
  'hosted/specialized/direct-write paths and OpenCode remain CI-backed. Merge blocking requires',