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.
- package/CHANGELOG.md +44 -2106
- package/README.md +10 -9
- package/bin/ark-check-runtime.mjs +38 -2
- package/bin/ark.mjs +13 -3
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/ci-merge-boundary.mjs +4 -2
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +1 -1
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +20 -2
- package/bin/lib/doctor-plan.mjs +152 -135
- package/bin/lib/enforcement-honesty.mjs +70 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/mcp-adoption.mjs +19 -0
- package/bin/lib/policy-delta-io.mjs +1 -1
- package/bin/lib/post-green-path.mjs +5 -1
- package/bin/lib/product-copy.mjs +6 -3
- package/bin/lib/start-preview.mjs +12 -22
- package/bin/lib/status-command.mjs +16 -0
- package/bin/lib/status-manifest.mjs +8 -2
- package/bin/lib/team-parliament-io.mjs +62 -2
- package/bin/lib/team-parliament.mjs +25 -5
- package/bin/lib/unavailable-analysis.mjs +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +2 -2
- package/docs/README.md +6 -10
- package/docs/ai-gates.md +12 -5
- package/docs/configuration.md +9 -1
- package/docs/diagnostics.md +2 -2
- package/docs/package-surface.md +5 -4
- package/docs/product-voice.md +6 -4
- package/docs/threat-model.md +2 -2
- package/docs/use.md +5 -4
- package/package.json +1 -1
- package/schemas/ark.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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' &&
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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 (
|
|
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('
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
938
|
-
if (productHonesty) {
|
|
1011
|
+
if (!options.all) {
|
|
939
1012
|
console.log('');
|
|
940
|
-
console.log(color.
|
|
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
|
-
|
|
943
|
-
|
|
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
|
-
|
|
959
|
-
|
|
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 });
|
|
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
|
|
9
|
-
arkgate start --apply write
|
|
10
|
-
arkgate-check --doctor
|
|
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>]
|
|
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
|
|
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"
|
|
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>
|