arkgate 4.7.0 → 4.7.1

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
@@ -5,6 +5,38 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.7.1 — 2026-08-25
9
+
10
+ **Patch** over **4.7.0**. One project skill catalog, visible package version in the
11
+ skill picker, no home duplicates, ArkRun routed through existing skill names.
12
+ **No required config migration.** Does not close Z09 / K01.
13
+
14
+ **Status: unpublished** (implementation on `main`; npm `latest` remains **4.7.0** until publish).
15
+
16
+ ### Added
17
+
18
+ - **Visible skill version (picker):** install stamps `description` with
19
+ `arkgate@<version>. ` so Codex/Claude/Cursor/Grok show the package pin without
20
+ opening the file. `arkVersion:` in YAML stays for doctor. Same-body stamp drift
21
+ refreshes without `--force` (`stamp-refresh`).
22
+ - **`--prune-home-duplicates`:** removes frozen `/ark-*` copies from
23
+ `$CODEX_HOME/skills`, `~/.claude/skills`, and `~/.grok/skills` when the project
24
+ already has `.agents/skills`. Never deletes non-Ark skills.
25
+
26
+ ### Changed
27
+
28
+ - **One project catalog:** `.agents/skills/<name>/SKILL.md` is the byte source.
29
+ Claude / Grok / OpenCode get relative adapter links. Cursor/Codex/Antigravity
30
+ already read `.agents/skills` — no second copy. `.cursor/commands/ark-*.md` is
31
+ no longer written (Cursor listed commands + skills as two copies).
32
+ - **`--codex-home` / `--agent-homes`:** skip home skill write (and home MCP bind)
33
+ when the project catalog or `.codex/config.toml` already exists. Codex lists
34
+ user+repo; a home copy is why `/ark-*` appeared twice and stayed old.
35
+ - **Doctor:** when home `ark-*` and project `.agents/skills` both exist, next
36
+ action is prune, not `--codex-home --force`.
37
+ - **`/ark-contract`:** routes ArkRun extra edits (first extra `/ark-adopt`,
38
+ companion `/ark-runtime`, new files `/ark-place`). No new skill names.
39
+
8
40
  ## 4.7.0 — 2026-08-25
9
41
 
10
42
  **Minor** over **4.6.7**. Ships **ArkRun**: an opt-in extra on schema `1.2` for kernel
@@ -12,8 +44,7 @@ usage and complete declarations, plus companion `@arkgate/runtime` DX. Absence i
12
44
  silent (Layers / ArkRules verdicts unchanged). In-memory stores remain
13
45
  reference-only. **No required config migration.** Does not close Z09 / K01.
14
46
 
15
- **Status: prepared** (see `docs/releases/4.7.0.md`). npm `latest` remains **4.6.7**
16
- until publish.
47
+ **Status: published** (on npm `latest`; see `docs/releases/4.7.0.md`).
17
48
 
18
49
  ### Added
19
50
 
package/README.md CHANGED
@@ -16,7 +16,7 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.7.0** is prepared on this tree. **4.6.7** remains npm `latest` until publish.
19
+ > **ArkGate 4.7.0** is on npm `latest`. Optional **ArkRun** extra on schema `1.2`.
20
20
  > A tree is **adopted** only with a required GitHub status running `arkgate-check --strict-merge`,
21
21
  > or `.ark/adoption-stance.json` `stance: "advisory-only"`. Doctor is compact (`--doctor --all`
22
22
  > for Details). [4.7.0 notes](docs/releases/4.7.0.md) · [4.6.7](docs/releases/4.6.7.md) ·
@@ -227,8 +227,8 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
227
227
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
228
228
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
229
229
  | Security | [SECURITY.md](SECURITY.md) |
230
- | Current tree (4.7.0 prepared) | [docs/releases/4.7.0.md](docs/releases/4.7.0.md) · [CHANGELOG](CHANGELOG.md) |
231
- | Current published (4.6.7 on npm `latest`) | [docs/releases/4.6.7.md](docs/releases/4.6.7.md) |
230
+ | Current published (4.7.0 on npm `latest`) | [docs/releases/4.7.0.md](docs/releases/4.7.0.md) · [CHANGELOG](CHANGELOG.md) |
231
+ | Prior published (4.6.7) | [docs/releases/4.6.7.md](docs/releases/4.6.7.md) |
232
232
  | Prior published (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) |
233
233
  | Prior published (4.6.5) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) |
234
234
  | Prior published (4.6.3) | [docs/releases/4.6.3.md](docs/releases/4.6.3.md) |
@@ -234,6 +234,35 @@ export function validateAgentSkillsPackage(entries) {
234
234
  presentCount: names.length,
235
235
  };
236
236
  }
237
+ /**
238
+ * Visible package stamp at the start of Agent Skills `description`.
239
+ * Hosts show `description` in the picker; `arkVersion:` in YAML is invisible there.
240
+ * Example: `arkgate@4.7.1. Session 0 — mark the Ark path.`
241
+ */
242
+ export const ARK_SKILL_DESCRIPTION_VERSION_PATTERN = /^arkgate@(\S+)\.\s/;
243
+ /** Prefix written at install time (`arkgate@<version>. `). */
244
+ export function skillDescriptionVersionPrefix(version) {
245
+ const v = String(version ?? '').trim();
246
+ return v ? `arkgate@${v}. ` : '';
247
+ }
248
+ /** Drop a leading `arkgate@<version>. ` stamp; other text is unchanged. */
249
+ export function stripSkillDescriptionVersion(description) {
250
+ return String(description ?? '').replace(ARK_SKILL_DESCRIPTION_VERSION_PATTERN, '');
251
+ }
252
+ /** Version inside a stamped description, or null when the prefix is absent. */
253
+ export function parseSkillDescriptionVersion(description) {
254
+ const match = String(description ?? '').match(ARK_SKILL_DESCRIPTION_VERSION_PATTERN);
255
+ return match?.[1] ?? null;
256
+ }
257
+ /**
258
+ * Idempotent: replace an existing `arkgate@…` prefix or add one.
259
+ * Empty `version` strips the prefix (authoring templates stay unversioned).
260
+ */
261
+ export function stampSkillDescription(description, version) {
262
+ const rest = stripSkillDescriptionVersion(description);
263
+ const v = typeof version === 'string' ? version.trim() : '';
264
+ return v ? `${skillDescriptionVersionPrefix(v)}${rest}` : rest;
265
+ }
237
266
  /**
238
267
  * Normalize skill file content for identity compare (LF newlines, strip BOM).
239
268
  * Does not strip or rewrite frontmatter — Agent Skills export is 1:1 with flat templates.
@@ -124,6 +124,7 @@ export function parseArgs(argv) {
124
124
  else if (arg === '--watch') args.watch = true;
125
125
  else if (arg === '--beginner') args.beginner = true;
126
126
  else if (arg === '--codex-home') args.codexHome = true;
127
+ else if (arg === '--prune-home-duplicates') args.pruneHomeDuplicates = true;
127
128
  else if (arg === '--claude-home') args.claudeHome = true;
128
129
  else if (arg === '--grok-home') args.grokHome = true;
129
130
  else if (arg === '--agent-homes') {
@@ -73,7 +73,11 @@ export function collectDoctorNextActions(ctx) {
73
73
  } else if (remStale > 0) {
74
74
  actions.push('refresh stale /ark-* skills (--install-agent-gates --skills-only --force) — gates are installed, catalog is stale');
75
75
  }
76
- if (ctx.codexHomeGap && ctx.codexConcernActive && ctx.codexHomeGap.preferProject !== true) {
76
+ if (ctx.codexHomeGap && ctx.codexConcernActive && ctx.codexHomeGap.duplicateHome) {
77
+ actions.push(
78
+ 'remove duplicate Codex home /ark-* skills (project .agents/skills is enough): --install-agent-gates --skills-only --prune-home-duplicates'
79
+ );
80
+ } else if (ctx.codexHomeGap && ctx.codexConcernActive && ctx.codexHomeGap.preferProject !== true) {
77
81
  actions.push(
78
82
  ctx.codexHomeGap.catalogMetadataInvalid
79
83
  ? 'repair invalid Codex home catalog metadata after verifying the newest installed version'
@@ -200,8 +200,8 @@ export function checkUsageAll() {
200
200
  'windsurf, cline, copilot, kiro, roo, continue, gemini',
201
201
  '(instruction-tier rule files derived from the same contract).',
202
202
  'It also installs the /ark-* skills shipped in templates/skills/ into each',
203
- 'detected tool\'s command location (.claude/skills/, .cursor/commands/,',
204
- '.agents/skills/ (Codex REPO catalog), .grok/skills/, .windsurf/workflows/,',
203
+ 'detected tool\'s command location (.agents/skills/ canonical catalog;',
204
+ '.claude/skills/ and .grok/skills/ adapters; .windsurf/workflows/,',
205
205
  '.clinerules/workflows/, .github/prompts/).',
206
206
  'Kiro, Roo, Continue, and Gemini have no command mechanism and receive only their',
207
207
  'rule file. Existing files are never overwritten without --force, so re-running',
@@ -68,8 +68,11 @@ import {
68
68
  detectSkillGaps,
69
69
  arkPackageVersion,
70
70
  verifyHostSkillCatalog,
71
+ canonicalSkillPath,
72
+ usesCanonicalSkillCatalog,
71
73
  } from './skill-install.mjs';
72
- import { installRepoSkillFile, installSkillCatalog, skillInstallLine, skillInstallNote } from './skill-write.mjs';
74
+ import { applySkillCatalogFollowup } from './skill-catalog-apply.mjs';
75
+ import { installRepoSkillFile, skillInstallNote } from './skill-write.mjs';
73
76
  import { detectDeployPathQuality } from './deploy-path.mjs';
74
77
  import {
75
78
  stripMcpServerArgs,
@@ -80,7 +83,6 @@ import {
80
83
  RUNNER_BEFORE_ARK,
81
84
  } from './mcp-adoption.mjs';
82
85
  import { inspectCodexInstallActivation, printCodexActivationHandoff, reportPartialInstall } from './install-activation.mjs';
83
- import { installRequestedAgentHomes } from './agent-homes.mjs';
84
86
  import {
85
87
  hasHardWriteHook,
86
88
  validateHardWriteRequest,
@@ -235,9 +237,18 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
235
237
  const skills = skillTemplates().map(([name, content]) => [name, stampSkill(content, version)]);
236
238
  const skillPaths = new Set();
237
239
  if (!compact) {
238
- for (const tool of selectedTools) {
240
+ const skillTools = [...selectedTools].filter((tool) => SKILL_TOOL_TARGETS[tool]);
241
+ const writeCanonical = skillTools.some((tool) => usesCanonicalSkillCatalog(tool));
242
+ if (writeCanonical) {
243
+ for (const [name, content] of skills) {
244
+ const relativePath = canonicalSkillPath(name);
245
+ skillPaths.add(relativePath);
246
+ add(relativePath, content, 'skill');
247
+ }
248
+ }
249
+ for (const tool of skillTools) {
250
+ if (usesCanonicalSkillCatalog(tool)) continue;
239
251
  const target = SKILL_TOOL_TARGETS[tool];
240
- if (!target) continue;
241
252
  for (const [name, content] of skills) {
242
253
  const relativePath = target(name);
243
254
  skillPaths.add(relativePath);
@@ -613,60 +624,12 @@ export function runInstallAgentGates(args) {
613
624
  console.log(` ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`);
614
625
  }
615
626
 
616
- // --codex-home writes SKILL.md skills to $CODEX_HOME/skills/<name>/SKILL.md.
617
- // Codex's real catalog loads skill directories (not flat $CODEX_HOME/prompts).
618
- // Repo installs already write `.agents/skills/<name>/SKILL.md` when `codex` is
619
- // selected; home install is for multi-project / non-repo-local refresh.
620
- const homeResults = [];
621
- if (args.codexHome) {
622
- const dir = codexSkillsDir();
623
- console.log('');
624
- console.log(
625
- `Codex home skills (scope=home-shared; source=${version ? `arkgate@${version}` : 'arkgate@unknown'}; target=${dir}/<name>/SKILL.md):`
626
- );
627
- console.log(
628
- ' Compatibility: monotonic downgrade protection requires every shared-catalog writer ' +
629
- 'to use ArkGate 4.2.0+; pre-4.2 --codex-home ignores this catalog. Upgrade legacy repos first.'
630
- );
631
- try {
632
- fs.mkdirSync(dir, { recursive: true });
633
- } catch (error) {
634
- console.error(` FAILED to create ${dir} (${error.message})`);
635
- homeResults.push({ relativePath: dir, status: 'failed' });
636
- }
637
- if (homeResults.length === 0) {
638
- const skillName = (skill) =>
639
- Array.isArray(skill) ? skill[0] : skill?.name || skill;
640
- const projectHasCatalog = skills.some((skill) =>
641
- fs.existsSync(path.join(root, '.agents', 'skills', skillName(skill), 'SKILL.md'))
642
- );
643
- if (projectHasCatalog) {
644
- console.log(
645
- ' Project .agents/skills already has this catalog; home write is optional. Prefer the project copy.'
646
- );
647
- }
648
- for (const result of installSkillCatalog({
649
- directory: dir,
650
- skills,
651
- packageVersion: version,
652
- force: args.force,
653
- scope: 'home',
654
- })) {
655
- console.log(skillInstallLine(result));
656
- homeResults.push(result);
657
- }
658
- }
659
- }
660
-
661
- installRequestedAgentHomes({
627
+ const { skillNames, homeResults } = applySkillCatalogFollowup({
662
628
  root,
629
+ tools,
663
630
  skills,
664
631
  version,
665
- force: args.force,
666
- claudeHome: args.claudeHome,
667
- grokHome: args.grokHome,
668
- agentHomes: args.agentHomes,
669
- json: args.json,
632
+ args,
670
633
  });
671
634
 
672
635
  // Optional legacy/home fallback. Normal Codex installs use the project-scoped
@@ -679,8 +642,10 @@ export function runInstallAgentGates(args) {
679
642
  // A redirected CODEX_HOME (tests/isolation) may still wire as requested.
680
643
  let codexMcp = null;
681
644
  const wantCodexWire = !args.compact && !args.skillsOnly && args.codexHome;
645
+ const projectCodexMcp = fs.existsSync(path.join(root, '.codex', 'config.toml'));
682
646
  const skipHomeWire =
683
- wantCodexWire && isTempOrUpgradeRoot(root) && usesDefaultCodexHome();
647
+ wantCodexWire &&
648
+ ((isTempOrUpgradeRoot(root) && usesDefaultCodexHome()) || projectCodexMcp);
684
649
  if (wantCodexWire && !skipHomeWire) {
685
650
  codexMcp = wireCodexMcp(root, args.force);
686
651
  console.log('');
@@ -702,7 +667,14 @@ export function runInstallAgentGates(args) {
702
667
  );
703
668
  }
704
669
  } else if (skipHomeWire) {
705
- codexMcp = { status: 'skipped', file: codexConfigPath(), reason: 'temp-root' };
670
+ const reason = projectCodexMcp ? 'project-config' : 'temp-root';
671
+ codexMcp = { status: 'skipped', file: codexConfigPath(), reason };
672
+ if (projectCodexMcp && !args.json) {
673
+ console.log('');
674
+ console.log(
675
+ 'Skip Codex home MCP — project .codex/config.toml is the binding. Home config pointing at another checkout is leftover; do not rebind it from this install.'
676
+ );
677
+ }
706
678
  }
707
679
 
708
680
  const { codexProjectConfigured, runtimeActivation } =
@@ -126,13 +126,8 @@ function hasArkText(root, relativePath) {
126
126
 
127
127
  const HOST_SIGNALS = {
128
128
  claude: ['.claude/settings.json', '.claude/skills/ark-upgrade/SKILL.md'],
129
- cursor: [
130
- '.cursor/mcp.json',
131
- '.cursor/hooks.json',
132
- '.cursor/rules/ark.mdc',
133
- '.cursor/commands/ark-upgrade.md',
134
- ],
135
- codex: ['.codex/hooks.json', '.codex/config.toml', '.agents/skills/ark-upgrade/SKILL.md'],
129
+ cursor: ['.cursor/mcp.json', '.cursor/hooks.json', '.cursor/rules/ark.mdc'],
130
+ codex: ['.codex/hooks.json', '.codex/config.toml'],
136
131
  grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json', '.grok/skills/ark-upgrade/SKILL.md'],
137
132
  antigravity: ['.agents/hooks.json'],
138
133
  opencode: ['opencode.json', '.opencode/skills/ark-upgrade/SKILL.md'],
@@ -0,0 +1,126 @@
1
+ /**
2
+ * HS post-write skill catalog: adapters, home skip/prune, Claude/Grok home skip.
3
+ * Kept out of install-migrate so that file stays inside its module budget.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { arkCommand } from '../ark-shared.mjs';
8
+ import { installRequestedAgentHomes } from './agent-homes.mjs';
9
+ import { codexSkillsDir } from './codex-home.mjs';
10
+ import {
11
+ canonicalSkillPath,
12
+ linkSkillHostAdapters,
13
+ pruneHomeArkSkillDuplicates,
14
+ skillTemplateNames,
15
+ } from './skill-install.mjs';
16
+ import { installSkillCatalog, skillInstallLine } from './skill-write.mjs';
17
+
18
+ function skillName(skill) {
19
+ return Array.isArray(skill) ? skill[0] : skill?.name || skill;
20
+ }
21
+
22
+ export function projectCatalogReady(root, skillNames) {
23
+ return skillNames.some((name) => fs.existsSync(path.join(root, canonicalSkillPath(name))));
24
+ }
25
+
26
+ export function applySkillCatalogFollowup({
27
+ root,
28
+ tools,
29
+ skills,
30
+ version,
31
+ args,
32
+ }) {
33
+ const skillNames = skills.map(skillName);
34
+ if (!args.compact && skillNames.length > 0) {
35
+ const adapterResults = linkSkillHostAdapters(root, tools, skillNames, Boolean(args.force));
36
+ for (const row of adapterResults) {
37
+ if (row.status === 'linked' || row.status === 'copied') {
38
+ console.log(
39
+ ` ${row.status.padEnd(7)} ${row.relativePath} (adapter → .agents/skills/${row.name})`
40
+ );
41
+ }
42
+ }
43
+ }
44
+ if (args.pruneHomeDuplicates) {
45
+ const pruned = pruneHomeArkSkillDuplicates(
46
+ root,
47
+ skillNames.length ? skillNames : skillTemplateNames()
48
+ );
49
+ if (!pruned.ok) {
50
+ console.log(' skip --prune-home-duplicates (no project .agents/skills catalog yet)');
51
+ } else if (pruned.removed.length === 0) {
52
+ console.log(' skip --prune-home-duplicates (no home ark-* copies)');
53
+ } else {
54
+ console.log(` pruned ${pruned.removed.length} home ark-* path(s) (project catalog is enough)`);
55
+ }
56
+ }
57
+
58
+ const homeResults = [];
59
+ if (args.codexHome) {
60
+ const dir = codexSkillsDir();
61
+ console.log('');
62
+ console.log(
63
+ `Codex home skills (scope=home-shared; source=${version ? `arkgate@${version}` : 'arkgate@unknown'}; target=${dir}/<name>/SKILL.md):`
64
+ );
65
+ console.log(
66
+ ' Compatibility: monotonic downgrade protection requires every shared-catalog writer ' +
67
+ 'to use ArkGate 4.2.0+; pre-4.2 --codex-home ignores this catalog. Upgrade legacy repos first.'
68
+ );
69
+ try {
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ } catch (error) {
72
+ console.error(` FAILED to create ${dir} (${error.message})`);
73
+ homeResults.push({ relativePath: dir, status: 'failed' });
74
+ }
75
+ if (homeResults.length === 0) {
76
+ const hasCatalog = skills.some((skill) =>
77
+ fs.existsSync(path.join(root, '.agents', 'skills', skillName(skill), 'SKILL.md'))
78
+ );
79
+ if (hasCatalog) {
80
+ console.log(
81
+ ' Skip home write — project .agents/skills is the catalog. Codex lists user+repo; a home copy duplicates every /ark-*.'
82
+ );
83
+ console.log(
84
+ ` Remove leftover home copies: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --prune-home-duplicates')}`
85
+ );
86
+ } else {
87
+ for (const result of installSkillCatalog({
88
+ directory: dir,
89
+ skills,
90
+ packageVersion: version,
91
+ force: args.force,
92
+ scope: 'home',
93
+ })) {
94
+ console.log(skillInstallLine(result));
95
+ homeResults.push(result);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ const catalogReady = projectCatalogReady(root, skillNames);
102
+ if ((args.claudeHome || args.grokHome || args.agentHomes) && catalogReady) {
103
+ if (!args.json) {
104
+ console.log('');
105
+ console.log(
106
+ 'Skip Claude/Grok home skill write — project .agents/skills + adapters are the catalog. Home ark-* copies override or duplicate.'
107
+ );
108
+ console.log(
109
+ ` Remove leftover home copies: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --prune-home-duplicates')}`
110
+ );
111
+ }
112
+ } else {
113
+ installRequestedAgentHomes({
114
+ root,
115
+ skills,
116
+ version,
117
+ force: args.force,
118
+ claudeHome: args.claudeHome,
119
+ grokHome: args.grokHome,
120
+ agentHomes: args.agentHomes,
121
+ json: args.json,
122
+ });
123
+ }
124
+
125
+ return { skillNames, homeResults };
126
+ }