arkgate 3.0.3 → 3.0.5
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 +69 -1
- package/bin/ark-check.mjs +33 -50
- package/bin/ark.mjs +3 -3
- package/bin/lib/agent-gates.mjs +9 -0
- package/bin/lib/codex-home.mjs +10 -1
- package/bin/lib/doctor-plan.mjs +30 -5
- package/bin/lib/html-report-depth.mjs +282 -0
- package/bin/lib/html-report.mjs +214 -21
- package/bin/lib/install-migrate.mjs +81 -25
- package/bin/lib/mcp-adoption.mjs +8 -0
- package/bin/lib/skill-install.mjs +304 -23
- package/bin/lib/weakest-link.mjs +61 -12
- package/bin/lib/write-path-capabilities.mjs +3 -1
- package/bin/lib/write-path-detect.mjs +39 -22
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/agent-guide.md +1 -1
- package/docs/ai-gates.md +27 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/skills/ark-upgrade.md +9 -5
package/bin/lib/html-report.mjs
CHANGED
|
@@ -11,6 +11,13 @@ import {
|
|
|
11
11
|
resolveOperatingMode,
|
|
12
12
|
} from '../ark-shared.mjs';
|
|
13
13
|
import { collectAdoptionGaps, arkCheckCommand } from './agent-gates.mjs';
|
|
14
|
+
import { CORE_LAYER_NAMES } from './core-layers.mjs';
|
|
15
|
+
import {
|
|
16
|
+
renderBaselineSignalLegend,
|
|
17
|
+
renderDesignCleanNote,
|
|
18
|
+
renderDesignDepthStrip,
|
|
19
|
+
renderWritePathAdoptionBlock,
|
|
20
|
+
} from './html-report-depth.mjs';
|
|
14
21
|
import { FIX_HINTS } from './violations.mjs';
|
|
15
22
|
|
|
16
23
|
export function detectEnforcement(root) {
|
|
@@ -58,6 +65,53 @@ export function htmlEscape(value) {
|
|
|
58
65
|
.replace(/"/g, '"');
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
/**
|
|
69
|
+
* KPI tile with plain-language hint (visible micro-copy + native tooltip).
|
|
70
|
+
* Helps newcomers read the showcase without memorizing Ark jargon.
|
|
71
|
+
*
|
|
72
|
+
* @param {string|number} value
|
|
73
|
+
* @param {string} label short metric name
|
|
74
|
+
* @param {string} hint one-sentence meaning
|
|
75
|
+
*/
|
|
76
|
+
export function metricKpi(value, label, hint) {
|
|
77
|
+
const v = htmlEscape(String(value));
|
|
78
|
+
const l = htmlEscape(label);
|
|
79
|
+
const h = htmlEscape(hint);
|
|
80
|
+
return `<div class="kpi" title="${h}" aria-label="${l}: ${v}. ${h}">
|
|
81
|
+
<b>${v}</b>
|
|
82
|
+
<span>${l}</span>
|
|
83
|
+
<em class="kpi-hint">${h}</em>
|
|
84
|
+
</div>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Baseline policy signal → human meaning (adoption card). */
|
|
88
|
+
export function baselineSignalHint(signal) {
|
|
89
|
+
switch (String(signal || '')) {
|
|
90
|
+
case 'keep-empty':
|
|
91
|
+
return 'Baseline file exists and freezes 0 keys — every violation is active (honest green).';
|
|
92
|
+
case 'active-ratchet':
|
|
93
|
+
return 'Baseline freezes known debt keys; new distinct violations still fail the check.';
|
|
94
|
+
case 'absent':
|
|
95
|
+
return 'No .ark-baseline.json — all findings are active (or you have not adopted a freeze file).';
|
|
96
|
+
default:
|
|
97
|
+
return 'How frozen debt is handled relative to active architecture violations.';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Operating mode badge tooltip. */
|
|
102
|
+
export function modeBadgeHint(mode) {
|
|
103
|
+
switch (String(mode || '').toLowerCase()) {
|
|
104
|
+
case 'enforce':
|
|
105
|
+
return 'Contract matches the tree: cores are required where populated, coverage is honest, gates can hold the line.';
|
|
106
|
+
case 'adapt':
|
|
107
|
+
return 'Contract is live but still aligning (optional cores with files, empty cores, or presentation-bag false green).';
|
|
108
|
+
case 'suggest':
|
|
109
|
+
return 'Starter shape — expand layers and raise governed coverage as the codebase grows.';
|
|
110
|
+
default:
|
|
111
|
+
return 'Operating mode for co-pilot surfaces (suggest · adapt · enforce).';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
61
115
|
/** Directory for origin / latest / history architecture report snapshots. */
|
|
62
116
|
const ARK_REPORTS_DIR = path.join('.ark', 'reports');
|
|
63
117
|
const ARK_REPORT_HISTORY_MAX = 20;
|
|
@@ -159,7 +213,10 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
|
|
|
159
213
|
const presentationRow = (coverage?.layers ?? []).find(
|
|
160
214
|
(r) => r.name === 'PresentationAdapters'
|
|
161
215
|
);
|
|
216
|
+
// Same honesty gate as doctor (`mcp-adoption` coreOptional): only the four cores
|
|
217
|
+
// matter. Secondary optional layers with files must not force ADAPT on the HTML report.
|
|
162
218
|
const coreOptionalWithFiles = (config?.layers ?? []).filter((layer) => {
|
|
219
|
+
if (!CORE_LAYER_NAMES.has(layer.name)) return false;
|
|
163
220
|
if (layer.optional !== true) return false;
|
|
164
221
|
const row = (coverage?.layers ?? []).find((r) => r.name === layer.name);
|
|
165
222
|
return (row?.files ?? 0) > 0;
|
|
@@ -396,6 +453,8 @@ export function renderHtmlReport({
|
|
|
396
453
|
currentSnapshot = null,
|
|
397
454
|
originJustCreated = false,
|
|
398
455
|
adoption = null,
|
|
456
|
+
/** Optional design-depth (doctor parity): designFitness, designSmells, pilotLoop, postGreenPath, goldenPattern */
|
|
457
|
+
designDepth = null,
|
|
399
458
|
}) {
|
|
400
459
|
const layers = Array.isArray(config.layers) ? config.layers : [];
|
|
401
460
|
const rules = Array.isArray(config.rules) ? config.rules : [];
|
|
@@ -448,6 +507,29 @@ export function renderHtmlReport({
|
|
|
448
507
|
} = fitness;
|
|
449
508
|
|
|
450
509
|
const adoptionView = adoption || collectAdoptionGaps(root, config, coverage);
|
|
510
|
+
const depth = designDepth && typeof designDepth === 'object' ? designDepth : {};
|
|
511
|
+
const designFitness = depth.designFitness ?? null;
|
|
512
|
+
const designSmells = Array.isArray(depth.designSmells) ? depth.designSmells : [];
|
|
513
|
+
const designWeakBadge =
|
|
514
|
+
designFitness?.designWeak === true
|
|
515
|
+
? ` <span class="badge design" title="Edges can be green while lived design residual remains (Shape). Not a FAIL.">design-weak</span>`
|
|
516
|
+
: '';
|
|
517
|
+
const designStripHtml =
|
|
518
|
+
renderDesignDepthStrip({
|
|
519
|
+
designFitness,
|
|
520
|
+
designSmells,
|
|
521
|
+
pilotLoop: depth.pilotLoop,
|
|
522
|
+
postGreenPath: depth.postGreenPath,
|
|
523
|
+
goldenPattern: depth.goldenPattern,
|
|
524
|
+
mode,
|
|
525
|
+
}) ||
|
|
526
|
+
renderDesignCleanNote({
|
|
527
|
+
designFitness,
|
|
528
|
+
ok,
|
|
529
|
+
mode,
|
|
530
|
+
});
|
|
531
|
+
const writePathHtml = renderWritePathAdoptionBlock(adoptionView.writePath);
|
|
532
|
+
const baselineLegendHtml = renderBaselineSignalLegend();
|
|
451
533
|
|
|
452
534
|
// ── Senior diagnostics (coupling, purity, contract density) ──────────────
|
|
453
535
|
const layerNames = ordered.map((l) => l.name);
|
|
@@ -818,9 +900,33 @@ export function renderHtmlReport({
|
|
|
818
900
|
.score-cap { color: var(--dim); font-size: .85rem; margin: 0; }
|
|
819
901
|
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: .65rem; margin: 1rem 0 0; }
|
|
820
902
|
@media (max-width: 720px) { .kpis { grid-template-columns: repeat(2, 1fr); } }
|
|
821
|
-
.kpi { background: var(--panel2); border: 1px solid var(--line); border-radius: 12px; padding: .7rem .8rem; }
|
|
903
|
+
.kpi { background: var(--panel2); border: 1px solid var(--line); border-radius: 12px; padding: .7rem .8rem; cursor: help; }
|
|
822
904
|
.kpi b { display: block; font-size: 1.25rem; letter-spacing: -0.02em; }
|
|
823
905
|
.kpi span { color: var(--dim); font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; }
|
|
906
|
+
.kpi-hint {
|
|
907
|
+
display: block; margin-top: .4rem; color: var(--dim); font-size: .68rem; font-style: normal;
|
|
908
|
+
font-weight: 450; line-height: 1.35; letter-spacing: 0; text-transform: none; max-width: 16rem;
|
|
909
|
+
}
|
|
910
|
+
.score-parts span { cursor: help; border-bottom: 1px dotted color-mix(in srgb, var(--dim) 55%, transparent); }
|
|
911
|
+
.badge[title] { cursor: help; }
|
|
912
|
+
.badge.design {
|
|
913
|
+
background: color-mix(in srgb, var(--gold) 18%, transparent); color: var(--gold);
|
|
914
|
+
border-color: color-mix(in srgb, var(--gold) 40%, transparent);
|
|
915
|
+
}
|
|
916
|
+
.badge.design-ok {
|
|
917
|
+
background: color-mix(in srgb, var(--green) 16%, transparent); color: var(--green);
|
|
918
|
+
border-color: color-mix(in srgb, var(--green) 35%, transparent);
|
|
919
|
+
}
|
|
920
|
+
.design-strip { border-left: 3px solid var(--gold); }
|
|
921
|
+
.design-strip.is-clean { border-left-color: var(--green); }
|
|
922
|
+
.design-strip.has-smells { border-left-color: var(--gold); }
|
|
923
|
+
.design-head { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
|
|
924
|
+
.pilot-card {
|
|
925
|
+
margin-top: .35rem; padding: .75rem .9rem; border-radius: 12px;
|
|
926
|
+
background: var(--panel2); border: 1px solid var(--line);
|
|
927
|
+
}
|
|
928
|
+
.write-path-block { margin-top: .15rem; }
|
|
929
|
+
.baseline-legend summary { cursor: pointer; color: var(--dim); font-size: .84rem; }
|
|
824
930
|
.section { margin-top: 1.35rem; }
|
|
825
931
|
.grid-2 { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 1rem; }
|
|
826
932
|
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
|
|
@@ -936,34 +1042,79 @@ export function renderHtmlReport({
|
|
|
936
1042
|
<div class="hero">
|
|
937
1043
|
<div class="card">
|
|
938
1044
|
<div class="brand"><i></i> Ark architecture report</div>
|
|
939
|
-
<h1>${esc(project)} <span class="badge ${status}">${status}</span> <span class="badge mode">${esc(modeLabel)}</span
|
|
940
|
-
<p class="lede">${esc(modeBlurb)} One machine-readable contract · write gate · CI · optional runtime.</p>
|
|
1045
|
+
<h1>${esc(project)} <span class="badge ${status}" title="${status === 'PASS' ? 'Architecture check is green: 0 active violations against the contract.' : 'Architecture check failed: active violations remain (or the scan could not complete cleanly).'}">${status}</span> <span class="badge mode" title="${esc(modeBadgeHint(mode))}">${esc(modeLabel)}</span>${designWeakBadge}</h1>
|
|
1046
|
+
<p class="lede">${esc(modeBlurb)}${designFitness?.designWeak ? ' Design residual remains (see strip below) — not a FAIL.' : ''} One machine-readable contract · write gate · CI · optional runtime.</p>
|
|
941
1047
|
<div class="kpis">
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1048
|
+
${metricKpi(
|
|
1049
|
+
govLabel,
|
|
1050
|
+
'Governed',
|
|
1051
|
+
'Share of scanned files assigned to a contract layer. 100% means every in-scope file has a home.'
|
|
1052
|
+
)}
|
|
1053
|
+
${metricKpi(
|
|
1054
|
+
layers.length,
|
|
1055
|
+
'Layers',
|
|
1056
|
+
'How many architecture layers the contract defines (cores + optional product layers).'
|
|
1057
|
+
)}
|
|
1058
|
+
${metricKpi(
|
|
1059
|
+
`${gatesOn}/${enforcement.length}`,
|
|
1060
|
+
'Gates live',
|
|
1061
|
+
'Write hook, CI workflow, ESLint plugin, and baseline file — how many enforcement points are actually present.'
|
|
1062
|
+
)}
|
|
1063
|
+
${metricKpi(
|
|
1064
|
+
`${violations.length}${suppressed ? ` · ${suppressed}Δ` : ''}`,
|
|
1065
|
+
`Violations${suppressed ? ' · frozen' : ''}`,
|
|
1066
|
+
suppressed
|
|
1067
|
+
? 'Active contract breaks right now; Δ = keys frozen in baseline (not failing until ratchet).'
|
|
1068
|
+
: 'Active contract breaks (layer imports, purity, etc.). Zero means edges match the rules.'
|
|
1069
|
+
)}
|
|
946
1070
|
</div>
|
|
947
1071
|
<p class="meta">${meta}</p>
|
|
948
1072
|
${skillsNote}
|
|
949
1073
|
</div>
|
|
950
|
-
<div class="card score-card">
|
|
1074
|
+
<div class="card score-card" title="Human fitness signal only — not a CI gate. Weighted blend of coverage, cleanliness, live gates, and rule density.">
|
|
951
1075
|
<div class="score-ring ${scoreTone}"><div><div class="score-n">${score}</div><div class="dim" style="font-size:.72rem;letter-spacing:.08em;text-transform:uppercase">Ark score</div></div></div>
|
|
952
1076
|
<p class="score-cap">${esc(scoreCaption)}</p>
|
|
953
|
-
<p class="meta" style="margin-top:.65rem">
|
|
1077
|
+
<p class="meta score-parts" style="margin-top:.65rem">
|
|
1078
|
+
<span title="0.4 weight — governed file percent (or 50 if coverage unknown).">${esc(`Coverage ${scoreCoverage}`)}</span>
|
|
1079
|
+
· <span title="0.3 weight — 100 with zero active violations; drops as violations pile up.">${esc(`Clean ${scoreClean}`)}</span>
|
|
1080
|
+
· <span title="0.2 weight — share of enforcement points that are present on disk (hook, CI, ESLint, baseline).">${esc(`Gates ${scoreGates}`)}</span>
|
|
1081
|
+
· <span title="0.1 weight — how dense the deny matrix is relative to layer pairs (stricter inward architecture scores higher).">${esc(`Rules ${scoreRules}`)}</span>
|
|
1082
|
+
</p>
|
|
954
1083
|
</div>
|
|
955
1084
|
</div>
|
|
956
1085
|
|
|
1086
|
+
${designStripHtml}
|
|
1087
|
+
|
|
957
1088
|
<div class="section card" id="adoption">
|
|
958
1089
|
<h2>Adoption</h2>
|
|
959
1090
|
<p class="dim" style="margin:.15rem 0 .75rem;font-size:.88rem">
|
|
960
1091
|
Co-pilot completeness — separate from the 0–100 fitness score above. Hosts, MCP health, origin snapshot, core optionality, baseline policy.
|
|
961
1092
|
</p>
|
|
962
1093
|
<div class="kpis" style="margin-bottom:.75rem">
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1094
|
+
${metricKpi(
|
|
1095
|
+
adoptionView.gaps.length === 0 ? 'OK' : adoptionView.gaps.length,
|
|
1096
|
+
adoptionView.gaps.length === 0 ? 'No adoption gaps' : 'Adoption gap(s)',
|
|
1097
|
+
adoptionView.gaps.length === 0
|
|
1098
|
+
? 'Hosts, MCP argv, origin snapshot, and core optionality look complete for co-pilot use.'
|
|
1099
|
+
: 'Install or fix the listed gaps so agents get write gates, MCP, and honest cores.'
|
|
1100
|
+
)}
|
|
1101
|
+
${metricKpi(
|
|
1102
|
+
adoptionView.originReport.present ? 'yes' : 'no',
|
|
1103
|
+
'Origin report',
|
|
1104
|
+
'First architecture snapshot under .ark/reports/origin.* — future reports show evolution deltas against it.'
|
|
1105
|
+
)}
|
|
1106
|
+
${metricKpi(
|
|
1107
|
+
adoptionView.baseline.signal,
|
|
1108
|
+
'Baseline policy',
|
|
1109
|
+
baselineSignalHint(adoptionView.baseline.signal)
|
|
1110
|
+
)}
|
|
1111
|
+
${metricKpi(
|
|
1112
|
+
adoptionView.mcp.ok ? 'ok' : 'fix',
|
|
1113
|
+
'Repo MCP argv',
|
|
1114
|
+
adoptionView.mcp.ok
|
|
1115
|
+
? 'Repo MCP config points at a single ark/arkgate MCP bin (no dual-bin conflict).'
|
|
1116
|
+
: 'Broken MCP argv: more than one of ark-mcp/arkgate-mcp — migrate with --install-agent-gates --migrate-commands.'
|
|
1117
|
+
)}
|
|
967
1118
|
</div>
|
|
968
1119
|
${
|
|
969
1120
|
adoptionView.gaps.length
|
|
@@ -991,6 +1142,8 @@ export function renderHtmlReport({
|
|
|
991
1142
|
.join(' · ')}</p>`
|
|
992
1143
|
: ''
|
|
993
1144
|
}
|
|
1145
|
+
${writePathHtml}
|
|
1146
|
+
${baselineLegendHtml}
|
|
994
1147
|
</div>
|
|
995
1148
|
|
|
996
1149
|
<div class="section grid-2">
|
|
@@ -1138,10 +1291,26 @@ export function renderHtmlReport({
|
|
|
1138
1291
|
|
|
1139
1292
|
<h3>Contract density</h3>
|
|
1140
1293
|
<div class="kpis" style="margin-top:.35rem">
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1294
|
+
${metricKpi(
|
|
1295
|
+
`${denyRatio}%`,
|
|
1296
|
+
'Edges denied',
|
|
1297
|
+
'Denied directed layer pairs ÷ all possible pairs. Higher = stricter inward dependency rules.'
|
|
1298
|
+
)}
|
|
1299
|
+
${metricKpi(
|
|
1300
|
+
deniedCount,
|
|
1301
|
+
'Deny rules',
|
|
1302
|
+
'Explicit allowed:false rules in ark.config.json (row may not import column).'
|
|
1303
|
+
)}
|
|
1304
|
+
${metricKpi(
|
|
1305
|
+
allowedCount,
|
|
1306
|
+
'Explicit allows',
|
|
1307
|
+
'Explicit allowed:true edges. Most opens are implicit (no rule) unless you document them.'
|
|
1308
|
+
)}
|
|
1309
|
+
${metricKpi(
|
|
1310
|
+
pairCount,
|
|
1311
|
+
'Directed pairs',
|
|
1312
|
+
'layers × (layers − 1) — every ordered from→to pair the matrix can constrain.'
|
|
1313
|
+
)}
|
|
1145
1314
|
</div>
|
|
1146
1315
|
<p class="dim" style="margin:.55rem 0 0;font-size:.84rem">
|
|
1147
1316
|
Deny ratio = denied ÷ (layers × (layers−1)). High ratio = strict inward architecture.
|
|
@@ -1270,10 +1439,26 @@ export function renderHtmlReport({
|
|
|
1270
1439
|
|
|
1271
1440
|
<h3>Debt & violation taxonomy</h3>
|
|
1272
1441
|
<div class="kpis" style="margin-top:.35rem">
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1442
|
+
${metricKpi(
|
|
1443
|
+
violations.length,
|
|
1444
|
+
'Active',
|
|
1445
|
+
'Violations that fail the check right now (not frozen by baseline).'
|
|
1446
|
+
)}
|
|
1447
|
+
${metricKpi(
|
|
1448
|
+
valueN,
|
|
1449
|
+
'Value edges',
|
|
1450
|
+
'Runtime import edges that cross a deny rule (stronger debt than type-only).'
|
|
1451
|
+
)}
|
|
1452
|
+
${metricKpi(
|
|
1453
|
+
typeOnlyN,
|
|
1454
|
+
'Type-only',
|
|
1455
|
+
'Type-only imports across a deny edge — often mechanical-safe to rewrite as import type.'
|
|
1456
|
+
)}
|
|
1457
|
+
${metricKpi(
|
|
1458
|
+
suppressed || baselineKeys,
|
|
1459
|
+
'Baseline keys',
|
|
1460
|
+
'Distinct frozen debt keys in .ark-baseline.json (or suppressed count for this run).'
|
|
1461
|
+
)}
|
|
1277
1462
|
</div>
|
|
1278
1463
|
${
|
|
1279
1464
|
topEdges.length
|
|
@@ -1291,6 +1476,14 @@ export function renderHtmlReport({
|
|
|
1291
1476
|
Coverage=${scoreCoverage}, clean=${scoreClean}, gates=${scoreGates}, rules=${scoreRules} → <b>${score}</b>.
|
|
1292
1477
|
This is a fitness signal for humans, not a CI gate.
|
|
1293
1478
|
</p>
|
|
1479
|
+
<ul class="senior-list" style="margin-top:.45rem">
|
|
1480
|
+
<li><b>Coverage</b> — % of in-scope files that match a layer pattern.</li>
|
|
1481
|
+
<li><b>Clean</b> — 100 with zero active violations; falls as breaks accumulate.</li>
|
|
1482
|
+
<li><b>Gates</b> — share of write / CI / ESLint / baseline enforcement points present.</li>
|
|
1483
|
+
<li><b>Rules</b> — deny-matrix density (more inward denies → higher component).</li>
|
|
1484
|
+
<li><b>PASS / FAIL</b> — binary edge honesty (active violations), independent of the 0–100 score.</li>
|
|
1485
|
+
<li><b>SUGGEST / ADAPT / ENFORCE</b> — whether the contract is honest enough to protect the tree (not a skill grade).</li>
|
|
1486
|
+
</ul>
|
|
1294
1487
|
</details>
|
|
1295
1488
|
</div>
|
|
1296
1489
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../ark-shared.mjs';
|
|
13
13
|
import {
|
|
14
14
|
codexPromptsDir,
|
|
15
|
+
codexSkillsDir,
|
|
15
16
|
codexConfigPath,
|
|
16
17
|
isTempOrUpgradeRoot,
|
|
17
18
|
usesDefaultCodexHome,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
isVersionOlder,
|
|
53
54
|
detectSkillGaps,
|
|
54
55
|
arkPackageVersion,
|
|
56
|
+
verifyHostSkillCatalog,
|
|
55
57
|
} from './skill-install.mjs';
|
|
56
58
|
import { detectDeployPathQuality } from './deploy-path.mjs';
|
|
57
59
|
import {
|
|
@@ -383,15 +385,15 @@ export function runInstallAgentGates(args) {
|
|
|
383
385
|
console.log(` ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`);
|
|
384
386
|
}
|
|
385
387
|
|
|
386
|
-
// --codex-home writes
|
|
387
|
-
// Codex
|
|
388
|
-
//
|
|
389
|
-
//
|
|
388
|
+
// --codex-home writes SKILL.md skills to $CODEX_HOME/skills/<name>/SKILL.md.
|
|
389
|
+
// Codex's real catalog loads skill directories (not flat $CODEX_HOME/prompts).
|
|
390
|
+
// Repo installs already write `.agents/skills/<name>/SKILL.md` when `codex` is
|
|
391
|
+
// selected; home install is for multi-project / non-repo-local refresh.
|
|
390
392
|
const homeResults = [];
|
|
391
393
|
if (args.codexHome) {
|
|
392
|
-
const dir =
|
|
394
|
+
const dir = codexSkillsDir();
|
|
393
395
|
console.log('');
|
|
394
|
-
console.log(`Codex home skills (${dir}):`);
|
|
396
|
+
console.log(`Codex home skills (${dir}/<name>/SKILL.md):`);
|
|
395
397
|
try {
|
|
396
398
|
fs.mkdirSync(dir, { recursive: true });
|
|
397
399
|
} catch (error) {
|
|
@@ -400,23 +402,25 @@ export function runInstallAgentGates(args) {
|
|
|
400
402
|
}
|
|
401
403
|
if (homeResults.length === 0) {
|
|
402
404
|
for (const [name, content] of skills) {
|
|
403
|
-
const
|
|
405
|
+
const skillDir = path.join(dir, name);
|
|
406
|
+
const file = path.join(skillDir, 'SKILL.md');
|
|
404
407
|
if (fs.existsSync(file) && !args.force) {
|
|
405
408
|
const installed = installedSkillVersion(file);
|
|
406
409
|
const behind = installed === null || (version && isVersionOlder(installed, version));
|
|
407
410
|
const note = behind
|
|
408
411
|
? ` (stale: ${installed ?? 'no stamp'} < ${version}; use --force)`
|
|
409
412
|
: ' (up to date)';
|
|
410
|
-
console.log(` ${'skipped'.padEnd(7)} ${name}.md${note}`);
|
|
413
|
+
console.log(` ${'skipped'.padEnd(7)} ${name}/SKILL.md${note}`);
|
|
411
414
|
homeResults.push({ status: 'skipped' });
|
|
412
415
|
continue;
|
|
413
416
|
}
|
|
414
417
|
try {
|
|
418
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
415
419
|
fs.writeFileSync(file, content);
|
|
416
|
-
console.log(` ${'wrote'.padEnd(7)} ${name}.md`);
|
|
420
|
+
console.log(` ${'wrote'.padEnd(7)} ${name}/SKILL.md`);
|
|
417
421
|
homeResults.push({ status: 'written' });
|
|
418
422
|
} catch (error) {
|
|
419
|
-
console.log(` ${'FAILED'.padEnd(7)} ${name}.md (${error.message})`);
|
|
423
|
+
console.log(` ${'FAILED'.padEnd(7)} ${name}/SKILL.md (${error.message})`);
|
|
420
424
|
homeResults.push({ status: 'failed' });
|
|
421
425
|
}
|
|
422
426
|
}
|
|
@@ -429,18 +433,15 @@ export function runInstallAgentGates(args) {
|
|
|
429
433
|
// home-dir merge instead. Fires whenever Codex is in play so `ark://manifest` is live
|
|
430
434
|
// without a manual copy step.
|
|
431
435
|
//
|
|
432
|
-
// Skip home
|
|
433
|
-
// CODEX_HOME
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
436
|
+
// Skip home MCP mutation when the project root is a temp/upgrade scratch *and*
|
|
437
|
+
// CODEX_HOME is the default (~/.codex). Fixtures and agent smokes must not rewrite
|
|
438
|
+
// the developer's real config.toml with a temp --root. --codex-home still refreshes
|
|
439
|
+
// home *skills* below; MCP binding of a temp root into default home is never safe.
|
|
440
|
+
// A redirected CODEX_HOME (tests/isolation) may still wire as requested.
|
|
437
441
|
let codexMcp = null;
|
|
438
442
|
const wantCodexWire = !args.compact && (tools.has('codex') || args.codexHome);
|
|
439
443
|
const skipHomeWire =
|
|
440
|
-
wantCodexWire &&
|
|
441
|
-
isTempOrUpgradeRoot(root) &&
|
|
442
|
-
!args.codexHome &&
|
|
443
|
-
usesDefaultCodexHome();
|
|
444
|
+
wantCodexWire && isTempOrUpgradeRoot(root) && usesDefaultCodexHome();
|
|
444
445
|
if (wantCodexWire && !skipHomeWire) {
|
|
445
446
|
codexMcp = wireCodexMcp(root, args.force);
|
|
446
447
|
console.log('');
|
|
@@ -498,13 +499,68 @@ export function runInstallAgentGates(args) {
|
|
|
498
499
|
if (codexMcp && codexMcp.status !== 'failed') {
|
|
499
500
|
console.log(` Codex: ark MCP registered in ${codexMcp.file} — restart Codex so \`ark://manifest\` loads.`);
|
|
500
501
|
}
|
|
502
|
+
if (tools.has('codex') && !args.compact) {
|
|
503
|
+
console.log(' Codex write path (honest):');
|
|
504
|
+
console.log(' - Local: advisory MCP + best-effort .codex/hooks.json (not a hard boundary).');
|
|
505
|
+
console.log(' - Hard merge backstop: CI --strict-merge + required status check.');
|
|
506
|
+
console.log(' - Not equivalent to Claude/Grok PreToolUse hard-write + repair.');
|
|
507
|
+
}
|
|
501
508
|
if (args.codexHome) {
|
|
502
|
-
console.log(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
console.log(
|
|
507
|
-
|
|
509
|
+
console.log(
|
|
510
|
+
` Codex: refreshed home skills under ${codexSkillsDir()}/<name>/SKILL.md (Codex skill catalog).`
|
|
511
|
+
);
|
|
512
|
+
} else if (tools.has('codex') && skills.length > 0 && !args.compact) {
|
|
513
|
+
console.log(
|
|
514
|
+
' Codex: project skills at `.agents/skills/<name>/SKILL.md` (Agent Skills REPO scope).'
|
|
515
|
+
);
|
|
516
|
+
console.log(
|
|
517
|
+
` Optional multi-project home copy: ${arkCommand(root, 'ark-check', '--install-agent-gates --codex-home')}`
|
|
518
|
+
);
|
|
519
|
+
console.log(
|
|
520
|
+
` (writes $CODEX_HOME/skills; legacy flat prompts at ${codexPromptsDir()} are not the skill catalog).`
|
|
521
|
+
);
|
|
522
|
+
// Best-effort note when dead prompts still sit under the repo.
|
|
523
|
+
try {
|
|
524
|
+
const promptDir = path.join(root, '.codex', 'prompts');
|
|
525
|
+
if (fs.existsSync(promptDir)) {
|
|
526
|
+
const legacy = fs.readdirSync(promptDir).filter((n) => /^ark-[a-z0-9-]+\.md$/.test(n));
|
|
527
|
+
if (legacy.length > 0) {
|
|
528
|
+
console.log(
|
|
529
|
+
` Note: ignoring ${legacy.length} legacy .codex/prompts/ark-*.md file(s) — not loadable as Codex skills.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
// ignore
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Post-install: AGENTS.md /ark-* refs must exist in each selected host catalog.
|
|
540
|
+
// Compact routers intentionally omit those refs (package/MCP is the router).
|
|
541
|
+
if (!args.compact && skills.length > 0) {
|
|
542
|
+
const catalog = verifyHostSkillCatalog(root, tools, {
|
|
543
|
+
skillNames: skills.map(([name]) => name),
|
|
544
|
+
});
|
|
545
|
+
if (!catalog.ok) {
|
|
546
|
+
console.log('');
|
|
547
|
+
console.error(
|
|
548
|
+
`Skill catalog verification failed: ${catalog.missing.length} AGENTS.md /ark-* reference(s) missing from host catalogs.`
|
|
549
|
+
);
|
|
550
|
+
for (const miss of catalog.missing.slice(0, 12)) {
|
|
551
|
+
console.error(` missing ${miss.tool}: ${miss.path}`);
|
|
552
|
+
}
|
|
553
|
+
if (catalog.missing.length > 12) {
|
|
554
|
+
console.error(` …and ${catalog.missing.length - 12} more`);
|
|
555
|
+
}
|
|
556
|
+
process.exitCode = 1;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (catalog.checkedTools.length > 0 && catalog.referenced.length > 0) {
|
|
560
|
+
console.log('');
|
|
561
|
+
console.log(
|
|
562
|
+
`Skill catalog verified: ${catalog.referenced.length} AGENTS.md /ark-* skill(s) present for ${catalog.checkedTools.join(', ')}.`
|
|
563
|
+
);
|
|
508
564
|
}
|
|
509
565
|
}
|
|
510
566
|
warnLockfileConflict(root);
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -143,6 +143,14 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
143
143
|
extras: [['.cursor/mcp.json', 'MCP config']],
|
|
144
144
|
toolsFlag: 'cursor',
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
host: 'codex',
|
|
148
|
+
dir: '.codex',
|
|
149
|
+
// Official Codex REPO skill catalog (Agent Skills standard) — not .codex/prompts.
|
|
150
|
+
skill: (n) => path.join(root, '.agents', 'skills', n, 'SKILL.md'),
|
|
151
|
+
extras: [['.codex/hooks.json', 'hooks']],
|
|
152
|
+
toolsFlag: 'codex',
|
|
153
|
+
},
|
|
146
154
|
];
|
|
147
155
|
for (const h of hostChecks) {
|
|
148
156
|
if (!fs.existsSync(path.join(root, h.dir))) continue;
|