@dalzoubi/dev-agents-sync 2.0.24 → 2.0.26

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
@@ -51,9 +51,9 @@ npx --yes @dalzoubi/dev-agents-sync@1 init --targets codex
51
51
 
52
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`.
53
53
 
54
- **Output shape.** `init --targets codex` writes six directory-per-skill files — one per agent — at `.agents/skills/<agent>/SKILL.md` (`analyze`, `define`, `implement`, `supervise`, `test`, `validate`), 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` — 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.
55
55
 
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 six agent workflows.
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
  **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.
59
59
 
@@ -109,12 +109,11 @@ The CLI seeds a blank steering scaffold under `.claude/steering/` — `decisions
109
109
 
110
110
  ### Hook scripts (managed)
111
111
 
112
- `init` and `update` install four hook scripts into `.claude/hooks/` as marker-managed, lockfile-tracked, executable files:
112
+ `init` and `update` install three hook scripts into `.claude/hooks/` as marker-managed, lockfile-tracked, executable files:
113
113
 
114
114
  | Script | Claude Code event | Purpose |
115
115
  |---|---|---|
116
116
  | `steering-injector.sh` | `SessionStart` | Surfaces steering context at the start of a session. |
117
- | `rules-injector.sh` | `SessionStart` | Injects the always-on shared rule bodies (`structured-choices`, `tone`) from `.claude/rules/` into every session; fail-safe (skips absent rules, exits 0). |
118
117
  | `managed-file-guard.sh` | `PostToolUse` | Guards managed files against unexpected edits. |
119
118
  | `uncommitted-check.sh` | `Stop` | Flags uncommitted work when a session ends. |
120
119
 
@@ -122,11 +121,12 @@ Hook scripts use the shell-comment marker form `# managed-by: dev-agents-sync vX
122
121
 
123
122
  ### settings.json auto-wiring
124
123
 
125
- By default, `init` and `update` also register the four hooks in `.claude/settings.json` so Claude Code runs them. The merge is:
124
+ By default, `init` and `update` also register the three hooks in `.claude/settings.json` so Claude Code runs them. The merge is:
126
125
 
127
126
  - **Additive** — every existing setting and every consumer-authored hook is preserved untouched.
128
127
  - **Idempotent** — running `update` twice produces no duplicate entries.
129
128
  - **Tagged** — each injected entry is marked as CLI-managed so it can be identified and removed later without disturbing consumer-authored hooks.
129
+ - **Self-cleaning** — when the toolkit retires a hook, `update` also prunes the CLI-managed registration for it, so no consumer is left with an entry pointing at a script that no longer ships. Only tagged entries are pruned, and only the retired command within them: a consumer-authored entry for the same command is left untouched, and a command you added yourself inside a tagged entry survives alongside. The prune is reported on stderr (`info: dev-agents-sync: pruned N retired hook registration(s)`, counting registrations removed) and is computed — and surfaced in the `--dry-run` result — without writing anything. `rules-injector.sh` is the first retired hook — Claude Code now loads `.claude/rules/*.md` natively, so the hook is redundant.
130
130
 
131
131
  > **Carve-out.** `settings.json` is a consumer-owned configuration file, **not** a marker-managed file. The CLI never replaces it wholesale; it only merges in its own tagged hook registrations and otherwise leaves the file alone. If `settings.json` contains malformed JSON, `update` fails loud with the stable code `ESETTINGS_PARSE` and leaves the file byte-identical — it will never half-write or clobber an unparseable file.
132
132
 
@@ -136,39 +136,49 @@ To install the hook scripts but skip the `settings.json` merge entirely, pass `-
136
136
  npx --yes @dalzoubi/dev-agents-sync@1 update --no-hooks-wiring
137
137
  ```
138
138
 
139
- With `--no-hooks-wiring`, the hook scripts are still installed under `.claude/hooks/`, but `settings.json` is neither created nor modified — wire the hooks yourself if you want them active.
139
+ With `--no-hooks-wiring`, the hook scripts are still installed under `.claude/hooks/`, but `settings.json` is neither created nor modified — wire the hooks yourself if you want them active. Note that the flag skips the whole merge, **including the self-cleaning de-registration above**: a registration for a retired hook stays in your `settings.json` until you run without the flag or remove it yourself.
140
140
 
141
141
  ## Agents
142
142
 
143
- Syncing installs six agents that form a spec-driven workflow:
144
- **Define → Analyze → Implement / Test / Validate**, or **Supervise** to run the
145
- loop autonomously. Each agent has a matching slash command (for example
146
- `/define`), and every agent reads the project's `CLAUDE.md` / `AGENTS.md` at the
147
- start of a session.
143
+ Syncing installs the `define`/`deliver` agent topology: two user-facing
144
+ orchestrators plus their namespaced workers (12 agents in total). Each agent has
145
+ a matching slash command (for example `/define`), and every agent reads the
146
+ project's `CLAUDE.md` / `AGENTS.md` at the start of a session.
147
+
148
+ **`define` namespace — spec authoring:**
148
149
 
149
150
  | Agent | Command | Purpose |
150
151
  |---|---|---|
151
- | `define` | `/define` | Planning agent for spec-driven development. Runs a one-question-at-a-time Q&A to turn a vague ask into a spec, then writes it to the project's spec directory. Read-only on source code; produces spec markdown only and auto-runs Analyze when done. |
152
- | `analyze` | `/analyze` | Pre-implementation spec checker. Scans for unresolved `[NEEDS CLARIFICATION]` markers, checks structure and estimates, and emits a `PASS` or `BLOCKED` verdict. Read-only; reports only. |
153
- | `implement` | `/implement` | Build-and-refactor agent. Builds features from an approved spec, refactors, debugs, and runs fix-build-test loops. The only agent that changes source files; pauses at checkpoints and won't commit unless asked. |
154
- | `test` | `/test` | Test and quality agent. Writes unit/integration/regression tests, reproduces bugs with a failing test first, and adds edge cases. Edits **test files only** — never production code. |
155
- | `validate` | `/validate` | Review and risk agent. Audits the current branch before a PR for security, regressions, accessibility, performance, spec conformance, and coverage gaps. Read-only; outputs a severity-ranked report. |
156
- | `supervise` | `/supervise <spec-path> [--auto] [--verbose]` | Autonomous TDD orchestrator. Drives test → implement → validate across an approved spec's slices, one at a time, with per-slice cycle budgets and recurrence detection. Delegates to the other agents; pauses on recurring failures, budget exhaustion, or interrupt. |
152
+ | `define` | `/define` | Q&A orchestrator. Runs a one-question-at-a-time Q&A to turn a vague ask into spec/plan/tasks artifacts, then auto-runs `define-analyze`. Read-only on source code. |
153
+ | `define-spec` | `/define-spec` | Writes `spec.md` (requirements). |
154
+ | `define-plan` | `/define-plan` | Writes `plan.md` (technical plan + authoritative working branch). |
155
+ | `define-tasks` | `/define-tasks` | Writes `tasks.md` (slices + estimates). |
156
+ | `define-analyze` | `/define-analyze` | Readiness gate. Scans for unresolved `[NEEDS CLARIFICATION]` markers, checks structure and estimates, and emits a `PASS` or `BLOCKED` verdict. Read-only; reports only. |
157
+
158
+ **`deliver` namespace — autonomous TDD delivery:**
157
159
 
158
- A bonus command, `/preflight`, runs a pre-review checklist (lint, unit tests,
159
- and build if relevant) and summarizes what changed. It does not push or open a PR.
160
+ | Agent | Command | Purpose |
161
+ |---|---|---|
162
+ | `deliver` | `/deliver <spec-path> [--auto] [--verbose] [--max-parallel=N]` | Autonomous TDD orchestrator. Drives the four-step cycle `deliver-test → deliver-solution → deliver-refactor → deliver-validate` across an approved spec's slices, with per-slice cycle budgets and recurrence detection; finalizes with `deliver-preflight`. Pauses on recurring failures, budget exhaustion, or interrupt. |
163
+ | `deliver-solution` | `/deliver-solution` | Implementation and debugging worker. Builds features from an approved spec, debugs, and runs fix-build-test loops. Pauses at checkpoints and won't commit unless asked. |
164
+ | `deliver-test` | `/deliver-test` | Test and quality worker. Writes failing-first unit/integration/regression tests and adds edge cases. Edits **test files only** — never production code. |
165
+ | `deliver-refactor` | `/deliver-refactor` | Behavior-preserving refactor worker. Improves production-source structure after tests are green; no new features, never test files. |
166
+ | `deliver-validate` | `/deliver-validate` | Review and risk worker. Audits the current branch before a PR for security, regressions, accessibility, performance, spec conformance, and coverage gaps. Read-only; outputs a severity-ranked report. |
167
+ | `deliver-preflight` | `/deliver-preflight` | Pre-review gate. Runs lint, unit tests, and build if relevant, then summarizes what changed. Does not push or open a PR. |
168
+ | `deliver-document` | `/deliver-document` | End-of-feature docs/CHANGELOG finalization; flips the spec's Status to Shipped. |
160
169
 
161
170
  ### When to use which
162
171
 
163
172
  | You want to… | Use |
164
173
  |---|---|
165
174
  | Scope a new feature or change before writing code | `/define` |
166
- | Confirm a spec is ready to build from | `/analyze` |
167
- | Build from an approved spec, refactor, or debug | `/implement` |
168
- | Add test coverage or pin a bug with a failing test | `/test` |
169
- | Audit risk right before opening a PR | `/validate` |
170
- | Build an approved spec largely hands-off, slice by slice | `/supervise` |
171
- | Run lint/tests/build before review | `/preflight` |
175
+ | Confirm a spec is ready to build from | `/define-analyze` |
176
+ | Build from an approved spec or debug | `/deliver-solution` |
177
+ | Add test coverage or pin a bug with a failing test | `/deliver-test` |
178
+ | Clean up structure once the scoped tests are green | `/deliver-refactor` |
179
+ | Audit risk right before opening a PR | `/deliver-validate` |
180
+ | Build an approved spec largely hands-off, slice by slice | `/deliver` |
181
+ | Run lint/tests/build before review | `/deliver-preflight` |
172
182
 
173
183
  ## Common Flags
174
184
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalzoubi/dev-agents-sync",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
4
4
  "type": "module",
5
5
  "description": "CLI that syncs managed dev-agent prompts into consumer repos (.claude/ and/or .cursor/).",
6
6
  "license": "UNLICENSED",
@@ -17,8 +17,7 @@ import { resolveConsumerPath, writeManagedFile, normalizeFileMap } from '../writ
17
17
  import { hasMarker, hasMarkerOnFirstLine } from '../marker.mjs';
18
18
  import { injectClaudeMd } from '../claudeMd.mjs';
19
19
  import { seedSteering, steeringSummary } from '../steering.mjs';
20
- import { mergeHookSettings } from '../settings.mjs';
21
- import { collectHookPaths, hookScriptsForInstalled } from '../hooks.mjs';
20
+ import { collectHookPaths, hookScriptsForInstalled, wireHookSettings } from '../hooks.mjs';
22
21
 
23
22
  const _require = createRequire(import.meta.url);
24
23
  const CLI_VERSION = _require('../../package.json').version;
@@ -134,6 +133,7 @@ export async function runInit(consumerCwd, opts = {}) {
134
133
  const claudeMdPath = path.join(consumerCwd, 'CLAUDE.md');
135
134
 
136
135
  if (dryRun) {
136
+ const plannedKeys = plannedWrites.map(({ relKey }) => relKey);
137
137
  // Run inject in dry-run mode to compute the planned change.
138
138
  const claudeMdResult = routingContent
139
139
  ? injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent, { dryRun: true })
@@ -144,13 +144,24 @@ export async function runInit(consumerCwd, opts = {}) {
144
144
  ? seedSteering(consumerCwd, { dryRun: true })
145
145
  : null;
146
146
 
147
+ // Compute the planned settings.json merge too, mirroring update's dry-run
148
+ // branch: skipping it would under-report a real, intended change to
149
+ // consumer-owned state — and `init` over an existing .claude/ can meet a
150
+ // pre-retirement settings.json (same exposure the wiring call below cites).
151
+ // The merge writes nothing in dry-run, so settings.json stays byte-identical.
152
+ const plannedPrunedRetired =
153
+ claudeTargetActive && !noHooksWiring
154
+ ? wireHookSettings(consumerCwd, hookScriptsForInstalled(plannedKeys), { dryRun: true })
155
+ : [];
156
+
147
157
  return {
148
158
  dryRun: true,
149
159
  resolvedVersion,
150
160
  targets: resolvedTargets,
151
161
  plannedWrites: plannedWrites.map(({ relKey, absPath }) => ({ relKey, absPath })),
152
- files: plannedWrites.map(({ relKey }) => relKey),
162
+ files: plannedKeys,
153
163
  claudeMd: claudeMdResult,
164
+ prunedRetired: plannedPrunedRetired,
154
165
  steering,
155
166
  };
156
167
  }
@@ -203,11 +214,11 @@ export async function runInit(consumerCwd, opts = {}) {
203
214
  // additive + idempotent and fails loud (ESETTINGS_PARSE) on a malformed file.
204
215
  const installedHookKeys = plannedWrites.map(({ relKey }) => relKey);
205
216
  const hookPaths = collectHookPaths(installedHookKeys);
217
+ let prunedRetired = [];
206
218
  if (claudeTargetActive && !noHooksWiring) {
207
- const hookSpec = hookScriptsForInstalled(installedHookKeys);
208
- if (hookSpec.length > 0) {
209
- mergeHookSettings(consumerCwd, hookSpec);
210
- }
219
+ // Unconditional on the spec being non-empty: the prune half needs no spec,
220
+ // and `init` over an existing .claude/ can meet a pre-retirement settings.json.
221
+ prunedRetired = wireHookSettings(consumerCwd, hookScriptsForInstalled(installedHookKeys));
211
222
  }
212
223
 
213
224
  writeLockfile(consumerCwd, {
@@ -225,6 +236,7 @@ export async function runInit(consumerCwd, opts = {}) {
225
236
  plannedWrites: plannedWrites.map(({ relKey, absPath }) => ({ relKey, absPath })),
226
237
  files: plannedWrites.map(({ relKey }) => relKey),
227
238
  hooks: hookPaths,
239
+ prunedRetired,
228
240
  steering,
229
241
  };
230
242
  }
@@ -15,8 +15,8 @@ import { resolveConsumerPath, writeManagedFile, normalizeFileMap } from '../writ
15
15
  import { hasMarker, hasMarkerOnFirstLine } from '../marker.mjs';
16
16
  import { injectClaudeMd } from '../claudeMd.mjs';
17
17
  import { seedSteering, steeringSummary } from '../steering.mjs';
18
- import { mergeHookSettings, DEFAULT_HOOK_SCRIPTS } from '../settings.mjs';
19
- import { collectHookPaths, hookScriptsForInstalled } from '../hooks.mjs';
18
+ import { DEFAULT_HOOK_SCRIPTS } from '../settings.mjs';
19
+ import { collectHookPaths, hookScriptsForInstalled, wireHookSettings } from '../hooks.mjs';
20
20
 
21
21
  const _require = createRequire(import.meta.url);
22
22
  const CLI_VERSION = _require('../../package.json').version;
@@ -144,19 +144,21 @@ export async function runUpdate(consumerCwd, opts = {}) {
144
144
  // here (the short-circuit's whole point is to avoid a fetch); instead we wire
145
145
  // only the hook scripts that already exist on disk. The merge is additive +
146
146
  // idempotent, so running twice == once. --dry-run still writes nothing.
147
- if (claudeTargetActive && !noHooksWiring && !dryRun) {
147
+ let prunedRetired = [];
148
+ if (claudeTargetActive && !noHooksWiring) {
148
149
  const hookSpec = DEFAULT_HOOK_SCRIPTS.filter((h) =>
149
150
  existsSync(path.join(consumerCwd, h.command)),
150
151
  );
151
- if (hookSpec.length > 0) {
152
- mergeHookSettings(consumerCwd, hookSpec);
153
- }
152
+ // Unconditional on hookSpec.length: the prune needs no spec, so a consumer
153
+ // with no hook scripts on disk still gets retired registrations removed.
154
+ prunedRetired = wireHookSettings(consumerCwd, hookSpec, { dryRun });
154
155
  }
155
156
 
156
157
  return {
157
158
  upToDate: true,
158
159
  resolvedVersion: lock.resolvedVersion,
159
160
  message: `already up to date at v${lock.resolvedVersion}`,
161
+ prunedRetired,
160
162
  steering,
161
163
  };
162
164
  }
@@ -185,18 +187,27 @@ export async function runUpdate(consumerCwd, opts = {}) {
185
187
  const orphans = computeOrphans(consumerCwd, scopedKeys, lock.targets);
186
188
 
187
189
  if (dryRun) {
190
+ const plannedKeys = plannedWrites.map(({ relKey }) => relKey);
188
191
  // Compute the planned CLAUDE.md change so callers can inspect it without
189
192
  // the result shape silently diverging from the init --dry-run contract.
190
193
  const claudeMdResult = routingContent
191
194
  ? injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent, { dryRun: true })
192
195
  : null;
196
+ // Compute the planned settings.json merge too — a dry run that skipped it
197
+ // would under-report a real, intended change to consumer-owned state. The
198
+ // merge writes nothing in dry-run, so settings.json stays byte-identical.
199
+ const plannedPrunedRetired =
200
+ claudeTargetActive && !noHooksWiring
201
+ ? wireHookSettings(consumerCwd, hookScriptsForInstalled(plannedKeys), { dryRun: true })
202
+ : [];
193
203
  return {
194
204
  dryRun: true,
195
205
  resolvedVersion: targetVersion,
196
206
  previousVersion: lock.resolvedVersion,
197
- files: plannedWrites.map(({ relKey }) => relKey),
207
+ files: plannedKeys,
198
208
  claudeMd: claudeMdResult,
199
209
  plannedPrune: orphans,
210
+ prunedRetired: plannedPrunedRetired,
200
211
  steering,
201
212
  };
202
213
  }
@@ -239,11 +250,10 @@ export async function runUpdate(consumerCwd, opts = {}) {
239
250
  // on a malformed settings.json, leaving it byte-identical.
240
251
  const installedHookKeys = plannedWrites.map(({ relKey }) => relKey);
241
252
  const hookPaths = collectHookPaths(installedHookKeys);
253
+ let prunedRetired = [];
242
254
  if (claudeTargetActive && !noHooksWiring) {
243
- const hookSpec = hookScriptsForInstalled(installedHookKeys);
244
- if (hookSpec.length > 0) {
245
- mergeHookSettings(consumerCwd, hookSpec);
246
- }
255
+ // Unconditional on the spec being non-empty: the prune half needs no spec.
256
+ prunedRetired = wireHookSettings(consumerCwd, hookScriptsForInstalled(installedHookKeys));
247
257
  }
248
258
 
249
259
  // Prune orphans: marker-bearing managed files no longer shipped. Delete in
@@ -293,6 +303,7 @@ export async function runUpdate(consumerCwd, opts = {}) {
293
303
  hooks: hookPaths,
294
304
  pruned,
295
305
  prunedCount: pruned.length,
306
+ prunedRetired,
296
307
  summary: pruneSummary,
297
308
  steering,
298
309
  };
package/src/hooks.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  * CLI only wires hooks that genuinely shipped in the resolved version.
10
10
  */
11
11
 
12
- import { DEFAULT_HOOK_SCRIPTS } from './settings.mjs';
12
+ import { DEFAULT_HOOK_SCRIPTS, mergeHookSettings } from './settings.mjs';
13
13
 
14
14
  /** True for a FileMap key that is a managed hook script (claude/hooks/*.sh). */
15
15
  function isHookKey(relKey) {
@@ -45,3 +45,40 @@ export function hookScriptsForInstalled(relKeys) {
45
45
  const installed = new Set(collectHookPaths(relKeys));
46
46
  return DEFAULT_HOOK_SCRIPTS.filter((h) => installed.has(h.command));
47
47
  }
48
+
49
+ /**
50
+ * Merges `hookScripts` into the consumer's .claude/settings.json and reports any
51
+ * retired registrations the merge de-registered. Shared by init and update so
52
+ * all three wiring call sites report identically.
53
+ *
54
+ * Called even when `hookScripts` is EMPTY: the prune half of the merge needs no
55
+ * spec, so a consumer with no hook scripts on disk still gets stale
56
+ * registrations for retired hooks cleaned up. With nothing to add and nothing to
57
+ * prune the merge is a no-op that writes nothing — it does not create the file.
58
+ *
59
+ * De-registration is a real edit to consumer-owned state, so it is never silent
60
+ * (§6): it emits the same `info: dev-agents-sync: ...` line as update's
61
+ * orphaned-file prune. Nothing is printed under --dry-run (planning is not
62
+ * doing) or when nothing was pruned (no spurious line on every run).
63
+ *
64
+ * The line reports `prunedRetiredCount` — REGISTRATIONS removed — not
65
+ * `prunedRetired.length`, which is the same list deduped by command. One retired
66
+ * command registered under two event buckets is two registrations gone from the
67
+ * consumer's file; saying "1" there would under-report the edit we just made.
68
+ *
69
+ * @returns {string[]} the DISTINCT retired commands de-registered (or, in
70
+ * dry-run, that would be de-registered) by this call. Always an array. Callers
71
+ * match on it ("was X pruned?"); the user-facing count stays inside this
72
+ * function, which is the toolkit's single reporting site for the prune.
73
+ */
74
+ export function wireHookSettings(consumerCwd, hookScripts, { dryRun = false } = {}) {
75
+ const { prunedRetired, prunedRetiredCount } = mergeHookSettings(consumerCwd, hookScripts, {
76
+ dryRun,
77
+ });
78
+ if (!dryRun && prunedRetiredCount > 0) {
79
+ process.stderr.write(
80
+ `info: dev-agents-sync: pruned ${prunedRetiredCount} retired hook registration(s)\n`,
81
+ );
82
+ }
83
+ return prunedRetired;
84
+ }