@dalzoubi/dev-agents-sync 3.0.2 → 3.0.4

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/README.md CHANGED
@@ -49,15 +49,17 @@ To adopt the toolkit with OpenAI Codex, pass the `codex` target explicitly:
49
49
  npx --yes @dalzoubi/dev-agents-sync@1 init --targets codex
50
50
  ```
51
51
 
52
- Codex is **opt-in only**. Unlike other targets there is no filesystem signal to detect (there is no `.codex/` directory), so `codex` is never auto-selected — you must pass `--targets codex` explicitly. You can combine it with other targets, e.g. `--targets claude,codex`.
52
+ Codex is **opt-in only** so an existing `AGENTS.md` or `.codex/` configuration is never adopted accidentally. Pass `--targets codex` explicitly; you can combine it with other targets, e.g. `--targets claude,codex`.
53
53
 
54
- **Output shape.** `init --targets codex` writes one directory-per-skill file per agent at `.agents/skills/<agent>/SKILL.md` — covering the full `define`/`deliver` topology listed under [Agents](#agents) — plus a thin root `AGENTS.md` index. Codex discovers these by scanning from the working directory up to the repo root, so each agent lives at its own `.agents/skills/<agent>/SKILL.md` path. Every file carries the managed-by marker, and `update` and `check` scope to these paths exactly like the other targets.
54
+ **Output shape.** `init --targets codex` writes one directory-per-skill file per agent at `.agents/skills/<agent>/SKILL.md`, a native custom-agent definition at `.codex/agents/<agent>.toml`, the shared lifecycle scripts under `.codex/hooks/`, their adapter and `.codex/hooks.json`, plus a thin root `AGENTS.md` index. Every file carries the managed-by marker, and `update`, `status`, and `check` scope to these paths exactly like the other targets.
55
55
 
56
56
  **Standalone.** Codex is a first-class standalone target. Each emitted `SKILL.md` bundles the workflow support docs it references, so a Codex-only install does **not** need `.claude/skills/*.md`, `.cursor/`, or any other target files to run the agent workflows.
57
57
 
58
- **Model-invoked, not slash commands.** Codex Agent Skills are model-invoked: a Codex user triggers an agent through `/skills`, a `$<name>` mention, or implicit selection by the model — *not* by typing a literal `/define`-style slash command the way Claude commands work. This is an accepted approximation of the slash-command UX the other targets provide; the agent content is the same, only the invocation differs.
58
+ **Invocation and delegation.** Codex Agent Skills are model-invoked through `/skills`, a `$<name>` mention, or implicit selection. The matching `.codex/agents/*.toml` definitions also make the topology available to Codex's native subagent delegation.
59
59
 
60
- **Manual removal.** Like every target, there is no uninstall or cleanup command. Dropping `codex` from the `targets` in `.dev-agents-sync.json` stops future writes, but the already-written `.agents/skills/<agent>/SKILL.md` directories and the root `AGENTS.md` are removed manually.
60
+ **Hooks.** The Codex target registers the same nine lifecycle policies as the Claude target in project-local `.codex/hooks.json`; Claude wiring remains in `.claude/settings.json`. Codex asks the user to trust project hooks the first time it loads them; review with `/hooks` and choose the appropriate trust option. The adapter translates Codex `apply_patch` events into the shared Edit/Write contract, so the same blocking and warning scripts remain the source of truth.
61
+
62
+ **Manual removal.** Like every target, there is no uninstall or cleanup command. Dropping `codex` from the `targets` in `.dev-agents-sync.json` stops future writes, but the already-written `.agents/skills/<agent>/SKILL.md`, `.codex/`, and root `AGENTS.md` files are removed manually.
61
63
 
62
64
  > **Maintainer note — two file shapes.** Do not confuse the Codex output with this toolkit's own authoring sources. In the `dalzoubi/dev-agents` repo, the canonical skill sources are **flat single files** at `.agents/skills/*.md`. The Codex target **output** is the **directory-per-skill** form `.agents/skills/<agent>/SKILL.md`. Both share the `.agents/skills/` prefix, but the flat `.md` files are for authoring and the `SKILL.md` directories are the generated codex output shape.
63
65
 
@@ -101,7 +103,7 @@ npx --yes @dalzoubi/dev-agents-sync@1 status
101
103
 
102
104
  ## Steering and hooks
103
105
 
104
- For the `claude` target, `init` and `update` set up two extra pieces of project memory and automation: a consumer-owned steering scaffold, and managed hook scripts that are auto-wired into `.claude/settings.json`.
106
+ For the `claude` target, `init` and `update` seed consumer-owned steering and auto-wire hooks into `.claude/settings.json`. The `codex` target ships the same hook policies through project-local `.codex/hooks.json`; steering remains consumer-owned and the workflow scratch path is `.agents/state/` for either runtime.
105
107
 
106
108
  ### Steering scaffold (consumer-owned)
107
109
 
@@ -111,7 +113,7 @@ The CLI seeds a blank steering scaffold under `.claude/steering/` — `decisions
111
113
 
112
114
  ### Hook scripts (managed)
113
115
 
114
- `init` and `update` install the toolkit's hook scripts into `.claude/hooks/` as marker-managed, lockfile-tracked, executable files. Most are **warning** hooks: they print one line to stderr and always exit 0. Three are **blocking** hooks that can deny a tool call outright.
116
+ `init` and `update` install the toolkit's hook scripts as marker-managed, lockfile-tracked executable files: `.claude/hooks/` for Claude and `.codex/hooks/` for Codex. Most are **warning** hooks: they print one line to stderr and always exit 0. Four are **blocking** hooks that can deny a tool call outright. Claude registrations are merged into `.claude/settings.json`; Codex registrations ship as managed `.codex/hooks.json` plus `.codex/hook-adapter.mjs`.
115
117
 
116
118
  | Script | Event | Matcher | Mode | What it does |
117
119
  |---|---|---|---|---|
@@ -119,6 +121,7 @@ The CLI seeds a blank steering scaffold under `.claude/steering/` — `decisions
119
121
  | `managed-file-guard.sh` | `PostToolUse` | `Edit\|Write` | warning | Guards managed files against unexpected edits. |
120
122
  | `uncommitted-check.sh` | `Stop` | `.*` | warning | Flags uncommitted work when a session ends. |
121
123
  | `force-push-guard.sh` | `PreToolUse` | `Bash` | **blocking** | Denies `git push` carrying `--force` / `-f` / `--force-with-lease` at `main` or `master` (by explicit refspec, or by the checked-out branch when the push names no ref). Force-pushing a feature branch, and a plain push to `main`, both pass. |
124
+ | `direct-push-guard.sh` | `PreToolUse` | `Bash` | **blocking** | Denies ANY `git push` — force or not — at `main`, `master`, or `develop` (by explicit refspec, or by the checked-out branch when the push names no ref). Pushing a feature branch passes; open a pull request instead. |
122
125
  | `spec-approval-gate.sh` | `PreToolUse` | `Edit\|Write` | **blocking** | Denies an edit that sets a spec to `Status: Approved` while an unresolved clarification marker — a bare bracketed `NEEDS CLARIFICATION` token — remains in prose. Occurrences inside code spans or fenced blocks are documentation and never match. |
123
126
  | `handoff-state-guard.sh` | `PreToolUse` | `Write` | **blocking** | Denies a write to `.claude/state/*.json` whose `handoff.status`, `from_agent`, or `to_agent` is outside the vocabulary `.claude/rules/handoff.md` defines. |
124
127
  | `spec-drift-warn.sh` | `PostToolUse` | `Edit\|Write` | warning | After an edit to `spec.md` / `design.md` / `tasks.md` under `docs/specs/`, warns when that folder's `spec.md` has already moved past `Draft`. Draft folders stay silent. |
@@ -135,6 +138,7 @@ A blocking hook runs on `PreToolUse`, where exiting 2 denies the tool call and t
135
138
  | Code | Raised by | Example message |
136
139
  |---|---|---|
137
140
  | `EHOOK_FORCE_PUSH` | `force-push-guard.sh` | `blocked: EHOOK_FORCE_PUSH — force-push to protected branch 'main' — push without a force flag or open a pull request; to override, set DEV_AGENTS_HOOKS_BYPASS=force-push-guard` |
141
+ | `EHOOK_DIRECT_PUSH` | `direct-push-guard.sh` | `blocked: EHOOK_DIRECT_PUSH — direct push to protected branch 'main' — open a pull request instead; to override, set DEV_AGENTS_HOOKS_BYPASS=direct-push-guard` |
138
142
  | `EHOOK_SPEC_APPROVAL` | `spec-approval-gate.sh` | `blocked: EHOOK_SPEC_APPROVAL — <path> sets Status: Approved while an unresolved clarification marker (a bare bracketed NEEDS CLARIFICATION token) remains in prose — resolve it via /define-analyze or /define before approving; to override, set DEV_AGENTS_HOOKS_BYPASS=spec-approval-gate` |
139
143
  | `EHOOK_HANDOFF_STATE` | `handoff-state-guard.sh` | `blocked: EHOOK_HANDOFF_STATE — handoff.status "done" is outside the status enum — fix the value in the state file before writing, or re-run with DEV_AGENTS_HOOKS_BYPASS=handoff-state-guard` |
140
144
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@dalzoubi/dev-agents-sync",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "type": "module",
5
- "description": "CLI that syncs managed dev-agent prompts into consumer repos (.claude/ and/or .cursor/).",
5
+ "description": "CLI that syncs managed dev-agent workflows into Claude Code, Cursor, Copilot, and Codex projects.",
6
6
  "license": "UNLICENSED",
7
7
  "bin": {
8
8
  "dev-agents-sync": "src/cli.mjs"
@@ -13,11 +13,7 @@ const TARGET_DIRS = {
13
13
  cursor: ['.cursor', 'rules'],
14
14
  };
15
15
 
16
- function countTargetFiles(consumerCwd, target) {
17
- const segs = TARGET_DIRS[target];
18
- if (!segs) return 0;
19
- // Count files recursively under <target-root>/
20
- const root = path.join(consumerCwd, segs[0]);
16
+ function countFiles(root) {
21
17
  if (!existsSync(root)) return 0;
22
18
  let n = 0;
23
19
  const stack = [root];
@@ -38,6 +34,19 @@ function countTargetFiles(consumerCwd, target) {
38
34
  return n;
39
35
  }
40
36
 
37
+ function countTargetFiles(consumerCwd, target) {
38
+ if (target === 'codex') {
39
+ return (existsSync(path.join(consumerCwd, 'AGENTS.md')) ? 1 : 0) +
40
+ countFiles(path.join(consumerCwd, '.agents', 'skills')) +
41
+ countFiles(path.join(consumerCwd, '.codex'));
42
+ }
43
+ const segs = TARGET_DIRS[target];
44
+ if (!segs) return 0;
45
+ // Count files recursively under <target-root>/
46
+ const root = path.join(consumerCwd, segs[0]);
47
+ return countFiles(root);
48
+ }
49
+
41
50
  export async function runStatus(consumerCwd, opts = {}) {
42
51
  const { availableTags } = opts;
43
52
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Target-neutral lifecycle-hook manifest.
3
+ *
4
+ * The content build and the sync CLI both import this module so Claude settings
5
+ * wiring and generated Codex hooks.json cannot silently drift apart.
6
+ */
7
+ export const HOOK_MANIFEST = Object.freeze([
8
+ { event: 'SessionStart', matcher: '.*', file: 'steering-injector.sh' },
9
+ { event: 'PostToolUse', matcher: 'Edit|Write', file: 'managed-file-guard.sh' },
10
+ { event: 'Stop', matcher: '.*', file: 'uncommitted-check.sh' },
11
+ { event: 'PreToolUse', matcher: 'Bash', file: 'force-push-guard.sh' },
12
+ { event: 'PreToolUse', matcher: 'Bash', file: 'direct-push-guard.sh' },
13
+ { event: 'PreToolUse', matcher: 'Edit|Write', file: 'spec-approval-gate.sh' },
14
+ { event: 'PreToolUse', matcher: 'Write', file: 'handoff-state-guard.sh' },
15
+ { event: 'PostToolUse', matcher: 'Edit|Write', file: 'spec-drift-warn.sh' },
16
+ { event: 'PreToolUse', matcher: 'Bash', file: 'pre-pr-preflight.sh' },
17
+ { event: 'Stop', matcher: '.*', file: 'ui-verification-reminder.sh' },
18
+ ]);
19
+
20
+ export function hookPathFor(target, file) {
21
+ if (target === 'claude') return `.claude/hooks/${file}`;
22
+ if (target === 'codex') return `.codex/hooks/${file}`;
23
+ throw new Error(`unsupported hook target: ${target}`);
24
+ }
package/src/marker.mjs CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const MARKER_RE = /^<!--\s*managed-by:\s*dev-agents-sync\s+v([0-9]+\.[0-9]+\.[0-9]+(?:[-+][\w.-]+)?)\s*-->\s*$/;
12
+ const COMMENT_MARKER_RE = /^(?:#|\/\/)\s*managed-by:\s*dev-agents-sync\s+v([0-9]+\.[0-9]+\.[0-9]+(?:[-+][\w.-]+)?)\s*$/;
12
13
 
13
14
  export function buildMarker(version) {
14
15
  return `<!-- managed-by: dev-agents-sync v${version} -->`;
@@ -48,7 +49,22 @@ export function hasMarker(content) {
48
49
  const body = stripFrontmatter(content);
49
50
  const first = firstNonEmptyLine(body);
50
51
  if (first === null) return false;
51
- return MARKER_RE.test(first);
52
+ if (MARKER_RE.test(first) || COMMENT_MARKER_RE.test(first)) return true;
53
+
54
+ // Executable scripts preserve their shebang on line 1 and carry the marker
55
+ // immediately after it. JSON cannot contain comments, so generated Codex
56
+ // hooks.json stores the unchanged HTML marker string in `_managedBy`.
57
+ const lines = body.split(/\r?\n/);
58
+ if (lines[0]?.startsWith('#!')) {
59
+ const second = firstNonEmptyLine(lines.slice(1).join('\n'));
60
+ return second !== null && COMMENT_MARKER_RE.test(second);
61
+ }
62
+ try {
63
+ const parsed = JSON.parse(body);
64
+ return typeof parsed?._managedBy === 'string' && MARKER_RE.test(parsed._managedBy);
65
+ } catch {
66
+ return false;
67
+ }
52
68
  }
53
69
 
54
70
  /**
@@ -66,6 +82,17 @@ export function extractMarkerVersion(content) {
66
82
  const body = stripFrontmatter(content);
67
83
  const first = firstNonEmptyLine(body);
68
84
  if (first === null) return null;
69
- const m = first.match(MARKER_RE);
85
+ let m = first.match(MARKER_RE) ?? first.match(COMMENT_MARKER_RE);
86
+ if (!m && first.startsWith('#!')) {
87
+ const second = firstNonEmptyLine(body.split(/\r?\n/).slice(1).join('\n'));
88
+ m = second?.match(COMMENT_MARKER_RE) ?? null;
89
+ }
90
+ if (!m) {
91
+ try {
92
+ m = JSON.parse(body)?._managedBy?.match(MARKER_RE) ?? null;
93
+ } catch {
94
+ m = null;
95
+ }
96
+ }
70
97
  return m ? m[1] : null;
71
98
  }
package/src/settings.mjs CHANGED
@@ -25,6 +25,7 @@
25
25
 
26
26
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
27
27
  import path from 'node:path';
28
+ import { HOOK_MANIFEST, hookPathFor } from './hook-manifest.mjs';
28
29
 
29
30
  /**
30
31
  * Property injected onto every CLI-managed hook entry so it can be identified
@@ -436,26 +437,10 @@ export function hookCommandFor(script) {
436
437
  * `Bash` matcher — one blocking, one warning — which the merge handles as two
437
438
  * independent registrations keyed by command.
438
439
  */
439
- export const DEFAULT_HOOK_SCRIPTS = [
440
- { event: 'SessionStart', matcher: '.*', script: '.claude/hooks/steering-injector.sh' },
441
- // Edit|Write, not `.*`: this hook reads a written file path out of the hook
442
- // payload and exits 0 silently when there is none, so on `.*` it was spawned
443
- // by every tool call — every Read, every Bash — to do nothing.
444
- { event: 'PostToolUse', matcher: 'Edit|Write', script: '.claude/hooks/managed-file-guard.sh' },
445
- { event: 'Stop', matcher: '.*', script: '.claude/hooks/uncommitted-check.sh' },
446
- // Added by docs/specs/260805-hooks-wave-2/ — the toolkit's first blocking
447
- // hooks plus two warning hooks. Blockers deny only on a positive violation
448
- // match, fail open otherwise, and honor DEV_AGENTS_HOOKS_BYPASS.
449
- { event: 'PreToolUse', matcher: 'Bash', script: '.claude/hooks/force-push-guard.sh' },
450
- { event: 'PreToolUse', matcher: 'Edit|Write', script: '.claude/hooks/spec-approval-gate.sh' },
451
- { event: 'PreToolUse', matcher: 'Write', script: '.claude/hooks/handoff-state-guard.sh' },
452
- { event: 'PostToolUse', matcher: 'Edit|Write', script: '.claude/hooks/spec-drift-warn.sh' },
453
- { event: 'PreToolUse', matcher: 'Bash', script: '.claude/hooks/pre-pr-preflight.sh' },
454
- // UI/UX framework: a Stop-time reminder that frontend work is governed by the
455
- // UI Definition of Done, not by a green build. Non-blocking by construction —
456
- // a Stop hook cannot deny, and this one exits 0 on every path.
457
- { event: 'Stop', matcher: '.*', script: '.claude/hooks/ui-verification-reminder.sh' },
458
- ].map((h) => ({ ...h, command: hookCommandFor(h.script) }));
440
+ export const DEFAULT_HOOK_SCRIPTS = HOOK_MANIFEST.map(({ event, matcher, file }) => {
441
+ const script = hookPathFor('claude', file);
442
+ return { event, matcher, script, command: hookCommandFor(script) };
443
+ });
459
444
 
460
445
  /**
461
446
  * Hook registrations the toolkit once shipped and has since retired.
@@ -511,4 +496,9 @@ export const RETIRED_HOOK_SCRIPTS = [
511
496
  // or by a future regression, and pruning it is always correct since that form
512
497
  // is never in DEFAULT_HOOK_SCRIPTS.
513
498
  { event: 'Stop', matcher: '.*', command: '.claude/hooks/ui-verification-reminder.sh' },
499
+ // direct-push-guard.sh ships only in the `bash …` form — same reasoning as
500
+ // ui-verification-reminder.sh above: it never shipped bare, but the
501
+ // bare-path form is pruned defensively so a hand-edit or regression can't
502
+ // register it live alongside the real one.
503
+ { event: 'PreToolUse', matcher: 'Bash', command: '.claude/hooks/direct-push-guard.sh' },
514
504
  ];
@@ -324,10 +324,12 @@ describe('S5 E1 — integrated init → update → check flow', () => {
324
324
  assert.ok(existsSync(absPath), `update must install .claude/hooks/${file}`);
325
325
 
326
326
  const st = statSync(absPath);
327
- assert.ok(
328
- (st.mode & 0o111) !== 0,
329
- `${file} must be executable after install (mode: 0o${st.mode.toString(8)})`,
330
- );
327
+ if (process.platform !== 'win32') {
328
+ assert.ok(
329
+ (st.mode & 0o111) !== 0,
330
+ `${file} must be executable after install (mode: 0o${st.mode.toString(8)})`,
331
+ );
332
+ }
331
333
  }
332
334
 
333
335
  // Lockfile must reference each installed hook script.
@@ -323,10 +323,12 @@ describe('S4 H1 — init installs hook scripts', () => {
323
323
  const absPath = path.join(hooksDir, file);
324
324
  const st = statSync(absPath);
325
325
  // On POSIX: mode & 0o111 checks owner+group+other execute bits.
326
- assert.ok(
327
- (st.mode & 0o111) !== 0,
328
- `${file} must be executable after install (mode: 0o${st.mode.toString(8)})`,
329
- );
326
+ if (process.platform !== 'win32') {
327
+ assert.ok(
328
+ (st.mode & 0o111) !== 0,
329
+ `${file} must be executable after install (mode: 0o${st.mode.toString(8)})`,
330
+ );
331
+ }
330
332
  }
331
333
  });
332
334
 
@@ -412,10 +414,12 @@ describe('S4 H1 — update installs hook scripts', () => {
412
414
  const hooksDir = path.join(consumerDir, '.claude', 'hooks');
413
415
  for (const file of HOOK_FILES) {
414
416
  const st = statSync(path.join(hooksDir, file));
415
- assert.ok(
416
- (st.mode & 0o111) !== 0,
417
- `update-installed ${file} must be executable`,
418
- );
417
+ if (process.platform !== 'win32') {
418
+ assert.ok(
419
+ (st.mode & 0o111) !== 0,
420
+ `update-installed ${file} must be executable`,
421
+ );
422
+ }
419
423
  }
420
424
  });
421
425
  });
@@ -295,10 +295,10 @@ describe('W2 — init and update install and wire every registered hook', () =>
295
295
  }
296
296
  });
297
297
 
298
- it('W2d: the two Bash PreToolUse hooks are both wired (a shared event+matcher is not deduped)', async () => {
299
- // force-push-guard and pre-pr-preflight share PreToolUse/Bash. Merge
300
- // identity is the COMMAND, so both must survive — collapsing on
301
- // event+matcher would silently drop one.
298
+ it('W2d: all three Bash PreToolUse hooks are wired (a shared event+matcher is not deduped)', async () => {
299
+ // force-push-guard, direct-push-guard, and pre-pr-preflight all share
300
+ // PreToolUse/Bash. Merge identity is the COMMAND, so all three must
301
+ // survive — collapsing on event+matcher would silently drop one.
302
302
  await runInit(consumerDir, {
303
303
  targets: ['claude'],
304
304
  fetcher: makeFixtureFetcher(),
@@ -312,10 +312,11 @@ describe('W2 — init and update install and wire every registered hook', () =>
312
312
  assert.deepEqual(
313
313
  commands,
314
314
  [
315
+ expectedCommand('.claude/hooks/direct-push-guard.sh'),
315
316
  expectedCommand('.claude/hooks/force-push-guard.sh'),
316
317
  expectedCommand('.claude/hooks/pre-pr-preflight.sh'),
317
318
  ],
318
- 'both PreToolUse/Bash hooks must be wired independently',
319
+ 'all three PreToolUse/Bash hooks must be wired independently',
319
320
  );
320
321
  });
321
322
 
@@ -187,4 +187,22 @@ describe('extractMarkerVersion', () => {
187
187
  const content = '# No marker here\nBody.';
188
188
  assert.equal(extractMarkerVersion(content), null);
189
189
  });
190
+
191
+ it('extracts versions from TOML, shebang scripts, and JSON manifests', () => {
192
+ assert.equal(extractMarkerVersion('# managed-by: dev-agents-sync v1.2.3\nname = "x"'), '1.2.3');
193
+ assert.equal(extractMarkerVersion('#!/usr/bin/env node\n// managed-by: dev-agents-sync v2.3.4\n'), '2.3.4');
194
+ assert.equal(extractMarkerVersion(JSON.stringify({ _managedBy: '<!-- managed-by: dev-agents-sync v3.4.5 -->' })), '3.4.5');
195
+ });
196
+ });
197
+
198
+ describe('hasMarker — Codex native file formats', () => {
199
+ it('accepts TOML comments, shebang-plus-comment scripts, and JSON manifests', () => {
200
+ assert.ok(hasMarker('# managed-by: dev-agents-sync v1.2.3\nname = "x"'));
201
+ assert.ok(hasMarker('#!/usr/bin/env node\n// managed-by: dev-agents-sync v1.2.3\n'));
202
+ assert.ok(hasMarker(JSON.stringify({ _managedBy: '<!-- managed-by: dev-agents-sync v1.2.3 -->' })));
203
+ });
204
+
205
+ it('rejects a comment marker buried after ordinary content', () => {
206
+ assert.equal(hasMarker('name = "x"\n# managed-by: dev-agents-sync v1.2.3'), false);
207
+ });
190
208
  });
@@ -205,6 +205,21 @@ describe('status', () => {
205
205
  'result must include file counts',
206
206
  );
207
207
  });
208
+
209
+ it('counts Codex root, skill, hook, and custom-agent files', async () => {
210
+ setupConsumerRepo(consumerDir, { targets: ['codex'], writeFiles: false });
211
+ mkdirSync(path.join(consumerDir, '.agents', 'skills', 'define'), { recursive: true });
212
+ mkdirSync(path.join(consumerDir, '.codex', 'agents'), { recursive: true });
213
+ mkdirSync(path.join(consumerDir, '.codex', 'hooks'), { recursive: true });
214
+ writeFileSync(path.join(consumerDir, 'AGENTS.md'), 'index');
215
+ writeFileSync(path.join(consumerDir, '.agents', 'skills', 'define', 'SKILL.md'), 'skill');
216
+ writeFileSync(path.join(consumerDir, '.codex', 'agents', 'define.toml'), 'agent');
217
+ writeFileSync(path.join(consumerDir, '.codex', 'hooks.json'), '{}');
218
+ writeFileSync(path.join(consumerDir, '.codex', 'hooks', 'guard.sh'), 'exit 0');
219
+
220
+ const result = await runStatus(consumerDir, { availableTags: ['v1.2.0'] });
221
+ assert.equal(result.fileCounts.codex, 5);
222
+ });
208
223
  });
209
224
 
210
225
  // ---------------------------------------------------------------------------