@dalzoubi/dev-agents-sync 3.0.1 → 3.0.3

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. Three 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
  |---|---|---|---|---|
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@dalzoubi/dev-agents-sync",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
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,23 @@
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: 'Edit|Write', file: 'spec-approval-gate.sh' },
13
+ { event: 'PreToolUse', matcher: 'Write', file: 'handoff-state-guard.sh' },
14
+ { event: 'PostToolUse', matcher: 'Edit|Write', file: 'spec-drift-warn.sh' },
15
+ { event: 'PreToolUse', matcher: 'Bash', file: 'pre-pr-preflight.sh' },
16
+ { event: 'Stop', matcher: '.*', file: 'ui-verification-reminder.sh' },
17
+ ]);
18
+
19
+ export function hookPathFor(target, file) {
20
+ if (target === 'claude') return `.claude/hooks/${file}`;
21
+ if (target === 'codex') return `.codex/hooks/${file}`;
22
+ throw new Error(`unsupported hook target: ${target}`);
23
+ }
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.
@@ -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
  });
@@ -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
  // ---------------------------------------------------------------------------