arkgate 4.1.1 → 4.2.0
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 +81 -3
- package/README.md +15 -4
- package/bin/ark-check-runtime.mjs +16 -5
- package/bin/ark-mcp-runtime.mjs +766 -64
- package/bin/lib/agent-gates.mjs +1 -0
- package/bin/lib/ci-and-commands.mjs +16 -7
- package/bin/lib/codex-home.mjs +90 -8
- package/bin/lib/design-smells.mjs +71 -9
- package/bin/lib/doctor-plan.mjs +36 -36
- package/bin/lib/effective-contract-load.mjs +73 -9
- package/bin/lib/enforcement-state.mjs +1 -1
- package/bin/lib/gate-files.mjs +441 -9
- package/bin/lib/github-enforcement.mjs +16 -3
- package/bin/lib/hook-templates.mjs +12 -11
- package/bin/lib/html-report-evolution.mjs +114 -0
- package/bin/lib/html-report.mjs +11 -89
- package/bin/lib/import-resolve.mjs +33 -11
- package/bin/lib/install-activation.mjs +87 -0
- package/bin/lib/install-migrate.mjs +66 -50
- package/bin/lib/managed-upgrade.mjs +10 -41
- package/bin/lib/mcp-adoption.mjs +15 -5
- package/bin/lib/pilot-loop.mjs +25 -8
- package/bin/lib/project-identity.mjs +103 -0
- package/bin/lib/report-snapshot-context.mjs +28 -0
- package/bin/lib/resident-hook.mjs +33 -9
- package/bin/lib/rules-inventory.mjs +100 -8
- package/bin/lib/skill-install.mjs +272 -22
- package/bin/lib/skill-write.mjs +899 -0
- package/bin/lib/start-preview.mjs +84 -1
- package/bin/lib/upgrade-command.mjs +2 -5
- package/dist/index.cjs +13 -13
- package/dist/index.d.ts +194 -2
- package/dist/index.js +13 -13
- package/docs/README.md +5 -3
- package/docs/agent-guide.md +110 -14
- package/docs/ai-gates.md +103 -18
- package/docs/assets/ark-write-gate.svg +2 -2
- package/docs/enthusiast/how-to-agent-gates.md +6 -0
- package/docs/package-surface.md +14 -9
- package/docs/product-voice.md +13 -1
- package/package.json +3 -1
- package/schemas/ark.project-identity.schema.json +116 -0
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +9 -0
- package/templates/skills/ark-architect.md +12 -2
- package/templates/skills/ark-autopilot.md +9 -0
- package/templates/skills/ark-contract.md +11 -1
- package/templates/skills/ark-coverage.md +9 -0
- package/templates/skills/ark-explain.md +13 -1
- package/templates/skills/ark-explore.md +9 -0
- package/templates/skills/ark-fix.md +10 -1
- package/templates/skills/ark-loop.md +11 -2
- package/templates/skills/ark-place.md +17 -6
- package/templates/skills/ark-runtime.md +8 -0
- package/templates/skills/ark-think.md +14 -2
- package/templates/skills/ark-upgrade.md +9 -0
|
@@ -224,10 +224,12 @@ export function arkPackageVersion() {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Insert `arkVersion: <v>` into a skill's YAML frontmatter (before its closing
|
|
227
|
-
// `---`). No frontmatter → returned unchanged. Idempotent for a given version
|
|
227
|
+
// `---`). No frontmatter → returned unchanged. Idempotent for a given version
|
|
228
|
+
// and preserves the checked-out line ending on Windows.
|
|
228
229
|
export function stampSkill(content, version) {
|
|
229
230
|
if (!version) return content;
|
|
230
|
-
const
|
|
231
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
232
|
+
const lines = content.split(/\r?\n/);
|
|
231
233
|
if (lines[0] !== '---') return content;
|
|
232
234
|
const closeIdx = lines.indexOf('---', 1);
|
|
233
235
|
if (closeIdx === -1) return content;
|
|
@@ -239,7 +241,7 @@ export function stampSkill(content, version) {
|
|
|
239
241
|
} else {
|
|
240
242
|
lines.splice(closeIdx, 0, `arkVersion: ${version}`);
|
|
241
243
|
}
|
|
242
|
-
return lines.join(
|
|
244
|
+
return lines.join(newline);
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
// Read the `arkVersion:` stamp from an installed skill file. Returns null when
|
|
@@ -260,17 +262,61 @@ function skillVersionFromContent(content) {
|
|
|
260
262
|
return match ? match[1].trim() : null;
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
const VERSION_PATTERN =
|
|
266
|
+
/^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
267
|
+
|
|
268
|
+
function parseVersion(value, strict) {
|
|
269
|
+
if (typeof value !== 'string') return null;
|
|
270
|
+
const match = value.match(VERSION_PATTERN);
|
|
271
|
+
if (!match || (strict && (match[2] === undefined || match[3] === undefined))) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const prerelease = match[4]?.split('.') ?? [];
|
|
275
|
+
if (
|
|
276
|
+
prerelease.some(
|
|
277
|
+
(identifier) =>
|
|
278
|
+
/^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith('0')
|
|
279
|
+
)
|
|
280
|
+
) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
core: [match[1], match[2] ?? '0', match[3] ?? '0'],
|
|
285
|
+
prerelease,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** True only for a complete SemVer 2.0.0 version. */
|
|
290
|
+
export function isValidSemver(value) {
|
|
291
|
+
return parseVersion(value, true) !== null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// SemVer precedence compare. A one- or two-component numeric core remains
|
|
295
|
+
// accepted for legacy skill stamps, so "1.7" < "1.7.5"; shared catalog metadata
|
|
296
|
+
// uses isValidSemver and therefore requires the complete x.y.z form.
|
|
265
297
|
export function isVersionOlder(a, b) {
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
298
|
+
const av = parseVersion(a, false);
|
|
299
|
+
const bv = parseVersion(b, false);
|
|
300
|
+
if (!av || !bv) return false;
|
|
301
|
+
for (let index = 0; index < 3; index += 1) {
|
|
302
|
+
const left = BigInt(av.core[index]);
|
|
303
|
+
const right = BigInt(bv.core[index]);
|
|
304
|
+
if (left !== right) return left < right;
|
|
305
|
+
}
|
|
306
|
+
if (av.prerelease.length === 0 || bv.prerelease.length === 0) {
|
|
307
|
+
return av.prerelease.length > 0 && bv.prerelease.length === 0;
|
|
308
|
+
}
|
|
309
|
+
const length = Math.max(av.prerelease.length, bv.prerelease.length);
|
|
310
|
+
for (let index = 0; index < length; index += 1) {
|
|
311
|
+
const left = av.prerelease[index];
|
|
312
|
+
const right = bv.prerelease[index];
|
|
313
|
+
if (left === undefined || right === undefined) return left === undefined;
|
|
314
|
+
if (left === right) continue;
|
|
315
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
316
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
317
|
+
if (leftNumeric && rightNumeric) return BigInt(left) < BigInt(right);
|
|
318
|
+
if (leftNumeric !== rightNumeric) return leftNumeric;
|
|
319
|
+
return left < right;
|
|
274
320
|
}
|
|
275
321
|
return false;
|
|
276
322
|
}
|
|
@@ -315,6 +361,74 @@ export function skillContentMatchesTemplate(installedContent, templateContent) {
|
|
|
315
361
|
return installedId === skillContentIdentity(stampSkill(templateContent, '0.0.0'));
|
|
316
362
|
}
|
|
317
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Decide whether one managed skill should be written.
|
|
366
|
+
*
|
|
367
|
+
* Repo catalogs belong to that repo's installed package, so an explicit --force
|
|
368
|
+
* may move them in either direction. Codex home is shared by every repo on the
|
|
369
|
+
* machine: a package older than the installed home stamp must never win, even
|
|
370
|
+
* under --force. In both scopes a version-stamp-only difference is a no-op; the
|
|
371
|
+
* skill body is the capability contract.
|
|
372
|
+
*
|
|
373
|
+
* @param {{
|
|
374
|
+
* existingContent?: string|null,
|
|
375
|
+
* targetContent: string,
|
|
376
|
+
* packageVersion?: string|null,
|
|
377
|
+
* force?: boolean,
|
|
378
|
+
* scope?: 'repo'|'home',
|
|
379
|
+
* }} input
|
|
380
|
+
* @returns {{
|
|
381
|
+
* action: 'write'|'skip',
|
|
382
|
+
* reason: 'missing'|'content-current'|'newer-home-version'|'unknown-source-version'|'existing-preserved'|'content-update',
|
|
383
|
+
* scope: 'repo'|'home',
|
|
384
|
+
* sourceVersion: string|null,
|
|
385
|
+
* installedVersion: string|null,
|
|
386
|
+
* conflict: boolean,
|
|
387
|
+
* downgradeBlocked: boolean,
|
|
388
|
+
* }}
|
|
389
|
+
*/
|
|
390
|
+
export function planSkillInstall(input) {
|
|
391
|
+
const scope = input.scope === 'home' ? 'home' : 'repo';
|
|
392
|
+
const existingContent = input.existingContent ?? null;
|
|
393
|
+
const targetContent = String(input.targetContent);
|
|
394
|
+
const sourceVersion =
|
|
395
|
+
input.packageVersion ?? skillVersionFromContent(targetContent);
|
|
396
|
+
const installedVersion = skillVersionFromContent(existingContent);
|
|
397
|
+
const result = (action, reason, conflict = false, downgradeBlocked = false) => ({
|
|
398
|
+
action,
|
|
399
|
+
reason,
|
|
400
|
+
scope,
|
|
401
|
+
sourceVersion,
|
|
402
|
+
installedVersion,
|
|
403
|
+
conflict,
|
|
404
|
+
downgradeBlocked,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (existingContent === null) return result('write', 'missing');
|
|
408
|
+
if (
|
|
409
|
+
existingContent === targetContent ||
|
|
410
|
+
skillContentIdentity(existingContent) === skillContentIdentity(targetContent)
|
|
411
|
+
) {
|
|
412
|
+
return result('skip', 'content-current');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (scope === 'home') {
|
|
416
|
+
if (installedVersion && !sourceVersion) {
|
|
417
|
+
return result('skip', 'unknown-source-version', true, true);
|
|
418
|
+
}
|
|
419
|
+
if (
|
|
420
|
+
installedVersion &&
|
|
421
|
+
sourceVersion &&
|
|
422
|
+
isVersionOlder(sourceVersion, installedVersion)
|
|
423
|
+
) {
|
|
424
|
+
return result('skip', 'newer-home-version', true, true);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!input.force) return result('skip', 'existing-preserved', true);
|
|
429
|
+
return result('write', 'content-update');
|
|
430
|
+
}
|
|
431
|
+
|
|
318
432
|
/** @returns {Record<string, string>} skill name → template body from package */
|
|
319
433
|
export function skillTemplateBodies() {
|
|
320
434
|
return Object.fromEntries(skillTemplates());
|
|
@@ -422,6 +536,96 @@ export function assessSkillCatalogParity(skillNames, skillFile, packageVersion,
|
|
|
422
536
|
};
|
|
423
537
|
}
|
|
424
538
|
|
|
539
|
+
const CODEX_HOME_CATALOG = '.arkgate-catalog.json';
|
|
540
|
+
const CODEX_HOME_PENDING_CATALOG = '.arkgate-catalog.pending.json';
|
|
541
|
+
const CATALOG_TOKEN_PATTERN =
|
|
542
|
+
/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
543
|
+
|
|
544
|
+
function readCodexHomeCatalogMetadata(file, kind) {
|
|
545
|
+
try {
|
|
546
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
547
|
+
if (!stat) return { exists: false, valid: false, version: null };
|
|
548
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
|
|
549
|
+
return { exists: true, valid: false, version: null };
|
|
550
|
+
}
|
|
551
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
552
|
+
if (kind === 'pending') {
|
|
553
|
+
const keys =
|
|
554
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
555
|
+
? Object.keys(value).sort()
|
|
556
|
+
: [];
|
|
557
|
+
const valid =
|
|
558
|
+
keys.join(',') === 'packageVersion,schemaVersion,token' &&
|
|
559
|
+
value.schemaVersion === '1.0' &&
|
|
560
|
+
isValidSemver(value.packageVersion) &&
|
|
561
|
+
typeof value.token === 'string' &&
|
|
562
|
+
CATALOG_TOKEN_PATTERN.test(value.token);
|
|
563
|
+
return {
|
|
564
|
+
exists: true,
|
|
565
|
+
valid,
|
|
566
|
+
version: valid ? value.packageVersion : null,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (
|
|
570
|
+
value?.schemaVersion !== '1.0' ||
|
|
571
|
+
!isValidSemver(value.packageVersion) ||
|
|
572
|
+
!Array.isArray(value.skills)
|
|
573
|
+
) {
|
|
574
|
+
return { exists: true, valid: false, version: null };
|
|
575
|
+
}
|
|
576
|
+
const seen = new Set();
|
|
577
|
+
for (const skill of value.skills) {
|
|
578
|
+
if (
|
|
579
|
+
!skill ||
|
|
580
|
+
typeof skill.name !== 'string' ||
|
|
581
|
+
!/^ark-[a-z0-9-]+$/.test(skill.name) ||
|
|
582
|
+
typeof skill.contentIdentity !== 'string' ||
|
|
583
|
+
!/^sha256:[a-f0-9]{64}$/.test(skill.contentIdentity) ||
|
|
584
|
+
seen.has(skill.name)
|
|
585
|
+
) {
|
|
586
|
+
return { exists: true, valid: false, version: null };
|
|
587
|
+
}
|
|
588
|
+
seen.add(skill.name);
|
|
589
|
+
}
|
|
590
|
+
return { exists: true, valid: true, version: value.packageVersion };
|
|
591
|
+
} catch {
|
|
592
|
+
return { exists: true, valid: false, version: null };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function codexHomeCatalogState(skillsDir) {
|
|
597
|
+
const catalog = readCodexHomeCatalogMetadata(
|
|
598
|
+
path.join(skillsDir, CODEX_HOME_CATALOG),
|
|
599
|
+
'catalog'
|
|
600
|
+
);
|
|
601
|
+
const pending = readCodexHomeCatalogMetadata(
|
|
602
|
+
path.join(skillsDir, CODEX_HOME_PENDING_CATALOG),
|
|
603
|
+
'pending'
|
|
604
|
+
);
|
|
605
|
+
let floorVersion = catalog.version;
|
|
606
|
+
if (
|
|
607
|
+
pending.version &&
|
|
608
|
+
(!floorVersion || isVersionOlder(floorVersion, pending.version))
|
|
609
|
+
) {
|
|
610
|
+
floorVersion = pending.version;
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
floorVersion,
|
|
614
|
+
pendingVersion: pending.version,
|
|
615
|
+
hasMetadata: catalog.exists || pending.exists,
|
|
616
|
+
metadataInvalid:
|
|
617
|
+
(catalog.exists && !catalog.valid) || (pending.exists && !pending.valid),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function newerCodexHomeCatalogVersion(skillsDir, packageVersion, state = null) {
|
|
622
|
+
if (!isValidSemver(packageVersion)) return null;
|
|
623
|
+
const floorVersion = (state ?? codexHomeCatalogState(skillsDir)).floorVersion;
|
|
624
|
+
return floorVersion && isVersionOlder(packageVersion, floorVersion)
|
|
625
|
+
? floorVersion
|
|
626
|
+
: null;
|
|
627
|
+
}
|
|
628
|
+
|
|
425
629
|
/**
|
|
426
630
|
* Repo + home Codex skill parity against the shipping package skill set.
|
|
427
631
|
* Producer trees (templates/skills) and projects without AGENTS.md return null.
|
|
@@ -459,6 +663,14 @@ export function assessCodexSkillParity(root) {
|
|
|
459
663
|
const home = assessSkillCatalogParity(skillNames, homeSkill, packageVersion, {
|
|
460
664
|
legacyFile: homeLegacy,
|
|
461
665
|
});
|
|
666
|
+
const homeCatalogState = codexHomeCatalogState(skillsDir);
|
|
667
|
+
const newerHomeCatalog = newerCodexHomeCatalogVersion(
|
|
668
|
+
skillsDir,
|
|
669
|
+
packageVersion,
|
|
670
|
+
homeCatalogState
|
|
671
|
+
);
|
|
672
|
+
const pendingRecoveryRequired =
|
|
673
|
+
homeCatalogState.pendingVersion !== null && newerHomeCatalog === null;
|
|
462
674
|
|
|
463
675
|
// Repo catalog matters when .codex is present (Codex host adopted) or repo skills/prompts exist.
|
|
464
676
|
const repoInPlay =
|
|
@@ -467,14 +679,23 @@ export function assessCodexSkillParity(root) {
|
|
|
467
679
|
repo.hasLegacyPrompts;
|
|
468
680
|
// Home is "in play" only when ark skills or legacy prompts were actually installed there
|
|
469
681
|
// (empty $CODEX_HOME/skills is optional multi-project — not debt).
|
|
470
|
-
const homeInPlay =
|
|
682
|
+
const homeInPlay =
|
|
683
|
+
home.presentCount > 0 ||
|
|
684
|
+
home.hasLegacyPrompts ||
|
|
685
|
+
homeCatalogState.hasMetadata;
|
|
471
686
|
|
|
472
687
|
if (!repoInPlay && !homeInPlay) return null;
|
|
473
688
|
|
|
474
689
|
const repoNeedsAttention =
|
|
475
690
|
repoInPlay && (repo.missing > 0 || repo.stale > 0 || repo.legacyPromptsOnly);
|
|
476
691
|
const homeNeedsAttention =
|
|
477
|
-
|
|
692
|
+
newerHomeCatalog === null &&
|
|
693
|
+
homeInPlay &&
|
|
694
|
+
(home.missing > 0 ||
|
|
695
|
+
home.stale > 0 ||
|
|
696
|
+
home.legacyPromptsOnly ||
|
|
697
|
+
pendingRecoveryRequired ||
|
|
698
|
+
homeCatalogState.metadataInvalid);
|
|
478
699
|
|
|
479
700
|
return {
|
|
480
701
|
packageVersion,
|
|
@@ -485,6 +706,11 @@ export function assessCodexSkillParity(root) {
|
|
|
485
706
|
inPlay: homeInPlay,
|
|
486
707
|
skillsDir,
|
|
487
708
|
promptsDir,
|
|
709
|
+
catalogVersion: homeCatalogState.floorVersion,
|
|
710
|
+
catalogNewerThanPackage: newerHomeCatalog !== null,
|
|
711
|
+
pendingCatalogVersion: homeCatalogState.pendingVersion,
|
|
712
|
+
pendingRecoveryRequired,
|
|
713
|
+
catalogMetadataInvalid: homeCatalogState.metadataInvalid,
|
|
488
714
|
},
|
|
489
715
|
skillsDir,
|
|
490
716
|
promptsDir,
|
|
@@ -515,6 +741,14 @@ export function detectCodexHomeGap(root) {
|
|
|
515
741
|
expectedCount,
|
|
516
742
|
packageVersion,
|
|
517
743
|
skillsDir,
|
|
744
|
+
catalogVersion: home.catalogVersion,
|
|
745
|
+
pendingRecoveryRequired: Boolean(home.pendingRecoveryRequired),
|
|
746
|
+
catalogMetadataInvalid: Boolean(home.catalogMetadataInvalid),
|
|
747
|
+
catalogStateReason: home.catalogMetadataInvalid
|
|
748
|
+
? 'invalid catalog metadata'
|
|
749
|
+
: home.pendingRecoveryRequired
|
|
750
|
+
? 'interrupted catalog commit'
|
|
751
|
+
: null,
|
|
518
752
|
};
|
|
519
753
|
}
|
|
520
754
|
|
|
@@ -670,20 +904,32 @@ export function detectSkillGaps(root) {
|
|
|
670
904
|
return gaps;
|
|
671
905
|
}
|
|
672
906
|
|
|
907
|
+
/**
|
|
908
|
+
* Preserve the full detected inventory for JSON/reporting, but keep immediate
|
|
909
|
+
* human remediation scoped to the host running this process.
|
|
910
|
+
*/
|
|
911
|
+
export function skillGapsForActiveHost(skillGaps, env = process.env) {
|
|
912
|
+
const activeHost = detectActiveAgentHost(env);
|
|
913
|
+
if (!activeHost) return skillGaps ?? [];
|
|
914
|
+
return (skillGaps ?? []).filter((gap) => gap.tool === activeHost);
|
|
915
|
+
}
|
|
916
|
+
|
|
673
917
|
/**
|
|
674
918
|
* Human-facing skill / Codex catalog gap lines for ark-check (non-JSON).
|
|
675
919
|
* @param {string} root
|
|
676
|
-
* @param {{ skillGaps: object[], codexHomeGap: object|null, codexRepoSkillGap: object|null, codexSessionActive: boolean, color: { dim: Function, yellow: Function } }} opts
|
|
920
|
+
* @param {{ skillGaps: object[], codexHomeGap: object|null, codexRepoSkillGap: object|null, codexSessionActive: boolean, env?: NodeJS.ProcessEnv, color: { dim: Function, yellow: Function } }} opts
|
|
677
921
|
*/
|
|
678
922
|
export function printSkillAndCodexGapHints(root, opts) {
|
|
679
923
|
const { skillGaps, codexHomeGap, codexRepoSkillGap, codexSessionActive, color } = opts;
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
924
|
+
const activeSkillGaps = skillGapsForActiveHost(skillGaps, opts.env);
|
|
925
|
+
if (activeSkillGaps.length > 0) {
|
|
926
|
+
const legacyCodex = activeSkillGaps.some(
|
|
927
|
+
(gap) => gap.tool === 'codex' && gap.legacyPromptsOnly
|
|
928
|
+
);
|
|
929
|
+
const legacyAdvisory = activeSkillGaps.some(
|
|
683
930
|
(gap) => gap.tool === 'codex' && gap.legacyAdvisory && gap.catalogComplete
|
|
684
931
|
);
|
|
685
|
-
|
|
686
|
-
const remaining = skillGaps.filter(
|
|
932
|
+
const remaining = activeSkillGaps.filter(
|
|
687
933
|
(gap) =>
|
|
688
934
|
!(gap.tool === 'codex' && (gap.legacyPromptsOnly || gap.legacyAdvisory))
|
|
689
935
|
);
|
|
@@ -727,6 +973,8 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
727
973
|
if (codexHomeGap.legacyPromptsOnly) parts.push('legacy-prompts-only');
|
|
728
974
|
if (codexHomeGap.missing > 0) parts.push(`${codexHomeGap.missing} missing`);
|
|
729
975
|
if (codexHomeGap.stale > 0) parts.push(`${codexHomeGap.stale} content-behind-package`);
|
|
976
|
+
if (codexHomeGap.pendingRecoveryRequired) parts.push('interrupted catalog commit');
|
|
977
|
+
if (codexHomeGap.catalogMetadataInvalid) parts.push('invalid catalog metadata');
|
|
730
978
|
const deferred = !codexSessionActive;
|
|
731
979
|
const deferredNote = deferred
|
|
732
980
|
? ' Deferred unless you use Codex — not a blocker for Grok/Claude/Cursor. '
|
|
@@ -735,7 +983,9 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
735
983
|
`Codex home skill catalog (${codexSkillsDir()}) behind this Ark (${parts.join(', ')}).` +
|
|
736
984
|
deferredNote +
|
|
737
985
|
`Catalog is $CODEX_HOME/skills/<name>/SKILL.md (not flat prompts). ` +
|
|
738
|
-
|
|
986
|
+
(codexHomeGap.catalogMetadataInvalid
|
|
987
|
+
? 'Inspect the shared catalog metadata before retrying; invalid metadata fails safe.'
|
|
988
|
+
: `When using Codex: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --codex-home --force')}`);
|
|
739
989
|
console.log(deferred ? color.dim(msg) : color.yellow(msg));
|
|
740
990
|
}
|
|
741
991
|
if (codexRepoSkillGap && codexSessionActive) {
|