arkgate 4.6.5 → 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 (41) hide show
  1. package/CHANGELOG.md +44 -2106
  2. package/README.md +10 -9
  3. package/bin/ark-check-runtime.mjs +38 -2
  4. package/bin/ark.mjs +13 -3
  5. package/bin/lib/adoption-stance.mjs +104 -0
  6. package/bin/lib/ci-merge-boundary.mjs +4 -2
  7. package/bin/lib/design-delta.mjs +2 -2
  8. package/bin/lib/design-smells.mjs +1 -1
  9. package/bin/lib/diagnostic-catalog.mjs +1 -1
  10. package/bin/lib/doctor-advisories.mjs +2 -2
  11. package/bin/lib/doctor-next-actions.mjs +20 -2
  12. package/bin/lib/doctor-plan.mjs +152 -135
  13. package/bin/lib/enforcement-honesty.mjs +70 -0
  14. package/bin/lib/first-run-help.mjs +8 -7
  15. package/bin/lib/html-report-advisories.mjs +10 -2
  16. package/bin/lib/html-report.mjs +2 -2
  17. package/bin/lib/mcp-adoption.mjs +19 -0
  18. package/bin/lib/policy-delta-io.mjs +1 -1
  19. package/bin/lib/post-green-path.mjs +5 -1
  20. package/bin/lib/product-copy.mjs +6 -3
  21. package/bin/lib/start-preview.mjs +12 -22
  22. package/bin/lib/status-command.mjs +16 -0
  23. package/bin/lib/status-manifest.mjs +8 -2
  24. package/bin/lib/team-parliament-io.mjs +62 -2
  25. package/bin/lib/team-parliament.mjs +25 -5
  26. package/bin/lib/unavailable-analysis.mjs +1 -0
  27. package/dist/index.cjs +2 -2
  28. package/dist/index.d.ts +10 -2
  29. package/dist/index.js +2 -2
  30. package/docs/README.md +6 -10
  31. package/docs/ai-gates.md +12 -5
  32. package/docs/configuration.md +9 -1
  33. package/docs/diagnostics.md +2 -2
  34. package/docs/package-surface.md +5 -4
  35. package/docs/product-voice.md +6 -4
  36. package/docs/threat-model.md +2 -2
  37. package/docs/use.md +5 -4
  38. package/package.json +1 -1
  39. package/schemas/ark.design-delta.schema.json +1 -1
  40. package/server.json +2 -2
  41. package/templates/agent-skills/README.md +1 -1
@@ -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,18 +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';
70
- import {
71
- buildDoctorImprovementCompass,
72
- printImprovementCompassSection,
73
- } from './improvement-compass-doctor.mjs';
74
- import {
75
- buildDeepModuleCoachAdvisory,
76
- printDeepModuleCoachSection,
77
- } from './deep-module-coach.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';
78
70
  import { writeCiMergeBoundary } from './ci-merge-boundary.mjs';
71
+ import {
72
+ classifyAdopted,
73
+ githubEvidenceForCiMergeBoundary,
74
+ readAdoptionStance,
75
+ NOT_ADOPTED_NEXT_ACTION,
76
+ } from './adoption-stance.mjs';
79
77
 
80
78
  const color = {
81
79
  green: (s) => `\x1b[32m${s}\x1b[0m`,
@@ -540,7 +538,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
540
538
  try {
541
539
  ciMergeBoundary = writeCiMergeBoundary(root, {
542
540
  writePath,
543
- github: adoption.deployPath?.github ?? writePath.enforcementState?.ciMerge ?? {},
541
+ github: githubEvidenceForCiMergeBoundary(adoption, writePath),
544
542
  });
545
543
  } catch {
546
544
  ciMergeBoundary = null;
@@ -631,6 +629,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
631
629
  : packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT'
632
630
  ? 'Add arkgate to package.json and install so CI/npx resolve this CLI (PACKAGE_PIN_ABSENT)'
633
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
+ });
634
639
  const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
635
640
  computeDoctorEnforcementHonesty({
636
641
  governedPercent: cov.governed.percent,
@@ -656,12 +661,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
656
661
  : rulesUnderContract?.active === true
657
662
  ? { active: true, extraMergeTeeth: false }
658
663
  : null,
659
- primaryNextAction: postGreenPath?.action ?? dualTruthNext,
664
+ primaryNextAction:
665
+ adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
660
666
  operatingMode,
661
667
  packageInstalled,
662
668
  selfHost:
663
669
  packageVersionTruth?.selfHost === true ||
664
670
  packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
671
+ adopted,
672
+ ciMergeBoundary,
673
+ github: githubForBoundary,
674
+ adoptionStance: stanceFile,
675
+ stewardNudge: doctorAdvisories.stewardNudge,
665
676
  });
666
677
 
667
678
  // Improvement compass: projection only — never feeds ok/valid/goal.met.
@@ -687,6 +698,11 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
687
698
  improvementCompass,
688
699
  pilotLoop,
689
700
  });
701
+ const stewardUnfinished = Boolean(
702
+ doctorAdvisories.stewardNudge?.emptyStewardsPastGrace ||
703
+ (doctorAdvisories.stewardNudge?.needsStewards &&
704
+ (doctorAdvisories.stewardNudge?.stewardCount ?? 0) === 0)
705
+ );
690
706
 
691
707
  if (asJson) {
692
708
  (options.writeJson ?? console.log)(
@@ -711,11 +727,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
711
727
  ...(options.designDelta ? { designDelta: options.designDelta } : {}),
712
728
  // Q01: primary next action when Shape residual dominates (null if not design-weak).
713
729
  postGreenPath,
714
- ...(postGreenPath
715
- ? { primaryNextAction: postGreenPath.action, ...DESIGN_WEAK_HONESTY_FLAGS }
716
- : productHonesty.primaryNextAction
717
- ? { primaryNextAction: productHonesty.primaryNextAction }
718
- : {}),
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
+ : {}),
719
744
  // Q03: advisory golden for new-code placement (absent = no claim).
720
745
  goldenPattern,
721
746
  // Y06: advisory pure-layer opt-in (null when not applicable).
@@ -840,6 +865,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
840
865
  const line = (mark, text) => console.log(` ${mark} ${text}`);
841
866
  console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
842
867
  if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
868
+ printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line });
843
869
 
844
870
  const emptyScope = emptyScopeEarly;
845
871
  const mode = operatingMode;
@@ -848,24 +874,27 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
848
874
  // Modes are detected states, not user-picked settings. Plain-language "what you do next".
849
875
  // Never paint green (ok) under design residual — edges clean ≠ design done (product-voice).
850
876
  const modeMark =
851
- mode === 'enforce' && !designFitness.designWeak
877
+ mode === 'enforce' &&
878
+ !designFitness.designWeak &&
879
+ adopted !== 'not-adopted' &&
880
+ !stewardUnfinished
852
881
  ? ok
853
882
  : warn;
854
883
  // modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce.
855
884
  const modeHelp = {
856
- suggest:
857
- '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.',
858
- adapt:
859
- '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.',
860
887
  enforce:
861
- '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.',
862
891
  };
863
- const modeTitle = operatingModeTitle(mode, designFitness.designWeak);
892
+ const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished);
864
893
  line(
865
894
  modeMark,
866
895
  `${modeTitle} — ${
867
896
  designFitness.designWeak
868
- ? '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.'
869
898
  : modeHelp[mode]
870
899
  }`
871
900
  );
@@ -911,52 +940,115 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
911
940
  showNewHere,
912
941
  designFitness,
913
942
  operatingMode,
943
+ adopted,
944
+ stewardNudge: doctorAdvisories.stewardNudge,
914
945
  });
915
946
  console.log('');
916
- 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)) {
917
957
  console.log(color.green('✔ Healthy — nothing to do.'));
918
- console.log(color.dim(' Contract edges and design residual are clear. Keep write path + CI.'));
958
+ console.log(color.dim(' Keep write path + CI.'));
919
959
  } else {
920
960
  console.log(color.bold('Primary next action'));
921
961
  console.log(` 1. ${uniqueActions[0]}`);
922
- if (uniqueActions.length > 1) {
923
- console.log(color.bold(`Also (${uniqueActions.length - 1}):`));
924
- 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.');
925
985
  }
926
- if (postGreenPath) {
927
- console.log(
928
- color.dim(
929
- ` Shape residual is the primary door under ${modeTitle} — do not skill-shop explore vs coverage vs think.`
930
- )
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'
931
1007
  );
932
- } else {
933
- console.log(color.dim(' Doctor is the control plane: do #1 first, then re-run --doctor.'));
934
1008
  }
935
1009
  }
936
1010
 
937
- // P0-B — single honesty surface (never a score; never "all good" when residual remains).
938
- if (productHonesty) {
1011
+ if (!options.all) {
939
1012
  console.log('');
940
- 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) {
941
1030
  line(
942
- productHonesty.unfinished ? warn : ok,
943
- `${productHonesty.headline} — ${productHonesty.primaryMessage}`
1031
+ warn,
1032
+ `Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
944
1033
  );
945
- if (Array.isArray(productHonesty.reasonIds) && productHonesty.reasonIds.length > 0) {
946
- line(' ', color.dim(`signals: ${productHonesty.reasonIds.join(', ')} (notAScore)`));
947
- }
948
- if (rulesUnderContract?.mergePlanes?.failMergeWhen) {
949
- line(
950
- ' ',
951
- color.dim(
952
- `merge planes: ${rulesUnderContract.mergePlanes.failMergeWhen} · ${rulesUnderContract.mergePlanes.dualPlaneStamp}`
953
- )
954
- );
955
- }
956
1034
  }
1035
+ if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
957
1036
 
958
- printImprovementCompassSection(improvementCompass, { line, warn, ok, color });
959
- 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
+ }
960
1052
 
961
1053
  console.log('');
962
1054
  console.log(color.bold('Design fitness'));
@@ -965,7 +1057,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
965
1057
  } else {
966
1058
  line(designFitness.designWeak ? warn : warn, designFitness.label);
967
1059
  for (const smell of designSmells.slice(0, 5)) {
968
- // Q02: outcome-first (plain language); technical message stays in JSON + dim detail.
969
1060
  const outcome = smell.outcome || smell.message;
970
1061
  line(' ', `[${smell.id}] ${outcome}`);
971
1062
  if (smell.outcome && smell.message && smell.message !== smell.outcome) {
@@ -975,7 +1066,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
975
1066
  line(' ', color.dim(`evidence: ${smell.evidence.slice(0, 4).join(', ')}`));
976
1067
  }
977
1068
  }
978
- // Q04 — surface one next pilot under leftover design work.
979
1069
  if (pilotLoop?.active && pilotLoop.nextPilot) {
980
1070
  const np = pilotLoop.nextPilot;
981
1071
  line(
@@ -994,7 +1084,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
994
1084
  line(row.level === 'bad' ? bad : row.level === 'ok' ? ok : ' ', row.level === 'dim' ? color.dim(row.text) : row.text);
995
1085
  }
996
1086
 
997
- // Q03 — optional golden pattern note (advisory for new code only).
998
1087
  if (goldenPattern.present) {
999
1088
  console.log('');
1000
1089
  console.log(color.bold('Golden pattern (new code)'));
@@ -1013,76 +1102,11 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1013
1102
  'Fix or remove it — absence is fine; a bad file is not guidance.'
1014
1103
  );
1015
1104
  }
1016
- // Y06 — one-line pure-layer opt-in (U05 voice; never blocker).
1017
1105
  if (pureLayerOptIn) {
1018
1106
  line(' ', color.dim(pureLayerOptIn.message));
1019
1107
  }
1020
1108
 
1021
- printDoctorAdvisories(doctorAdvisories, { line, warn, color }); // advisory sections
1022
-
1023
- console.log('');
1024
- console.log(color.bold('Coverage'));
1025
- const govMark =
1026
- emptyScope || cov.governed.percent < 50
1027
- ? bad
1028
- : cov.governed.percent >= 80
1029
- ? ok
1030
- : warn;
1031
- line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
1032
- if (coverageHonesty.greenIsNotEnforcement) {
1033
- line(coverageHonesty.worseThanNoGate ? bad : warn, coverageHonesty.message);
1034
- }
1035
- if (cov.suggestions.length > 0) {
1036
- line(warn, `${cov.suggestions.length} ungoverned director(y/ies) — proposals: ${arkCommand(root, 'ark-check', '--coverage')}`);
1037
- }
1038
- if (cov.emptyLayers.length > 0) line(warn, `Empty layers (pattern matches nothing): ${cov.emptyLayers.join(', ')}`);
1039
- if (cov.layersWithoutRules.length > 0) line(warn, `Layers with no rule edge: ${cov.layersWithoutRules.join(', ')}`);
1040
- if (cov.dualMembership?.count > 0) {
1041
- line(
1042
- warn,
1043
- `Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
1044
- );
1045
- }
1046
- if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
1047
-
1048
- if (packageVersionTruth?.dualTruth) {
1049
- console.log('');
1050
- console.log(color.bold('Package pin (dual-truth)'));
1051
- line(warn, packageVersionTruth.note);
1052
- } else if (packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT') {
1053
- console.log('');
1054
- console.log(color.bold('Package pin'));
1055
- line(warn, packageVersionTruth.note);
1056
- }
1057
- if (options.configWalkedUp && options.configRoot) {
1058
- line(
1059
- ok,
1060
- `Config walk-up: using monorepo root ${options.configRoot} (ark.config.json not in cwd package)`
1061
- );
1062
- }
1063
-
1064
- if (showNewHere) {
1065
- console.log('');
1066
- console.log(color.bold('New here?'));
1067
- // Suggest residual: start → doctor only (not a competing recommend/architect curriculum).
1068
- line(ok, `Primary path: ${arkCommand(root, 'ark', 'start')} (preview) → ${arkCommand(root, 'ark', 'start --apply')} → re-run --doctor`);
1069
- if (recommendation) {
1070
- line(warn, `Sensor shape hint (not a second curriculum): ${recommendation.archetype} — ${recommendation.label} (preset ${recommendation.preset})`);
1071
- if (recommendation.galleryStarter) line(ok, `Gallery starter (optional): ${recommendation.galleryStarter}`);
1072
- if (recommendation.policyPack) {
1073
- line(ok, `Policy pack (optional expert): ${arkCommand(root, 'ark-check', `--apply-policy-pack ${recommendation.policyPack}`)}`);
1074
- }
1075
- if (recommendation.signals?.nestFramework) {
1076
- line(ok, 'Nest modular monolith → prefer hexagonal (or ddd-bounded-contexts if you have src/contexts/*)');
1077
- }
1078
- if (recommendation.signals?.monorepoTooling?.length) {
1079
- line(ok, `Monorepo tooling (${recommendation.signals.monorepoTooling.join(', ')}) → preset monorepo (apps/packages/libs)`);
1080
- }
1081
- } else {
1082
- line(warn, 'Low governed coverage or fresh config — finish start, then re-run doctor before adding layers of code.');
1083
- }
1084
- line(ok, `Optional sensor detail: ${arkCommand(root, 'ark-check', '--recommend')}`);
1085
- }
1109
+ printDoctorAdvisories(doctorAdvisories, { line, warn, color });
1086
1110
 
1087
1111
  console.log('');
1088
1112
  console.log(color.bold('Violations'));
@@ -1135,7 +1159,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1135
1159
  }
1136
1160
  const enforcement = writePath.enforcementState;
1137
1161
  for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
1138
- // EH07 Repair: use support/matrix caps (inventory omits envelope-emitted on Codex).
1139
1162
  const supportCaps = writePath.support?.capabilities || {};
1140
1163
  const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
1141
1164
  const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
@@ -1196,7 +1219,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1196
1219
  codexHomeGap.stale > 0 ? `${codexHomeGap.stale} content-behind-package` : null, codexHomeGap.catalogStateReason,
1197
1220
  ].filter(Boolean);
1198
1221
  const deferred = !codexConcernIsActive();
1199
- // Deferred home debt is dim/info (not warn) so non-Codex sessions are not "incomplete".
1200
1222
  if (deferred) {
1201
1223
  line(color.dim('·'), color.dim(`Codex home skills ${parts.join(', ')} (deferred — not on Codex session)`));
1202
1224
  } else {
@@ -1223,8 +1245,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1223
1245
  if (!baseline.exists) {
1224
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)');
1225
1247
  } else {
1226
- // Baseline keys are line-agnostic, so N keys can suppress ≥N violations — label as keys
1227
- // to avoid an apparent mismatch with the "frozen" violation count above.
1228
1248
  const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok;
1229
1249
  line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
1230
1250
  if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
@@ -1242,7 +1262,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1242
1262
  line(warn, `Stale runner in ${staleRunners.join(', ')}`);
1243
1263
  }
1244
1264
 
1245
- // Adoption completeness (hosts, MCP health, codex home, core optionality, origin, baseline policy)
1246
1265
  console.log('');
1247
1266
  console.log(color.bold('Adoption (separate from fitness score)'));
1248
1267
  if (adoption.gaps.length === 0 && !adoption.layerBalance) {
@@ -1252,8 +1271,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
1252
1271
  );
1253
1272
  } else {
1254
1273
  for (const gap of adoption.gaps) {
1255
- // Deferred Codex-home debt (non-temp) is annotated, not a top action, when the
1256
- // session host is not Codex — fix when that host is used.
1257
1274
  const mark = gap.deferred
1258
1275
  ? color.dim('·')
1259
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.';
@@ -558,6 +607,12 @@ export function computeDoctorEnforcementHonesty({
558
607
  packageInstalled,
559
608
  selfHost,
560
609
  motherCli,
610
+ adopted: adoptedInput,
611
+ ciMergeBoundary,
612
+ github,
613
+ adoptionStance,
614
+ emptyStewards,
615
+ stewardNudge,
561
616
  } = {}) {
562
617
  const coverageHonesty = buildCoverageHonesty({
563
618
  percent: governedPercent,
@@ -583,6 +638,14 @@ export function computeDoctorEnforcementHonesty({
583
638
  const blockingForHonesty = Number.isFinite(Number(activeBlockingViolations))
584
639
  ? Math.max(0, Number(activeBlockingViolations))
585
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
+ });
586
649
  const productHonesty = buildProductHonesty({
587
650
  coverageHonesty,
588
651
  baselineHonesty,
@@ -598,6 +661,13 @@ export function computeDoctorEnforcementHonesty({
598
661
  primaryNextAction,
599
662
  operatingMode,
600
663
  activeBlockingViolations: blockingForHonesty,
664
+ adopted,
665
+ ciMergeBoundary,
666
+ github,
667
+ adoptionStance,
668
+ considerMergeBoundary: true,
669
+ emptyStewards,
670
+ stewardNudge,
601
671
  });
602
672
  return {
603
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',
@@ -313,17 +313,25 @@ function deepModuleCoachHtml(coach) {
313
313
 
314
314
  function stewardNudgeHtml(nudge) {
315
315
  if (!nudge || nudge.notAScore !== true) return '';
316
+ const unfinished = Boolean(
317
+ nudge.emptyStewardsPastGrace || (nudge.needsStewards && (nudge.stewardCount ?? 0) === 0)
318
+ );
316
319
  const ask =
317
- (nudge.needsStewards || nudge.drift) && typeof nudge.ask === 'string' && nudge.ask
320
+ (nudge.needsStewards || nudge.drift || nudge.emptyStewardsPastGrace) &&
321
+ typeof nudge.ask === 'string' &&
322
+ nudge.ask
318
323
  ? `<p>${esc(nudge.ask)}</p>`
319
324
  : '<p class="muted">No steward list gap (advisory).</p>';
320
325
  const next =
321
326
  typeof nudge.nextAction === 'string' && nudge.nextAction
322
327
  ? `<p class="muted">Next: ${esc(nudge.nextAction)}</p>`
323
328
  : '';
329
+ const qualifier = unfinished
330
+ ? '(unfinished residual — changes finished, not check valid)'
331
+ : '(advisory — never changes the check valid bit)';
324
332
  return `
325
333
  <section class="section card" data-advisory="stewardNudge">
326
- <h2>Stewards <span class="muted">(advisory — never changes the verdict)</span></h2>
334
+ <h2>Stewards <span class="muted">${qualifier}</span></h2>
327
335
  ${ask}
328
336
  ${next}
329
337
  <p class="muted">GitHub handle or email. Never invent names. Always <code>notAScore</code>.</p>