@dalzoubi/dev-agents-sync 3.0.10 → 3.0.12
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 +17 -3
- package/package.json +1 -1
- package/src/commands/init.mjs +10 -6
- package/src/commands/update.mjs +7 -3
- package/src/hooks.mjs +7 -0
- package/src/steering.mjs +9 -4
- package/tests/codex-steering.test.mjs +62 -0
package/README.md
CHANGED
|
@@ -51,13 +51,27 @@ npx --yes @dalzoubi/dev-agents-sync@1 init --targets codex
|
|
|
51
51
|
|
|
52
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`, 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.
|
|
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. Claude hook wiring uses `.claude/settings.json`. 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
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
|
-
|
|
60
|
+
For example: `$define Add export support`, `$deliver docs/specs/<spec>/spec.md --auto`,
|
|
61
|
+
`$deliver-solution Fix the export filename`, or `$deliver-validate docs/specs/<spec>/`.
|
|
62
|
+
The generated skills retain their command arguments (`$ARGUMENTS`) and flags,
|
|
63
|
+
including `--review-all`, `--verbose`, `--max-parallel=N`, and `--branch=<name>`.
|
|
64
|
+
Their runtime mapping translates `Agent`, `AskUserQuestion`, file tools, and
|
|
65
|
+
shell tools to Codex equivalents. Delegated custom agents carry the role and
|
|
66
|
+
support docs; main-thread command routing stays in the skill. Always-applied
|
|
67
|
+
rules accompany both forms. Models and runtime permissions inherit from Codex.
|
|
68
|
+
|
|
69
|
+
**Steering when switching.** Codex-only `init` and `update` seed consumer-owned
|
|
70
|
+
top-level `steering/`, without creating `.claude/`. If `.claude/steering/` already
|
|
71
|
+
exists, they reuse it and preserve its contents. Mixed Claude/Codex installs keep
|
|
72
|
+
that existing layout. `--dry-run` writes neither steering nor runtime files.
|
|
73
|
+
|
|
74
|
+
**Hooks.** The Codex target registers the same 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, reconstructing resulting content for updates and checking rename destinations. Shared scripts run from the git root even when the session starts in a subdirectory. Windows requires Git Bash. Hook execution or patch-reconstruction failures produce diagnostics rather than a false clean result.
|
|
61
75
|
|
|
62
76
|
**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.
|
|
63
77
|
|
|
@@ -107,7 +121,7 @@ For the `claude` target, `init` and `update` seed consumer-owned steering and au
|
|
|
107
121
|
|
|
108
122
|
### Steering scaffold (consumer-owned)
|
|
109
123
|
|
|
110
|
-
The CLI seeds a blank steering scaffold under `.claude/steering/` — `decisions.md`, `glossary.md`, `architecture.md`, and `delivery-log.md` — on first run. These files are **consumer-owned**: they carry no managed-by marker, are never tracked in the lockfile, and are **never overwritten** once they exist. Agents read all four at session start and append to them; the toolkit only seeds the blank files once. Editing them is expected.
|
|
124
|
+
The CLI seeds a blank steering scaffold under `.claude/steering/` for Claude or an existing shared history, and top-level `steering/` for a fresh Codex-only install — `decisions.md`, `glossary.md`, `architecture.md`, and `delivery-log.md` — on first run. These files are **consumer-owned**: they carry no managed-by marker, are never tracked in the lockfile, and are **never overwritten** once they exist. Agents read all four at session start and append to them; the toolkit only seeds the blank files once. Editing them is expected.
|
|
111
125
|
|
|
112
126
|
`delivery-log.md` is the feedback channel between the two agent namespaces: `deliver-document` appends one dated retrospective per shipped feature, and `define` reads that history to calibrate future estimates and cycle budgets.
|
|
113
127
|
|
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { hasMarker, hasMarkerOnFirstLine } from '../marker.mjs';
|
|
|
18
18
|
import { injectClaudeMd } from '../claudeMd.mjs';
|
|
19
19
|
import { seedSteering, steeringSummary } from '../steering.mjs';
|
|
20
20
|
import {
|
|
21
|
+
codexHooksSummary,
|
|
21
22
|
collectHookPaths,
|
|
22
23
|
hookScriptsForInstalled,
|
|
23
24
|
hooksSummary,
|
|
@@ -133,6 +134,7 @@ export async function runInit(consumerCwd, opts = {}) {
|
|
|
133
134
|
// We read from the normalised map so the content is available regardless of
|
|
134
135
|
// whether the `skills` dir ends up in the consumer's scoped file set.
|
|
135
136
|
const claudeTargetActive = resolvedTargets.includes('claude');
|
|
137
|
+
const steeringTarget = claudeTargetActive ? 'claude' : resolvedTargets.includes('codex') ? 'codex' : null;
|
|
136
138
|
const routingContent = claudeTargetActive
|
|
137
139
|
? (normalized['claude/skills/auto-route.md'] ?? null)
|
|
138
140
|
: null;
|
|
@@ -145,9 +147,9 @@ export async function runInit(consumerCwd, opts = {}) {
|
|
|
145
147
|
? injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent, { dryRun: true })
|
|
146
148
|
: null;
|
|
147
149
|
|
|
148
|
-
// Steering seed plan
|
|
149
|
-
const steering =
|
|
150
|
-
? seedSteering(consumerCwd, { dryRun: true })
|
|
150
|
+
// Steering seed plan for Claude or Codex. Writes nothing in dry-run.
|
|
151
|
+
const steering = steeringTarget
|
|
152
|
+
? seedSteering(consumerCwd, { dryRun: true, target: steeringTarget })
|
|
151
153
|
: null;
|
|
152
154
|
|
|
153
155
|
// Compute the planned settings.json merge too, mirroring update's dry-run
|
|
@@ -207,12 +209,12 @@ export async function runInit(consumerCwd, opts = {}) {
|
|
|
207
209
|
injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent);
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
// Seed the consumer-owned steering scaffold
|
|
212
|
+
// Seed the consumer-owned steering scaffold for Claude or Codex. Seed-if-absent:
|
|
211
213
|
// never overwrites edited steering, never recorded in the lockfile (it is unmanaged
|
|
212
214
|
// by design). Fails loud with ESTEER_WRITE on a write error.
|
|
213
215
|
let steering = null;
|
|
214
|
-
if (
|
|
215
|
-
steering = seedSteering(consumerCwd);
|
|
216
|
+
if (steeringTarget) {
|
|
217
|
+
steering = seedSteering(consumerCwd, { target: steeringTarget });
|
|
216
218
|
process.stderr.write(`info: dev-agents-sync: ${steeringSummary(steering)}\n`);
|
|
217
219
|
}
|
|
218
220
|
|
|
@@ -220,6 +222,8 @@ export async function runInit(consumerCwd, opts = {}) {
|
|
|
220
222
|
// only), unless the consumer opted out with --no-hooks-wiring. The merge is
|
|
221
223
|
// additive + idempotent and fails loud (ESETTINGS_PARSE) on a malformed file.
|
|
222
224
|
const installedHookKeys = plannedWrites.map(({ relKey }) => relKey);
|
|
225
|
+
const codexNotice = codexHooksSummary(installedHookKeys);
|
|
226
|
+
if (codexNotice) process.stderr.write(`info: dev-agents-sync: ${codexNotice}\n`);
|
|
223
227
|
const hookPaths = collectHookPaths(installedHookKeys);
|
|
224
228
|
const hookRegistrations = hookScriptsForInstalled(installedHookKeys);
|
|
225
229
|
// Report the install BEFORE wiring, so the pair reads in the order it happened
|
package/src/commands/update.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { injectClaudeMd } from '../claudeMd.mjs';
|
|
|
17
17
|
import { seedSteering, steeringSummary } from '../steering.mjs';
|
|
18
18
|
import { DEFAULT_HOOK_SCRIPTS } from '../settings.mjs';
|
|
19
19
|
import {
|
|
20
|
+
codexHooksSummary,
|
|
20
21
|
collectHookPaths,
|
|
21
22
|
hookScriptsForInstalled,
|
|
22
23
|
hooksSummary,
|
|
@@ -130,14 +131,15 @@ export async function runUpdate(consumerCwd, opts = {}) {
|
|
|
130
131
|
|
|
131
132
|
const targetVersion = resolveRange(availableTags, lock.range);
|
|
132
133
|
|
|
133
|
-
// Seed the consumer-owned steering scaffold
|
|
134
|
+
// Seed the consumer-owned steering scaffold for Claude or Codex, seed-if-absent,
|
|
134
135
|
// BEFORE the up-to-date short-circuit so existing consumers adopt steering on any
|
|
135
136
|
// `update` — not only when a version bump is available. Never overwrites edited
|
|
136
137
|
// steering; never recorded in the lockfile (unmanaged by design); respects --dry-run.
|
|
137
138
|
const claudeTargetActive = lock.targets.includes('claude');
|
|
139
|
+
const steeringTarget = claudeTargetActive ? 'claude' : lock.targets.includes('codex') ? 'codex' : null;
|
|
138
140
|
let steering = null;
|
|
139
|
-
if (
|
|
140
|
-
steering = seedSteering(consumerCwd, { dryRun });
|
|
141
|
+
if (steeringTarget) {
|
|
142
|
+
steering = seedSteering(consumerCwd, { dryRun, target: steeringTarget });
|
|
141
143
|
if (!dryRun) {
|
|
142
144
|
process.stderr.write(`info: dev-agents-sync: ${steeringSummary(steering)}\n`);
|
|
143
145
|
}
|
|
@@ -278,6 +280,8 @@ export async function runUpdate(consumerCwd, opts = {}) {
|
|
|
278
280
|
// unless --no-hooks-wiring. Additive + idempotent; fails loud (ESETTINGS_PARSE)
|
|
279
281
|
// on a malformed settings.json, leaving it byte-identical.
|
|
280
282
|
const installedHookKeys = plannedWrites.map(({ relKey }) => relKey);
|
|
283
|
+
const codexNotice = codexHooksSummary(installedHookKeys);
|
|
284
|
+
if (codexNotice) process.stderr.write(`info: dev-agents-sync: ${codexNotice}\n`);
|
|
281
285
|
const hookPaths = collectHookPaths(installedHookKeys);
|
|
282
286
|
const hookRegistrations = hookScriptsForInstalled(installedHookKeys);
|
|
283
287
|
// Same pairing as init: install line, then any retired-prune line emitted by
|
package/src/hooks.mjs
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import { DEFAULT_HOOK_SCRIPTS, mergeHookSettings } from './settings.mjs';
|
|
13
13
|
|
|
14
|
+
/** Report installed Codex configuration without changing the user's trust. */
|
|
15
|
+
export function codexHooksSummary(relKeys) {
|
|
16
|
+
return relKeys.includes('codex/.codex/hooks.json')
|
|
17
|
+
? 'Codex hooks installed at .codex/hooks.json — review and trust with /hooks in Codex; Windows requires Git Bash'
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* True for a FileMap key that is a managed hook script (claude/hooks/*.sh).
|
|
16
23
|
*
|
package/src/steering.mjs
CHANGED
|
@@ -69,14 +69,19 @@ class SteeringWriteError extends Error {
|
|
|
69
69
|
* each scaffold file only when it is absent.
|
|
70
70
|
*
|
|
71
71
|
* @param {string} consumerCwd
|
|
72
|
-
* @param {{ dryRun?: boolean }} [opts]
|
|
72
|
+
* @param {{ dryRun?: boolean, target?: 'claude' | 'codex' }} [opts]
|
|
73
73
|
* @returns {{ seeded: string[], present: string[], dryRun: boolean }}
|
|
74
74
|
* seeded — relative paths (steering/<name>) that were (or would be) written.
|
|
75
75
|
* present — relative paths left untouched because they already existed.
|
|
76
76
|
*/
|
|
77
77
|
export function seedSteering(consumerCwd, opts = {}) {
|
|
78
|
-
const { dryRun = false } = opts;
|
|
79
|
-
|
|
78
|
+
const { dryRun = false, target = 'claude' } = opts;
|
|
79
|
+
// Reuse Claude history when switching runtimes. A fresh Codex-only install
|
|
80
|
+
// uses the top-level layout already supported by every workflow and hook.
|
|
81
|
+
const claudeSteering = path.join(consumerCwd, '.claude', 'steering');
|
|
82
|
+
const steeringDir = target === 'codex' && !existsSync(claudeSteering)
|
|
83
|
+
? path.join(consumerCwd, 'steering')
|
|
84
|
+
: claudeSteering;
|
|
80
85
|
|
|
81
86
|
const seeded = [];
|
|
82
87
|
const present = [];
|
|
@@ -112,7 +117,7 @@ export function seedSteering(consumerCwd, opts = {}) {
|
|
|
112
117
|
}
|
|
113
118
|
} catch (err) {
|
|
114
119
|
throw new SteeringWriteError(
|
|
115
|
-
`ESTEER_WRITE: could not write ${path.
|
|
120
|
+
`ESTEER_WRITE: could not write ${path.relative(consumerCwd, steeringDir)} — check directory permissions`,
|
|
116
121
|
{ cause: err },
|
|
117
122
|
);
|
|
118
123
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runInit } from '../src/commands/init.mjs';
|
|
7
|
+
import { runUpdate } from '../src/commands/update.mjs';
|
|
8
|
+
import { codexHooksSummary } from '../src/hooks.mjs';
|
|
9
|
+
|
|
10
|
+
const opts = {
|
|
11
|
+
targets: ['codex'], availableTags: ['v1.0.0'],
|
|
12
|
+
fetcher: async () => ({ 'codex/AGENTS.md': '<!-- managed-by: dev-agents-sync v1.0.0 -->\n# Agents\n' }),
|
|
13
|
+
};
|
|
14
|
+
async function workspace(run) {
|
|
15
|
+
const dir = mkdtempSync(path.join(tmpdir(), 'codex-steering-'));
|
|
16
|
+
try { await run(dir); } finally { rmSync(dir, { recursive: true, force: true }); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('Codex-only steering adoption', () => {
|
|
20
|
+
it('reports hook trust only when a Codex hook manifest was installed', () => {
|
|
21
|
+
assert.match(codexHooksSummary(['codex/.codex/hooks.json']), /review and trust with \/hooks/);
|
|
22
|
+
assert.equal(codexHooksSummary(['claude/hooks/spec-approval-gate.sh']), null);
|
|
23
|
+
});
|
|
24
|
+
it('seeds the shared layout without creating .claude and preserves user history on update', () => workspace(async dir => {
|
|
25
|
+
await runInit(dir, opts);
|
|
26
|
+
const file = path.join(dir, 'steering', 'decisions.md');
|
|
27
|
+
assert.ok(existsSync(file));
|
|
28
|
+
assert.ok(!existsSync(path.join(dir, '.claude')));
|
|
29
|
+
writeFileSync(file, '# Decisions\nKeep this decision\n');
|
|
30
|
+
const result = await runUpdate(dir, opts);
|
|
31
|
+
assert.equal(result.upToDate, true);
|
|
32
|
+
assert.equal(readFileSync(file, 'utf8'), '# Decisions\nKeep this decision\n');
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
it('reuses existing Claude steering without splitting its history', () => workspace(async dir => {
|
|
36
|
+
mkdirSync(path.join(dir, '.claude', 'steering'), { recursive: true });
|
|
37
|
+
const file = path.join(dir, '.claude', 'steering', 'decisions.md');
|
|
38
|
+
writeFileSync(file, '# Decisions\nPrevious Claude decision\n');
|
|
39
|
+
await runInit(dir, opts);
|
|
40
|
+
assert.equal(readFileSync(file, 'utf8'), '# Decisions\nPrevious Claude decision\n');
|
|
41
|
+
assert.ok(existsSync(path.join(dir, '.claude', 'steering', 'delivery-log.md')));
|
|
42
|
+
assert.ok(!existsSync(path.join(dir, 'steering')));
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
it('seeds missing Codex steering during an already-current update', () => workspace(async dir => {
|
|
46
|
+
await runInit(dir, opts);
|
|
47
|
+
rmSync(path.join(dir, 'steering'), { recursive: true });
|
|
48
|
+
await runUpdate(dir, opts);
|
|
49
|
+
assert.ok(existsSync(path.join(dir, 'steering', 'delivery-log.md')));
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
it('does not write steering or runtime files under dry-run', () => workspace(async dir => {
|
|
53
|
+
const result = await runInit(dir, { ...opts, dryRun: true });
|
|
54
|
+
assert.equal(result.steering.seeded.length, 4);
|
|
55
|
+
assert.ok(!existsSync(path.join(dir, 'steering')));
|
|
56
|
+
assert.ok(!existsSync(path.join(dir, 'AGENTS.md')));
|
|
57
|
+
await runInit(dir, opts);
|
|
58
|
+
rmSync(path.join(dir, 'steering'), { recursive: true });
|
|
59
|
+
await runUpdate(dir, { ...opts, dryRun: true });
|
|
60
|
+
assert.ok(!existsSync(path.join(dir, 'steering')));
|
|
61
|
+
}));
|
|
62
|
+
});
|