agentsys 5.8.3 → 5.8.5

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
@@ -9,6 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [5.8.5] - 2026-04-23
13
+
14
+ ### Fixed
15
+ - **Hardcoded developer paths in web-ctl skills** (#333) - replaced 76 occurrences of `/Users/avifen/.agentsys/plugins/web-ctl/scripts/web-ctl.js` with `~/.agentsys/...` across `.kiro/skills/web-auth/SKILL.md` (16 sites) and `.kiro/skills/web-browse/SKILL.md` (60 sites). The original absolute path only existed on the maintainer's machine, so every CLI example silently failed for any other user. The portable form matches the install path documented in `meta/skills/maintain-cross-platform/SKILL.md` and works for both shell copy-paste and agent execution (Bash tool's `bash -c` performs tilde expansion).
16
+ - **`prepare` lifecycle hook auto-installed git hooks on every `npm install`** (#334) - moved hook installation from npm's `prepare` script to an explicit `setup-hooks` script so consumers no longer get hooks injected as a side effect of `npm install`. Documented opt-in flow in `CONTRIBUTING.md`. Also removed the no-op pre-commit placeholder (it just wrote a comment file - lib/ sync is handled by agent-core CI now), so only the actually-active pre-push hook (preflight + `/enhance` reminder + release-tag validation) is installed.
17
+ - **`npm version` lifecycle dropped downstream version stamps** (#339, #342) - replaced `git add -A` (which would sweep unrelated working-tree changes into the version commit) with an explicit allowlist covering every file `stamp-version.js` writes plus npm's own lockfile and `CHANGELOG.md`: `package.json`, `package-lock.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `site/content.json`, `CHANGELOG.md`. Preserves the original intent (no working-tree sweep) while keeping all version manifests consistent after `npm version`. (CHANGELOG.md added per gemini-code-assist review on #342 - the developer manually edits CHANGELOG before each release, so it must be in the allowlist or `npm version`'s auto-commit drops the changelog entry.)
18
+
19
+ ### Changed
20
+ - **`js-yaml` dependency range tightened from `^4.1.1` to `~4.1.1`** (#335) - blocks unintended `4.x` minor bumps while still allowing `4.1.x` patch updates so runtime security fixes flow in automatically. Lockfile root entry synced to match.
21
+
22
+ ## [5.8.4] - 2026-04-20
23
+
24
+ ### Fixed
25
+ - **tasks.json atomic optimistic locking** (#331) - Concurrent `/next-task` and `/ship` runs could silently lose claims or leave stale registry entries due to unguarded read-modify-write on `tasks.json`. Fix uses `_version` + per-write `_writerId` optimistic locking (mirrors existing `flow.json` pattern): write atomically via rename, re-read and verify both fields match before declaring success, retry up to 5× with jitter on mismatch.
26
+ - **tasks.json schema unification** - `worktree-manager` wrote `{ version, tasks[] }` while `workflow-state.js` read `{ active }`, causing claim exclusion in `discover-tasks` to always return an empty set. Unified schema is `{ active, tasks[], _version, _writerId }` with on-read normalization of both legacy formats — no migration needed.
27
+ - **Silent corruption risk** - `readTasks()` now throws on corrupted JSON instead of returning a safe default, preventing `updateTasks` from silently overwriting potentially recoverable data.
28
+ - **Agent prompt raw file writes** - `worktree-manager` Phase 6 and Cleanup Reference replaced inline `fs.writeFileSync` with `workflowState.claimTask()` / `workflowState.releaseTask()` library calls that are atomic and retry-safe.
29
+
30
+ ### Added
31
+ - `updateTasks(mutatorFn)` - optimistic-lock loop for `tasks.json` mutations (mirrors `updateFlow`)
32
+ - `claimTask(entry, projectPath)` - atomic upsert into `tasks[]` registry for worktree-manager
33
+ - `releaseTask(taskId, projectPath)` - atomic removal from `tasks[]` registry for ship/abort; idempotent
34
+
12
35
  ## [5.8.3] - 2026-04-11
13
36
 
14
37
  ### Fixed
package/README.md CHANGED
@@ -19,8 +19,8 @@
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
- <b>19 plugins · 47 agents · 40 skills (across all repos) · 30k lines of lib code · 3,583 tests · 5 platforms</b><br>
23
- <em>Plugins distributed as standalone repos under <a href="https://github.com/agent-sh">agent-sh</a> org agentsys is the marketplace &amp; installer</em>
22
+ <b>19 plugins · 49 agents · 41 skills (across all repos) · 30k lines of lib code · 3,507 tests · 5 platforms</b><br>
23
+ <em>Plugins distributed as standalone repos under <a href="https://github.com/agent-sh">agent-sh</a> org - agentsys is the marketplace &amp; installer</em>
24
24
  </p>
25
25
 
26
26
  <p align="center">
@@ -38,14 +38,14 @@
38
38
 
39
39
  ---
40
40
 
41
- AI models can write code. That's not the hard part anymore. The hard part is everything around it task selection, branch management, code review, artifact cleanup, CI, PR comments, deployment. **AgentSys is the runtime that orchestrates agents to handle all of it** structured pipelines, gated phases, specialized agents, and persistent state that survives session boundaries.
41
+ AI models can write code. That's not the hard part anymore. The hard part is everything around it - task selection, branch management, code review, artifact cleanup, CI, PR comments, deployment. **AgentSys is the runtime that orchestrates agents to handle all of it** - structured pipelines, gated phases, specialized agents, and persistent state that survives session boundaries.
42
42
 
43
43
  ---
44
- > Building custom skills, agents, hooks, or MCP tools? [agnix](https://github.com/agent-sh/agnix) is the CLI + LSP linter that catches config errors before they fail silently - real-time IDE validation, auto suggestions, auto-fix, and 385 rules for Claude Code, Codex, OpenCode, Cursor, Kiro, Copilot, Gemini CLI, Cline, Windsurf, Roo Code, Amp, and more.
44
+ > Building custom skills, agents, hooks, or MCP tools? [agnix](https://github.com/agent-sh/agnix) is the CLI + LSP linter that catches config errors before they fail silently - real-time IDE validation, auto suggestions, auto-fix, and 399 rules for Claude Code, Codex, OpenCode, Cursor, Kiro, Copilot, Gemini CLI, Cline, Windsurf, Roo Code, Amp, and more.
45
45
 
46
46
  ## What This Is
47
47
 
48
- An agent orchestration system 19 plugins, 47 agents, and 40 skills that compose into structured pipelines for software development. Each plugin lives in its own standalone repo under the [agent-sh](https://github.com/agent-sh) org. agentsys is the marketplace and installer that ties them together.
48
+ An agent orchestration system - 19 plugins, 49 agents (39 file-based + 10 role-based specialists in audit-project), and 41 skills that compose into structured pipelines for software development. Each plugin lives in its own standalone repo under the [agent-sh](https://github.com/agent-sh) org. agentsys is the marketplace and installer that ties them together.
49
49
 
50
50
  Each agent has a single responsibility, a specific model assignment, and defined inputs/outputs. Pipelines enforce phase gates so agents can't skip steps. State persists across sessions so work survives interruptions.
51
51
 
@@ -57,8 +57,8 @@ The system runs on Claude Code, OpenCode, Codex CLI, Cursor, and Kiro. Install v
57
57
 
58
58
  **Code does code work. AI does AI work.**
59
59
 
60
- - **Detection**: regex, AST analysis, static analysisfast, deterministic, no tokens wasted
61
- - **Judgment**: LLM calls for synthesis, planning, reviewwhere reasoning matters
60
+ - **Detection**: regex, AST analysis, static analysis - fast, deterministic, no tokens wasted
61
+ - **Judgment**: LLM calls for synthesis, planning, review - where reasoning matters
62
62
  - **Result**: 77% fewer tokens for [/drift-detect](#drift-detect) vs multi-agent approaches, certainty-graded findings throughout
63
63
 
64
64
  **Certainty levels exist because not all findings are equal:**
@@ -118,7 +118,7 @@ The investment shifts from model spend to pipeline design. Better prompts, riche
118
118
  | [`/next-task`](#next-task) | Task workflow: discovery, implementation, PR, merge |
119
119
  | [`/prepare-delivery`](#prepare-delivery) | Pre-ship quality gates: deslop, review, validation, docs sync |
120
120
  | [`/gate-and-ship`](#gate-and-ship) | Quality gates then ship (/prepare-delivery + /ship) |
121
- | [`/agnix`](#agnix) | Lint agent configurations (385 rules) |
121
+ | [`/agnix`](#agnix) | Lint agent configurations (399 rules) |
122
122
  | [`/ship`](#ship) | PR creation, CI monitoring, merge |
123
123
  | [`/deslop`](#deslop) | Clean AI slop patterns |
124
124
  | [`/perf`](#perf) | Performance investigation with baselines and profiling |
@@ -142,7 +142,7 @@ Each command works standalone. Together, they compose into end-to-end pipelines.
142
142
 
143
143
  ## Skills
144
144
 
145
- 40 skills included across the plugins:
145
+ 41 skills included across the plugins:
146
146
 
147
147
  | Category | Skills |
148
148
  |----------|--------|
@@ -157,6 +157,7 @@ Each command works standalone. Together, they compose into end-to-end pipelines.
157
157
  | **Web** | `web-auth`, `web-browse` |
158
158
  | **Release** | `release` |
159
159
  | **Analysis** | `drift-analysis`, `repo-intel` |
160
+ | **Linting** | `agnix` |
160
161
 
161
162
  **External skill plugins** (standalone repos, installed separately):
162
163
 
@@ -175,7 +176,7 @@ Skills are the reusable implementation units. Agents invoke skills; commands orc
175
176
  | [The Approach](#the-approach) | Why it's built this way |
176
177
  | [Benchmarks](#benchmarks) | Sonnet + agentsys vs raw Opus |
177
178
  | [Commands](#commands) | All 20 commands overview |
178
- | [Skills](#skills) | 40 skills across plugins |
179
+ | [Skills](#skills) | 41 skills across plugins |
179
180
  | [Skill-Only Plugins](#skill-only-plugins) | glide-mq and other non-command plugins |
180
181
  | [Command Details](#command-details) | Deep dive into each command |
181
182
  | [How Commands Work Together](#how-commands-work-together) | Standalone vs integrated |
@@ -328,7 +329,7 @@ agnix catches these issues before they cause problems.
328
329
  | **Best Practices** | Tool restrictions, model selection, trigger phrase quality |
329
330
  | **Cross-Platform** | Compatibility across Claude Code, Codex, OpenCode, Cursor, Kiro, Copilot, Gemini CLI, Cline, Windsurf, Roo Code, Amp, and more |
330
331
 
331
- **385 validation rules** (102 auto-fixable) derived from:
332
+ **399 validation rules** (126 auto-fixable) derived from:
332
333
  - Official tool specifications (Claude Code, Codex CLI, OpenCode, Cursor, Kiro, GitHub Copilot, Gemini CLI, Cline, Windsurf, Roo Code, Amp, and more)
333
334
  - Research papers on agent reliability and prompt injection
334
335
  - Real-world testing across 500+ repositories
@@ -442,7 +443,7 @@ If something can't be fixed, the workflow replies explaining why and resolves th
442
443
 
443
444
  ### /deslop
444
445
 
445
- **Purpose:** Finds AI slopdebug statements, placeholder text, verbose comments, TODOsand removes it.
446
+ **Purpose:** Finds AI slop - debug statements, placeholder text, verbose comments, TODOs - and removes it.
446
447
 
447
448
  **How detection works:**
448
449
 
@@ -613,13 +614,14 @@ Findings are collected and categorized by severity (critical/high/medium/low). A
613
614
 
614
615
  **Purpose:** Analyzes your prompts, plugins, agents, docs, hooks, and skills for improvement opportunities.
615
616
 
616
- **Seven analyzers run in parallel:**
617
+ **Eight analyzers run in parallel:**
617
618
 
618
619
  | Analyzer | What it checks |
619
620
  |----------|----------------|
620
621
  | plugin-enhancer | Plugin structure, MCP tool definitions, security patterns |
621
622
  | agent-enhancer | Agent frontmatter, prompt quality |
622
623
  | claudemd-enhancer | CLAUDE.md/AGENTS.md structure, token efficiency |
624
+ | cross-file-enhancer | Cross-file consistency (tools vs frontmatter, duplicate rules, conflicts) |
623
625
  | docs-enhancer | Documentation readability, RAG optimization |
624
626
  | prompt-enhancer | Prompt engineering patterns, clarity, examples |
625
627
  | hooks-enhancer | Hook frontmatter, structure, safety |
@@ -656,7 +658,7 @@ Findings are collected and categorized by severity (critical/high/medium/low). A
656
658
  - AST symbol mapping: exports, functions, classes, imports
657
659
  - Project metadata and health metrics
658
660
 
659
- Output is cached at `{state-dir}/repo-intel.json` and `{state-dir}/repo-map.json`.
661
+ Output is cached at `{state-dir}/repo-intel.json` (external repo-intel plugin) and `{state-dir}/repo-map.json` (agentsys internal repo-map library). `{state-dir}` is `.claude/`, `.opencode/`, or `.codex/` depending on your platform.
660
662
 
661
663
  **Why it matters:**
662
664
 
@@ -678,7 +680,7 @@ Backed by [agent-analyzer](https://github.com/agent-sh/agent-analyzer) Rust bina
678
680
 
679
681
  ### /sync-docs
680
682
 
681
- **Purpose:** Sync documentation with actual code changesfind outdated refs, update CHANGELOG, flag stale examples.
683
+ **Purpose:** Sync documentation with actual code changes - find outdated refs, update CHANGELOG, flag stale examples.
682
684
 
683
685
  **The problem it solves:**
684
686
 
@@ -958,7 +960,7 @@ No per-turn overhead - it reads transcripts that Claude Code already saves.
958
960
  **What happens when you run it:**
959
961
 
960
962
  1. **Collect** (68ms median) - Pure JavaScript scans manifest, structure, README, CI, git info. Normal depth adds CLAUDE.md/AGENTS.md and repo-intel. No LLM tokens.
961
- 2. **Synthesize** - Opus agent produces a structured overview: tech stack, key files, active areas, conventions
963
+ 2. **Synthesize** - Sonnet agent produces a structured overview: tech stack, key files, active areas, conventions
962
964
  3. **Guide** - Interactive Q&A: ask about specific files, areas, or patterns
963
965
 
964
966
  **74% fewer tokens** than manual onboarding. Validated on 100 repos across JS/TS, Rust, Go, Python, C/C++, Java, and Deno.
@@ -981,7 +983,7 @@ No per-turn overhead - it reads transcripts that Claude Code already saves.
981
983
  /onboard --depth=deep # Include AST data
982
984
  ```
983
985
 
984
- **Agent:** onboard-agent (opus model)
986
+ **Agent:** onboard-agent (sonnet model)
985
987
 
986
988
  [Full documentation →](https://github.com/agent-sh/onboard)
987
989
 
@@ -994,7 +996,7 @@ No per-turn overhead - it reads transcripts that Claude Code already saves.
994
996
  **What happens when you run it:**
995
997
 
996
998
  1. **Collect** - Gathers project data + contributor signals (test gaps, doc drift, bugspots, good-first areas, open issues). Validated on 100 repos.
997
- 2. **Match** - Opus agent asks about developer background and matches skills to project needs
999
+ 2. **Match** - Sonnet agent asks about developer background and matches skills to project needs
998
1000
  3. **Guide** - For each recommendation: reads code, explains what needs doing, gives a concrete first step
999
1001
 
1000
1002
  **Matching:**
@@ -1015,7 +1017,7 @@ No per-turn overhead - it reads transcripts that Claude Code already saves.
1015
1017
  /can-i-help --depth=deep # Include AST data
1016
1018
  ```
1017
1019
 
1018
- **Agent:** can-i-help-agent (opus model)
1020
+ **Agent:** can-i-help-agent (sonnet model)
1019
1021
 
1020
1022
  [Full documentation →](https://github.com/agent-sh/can-i-help)
1021
1023
 
@@ -1092,7 +1094,7 @@ Same principle as good code: single responsibility. The exploration-agent explor
1092
1094
 
1093
1095
  **2. Pipeline with gates, not a monolith**
1094
1096
 
1095
- Same principle as DevOps. Each step must pass before the next begins. Can't push before review. Can't merge before CI passes. Hooks enforce thisagents literally cannot skip phases.
1097
+ Same principle as DevOps. Each step must pass before the next begins. Can't push before review. Can't merge before CI passes. Hooks enforce this - agents literally cannot skip phases.
1096
1098
 
1097
1099
  **3. Tools do tool work, agents do agent work**
1098
1100
 
@@ -1235,7 +1237,7 @@ The system is built on research, not guesswork.
1235
1237
  - Instruction following reliability
1236
1238
 
1237
1239
  **Testing:**
1238
- - 3,583 tests passing
1240
+ - 3,507 tests passing
1239
1241
  - Drift-detect validated on 1,000+ repositories
1240
1242
  - E2E workflow testing across all commands
1241
1243
  - Cross-platform validation (Claude Code, OpenCode, Codex CLI, Cursor, Kiro)
@@ -117,62 +117,248 @@ function getTasksPath(projectPath = process.cwd()) {
117
117
  }
118
118
 
119
119
  /**
120
- * Read tasks.json from main project
121
- * Returns { active: null } if file doesn't exist or is corrupted
122
- * Logs critical error on corruption to prevent silent data loss
120
+ * Read tasks.json from main project.
121
+ *
122
+ * Unified schema (v2):
123
+ * { active: null|Object, tasks: [], _version: number }
124
+ *
125
+ * - active: single active workflow entry (set by createFlow / cleared by completeWorkflow)
126
+ * - tasks: worktree claim registry (set by worktree-manager / cleared by ship or --abort)
127
+ * - _version: monotonic counter for optimistic locking (managed by writeTasks)
128
+ * - _writerId: per-write unique token used by updateTasks to detect concurrent wins
129
+ *
130
+ * Legacy formats are normalized on read — no migration script needed.
131
+ * Throws on corruption so callers can decide whether to abort or recover,
132
+ * rather than silently overwriting potentially recoverable data.
133
+ *
134
+ * @param {string} projectPath
135
+ * @returns {{ active: null|Object, tasks: Array, _version: number, _writerId?: string }}
136
+ * @throws {Error} If tasks.json exists but cannot be parsed
123
137
  */
124
138
  function readTasks(projectPath = process.cwd()) {
125
139
  const tasksPath = getTasksPath(projectPath);
126
140
  if (!fs.existsSync(tasksPath)) {
127
- return { active: null };
141
+ return { active: null, tasks: [], _version: 0 };
128
142
  }
143
+ const raw = fs.readFileSync(tasksPath, 'utf8');
144
+ let data;
129
145
  try {
130
- const data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
131
- // Normalize legacy format that may not have 'active' field
132
- if (!Object.prototype.hasOwnProperty.call(data, 'active')) {
133
- return { active: null };
134
- }
135
- return data;
146
+ data = JSON.parse(raw);
136
147
  } catch (e) {
137
- console.error(`[CRITICAL] Corrupted tasks.json at ${tasksPath}: ${e.message}`);
138
- return { active: null };
148
+ throw new Error(`[CRITICAL] Corrupted tasks.json at ${tasksPath}: ${e.message}. File must be repaired or deleted manually before writes are allowed.`);
139
149
  }
150
+ // Normalize: ensure every field exists (handles legacy { active } and legacy { version, tasks[] })
151
+ return {
152
+ active: Object.prototype.hasOwnProperty.call(data, 'active') ? data.active : null,
153
+ tasks: Array.isArray(data.tasks) ? data.tasks : [],
154
+ _version: typeof data._version === 'number' ? data._version : 0,
155
+ _writerId: typeof data._writerId === 'string' ? data._writerId : undefined
156
+ };
140
157
  }
141
158
 
142
159
  /**
143
- * Write tasks.json to main project
160
+ * Write tasks.json atomically.
161
+ * Increments _version and stamps a unique _writerId per write.
162
+ *
163
+ * Both fields are used by updateTasks to verify it was the winning writer:
164
+ * if two processes both read _version N, both write _version N+1 (last
165
+ * renameSync wins), the loser re-reads and finds a _writerId that does not
166
+ * match its own — it knows it lost and retries.
167
+ *
168
+ * @returns {string} The _writerId stamped into this write
144
169
  */
145
170
  function writeTasks(tasks, projectPath = process.cwd()) {
146
171
  ensureStateDir(projectPath);
172
+ const copy = structuredClone(tasks);
173
+ copy._version = (copy._version || 0) + 1;
174
+ copy._writerId = crypto.randomBytes(8).toString('hex');
147
175
  const tasksPath = getTasksPath(projectPath);
148
- writeJsonAtomic(tasksPath, tasks);
149
- return true;
176
+ writeJsonAtomic(tasksPath, copy);
177
+ return copy._writerId;
150
178
  }
151
179
 
152
180
  /**
153
- * Set active task in main project
181
+ * Apply a mutation to tasks.json with optimistic locking.
182
+ *
183
+ * Uses _version + _writerId to detect wins in concurrent-writer races:
184
+ * 1. Read current state, snapshot _version
185
+ * 2. Apply mutatorFn(clone) → new state; skip write if state unchanged
186
+ * 3. Stamp a unique writerId, write atomically (increments _version)
187
+ * 4. Re-read: if _version === initialVersion + 1 AND _writerId matches → we won
188
+ * 5. Otherwise another writer raced us → back off with jitter, retry
189
+ *
190
+ * @param {function(Object): Object} mutatorFn - Pure function that receives a
191
+ * deep clone of current tasks state and returns the desired new state.
192
+ * Must not have side effects; may be called multiple times on retry.
193
+ * @param {string} projectPath
194
+ * @returns {boolean} true on success, false after MAX_RETRIES exhausted or on corruption
195
+ */
196
+ function updateTasks(mutatorFn, projectPath = process.cwd()) {
197
+ const MAX_RETRIES = 5;
198
+
199
+ let current;
200
+ try {
201
+ current = readTasks(projectPath);
202
+ } catch (e) {
203
+ console.error(`[ERROR] updateTasks: cannot read tasks.json — ${e.message}`);
204
+ return false;
205
+ }
206
+
207
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
208
+ const initialVersion = current._version || 0;
209
+
210
+ let updated;
211
+ try {
212
+ updated = mutatorFn(structuredClone(current));
213
+ } catch (e) {
214
+ console.error(`[ERROR] updateTasks: mutatorFn threw on attempt ${attempt + 1}: ${e.message}`);
215
+ return false;
216
+ }
217
+
218
+ // Skip write if mutatorFn made no changes — avoids spurious version bumps
219
+ if (JSON.stringify(updated) === JSON.stringify(current)) {
220
+ return true;
221
+ }
222
+
223
+ // Carry forward the pre-write version so writeTasks increments it by exactly 1
224
+ updated._version = initialVersion;
225
+
226
+ const writerId = writeTasks(updated, projectPath);
227
+
228
+ // Verify we won: version must be exactly initialVersion + 1 AND writerId must match ours.
229
+ // If another process also wrote _version: initialVersion + 1, only one writerId survives.
230
+ let afterWrite;
231
+ try {
232
+ afterWrite = readTasks(projectPath);
233
+ } catch (e) {
234
+ console.error(`[ERROR] updateTasks: tasks.json corrupted after write on attempt ${attempt + 1}: ${e.message}`);
235
+ return false;
236
+ }
237
+
238
+ if (afterWrite._version === initialVersion + 1 && afterWrite._writerId === writerId) {
239
+ return true;
240
+ }
241
+
242
+ // Lost the race — retry from the current on-disk state
243
+ if (attempt < MAX_RETRIES - 1) {
244
+ const delay = Math.floor(Math.random() * 50) + 10;
245
+ sleepForRetry(delay);
246
+ try {
247
+ current = readTasks(projectPath);
248
+ } catch (e) {
249
+ console.error(`[ERROR] updateTasks: tasks.json corrupted during retry ${attempt + 1}: ${e.message}`);
250
+ return false;
251
+ }
252
+ }
253
+ }
254
+
255
+ const tasksPath = getTasksPath(projectPath);
256
+ let lastVersion = '(unreadable)';
257
+ try { lastVersion = readTasks(projectPath)._version; } catch {}
258
+
259
+ // Final fallback: if a concurrent writer happened to apply the exact same
260
+ // mutation (idempotent operations like releaseTask on an already-absent entry),
261
+ // treat the outcome as a success rather than reporting a spurious failure.
262
+ let latest;
263
+ try { latest = readTasks(projectPath); } catch {}
264
+ if (latest) {
265
+ // Re-run the mutator on what's on disk; if the result is identical to
266
+ // what's already there, our desired state is already achieved.
267
+ try {
268
+ const wouldBe = mutatorFn(structuredClone(latest));
269
+ // Normalize _version/_writerId before comparing content
270
+ wouldBe._version = latest._version;
271
+ wouldBe._writerId = latest._writerId;
272
+ if (JSON.stringify(wouldBe) === JSON.stringify(latest)) {
273
+ return true;
274
+ }
275
+ } catch {}
276
+ }
277
+
278
+ console.error(
279
+ `[ERROR] updateTasks: all ${MAX_RETRIES} attempts failed due to concurrent writers on ${tasksPath}. ` +
280
+ `Another agent process is modifying the registry simultaneously. ` +
281
+ `Last known _version: ${lastVersion}. ` +
282
+ `Suggested recovery: wait for the competing process to finish, then retry the operation.`
283
+ );
284
+ return false;
285
+ }
286
+
287
+ /**
288
+ * Set active task in main project (uses optimistic locking)
154
289
  */
155
290
  function setActiveTask(task, projectPath = process.cwd()) {
156
- const tasks = readTasks(projectPath);
157
- tasks.active = {
158
- ...task,
159
- startedAt: new Date().toISOString()
160
- };
161
- return writeTasks(tasks, projectPath);
291
+ return updateTasks(tasks => {
292
+ tasks.active = { ...task, startedAt: new Date().toISOString() };
293
+ return tasks;
294
+ }, projectPath);
162
295
  }
163
296
 
164
297
  /**
165
- * Clear active task
298
+ * Clear active task (uses optimistic locking)
166
299
  */
167
300
  function clearActiveTask(projectPath = process.cwd()) {
168
- const tasks = readTasks(projectPath);
169
- tasks.active = null;
170
- return writeTasks(tasks, projectPath);
301
+ return updateTasks(tasks => {
302
+ tasks.active = null;
303
+ return tasks;
304
+ }, projectPath);
305
+ }
306
+
307
+ /**
308
+ * Claim a task in the registry (uses optimistic locking).
309
+ * Used by worktree-manager; replaces the raw fs.writeFileSync inline in agent prompts.
310
+ *
311
+ * @param {Object} entry - { id, source, title, branch, worktreePath, claimedBy }
312
+ * @param {string} projectPath
313
+ */
314
+ function claimTask(entry, projectPath = process.cwd()) {
315
+ if (!entry || !entry.id) {
316
+ console.error('[ERROR] claimTask: entry.id is required');
317
+ return false;
318
+ }
319
+ return updateTasks(tasks => {
320
+ const idx = tasks.tasks.findIndex(t => t.id === entry.id);
321
+ const record = {
322
+ ...entry,
323
+ status: 'claimed',
324
+ claimedAt: entry.claimedAt || new Date().toISOString(),
325
+ lastActivityAt: new Date().toISOString()
326
+ };
327
+ if (idx >= 0) {
328
+ tasks.tasks[idx] = record;
329
+ } else {
330
+ tasks.tasks.push(record);
331
+ }
332
+ return tasks;
333
+ }, projectPath);
334
+ }
335
+
336
+ /**
337
+ * Release a claimed task from the registry (uses optimistic locking).
338
+ * Used by ship and --abort; replaces the raw fs.writeFileSync inline cleanup.
339
+ *
340
+ * @param {string} taskId
341
+ * @param {string} projectPath
342
+ */
343
+ function releaseTask(taskId, projectPath = process.cwd()) {
344
+ if (!taskId) {
345
+ console.error('[ERROR] releaseTask: taskId is required');
346
+ return false;
347
+ }
348
+ return updateTasks(tasks => {
349
+ const before = tasks.tasks.length;
350
+ tasks.tasks = tasks.tasks.filter(t => t.id !== taskId);
351
+ if (tasks.tasks.length === before) {
352
+ // Not found — that's fine, idempotent
353
+ console.error(`[WARN] releaseTask: task ${taskId} was not found in tasks.json registry. It may have already been released or never claimed.`);
354
+ }
355
+ return tasks;
356
+ }, projectPath);
171
357
  }
172
358
 
173
359
  /**
174
- * Check if there's an active task
175
- * Uses != null to catch both null and undefined (legacy format safety)
360
+ * Check if there's an active task.
361
+ * Uses != null to catch both null and undefined (legacy format safety).
176
362
  */
177
363
  function hasActiveTask(projectPath = process.cwd()) {
178
364
  const tasks = readTasks(projectPath);
@@ -548,8 +734,11 @@ module.exports = {
548
734
  getTasksPath,
549
735
  readTasks,
550
736
  writeTasks,
737
+ updateTasks,
551
738
  setActiveTask,
552
739
  clearActiveTask,
740
+ claimTask,
741
+ releaseTask,
553
742
  hasActiveTask,
554
743
 
555
744
  // Flow (worktree)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsys",
3
- "version": "5.8.3",
3
+ "version": "5.8.5",
4
4
  "description": "A modular runtime and orchestration system for AI agents - works with Claude Code, OpenCode, and Codex CLI",
5
5
  "main": "lib/platform/detect-platform.js",
6
6
  "type": "commonjs",
@@ -37,8 +37,8 @@
37
37
  "bump": "node bin/dev-cli.js bump",
38
38
  "detect": "node bin/dev-cli.js detect",
39
39
  "verify": "node bin/dev-cli.js verify",
40
- "version": "node scripts/stamp-version.js && git add -A",
41
- "prepare": "node bin/dev-cli.js setup-hooks"
40
+ "version": "node scripts/stamp-version.js && git add package.json package-lock.json .claude-plugin/plugin.json .claude-plugin/marketplace.json site/content.json CHANGELOG.md",
41
+ "setup-hooks": "node bin/dev-cli.js setup-hooks"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "agentsys": "^5.0.0",
85
- "js-yaml": "^4.1.1"
85
+ "js-yaml": "~4.1.1"
86
86
  },
87
87
  "devDependencies": {
88
88
  "jest": "^29.7.0"
@@ -109,7 +109,8 @@ const STATIC_SKILLS = [
109
109
  { plugin: 'audit-project', name: 'audit-project' },
110
110
  { plugin: 'glidemq', name: 'glide-mq' },
111
111
  { plugin: 'glidemq', name: 'glide-mq-migrate-bullmq' },
112
- { plugin: 'glidemq', name: 'glide-mq-migrate-bee' }
112
+ { plugin: 'glidemq', name: 'glide-mq-migrate-bee' },
113
+ { plugin: 'agnix', name: 'agnix' }
113
114
  ];
114
115
 
115
116
  // Purpose mapping for architecture table
@@ -192,7 +193,7 @@ function generateCommandsTable(commands) {
192
193
  'next-task': 'Task workflow: discovery, implementation, PR, merge',
193
194
  'prepare-delivery': 'Pre-ship quality gates: deslop, review, validation, docs sync',
194
195
  'gate-and-ship': 'Quality gates then ship (/prepare-delivery + /ship)',
195
- 'agnix': 'Lint agent configurations (385 rules)',
196
+ 'agnix': 'Lint agent configurations (399 rules)',
196
197
  'ship': 'PR creation, CI monitoring, merge',
197
198
  'deslop': 'Clean AI slop patterns',
198
199
  'perf': 'Performance investigation with baselines and profiling',
@@ -395,9 +396,34 @@ function generateAgentCounts(agents, plugins) {
395
396
  /**
396
397
  * Update counts in site/content.json programmatically.
397
398
  */
398
- // Static counts for cross-repo plugins not discoverable locally
399
- const STATIC_PLUGIN_COUNT = 19;
400
- const STATIC_AGENT_COUNT = 47;
399
+ // Static counts for cross-repo plugins not discoverable locally.
400
+ // Per-plugin file-based agent counts. Update this map when agents are added/removed
401
+ // in a plugin repo - it's the canonical source for the STATIC_AGENT_COUNT fallback.
402
+ const STATIC_PLUGIN_AGENT_COUNTS = {
403
+ 'next-task': 8,
404
+ 'prepare-delivery': 3,
405
+ 'gate-and-ship': 0,
406
+ 'ship': 1,
407
+ 'deslop': 1,
408
+ 'audit-project': 0,
409
+ 'drift-detect': 1,
410
+ 'enhance': 8,
411
+ 'sync-docs': 1,
412
+ 'repo-intel': 1,
413
+ 'perf': 6,
414
+ 'learn': 1,
415
+ 'agnix': 1,
416
+ 'consult': 1,
417
+ 'debate': 1,
418
+ 'web-ctl': 1,
419
+ 'skillers': 2,
420
+ 'onboard': 1,
421
+ 'can-i-help': 1
422
+ };
423
+ const STATIC_PLUGIN_COUNT = Object.keys(STATIC_PLUGIN_AGENT_COUNTS).length;
424
+ const STATIC_FILE_BASED_AGENT_COUNT = Object.values(STATIC_PLUGIN_AGENT_COUNTS).reduce((sum, count) => sum + count, 0);
425
+ // Total = file-based + role-based (audit-project specialists, spawned dynamically)
426
+ const STATIC_AGENT_COUNT = STATIC_FILE_BASED_AGENT_COUNT + ROLE_BASED_AGENT_COUNT;
401
427
 
402
428
  function updateSiteContent(plugins, agents, skills) {
403
429
  const contentPath = path.join(ROOT_DIR, 'site', 'content.json');
@@ -652,6 +678,8 @@ module.exports = {
652
678
  PURPOSE_MAP,
653
679
  ROLE_BASED_AGENT_COUNT,
654
680
  STATIC_SKILLS,
681
+ STATIC_PLUGIN_AGENT_COUNTS,
655
682
  STATIC_PLUGIN_COUNT,
683
+ STATIC_FILE_BASED_AGENT_COUNT,
656
684
  STATIC_AGENT_COUNT
657
685
  };
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Setup git hooks for development
4
- * - pre-commit: Placeholder (lib/ sync handled by agent-core CI)
5
4
  * - pre-push: Runs preflight checks, /enhance reminder, release validation
6
5
  */
7
6
 
@@ -9,13 +8,8 @@ const fs = require('fs');
9
8
  const path = require('path');
10
9
 
11
10
  const hookDir = path.join(__dirname, '..', '.git', 'hooks');
12
- const preCommitPath = path.join(hookDir, 'pre-commit');
13
11
  const prePushPath = path.join(hookDir, 'pre-push');
14
12
 
15
- const preCommitHook = `#!/bin/sh
16
- # Pre-commit hook (lib/ sync now handled by agent-core)
17
- `;
18
-
19
13
  const prePushHook = `#!/bin/sh
20
14
  # Pre-push validations:
21
15
  # 1. Run preflight checks (validators + gap checks)
@@ -135,14 +129,6 @@ function main() {
135
129
  return 0;
136
130
  }
137
131
 
138
- try {
139
- fs.writeFileSync(preCommitPath, preCommitHook, { mode: 0o755 });
140
- console.log('Git pre-commit hook installed');
141
- } catch (err) {
142
- // Non-fatal - might not have write permissions
143
- console.warn('Could not install pre-commit hook:', err.message);
144
- }
145
-
146
132
  try {
147
133
  fs.writeFileSync(prePushPath, prePushHook, { mode: 0o755 });
148
134
  console.log('Git pre-push hook installed (release tag validation)');