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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
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) {
|
|
@@ -43,10 +44,11 @@ export function detectActiveAgentHost(env = process.env) {
|
|
|
43
44
|
.toLowerCase();
|
|
44
45
|
if (explicit) return explicit;
|
|
45
46
|
|
|
46
|
-
// Grok / xAI Build
|
|
47
|
+
// Grok / xAI Build (include GROK_AGENT — common session signal missing in older detect)
|
|
47
48
|
if (
|
|
48
49
|
envTruthy(env.GROK_BUILD) ||
|
|
49
50
|
envTruthy(env.XAI_GROK) ||
|
|
51
|
+
envTruthy(env.GROK_AGENT) ||
|
|
50
52
|
env.GROK_WORKSPACE_ROOT ||
|
|
51
53
|
env.GROK_SESSION_ID
|
|
52
54
|
) {
|
|
@@ -143,13 +145,19 @@ export const KNOWN_TOOLS = [
|
|
|
143
145
|
];
|
|
144
146
|
|
|
145
147
|
// One canonical markdown per skill (templates/skills/*.md, shipped in the npm
|
|
146
|
-
// package); installed into each tool's slash-command location.
|
|
147
|
-
// frontmatter (name/description) is understood or harmlessly ignored
|
|
148
|
-
// host. Kiro has no command mechanism — its steering rule file is the
|
|
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.
|
|
149
156
|
export const SKILL_TOOL_TARGETS = {
|
|
150
157
|
claude: (name) => `.claude/skills/${name}/SKILL.md`,
|
|
151
158
|
cursor: (name) => `.cursor/commands/${name}.md`,
|
|
152
|
-
|
|
159
|
+
// Official Codex REPO skill scope (Agent Skills standard).
|
|
160
|
+
codex: (name) => `.agents/skills/${name}/SKILL.md`,
|
|
153
161
|
// Grok Build: project skills at .grok/skills/<name>/SKILL.md (slash-invocable).
|
|
154
162
|
grok: (name) => `.grok/skills/${name}/SKILL.md`,
|
|
155
163
|
windsurf: (name) => `.windsurf/workflows/${name}.md`,
|
|
@@ -253,6 +261,122 @@ export function skillTemplateNames() {
|
|
|
253
261
|
.map((entry) => path.basename(entry.name, '.md'));
|
|
254
262
|
}
|
|
255
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
|
+
|
|
256
380
|
// A normal ark-check run is the reliable discovery point for new /ark-* skills.
|
|
257
381
|
// Ark ships no install lifecycle script (a postinstall banner would be blocked by
|
|
258
382
|
// modern package managers' script-approval policy anyway, so careful users never
|
|
@@ -262,24 +386,94 @@ export function skillTemplateNames() {
|
|
|
262
386
|
// Advisory only — never affects the exit code. Copilot has no reliable directory
|
|
263
387
|
// signal, so it is not auto-detected (explicit --tools only), matching resolveTools.
|
|
264
388
|
export function detectCodexHomeGap(root) {
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
}
|
|
280
474
|
}
|
|
281
475
|
}
|
|
282
|
-
return
|
|
476
|
+
return { ok: missing.length === 0, missing, referenced, checkedTools };
|
|
283
477
|
}
|
|
284
478
|
|
|
285
479
|
export function detectSkillGaps(root) {
|
|
@@ -324,7 +518,94 @@ export function detectSkillGaps(root) {
|
|
|
324
518
|
if (installed === null || isVersionOlder(installed, version)) stale += 1;
|
|
325
519
|
}
|
|
326
520
|
}
|
|
327
|
-
|
|
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
|
+
}
|
|
328
540
|
}
|
|
329
541
|
return gaps;
|
|
330
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
|
+
}
|
package/bin/lib/weakest-link.mjs
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
350
|
-
severity: '
|
|
351
|
-
message:
|
|
352
|
-
'Architecture CI
|
|
353
|
-
|
|
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
|
-
|
|
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 = {
|
|
@@ -30,21 +30,32 @@ export function detectWritePathCapabilities(root, explicitHost) {
|
|
|
30
30
|
|
|
31
31
|
const tools = installToolsForHost(activeHost);
|
|
32
32
|
let gap = null;
|
|
33
|
+
// Repo inventory (any host) can show hard/advisory write while activeHost is
|
|
34
|
+
// unknown (plain shell / `npx ark-check --report` outside an agent session).
|
|
35
|
+
// Session projection stays mode=none (other hosts' hooks are not a guarantee for
|
|
36
|
+
// this process) — but do not open an adoption gap: gates exist on disk.
|
|
37
|
+
const inventoryHasWriteBoundary =
|
|
38
|
+
Boolean(inventory?.capabilities?.['hard-write']) ||
|
|
39
|
+
Boolean(inventory?.capabilities?.['advisory-write']);
|
|
33
40
|
if (mode === 'none') {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
if (activeHost === 'unknown' && inventoryHasWriteBoundary) {
|
|
42
|
+
gap = null;
|
|
43
|
+
} else {
|
|
44
|
+
gap = {
|
|
45
|
+
id: 'write-path-none',
|
|
46
|
+
severity: 'warn',
|
|
47
|
+
message:
|
|
48
|
+
`Active host ${activeHost} has no hard write boundary or advisory Ark MCP. ` +
|
|
49
|
+
(capabilities['merge-gate']
|
|
50
|
+
? 'The CI check remains separate and does not block local writes.'
|
|
51
|
+
: 'No Ark CI check was detected either.'),
|
|
52
|
+
fix: arkCommand(
|
|
53
|
+
root,
|
|
54
|
+
'ark-check',
|
|
55
|
+
`--install-agent-gates --tools ${tools}`
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
48
59
|
} else if (mode === 'reject-only') {
|
|
49
60
|
gap = {
|
|
50
61
|
id: 'write-path-reject-only',
|
|
@@ -61,17 +72,23 @@ export function detectWritePathCapabilities(root, explicitHost) {
|
|
|
61
72
|
),
|
|
62
73
|
};
|
|
63
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.';
|
|
64
82
|
gap = {
|
|
65
83
|
id: 'write-path-mcp-only',
|
|
66
84
|
severity: 'info',
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
),
|
|
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}`),
|
|
75
92
|
};
|
|
76
93
|
}
|
|
77
94
|
|
package/dist/index.cjs
CHANGED
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.
|
|
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.
|
|
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
package/docs/agent-guide.md
CHANGED
|
@@ -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/
|
|
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:
|