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 CHANGED
@@ -4,7 +4,75 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
4
4
 
5
5
  ## Unreleased
6
6
 
7
- No changes are scheduled after 3.0.3.
7
+ No changes are scheduled after 3.0.5.
8
+
9
+ ## 3.0.5 — 2026-07-14
10
+
11
+ Codex host skill catalog + residual honesty. **No breaking** CLI or `ark.config.json`
12
+ changes. **No gate weaken.**
13
+
14
+ ### Fixed
15
+
16
+ - **Codex `/ark-*` skills not invocable:** install wrote flat `.codex/prompts/*.md`, which
17
+ Codex does not load as skills. Repo catalog is now `.agents/skills/<name>/SKILL.md`
18
+ (Agent Skills REPO scope); optional home catalog is `$CODEX_HOME/skills/<name>/SKILL.md`
19
+ via `--codex-home`. Post-install verifies AGENTS.md `/ark-*` refs against each selected
20
+ host catalog.
21
+ - **Temp-root MCP footgun:** `--codex-home` no longer rebinds primary `[mcp_servers.ark]` in
22
+ the default `~/.codex/config.toml` when the project root is a temp/upgrade path (skills may
23
+ still refresh under an isolated or real home).
24
+ - **Multi-host skill hints:** Codex legacy-prompts-only debt no longer suppresses missing/stale
25
+ skill reports for Claude/Cursor/other hosts in doctor and `ark-check` human output.
26
+ - **Deferred Codex home debt severity:** outside a Codex session, home skill gaps are dim/info
27
+ (not warn) and are not Top actions; when the session host is Codex they stay warn + fix.
28
+
29
+ ### Added
30
+
31
+ - **Skill parity sensors:** missing / stale / legacy-prompts-only for repo and home catalogs,
32
+ with package `arkVersion` stamps; doctor and JSON expose concrete refresh fixes
33
+ (`--skills-only --tools codex` and/or `--codex-home`).
34
+ - **CI fail-closed detection:** workflows with ark-check but only `--strict-config` (or no
35
+ strict flags) surface `enforcement-ci-not-fail-closed` (warn) with a `--strict-merge` fix.
36
+ `--strict` / `--strict-merge` / `--require-gates` count as fail-closed. Merge-gate inventory
37
+ evidence requires that fail-closed profile.
38
+ - **Codex write-path honesty:** install and doctor state local Codex write is advisory (MCP +
39
+ best-effort hooks; not Claude/Grok hard-write + repair); CI `--strict-merge` + required
40
+ status is the hard merge backstop.
41
+
42
+ Release note: `docs/releases/3.0.5.md`.
43
+
44
+ ## 3.0.4 — 2026-07-14
45
+
46
+ Report honesty + showcase depth patch. **No breaking** CLI or `ark.config.json` changes.
47
+ **No gate weaken.**
48
+
49
+ ### Fixed
50
+
51
+ - **HTML report false ADAPT:** `computeReportFitness` counted *any* `optional: true` layer
52
+ with files as `coreOptionalWithFiles`, so doctor could report **ENFORCE** while
53
+ `ark-report.html` / `latest.json` mode stayed **ADAPT** (secondary layers like
54
+ SharedKernel / Integration / Workflow). Report now uses the same `CORE_LAYER_NAMES`
55
+ filter as doctor adoption (`DomainModel`, `ApplicationOrchestration`,
56
+ `PresentationAdapters`, `PersistenceAdapters`).
57
+ - **False adoption gap `write-path-none` on report/CI:** when `activeHost` is `unknown`
58
+ (plain `npx ark-check --report` outside an agent session) but the repo inventory already
59
+ has hard-write hooks or advisory MCP for Claude/Grok/Cursor/Codex, doctor/report no longer
60
+ open a `write-path-none` adoption gap. Session projection still reports `mode: none` for
61
+ honesty (other hosts' hooks are not a guarantee for this process). `detectActiveAgentHost`
62
+ also recognizes `GROK_AGENT`.
63
+
64
+ ### Added
65
+
66
+ - **Report metric hints:** HTML showcase KPIs (hero, adoption, contract density, debt) show
67
+ plain-language micro-copy under each tile plus native tooltips; PASS/mode badges and score
68
+ parts (Coverage/Clean/Gates/Rules) explain what they mean for newcomers.
69
+ - **Report design-depth strip:** `ark-check --report` includes doctor-parity Shape residual
70
+ (design-weak badge, smell outcomes, one next pilot, post-green door, optional golden pattern).
71
+ Clean ENFORCE with no smells shows a short “Design depth · OK” note (only when sensors ran).
72
+ - **Report adoption extras:** write-path line (active host · mode · inventory on disk) and a
73
+ fixed baseline-policy legend (`keep-empty` / `active-ratchet` / `absent`).
74
+
75
+ Release note: `docs/releases/3.0.4.md`.
8
76
 
9
77
  ## 3.0.3 — 2026-07-13
10
78
 
package/bin/ark-check.mjs CHANGED
@@ -33,7 +33,9 @@ import {
33
33
  loadTypeScript,
34
34
  detectSkillGaps,
35
35
  detectCodexHomeGap,
36
- detectActiveAgentHost,
36
+ detectCodexRepoSkillGap,
37
+ codexConcernIsActive,
38
+ printSkillAndCodexGapHints,
37
39
  missingGates,
38
40
  staleRunnerGateFiles,
39
41
  brokenMcpGateFiles,
@@ -44,7 +46,6 @@ import {
44
46
  arkPackageVersion,
45
47
  compactRouterHost,
46
48
  REQUIRED_GATE_FILES,
47
- codexPromptsDir,
48
49
  detectWritePathCapabilities,
49
50
  } from './lib/agent-gates.mjs';
50
51
  import { syncBaselineIntoCheckSurfaces } from './lib/field-install.mjs';
@@ -58,6 +59,7 @@ import {
58
59
  reportsDir,
59
60
  readJsonSafe,
60
61
  } from './lib/html-report.mjs';
62
+ import { buildReportDepthPayload } from './lib/html-report-depth.mjs';
61
63
  import { shouldOpenHtmlReport, openHtmlInBrowser } from './lib/open-html.mjs';
62
64
  import {
63
65
  computeCoverage,
@@ -300,8 +302,8 @@ function usage() {
300
302
  '(instruction-tier rule files derived from the same contract).',
301
303
  'It also installs the /ark-* skills shipped in templates/skills/ into each',
302
304
  'detected tool\'s command location (.claude/skills/, .cursor/commands/,',
303
- '.codex/prompts/, .grok/skills/, .windsurf/workflows/, .clinerules/workflows/,',
304
- '.github/prompts/).',
305
+ '.agents/skills/ (Codex REPO catalog), .grok/skills/, .windsurf/workflows/,',
306
+ '.clinerules/workflows/, .github/prompts/).',
305
307
  'Kiro, Roo, Continue, and Gemini have no command mechanism and receive only their',
306
308
  'rule file. Existing files are never overwritten without --force, so re-running',
307
309
  'after an update only adds what is missing. --skills-only restricts the write to',
@@ -1228,6 +1230,8 @@ async function main() {
1228
1230
 
1229
1231
  const skillGaps = detectSkillGaps(root);
1230
1232
  const codexHomeGap = detectCodexHomeGap(root);
1233
+ const codexRepoSkillGap = detectCodexRepoSkillGap(root);
1234
+ const codexSessionActive = codexConcernIsActive();
1231
1235
 
1232
1236
  if (args.report) {
1233
1237
  const exampleByLayer = new Map();
@@ -1266,6 +1270,13 @@ async function main() {
1266
1270
  const existingOrigin = args.resetOrigin
1267
1271
  ? null
1268
1272
  : readJsonSafe(path.join(reportsDir(root), 'origin.json'));
1273
+ const { adoption: adoptionForReport, designDepth } = buildReportDepthPayload(
1274
+ root,
1275
+ config,
1276
+ files,
1277
+ coverage,
1278
+ activeViolations
1279
+ );
1269
1280
  const reportPayload = {
1270
1281
  root,
1271
1282
  config,
@@ -1282,6 +1293,8 @@ async function main() {
1282
1293
  originSnapshot: existingOrigin,
1283
1294
  currentSnapshot,
1284
1295
  originJustCreated: !existingOrigin,
1296
+ adoption: adoptionForReport,
1297
+ designDepth,
1285
1298
  };
1286
1299
  const html = args.beginner
1287
1300
  ? renderBeginnerHtmlReport(reportPayload)
@@ -1362,7 +1375,15 @@ async function main() {
1362
1375
  warnings,
1363
1376
  ...(activeViolations.length > 0 ? { summary: summarizeViolations(activeViolations) } : {}),
1364
1377
  ...(skillGaps.length > 0 ? { skillGaps } : {}),
1365
- ...(codexHomeGap ? { codexHomeGap } : {}),
1378
+ ...(codexHomeGap
1379
+ ? {
1380
+ codexHomeGap: {
1381
+ ...codexHomeGap,
1382
+ deferred: !codexSessionActive,
1383
+ },
1384
+ }
1385
+ : {}),
1386
+ ...(codexRepoSkillGap ? { codexRepoSkillGap } : {}),
1366
1387
  }, null, 2));
1367
1388
  } else {
1368
1389
  for (const warning of warnings) {
@@ -1410,30 +1431,13 @@ async function main() {
1410
1431
  printViolationBreakdown(summarizeViolations(activeViolations), { toStderr: true });
1411
1432
  }
1412
1433
 
1413
- if (skillGaps.length > 0) {
1414
- const missingTotal = skillGaps.reduce((sum, gap) => sum + gap.missing, 0);
1415
- const staleTotal = skillGaps.reduce((sum, gap) => sum + gap.stale, 0);
1416
- const tools = skillGaps.map((gap) => gap.tool).join(', ');
1417
- if (missingTotal > 0) {
1418
- console.log(
1419
- color.dim(
1420
- `${missingTotal} /ark-* skill(s) not installed for ${tools} (this Ark version ships them). ` +
1421
- `Install: ${arkCommand(root, 'ark-check', '--install-agent-gates')}`
1422
- )
1423
- );
1424
- }
1425
- if (staleTotal > 0) {
1426
- // Stale skills already exist, so refreshing needs --force. --skills-only
1427
- // scopes the overwrite to the canonical skills, leaving a customized
1428
- // AGENTS.md / settings / CI untouched (a bare --force would clobber them).
1429
- console.log(
1430
- color.dim(
1431
- `${staleTotal} /ark-* skill(s) outdated for ${tools} (this Ark ships newer versions). ` +
1432
- `Refresh: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`
1433
- )
1434
- );
1435
- }
1436
- }
1434
+ printSkillAndCodexGapHints(root, {
1435
+ skillGaps,
1436
+ codexHomeGap,
1437
+ codexRepoSkillGap,
1438
+ codexSessionActive,
1439
+ color,
1440
+ });
1437
1441
 
1438
1442
  const staleRunners = staleRunnerGateFiles(root);
1439
1443
  if (staleRunners.length > 0) {
@@ -1454,27 +1458,6 @@ async function main() {
1454
1458
  )
1455
1459
  );
1456
1460
  }
1457
-
1458
- if (codexHomeGap) {
1459
- const parts = [];
1460
- if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
1461
- if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale} outdated`);
1462
- // Advisory always; when session host is known and not Codex, say so so
1463
- // /ark-upgrade does not chase home prompts as Incomplete.
1464
- const activeHost = detectActiveAgentHost();
1465
- const deferredNote =
1466
- activeHost != null && activeHost !== 'codex'
1467
- ? ' Deferred unless you use Codex — not a blocker for Grok/Claude/Cursor. '
1468
- : ' ';
1469
- console.log(
1470
- color.dim(
1471
- `/ark-* skills in ${codexPromptsDir()} are behind this Ark (${parts.join(', ')}).` +
1472
- deferredNote +
1473
- `Codex loads them from $CODEX_HOME/prompts, not the repo. ` +
1474
- `When using Codex: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --codex-home --force')}`
1475
- )
1476
- );
1477
- }
1478
1461
  }
1479
1462
 
1480
1463
  if (args.watch) {
package/bin/ark.mjs CHANGED
@@ -213,9 +213,9 @@ async function upgrade(args) {
213
213
  let status = runArkCheck(['--root', root, '--install-agent-gates'], { cwd: root });
214
214
  if (status !== 0) return status;
215
215
 
216
- // Codex loads slash-command prompts from $CODEX_HOME/prompts, not the repo refresh those
217
- // when a Codex home exists. --force rewrites temp/upgrade MCP roots to this project + arkgate-mcp.
218
- // Non-fatal: a permission error (e.g. sandbox) shouldn't fail the whole upgrade.
216
+ // Codex home skill catalog is $CODEX_HOME/skills/<name>/SKILL.md (repo uses .agents/skills/).
217
+ // Refresh home when a Codex home exists. --force rewrites temp/upgrade MCP roots to this
218
+ // project + arkgate-mcp. Non-fatal: a permission error (e.g. sandbox) shouldn't fail upgrade.
219
219
  const codexHomeBase = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
220
220
  if (fs.existsSync(codexHomeBase)) {
221
221
  console.log(`\n Refreshing Codex home (${codexHomeBase})…`);
@@ -10,6 +10,7 @@ export {
10
10
  codexPrimaryTable,
11
11
  codexProjectSlug,
12
12
  codexPromptsDir,
13
+ codexSkillsDir,
13
14
  codexScopedTableForRoot,
14
15
  extractCodexArkRootFromToml,
15
16
  extractCodexRootFromBlock,
@@ -77,6 +78,7 @@ export {
77
78
  normalizeToolsList,
78
79
  resolveTools,
79
80
  KNOWN_TOOLS,
81
+ SKILL_TOOL_TARGETS,
80
82
  detectActiveAgentHost,
81
83
  codexConcernIsActive,
82
84
  arkPackageVersion,
@@ -86,7 +88,13 @@ export {
86
88
  skillTemplates,
87
89
  skillTemplateNames,
88
90
  detectCodexHomeGap,
91
+ detectCodexRepoSkillGap,
92
+ assessCodexSkillParity,
93
+ assessSkillCatalogParity,
89
94
  detectSkillGaps,
95
+ agentsMdSkillRefs,
96
+ verifyHostSkillCatalog,
97
+ printSkillAndCodexGapHints,
90
98
  } from './skill-install.mjs';
91
99
 
92
100
  export { detectDeployPathQuality } from './deploy-path.mjs';
@@ -101,6 +109,7 @@ export {
101
109
  export {
102
110
  detectPreCommitArk,
103
111
  detectCiEnforcement,
112
+ classifyArkCheckFlags,
104
113
  detectConfigGateDrift,
105
114
  jobIdsThatRunArkCheck,
106
115
  isArkRequiredStatusCheck,
@@ -10,12 +10,21 @@ import { execCommandParts } from '../ark-shared.mjs';
10
10
 
11
11
  export const PREFERRED_CODEX_MCP_BIN = 'arkgate-mcp';
12
12
 
13
- /** Where Codex loads slash-command prompts ($CODEX_HOME/prompts). */
13
+ /** Where Codex loads slash-command prompts ($CODEX_HOME/prompts) — legacy, not the skill catalog. */
14
14
  export function codexPromptsDir() {
15
15
  const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
16
16
  return path.join(base, 'prompts');
17
17
  }
18
18
 
19
+ /**
20
+ * Where Codex loads user/home SKILL.md skills ($CODEX_HOME/skills/<name>/SKILL.md).
21
+ * Repo-scoped skills live at `.agents/skills/<name>/SKILL.md` (Agent Skills standard).
22
+ */
23
+ export function codexSkillsDir() {
24
+ const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
25
+ return path.join(base, 'skills');
26
+ }
27
+
19
28
  /** Where Codex loads MCP servers ($CODEX_HOME/config.toml) — global, not project-local. */
20
29
  export function codexConfigPath() {
21
30
  const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
@@ -14,6 +14,8 @@ import {
14
14
  import {
15
15
  collectAdoptionGaps,
16
16
  detectSkillGaps,
17
+ detectCodexHomeGap,
18
+ codexConcernIsActive,
17
19
  detectWritePathCapabilities,
18
20
  missingGates,
19
21
  staleRunnerGateFiles,
@@ -405,8 +407,6 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
405
407
  ? [...baseline.keys].filter((key) => !currentKeys.has(key)).length
406
408
  : 0;
407
409
  const activeCount = violations.length - suppressed;
408
- const missingSkills = skillGaps.reduce((sum, gap) => sum + gap.missing, 0);
409
- const staleSkills = skillGaps.reduce((sum, gap) => sum + gap.stale, 0);
410
410
  const designSmells = detectDesignSmells(root, config, files, cov);
411
411
  const designFitness = summarizeDesignFitness(designSmells, {
412
412
  activeViolations: activeCount,
@@ -769,11 +769,36 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
769
769
  line(bad, `Missing gates: ${gatesMissing.join(', ')}`);
770
770
  actions.push(`install gates (${arkCommand(root, 'ark-check', '--install-agent-gates')})`);
771
771
  }
772
- if (missingSkills + staleSkills === 0) line(ok, '/ark-* skills current for detected tools');
773
- else {
774
- line(warn, `${missingSkills} missing / ${staleSkills} outdated /ark-* skill(s) for ${skillGaps.map((g) => g.tool).join(', ')}`);
772
+ // Report Codex legacy prompts and other-host missing/stale independently (never exclusive).
773
+ const legacyCodex = skillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
774
+ const remainingGaps = skillGaps.filter((g) => !(g.tool === 'codex' && g.legacyPromptsOnly));
775
+ const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
776
+ const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
777
+ if (remMiss + remStale === 0 && !legacyCodex) line(ok, '/ark-* skills current for detected tools');
778
+ if (legacyCodex) {
779
+ line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)');
780
+ actions.push('install Codex SKILL.md catalog (--install-agent-gates --skills-only --tools codex --force)');
781
+ }
782
+ if (remMiss + remStale > 0) {
783
+ line(warn, `${remMiss} missing / ${remStale} outdated /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}`);
775
784
  actions.push('refresh /ark-* skills (--install-agent-gates --skills-only --force)');
776
785
  }
786
+ const codexHomeGap = detectCodexHomeGap(root);
787
+ if (codexHomeGap) {
788
+ const parts = [
789
+ codexHomeGap.legacyPromptsOnly ? 'legacy-prompts-only' : null,
790
+ codexHomeGap.missing > 0 ? `${codexHomeGap.missing} missing` : null,
791
+ codexHomeGap.stale > 0 ? `${codexHomeGap.stale} outdated` : null,
792
+ ].filter(Boolean);
793
+ const deferred = !codexConcernIsActive();
794
+ // Deferred home debt is dim/info (not warn) so non-Codex sessions are not "incomplete".
795
+ if (deferred) {
796
+ line(color.dim('·'), color.dim(`Codex home skills ${parts.join(', ')} (deferred — not on Codex session)`));
797
+ } else {
798
+ line(warn, `Codex home skills ${parts.join(', ')}`);
799
+ actions.push('refresh Codex home skills (--install-agent-gates --skills-only --codex-home --force)');
800
+ }
801
+ }
777
802
 
778
803
  console.log('');
779
804
  console.log(color.bold('Baseline'));
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Design-depth + adoption extras for the HTML showcase report.
3
+ * Kept separate from html-report.mjs so the main renderer stays under LOC budget.
4
+ * Does not import html-report.mjs (avoids a cycle).
5
+ */
6
+ import {
7
+ detectDesignSmells,
8
+ summarizeDesignFitness,
9
+ buildPatternBetsFromSmells,
10
+ } from './design-smells.mjs';
11
+ import { summarizePilotLoop } from './pilot-loop.mjs';
12
+ import { buildPostGreenNextAction } from './post-green-path.mjs';
13
+ import { loadGoldenPattern, summarizeGoldenPattern } from './golden-pattern.mjs';
14
+ import { collectAdoptionGaps } from './mcp-adoption.mjs';
15
+
16
+ function esc(value) {
17
+ return String(value)
18
+ .replace(/&/g, '&amp;')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;');
22
+ }
23
+
24
+ /**
25
+ * Doctor-parity design depth + adoption for ark-check --report.
26
+ * @param {string} root
27
+ * @param {object} config
28
+ * @param {string[]} files
29
+ * @param {object} coverage
30
+ * @param {object[]} activeViolations
31
+ */
32
+ export function buildReportDepthPayload(root, config, files, coverage, activeViolations = []) {
33
+ const designSmells = detectDesignSmells(root, config, files, coverage);
34
+ const designFitness = summarizeDesignFitness(designSmells, {
35
+ activeViolations: activeViolations.length,
36
+ governedPercent: coverage?.governed?.percent,
37
+ totalFiles: coverage?.governed?.totalFiles,
38
+ });
39
+ const postGreenPath = buildPostGreenNextAction(designFitness);
40
+ const patternBets = buildPatternBetsFromSmells(designSmells);
41
+ const pilotLoop = summarizePilotLoop({
42
+ designWeak: designFitness.designWeak,
43
+ patternBets,
44
+ designSmells,
45
+ });
46
+ const goldenPattern = summarizeGoldenPattern(loadGoldenPattern(root));
47
+ const adoption = collectAdoptionGaps(root, config, coverage);
48
+ return {
49
+ adoption,
50
+ designDepth: {
51
+ designFitness,
52
+ designSmells,
53
+ pilotLoop,
54
+ postGreenPath,
55
+ goldenPattern,
56
+ },
57
+ };
58
+ }
59
+
60
+ /** Write-path mode → human meaning (active host projection). */
61
+ export function writePathModeHint(mode) {
62
+ switch (String(mode || '')) {
63
+ case 'repair':
64
+ return 'Hard write hook with repair payload — best co-pilot path for the active host.';
65
+ case 'reject-only':
66
+ return 'Hard write boundary without repair payload; edits can be blocked without guided re-entry.';
67
+ case 'mcp-only':
68
+ return 'Advisory MCP only — prepare-write/autoPatch available, no hard PreToolUse for this host.';
69
+ case 'none':
70
+ return 'No hard write boundary or advisory MCP for the active host (or host is unknown in this process).';
71
+ default:
72
+ return 'Session write-path capability for the active agent host.';
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Configured hosts from writePath inventory (hard or advisory evidence on disk).
78
+ * @param {object|null|undefined} writePath
79
+ * @returns {string[]}
80
+ */
81
+ export function inventoryConfiguredHosts(writePath) {
82
+ const hosts = writePath?.inventory?.hosts;
83
+ if (!hosts || typeof hosts !== 'object') return [];
84
+ return Object.entries(hosts)
85
+ .filter(([, rec]) => rec && rec.configured)
86
+ .map(([name]) => name);
87
+ }
88
+
89
+ /**
90
+ * Compact write-path line for the Adoption card.
91
+ * @param {object|null|undefined} writePath
92
+ */
93
+ export function renderWritePathAdoptionBlock(writePath) {
94
+ if (!writePath || typeof writePath !== 'object') return '';
95
+ const mode = writePath.mode || 'none';
96
+ const host = writePath.activeHost || 'unknown';
97
+ const inv = inventoryConfiguredHosts(writePath);
98
+ const invLine =
99
+ inv.length > 0
100
+ ? `Inventory on disk: ${inv.map((h) => esc(h)).join(', ')}.`
101
+ : 'No host write gates found on disk yet.';
102
+ const unknownNote =
103
+ host === 'unknown' && inv.length > 0
104
+ ? ' Session host unknown (shell/CI) — inventory is still real; set ARK_ACTIVE_HOST or run from an agent for session-accurate mode.'
105
+ : '';
106
+ const gapNote = writePath.gap
107
+ ? ` Gap: <b>${esc(writePath.gap.id)}</b> — ${esc(writePath.gap.message || '')}`
108
+ : '';
109
+ // invLine / gapNote already include escaped user content; only plain strings go through esc().
110
+ return `<div class="write-path-block" title="${esc(writePathModeHint(mode))}">
111
+ <p class="dim" style="margin:.65rem 0 .2rem;font-size:.84rem">
112
+ <b>Write path</b> · active host <code>${esc(host)}</code>
113
+ · mode <code>${esc(mode)}</code>
114
+ ${writePath.hookRepair ? '· repair ✓' : writePath.hookPresent ? '· reject-only' : ''}
115
+ ${writePath.mcpPresent ? '· MCP ✓' : ''}
116
+ </p>
117
+ <p class="kpi-hint" style="max-width:none;margin:0">
118
+ ${esc(writePathModeHint(mode))} ${invLine}${esc(unknownNote)}${gapNote}
119
+ </p>
120
+ </div>`;
121
+ }
122
+
123
+ /** Fixed legend for baseline policy signals. */
124
+ export function renderBaselineSignalLegend() {
125
+ return `<details class="baseline-legend" style="margin-top:.75rem">
126
+ <summary>Baseline policy signals (legend)</summary>
127
+ <ul class="senior-list" style="margin-top:.4rem">
128
+ <li><b>keep-empty</b> — ${esc(baselineLegendBody('keep-empty'))}</li>
129
+ <li><b>active-ratchet</b> — ${esc(baselineLegendBody('active-ratchet'))}</li>
130
+ <li><b>absent</b> — ${esc(baselineLegendBody('absent'))}</li>
131
+ </ul>
132
+ </details>`;
133
+ }
134
+
135
+ function baselineLegendBody(signal) {
136
+ switch (signal) {
137
+ case 'keep-empty':
138
+ return '`.ark-baseline.json` exists with 0 frozen keys; every violation is active (honest green).';
139
+ case 'active-ratchet':
140
+ return 'Known debt keys are frozen; new distinct violations still fail the check.';
141
+ case 'absent':
142
+ return 'No baseline file — all findings are active (or freeze not adopted).';
143
+ default:
144
+ return '';
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Design-weak / Shape residual strip for the showcase report.
150
+ * Null HTML when there is nothing useful to show.
151
+ *
152
+ * @param {{
153
+ * designFitness?: object|null,
154
+ * designSmells?: object[],
155
+ * pilotLoop?: object|null,
156
+ * postGreenPath?: object|null,
157
+ * goldenPattern?: object|null,
158
+ * mode?: string,
159
+ * }} depth
160
+ */
161
+ export function renderDesignDepthStrip(depth = {}) {
162
+ const fitness = depth.designFitness;
163
+ const smells = Array.isArray(depth.designSmells) ? depth.designSmells : [];
164
+ const designWeak = fitness?.designWeak === true;
165
+ if (!designWeak && smells.length === 0) return '';
166
+
167
+ const mode = String(depth.mode || '').toLowerCase();
168
+ const title = designWeak
169
+ ? mode === 'enforce'
170
+ ? 'ENFORCE · design-weak'
171
+ : `${(mode || 'edges').toUpperCase()} · design-weak`
172
+ : 'Design smells (edges still open)';
173
+ const lede = designWeak
174
+ ? 'Contract edges are clean, but lived design residual remains. This does not fail PASS — it blocks “healthy finished” until Shape work lands.'
175
+ : 'Design smells exist alongside open edge debt. Fix edges first; treat smells as Shape residual after green.';
176
+
177
+ const smellItems = smells
178
+ .slice(0, 6)
179
+ .map((s) => {
180
+ const outcome = s.outcome || s.message || s.id;
181
+ const evidence = (s.evidence || [])
182
+ .filter((e) => typeof e === 'string' && !e.startsWith('layer:') && !e.startsWith('layout:'))
183
+ .slice(0, 3);
184
+ const ev =
185
+ evidence.length > 0
186
+ ? ` <span class="dim">· ${evidence.map((e) => `<code>${esc(e)}</code>`).join(' ')}</span>`
187
+ : '';
188
+ return `<li><b>${esc(s.id)}</b> — ${esc(outcome)}${ev}</li>`;
189
+ })
190
+ .join('');
191
+
192
+ const pilot = depth.pilotLoop?.active && depth.pilotLoop?.nextPilot ? depth.pilotLoop.nextPilot : null;
193
+ const pilotHtml = pilot
194
+ ? `<div class="pilot-card">
195
+ <h3 style="margin-top:.85rem">Next pilot (one at a time)</h3>
196
+ <p class="dim" style="margin:.15rem 0 .4rem;font-size:.86rem">
197
+ Judgment only — never mechanical-safe · never multi-pilot batch
198
+ </p>
199
+ <ul class="senior-list">
200
+ <li><b>Smell</b> · <code>${esc(pilot.smellId || pilot.id || '—')}</code></li>
201
+ <li><b>Target</b> · <code>${esc(pilot.pilotTarget || pilot.pilot || '—')}</code></li>
202
+ ${
203
+ pilot.move || pilot.fix
204
+ ? `<li><b>Move</b> · ${esc(pilot.move || pilot.fix)}</li>`
205
+ : ''
206
+ }
207
+ ${
208
+ pilot.successSignal
209
+ ? `<li><b>Success</b> · ${esc(pilot.successSignal)}</li>`
210
+ : ''
211
+ }
212
+ ${
213
+ pilot.killSwitch
214
+ ? `<li><b>Kill-switch</b> · ${esc(pilot.killSwitch)}</li>`
215
+ : ''
216
+ }
217
+ </ul>
218
+ </div>`
219
+ : '';
220
+
221
+ const next =
222
+ depth.postGreenPath?.short ||
223
+ depth.postGreenPath?.action ||
224
+ (designWeak
225
+ ? '/ark-explore shape-focus → dual-plan B, then /ark-autopilot only with OK'
226
+ : null);
227
+ const nextHtml = next
228
+ ? `<p class="meta" style="margin-top:.75rem"><b>Primary next</b> · ${esc(next)}</p>`
229
+ : '';
230
+
231
+ const golden = depth.goldenPattern;
232
+ const goldenHtml =
233
+ golden && golden.present !== false && (golden.name || golden.norm)
234
+ ? `<p class="dim" style="margin-top:.5rem;font-size:.84rem">
235
+ Golden pattern (advisory for <b>new</b> code only):
236
+ <code>${esc(golden.name || 'pattern')}</code>
237
+ ${golden.norm ? ` — ${esc(golden.norm)}` : ''}
238
+ ${golden.examplePath ? ` · e.g. <code>${esc(golden.examplePath)}</code>` : ''}
239
+ </p>`
240
+ : designWeak
241
+ ? `<p class="dim" style="margin-top:.5rem;font-size:.84rem">
242
+ No <code>.ark/golden-pattern.json</code> yet — optional; helps agents place <b>new</b> code only.
243
+ </p>`
244
+ : '';
245
+
246
+ return `<div class="section card design-strip ${designWeak ? 'is-weak' : 'has-smells'}" id="design-depth">
247
+ <div class="design-head">
248
+ <span class="badge design" title="Shape residual — separate from PASS/FAIL edge honesty">${esc(title)}</span>
249
+ <span class="dim" style="font-size:.86rem">${designWeak ? 'Edges clean · residual remains' : 'Smells + open edges'}</span>
250
+ </div>
251
+ <p class="dim" style="margin:.55rem 0 .5rem;font-size:.9rem">${esc(lede)}</p>
252
+ ${smellItems ? `<ul class="senior-list">${smellItems}</ul>` : ''}
253
+ ${pilotHtml}
254
+ ${nextHtml}
255
+ ${goldenHtml}
256
+ </div>`;
257
+ }
258
+
259
+ /**
260
+ * Optional clean-depth note when edges + design are both healthy.
261
+ * Requires designFitness from a real sensor run (object). Null/undefined means
262
+ * depth was not computed — do not claim “OK” from missing data.
263
+ * @param {{ designFitness?: object|null, ok?: boolean, mode?: string }} depth
264
+ */
265
+ export function renderDesignCleanNote(depth = {}) {
266
+ if (!depth.ok) return '';
267
+ // Sensors never ran (callers that omit designDepth) → no strip.
268
+ if (depth.designFitness == null || typeof depth.designFitness !== 'object') return '';
269
+ if (depth.designFitness.designWeak) return '';
270
+ if ((depth.designFitness.smellCount ?? 0) > 0) return '';
271
+ if (String(depth.mode || '').toLowerCase() !== 'enforce') return '';
272
+ return `<div class="section card design-strip is-clean" id="design-depth">
273
+ <div class="design-head">
274
+ <span class="badge design-ok" title="No deterministic design smells with clean edges">Design depth · OK</span>
275
+ <span class="dim" style="font-size:.86rem">No design-weak residual detected</span>
276
+ </div>
277
+ <p class="dim" style="margin:.45rem 0 0;font-size:.88rem">
278
+ Edges and deterministic design sensors agree. Keep placing new code on the golden path;
279
+ re-run doctor after large refactors.
280
+ </p>
281
+ </div>`;
282
+ }