arkgate 3.0.4 → 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,42 @@ 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.4.
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`.
8
43
 
9
44
  ## 3.0.4 — 2026-07-14
10
45
 
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';
@@ -301,8 +302,8 @@ function usage() {
301
302
  '(instruction-tier rule files derived from the same contract).',
302
303
  'It also installs the /ark-* skills shipped in templates/skills/ into each',
303
304
  'detected tool\'s command location (.claude/skills/, .cursor/commands/,',
304
- '.codex/prompts/, .grok/skills/, .windsurf/workflows/, .clinerules/workflows/,',
305
- '.github/prompts/).',
305
+ '.agents/skills/ (Codex REPO catalog), .grok/skills/, .windsurf/workflows/,',
306
+ '.clinerules/workflows/, .github/prompts/).',
306
307
  'Kiro, Roo, Continue, and Gemini have no command mechanism and receive only their',
307
308
  'rule file. Existing files are never overwritten without --force, so re-running',
308
309
  'after an update only adds what is missing. --skills-only restricts the write to',
@@ -1229,6 +1230,8 @@ async function main() {
1229
1230
 
1230
1231
  const skillGaps = detectSkillGaps(root);
1231
1232
  const codexHomeGap = detectCodexHomeGap(root);
1233
+ const codexRepoSkillGap = detectCodexRepoSkillGap(root);
1234
+ const codexSessionActive = codexConcernIsActive();
1232
1235
 
1233
1236
  if (args.report) {
1234
1237
  const exampleByLayer = new Map();
@@ -1372,7 +1375,15 @@ async function main() {
1372
1375
  warnings,
1373
1376
  ...(activeViolations.length > 0 ? { summary: summarizeViolations(activeViolations) } : {}),
1374
1377
  ...(skillGaps.length > 0 ? { skillGaps } : {}),
1375
- ...(codexHomeGap ? { codexHomeGap } : {}),
1378
+ ...(codexHomeGap
1379
+ ? {
1380
+ codexHomeGap: {
1381
+ ...codexHomeGap,
1382
+ deferred: !codexSessionActive,
1383
+ },
1384
+ }
1385
+ : {}),
1386
+ ...(codexRepoSkillGap ? { codexRepoSkillGap } : {}),
1376
1387
  }, null, 2));
1377
1388
  } else {
1378
1389
  for (const warning of warnings) {
@@ -1420,30 +1431,13 @@ async function main() {
1420
1431
  printViolationBreakdown(summarizeViolations(activeViolations), { toStderr: true });
1421
1432
  }
1422
1433
 
1423
- if (skillGaps.length > 0) {
1424
- const missingTotal = skillGaps.reduce((sum, gap) => sum + gap.missing, 0);
1425
- const staleTotal = skillGaps.reduce((sum, gap) => sum + gap.stale, 0);
1426
- const tools = skillGaps.map((gap) => gap.tool).join(', ');
1427
- if (missingTotal > 0) {
1428
- console.log(
1429
- color.dim(
1430
- `${missingTotal} /ark-* skill(s) not installed for ${tools} (this Ark version ships them). ` +
1431
- `Install: ${arkCommand(root, 'ark-check', '--install-agent-gates')}`
1432
- )
1433
- );
1434
- }
1435
- if (staleTotal > 0) {
1436
- // Stale skills already exist, so refreshing needs --force. --skills-only
1437
- // scopes the overwrite to the canonical skills, leaving a customized
1438
- // AGENTS.md / settings / CI untouched (a bare --force would clobber them).
1439
- console.log(
1440
- color.dim(
1441
- `${staleTotal} /ark-* skill(s) outdated for ${tools} (this Ark ships newer versions). ` +
1442
- `Refresh: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`
1443
- )
1444
- );
1445
- }
1446
- }
1434
+ printSkillAndCodexGapHints(root, {
1435
+ skillGaps,
1436
+ codexHomeGap,
1437
+ codexRepoSkillGap,
1438
+ codexSessionActive,
1439
+ color,
1440
+ });
1447
1441
 
1448
1442
  const staleRunners = staleRunnerGateFiles(root);
1449
1443
  if (staleRunners.length > 0) {
@@ -1464,27 +1458,6 @@ async function main() {
1464
1458
  )
1465
1459
  );
1466
1460
  }
1467
-
1468
- if (codexHomeGap) {
1469
- const parts = [];
1470
- if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
1471
- if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale} outdated`);
1472
- // Advisory always; when session host is known and not Codex, say so so
1473
- // /ark-upgrade does not chase home prompts as Incomplete.
1474
- const activeHost = detectActiveAgentHost();
1475
- const deferredNote =
1476
- activeHost != null && activeHost !== 'codex'
1477
- ? ' Deferred unless you use Codex — not a blocker for Grok/Claude/Cursor. '
1478
- : ' ';
1479
- console.log(
1480
- color.dim(
1481
- `/ark-* skills in ${codexPromptsDir()} are behind this Ark (${parts.join(', ')}).` +
1482
- deferredNote +
1483
- `Codex loads them from $CODEX_HOME/prompts, not the repo. ` +
1484
- `When using Codex: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --codex-home --force')}`
1485
- )
1486
- );
1487
- }
1488
1461
  }
1489
1462
 
1490
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'));
@@ -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 the canonical skills straight to $CODEX_HOME/prompts.
387
- // Codex reads prompts from there (not the repo), so this is the only way to
388
- // refresh them for a repo that isn't itself configured for Codex. It writes to
389
- // the user's home dir, hence explicit opt-in rather than part of a normal run.
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 = codexPromptsDir();
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 file = path.join(dir, `${name}.md`);
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-dir mutation when the project root is a temp/upgrade scratch *and*
433
- // CODEX_HOME resolves to the default (~/.codex). Codex itself may export that exact
434
- // path, so presence alone does not prove isolation. Fixtures must not rewrite the
435
- // developer's real config. A genuinely redirected CODEX_HOME or explicit
436
- // --codex-home still wires as requested.
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(` Codex: refreshed the /ark-* skills in ${codexPromptsDir()} — Codex loads them from there.`);
503
- } else if (skills.length > 0) {
504
- console.log(' Codex loads slash-command prompts from $CODEX_HOME/prompts (~/.codex/prompts),');
505
- console.log(' not the repo. Install the /ark-* skills there with:');
506
- console.log(` ${arkCommand(root, 'ark-check', '--install-agent-gates --codex-home')}`);
507
- console.log(' (writes to your home dir; agents driving this setup should offer to run it).');
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);
@@ -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;
@@ -3,7 +3,8 @@
3
3
  */
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- import { codexPromptsDir } from './codex-home.mjs';
6
+ import { arkCommand } from '../ark-shared.mjs';
7
+ import { codexPromptsDir, codexSkillsDir } from './codex-home.mjs';
7
8
  import { __packageRoot, isCompactRouterAgentsContent, readJson } from './gate-files.mjs';
8
9
 
9
10
  export function normalizeToolsList(tools) {
@@ -144,13 +145,19 @@ export const KNOWN_TOOLS = [
144
145
  ];
145
146
 
146
147
  // One canonical markdown per skill (templates/skills/*.md, shipped in the npm
147
- // package); installed into each tool's slash-command location. The YAML
148
- // frontmatter (name/description) is understood or harmlessly ignored by every
149
- // host. Kiro has no command mechanism — its steering rule file is the only gate.
148
+ // package); installed into each tool's slash-command / skill-catalog location.
149
+ // The YAML frontmatter (name/description) is understood or harmlessly ignored
150
+ // by every host. Kiro has no command mechanism — its steering rule file is the
151
+ // only gate.
152
+ //
153
+ // Codex: discovers Agent Skills directories with SKILL.md — repo path is the
154
+ // official `.agents/skills/<name>/SKILL.md` (not dead `.codex/prompts/*.md`).
155
+ // Home install uses `$CODEX_HOME/skills/<name>/SKILL.md` via --codex-home.
150
156
  export const SKILL_TOOL_TARGETS = {
151
157
  claude: (name) => `.claude/skills/${name}/SKILL.md`,
152
158
  cursor: (name) => `.cursor/commands/${name}.md`,
153
- codex: (name) => `.codex/prompts/${name}.md`,
159
+ // Official Codex REPO skill scope (Agent Skills standard).
160
+ codex: (name) => `.agents/skills/${name}/SKILL.md`,
154
161
  // Grok Build: project skills at .grok/skills/<name>/SKILL.md (slash-invocable).
155
162
  grok: (name) => `.grok/skills/${name}/SKILL.md`,
156
163
  windsurf: (name) => `.windsurf/workflows/${name}.md`,
@@ -254,6 +261,122 @@ export function skillTemplateNames() {
254
261
  .map((entry) => path.basename(entry.name, '.md'));
255
262
  }
256
263
 
264
+ /**
265
+ * Count present / stale / legacy-only skill files for one catalog root.
266
+ * @param {string[]} skillNames
267
+ * @param {(name: string) => string} skillFile path builder
268
+ * @param {string|null} packageVersion
269
+ * @param {{ legacyFile?: (name: string) => string }} [opts]
270
+ */
271
+ export function assessSkillCatalogParity(skillNames, skillFile, packageVersion, opts = {}) {
272
+ const expectedCount = skillNames.length;
273
+ const present = [];
274
+ let stale = 0;
275
+ for (const name of skillNames) {
276
+ const file = skillFile(name);
277
+ if (!fs.existsSync(file)) continue;
278
+ present.push(name);
279
+ if (packageVersion) {
280
+ const installed = installedSkillVersion(file);
281
+ if (installed === null || isVersionOlder(installed, packageVersion)) stale += 1;
282
+ }
283
+ }
284
+ let legacyCount = 0;
285
+ if (typeof opts.legacyFile === 'function') {
286
+ for (const name of skillNames) {
287
+ if (fs.existsSync(opts.legacyFile(name))) legacyCount += 1;
288
+ }
289
+ }
290
+ const presentCount = present.length;
291
+ const missing = expectedCount - presentCount;
292
+ const legacyPromptsOnly = presentCount === 0 && legacyCount > 0;
293
+ const hasLegacyPrompts = legacyCount > 0;
294
+ const ok = missing === 0 && stale === 0 && !legacyPromptsOnly;
295
+ return {
296
+ ok,
297
+ missing,
298
+ stale,
299
+ presentCount,
300
+ expectedCount,
301
+ packageVersion: packageVersion ?? null,
302
+ legacyPromptsOnly,
303
+ hasLegacyPrompts,
304
+ legacyCount,
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Repo + home Codex skill parity against the shipping package skill set.
310
+ * Producer trees (templates/skills) and projects without AGENTS.md return null.
311
+ *
312
+ * @param {string} root
313
+ * @returns {null | {
314
+ * packageVersion: string|null,
315
+ * expectedCount: number,
316
+ * repo: object,
317
+ * home: object,
318
+ * skillsDir: string,
319
+ * promptsDir: string,
320
+ * needsAttention: boolean,
321
+ * homeNeedsAttention: boolean,
322
+ * repoNeedsAttention: boolean,
323
+ * }}
324
+ */
325
+ export function assessCodexSkillParity(root) {
326
+ if (!fs.existsSync(path.join(root, 'AGENTS.md'))) return null;
327
+ if (fs.existsSync(path.join(root, 'templates', 'skills'))) return null;
328
+ const skillNames = skillTemplateNames();
329
+ if (skillNames.length === 0) return null;
330
+
331
+ const packageVersion = arkPackageVersion();
332
+ const skillsDir = codexSkillsDir();
333
+ const promptsDir = codexPromptsDir();
334
+ const repoSkill = (name) => path.join(root, SKILL_TOOL_TARGETS.codex(name));
335
+ const repoLegacy = (name) => path.join(root, '.codex', 'prompts', `${name}.md`);
336
+ const homeSkill = (name) => path.join(skillsDir, name, 'SKILL.md');
337
+ const homeLegacy = (name) => path.join(promptsDir, `${name}.md`);
338
+
339
+ const repo = assessSkillCatalogParity(skillNames, repoSkill, packageVersion, {
340
+ legacyFile: repoLegacy,
341
+ });
342
+ const home = assessSkillCatalogParity(skillNames, homeSkill, packageVersion, {
343
+ legacyFile: homeLegacy,
344
+ });
345
+
346
+ // Repo catalog matters when .codex is present (Codex host adopted) or repo skills/prompts exist.
347
+ const repoInPlay =
348
+ fs.existsSync(path.join(root, '.codex')) ||
349
+ repo.presentCount > 0 ||
350
+ repo.hasLegacyPrompts;
351
+ // Home is "in play" only when ark skills or legacy prompts were actually installed there
352
+ // (empty $CODEX_HOME/skills is optional multi-project — not debt).
353
+ const homeInPlay = home.presentCount > 0 || home.hasLegacyPrompts;
354
+
355
+ if (!repoInPlay && !homeInPlay) return null;
356
+
357
+ const repoNeedsAttention =
358
+ repoInPlay && (repo.missing > 0 || repo.stale > 0 || repo.legacyPromptsOnly);
359
+ const homeNeedsAttention =
360
+ homeInPlay && (home.missing > 0 || home.stale > 0 || home.legacyPromptsOnly);
361
+
362
+ return {
363
+ packageVersion,
364
+ expectedCount: skillNames.length,
365
+ repo: { ...repo, inPlay: repoInPlay },
366
+ home: {
367
+ ...home,
368
+ inPlay: homeInPlay,
369
+ skillsDir,
370
+ promptsDir,
371
+ },
372
+ skillsDir,
373
+ promptsDir,
374
+ repoNeedsAttention,
375
+ homeNeedsAttention,
376
+ needsAttention: repoNeedsAttention || homeNeedsAttention,
377
+ };
378
+ }
379
+
257
380
  // A normal ark-check run is the reliable discovery point for new /ark-* skills.
258
381
  // Ark ships no install lifecycle script (a postinstall banner would be blocked by
259
382
  // modern package managers' script-approval policy anyway, so careful users never
@@ -263,24 +386,94 @@ export function skillTemplateNames() {
263
386
  // Advisory only — never affects the exit code. Copilot has no reliable directory
264
387
  // signal, so it is not auto-detected (explicit --tools only), matching resolveTools.
265
388
  export function detectCodexHomeGap(root) {
266
- if (!fs.existsSync(path.join(root, 'AGENTS.md'))) return null;
267
- if (fs.existsSync(path.join(root, 'templates', 'skills'))) return null;
268
- const skillNames = skillTemplateNames();
269
- if (skillNames.length === 0) return null;
270
- const dir = codexPromptsDir();
271
- if (!fs.existsSync(dir)) return null;
272
- const present = skillNames.filter((name) => fs.existsSync(path.join(dir, `${name}.md`)));
273
- if (present.length === 0) return null; // Codex home never set up for Ark — don't nag.
274
- const version = arkPackageVersion();
275
- const missing = skillNames.length - present.length;
276
- let stale = 0;
277
- if (version) {
278
- for (const name of present) {
279
- const installed = installedSkillVersion(path.join(dir, `${name}.md`));
280
- if (installed === null || isVersionOlder(installed, version)) stale += 1;
389
+ const parity = assessCodexSkillParity(root);
390
+ if (!parity || !parity.homeNeedsAttention) return null;
391
+ const { home, packageVersion, expectedCount, skillsDir } = parity;
392
+ return {
393
+ missing: home.missing,
394
+ stale: home.stale,
395
+ legacyPromptsOnly: Boolean(home.legacyPromptsOnly),
396
+ hasLegacyPrompts: Boolean(home.hasLegacyPrompts),
397
+ presentCount: home.presentCount,
398
+ expectedCount,
399
+ packageVersion,
400
+ skillsDir,
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Repo-side Codex gaps: missing/stale .agents/skills or legacy .codex/prompts only.
406
+ * @param {string} root
407
+ * @returns {null | { missing: number, stale: number, legacyPromptsOnly: boolean, hasLegacyPrompts: boolean, presentCount: number, expectedCount: number, packageVersion: string|null }}
408
+ */
409
+ export function detectCodexRepoSkillGap(root) {
410
+ const parity = assessCodexSkillParity(root);
411
+ if (!parity || !parity.repoNeedsAttention) return null;
412
+ const { repo, packageVersion, expectedCount } = parity;
413
+ return {
414
+ missing: repo.missing,
415
+ stale: repo.stale,
416
+ legacyPromptsOnly: Boolean(repo.legacyPromptsOnly),
417
+ hasLegacyPrompts: Boolean(repo.hasLegacyPrompts),
418
+ presentCount: repo.presentCount,
419
+ expectedCount,
420
+ packageVersion,
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Skill names referenced as `/ark-*` in AGENTS.md (or any instruction text).
426
+ * @param {string} text
427
+ * @returns {string[]}
428
+ */
429
+ export function agentsMdSkillRefs(text) {
430
+ if (!text || typeof text !== 'string') return [];
431
+ const refs = new Set();
432
+ const re = /\/(ark-[a-z0-9-]+)/g;
433
+ let match;
434
+ while ((match = re.exec(text)) !== null) refs.add(match[1]);
435
+ return [...refs].sort();
436
+ }
437
+
438
+ /**
439
+ * Verify that every `/ark-*` skill referenced by AGENTS.md (and known to this
440
+ * package) is present in each selected host's skill catalog path.
441
+ *
442
+ * Compact routers intentionally omit `/ark-*` — they verify as ok with no checks.
443
+ *
444
+ * @param {string} root
445
+ * @param {Iterable<string>} tools
446
+ * @param {{ skillNames?: string[], agentsText?: string }} [options]
447
+ * @returns {{ ok: boolean, missing: Array<{ tool: string, name: string, path: string }>, referenced: string[], checkedTools: string[], compact?: boolean }}
448
+ */
449
+ export function verifyHostSkillCatalog(root, tools, options = {}) {
450
+ const skillNames = new Set(options.skillNames ?? skillTemplateNames());
451
+ let agentsText = options.agentsText;
452
+ if (agentsText == null) {
453
+ try {
454
+ agentsText = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
455
+ } catch {
456
+ return { ok: true, missing: [], referenced: [], checkedTools: [] };
457
+ }
458
+ }
459
+ if (isCompactRouterAgentsContent(agentsText)) {
460
+ return { ok: true, missing: [], referenced: [], checkedTools: [], compact: true };
461
+ }
462
+ const referenced = agentsMdSkillRefs(agentsText).filter((name) => skillNames.has(name));
463
+ const missing = [];
464
+ const checkedTools = [];
465
+ for (const tool of tools) {
466
+ const target = SKILL_TOOL_TARGETS[tool];
467
+ if (!target) continue;
468
+ checkedTools.push(tool);
469
+ for (const name of referenced) {
470
+ const relativePath = target(name);
471
+ if (!fs.existsSync(path.join(root, relativePath))) {
472
+ missing.push({ tool, name, path: relativePath });
473
+ }
281
474
  }
282
475
  }
283
- return missing > 0 || stale > 0 ? { missing, stale } : null;
476
+ return { ok: missing.length === 0, missing, referenced, checkedTools };
284
477
  }
285
478
 
286
479
  export function detectSkillGaps(root) {
@@ -325,7 +518,94 @@ export function detectSkillGaps(root) {
325
518
  if (installed === null || isVersionOlder(installed, version)) stale += 1;
326
519
  }
327
520
  }
328
- if (missing > 0 || stale > 0) gaps.push({ tool, missing, stale });
521
+ let legacyPromptsOnly = false;
522
+ let hasLegacyPrompts = false;
523
+ if (tool === 'codex') {
524
+ const legacyCount = skillNames.filter((name) =>
525
+ fs.existsSync(path.join(root, '.codex', 'prompts', `${name}.md`))
526
+ ).length;
527
+ hasLegacyPrompts = legacyCount > 0;
528
+ // Flat prompts without any SKILL.md catalog entries are not loadable.
529
+ legacyPromptsOnly = hasLegacyPrompts && missing === skillNames.length;
530
+ }
531
+ if (missing > 0 || stale > 0 || legacyPromptsOnly) {
532
+ gaps.push({
533
+ tool,
534
+ missing,
535
+ stale,
536
+ ...(legacyPromptsOnly ? { legacyPromptsOnly: true } : {}),
537
+ ...(hasLegacyPrompts ? { hasLegacyPrompts: true } : {}),
538
+ });
539
+ }
329
540
  }
330
541
  return gaps;
331
542
  }
543
+
544
+ /**
545
+ * Human-facing skill / Codex catalog gap lines for ark-check (non-JSON).
546
+ * @param {string} root
547
+ * @param {{ skillGaps: object[], codexHomeGap: object|null, codexRepoSkillGap: object|null, codexSessionActive: boolean, color: { dim: Function, yellow: Function } }} opts
548
+ */
549
+ export function printSkillAndCodexGapHints(root, opts) {
550
+ const { skillGaps, codexHomeGap, codexRepoSkillGap, codexSessionActive, color } = opts;
551
+ if (skillGaps?.length > 0) {
552
+ const legacyCodex = skillGaps.some((gap) => gap.tool === 'codex' && gap.legacyPromptsOnly);
553
+ // Report Codex legacy separately; never suppress missing/stale for other hosts.
554
+ const remaining = skillGaps.filter((gap) => !(gap.tool === 'codex' && gap.legacyPromptsOnly));
555
+ const missingTotal = remaining.reduce((sum, gap) => sum + gap.missing, 0);
556
+ const staleTotal = remaining.reduce((sum, gap) => sum + gap.stale, 0);
557
+ const tools = remaining.map((gap) => gap.tool).join(', ');
558
+ if (legacyCodex) {
559
+ console.log(
560
+ color.yellow(
561
+ 'Codex has legacy flat .codex/prompts/ark-*.md only — those are not loadable as skills. ' +
562
+ `Install the real catalog: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --tools codex --force')}`
563
+ )
564
+ );
565
+ }
566
+ if (missingTotal > 0) {
567
+ console.log(
568
+ color.dim(
569
+ `${missingTotal} /ark-* skill(s) not installed for ${tools} (this Ark version ships them). ` +
570
+ `Install: ${arkCommand(root, 'ark-check', '--install-agent-gates')}`
571
+ )
572
+ );
573
+ }
574
+ if (staleTotal > 0) {
575
+ console.log(
576
+ color.dim(
577
+ `${staleTotal} /ark-* skill(s) outdated for ${tools} (this Ark ships newer versions). ` +
578
+ `Refresh: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`
579
+ )
580
+ );
581
+ }
582
+ }
583
+ if (codexHomeGap) {
584
+ const parts = [];
585
+ if (codexHomeGap.legacyPromptsOnly) parts.push('legacy-prompts-only');
586
+ if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
587
+ if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale} outdated`);
588
+ const deferred = !codexSessionActive;
589
+ const deferredNote = deferred
590
+ ? ' Deferred unless you use Codex — not a blocker for Grok/Claude/Cursor. '
591
+ : ' ';
592
+ const msg =
593
+ `Codex home skill catalog (${codexSkillsDir()}) behind this Ark (${parts.join(', ')}).` +
594
+ deferredNote +
595
+ `Catalog is $CODEX_HOME/skills/<name>/SKILL.md (not flat prompts). ` +
596
+ `When using Codex: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --codex-home --force')}`;
597
+ console.log(deferred ? color.dim(msg) : color.yellow(msg));
598
+ }
599
+ if (codexRepoSkillGap && codexSessionActive) {
600
+ const parts = [];
601
+ if (codexRepoSkillGap.legacyPromptsOnly) parts.push('legacy-prompts-only');
602
+ if (codexRepoSkillGap.missing > 0) parts.push(`${codexRepoSkillGap.missing} missing`);
603
+ if (codexRepoSkillGap.stale > 0) parts.push(`${codexRepoSkillGap.stale} outdated`);
604
+ console.log(
605
+ color.yellow(
606
+ `Codex repo skill catalog (.agents/skills) needs refresh (${parts.join(', ')}). ` +
607
+ `Fix: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --tools codex --force')}`
608
+ )
609
+ );
610
+ }
611
+ }
@@ -48,6 +48,30 @@ export function detectPreCommitArk(root) {
48
48
  return { present, arkAware, path: hit };
49
49
  }
50
50
 
51
+ /**
52
+ * Classify ark-check flags in a workflow or package script body.
53
+ * CLI: `--strict` and `--strict-merge` both set strictConfig + requireGates (fail-closed).
54
+ * `--strict-config` alone does not require gate files.
55
+ *
56
+ * @param {string} text
57
+ * @returns {{ hasFailClosedFlag: boolean, hasStrictConfigOnly: boolean, hasStrictFlag: boolean }}
58
+ */
59
+ export function classifyArkCheckFlags(text) {
60
+ if (!text || typeof text !== 'string') {
61
+ return { hasFailClosedFlag: false, hasStrictConfigOnly: false, hasStrictFlag: false };
62
+ }
63
+ const hasStrictMerge = /--strict-merge\b/.test(text);
64
+ const hasRequireGates = /--require-gates\b/.test(text);
65
+ // Bare --strict (alias of --strict-merge), not --strict-config / already-matched merge.
66
+ const withoutLong = text.replace(/--strict-merge\b/g, ' ').replace(/--strict-config\b/g, ' ');
67
+ const hasBareStrict = /--strict\b/.test(withoutLong);
68
+ const hasFailClosedFlag = hasStrictMerge || hasRequireGates || hasBareStrict;
69
+ const hasStrictConfig = /--strict-config\b/.test(text);
70
+ const hasStrictConfigOnly = hasStrictConfig && !hasFailClosedFlag;
71
+ const hasStrictFlag = hasFailClosedFlag || hasStrictConfigOnly;
72
+ return { hasFailClosedFlag, hasStrictConfigOnly, hasStrictFlag };
73
+ }
74
+
51
75
  /**
52
76
  * @param {string} root
53
77
  * @returns {{
@@ -56,6 +80,9 @@ export function detectPreCommitArk(root) {
56
80
  * arkWorkflowFiles: string[],
57
81
  * hasArkCheckWorkflow: boolean,
58
82
  * hasStrictFlag: boolean,
83
+ * hasFailClosedFlag: boolean,
84
+ * hasStrictConfigOnly: boolean,
85
+ * failClosed: boolean,
59
86
  * hasArchitectureJobName: boolean,
60
87
  * }}
61
88
  */
@@ -67,6 +94,9 @@ export function detectCiEnforcement(root) {
67
94
  arkWorkflowFiles: [],
68
95
  hasArkCheckWorkflow: false,
69
96
  hasStrictFlag: false,
97
+ hasFailClosedFlag: false,
98
+ hasStrictConfigOnly: false,
99
+ failClosed: false,
70
100
  hasArchitectureJobName: false,
71
101
  };
72
102
  if (!out.hasWorkflowsDir) return out;
@@ -77,6 +107,19 @@ export function detectCiEnforcement(root) {
77
107
  return out;
78
108
  }
79
109
  out.workflowFiles = files;
110
+
111
+ let checkArchScript = '';
112
+ try {
113
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
114
+ checkArchScript =
115
+ typeof pkg?.scripts?.['check:architecture'] === 'string'
116
+ ? pkg.scripts['check:architecture']
117
+ : '';
118
+ } catch {
119
+ checkArchScript = '';
120
+ }
121
+ const scriptFlags = classifyArkCheckFlags(checkArchScript);
122
+
80
123
  for (const f of files) {
81
124
  let text = '';
82
125
  try {
@@ -92,9 +135,19 @@ export function detectCiEnforcement(root) {
92
135
  if (mentionsArk) {
93
136
  out.hasArkCheckWorkflow = true;
94
137
  out.arkWorkflowFiles.push(`.github/workflows/${f}`);
95
- if (/--strict\b/.test(text) || /check:architecture/.test(text)) {
138
+ const flags = classifyArkCheckFlags(text);
139
+ const viaScript = /check:architecture/.test(text) && scriptFlags.hasFailClosedFlag;
140
+ if (flags.hasFailClosedFlag || viaScript) {
141
+ out.hasFailClosedFlag = true;
142
+ out.failClosed = true;
143
+ out.hasStrictFlag = true;
144
+ } else if (flags.hasStrictConfigOnly) {
145
+ out.hasStrictConfigOnly = true;
146
+ out.hasStrictFlag = true;
147
+ } else if (flags.hasStrictFlag) {
96
148
  out.hasStrictFlag = true;
97
149
  }
150
+ // check:architecture without fail-closed flags in the script is NOT fail-closed.
98
151
  if (/architecture|ark-check|arkgate-check/i.test(f) || /name:\s*.*ark/i.test(text)) {
99
152
  out.hasArchitectureJobName = true;
100
153
  }
@@ -339,18 +392,14 @@ export function collectWeakestLinkGaps(root, opts = {}) {
339
392
  'CI workflows exist but none run ark-check / arkgate-check / check:architecture',
340
393
  fix: arkCommand(root, 'ark-check', '--install-agent-gates'),
341
394
  });
342
- } else if (
343
- adopted &&
344
- !isProducer &&
345
- ci.hasArkCheckWorkflow &&
346
- !ci.hasStrictFlag
347
- ) {
395
+ } else if (adopted && !isProducer && ci.hasArkCheckWorkflow && !ci.failClosed) {
348
396
  gaps.push({
349
- id: 'enforcement-ci-not-strict',
350
- severity: 'info',
351
- message:
352
- 'Architecture CI job found but does not pass --strict / check:architecture (weaker than recommended)',
353
- fix: 'Add --strict (or npm run check:architecture) to the architecture workflow step',
397
+ id: 'enforcement-ci-not-fail-closed',
398
+ severity: 'warn',
399
+ message: ci.hasStrictConfigOnly
400
+ ? 'Architecture CI uses --strict-config only (config coverage without gate-file presence). Prefer the fail-closed profile.'
401
+ : 'Architecture CI job found but does not use the fail-closed profile (--strict-merge / --strict / --require-gates)',
402
+ fix: 'ark-check --root . --config ark.config.json --strict-merge --baseline .ark-baseline.json',
354
403
  });
355
404
  }
356
405
 
@@ -121,7 +121,9 @@ function hostRecord(hard, advisory, repair, merge) {
121
121
  }
122
122
 
123
123
  export function detectWritePathInventory(root) {
124
- const merge = detectCiEnforcement(root).arkWorkflowFiles;
124
+ // Merge-gate evidence only when CI uses the fail-closed profile (not bare ark-check).
125
+ const ci = detectCiEnforcement(root);
126
+ const merge = ci.failClosed ? ci.arkWorkflowFiles : [];
125
127
  const claudeHook = hookEvidence(root, '.claude/settings.json');
126
128
  const grokHook = hookEvidence(root, '.grok/hooks/ark-write-gate.json');
127
129
  const hosts = {
@@ -72,17 +72,23 @@ export function detectWritePathCapabilities(root, explicitHost) {
72
72
  ),
73
73
  };
74
74
  } else if (mode === 'mcp-only') {
75
+ const codexHonesty =
76
+ activeHost === 'codex'
77
+ ? 'Codex local write is advisory (MCP + best-effort hooks.json — not a hard boundary; ' +
78
+ 'not equivalent to Claude/Grok PreToolUse hard-write + repair). ' +
79
+ 'The hard merge backstop is CI --strict-merge plus a required status check.'
80
+ : `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +
81
+ 'but no hard write boundary; the CI check can still reject the change before merge.';
75
82
  gap = {
76
83
  id: 'write-path-mcp-only',
77
84
  severity: 'info',
78
- message:
79
- `Active host ${activeHost} has advisory prepare-write/autoPatch tools, ` +
80
- 'but no hard write boundary; the CI check can still reject the change before merge.',
81
- fix: arkCommand(
82
- root,
83
- 'ark-check',
84
- `--install-agent-gates --tools ${tools}`
85
- ),
85
+ host: activeHost,
86
+ message: codexHonesty,
87
+ fix:
88
+ activeHost === 'codex'
89
+ ? 'Keep CI on --strict-merge and require the ark-check status on the default branch; ' +
90
+ `refresh Codex MCP/skills with ${arkCommand(root, 'ark-check', '--install-agent-gates --tools codex')}`
91
+ : arkCommand(root, 'ark-check', `--install-agent-gates --tools ${tools}`),
86
92
  };
87
93
  }
88
94
 
package/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ __export(gate_exports, {
50
50
  module.exports = __toCommonJS(gate_exports);
51
51
 
52
52
  // src/version.ts
53
- var version = "3.0.4";
53
+ var version = "3.0.5";
54
54
 
55
55
  // src/domain/adapterContract.ts
56
56
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.cjs';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.4";
5
+ declare const version = "3.0.5";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { a as ArkConfigRule, b as ArkConfigLayer, A as ArkConfig, c as ArkConfig
2
2
  export { d as ARK_CONFIG_SCHEMA, e as ARK_CONFIG_SCHEMA_VERSION, l as loadArkConfigContract, p as parseArkConfigJson } from './configContract-BxSIwVRo.js';
3
3
 
4
4
  /** ArkGate library version — single source of truth. */
5
- declare const version = "3.0.4";
5
+ declare const version = "3.0.5";
6
6
 
7
7
  /** Versioned public result contract shared by every ArkGate enforcement adapter. */
8
8
  declare const ARK_ANALYSIS_RESULT_SCHEMA_VERSION: "1.0";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var version = "3.0.4";
2
+ var version = "3.0.5";
3
3
 
4
4
  // src/domain/adapterContract.ts
5
5
  var ARK_ANALYSIS_RESULT_SCHEMA_VERSION = "1.0";
@@ -307,7 +307,7 @@ npx arkgate-check --install-agent-gates --tools claude,cursor,codex,grok
307
307
  |------|-----------------|-------------|
308
308
  | Claude Code | `.claude/settings.json` hook + `.mcp.json` / `claude mcp add` | `.claude/skills/<name>/SKILL.md` |
309
309
  | Cursor | `.cursor/mcp.json` + `.cursor/rules/ark.mdc` | `.cursor/commands/` |
310
- | OpenAI Codex | `$CODEX_HOME/config.toml` (global; absolute `--root`; multi-project → secondary `ark_<slug>` unless `--force`; doctor defers non-temp home gaps when session host ≠ Codex — see [ai-gates.md](ai-gates.md)) | `$CODEX_HOME/prompts` (`--codex-home`; fix when using Codex) |
310
+ | OpenAI Codex | `$CODEX_HOME/config.toml` (global; absolute `--root`; multi-project → secondary `ark_<slug>` unless `--force`; doctor defers non-temp home gaps when session host ≠ Codex — see [ai-gates.md](ai-gates.md)) | **Repo:** `.agents/skills/<name>/SKILL.md`; **home:** `$CODEX_HOME/skills/<name>/SKILL.md` (`--codex-home`) |
311
311
  | **Grok Build** | `.grok/hooks/ark-write-gate.json` + `.grok/config.toml` / `.mcp.json` | `.grok/skills/<name>/SKILL.md` |
312
312
 
313
313
  This is a path reference, not a guarantee table. Full copy-paste setups:
package/docs/ai-gates.md CHANGED
@@ -268,7 +268,7 @@ Hand-editing with relative `--root .` is wrong: Codex does not use the project a
268
268
 
269
269
  ```bash
270
270
  npx ark-check --install-agent-gates --tools codex
271
- # optional: install /ark-* slash prompts into $CODEX_HOME/prompts
271
+ # optional: install /ark-* skills into $CODEX_HOME/skills/<name>/SKILL.md
272
272
  npx ark-check --install-agent-gates --codex-home
273
273
  ```
274
274
 
@@ -309,11 +309,36 @@ primary A. It writes a **scoped secondary** table:
309
309
 
310
310
  `ark-check --doctor` surfaces the multi-project state so you are not left thinking B owns
311
311
  `ark://manifest` when only a secondary table exists. **Deferred (fix when using Codex):**
312
- non-temp Codex-home gaps (`codex-home-multi-project`, stale `$CODEX_HOME/prompts`) are
312
+ non-temp Codex-home gaps (`codex-home-multi-project`, stale `$CODEX_HOME/skills`) are
313
313
  severity **info**, marked `deferred: true`, and omitted from Top actions when the session
314
314
  host is known and not Codex — `/ark-upgrade` on Grok/Claude is not Incomplete because of
315
315
  them. **Temp/upgrade primary roots** stay fail-closed urgent (rewritten, not multi-project).
316
316
 
317
+ ### Codex skill catalog (SKILL.md, not flat prompts)
318
+
319
+ Codex discovers skills as directories containing `SKILL.md` (Agent Skills standard):
320
+
321
+ | Scope | Path |
322
+ |-------|------|
323
+ | **Repo** (written by `--tools codex`) | `.agents/skills/<name>/SKILL.md` |
324
+ | **Home** (optional `--codex-home`) | `$CODEX_HOME/skills/<name>/SKILL.md` |
325
+
326
+ Flat `.codex/prompts/*.md` files are **not** the invocable skill catalog. Install writes the
327
+ repo catalog above so AGENTS.md `/ark-*` references match what Codex can load. After install,
328
+ Ark verifies those references against each selected host catalog.
329
+
330
+ **Parity & honesty (doctor / install):**
331
+
332
+ - Doctor distinguishes **missing / stale / legacy-prompts-only** for repo (`.agents/skills`) and
333
+ home (`$CODEX_HOME/skills`). Home debt is **deferred** when the session host is not Codex.
334
+ - Legacy flat prompts alone are reported as non-loadable skill debt with a
335
+ `--skills-only --tools codex --force` (repo) or `--codex-home --force` (home) fix.
336
+ - Codex **write path is advisory**: MCP + best-effort `.codex/hooks.json` is **not** a hard
337
+ write boundary and is **not** equivalent to Claude/Grok PreToolUse hard-write + repair.
338
+ The hard merge backstop is CI `--strict-merge` (or `--strict`) plus a required status check.
339
+ - CI workflows that run ark-check without the fail-closed profile (or with only
340
+ `--strict-config`) surface gap `enforcement-ci-not-fail-closed`.
341
+
317
342
  ## Grok Build (xAI)
318
343
 
319
344
  Grok reads project rules from **`AGENTS.md`**, project MCP from **`.grok/config.toml`**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkgate",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "description": "ArkGate — architecture co-pilot for AI TypeScript (write gate, CI gate, plan/loop)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/pedroknigge/arkgate",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.0.4",
9
+ "version": "3.0.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "arkgate",
14
- "version": "3.0.4",
14
+ "version": "3.0.5",
15
15
  "runtimeHint": "npx",
16
16
  "transport": {
17
17
  "type": "stdio"
@@ -123,11 +123,15 @@ npx arkgate-check --install-agent-gates --skills-only --force
123
123
  **Active host first.** Refresh skills for the host running this skill (e.g.
124
124
  `.grok/skills/`, `.claude/skills/`, `.cursor/commands/`). Repo-local copies for
125
125
  other detected hosts are fine to refresh in the same pass when cheap.
126
- **Codex is deferred when you are not on Codex.** Prompts live in
127
- `$CODEX_HOME/prompts` (`~/.codex/prompts`), not the repo. `ark upgrade` may
128
- best-effort refresh that home when it exists; still list Codex under
129
- **Deferred hosts** and do **not** chase MCP multi-project / stale home skills
130
- until the user is on Codex (or asks). Fix command when needed:
126
+ **Codex is deferred when you are not on Codex.** Repo skills live in
127
+ `.agents/skills/<name>/SKILL.md`; optional home skills in
128
+ `$CODEX_HOME/skills/<name>/SKILL.md` (not legacy flat `$CODEX_HOME/prompts`).
129
+ When **on Codex**, refresh **both** repo catalog and home if doctor reports
130
+ missing/stale/legacy-prompts-only parity gaps. `ark upgrade` may best-effort
131
+ refresh home when it exists; still list Codex under **Deferred hosts** when
132
+ not on Codex and do **not** chase MCP multi-project / stale home skills until
133
+ the user is on Codex (or asks). Fix when needed:
134
+ `ark-check --install-agent-gates --skills-only --tools codex --force` and/or
131
135
  `ark-check --install-agent-gates --skills-only --codex-home --force`
132
136
  (and `--tools codex` / `--force` for primary MCP rebind). Exception: temp or
133
137
  `ark-upgrade` MCP `--root` paths — leave fail-closed rewrite to the CLI; do not