arkgate 4.6.6 → 4.6.7
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 +30 -2
- package/README.md +5 -4
- package/bin/ark-check-runtime.mjs +8 -340
- package/bin/ark-mcp-runtime.mjs +7 -323
- package/bin/ark-shared.mjs +24 -158
- package/bin/lib/check-args.mjs +173 -0
- package/bin/lib/check-config-detect.mjs +101 -0
- package/bin/lib/check-watch.mjs +80 -0
- package/bin/lib/deep-module-coach.mjs +3 -0
- package/bin/lib/doctor-human.mjs +509 -0
- package/bin/lib/doctor-plan.mjs +42 -429
- package/bin/lib/github-enforcement.mjs +22 -9
- package/bin/lib/html-report.mjs +24 -7
- package/bin/lib/mcp-hook-payload.mjs +328 -0
- package/bin/lib/package-manager.mjs +174 -0
- package/bin/lib/policy-delta-io.mjs +4 -0
- package/bin/lib/team-parliament-io.mjs +4 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/README.md +2 -2
- package/docs/package-surface.md +2 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -12,9 +12,10 @@ 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 { detectAgentHomeGaps
|
|
16
|
-
import { operatingModeTitle } from './product-copy.mjs';
|
|
15
|
+
import { detectAgentHomeGaps } from './agent-homes.mjs';
|
|
17
16
|
import { collectDoctorNextActions } from './doctor-next-actions.mjs';
|
|
17
|
+
import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
|
|
18
|
+
export { printDoctorCompactHuman, printDoctorDetailsHuman };
|
|
18
19
|
export { summarizeRulesUnderContract };
|
|
19
20
|
|
|
20
21
|
/** Optional S3 dual-match classifier when ark-shared exports it (soft dep for S5 landing). */
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
detectWritePathCapabilities,
|
|
31
32
|
missingGates,
|
|
32
33
|
staleRunnerGateFiles,
|
|
33
|
-
skillGapsForActiveHost,
|
|
34
34
|
} from './agent-gates.mjs';
|
|
35
35
|
import {
|
|
36
36
|
baselineOccurrenceKeys,
|
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
} from './design-smells.mjs';
|
|
48
48
|
import {
|
|
49
49
|
buildPostGreenNextAction,
|
|
50
|
-
isDoctorHealthyNothingToDo,
|
|
51
50
|
DESIGN_WEAK_HONESTY_FLAGS,
|
|
52
51
|
} from './post-green-path.mjs';
|
|
53
52
|
import {
|
|
@@ -60,10 +59,7 @@ import {
|
|
|
60
59
|
summarizeGoldenPattern,
|
|
61
60
|
} from './golden-pattern.mjs';
|
|
62
61
|
import { summarizePilotLoop } from './pilot-loop.mjs';
|
|
63
|
-
import { computeDoctorAdvisories
|
|
64
|
-
import { printParseHealthSection } from './parse-health.mjs';
|
|
65
|
-
import { designDeltaDoctorLines } from './design-delta.mjs';
|
|
66
|
-
import { enforcementDoctorLines } from './enforcement-state.mjs';
|
|
62
|
+
import { computeDoctorAdvisories } from './doctor-advisories.mjs';
|
|
67
63
|
import { ANALYSIS_COMPLETENESS, analysisIncompleteStatement, normalizeAnalysisCompleteness } from './analysis-completeness.mjs';
|
|
68
64
|
import { buildDoctorImprovementCompass } from './improvement-compass-doctor.mjs';
|
|
69
65
|
import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
|
|
@@ -859,52 +855,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
859
855
|
return;
|
|
860
856
|
}
|
|
861
857
|
|
|
862
|
-
const ok = color.green('✓');
|
|
863
|
-
const warn = color.yellow('!');
|
|
864
|
-
const bad = color.red('✗');
|
|
865
|
-
const line = (mark, text) => console.log(` ${mark} ${text}`);
|
|
866
|
-
console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
|
|
867
|
-
if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
|
|
868
|
-
printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line });
|
|
869
|
-
|
|
870
858
|
const emptyScope = emptyScopeEarly;
|
|
871
|
-
const mode = operatingMode;
|
|
872
|
-
console.log('');
|
|
873
|
-
console.log(color.bold('Operating mode'));
|
|
874
|
-
// Modes are detected states, not user-picked settings. Plain-language "what you do next".
|
|
875
|
-
// Never paint green (ok) under design residual — edges clean ≠ design done (product-voice).
|
|
876
|
-
const modeMark =
|
|
877
|
-
mode === 'enforce' &&
|
|
878
|
-
!designFitness.designWeak &&
|
|
879
|
-
adopted !== 'not-adopted' &&
|
|
880
|
-
!stewardUnfinished
|
|
881
|
-
? ok
|
|
882
|
-
: warn;
|
|
883
|
-
// modeTitle alone names the light — bodies must not re-prefix Suggest/Adapt/Enforce.
|
|
884
|
-
const modeHelp = {
|
|
885
|
-
suggest: 'thin or new tree. Next: ark start --apply, then doctor.',
|
|
886
|
-
adapt: 'config and tree still disagree. Next: do #1.',
|
|
887
|
-
enforce:
|
|
888
|
-
adopted === 'not-adopted'
|
|
889
|
-
? 'import rules check out; merge boundary not adopted.'
|
|
890
|
-
: 'import rules check out. Keep host + CI.',
|
|
891
|
-
};
|
|
892
|
-
const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished);
|
|
893
|
-
line(
|
|
894
|
-
modeMark,
|
|
895
|
-
`${modeTitle} — ${
|
|
896
|
-
designFitness.designWeak
|
|
897
|
-
? 'import rules check out; leftover design work remains.'
|
|
898
|
-
: modeHelp[mode]
|
|
899
|
-
}`
|
|
900
|
-
);
|
|
901
|
-
if (emptyScope) {
|
|
902
|
-
line(
|
|
903
|
-
bad,
|
|
904
|
-
'Empty scope: include paths match 0 source files — a green check is meaningless until include/layers match the tree (monorepo → apps/packages, or /ark-adopt).'
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
859
|
const safetyHasEntries = Boolean(
|
|
909
860
|
options.safety &&
|
|
910
861
|
[
|
|
@@ -943,381 +894,43 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
943
894
|
adopted,
|
|
944
895
|
stewardNudge: doctorAdvisories.stewardNudge,
|
|
945
896
|
});
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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.');
|
|
985
|
-
}
|
|
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'
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
if (!options.all) {
|
|
1012
|
-
console.log('');
|
|
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) {
|
|
1030
|
-
line(
|
|
1031
|
-
warn,
|
|
1032
|
-
`Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
|
|
1036
|
-
|
|
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
|
-
}
|
|
1052
|
-
|
|
1053
|
-
console.log('');
|
|
1054
|
-
console.log(color.bold('Design fitness'));
|
|
1055
|
-
if (designSmells.length === 0) {
|
|
1056
|
-
line(analysisComplete ? ok : warn, designFitness.label);
|
|
1057
|
-
} else {
|
|
1058
|
-
line(designFitness.designWeak ? warn : warn, designFitness.label);
|
|
1059
|
-
for (const smell of designSmells.slice(0, 5)) {
|
|
1060
|
-
const outcome = smell.outcome || smell.message;
|
|
1061
|
-
line(' ', `[${smell.id}] ${outcome}`);
|
|
1062
|
-
if (smell.outcome && smell.message && smell.message !== smell.outcome) {
|
|
1063
|
-
line(' ', color.dim(`detail: ${smell.message}`));
|
|
1064
|
-
}
|
|
1065
|
-
if (smell.evidence?.length) {
|
|
1066
|
-
line(' ', color.dim(`evidence: ${smell.evidence.slice(0, 4).join(', ')}`));
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
if (pilotLoop?.active && pilotLoop.nextPilot) {
|
|
1070
|
-
const np = pilotLoop.nextPilot;
|
|
1071
|
-
line(
|
|
1072
|
-
warn,
|
|
1073
|
-
`Next pilot (one at a time): ${np.pilotTarget || np.pilot} [${np.smellId}] → re-doctor after change`
|
|
1074
|
-
);
|
|
1075
|
-
line(' ', color.dim(`success: ${np.successSignal}`));
|
|
1076
|
-
line(' ', color.dim('never multi-pilot batch; pattern bets are never auto-applied'));
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
if (options.designDelta) {
|
|
1081
|
-
console.log('');
|
|
1082
|
-
console.log(color.bold('Design delta (opt-in)'));
|
|
1083
|
-
for (const row of designDeltaDoctorLines(options.designDelta))
|
|
1084
|
-
line(row.level === 'bad' ? bad : row.level === 'ok' ? ok : ' ', row.level === 'dim' ? color.dim(row.text) : row.text);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
if (goldenPattern.present) {
|
|
1088
|
-
console.log('');
|
|
1089
|
-
console.log(color.bold('Golden pattern (new code)'));
|
|
1090
|
-
line(
|
|
1091
|
-
ok,
|
|
1092
|
-
`"${goldenPattern.name}" — ${goldenPattern.norm}` +
|
|
1093
|
-
(goldenPattern.newCodeHome ? ` Prefer: ${goldenPattern.newCodeHome}.` : '') +
|
|
1094
|
-
' Advisory only — does not clear leftover design work or replace the gate.'
|
|
1095
|
-
);
|
|
1096
|
-
} else if (goldenPattern.invalid) {
|
|
1097
|
-
console.log('');
|
|
1098
|
-
console.log(color.bold('Golden pattern (new code)'));
|
|
1099
|
-
line(
|
|
1100
|
-
warn,
|
|
1101
|
-
`${goldenPattern.path} is present but invalid (${goldenPattern.error || 'invalid'}). ` +
|
|
1102
|
-
'Fix or remove it — absence is fine; a bad file is not guidance.'
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
if (pureLayerOptIn) {
|
|
1106
|
-
line(' ', color.dim(pureLayerOptIn.message));
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
printDoctorAdvisories(doctorAdvisories, { line, warn, color });
|
|
1110
|
-
|
|
1111
|
-
console.log('');
|
|
1112
|
-
console.log(color.bold('Violations'));
|
|
1113
|
-
if (violations.length === 0) {
|
|
1114
|
-
if (!analysisComplete) line(warn, 'No reported violations — contract compliance is not verified until analysis is complete');
|
|
1115
|
-
else if (emptyScope || cov.governed.percent < 50) {
|
|
1116
|
-
line(
|
|
1117
|
-
warn,
|
|
1118
|
-
'No active violations — coverage is still thin, so green is not yet honest enforcement'
|
|
1119
|
-
);
|
|
1120
|
-
} else if (designFitness.designWeak) {
|
|
1121
|
-
line(warn, `None on checked imports — import rules match the config; leftover design work remains (${modeTitle}). Not healthy finished.`);
|
|
1122
|
-
} else {
|
|
1123
|
-
line(ok, 'None — the code matches the contract on checked edges');
|
|
1124
|
-
}
|
|
1125
|
-
} else {
|
|
1126
|
-
const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
|
|
1127
|
-
const supNote = suppressed > 0 ? `, ${suppressed} frozen` : '';
|
|
1128
|
-
line(
|
|
1129
|
-
activeCount > 0 ? warn : ok,
|
|
1130
|
-
`${violations.length} total${typeNote}${supNote}${activeCount > 0 ? ` — ${activeCount} NOT baselined` : ''}`
|
|
1131
|
-
);
|
|
1132
|
-
for (const edge of summary.edges.slice(0, 3)) line(' ', color.dim(`${edge.count} ${edge.edge}`));
|
|
1133
|
-
if (summary.concentrated) {
|
|
1134
|
-
line(warn, color.dim(`${Math.round(summary.dominantShare * 100)}% on one edge (${summary.dominant}) — likely a contract fix, not debt`));
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
console.log('');
|
|
1139
|
-
console.log(color.bold('Write path (agent)'));
|
|
1140
|
-
const capabilities = writePath.capabilities;
|
|
1141
|
-
const writePathLabels = {
|
|
1142
|
-
repair: 'repair-capable — hard block + machine-readable autoPatch / ARK_REPAIR_JSON',
|
|
1143
|
-
'reject-only': 'reject-only — hard block with prose; no repair payload',
|
|
1144
|
-
'mcp-only': 'MCP tools only — prepare-write/autoPatch available; no PreToolUse hook',
|
|
1145
|
-
none: 'no write gate hook and no Ark MCP',
|
|
897
|
+
const humanView = {
|
|
898
|
+
root,
|
|
899
|
+
analysisComplete,
|
|
900
|
+
completeness,
|
|
901
|
+
doctorAdvisories,
|
|
902
|
+
operatingMode,
|
|
903
|
+
designFitness,
|
|
904
|
+
adopted,
|
|
905
|
+
stewardUnfinished,
|
|
906
|
+
emptyScope,
|
|
907
|
+
options,
|
|
908
|
+
uniqueActions,
|
|
909
|
+
ciMergeBoundary,
|
|
910
|
+
cov,
|
|
911
|
+
writePath,
|
|
912
|
+
writePathHonesty,
|
|
913
|
+
gatesMissing,
|
|
914
|
+
violations,
|
|
915
|
+
coverageHonesty,
|
|
916
|
+
packageVersionTruth,
|
|
917
|
+
designSmells,
|
|
918
|
+
pilotLoop,
|
|
919
|
+
goldenPattern,
|
|
920
|
+
pureLayerOptIn,
|
|
921
|
+
summary,
|
|
922
|
+
suppressed,
|
|
923
|
+
activeCount,
|
|
924
|
+
skillGaps,
|
|
925
|
+
agentHomeGaps,
|
|
926
|
+
baseline,
|
|
927
|
+
baselineHonesty,
|
|
928
|
+
staleBaseline,
|
|
929
|
+
staleRunners,
|
|
930
|
+
adoption,
|
|
931
|
+
color,
|
|
1146
932
|
};
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
? ok
|
|
1150
|
-
: capabilities['advisory-write'] || capabilities['merge-gate']
|
|
1151
|
-
? warn
|
|
1152
|
-
: bad;
|
|
1153
|
-
line(' ', `Active host: ${writePath.activeHost}`);
|
|
1154
|
-
line(' ', `Supported profile: ${writePath.supportSummary}`);
|
|
1155
|
-
line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
|
|
1156
|
-
if (writePathHonesty.message) line(warn, writePathHonesty.message);
|
|
1157
|
-
if (writePath.sessionNote) {
|
|
1158
|
-
line(warn, writePath.sessionNote);
|
|
1159
|
-
}
|
|
1160
|
-
const enforcement = writePath.enforcementState;
|
|
1161
|
-
for (const row of enforcementDoctorLines(enforcement)) line(row.level === 'ok' ? ok : row.level === 'bad' ? bad : warn, row.text);
|
|
1162
|
-
const supportCaps = writePath.support?.capabilities || {};
|
|
1163
|
-
const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
|
|
1164
|
-
const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
|
|
1165
|
-
line(
|
|
1166
|
-
repairReinjection ? ok : warn,
|
|
1167
|
-
repairReinjection
|
|
1168
|
-
? 'Repair: envelope + reinjection guaranteed on hard path when installed + trusted'
|
|
1169
|
-
: repairEnvelope
|
|
1170
|
-
? 'Repair: envelope may emit (`--hook-repair`); reinjection not guaranteed (advisory host)'
|
|
1171
|
-
: 'Repair: no hard-boundary payload'
|
|
1172
|
-
);
|
|
1173
|
-
if (writePath.gap) {
|
|
1174
|
-
line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message);
|
|
1175
|
-
if (writePath.gap.fix) {
|
|
1176
|
-
line(' ', color.dim(`Fix: ${writePath.gap.fix}`));
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
console.log('');
|
|
1181
|
-
console.log(color.bold('Gates & skills'));
|
|
1182
|
-
if (gatesMissing.length === 0) line(ok, 'Shared gate artifacts found on disk (AGENTS.md, .mcp.json, CI); runtime activation is reported separately');
|
|
1183
|
-
else {
|
|
1184
|
-
line(bad, `Missing gates: ${gatesMissing.join(', ')}`);
|
|
1185
|
-
}
|
|
1186
|
-
const humanSkillGaps = skillGapsForActiveHost(skillGaps);
|
|
1187
|
-
const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
|
|
1188
|
-
const codexLegacySafeDelete = humanSkillGaps.some(
|
|
1189
|
-
(g) => g.tool === 'codex' && g.legacyAdvisory && g.catalogComplete
|
|
1190
|
-
);
|
|
1191
|
-
const remainingGaps = humanSkillGaps.filter(
|
|
1192
|
-
(g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory))
|
|
1193
|
-
);
|
|
1194
|
-
const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
|
|
1195
|
-
const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
|
|
1196
|
-
if (remMiss + remStale === 0 && !legacyCodex) line(ok, '/ark-* skills current for detected tools');
|
|
1197
|
-
if (legacyCodex) {
|
|
1198
|
-
line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)');
|
|
1199
|
-
}
|
|
1200
|
-
if (codexLegacySafeDelete) {
|
|
1201
|
-
line(
|
|
1202
|
-
' ',
|
|
1203
|
-
color.dim(
|
|
1204
|
-
'Codex catalog complete — leftover .codex/prompts/ark-*.md are safe to delete (not loadable; not required).'
|
|
1205
|
-
)
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
if (remMiss + remStale > 0) {
|
|
1209
|
-
line(
|
|
1210
|
-
warn,
|
|
1211
|
-
`${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}`
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
const codexHomeGap = detectCodexHomeGap(root);
|
|
1215
|
-
if (codexHomeGap) {
|
|
1216
|
-
const parts = [
|
|
1217
|
-
codexHomeGap.legacyPromptsOnly ? 'legacy-prompts-only' : null,
|
|
1218
|
-
codexHomeGap.missing > 0 ? `${codexHomeGap.missing} missing` : null,
|
|
1219
|
-
codexHomeGap.stale > 0 ? `${codexHomeGap.stale} content-behind-package` : null, codexHomeGap.catalogStateReason,
|
|
1220
|
-
].filter(Boolean);
|
|
1221
|
-
const deferred = !codexConcernIsActive();
|
|
1222
|
-
if (deferred) {
|
|
1223
|
-
line(color.dim('·'), color.dim(`Codex home skills ${parts.join(', ')} (deferred — not on Codex session)`));
|
|
1224
|
-
} else {
|
|
1225
|
-
line(warn, `Codex home skills ${parts.join(', ')}`);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
for (const gap of agentHomeGaps) {
|
|
1229
|
-
const parts = [
|
|
1230
|
-
gap.missing > 0 ? `${gap.missing} missing` : null,
|
|
1231
|
-
gap.stale > 0 ? `${gap.stale} content-behind-package` : null,
|
|
1232
|
-
gap.catalogStateReason,
|
|
1233
|
-
].filter(Boolean);
|
|
1234
|
-
const deferred = !agentHomeConcernIsActive(gap.host);
|
|
1235
|
-
const summary = `${gap.label} shared agent skills ${parts.join(', ')}`;
|
|
1236
|
-
if (deferred) {
|
|
1237
|
-
line(color.dim('·'), color.dim(`${summary} (deferred — not this session)`));
|
|
1238
|
-
} else {
|
|
1239
|
-
line(warn, summary);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
console.log('');
|
|
1244
|
-
console.log(color.bold('Baseline'));
|
|
1245
|
-
if (!baseline.exists) {
|
|
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)');
|
|
1247
|
-
} else {
|
|
1248
|
-
const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok;
|
|
1249
|
-
line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
|
|
1250
|
-
if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
|
|
1251
|
-
line(warn, baselineHonesty.message);
|
|
1252
|
-
}
|
|
1253
|
-
if (analysisComplete && staleBaseline > 0) {
|
|
1254
|
-
line(warn, `${staleBaseline} stale entr(y/ies) no longer occur — tighten with --update-baseline`);
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
console.log('');
|
|
1259
|
-
console.log(color.bold('Command runners'));
|
|
1260
|
-
if (staleRunners.length === 0) line(ok, 'Emitted commands match the package manager');
|
|
1261
|
-
else {
|
|
1262
|
-
line(warn, `Stale runner in ${staleRunners.join(', ')}`);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
console.log('');
|
|
1266
|
-
console.log(color.bold('Adoption (separate from fitness score)'));
|
|
1267
|
-
if (adoption.gaps.length === 0 && !adoption.layerBalance) {
|
|
1268
|
-
line(
|
|
1269
|
-
ok,
|
|
1270
|
-
'Hosts, MCP argv, core optionality, origin report, baseline policy, and deploy-path lint/types look complete'
|
|
1271
|
-
);
|
|
1272
|
-
} else {
|
|
1273
|
-
for (const gap of adoption.gaps) {
|
|
1274
|
-
const mark = gap.deferred
|
|
1275
|
-
? color.dim('·')
|
|
1276
|
-
: gap.severity === 'warn'
|
|
1277
|
-
? warn
|
|
1278
|
-
: gap.severity === 'info'
|
|
1279
|
-
? warn
|
|
1280
|
-
: bad;
|
|
1281
|
-
line(mark, gap.message);
|
|
1282
|
-
if (gap.fix) {
|
|
1283
|
-
line(' ', color.dim(gap.deferred ? `When using Codex: ${gap.fix}` : `Fix: ${gap.fix}`));
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
if (adoption.layerBalance) {
|
|
1287
|
-
line(warn, color.dim(adoption.layerBalance.educational));
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
if (adoption.baseline) {
|
|
1291
|
-
line(
|
|
1292
|
-
' ',
|
|
1293
|
-
color.dim(
|
|
1294
|
-
`Baseline policy: ${adoption.baseline.signal}` +
|
|
1295
|
-
(adoption.baseline.primaryPathUsesBaseline
|
|
1296
|
-
? ' · primary path uses --baseline'
|
|
1297
|
-
: ' · primary path does not use --baseline')
|
|
1298
|
-
)
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
if (adoption.originReport.present) {
|
|
1302
|
-
line(ok, 'Origin architecture snapshot present (.ark/reports/origin.json)');
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
console.log('');
|
|
1306
|
-
console.log(color.bold('Safety / bypass resistance'));
|
|
1307
|
-
const safety = options.safety;
|
|
1308
|
-
if (!safety) {
|
|
1309
|
-
line(warn, 'Safety diagnostics unavailable');
|
|
1310
|
-
} else {
|
|
1311
|
-
const rows = [
|
|
1312
|
-
['Non-literal dynamic dependencies', safety.nonLiteralDynamicImports],
|
|
1313
|
-
['@ts-ignore / @ts-nocheck', safety.tsSuppressions],
|
|
1314
|
-
['Explicit any casts', safety.anyCasts],
|
|
1315
|
-
['InMemory stores in production source', safety.inMemoryProductionStores],
|
|
1316
|
-
['Rules with peerIsolation: false', safety.disabledPeerIsolationRules],
|
|
1317
|
-
];
|
|
1318
|
-
for (const [label, entries] of rows) {
|
|
1319
|
-
line(entries.length === 0 ? ok : warn, `${label}: ${entries.length}`);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
|
|
933
|
+
printDoctorCompactHuman(humanView);
|
|
934
|
+
if (options.all) printDoctorDetailsHuman(humanView);
|
|
1323
935
|
}
|
|
936
|
+
|
|
@@ -3,6 +3,19 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
/** Kill hung gh instead of stalling CI. */
|
|
7
|
+
export const SPAWN_TIMEOUT_MS = 8000;
|
|
8
|
+
|
|
9
|
+
function runGh(args, opts = {}) {
|
|
10
|
+
const { run, ...rest } = opts;
|
|
11
|
+
const spawn = typeof run === 'function' ? run : spawnSync;
|
|
12
|
+
return spawn('gh', args, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
...rest,
|
|
15
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
const IF_LINE = /^[ \t]*(?:-\s+)?(?:"if"|'if'|if):\s*(.*?)\s*(?:#.*)?$/i;
|
|
7
20
|
const CONTINUE_LINE = /^[ \t]*(?:-\s+)?(?:"continue-on-error"|'continue-on-error'|continue-on-error):\s*(.*?)\s*(?:#.*)?$/i;
|
|
8
21
|
const SAFE_IF = /^(?:['"]?true['"]?|['"]?\$\{\{\s*(?:true|always\(\))\s*\}\}['"]?)$/i;
|
|
@@ -434,7 +447,7 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
434
447
|
const cwd = opts.cwd ?? process.cwd();
|
|
435
448
|
const env = opts.env ?? process.env;
|
|
436
449
|
const limit = Number.isFinite(Number(opts.limit)) ? Math.max(1, Number(opts.limit)) : 30;
|
|
437
|
-
if (
|
|
450
|
+
if (runGh(['--version'], { env, run: opts.run }).status !== 0) {
|
|
438
451
|
return { runtimeObserved: false, latestCiRun: null, reason: 'gh-cli-unavailable' };
|
|
439
452
|
}
|
|
440
453
|
const args = [
|
|
@@ -443,7 +456,7 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
443
456
|
'--json', 'name,conclusion,status,workflowName,displayTitle,event',
|
|
444
457
|
];
|
|
445
458
|
if (opts.repo) args.push('--repo', opts.repo);
|
|
446
|
-
const result =
|
|
459
|
+
const result = runGh(args, { cwd, env, run: opts.run });
|
|
447
460
|
if (result.status !== 0) {
|
|
448
461
|
const err = `${result.stderr || ''}${result.stdout || ''}`.slice(0, 400);
|
|
449
462
|
return {
|
|
@@ -499,14 +512,14 @@ export function reportGithubCiRuntime(opts = {}) {
|
|
|
499
512
|
export function reportGithubBranchProtection(opts = {}) {
|
|
500
513
|
const cwd = opts.cwd ?? process.cwd();
|
|
501
514
|
const env = opts.env ?? process.env;
|
|
502
|
-
if (
|
|
515
|
+
if (runGh(['--version'], { env, run: opts.run }).status !== 0) {
|
|
503
516
|
return { available: false, reason: 'gh-cli-unavailable', runtimeObserved: false, latestCiRun: null };
|
|
504
517
|
}
|
|
505
518
|
let repo = opts.repo;
|
|
506
519
|
let branch = opts.branch;
|
|
507
520
|
if (!repo || !branch) {
|
|
508
521
|
const args = ['repo', 'view', ...(repo ? [repo] : []), '--json', 'nameWithOwner,defaultBranchRef'];
|
|
509
|
-
const metadata = parseJson(
|
|
522
|
+
const metadata = parseJson(runGh(args, { cwd, env, run: opts.run }));
|
|
510
523
|
if (!metadata?.nameWithOwner || !metadata?.defaultBranchRef?.name) {
|
|
511
524
|
return { available: false, reason: 'gh-repo-unavailable', runtimeObserved: false, latestCiRun: null };
|
|
512
525
|
}
|
|
@@ -514,13 +527,13 @@ export function reportGithubBranchProtection(opts = {}) {
|
|
|
514
527
|
branch ??= metadata.defaultBranchRef.name;
|
|
515
528
|
}
|
|
516
529
|
|
|
517
|
-
const classicResult =
|
|
530
|
+
const classicResult = runGh([
|
|
518
531
|
'api', `repos/${repo}/branches/${encodeURIComponent(branch)}/protection`, '--jq',
|
|
519
532
|
'{strict: .required_status_checks.strict, contexts: .required_status_checks.contexts, checks: .required_status_checks.checks, enforcesAdmins: .enforce_admins.enabled}',
|
|
520
|
-
], { cwd,
|
|
521
|
-
const rulesResult =
|
|
522
|
-
|
|
523
|
-
{ cwd,
|
|
533
|
+
], { cwd, env, run: opts.run });
|
|
534
|
+
const rulesResult = runGh(
|
|
535
|
+
['api', `repos/${repo}/rules/branches/${encodeURIComponent(branch)}`],
|
|
536
|
+
{ cwd, env, run: opts.run }
|
|
524
537
|
);
|
|
525
538
|
const classic = parseJson(classicResult);
|
|
526
539
|
const rules = parseJson(rulesResult);
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -28,6 +28,9 @@ import { captureGitSnapshot } from './report-snapshot-context.mjs';
|
|
|
28
28
|
|
|
29
29
|
export { arkGitignoreAppendDecision, gitignoreCoversArkState, gitignoreHasArkNegationException } from './ark-gitignore.mjs';
|
|
30
30
|
|
|
31
|
+
/** Cap the rendered violation list so showcase HTML cannot dump unbounded findings. */
|
|
32
|
+
export const HTML_REPORT_VIOLATION_LIST_CAP = 12;
|
|
33
|
+
|
|
31
34
|
export function detectEnforcement(root) {
|
|
32
35
|
const has = (rel) => fs.existsSync(path.join(root, rel));
|
|
33
36
|
const fileIncludes = (rel, needle) => {
|
|
@@ -392,9 +395,13 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
392
395
|
})
|
|
393
396
|
.join('\n');
|
|
394
397
|
|
|
398
|
+
const listedBeginner = violations.slice(0, HTML_REPORT_VIOLATION_LIST_CAP);
|
|
399
|
+
const hiddenBeginner = Math.max(0, violations.length - listedBeginner.length);
|
|
400
|
+
const beginnerRemainder = hiddenBeginner
|
|
401
|
+
? `<div class="dim">+${hiddenBeginner} more (${violations.length} total)</div>`
|
|
402
|
+
: '';
|
|
395
403
|
const violationRows = violations.length
|
|
396
|
-
?
|
|
397
|
-
.slice(0, 12)
|
|
404
|
+
? listedBeginner
|
|
398
405
|
.map((v) => {
|
|
399
406
|
const enriched = enrichViolationWithFixClass(v);
|
|
400
407
|
return `<li><code>${esc(v.file)}:${v.line}</code> — ${esc(enriched.enthusiastHint ?? v.message)}</li>`;
|
|
@@ -436,6 +443,7 @@ export function renderBeginnerHtmlReport({ root, config, violations, ok, version
|
|
|
436
443
|
</table>
|
|
437
444
|
<h2>What to fix first</h2>
|
|
438
445
|
<ul>${violationRows}</ul>
|
|
446
|
+
${beginnerRemainder}
|
|
439
447
|
<h2>Next steps</h2>
|
|
440
448
|
<p><code>${arkCheckCommand(root)}</code></p>
|
|
441
449
|
<p><code>${arkCommand(root, 'ark-check', '--recommend')}</code></p>
|
|
@@ -781,13 +789,22 @@ export function renderHtmlReport({
|
|
|
781
789
|
})
|
|
782
790
|
.join('\n');
|
|
783
791
|
|
|
784
|
-
const
|
|
792
|
+
const listedViolations = violations.slice(0, HTML_REPORT_VIOLATION_LIST_CAP);
|
|
793
|
+
const hiddenViolationCount = Math.max(0, violations.length - listedViolations.length);
|
|
794
|
+
const ruleTotals = new Map();
|
|
785
795
|
for (const v of violations) {
|
|
796
|
+
ruleTotals.set(v.ruleId, (ruleTotals.get(v.ruleId) || 0) + 1);
|
|
797
|
+
}
|
|
798
|
+
const byRule = new Map();
|
|
799
|
+
for (const v of listedViolations) {
|
|
786
800
|
if (!byRule.has(v.ruleId)) byRule.set(v.ruleId, []);
|
|
787
801
|
byRule.get(v.ruleId).push(v);
|
|
788
802
|
}
|
|
789
|
-
const
|
|
790
|
-
?
|
|
803
|
+
const remainderNote = hiddenViolationCount
|
|
804
|
+
? `<div class="dim">+${hiddenViolationCount} more (${violations.length} total)</div>`
|
|
805
|
+
: '';
|
|
806
|
+
const violationBlocks = listedViolations.length
|
|
807
|
+
? `${[...byRule.entries()]
|
|
791
808
|
.map(([ruleId, items]) => {
|
|
792
809
|
const hint = FIX_HINTS[ruleId];
|
|
793
810
|
const rows = items
|
|
@@ -803,12 +820,12 @@ export function renderHtmlReport({
|
|
|
803
820
|
})
|
|
804
821
|
.join('\n');
|
|
805
822
|
return `<div class="vgroup">
|
|
806
|
-
<div class="vghead"><span class="rule">${esc(ruleId)}</span> <span class="dim">${items.length}</span></div>
|
|
823
|
+
<div class="vghead"><span class="rule">${esc(ruleId)}</span> <span class="dim">${ruleTotals.get(ruleId) ?? items.length}</span></div>
|
|
807
824
|
<ul class="vitems">${rows}</ul>
|
|
808
825
|
${hint ? `<div class="fix">fix: ${esc(hint)}</div>` : ''}
|
|
809
826
|
</div>`;
|
|
810
827
|
})
|
|
811
|
-
.join('\n')
|
|
828
|
+
.join('\n')}${remainderNote}`
|
|
812
829
|
: `<div class="clean hero-clean">
|
|
813
830
|
<div class="clean-title">Architecture matches the contract</div>
|
|
814
831
|
<div class="clean-body">No active violations${suppressed ? ` · ${suppressed} frozen by baseline` : ''}. This is what “honest green” looks like when coverage is real.</div>
|