agentsys 5.8.5 → 5.9.0

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.
@@ -116,216 +116,136 @@ function getTasksPath(projectPath = process.cwd()) {
116
116
  return tasksPath;
117
117
  }
118
118
 
119
+ /**
120
+ * Build the canonical default tasks schema.
121
+ */
122
+ function defaultTasksSchema() {
123
+ return { active: null, tasks: [], _version: 0 };
124
+ }
125
+
126
+ /**
127
+ * Normalize on-disk data to the canonical schema.
128
+ * Handles two legacy formats:
129
+ * - { active } only (pre-v5.8.4)
130
+ * - { version, tasks[] } (worktree-manager format)
131
+ */
132
+ function normalizeTasksData(data) {
133
+ const out = Object.assign(defaultTasksSchema(), {});
134
+ // active field
135
+ if (Object.prototype.hasOwnProperty.call(data, 'active')) {
136
+ out.active = data.active;
137
+ }
138
+ // tasks array
139
+ if (Array.isArray(data.tasks)) {
140
+ out.tasks = data.tasks;
141
+ }
142
+ // _version
143
+ if (typeof data._version === 'number') {
144
+ out._version = data._version;
145
+ }
146
+ // _writerId (preserve if present)
147
+ if (typeof data._writerId === 'string') {
148
+ out._writerId = data._writerId;
149
+ }
150
+ return out;
151
+ }
152
+
119
153
  /**
120
154
  * 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
155
+ * Returns default schema when file does not exist.
156
+ * Throws on corrupted JSON to prevent silent data loss.
137
157
  */
138
158
  function readTasks(projectPath = process.cwd()) {
139
159
  const tasksPath = getTasksPath(projectPath);
140
160
  if (!fs.existsSync(tasksPath)) {
141
- return { active: null, tasks: [], _version: 0 };
161
+ return defaultTasksSchema();
142
162
  }
143
- const raw = fs.readFileSync(tasksPath, 'utf8');
144
163
  let data;
145
164
  try {
146
- data = JSON.parse(raw);
165
+ data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
147
166
  } catch (e) {
148
- throw new Error(`[CRITICAL] Corrupted tasks.json at ${tasksPath}: ${e.message}. File must be repaired or deleted manually before writes are allowed.`);
167
+ throw new Error(`Corrupted tasks.json at ${tasksPath}: ${e.message}`);
149
168
  }
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
- };
169
+ return normalizeTasksData(data);
157
170
  }
158
171
 
159
172
  /**
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
173
+ * Write tasks.json to main project.
174
+ * Increments _version and stamps _writerId.
175
+ * Returns the writerId that was stamped (string).
169
176
  */
170
177
  function writeTasks(tasks, projectPath = process.cwd()) {
171
178
  ensureStateDir(projectPath);
172
- const copy = structuredClone(tasks);
173
- copy._version = (copy._version || 0) + 1;
174
- copy._writerId = crypto.randomBytes(8).toString('hex');
175
179
  const tasksPath = getTasksPath(projectPath);
176
- writeJsonAtomic(tasksPath, copy);
177
- return copy._writerId;
180
+ const writerId = crypto.randomBytes(8).toString('hex');
181
+ const toWrite = Object.assign({}, tasks, {
182
+ _version: (tasks._version || 0) + 1,
183
+ _writerId: writerId
184
+ });
185
+ writeJsonAtomic(tasksPath, toWrite);
186
+ return writerId;
178
187
  }
179
188
 
180
189
  /**
181
190
  * Apply a mutation to tasks.json with optimistic locking.
191
+ * Retries up to 5 times on concurrent-writer conflict.
192
+ * Returns true on success, false if tasks.json is corrupted.
182
193
  *
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
194
+ * @param {function(Object): Object} mutatorFn - Receives current tasks, must return updated tasks.
195
+ * @param {string} [projectPath]
196
+ * @returns {boolean}
195
197
  */
196
198
  function updateTasks(mutatorFn, projectPath = process.cwd()) {
197
199
  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
200
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
208
- const initialVersion = current._version || 0;
209
-
210
- let updated;
201
+ let current;
211
202
  try {
212
- updated = mutatorFn(structuredClone(current));
203
+ current = readTasks(projectPath);
213
204
  } catch (e) {
214
- console.error(`[ERROR] updateTasks: mutatorFn threw on attempt ${attempt + 1}: ${e.message}`);
205
+ // Corrupted do not overwrite
215
206
  return false;
216
207
  }
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
-
208
+ const updated = mutatorFn(Object.assign({}, current));
226
209
  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.
210
+ // Verify our write won (optimistic lock check)
230
211
  let afterWrite;
231
212
  try {
232
213
  afterWrite = readTasks(projectPath);
233
214
  } catch (e) {
234
- console.error(`[ERROR] updateTasks: tasks.json corrupted after write on attempt ${attempt + 1}: ${e.message}`);
235
215
  return false;
236
216
  }
237
-
238
- if (afterWrite._version === initialVersion + 1 && afterWrite._writerId === writerId) {
217
+ if (afterWrite._writerId === writerId) {
239
218
  return true;
240
219
  }
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
- }
220
+ // Another writer won — retry with jitter
221
+ const jitter = Math.floor(Math.random() * 20);
222
+ const start = Date.now();
223
+ while (Date.now() - start < jitter) { /* busy-wait for short jitter */ }
253
224
  }
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
225
  return false;
285
226
  }
286
227
 
287
228
  /**
288
- * Set active task in main project (uses optimistic locking)
289
- */
290
- function setActiveTask(task, projectPath = process.cwd()) {
291
- return updateTasks(tasks => {
292
- tasks.active = { ...task, startedAt: new Date().toISOString() };
293
- return tasks;
294
- }, projectPath);
295
- }
296
-
297
- /**
298
- * Clear active task (uses optimistic locking)
299
- */
300
- function clearActiveTask(projectPath = process.cwd()) {
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.
229
+ * Claim (upsert) a task entry in tasks[].
230
+ * Entry must have an `id` field.
231
+ * Returns false if entry.id is missing.
310
232
  *
311
- * @param {Object} entry - { id, source, title, branch, worktreePath, claimedBy }
312
- * @param {string} projectPath
233
+ * @param {{ id: string, [key: string]: * }} entry
234
+ * @param {string} [projectPath]
235
+ * @returns {boolean}
313
236
  */
314
237
  function claimTask(entry, projectPath = process.cwd()) {
315
- if (!entry || !entry.id) {
316
- console.error('[ERROR] claimTask: entry.id is required');
238
+ if (!entry || typeof entry.id !== 'string' || entry.id.length === 0) {
317
239
  return false;
318
240
  }
319
241
  return updateTasks(tasks => {
320
- const idx = tasks.tasks.findIndex(t => t.id === entry.id);
321
- const record = {
322
- ...entry,
242
+ const existing = tasks.tasks.findIndex(t => t.id === entry.id);
243
+ const record = Object.assign({}, entry, {
323
244
  status: 'claimed',
324
- claimedAt: entry.claimedAt || new Date().toISOString(),
325
245
  lastActivityAt: new Date().toISOString()
326
- };
327
- if (idx >= 0) {
328
- tasks.tasks[idx] = record;
246
+ });
247
+ if (existing >= 0) {
248
+ tasks.tasks[existing] = record;
329
249
  } else {
330
250
  tasks.tasks.push(record);
331
251
  }
@@ -334,31 +254,44 @@ function claimTask(entry, projectPath = process.cwd()) {
334
254
  }
335
255
 
336
256
  /**
337
- * Release a claimed task from the registry (uses optimistic locking).
338
- * Used by ship and --abort; replaces the raw fs.writeFileSync inline cleanup.
257
+ * Release (remove) a task from tasks[] by id.
258
+ * Idempotent returns true even if task was not found.
339
259
  *
340
260
  * @param {string} taskId
341
- * @param {string} projectPath
261
+ * @param {string} [projectPath]
262
+ * @returns {boolean}
342
263
  */
343
264
  function releaseTask(taskId, projectPath = process.cwd()) {
344
- if (!taskId) {
345
- console.error('[ERROR] releaseTask: taskId is required');
346
- return false;
347
- }
348
265
  return updateTasks(tasks => {
349
- const before = tasks.tasks.length;
350
266
  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
267
  return tasks;
356
268
  }, projectPath);
357
269
  }
358
270
 
359
271
  /**
360
- * Check if there's an active task.
361
- * Uses != null to catch both null and undefined (legacy format safety).
272
+ * Set active task in main project
273
+ */
274
+ function setActiveTask(task, projectPath = process.cwd()) {
275
+ const tasks = readTasks(projectPath);
276
+ tasks.active = {
277
+ ...task,
278
+ startedAt: new Date().toISOString()
279
+ };
280
+ return writeTasks(tasks, projectPath);
281
+ }
282
+
283
+ /**
284
+ * Clear active task
285
+ */
286
+ function clearActiveTask(projectPath = process.cwd()) {
287
+ const tasks = readTasks(projectPath);
288
+ tasks.active = null;
289
+ return writeTasks(tasks, projectPath);
290
+ }
291
+
292
+ /**
293
+ * Check if there's an active task
294
+ * Uses != null to catch both null and undefined (legacy format safety)
362
295
  */
363
296
  function hasActiveTask(projectPath = process.cwd()) {
364
297
  const tasks = readTasks(projectPath);
@@ -735,10 +668,10 @@ module.exports = {
735
668
  readTasks,
736
669
  writeTasks,
737
670
  updateTasks,
738
- setActiveTask,
739
- clearActiveTask,
740
671
  claimTask,
741
672
  releaseTask,
673
+ setActiveTask,
674
+ clearActiveTask,
742
675
  hasActiveTask,
743
676
 
744
677
  // Flow (worktree)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsys",
3
- "version": "5.8.5",
3
+ "version": "5.9.0",
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",
@@ -373,20 +373,31 @@ function generateAgentNavTable(agents, plugins) {
373
373
 
374
374
  /**
375
375
  * Generate the agents count summary line for docs/reference/AGENTS.md.
376
+ *
377
+ * Uses static fallbacks when local discovery returns nothing — the agentsys
378
+ * monorepo no longer contains plugins/, so discovery is empty in CI. Static
379
+ * counts are derived from STATIC_PLUGIN_AGENT_COUNTS so they stay in sync as
380
+ * plugins are added.
376
381
  */
377
382
  function generateAgentCounts(agents, plugins) {
378
- const fileBasedAgents = agents.length;
379
- const totalAgents = fileBasedAgents + ROLE_BASED_AGENT_COUNT;
380
-
381
- // Count plugins with agents (file-based or role-based)
382
- const pluginsWithAgents = new Set();
383
- for (const agent of agents) {
384
- pluginsWithAgents.add(agent.plugin);
383
+ const totalAgents = agents.length > 0
384
+ ? agents.length + ROLE_BASED_AGENT_COUNT
385
+ : STATIC_AGENT_COUNT;
386
+ const totalPlugins = plugins.length > 0 ? plugins.length : STATIC_PLUGIN_COUNT;
387
+
388
+ let pluginsWithAgentsCount;
389
+ if (agents.length > 0) {
390
+ const pluginsWithAgents = new Set(agents.map(a => a.plugin));
391
+ pluginsWithAgents.add('audit-project'); // role-based
392
+ pluginsWithAgentsCount = pluginsWithAgents.size;
393
+ } else {
394
+ // Static fallback: file-based plugins (count > 0 in static map) plus
395
+ // audit-project (role-based, count is 0 in the map but still has agents).
396
+ const fileBasedPluginsWithAgents = Object.values(STATIC_PLUGIN_AGENT_COUNTS).filter(c => c > 0).length;
397
+ pluginsWithAgentsCount = fileBasedPluginsWithAgents + 1;
385
398
  }
386
- pluginsWithAgents.add('audit-project'); // role-based
387
- const pluginCount = pluginsWithAgents.size;
388
399
 
389
- return `**TL;DR:** ${totalAgents} agents across ${plugins.length} plugins (${pluginCount} have agents). opus for reasoning, sonnet for patterns, haiku for execution. Each agent does one thing well. <!-- AGENT_COUNT_TOTAL: ${totalAgents} -->`;
400
+ return `**TL;DR:** ${totalAgents} agents across ${totalPlugins} plugins (${pluginsWithAgentsCount} have agents). opus for reasoning, sonnet for patterns, haiku for execution. Each agent does one thing well. <!-- AGENT_COUNT_TOTAL: ${totalAgents} -->`;
390
401
  }
391
402
 
392
403
  // ---------------------------------------------------------------------------
@@ -418,7 +429,8 @@ const STATIC_PLUGIN_AGENT_COUNTS = {
418
429
  'web-ctl': 1,
419
430
  'skillers': 2,
420
431
  'onboard': 1,
421
- 'can-i-help': 1
432
+ 'can-i-help': 1,
433
+ 'zig-lsp': 0
422
434
  };
423
435
  const STATIC_PLUGIN_COUNT = Object.keys(STATIC_PLUGIN_AGENT_COUNTS).length;
424
436
  const STATIC_FILE_BASED_AGENT_COUNT = Object.values(STATIC_PLUGIN_AGENT_COUNTS).reduce((sum, count) => sum + count, 0);
@@ -17,3 +17,4 @@ ship
17
17
  skillers
18
18
  sync-docs
19
19
  web-ctl
20
+ zig-lsp
package/site/content.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "agentsys",
4
- "description": "A modular runtime and orchestration system for AI agents. 19 plugins, 49 agents, 41 skills - structured pipelines for Claude Code, OpenCode, Codex CLI, Cursor, and Kiro.",
4
+ "description": "A modular runtime and orchestration system for AI agents. 20 plugins, 49 agents, 41 skills - structured pipelines for Claude Code, OpenCode, Codex CLI, Cursor, and Kiro.",
5
5
  "url": "https://agent-sh.github.io/agentsys",
6
6
  "repo": "https://github.com/agent-sh/agentsys",
7
7
  "npm": "https://www.npmjs.com/package/agentsys",
8
- "version": "5.8.5",
8
+ "version": "5.9.0",
9
9
  "author": "Avi Fenesh",
10
10
  "author_url": "https://github.com/avifenesh"
11
11
  },
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "stats": [
25
25
  {
26
- "value": "19",
26
+ "value": "20",
27
27
  "label": "Plugins",
28
28
  "suffix": ""
29
29
  },
package/site/index.html CHANGED
@@ -4,12 +4,12 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>AgentSys - Agent Runtime &amp; Orchestration System</title>
7
- <meta name="description" content="A modular runtime and orchestration system for AI agents. 19 plugins, 49 agents, 41 skills - structured pipelines for Claude Code, OpenCode, Codex CLI, Cursor, and Kiro.">
7
+ <meta name="description" content="A modular runtime and orchestration system for AI agents. 20 plugins, 49 agents, 41 skills - structured pipelines for Claude Code, OpenCode, Codex CLI, Cursor, and Kiro.">
8
8
  <meta name="theme-color" content="#09090b">
9
9
 
10
10
  <!-- Open Graph -->
11
11
  <meta property="og:title" content="AgentSys">
12
- <meta property="og:description" content="A modular runtime and orchestration system for AI agents. 19 plugins, 49 agents, 41 skills.">
12
+ <meta property="og:description" content="A modular runtime and orchestration system for AI agents. 20 plugins, 49 agents, 41 skills.">
13
13
  <meta property="og:image" content="https://agent-sh.github.io/agentsys/assets/logo.png">
14
14
  <meta property="og:url" content="https://agent-sh.github.io/agentsys/">
15
15
  <meta property="og:type" content="website">
@@ -17,7 +17,7 @@
17
17
  <!-- Twitter Card -->
18
18
  <meta name="twitter:card" content="summary_large_image">
19
19
  <meta name="twitter:title" content="AgentSys">
20
- <meta name="twitter:description" content="AI workflow automation. 19 plugins, 49 agents, 41 skills.">
20
+ <meta name="twitter:description" content="AI workflow automation. 20 plugins, 49 agents, 41 skills.">
21
21
  <meta name="twitter:image" content="https://agent-sh.github.io/agentsys/assets/logo.png">
22
22
 
23
23
  <!-- Content Security Policy -->
@@ -107,7 +107,7 @@
107
107
  <div class="hero__inner">
108
108
  <div class="hero__content">
109
109
  <div class="hero__badge anim-fade-in" data-delay="100">
110
- 19 plugins &middot; 49 agents &middot; 41 skills
110
+ 20 plugins &middot; 49 agents &middot; 41 skills
111
111
  </div>
112
112
  <h1 class="hero__title anim-fade-up" id="hero-title" data-delay="200">
113
113
  A modular <span class="text-gradient">runtime and orchestration system</span><br>
@@ -156,7 +156,7 @@
156
156
  <section class="stats" id="stats" aria-label="Project statistics">
157
157
  <div class="stats__inner">
158
158
  <div class="stats__item">
159
- <span class="stats__number" aria-live="polite" data-target="19">0</span>
159
+ <span class="stats__number" aria-live="polite" data-target="20">0</span>
160
160
  <span class="stats__label">Plugins</span>
161
161
  </div>
162
162
  <div class="stats__item">
package/site/ux-spec.md CHANGED
@@ -94,7 +94,7 @@ Scroll order with rationale for each section. All sections are full-width, alter
94
94
  - **Single column on mobile:** Text above, terminal below. Stack with 48px gap.
95
95
 
96
96
  ### Left Column Content
97
- 1. **Badge** (top, above title): Small pill showing version or "19 plugins . 49 agents . 41 skills"
97
+ 1. **Badge** (top, above title): Small pill showing version or "20 plugins . 49 agents . 41 skills"
98
98
  - Background: `rgba(99, 102, 241, 0.12)`, border: `1px solid rgba(99, 102, 241, 0.25)`, border-radius: 9999px
99
99
  - Font: 13px, font-weight 500, primary accent color
100
100
  - Padding: 4px 14px
@@ -104,7 +104,7 @@ Scroll order with rationale for each section. All sections are full-width, alter
104
104
  - Color: white
105
105
  - "entire dev workflow" portion highlighted with a subtle gradient text (primary-to-secondary accent via `background-clip: text`)
106
106
 
107
- 3. **Subtitle:** "19 plugins, 49 agents, 41 skills. From task selection to merged PR. Works with Claude Code, OpenCode, Codex CLI, Cursor, and Kiro."
107
+ 3. **Subtitle:** "20 plugins, 49 agents, 41 skills. From task selection to merged PR. Works with Claude Code, OpenCode, Codex CLI, Cursor, and Kiro."
108
108
  - Font: 18px on desktop, 16px on mobile, font-weight 400, line-height 1.6
109
109
  - Color: `rgba(255, 255, 255, 0.6)`
110
110
  - Max-width: 520px
@@ -226,9 +226,9 @@ Done. Task to merged PR in 12 minutes.
226
226
  ### Stats (left to right)
227
227
  | Stat | Value | Label |
228
228
  |------|-------|-------|
229
- | 1 | 19 | Plugins |
229
+ | 1 | 20 | Plugins |
230
230
  | 2 | 49 | Agents |
231
- | 3 | 40 | Skills |
231
+ | 3 | 41 | Skills |
232
232
  | 4 | 3,507 | Tests Passing |
233
233
 
234
234
  ### Styling
@@ -650,13 +650,13 @@ This disables:
650
650
  ### Head Content
651
651
  ```html
652
652
  <title>AgentSys - Agent Runtime &amp; Orchestration System</title>
653
- <meta name="description" content="A modular runtime and orchestration system for AI agents. 19 plugins, 49 agents, 41 skills - structured pipelines for Claude Code, OpenCode, Codex CLI, Cursor, and Kiro.">
653
+ <meta name="description" content="A modular runtime and orchestration system for AI agents. 20 plugins, 49 agents, 41 skills - structured pipelines for Claude Code, OpenCode, Codex CLI, Cursor, and Kiro.">
654
654
  <meta name="viewport" content="width=device-width, initial-scale=1">
655
655
  <meta name="theme-color" content="#0a0a0f">
656
656
 
657
657
  <!-- Open Graph -->
658
658
  <meta property="og:title" content="AgentSys">
659
- <meta property="og:description" content="AI workflow automation. 19 plugins, 49 agents, 41 skills. Task to merged PR.">
659
+ <meta property="og:description" content="AI workflow automation. 20 plugins, 49 agents, 41 skills. Task to merged PR.">
660
660
  <meta property="og:image" content="https://agent-sh.github.io/agentsys/assets/og-image.png">
661
661
  <meta property="og:url" content="https://agent-sh.github.io/agentsys/">
662
662
  <meta property="og:type" content="website">
@@ -664,7 +664,7 @@ This disables:
664
664
  <!-- Twitter Card -->
665
665
  <meta name="twitter:card" content="summary_large_image">
666
666
  <meta name="twitter:title" content="AgentSys">
667
- <meta name="twitter:description" content="AI workflow automation. 19 plugins, 49 agents, 41 skills.">
667
+ <meta name="twitter:description" content="AI workflow automation. 20 plugins, 49 agents, 41 skills.">
668
668
  <meta name="twitter:image" content="https://agent-sh.github.io/agentsys/assets/og-image.png">
669
669
  ```
670
670