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.
- package/CHANGELOG.md +77 -2105
- package/README.md +11 -9
- package/bin/ark-check-runtime.mjs +136 -16
- package/bin/ark-mcp-runtime.mjs +18 -30
- package/bin/ark.mjs +13 -3
- package/bin/lib/adapter-contract.mjs +13 -9
- package/bin/lib/adoption-stance.mjs +104 -0
- package/bin/lib/agent-projection-command.mjs +18 -0
- package/bin/lib/agent-projection.mjs +2 -2
- package/bin/lib/analysis-engine.mjs +5 -5
- package/bin/lib/ci-and-commands.mjs +3 -3
- package/bin/lib/ci-merge-boundary.mjs +91 -0
- package/bin/lib/config-contract.mjs +2 -0
- package/bin/lib/design-delta.mjs +2 -2
- package/bin/lib/design-smells.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +6 -5
- package/bin/lib/doctor-advisories.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +35 -5
- package/bin/lib/doctor-plan.mjs +164 -133
- package/bin/lib/enforcement-honesty.mjs +72 -0
- package/bin/lib/first-run-help.mjs +8 -7
- package/bin/lib/graph-blind.mjs +15 -6
- package/bin/lib/html-report-advisories.mjs +10 -2
- package/bin/lib/html-report.mjs +2 -2
- package/bin/lib/install-migrate.mjs +10 -0
- package/bin/lib/invariant-coverage.mjs +6 -2
- package/bin/lib/managed-upgrade.mjs +8 -3
- 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/presets.mjs +22 -0
- package/bin/lib/product-copy.mjs +6 -3
- package/bin/lib/remediation.mjs +74 -10
- package/bin/lib/skill-install.mjs +2 -0
- package/bin/lib/snippet-analysis.mjs +40 -8
- 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/{configTypes-B8uIcLaG.d.ts → configTypes-l6XiwiC1.d.ts} +7 -0
- package/dist/eslint/index.cjs +3 -3
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +3 -3
- package/dist/index.cjs +26 -26
- package/dist/index.d.ts +20 -3
- package/dist/index.js +29 -29
- package/docs/README.md +6 -9
- package/docs/agent-guide.md +10 -0
- package/docs/ai-gates.md +12 -5
- package/docs/brownfield-adoption.md +7 -1
- package/docs/configuration.md +11 -2
- package/docs/develop.md +4 -2
- package/docs/diagnostics.md +17 -7
- package/docs/package-surface.md +6 -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.config.schema.json +6 -0
- package/schemas/ark.design-delta.schema.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +7 -0
- package/templates/agent-skills/ark-explore/SKILL.md +6 -0
- package/templates/agent-skills/ark-place/SKILL.md +11 -4
- package/templates/agent-skills/ark-upgrade/SKILL.md +9 -2
- package/templates/skills/ark-adopt.md +7 -0
- package/templates/skills/ark-explore.md +6 -0
- package/templates/skills/ark-place.md +11 -4
- package/templates/skills/ark-upgrade.md +9 -2
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,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 {
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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' &&
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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 (
|
|
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('
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
924
|
-
if (productHonesty) {
|
|
1011
|
+
if (!options.all) {
|
|
925
1012
|
console.log('');
|
|
926
|
-
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) {
|
|
927
1030
|
line(
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
945
|
-
|
|
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 });
|
|
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
|
|
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',
|