@wrongstack/core 0.306.4 → 0.307.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.
Files changed (103) hide show
  1. package/dist/chronicle/index.js +957 -953
  2. package/dist/chronicle/metrics-ingest.d.ts +55 -0
  3. package/dist/chronicle/metrics-schema.d.ts +86 -0
  4. package/dist/chronicle/metrics-store.d.ts +5 -120
  5. package/dist/chronicle/project-server.js +1083 -1084
  6. package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
  7. package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +12 -177
  9. package/dist/coordination/agents/index.js +1738 -1690
  10. package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
  11. package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
  12. package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
  13. package/dist/coordination/index.js +6011 -5894
  14. package/dist/coordination/mail-tools.d.ts +1 -1
  15. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  16. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  17. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  18. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  19. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  20. package/dist/coordination/mailbox-project-server.js +1 -1
  21. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  22. package/dist/coordination/mailbox-types.d.ts +6 -793
  23. package/dist/core/agent-loop-context.d.ts +28 -0
  24. package/dist/core/agent-loop-detector.d.ts +25 -0
  25. package/dist/core/fallback-doctor.d.ts +72 -0
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.js +1159 -888
  28. package/dist/defaults/index.js +3579 -3545
  29. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  30. package/dist/execution/autonomy-brain.d.ts +3 -156
  31. package/dist/execution/compaction-budget.d.ts +38 -0
  32. package/dist/execution/compaction-core.d.ts +3 -170
  33. package/dist/execution/compaction-elision.d.ts +58 -0
  34. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  35. package/dist/execution/council-orchestrator.d.ts +3 -42
  36. package/dist/execution/council-response-parser.d.ts +56 -0
  37. package/dist/execution/index.js +2415 -2443
  38. package/dist/execution/prompt-enhancer.js +11 -3
  39. package/dist/execution/tool-executor-guard.d.ts +18 -0
  40. package/dist/execution/tool-executor-runner.d.ts +12 -0
  41. package/dist/execution/tool-executor.d.ts +0 -75
  42. package/dist/goal/index.js +19 -6
  43. package/dist/hq/index.js +8 -2
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +7813 -7292
  46. package/dist/infrastructure/index.js +3 -1
  47. package/dist/kernel/events/agent-events.d.ts +2 -0
  48. package/dist/kernel/events/provider-events.d.ts +13 -0
  49. package/dist/plugin/index.js +2590 -2561
  50. package/dist/plugins/auto-review-config.d.ts +53 -0
  51. package/dist/plugins/auto-review-git.d.ts +19 -0
  52. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  53. package/dist/plugins/review-finding-verification.d.ts +7 -0
  54. package/dist/prompts/index.d.ts +1 -0
  55. package/dist/prompts/prompt-journal.d.ts +94 -0
  56. package/dist/security/index.js +627 -652
  57. package/dist/security/permission-explain.d.ts +19 -0
  58. package/dist/security/permission-policy.d.ts +0 -101
  59. package/dist/security/yolo-risk.d.ts +5 -4
  60. package/dist/session-catalog/index.js +220 -212
  61. package/dist/session-catalog/project-server.js +227 -219
  62. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  63. package/dist/session-catalog/store-schema.d.ts +48 -0
  64. package/dist/session-catalog/store.d.ts +0 -17
  65. package/dist/storage/director-state.d.ts +6 -0
  66. package/dist/storage/file-session-writer.d.ts +2 -58
  67. package/dist/storage/index.d.ts +2 -1
  68. package/dist/storage/index.js +2057 -1925
  69. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  70. package/dist/storage/session-recovery.d.ts +9 -0
  71. package/dist/storage/session-store/rename-session.d.ts +16 -0
  72. package/dist/storage/session-store/resume-session.d.ts +26 -0
  73. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  74. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  75. package/dist/storage/session-store.d.ts +1 -129
  76. package/dist/storage/session-summary-tracker.d.ts +39 -0
  77. package/dist/storage/session-write-buffer.d.ts +43 -0
  78. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  79. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  80. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  81. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  82. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  83. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  84. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  85. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  86. package/dist/tools/index.js +5070 -5071
  87. package/dist/types/config/root.d.ts +7 -2
  88. package/dist/types/default-config.d.ts +1 -1
  89. package/dist/types/index.d.ts +54 -55
  90. package/dist/types/index.js +953 -932
  91. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  92. package/dist/types/session-markers.d.ts +12 -0
  93. package/dist/types/session-markers.js +19 -0
  94. package/dist/worktree/worktree-manager.d.ts +1 -1
  95. package/instructions/coordination/subagent-baseline.md +2 -2
  96. package/instructions/llm/prompt-enhancer.md +2 -1
  97. package/instructions/sections/tool/common-patterns.md +21 -0
  98. package/instructions/sections/tool/delegation-compact.md +3 -1
  99. package/instructions/sections/tool/delegation-full.md +5 -1
  100. package/instructions/system-lite.md +28 -3
  101. package/instructions/system-pro.md +37 -8
  102. package/instructions/system.md +4 -2
  103. package/package.json +5 -5
@@ -1,6 +1,6 @@
1
1
  // src/coordination/agents/agent-prompts.ts
2
- import { readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
3
- import * as path12 from "node:path";
2
+ import { readFileSync as readFileSync11, statSync as statSync2 } from "node:fs";
3
+ import * as path13 from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  // src/utils/wstack-paths.ts
@@ -143,591 +143,502 @@ function resolveWstackPaths(opts) {
143
143
  }
144
144
 
145
145
  // src/coordination/agents/project-agent-identity.ts
146
- import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
147
- import * as path11 from "node:path";
146
+ import { existsSync as existsSync5, readFileSync as readFileSync10 } from "node:fs";
147
+ import * as path12 from "node:path";
148
148
 
149
- // src/security/capabilities.ts
150
- var ToolCapabilities = {
151
- /** Can execute arbitrary commands in the user's shell (the `bash` tool). */
152
- SHELL_ARBITRARY: "shell.arbitrary",
153
- /**
154
- * Can execute the `exec` tool's allowlisted commands.
155
- *
156
- * "Restricted" describes the command NAME check, not the resulting power.
157
- * Treat this as equivalent to {@link SHELL_ARBITRARY} when deciding what to
158
- * grant: the allowlist includes general-purpose interpreters (`node`,
159
- * `python`, `perl`, `ruby`) and, on Windows, the platform shells (`cmd`,
160
- * `powershell`, `pwsh`) — deliberately, since without them `exec` cannot run
161
- * `dir`/`type`/any cmdlet. `node -e …` or `cmd /c …` therefore reaches
162
- * arbitrary execution, and per-argument denylisting cannot close that while
163
- * the shells remain (blocking `node -e` still leaves `cmd /c node -e`).
164
- *
165
- * What actually bounds this is downstream, not the name check: the permission
166
- * policy reconstructs the full command line and runs it through the
167
- * destructive classifier, so destructive calls still require confirmation.
168
- *
169
- * Granting `shell.restricted` while withholding `shell.arbitrary` therefore
170
- * buys no meaningful reduction in blast radius. Documented rather than
171
- * silently implied, because the two read as a narrow/wide pair (WS-083).
172
- */
173
- SHELL_RESTRICTED: "shell.restricted",
174
- /** Can run a restricted project formatter/linter-style command. */
175
- SHELL_EXEC: "shell.exec",
176
- /** Can read files inside the project (and possibly outside via symlinks if not guarded). */
177
- FS_READ: "fs.read",
178
- /** Can write / modify / delete files inside the project. */
179
- FS_WRITE: "fs.write",
180
- /** Can write files outside the current project root (very high risk). */
181
- FS_WRITE_OUTSIDE_PROJECT: "fs.write.outside-project",
182
- /** Can perform outbound network requests. */
183
- NET_OUTBOUND: "net.outbound",
184
- /** Can mutate in-memory session todos only. */
185
- SESSION_TODO: "session.todo",
186
- /** Can park after-task `<nextsteps>` suggestions for the current turn. */
187
- SESSION_NEXTSTEPS: "session.nextsteps",
188
- /** Can mutate in-memory session mode only. */
189
- SESSION_MODE: "session.mode",
190
- /** Can inspect registered tool metadata. */
191
- TOOL_META: "tool.meta",
192
- /** Can invoke arbitrary registered tools through a meta-tool. */
193
- TOOL_MUTATE_ANY: "tool.mutate.any",
194
- /** Can read persistent memory. */
195
- MEMORY_READ: "memory.read",
196
- /** Can write persistent memory. */
197
- MEMORY_WRITE: "memory.write",
198
- /** Can delete persistent memory. */
199
- MEMORY_DELETE: "memory.delete",
200
- /** Proxies tools from external MCP servers (unknown capability). */
201
- MCP_PROXY: "mcp.proxy",
202
- /** Can spawn or manage subagents / multi-agent tasks. */
203
- SUBAGENT_SPAWN: "subagent.spawn",
204
- /** Can inspect fleet/subagent coordination state without mutating it. */
205
- COORDINATION_FLEET_READ: "coordination.fleet.read",
206
- /** Can publish attributed, schema-checked events onto the fleet bus. */
207
- COORDINATION_FLEET_EMIT: "coordination.fleet.emit",
208
- /** Can submit a task-local structured result to the parent Director. */
209
- COORDINATION_RESULT_SUBMIT: "coordination.result.submit",
210
- /** Can read or write inter-agent mailbox messages. */
211
- COORDINATION_MAIL: "coordination.mail",
212
- /** Can schedule, inspect, or cancel in-session cron jobs. */
213
- COORDINATION_CRON: "coordination.cron",
214
- /** Can mutate global or session configuration / trust state. */
215
- CONFIG_MUTATE: "config.mutate",
216
- /** Can install packages or run package managers with side effects. */
217
- PACKAGE_INSTALL: "package.install"
218
- };
219
- var DANGEROUS_FOR_SUBAGENTS = [
220
- ToolCapabilities.SHELL_ARBITRARY,
221
- ToolCapabilities.SHELL_RESTRICTED,
222
- ToolCapabilities.SHELL_EXEC,
223
- ToolCapabilities.FS_WRITE,
224
- ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
225
- ToolCapabilities.TOOL_MUTATE_ANY,
226
- ToolCapabilities.MEMORY_WRITE,
227
- ToolCapabilities.MEMORY_DELETE,
228
- ToolCapabilities.MCP_PROXY,
229
- ToolCapabilities.SUBAGENT_SPAWN,
230
- ToolCapabilities.CONFIG_MUTATE,
231
- ToolCapabilities.PACKAGE_INSTALL
232
- ];
233
- var WIDE_SUBAGENT_CAPABILITIES = [
234
- ToolCapabilities.FS_READ,
235
- ToolCapabilities.FS_WRITE,
236
- ToolCapabilities.NET_OUTBOUND,
237
- ToolCapabilities.SESSION_TODO,
238
- ToolCapabilities.TOOL_META,
239
- ToolCapabilities.MEMORY_READ,
240
- ToolCapabilities.SHELL_ARBITRARY,
241
- ToolCapabilities.SHELL_RESTRICTED,
242
- ToolCapabilities.SHELL_EXEC,
243
- ToolCapabilities.PACKAGE_INSTALL,
244
- // Read-only fleet visibility (fleet_status) — peer awareness has no blast
245
- // radius and every fleet worker should coordinate around its peers.
246
- ToolCapabilities.COORDINATION_FLEET_READ,
247
- ToolCapabilities.COORDINATION_RESULT_SUBMIT
248
- ];
249
- function clampSubagentCapabilities(requested, ceiling = WIDE_SUBAGENT_CAPABILITIES) {
250
- const ceilingSet = new Set(ceiling);
251
- const requestedSet = new Set(requested);
149
+ // src/coordination/agents/project-agent-capture-window.ts
150
+ var CAPTURE_COOLDOWN_MS = 12e4;
151
+ var CAPTURE_MAX_PER_SESSION = 3;
152
+ var CAPTURE_SESSION_WINDOW_MS = 30 * 6e4;
153
+ var MAX_WINDOWS = 1e3;
154
+ var windows = /* @__PURE__ */ new Map();
155
+ function currentWindow(key, now) {
156
+ const existing = windows.get(key);
157
+ if (!existing || now - existing.windowStartedAt >= CAPTURE_SESSION_WINDOW_MS) {
158
+ const fresh = {
159
+ windowStartedAt: now,
160
+ count: 0,
161
+ lastCaptureAt: existing?.lastCaptureAt
162
+ };
163
+ if (windows.size >= MAX_WINDOWS && !windows.has(key)) {
164
+ const oldestKey = windows.keys().next().value;
165
+ if (oldestKey !== void 0) {
166
+ windows.delete(oldestKey);
167
+ }
168
+ }
169
+ windows.set(key, fresh);
170
+ return fresh;
171
+ }
172
+ windows.delete(key);
173
+ windows.set(key, existing);
174
+ return existing;
175
+ }
176
+ function captureWindowState(key, now = Date.now()) {
177
+ const window = currentWindow(key, now);
252
178
  return {
253
- granted: ceiling.filter((capability) => requestedSet.has(capability)),
254
- dropped: [...requestedSet].filter((capability) => !ceilingSet.has(capability))
179
+ count: window.count,
180
+ lastCaptureAt: window.lastCaptureAt,
181
+ remaining: Math.max(0, CAPTURE_MAX_PER_SESSION - window.count)
255
182
  };
256
183
  }
184
+ function recordCaptureAttempt(key, now = Date.now()) {
185
+ const window = currentWindow(key, now);
186
+ window.count += 1;
187
+ window.lastCaptureAt = now;
188
+ }
189
+ function resetCaptureWindows() {
190
+ windows.clear();
191
+ }
192
+ function resetCaptureWindow(key) {
193
+ windows.delete(key);
194
+ }
257
195
 
258
- // src/types/runtime-capability-manifest.ts
259
- var PLAYWRIGHT_ALIASES = {
260
- playwright_navigate: "browser_navigate",
261
- playwright_screenshot: "browser_screenshot",
262
- playwright_click: "browser_click",
263
- playwright_type: "browser_type",
264
- playwright_evaluate: "browser_evaluate",
265
- playwright_select_option: "browser_select",
266
- playwright_hover: "browser_hover",
267
- playwright_fill_form: "browser_type",
268
- playwright_wait_for: "browser_wait",
269
- playwright_press_key: "browser_press",
270
- playwright_drag: "browser_drag"
271
- };
272
- var RUNTIME_CAPABILITY_MANIFEST = [
273
- {
274
- id: "filesystem.read",
275
- pack: "core",
276
- exposure: "direct",
277
- tools: ["read", "grep", "glob", "tree"]
278
- },
279
- {
280
- id: "code.inspect",
281
- pack: "core",
282
- exposure: "direct",
283
- tools: [
284
- "codebase-stats",
285
- "codebase-search",
286
- "codebase-incoming-calls",
287
- "codebase-outgoing-calls",
288
- "codebase-index",
289
- "dead-code-scan",
290
- "diff",
291
- "json",
292
- "logs"
293
- ]
294
- },
295
- {
296
- id: "filesystem.write",
297
- pack: "development",
298
- exposure: "direct",
299
- tools: ["write", "edit", "replace", "patch"]
300
- },
301
- {
302
- id: "execution.shell",
303
- pack: "development",
304
- exposure: "direct",
305
- tools: ["bash", "exec", "language", "language_info", "language_package"]
306
- },
307
- {
308
- id: "verification.run",
309
- pack: "development",
310
- exposure: "direct",
311
- tools: ["lint", "format", "typecheck", "test", "e2e_plan"]
312
- },
313
- {
314
- id: "version-control.manage",
315
- pack: "development",
316
- exposure: "direct",
317
- tools: ["git"]
318
- },
196
+ // src/coordination/agents/project-agent-consolidation.ts
197
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
198
+ import * as path4 from "node:path";
199
+
200
+ // src/coordination/agents/project-agent-learning-entries.ts
201
+ function tokenOverlap(a, b) {
202
+ const setA = new Set(a.split(/\s+/));
203
+ const setB = new Set(b.split(/\s+/));
204
+ const intersect = new Set([...setA].filter((t) => setB.has(t)));
205
+ const union = /* @__PURE__ */ new Set([...setA, ...setB]);
206
+ return union.size === 0 ? 0 : intersect.size / union.size;
207
+ }
208
+ function splitLearnedEntries(body) {
209
+ return body.split(/\n---\n+/).map((entry) => entry.trim()).map(
210
+ (entry) => entry.replace(/^#\s+Learned (?:wisdom|instructions) for .+$/im, "").replace(/^>\s*Project-specific learning data for[\s\S]*?$/im, "").replace(/^_No learned entries yet\._$/im, "").replace(/^\*Last capture:[^\n]*\*$/im, "").replace(/<!--[\s\S]*?-->/g, "").trim()
211
+ ).filter(Boolean);
212
+ }
213
+
214
+ // src/coordination/agents/project-agent-learning-normalize.ts
215
+ var LEARNED_SOFT_LIMIT = 8192;
216
+ var LEARNED_HARD_LIMIT = 16384;
217
+ var LEARNED_ENTRY_MAX_CHARS = 600;
218
+ var MIN_INSTRUCTIVE_LENGTH = 30;
219
+ var NARRATIVE_SENTENCE_STARTS = [
220
+ /^(?:when|while|during|after|before|once|since)\s+(?:i|we|the agent|i was|i were|i had)\b/i,
221
+ /^(?:i|we)\s+(?:found|noticed|learned|discovered|realized|saw|encountered|hit|ran into)\b/i,
222
+ /^(?:i|we)\s+(?:was|were|had|did|ran|tried|attempted|used|modified|changed|updated)\b/i,
223
+ /^(?:this|that|the)\s+(?:task|session|run|commit|change|pr|pull request|invocation)\b/i,
224
+ /^(?:in|on|for|at|during)\s+(?:this|that|the)\s+(?:task|session|run|commit|change|pr)\b/i,
225
+ /^(?:today|yesterday|earlier|just now|recently|lately)\b/i
226
+ ];
227
+ var EPHEMERAL_PATTERNS = [
228
+ // Commit SHAs. A bare `[0-9a-f]{7,40}` also matches ordinary English words
229
+ // spelled entirely from hex letters ("defaced", "acceded", "effaced"), so
230
+ // require at least one digit — every real abbreviated SHA has one in
231
+ // practice, and the false-positive class disappears.
232
+ { pattern: /\b(?=[0-9a-f]{7,40}\b)[a-f]*\d[0-9a-f]*\b/g, replacement: "" },
319
233
  {
320
- id: "dependencies.manage",
321
- pack: "development",
322
- exposure: "on-demand",
323
- tools: ["install", "outdated", "audit"]
234
+ pattern: /\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b/g,
235
+ replacement: ""
324
236
  },
325
237
  {
326
- id: "documentation.author",
327
- pack: "development",
328
- exposure: "on-demand",
329
- tools: ["document", "design", "scaffold"]
238
+ pattern: /(?<=\b(?:on|from|since|before|after|dated|until)\s)\d{4}-\d{2}-\d{2}\b/gi,
239
+ replacement: ""
330
240
  },
331
- {
332
- id: "web.research",
333
- pack: "core",
334
- exposure: "direct",
335
- tools: ["search", "fetch"]
336
- },
337
- {
338
- id: "work.plan",
339
- pack: "core",
340
- exposure: "direct",
341
- tools: ["todo", "plan", "task", "kanban", "nextsteps"]
342
- },
343
- {
344
- id: "coordination.mailbox",
345
- pack: "fleet",
346
- exposure: "direct",
347
- tools: ["mailbox", "mail_send", "mail_inbox", "fleet_status"]
348
- },
349
- {
350
- id: "fleet.delegate",
351
- pack: "fleet",
352
- exposure: "on-demand",
353
- tools: [
354
- "delegate",
355
- "spawn_subagent",
356
- "assign_task",
357
- "kanban_queue",
358
- "await_tasks",
359
- "ask_subagent",
360
- "ask_result",
361
- "roll_up",
362
- "quality_gate",
363
- "terminate_subagent",
364
- "terminate_all",
365
- "work_complete",
366
- "collab_debug",
367
- "fleet_emit",
368
- "fleet"
369
- ]
370
- },
371
- {
372
- id: "reasoning.oneshot",
373
- pack: "core",
374
- exposure: "on-demand",
375
- tools: ["llm", "council"]
376
- },
377
- {
378
- id: "memory.manage",
379
- pack: "memory",
380
- exposure: "on-demand",
381
- tools: ["remember", "search_memory", "find_related_memories", "forget"]
382
- },
383
- {
384
- id: "memory.curate",
385
- pack: "memory",
386
- exposure: "on-demand",
387
- tools: [
388
- "memory_candidates",
389
- "memory_update",
390
- "memory_delete",
391
- "memory_recover",
392
- "memory_backfill_recoverable",
393
- "memory_for_file",
394
- "memory_for_path",
395
- "memory_search",
396
- "memory_graph",
397
- "memory_gather_batch",
398
- "memory_verify",
399
- "memory_hygiene"
400
- ]
401
- },
402
- {
403
- id: "browser.interact",
404
- pack: "browser",
405
- exposure: "on-demand",
406
- tools: [
407
- "browser_open",
408
- "browser_status",
409
- "browser_list",
410
- "browser_navigate",
411
- "browser_snapshot",
412
- "browser_screenshot",
413
- "browser_click",
414
- "browser_type",
415
- "browser_select",
416
- "browser_press",
417
- "browser_hover",
418
- "browser_drag",
419
- "browser_wait",
420
- "browser_evaluate",
421
- "browser_upload",
422
- "browser_close"
423
- ],
424
- aliases: PLAYWRIGHT_ALIASES
425
- },
426
- {
427
- id: "mcp.dynamic",
428
- pack: "mcp",
429
- exposure: "on-demand",
430
- tools: ["mcp_use"]
431
- },
432
- {
433
- id: "automation.manage",
434
- pack: "admin",
435
- exposure: "on-demand",
436
- tools: ["cron_schedule", "cron_list", "cron_cancel", "watch_start", "watch_list", "watch_stop"]
437
- },
438
- {
439
- id: "context.pin",
440
- pack: "admin",
441
- exposure: "on-demand",
442
- tools: ["pin_add", "pin_list", "pin_remove"]
443
- },
444
- {
445
- id: "quality.analyze",
446
- pack: "development",
447
- exposure: "on-demand",
448
- tools: ["dead_code_scan", "detect_duplicate_code", "secret_scanner_test", "error_lens_history"]
449
- },
450
- {
451
- id: "release.manage",
452
- pack: "development",
453
- exposure: "on-demand",
454
- tools: ["git_autocommit", "semver_current", "semver_bump", "semver_changelog"]
455
- },
456
- {
457
- id: "messaging.telegram",
458
- pack: "fleet",
459
- exposure: "on-demand",
460
- tools: ["telegram_send", "telegram_read", "telegram_approve"]
461
- },
462
- {
463
- id: "tools.discover",
464
- pack: "admin",
465
- exposure: "direct",
466
- tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use"]
467
- },
468
- {
469
- id: "runtime.admin",
470
- pack: "admin",
471
- exposure: "internal",
472
- tools: ["set_working_dir", "context_manager", "mode", "skill", "mcp_control"]
473
- }
241
+ { pattern: /\b(?:line|lines?)\s+\d+(?:\s*[-–]\s*\d+)?/gi, replacement: "" },
242
+ { pattern: /#\s*(?:PR|issue|MR)\s*\d+/gi, replacement: "" },
243
+ { pattern: /(?<=\s)#\d{1,6}\b/g, replacement: "" }
474
244
  ];
475
- var CAPABILITY_BY_ID = new Map(
476
- RUNTIME_CAPABILITY_MANIFEST.map((definition) => [definition.id, definition])
477
- );
478
- var CAPABILITY_BY_TOOL = /* @__PURE__ */ new Map();
479
- var TOOL_ALIASES = /* @__PURE__ */ new Map();
480
- for (const definition of RUNTIME_CAPABILITY_MANIFEST) {
481
- for (const tool of definition.tools) {
482
- CAPABILITY_BY_TOOL.set(tool, definition.id);
245
+ function normalizeLearnedEntry(raw) {
246
+ if (!raw) return null;
247
+ let cleaned = raw;
248
+ for (const { pattern, replacement } of EPHEMERAL_PATTERNS) {
249
+ cleaned = cleaned.replace(pattern, replacement);
483
250
  }
484
- for (const [alias, tool] of Object.entries(definition.aliases ?? {})) {
485
- TOOL_ALIASES.set(alias, tool);
251
+ const sentences = splitSentences(cleaned);
252
+ const directiveSentences = [];
253
+ let hadSubstantiveNarrative = false;
254
+ for (const sentence of sentences) {
255
+ const trimmed = sentence.trim();
256
+ if (!trimmed) continue;
257
+ if (isNarrativeSentence(trimmed)) {
258
+ hadSubstantiveNarrative = true;
259
+ const salvaged = salvageDirectiveTail(trimmed);
260
+ if (salvaged) directiveSentences.push(salvaged);
261
+ continue;
262
+ }
263
+ directiveSentences.push(trimmed);
264
+ }
265
+ if (directiveSentences.length === 0) return null;
266
+ let normalized = directiveSentences.join(" ").replace(/\s+/g, " ").trim();
267
+ if (normalized.length > LEARNED_ENTRY_MAX_CHARS) {
268
+ normalized = truncateToInstructive(normalized, LEARNED_ENTRY_MAX_CHARS);
269
+ if (normalized.length < MIN_INSTRUCTIVE_LENGTH) return null;
270
+ }
271
+ if (normalized.length < MIN_INSTRUCTIVE_LENGTH) return null;
272
+ if (hadSubstantiveNarrative && directiveSentences.length < sentences.length / 2) {
273
+ if (normalized.length < MIN_INSTRUCTIVE_LENGTH * 2) return null;
486
274
  }
275
+ return {
276
+ text: normalized,
277
+ category: classifyLearnedEntry(normalized)
278
+ };
487
279
  }
488
- function toolsForRuntimeCapabilities(capabilityIds) {
489
- return [
490
- ...new Set(
491
- capabilityIds.flatMap((id) => {
492
- const definition = CAPABILITY_BY_ID.get(id);
493
- if (!definition) throw new Error(`Unknown runtime capability: "${id}"`);
494
- return definition.tools;
495
- })
496
- )
497
- ];
280
+ function splitSentences(text) {
281
+ return text.replace(/\s+/g, " ").split(/(?<=[.!?])\s+(?=[A-Z(])/).map((s) => s.trim()).filter(Boolean);
498
282
  }
499
- function normalizeRuntimeToolName(name) {
500
- return TOOL_ALIASES.get(name) ?? name;
283
+ function isNarrativeSentence(sentence) {
284
+ return NARRATIVE_SENTENCE_STARTS.some((re) => re.test(sentence));
501
285
  }
502
- function runtimeCapabilityForTool(name) {
503
- return CAPABILITY_BY_TOOL.get(normalizeRuntimeToolName(name));
286
+ function salvageDirectiveTail(sentence) {
287
+ for (const re of NARRATIVE_SENTENCE_STARTS) {
288
+ const m = re.exec(sentence);
289
+ if (!m) continue;
290
+ const tail = sentence.slice(m[0].length).replace(/^[,\s]+/, "").trim();
291
+ if (!tail) return null;
292
+ if (looksDirective(tail)) return tail;
293
+ return null;
294
+ }
295
+ return null;
504
296
  }
505
- function isKnownRuntimeCapability(id) {
506
- return CAPABILITY_BY_ID.has(id);
297
+ var DIRECTIVE_VERB_PATTERN = /^(?:always|never|prefer|avoid|use|ensure|verify|check|run|execute|apply|adopt|choose|require|must|should|don'?t|do not|keep|maintain|set|define|specify|validate|confirm|inspect|review|handle|enable|disable|restrict|cap|limit|wrap|isolate|separate|combine|split|merge|refactor|rename|move|extract|inline)\b/i;
298
+ function looksDirective(text) {
299
+ if (DIRECTIVE_VERB_PATTERN.test(text)) return true;
300
+ if (/\b(?:is|are|must be|should be|required to)\s+(?:always|never|used|preferred)\b/i.test(text))
301
+ return true;
302
+ if (/\b(?:had to|has to|have to|needs? to|must|should|ought to)\s+\w+/i.test(text) && text.length > MIN_INSTRUCTIVE_LENGTH)
303
+ return true;
304
+ return false;
507
305
  }
508
- function inferRuntimeCapabilities(toolNames) {
509
- const ids = /* @__PURE__ */ new Set();
510
- for (const rawName of toolNames) {
511
- const id = runtimeCapabilityForTool(rawName);
512
- if (id) ids.add(id);
306
+ function truncateToInstructive(text, maxChars) {
307
+ const sentences = splitSentences(text);
308
+ let acc = "";
309
+ for (const s of sentences) {
310
+ const candidate = acc ? `${acc} ${s}` : s;
311
+ if (candidate.length > maxChars) break;
312
+ acc = candidate;
513
313
  }
514
- return [...ids];
314
+ if (acc) return acc;
315
+ return `${text.slice(0, maxChars - 1).trimEnd()}\u2026`;
515
316
  }
516
- function missingRuntimeCapabilities(required, toolNames) {
517
- if (!required || required.length === 0) return [];
518
- const available = new Set(inferRuntimeCapabilities(toolNames));
519
- return required.filter((id) => !CAPABILITY_BY_ID.has(id) || !available.has(id));
317
+ function classifyLearnedEntry(text) {
318
+ const lower = text.toLowerCase();
319
+ if (/\b(?:avoid|never|don'?t|do not|must not|prevent|watch out|beware|pitfall|gotcha|hazard|risk)\b/.test(
320
+ lower
321
+ ))
322
+ return "warning";
323
+ if (/\b(?:use|prefer|choose|adopt|leverage|apply|switch to|migrate to)\b/.test(lower) || /\b(?:pattern|approach|strategy|technique|idiom)\b/.test(lower))
324
+ return "pattern";
325
+ if (/\b(?:always|must|should|require|ensure|verify|check|run|execute|before|after|when)\b/.test(
326
+ lower
327
+ ))
328
+ return "convention";
329
+ return "fact";
520
330
  }
521
- function missingRequiredRuntimeTools(required, toolNames) {
522
- if (!required || required.length === 0) return [];
523
- const available = new Set(toolNames.map(normalizeRuntimeToolName));
524
- return required.map(normalizeRuntimeToolName).filter((name) => !CAPABILITY_BY_TOOL.has(name) || !available.has(name));
331
+ function normalizeForComparison(text) {
332
+ return text.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().split(/\s+/).sort().join(" ");
525
333
  }
526
- function runtimeToolReferencesFromText(text) {
527
- const references = /* @__PURE__ */ new Set();
528
- for (const match of text.matchAll(/`([a-z][a-z0-9_-]+)`/gi)) {
529
- const name = normalizeRuntimeToolName(match[1] ?? "");
530
- if (CAPABILITY_BY_TOOL.has(name)) references.add(name);
531
- }
532
- for (const pattern of [
533
- /\b(?:call|use|invoke|run)\s+`([a-z][a-z0-9_-]+)`/gi,
534
- /`([a-z][a-z0-9_-]+)`\s+tool\b/gi
535
- ]) {
536
- for (const match of text.matchAll(pattern)) {
537
- references.add(normalizeRuntimeToolName(match[1] ?? ""));
538
- }
539
- }
540
- return [...references];
334
+
335
+ // src/coordination/agents/project-agent-learning-structured.ts
336
+ function directiveTrials(entry) {
337
+ const count = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
338
+ const applied = count(entry.applied);
339
+ const wins = Math.min(applied, count(entry.wins));
340
+ return { applied, wins, losses: applied - wins };
541
341
  }
542
- function isDeprecatedRuntimeToolAlias(name) {
543
- return TOOL_ALIASES.has(name) || name.startsWith("mcp__");
342
+ function directiveUtility(entry) {
343
+ const { applied, wins } = directiveTrials(entry);
344
+ return (wins + 1) / (applied + 2);
544
345
  }
545
-
546
- // src/coordination/agents/project-agent-capture-window.ts
547
- var CAPTURE_COOLDOWN_MS = 12e4;
548
- var CAPTURE_MAX_PER_SESSION = 3;
549
- var CAPTURE_SESSION_WINDOW_MS = 30 * 6e4;
550
- var windows = /* @__PURE__ */ new Map();
551
- function currentWindow(key, now) {
552
- const existing = windows.get(key);
553
- if (!existing || now - existing.windowStartedAt >= CAPTURE_SESSION_WINDOW_MS) {
554
- const fresh = {
555
- windowStartedAt: now,
556
- count: 0,
557
- lastCaptureAt: existing?.lastCaptureAt
346
+ var DIRECTIVE_PROVEN_MIN_APPLIED = 5;
347
+ var DIRECTIVE_PROVEN_MIN_UTILITY = 0.7;
348
+ function isProvenDirective(entry) {
349
+ return directiveTrials(entry).applied >= DIRECTIVE_PROVEN_MIN_APPLIED && directiveUtility(entry) >= DIRECTIVE_PROVEN_MIN_UTILITY;
350
+ }
351
+ function parseLearnedEntryStamp(entry) {
352
+ const structuredMatch = entry.match(
353
+ /<!--\s*learned-stamp:\s*category=([\w-]+);\s*capturedAt=([^;]+?)\s*-->/
354
+ );
355
+ if (structuredMatch) {
356
+ const rawCategory = structuredMatch[1];
357
+ const candidate = parseLearnedCategory(rawCategory);
358
+ return {
359
+ capturedAt: typeof structuredMatch[2] === "string" ? structuredMatch[2].trim() : "",
360
+ category: candidate ?? null
558
361
  };
559
- windows.set(key, fresh);
560
- return fresh;
561
362
  }
562
- return existing;
563
- }
564
- function captureWindowState(key, now = Date.now()) {
565
- const window = currentWindow(key, now);
363
+ const stampMatch = entry.match(/^>\s*(?:\[\s*([\w-]+)\s*\]\s+)?(?:Captured|Taught)\s+(\S+)/m);
364
+ if (!stampMatch) {
365
+ return { capturedAt: "", category: null };
366
+ }
566
367
  return {
567
- count: window.count,
568
- lastCaptureAt: window.lastCaptureAt,
569
- remaining: Math.max(0, CAPTURE_MAX_PER_SESSION - window.count)
368
+ capturedAt: typeof stampMatch[2] === "string" ? stampMatch[2] : "",
369
+ category: parseLearnedCategory(stampMatch[1]) ?? null
570
370
  };
571
371
  }
572
- function recordCaptureAttempt(key, now = Date.now()) {
573
- const window = currentWindow(key, now);
574
- window.count += 1;
575
- window.lastCaptureAt = now;
576
- }
577
- function resetCaptureWindows() {
578
- windows.clear();
372
+ function parseLearnedCategory(value) {
373
+ return value === "convention" || value === "pattern" || value === "warning" || value === "fact" ? value : void 0;
579
374
  }
580
- function resetCaptureWindow(key) {
581
- windows.delete(key);
375
+ function stripStamp(entry) {
376
+ return entry.replace(/<!--\s*learned-stamp:[\s\S]*?-->/g, "").replace(/^>\s*(?:\[[\w-]+\]\s+)?(?:Captured|Taught)\s+.+$/m, "").replace(/\n{3,}/g, "\n\n").trim();
582
377
  }
583
-
584
- // src/coordination/agents/project-agent-config-io.ts
585
- import { readFileSync as readFileSync2 } from "node:fs";
586
- import * as path4 from "node:path";
587
-
588
- // src/coordination/agents/project-agent-config-validation.ts
589
- import * as path2 from "node:path";
590
- var PROJECT_AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
591
- "tools",
592
- "skillNames",
593
- "budget",
594
- "provider",
595
- "model",
596
- "allowedCapabilities",
597
- "modelPolicy",
598
- "fallbackProfile",
599
- "cwd",
600
- "worktree",
601
- "availability"
602
- ]);
603
- var PROJECT_AGENT_BUDGET_KEYS = /* @__PURE__ */ new Set([
604
- "timeoutMs",
605
- "idleTimeoutMs",
606
- "maxIterations",
607
- "maxToolCalls",
608
- "maxTokens",
609
- "maxCostUsd"
610
- ]);
611
- function isPlainRecord(value) {
612
- return typeof value === "object" && value !== null && !Array.isArray(value);
378
+ function directiveKey(text) {
379
+ return normalizeForComparison(text);
613
380
  }
614
- function assertStringList(value, field) {
615
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
616
- throw new Error(`Project agent config "${field}" must be an array of non-empty strings.`);
617
- }
381
+ function decomposeLearnedEntry(text, category) {
382
+ const what = text;
383
+ const why = deriveWhy(category, text);
384
+ const how = extractHow(text);
385
+ return { what, why, how };
618
386
  }
619
- function validateProjectAgentConfig(value) {
620
- if (!isPlainRecord(value)) throw new Error("Project agent config must be an object.");
621
- const unknownKey = Object.keys(value).find((key) => !PROJECT_AGENT_CONFIG_KEYS.has(key));
622
- if (unknownKey) throw new Error(`Unknown project agent config field: "${unknownKey}".`);
623
- if (value.tools !== void 0) assertStringList(value.tools, "tools");
624
- if (value.skillNames !== void 0) assertStringList(value.skillNames, "skillNames");
625
- if (value.allowedCapabilities !== void 0) {
626
- assertStringList(value.allowedCapabilities, "allowedCapabilities");
627
- }
628
- for (const field of ["provider", "model", "fallbackProfile"]) {
629
- const fieldValue = value[field];
630
- if (fieldValue !== void 0 && (typeof fieldValue !== "string" || !fieldValue.trim())) {
631
- throw new Error(`Project agent config "${field}" must be a non-empty string.`);
632
- }
633
- }
634
- if (value.cwd !== void 0) {
635
- if (typeof value.cwd !== "string" || !value.cwd.trim()) {
636
- throw new Error('Project agent config "cwd" must be a non-empty relative path.');
637
- }
638
- const cwd = value.cwd.replace(/\\/g, "/").trim();
639
- if (path2.posix.isAbsolute(cwd) || /^[a-z]:\//i.test(cwd) || cwd.split("/").includes("..")) {
640
- throw new Error('Project agent config "cwd" must stay inside the assigned checkout.');
641
- }
642
- value.cwd = cwd;
387
+ var WHY_BY_CATEGORY = {
388
+ convention: "Established convention for this codebase \u2014 skipping it risks regressions, merge friction, or out-of-sync state with peers.",
389
+ pattern: "This project's chosen approach \u2014 alternatives were considered and either conflict with existing architecture or were rejected for known reasons.",
390
+ warning: "Known failure mode \u2014 skipping this has caused real defects in this codebase. The cost of getting it wrong outweighs the cost of the check.",
391
+ fact: "Current state of the project \u2014 assumed by other conventions, build steps, or peers, so acting on a stale assumption wastes a cycle."
392
+ };
393
+ function deriveWhy(category, text) {
394
+ const base = WHY_BY_CATEGORY[category];
395
+ const signals = [];
396
+ const lower = text.toLowerCase();
397
+ if (/\bbefore\s+(?:merge|commit|deploy|release|shipping|publishing)\b/.test(lower))
398
+ signals.push("guard before shipping");
399
+ if (/\bto\s+avoid\b/.test(lower)) {
400
+ const m = text.match(/to avoid ([^.!?]+)/i);
401
+ if (m?.[1]) signals.push(`avoid ${m[1].trim()}`);
643
402
  }
644
- if (value.worktree !== void 0 && value.worktree !== true && value.worktree !== false && value.worktree !== "auto" && value.worktree !== "required" && value.worktree !== "off") {
645
- throw new Error('Project agent config "worktree" must be auto, required, off, or boolean.');
403
+ if (/\bso\s+(?:that|we|the project)\b/.test(lower)) {
404
+ const m = text.match(/so (?:that |we |the project )?([^.!?]+)/i);
405
+ if (m?.[1]) signals.push(`so ${m[1].trim()}`);
646
406
  }
647
- if (value.modelPolicy !== void 0) validateProjectAgentModelPolicy(value.modelPolicy);
648
- if (value.availability !== void 0) validateProjectAgentAvailability(value.availability);
649
- if (value.budget !== void 0) {
650
- if (!isPlainRecord(value.budget)) {
651
- throw new Error('Project agent config "budget" must be an object.');
652
- }
653
- const unknownBudgetKey = Object.keys(value.budget).find(
654
- (key) => !PROJECT_AGENT_BUDGET_KEYS.has(key)
655
- );
656
- if (unknownBudgetKey) {
657
- throw new Error(`Unknown project agent budget field: "${unknownBudgetKey}".`);
658
- }
659
- for (const [field, amount] of Object.entries(value.budget)) {
660
- if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
661
- throw new Error(`Project agent budget "${field}" must be a finite non-negative number.`);
662
- }
663
- if (field !== "maxCostUsd" && !Number.isInteger(amount)) {
664
- throw new Error(`Project agent budget "${field}" must be an integer.`);
665
- }
666
- }
407
+ if (signals.length === 0) return base;
408
+ return `${base} Project signals: ${signals.join("; ")}.`;
409
+ }
410
+ function extractHow(text) {
411
+ const anchors = /* @__PURE__ */ new Set();
412
+ const backticked = text.match(/`([^`]+)`/g) ?? [];
413
+ for (const raw of backticked) {
414
+ const inner = raw.replace(/`/g, "").trim();
415
+ if (inner.length > 0 && inner.length <= 120) anchors.add(inner);
667
416
  }
417
+ const pathMatches = text.match(
418
+ /(?:[a-zA-Z0-9_.-]+\/)+[a-zA-Z0-9_.-]+\.(?:tsx|jsonc|json|jsx|yaml|mjs|cjs|yml|ts|js|md)\b/g
419
+ ) ?? [];
420
+ for (const p of pathMatches) anchors.add(p);
421
+ const scoped = text.match(/@[a-z0-9][\w.-]*\/[a-z0-9][\w.-]*/gi) ?? [];
422
+ for (const p of scoped) anchors.add(p);
423
+ if (anchors.size === 0) return "";
424
+ return [...anchors].map((anchor) => `\`${anchor}\``).join("\n");
425
+ }
426
+ function cleanHowLine(line) {
427
+ let value = line.trim();
428
+ let previous;
429
+ do {
430
+ previous = value;
431
+ value = value.replace(/^[-*]\s+/, "").replace(/^\*How:\*\s*/i, "").trim();
432
+ } while (value !== previous);
668
433
  return value;
669
434
  }
670
- function validateProjectAgentModelPolicy(value) {
671
- if (!isPlainRecord(value)) throw new Error("Project agent modelPolicy must be an object.");
672
- const allowedKeys = /* @__PURE__ */ new Set(["allowed", "fallbacks", "strict"]);
673
- const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key));
674
- if (unknownKey) throw new Error(`Unknown modelPolicy field: "${unknownKey}".`);
675
- const validateTargets = (targets, field, allowEmpty) => {
676
- if (!Array.isArray(targets) || !allowEmpty && targets.length === 0) {
677
- throw new Error(`Project agent modelPolicy "${field}" must be a non-empty array.`);
678
- }
679
- for (const target of targets) {
680
- if (!isPlainRecord(target) || typeof target.provider !== "string" || !target.provider.trim() || typeof target.model !== "string" || !target.model.trim()) {
681
- throw new Error(`Project agent modelPolicy "${field}" contains an invalid target.`);
682
- }
683
- }
684
- };
685
- validateTargets(value.allowed, "allowed", false);
686
- if (value.fallbacks !== void 0) {
687
- validateTargets(value.fallbacks, "fallbacks", true);
688
- const allowed = new Set(
689
- value.allowed.map(
690
- (target) => `${target.provider}\0${target.model}`
691
- )
692
- );
693
- for (const target of value.fallbacks) {
694
- if (!allowed.has(`${target.provider}\0${target.model}`)) {
695
- throw new Error("Every modelPolicy fallback must also appear in allowed.");
696
- }
697
- }
698
- }
699
- if (value.strict !== void 0 && typeof value.strict !== "boolean") {
700
- throw new Error('Project agent modelPolicy "strict" must be boolean.');
435
+ function parseStampAttributes(body) {
436
+ const attributes = {};
437
+ for (const part of body.split(";")) {
438
+ const eq = part.indexOf("=");
439
+ if (eq === -1) continue;
440
+ const key = part.slice(0, eq).trim();
441
+ const value = part.slice(eq + 1).trim();
442
+ if (key) attributes[key] = value;
701
443
  }
444
+ return attributes;
702
445
  }
703
- function validateProjectAgentAvailability(value) {
704
- if (!isPlainRecord(value)) throw new Error("Project agent availability must be an object.");
705
- const allowedKeys = /* @__PURE__ */ new Set(["timezone", "days", "start", "end", "mode"]);
706
- const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key));
707
- if (unknownKey) throw new Error(`Unknown availability field: "${unknownKey}".`);
708
- if (typeof value.timezone !== "string" || !value.timezone.trim()) {
709
- throw new Error("Project agent availability requires a timezone.");
446
+ function parseEntryBody(body) {
447
+ const lines = [];
448
+ for (const line of body.split("\n")) {
449
+ if (/^##\s/.test(line) || /^---\s*$/.test(line) || /^<!--/.test(line)) break;
450
+ lines.push(line);
710
451
  }
711
- try {
712
- new Intl.DateTimeFormat("en-US", { timeZone: value.timezone }).format();
713
- } catch {
714
- throw new Error(`Invalid project agent availability timezone: "${value.timezone}".`);
452
+ const text = lines.join("\n");
453
+ const what = /^-\s+\*\*([\s\S]+?)\*\*\s*$/m.exec(text)?.[1]?.replace(/\s+/g, " ").trim() ?? "";
454
+ if (!what) return void 0;
455
+ const why = /^\s+-\s+\*Why:\*\s+(.+)$/m.exec(text)?.[1]?.trim() ?? "";
456
+ const how = lines.filter((line) => /^\s+-\s+\*How:\*/i.test(line)).map((line) => cleanHowLine(line)).filter(Boolean).join("\n");
457
+ return { what, why, how };
458
+ }
459
+ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLearnedEntries(raw)) {
460
+ if (!raw) return [];
461
+ const structured = [];
462
+ const stampPattern = /<!--\s*learned-stamp:\s*([^>]*?)\s*-->/g;
463
+ const stamps = [...raw.matchAll(stampPattern)];
464
+ for (const [index, stamp] of stamps.entries()) {
465
+ const attributes = parseStampAttributes(stamp[1] ?? "");
466
+ const category = parseLearnedCategory(attributes["category"]) ?? "fact";
467
+ const capturedAt = attributes["capturedAt"] ?? "";
468
+ const skill = attributes["skill"];
469
+ const trials = directiveTrials({
470
+ applied: Number(attributes["applied"]),
471
+ wins: Number(attributes["wins"])
472
+ });
473
+ const start = (stamp.index ?? 0) + stamp[0].length;
474
+ const end = stamps[index + 1]?.index ?? raw.length;
475
+ const parsed = parseEntryBody(raw.slice(start, end));
476
+ if (!parsed || parsed.what.length < MIN_INSTRUCTIVE_LENGTH) continue;
477
+ structured.push({
478
+ key: directiveKey(parsed.what),
479
+ category,
480
+ what: parsed.what,
481
+ why: parsed.why || WHY_BY_CATEGORY[category],
482
+ how: parsed.how,
483
+ capturedAt,
484
+ ...skill ? { skill } : {},
485
+ ...trials.applied > 0 ? { applied: trials.applied, wins: trials.wins } : {}
486
+ });
715
487
  }
716
- if (!Array.isArray(value.days) || value.days.length === 0 || value.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) {
717
- throw new Error("Project agent availability days must contain weekday numbers 0-6.");
488
+ if (structured.length === 0) {
489
+ for (const chunk of legacyEntries) {
490
+ const stamp = parseLearnedEntryStamp(chunk);
491
+ const directive = stamp.capturedAt || stamp.category ? stripStamp(chunk) : chunk;
492
+ if (directive.length < MIN_INSTRUCTIVE_LENGTH) continue;
493
+ const category = stamp.category ?? classifyLearnedEntry(directive);
494
+ const { what, why, how } = decomposeLearnedEntry(directive, category);
495
+ structured.push({
496
+ key: directiveKey(directive),
497
+ category,
498
+ what,
499
+ why,
500
+ how,
501
+ capturedAt: stamp.capturedAt
502
+ });
503
+ }
718
504
  }
719
- if (typeof value.start !== "string" || typeof value.end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(value.start) || !/^([01]\d|2[0-3]):[0-5]\d$/.test(value.end)) {
720
- throw new Error("Project agent availability start/end must use HH:MM.");
505
+ return structured;
506
+ }
507
+ function mergeStructuredEntries(existing, fresh) {
508
+ const { what, why, how } = decomposeLearnedEntry(fresh.text, fresh.category);
509
+ const key = directiveKey(fresh.text);
510
+ const overlapping = existing.filter((entry) => tokenOverlap(entry.key, key) >= 0.55);
511
+ const proven = overlapping.find(
512
+ (entry) => entry.category === fresh.category && isProvenDirective(entry)
513
+ );
514
+ if (proven) return sortStructuredEntries([...existing]);
515
+ const ancestor = overlapping.filter((entry) => entry.category === fresh.category).sort((a, b) => directiveTrials(b).applied - directiveTrials(a).applied)[0];
516
+ const inherited = ancestor ? directiveTrials(ancestor) : { applied: 0, wins: 0 };
517
+ const freshEntry = {
518
+ key,
519
+ category: fresh.category,
520
+ what,
521
+ why,
522
+ how,
523
+ capturedAt: fresh.capturedAt,
524
+ ...fresh.skill ? { skill: fresh.skill } : {},
525
+ ...inherited.applied > 0 ? { applied: inherited.applied, wins: inherited.wins } : {}
526
+ };
527
+ const merged = existing.filter(
528
+ (entry) => entry.category !== fresh.category || tokenOverlap(entry.key, key) < 0.55
529
+ );
530
+ merged.push(freshEntry);
531
+ return sortStructuredEntries(merged);
532
+ }
533
+ function sortStructuredEntries(merged) {
534
+ const order = {
535
+ warning: 0,
536
+ convention: 1,
537
+ pattern: 2,
538
+ fact: 3
539
+ };
540
+ merged.sort((a, b) => {
541
+ const cmp = order[a.category] - order[b.category];
542
+ if (cmp !== 0) return cmp;
543
+ return a.what.localeCompare(b.what);
544
+ });
545
+ return merged;
546
+ }
547
+ var SECTION_TITLE = {
548
+ warning: "## What to avoid",
549
+ convention: "## What to do",
550
+ pattern: "## Patterns to follow",
551
+ fact: "## Project facts"
552
+ };
553
+ function renderLearnedInstructions(role, entries, capturedAt) {
554
+ const headerLines = [
555
+ `# Learned instructions for \`${role}\``,
556
+ "",
557
+ "> Project-specific learning data for the `" + role + "` agent. Each entry is a directive \u2014 read it as an instruction, not a journal entry. Entries are re-derived on every capture, so this file is always a current, structured snapshot of what this agent has learned.",
558
+ ""
559
+ ];
560
+ if (entries.length === 0) {
561
+ return headerLines.join("\n") + [
562
+ "_No learned entries yet._",
563
+ "",
564
+ "---",
565
+ `*Last capture: ${capturedAt} \xB7 0 entries*`,
566
+ ""
567
+ ].join("\n");
721
568
  }
722
- if (value.mode !== void 0 && value.mode !== "advisory" && value.mode !== "enforce") {
723
- throw new Error("Project agent availability mode must be advisory or enforce.");
569
+ const buckets = {
570
+ warning: [],
571
+ convention: [],
572
+ pattern: [],
573
+ fact: []
574
+ };
575
+ for (const entry of entries) buckets[entry.category].push(entry);
576
+ const sections = [];
577
+ for (const category of ["warning", "convention", "pattern", "fact"]) {
578
+ const list = buckets[category];
579
+ if (list.length === 0) continue;
580
+ sections.push(SECTION_TITLE[category]);
581
+ sections.push("");
582
+ for (const entry of list) {
583
+ const trials = directiveTrials(entry);
584
+ const attributes = [
585
+ `category=${entry.category}`,
586
+ `capturedAt=${entry.capturedAt}`,
587
+ ...entry.skill ? [`skill=${entry.skill}`] : [],
588
+ // Zero counters are omitted so an entry with no track record renders
589
+ // exactly as it did before this field existed — the parse→render
590
+ // fixed point holds for every pre-existing buffer on disk.
591
+ ...trials.applied > 0 ? [`applied=${trials.applied}`, `wins=${trials.wins}`] : []
592
+ ].join("; ");
593
+ sections.push(`<!-- learned-stamp: ${attributes} -->`);
594
+ sections.push(`- **${entry.what}**`);
595
+ sections.push(` - *Why:* ${entry.why}`);
596
+ for (const line of entry.how.split("\n").map(cleanHowLine).filter(Boolean)) {
597
+ sections.push(` - *How:* ${line}`);
598
+ }
599
+ sections.push("");
600
+ }
601
+ }
602
+ const footer = ["---", `*Last capture: ${capturedAt} \xB7 ${entries.length} entries*`, ""];
603
+ return [...headerLines, ...sections, ...footer].join("\n");
604
+ }
605
+ var DROP_PRIORITY = {
606
+ fact: 0,
607
+ pattern: 1,
608
+ convention: 2,
609
+ warning: 3
610
+ };
611
+ function enforceLearnedBudget(entries, capturedAt, maxBytes, role = "agent") {
612
+ const kept = [...entries];
613
+ const dropped = [];
614
+ const size = (list) => Buffer.byteLength(renderLearnedInstructions(role, list, capturedAt), "utf8");
615
+ const UTILITY_EPSILON = 1e-9;
616
+ while (kept.length > 1 && size(kept) > maxBytes) {
617
+ let victimIndex = 0;
618
+ for (let i = 1; i < kept.length; i++) {
619
+ const a = kept[i];
620
+ const b = kept[victimIndex];
621
+ const byPriority = DROP_PRIORITY[a.category] - DROP_PRIORITY[b.category];
622
+ if (byPriority !== 0) {
623
+ if (byPriority < 0) victimIndex = i;
624
+ continue;
625
+ }
626
+ const byUtility = directiveUtility(a) - directiveUtility(b);
627
+ if (Math.abs(byUtility) > UTILITY_EPSILON) {
628
+ if (byUtility < 0) victimIndex = i;
629
+ continue;
630
+ }
631
+ if (a.capturedAt < b.capturedAt) victimIndex = i;
632
+ }
633
+ dropped.push(...kept.splice(victimIndex, 1));
724
634
  }
635
+ return { kept, dropped };
725
636
  }
726
637
 
727
638
  // src/coordination/agents/project-agent-paths.ts
728
639
  import { randomUUID } from "node:crypto";
729
640
  import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
730
- import * as path3 from "node:path";
641
+ import * as path2 from "node:path";
731
642
  var AGENT_ROLE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,95}$/i;
732
643
  function assertProjectAgentRole(role) {
733
644
  const normalized = role.trim();
@@ -741,13 +652,13 @@ function isProjectAgentRoleName(name) {
741
652
  }
742
653
  function agentsDir(projectRoot) {
743
654
  const root = projectRoot ?? process.cwd();
744
- return path3.join(root, ".wrongstack", "agents");
655
+ return path2.join(root, ".wrongstack", "agents");
745
656
  }
746
657
  function roleDir(role, projectRoot) {
747
- return path3.join(agentsDir(projectRoot), assertProjectAgentRole(role));
658
+ return path2.join(agentsDir(projectRoot), assertProjectAgentRole(role));
748
659
  }
749
660
  function writeTextAtomically(filePath, content) {
750
- mkdirSync(path3.dirname(filePath), { recursive: true });
661
+ mkdirSync(path2.dirname(filePath), { recursive: true });
751
662
  const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
752
663
  try {
753
664
  writeFileSync(temporaryPath, content, "utf8");
@@ -760,959 +671,957 @@ function writeTextAtomically(filePath, content) {
760
671
  }
761
672
  }
762
673
  function learningPolicyPath(role, projectRoot) {
763
- return path3.join(roleDir(role, projectRoot), "learning.json");
674
+ return path2.join(roleDir(role, projectRoot), "learning.json");
764
675
  }
765
676
  function projectAgentProfilePath(role, projectRoot) {
766
- return path3.join(roleDir(role, projectRoot), "profile.json");
677
+ return path2.join(roleDir(role, projectRoot), "profile.json");
767
678
  }
768
679
 
769
- // src/coordination/agents/project-agent-config-io.ts
770
- function loadProjectAgentConfig(role, projectRoot) {
771
- const cfgPath = path4.join(roleDir(role, projectRoot), "config.json");
680
+ // src/coordination/agents/project-agent-quarantine.ts
681
+ import { readFileSync as readFileSync2 } from "node:fs";
682
+ import * as path3 from "node:path";
683
+ var QUARANTINE_MAX_BYTES = 64 * 1024;
684
+ var RETIRED_MATCH_THRESHOLD = 0.55;
685
+ function quarantinePath(role, projectRoot) {
686
+ return path3.join(roleDir(role, projectRoot), "quarantine.md");
687
+ }
688
+ function readTextOrEmpty(filePath) {
772
689
  try {
773
- return validateProjectAgentConfig(JSON.parse(readFileSync2(cfgPath, "utf8")));
690
+ return readFileSync2(filePath, "utf8");
774
691
  } catch {
775
- return void 0;
692
+ return "";
776
693
  }
777
694
  }
695
+ function appendQuarantine(role, retired, at, projectRoot) {
696
+ if (retired.length === 0) return;
697
+ const filePath = quarantinePath(role, projectRoot);
698
+ const existing = readTextOrEmpty(filePath);
699
+ const header = existing ? "" : `# Retired directives for \`${role}\`
778
700
 
779
- // src/coordination/agents/project-agent-consolidation.ts
780
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
781
- import * as path6 from "node:path";
782
-
783
- // src/coordination/agents/project-agent-learning-entries.ts
784
- function tokenOverlap(a, b) {
785
- const setA = new Set(a.split(/\s+/));
786
- const setB = new Set(b.split(/\s+/));
787
- const intersect = new Set([...setA].filter((t) => setB.has(t)));
788
- const union = /* @__PURE__ */ new Set([...setA, ...setB]);
789
- return union.size === 0 ? 0 : intersect.size / union.size;
790
- }
791
- function splitLearnedEntries(body) {
792
- return body.split(/\n---\n+/).map((entry) => entry.trim()).map(
793
- (entry) => entry.replace(/^#\s+Learned (?:wisdom|instructions) for .+$/im, "").replace(/^>\s*Project-specific learning data for[\s\S]*?$/im, "").replace(/^_No learned entries yet\._$/im, "").replace(/^\*Last capture:[^\n]*\*$/im, "").replace(/<!--[\s\S]*?-->/g, "").trim()
794
- ).filter(Boolean);
795
- }
701
+ > Directives that were exercised repeatedly and kept correlating with
702
+ > failure. They are no longer injected anywhere. Kept for audit \u2014 a directive
703
+ > can be right about a project that has since changed.
796
704
 
797
- // src/coordination/agents/project-agent-learning-normalize.ts
798
- var LEARNED_SOFT_LIMIT = 8192;
799
- var LEARNED_HARD_LIMIT = 16384;
800
- var LEARNED_ENTRY_MAX_CHARS = 600;
801
- var MIN_INSTRUCTIVE_LENGTH = 30;
802
- var NARRATIVE_SENTENCE_STARTS = [
803
- /^(?:when|while|during|after|before|once|since)\s+(?:i|we|the agent|i was|i were|i had)\b/i,
804
- /^(?:i|we)\s+(?:found|noticed|learned|discovered|realized|saw|encountered|hit|ran into)\b/i,
805
- /^(?:i|we)\s+(?:was|were|had|did|ran|tried|attempted|used|modified|changed|updated)\b/i,
806
- /^(?:this|that|the)\s+(?:task|session|run|commit|change|pr|pull request|invocation)\b/i,
807
- /^(?:in|on|for|at|during)\s+(?:this|that|the)\s+(?:task|session|run|commit|change|pr)\b/i,
808
- /^(?:today|yesterday|earlier|just now|recently|lately)\b/i
809
- ];
810
- var EPHEMERAL_PATTERNS = [
811
- // Commit SHAs. A bare `[0-9a-f]{7,40}` also matches ordinary English words
812
- // spelled entirely from hex letters ("defaced", "acceded", "effaced"), so
813
- // require at least one digit — every real abbreviated SHA has one in
814
- // practice, and the false-positive class disappears.
815
- { pattern: /\b(?=[0-9a-f]{7,40}\b)[a-f]*\d[0-9a-f]*\b/g, replacement: "" },
816
- {
817
- pattern: /\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b/g,
818
- replacement: ""
819
- },
820
- {
821
- pattern: /(?<=\b(?:on|from|since|before|after|dated|until)\s)\d{4}-\d{2}-\d{2}\b/gi,
822
- replacement: ""
823
- },
824
- { pattern: /\b(?:line|lines?)\s+\d+(?:\s*[-–]\s*\d+)?/gi, replacement: "" },
825
- { pattern: /#\s*(?:PR|issue|MR)\s*\d+/gi, replacement: "" },
826
- { pattern: /(?<=\s)#\d{1,6}\b/g, replacement: "" }
827
- ];
828
- function normalizeLearnedEntry(raw) {
829
- if (!raw) return null;
830
- let cleaned = raw;
831
- for (const { pattern, replacement } of EPHEMERAL_PATTERNS) {
832
- cleaned = cleaned.replace(pattern, replacement);
705
+ `;
706
+ const block = retired.map((entry) => {
707
+ const { applied, wins, losses } = directiveTrials(entry);
708
+ const attributes = [
709
+ `at=${at}`,
710
+ `category=${entry.category}`,
711
+ ...entry.skill ? [`skill=${entry.skill}`] : [],
712
+ `applied=${applied}`,
713
+ `wins=${wins}`
714
+ ].join("; ");
715
+ return [
716
+ `<!-- quarantined: ${attributes} -->`,
717
+ `- **${entry.what}**`,
718
+ ` - *Record:* ${wins} succeeded / ${losses} failed of ${applied} applied`,
719
+ ""
720
+ ].join("\n");
721
+ }).join("\n");
722
+ let next = `${existing}${header}${block}`;
723
+ if (Buffer.byteLength(next, "utf8") > QUARANTINE_MAX_BYTES) {
724
+ const marker = "<!-- quarantined:";
725
+ const overflow = Buffer.byteLength(next, "utf8") - QUARANTINE_MAX_BYTES;
726
+ const cut = next.indexOf(marker, overflow);
727
+ next = cut === -1 ? block : next.slice(cut);
833
728
  }
834
- const sentences = splitSentences(cleaned);
835
- const directiveSentences = [];
836
- let hadSubstantiveNarrative = false;
837
- for (const sentence of sentences) {
838
- const trimmed = sentence.trim();
839
- if (!trimmed) continue;
840
- if (isNarrativeSentence(trimmed)) {
841
- hadSubstantiveNarrative = true;
842
- const salvaged = salvageDirectiveTail(trimmed);
843
- if (salvaged) directiveSentences.push(salvaged);
844
- continue;
729
+ writeTextAtomically(filePath, next);
730
+ }
731
+ function readQuarantinedDirectives(role, projectRoot) {
732
+ const raw = readTextOrEmpty(quarantinePath(role, projectRoot));
733
+ if (!raw) return [];
734
+ const out = [];
735
+ const stamps = [...raw.matchAll(/<!--\s*quarantined:\s*([^>]*?)\s*-->/g)];
736
+ for (const [index, stamp] of stamps.entries()) {
737
+ const attributes = {};
738
+ for (const part of (stamp[1] ?? "").split(";")) {
739
+ const eq = part.indexOf("=");
740
+ if (eq === -1) continue;
741
+ attributes[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
845
742
  }
846
- directiveSentences.push(trimmed);
847
- }
848
- if (directiveSentences.length === 0) return null;
849
- let normalized = directiveSentences.join(" ").replace(/\s+/g, " ").trim();
850
- if (normalized.length > LEARNED_ENTRY_MAX_CHARS) {
851
- normalized = truncateToInstructive(normalized, LEARNED_ENTRY_MAX_CHARS);
852
- if (normalized.length < MIN_INSTRUCTIVE_LENGTH) return null;
853
- }
854
- if (normalized.length < MIN_INSTRUCTIVE_LENGTH) return null;
855
- if (hadSubstantiveNarrative && directiveSentences.length < sentences.length / 2) {
856
- if (normalized.length < MIN_INSTRUCTIVE_LENGTH * 2) return null;
743
+ const start = (stamp.index ?? 0) + stamp[0].length;
744
+ const end = stamps[index + 1]?.index ?? raw.length;
745
+ const what = /^-\s+\*\*([\s\S]+?)\*\*\s*$/m.exec(raw.slice(start, end))?.[1]?.replace(/\s+/g, " ").trim();
746
+ if (!what) continue;
747
+ const skill = attributes["skill"];
748
+ out.push({ what, ...skill ? { skill } : {} });
857
749
  }
858
- return {
859
- text: normalized,
860
- category: classifyLearnedEntry(normalized)
861
- };
750
+ return out;
862
751
  }
863
- function splitSentences(text) {
864
- return text.replace(/\s+/g, " ").split(/(?<=[.!?])\s+(?=[A-Z(])/).map((s) => s.trim()).filter(Boolean);
752
+ function bareClaim(line) {
753
+ return line.replace(/^\s*[-*+]\s+/, "").replace(/^\s*\d+[.)]\s+/, "").replace(/\*\*/g, "").replace(/`/g, "").trim();
865
754
  }
866
- function isNarrativeSentence(sentence) {
867
- return NARRATIVE_SENTENCE_STARTS.some((re) => re.test(sentence));
755
+ function indentOf(line) {
756
+ return line.length - line.trimStart().length;
868
757
  }
869
- function salvageDirectiveTail(sentence) {
870
- for (const re of NARRATIVE_SENTENCE_STARTS) {
871
- const m = re.exec(sentence);
872
- if (!m) continue;
873
- const tail = sentence.slice(m[0].length).replace(/^[,\s]+/, "").trim();
874
- if (!tail) return null;
875
- if (looksDirective(tail)) return tail;
876
- return null;
758
+ function scrubRetiredLines(text, retired) {
759
+ if (!text.trim() || retired.length === 0) return text;
760
+ const keys = retired.map((what) => normalizeForComparison(what));
761
+ const lines = text.split("\n");
762
+ const kept = [];
763
+ for (let i = 0; i < lines.length; i++) {
764
+ const line = lines[i];
765
+ const claim = bareClaim(line);
766
+ if (claim.length >= MIN_INSTRUCTIVE_LENGTH && keys.some(
767
+ (key) => tokenOverlap(key, normalizeForComparison(claim)) >= RETIRED_MATCH_THRESHOLD
768
+ )) {
769
+ const base = indentOf(line);
770
+ while (i + 1 < lines.length) {
771
+ const next = lines[i + 1];
772
+ if (!next.trim() || indentOf(next) <= base) break;
773
+ i++;
774
+ }
775
+ continue;
776
+ }
777
+ kept.push(line);
877
778
  }
878
- return null;
779
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
879
780
  }
880
- var DIRECTIVE_VERB_PATTERN = /^(?:always|never|prefer|avoid|use|ensure|verify|check|run|execute|apply|adopt|choose|require|must|should|don'?t|do not|keep|maintain|set|define|specify|validate|confirm|inspect|review|handle|enable|disable|restrict|cap|limit|wrap|isolate|separate|combine|split|merge|refactor|rename|move|extract|inline)\b/i;
881
- function looksDirective(text) {
882
- if (DIRECTIVE_VERB_PATTERN.test(text)) return true;
883
- if (/\b(?:is|are|must be|should be|required to)\s+(?:always|never|used|preferred)\b/i.test(text))
884
- return true;
885
- if (/\b(?:had to|has to|have to|needs? to|must|should|ought to)\s+\w+/i.test(text) && text.length > MIN_INSTRUCTIVE_LENGTH)
886
- return true;
887
- return false;
781
+ function hasDirectiveContent(text) {
782
+ return text.split("\n").some((line) => {
783
+ const trimmed = line.trimStart();
784
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(">")) return false;
785
+ if (/^-{3,}\s*$/.test(trimmed)) return false;
786
+ if (/^\*[^*].*\*$/.test(trimmed)) return false;
787
+ return bareClaim(line).length >= MIN_INSTRUCTIVE_LENGTH;
788
+ });
888
789
  }
889
- function truncateToInstructive(text, maxChars) {
890
- const sentences = splitSentences(text);
891
- let acc = "";
892
- for (const s of sentences) {
893
- const candidate = acc ? `${acc} ${s}` : s;
894
- if (candidate.length > maxChars) break;
895
- acc = candidate;
896
- }
897
- if (acc) return acc;
898
- return `${text.slice(0, maxChars - 1).trimEnd()}\u2026`;
790
+ function retiredDirectivesToWarnAbout(role, currentDirectives, projectRoot, skill) {
791
+ const currentKeys = currentDirectives.map((text) => normalizeForComparison(text));
792
+ return readQuarantinedDirectives(role, projectRoot).filter((entry) => skill ? entry.skill === skill : true).map((entry) => entry.what).filter(
793
+ (what) => !currentKeys.some(
794
+ (key) => tokenOverlap(key, normalizeForComparison(what)) >= RETIRED_MATCH_THRESHOLD
795
+ )
796
+ );
899
797
  }
900
- function classifyLearnedEntry(text) {
901
- const lower = text.toLowerCase();
902
- if (/\b(?:avoid|never|don'?t|do not|must not|prevent|watch out|beware|pitfall|gotcha|hazard|risk)\b/.test(
903
- lower
904
- ))
905
- return "warning";
906
- if (/\b(?:use|prefer|choose|adopt|leverage|apply|switch to|migrate to)\b/.test(lower) || /\b(?:pattern|approach|strategy|technique|idiom)\b/.test(lower))
907
- return "pattern";
908
- if (/\b(?:always|must|should|require|ensure|verify|check|run|execute|before|after|when)\b/.test(
909
- lower
910
- ))
911
- return "convention";
912
- return "fact";
798
+
799
+ // src/coordination/agents/project-agent-consolidation.ts
800
+ function learnedPath(role, projectRoot) {
801
+ return path4.join(roleDir(role, projectRoot), "learned.md");
913
802
  }
914
- function normalizeForComparison(text) {
915
- return text.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().split(/\s+/).sort().join(" ");
803
+ function loadProjectAgentLearnedText(role, projectRoot) {
804
+ try {
805
+ return readFileSync3(learnedPath(role, projectRoot), "utf8").trim();
806
+ } catch {
807
+ return "";
808
+ }
916
809
  }
917
-
918
- // src/coordination/agents/project-agent-learning-structured.ts
919
- function directiveTrials(entry) {
920
- const count = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
921
- const applied = count(entry.applied);
922
- const wins = Math.min(applied, count(entry.wins));
923
- return { applied, wins, losses: applied - wins };
810
+ function readRawLearnedEntries(role, projectRoot) {
811
+ const raw = loadProjectAgentLearnedText(role, projectRoot);
812
+ return parseStructuredLearnedEntriesFromContent(raw, splitLearnedEntries(raw));
924
813
  }
925
- function directiveUtility(entry) {
926
- const { applied, wins } = directiveTrials(entry);
927
- return (wins + 1) / (applied + 2);
814
+ function consolidationPath(role, projectRoot) {
815
+ return path4.join(roleDir(role, projectRoot), "consolidated.md");
928
816
  }
929
- var DIRECTIVE_PROVEN_MIN_APPLIED = 5;
930
- var DIRECTIVE_PROVEN_MIN_UTILITY = 0.7;
931
- function isProvenDirective(entry) {
932
- return directiveTrials(entry).applied >= DIRECTIVE_PROVEN_MIN_APPLIED && directiveUtility(entry) >= DIRECTIVE_PROVEN_MIN_UTILITY;
817
+ function consolidatedDocumentPath(role, projectRoot) {
818
+ return consolidationPath(assertProjectAgentRole(role), projectRoot);
933
819
  }
934
- function parseLearnedEntryStamp(entry) {
935
- const structuredMatch = entry.match(
936
- /<!--\s*learned-stamp:\s*category=([\w-]+);\s*capturedAt=([^;]+?)\s*-->/
937
- );
938
- if (structuredMatch) {
939
- const rawCategory = structuredMatch[1];
940
- const candidate = parseLearnedCategory(rawCategory);
941
- return {
942
- capturedAt: typeof structuredMatch[2] === "string" ? structuredMatch[2].trim() : "",
943
- category: candidate ?? null
944
- };
945
- }
946
- const stampMatch = entry.match(/^>\s*(?:\[\s*([\w-]+)\s*\]\s+)?(?:Captured|Taught)\s+(\S+)/m);
947
- if (!stampMatch) {
948
- return { capturedAt: "", category: null };
949
- }
950
- return {
951
- capturedAt: typeof stampMatch[2] === "string" ? stampMatch[2] : "",
952
- category: parseLearnedCategory(stampMatch[1]) ?? null
953
- };
820
+ function consolidationMetaPath(role, projectRoot) {
821
+ return path4.join(roleDir(role, projectRoot), "consolidation.json");
954
822
  }
955
- function parseLearnedCategory(value) {
956
- return value === "convention" || value === "pattern" || value === "warning" || value === "fact" ? value : void 0;
823
+ function archivePath(role, at, projectRoot) {
824
+ return path4.join(roleDir(role, projectRoot), "archive", `learned-${at.replace(/[:.]/g, "-")}.md`);
957
825
  }
958
- function stripStamp(entry) {
959
- return entry.replace(/<!--\s*learned-stamp:[\s\S]*?-->/g, "").replace(/^>\s*(?:\[[\w-]+\]\s+)?(?:Captured|Taught)\s+.+$/m, "").replace(/\n{3,}/g, "\n\n").trim();
826
+ function loadProjectAgentConsolidated(role, projectRoot) {
827
+ try {
828
+ return readFileSync3(consolidationPath(role, projectRoot), "utf8").trim();
829
+ } catch {
830
+ return "";
831
+ }
960
832
  }
961
- function directiveKey(text) {
962
- return normalizeForComparison(text);
833
+ function loadConsolidationMetadata(role, projectRoot) {
834
+ try {
835
+ const raw = readFileSync3(consolidationMetaPath(role, projectRoot), "utf8");
836
+ const parsed = JSON.parse(raw);
837
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.consolidatedAt === "string" && typeof parsed.sourceEntryCount === "number") {
838
+ return parsed;
839
+ }
840
+ return void 0;
841
+ } catch {
842
+ return void 0;
843
+ }
963
844
  }
964
- function decomposeLearnedEntry(text, category) {
965
- const what = text;
966
- const why = deriveWhy(category, text);
967
- const how = extractHow(text);
968
- return { what, why, how };
845
+ function isConsolidated(role, projectRoot) {
846
+ return existsSync(consolidationMetaPath(role, projectRoot));
969
847
  }
970
- var WHY_BY_CATEGORY = {
971
- convention: "Established convention for this codebase \u2014 skipping it risks regressions, merge friction, or out-of-sync state with peers.",
972
- pattern: "This project's chosen approach \u2014 alternatives were considered and either conflict with existing architecture or were rejected for known reasons.",
973
- warning: "Known failure mode \u2014 skipping this has caused real defects in this codebase. The cost of getting it wrong outweighs the cost of the check.",
974
- fact: "Current state of the project \u2014 assumed by other conventions, build steps, or peers, so acting on a stale assumption wastes a cycle."
975
- };
976
- function deriveWhy(category, text) {
977
- const base = WHY_BY_CATEGORY[category];
978
- const signals = [];
979
- const lower = text.toLowerCase();
980
- if (/\bbefore\s+(?:merge|commit|deploy|release|shipping|publishing)\b/.test(lower))
981
- signals.push("guard before shipping");
982
- if (/\bto\s+avoid\b/.test(lower)) {
983
- const m = text.match(/to avoid ([^.!?]+)/i);
984
- if (m?.[1]) signals.push(`avoid ${m[1].trim()}`);
985
- }
986
- if (/\bso\s+(?:that|we|the project)\b/.test(lower)) {
987
- const m = text.match(/so (?:that |we |the project )?([^.!?]+)/i);
988
- if (m?.[1]) signals.push(`so ${m[1].trim()}`);
989
- }
990
- if (signals.length === 0) return base;
991
- return `${base} Project signals: ${signals.join("; ")}.`;
992
- }
993
- function extractHow(text) {
994
- const anchors = /* @__PURE__ */ new Set();
995
- const backticked = text.match(/`([^`]+)`/g) ?? [];
996
- for (const raw of backticked) {
997
- const inner = raw.replace(/`/g, "").trim();
998
- if (inner.length > 0 && inner.length <= 120) anchors.add(inner);
999
- }
1000
- const pathMatches = text.match(
1001
- /(?:[a-zA-Z0-9_.-]+\/)+[a-zA-Z0-9_.-]+\.(?:tsx|jsonc|json|jsx|yaml|mjs|cjs|yml|ts|js|md)\b/g
1002
- ) ?? [];
1003
- for (const p of pathMatches) anchors.add(p);
1004
- const scoped = text.match(/@[a-z0-9][\w.-]*\/[a-z0-9][\w.-]*/gi) ?? [];
1005
- for (const p of scoped) anchors.add(p);
1006
- if (anchors.size === 0) return "";
1007
- return [...anchors].map((anchor) => `\`${anchor}\``).join("\n");
1008
- }
1009
- function cleanHowLine(line) {
1010
- let value = line.trim();
1011
- let previous;
1012
- do {
1013
- previous = value;
1014
- value = value.replace(/^[-*]\s+/, "").replace(/^\*How:\*\s*/i, "").trim();
1015
- } while (value !== previous);
1016
- return value;
1017
- }
1018
- function parseStampAttributes(body) {
1019
- const attributes = {};
1020
- for (const part of body.split(";")) {
1021
- const eq = part.indexOf("=");
1022
- if (eq === -1) continue;
1023
- const key = part.slice(0, eq).trim();
1024
- const value = part.slice(eq + 1).trim();
1025
- if (key) attributes[key] = value;
1026
- }
1027
- return attributes;
1028
- }
1029
- function parseEntryBody(body) {
1030
- const lines = [];
1031
- for (const line of body.split("\n")) {
1032
- if (/^##\s/.test(line) || /^---\s*$/.test(line) || /^<!--/.test(line)) break;
1033
- lines.push(line);
1034
- }
1035
- const text = lines.join("\n");
1036
- const what = /^-\s+\*\*([\s\S]+?)\*\*\s*$/m.exec(text)?.[1]?.replace(/\s+/g, " ").trim() ?? "";
1037
- if (!what) return void 0;
1038
- const why = /^\s+-\s+\*Why:\*\s+(.+)$/m.exec(text)?.[1]?.trim() ?? "";
1039
- const how = lines.filter((line) => /^\s+-\s+\*How:\*/i.test(line)).map((line) => cleanHowLine(line)).filter(Boolean).join("\n");
1040
- return { what, why, how };
1041
- }
1042
- function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLearnedEntries(raw)) {
1043
- if (!raw) return [];
1044
- const structured = [];
1045
- const stampPattern = /<!--\s*learned-stamp:\s*([^>]*?)\s*-->/g;
1046
- const stamps = [...raw.matchAll(stampPattern)];
1047
- for (const [index, stamp] of stamps.entries()) {
1048
- const attributes = parseStampAttributes(stamp[1] ?? "");
1049
- const category = parseLearnedCategory(attributes["category"]) ?? "fact";
1050
- const capturedAt = attributes["capturedAt"] ?? "";
1051
- const skill = attributes["skill"];
1052
- const trials = directiveTrials({
1053
- applied: Number(attributes["applied"]),
1054
- wins: Number(attributes["wins"])
1055
- });
1056
- const start = (stamp.index ?? 0) + stamp[0].length;
1057
- const end = stamps[index + 1]?.index ?? raw.length;
1058
- const parsed = parseEntryBody(raw.slice(start, end));
1059
- if (!parsed || parsed.what.length < MIN_INSTRUCTIVE_LENGTH) continue;
1060
- structured.push({
1061
- key: directiveKey(parsed.what),
1062
- category,
1063
- what: parsed.what,
1064
- why: parsed.why || WHY_BY_CATEGORY[category],
1065
- how: parsed.how,
1066
- capturedAt,
1067
- ...skill ? { skill } : {},
1068
- ...trials.applied > 0 ? { applied: trials.applied, wins: trials.wins } : {}
1069
- });
1070
- }
1071
- if (structured.length === 0) {
1072
- for (const chunk of legacyEntries) {
1073
- const stamp = parseLearnedEntryStamp(chunk);
1074
- const directive = stamp.capturedAt || stamp.category ? stripStamp(chunk) : chunk;
1075
- if (directive.length < MIN_INSTRUCTIVE_LENGTH) continue;
1076
- const category = stamp.category ?? classifyLearnedEntry(directive);
1077
- const { what, why, how } = decomposeLearnedEntry(directive, category);
1078
- structured.push({
1079
- key: directiveKey(directive),
1080
- category,
1081
- what,
1082
- why,
1083
- how,
1084
- capturedAt: stamp.capturedAt
1085
- });
848
+ function saveProjectAgentConsolidated(role, content, projectRoot, options) {
849
+ const normalizedRole = assertProjectAgentRole(role);
850
+ if (!content.trim()) return consolidationPath(normalizedRole, projectRoot);
851
+ const dir = roleDir(normalizedRole, projectRoot);
852
+ mkdirSync2(dir, { recursive: true });
853
+ const fp = consolidationPath(normalizedRole, projectRoot);
854
+ writeTextAtomically(fp, content);
855
+ const consolidatedAt = (/* @__PURE__ */ new Date()).toISOString();
856
+ let archived;
857
+ if (options?.prune) {
858
+ const rawText = loadProjectAgentLearnedText(normalizedRole, projectRoot);
859
+ if (rawText) {
860
+ archived = archivePath(normalizedRole, consolidatedAt, projectRoot);
861
+ writeTextAtomically(archived, rawText);
1086
862
  }
863
+ writeTextAtomically(
864
+ learnedPath(normalizedRole, projectRoot),
865
+ renderLearnedInstructions(normalizedRole, [], consolidatedAt)
866
+ );
1087
867
  }
1088
- return structured;
1089
- }
1090
- function mergeStructuredEntries(existing, fresh) {
1091
- const { what, why, how } = decomposeLearnedEntry(fresh.text, fresh.category);
1092
- const key = directiveKey(fresh.text);
1093
- const overlapping = existing.filter((entry) => tokenOverlap(entry.key, key) >= 0.55);
1094
- const proven = overlapping.find(
1095
- (entry) => entry.category === fresh.category && isProvenDirective(entry)
868
+ const rawEntries = readRawLearnedEntries(normalizedRole, projectRoot);
869
+ const rawBytes = Buffer.byteLength(
870
+ loadProjectAgentLearnedText(normalizedRole, projectRoot),
871
+ "utf8"
1096
872
  );
1097
- if (proven) return sortStructuredEntries([...existing]);
1098
- const ancestor = overlapping.filter((entry) => entry.category === fresh.category).sort((a, b) => directiveTrials(b).applied - directiveTrials(a).applied)[0];
1099
- const inherited = ancestor ? directiveTrials(ancestor) : { applied: 0, wins: 0 };
1100
- const freshEntry = {
1101
- key,
1102
- category: fresh.category,
1103
- what,
1104
- why,
1105
- how,
1106
- capturedAt: fresh.capturedAt,
1107
- ...fresh.skill ? { skill: fresh.skill } : {},
1108
- ...inherited.applied > 0 ? { applied: inherited.applied, wins: inherited.wins } : {}
873
+ const meta = {
874
+ consolidatedAt,
875
+ sourceEntryCount: rawEntries.length,
876
+ sourceBytes: rawBytes,
877
+ consolidatedBytes: Buffer.byteLength(content, "utf8"),
878
+ trigger: options?.trigger ?? "manual",
879
+ ...options?.model ? { model: options.model } : {},
880
+ ...options?.prune ? { pruned: true } : {},
881
+ ...archived ? { archivePath: archived } : {},
882
+ ...options?.skills?.length ? { skills: [...options.skills] } : {}
1109
883
  };
1110
- const merged = existing.filter(
1111
- (entry) => entry.category !== fresh.category || tokenOverlap(entry.key, key) < 0.55
884
+ writeTextAtomically(
885
+ consolidationMetaPath(normalizedRole, projectRoot),
886
+ `${JSON.stringify(meta, null, 2)}
887
+ `
1112
888
  );
1113
- merged.push(freshEntry);
1114
- return sortStructuredEntries(merged);
889
+ return fp;
1115
890
  }
1116
- function sortStructuredEntries(merged) {
1117
- const order = {
1118
- warning: 0,
1119
- convention: 1,
1120
- pattern: 2,
1121
- fact: 3
1122
- };
1123
- merged.sort((a, b) => {
1124
- const cmp = order[a.category] - order[b.category];
1125
- if (cmp !== 0) return cmp;
1126
- return a.what.localeCompare(b.what);
1127
- });
1128
- return merged;
891
+ function clearProjectAgentConsolidated(role, projectRoot) {
892
+ const normalizedRole = assertProjectAgentRole(role);
893
+ for (const file of [
894
+ consolidationPath(normalizedRole, projectRoot),
895
+ consolidationMetaPath(normalizedRole, projectRoot)
896
+ ]) {
897
+ try {
898
+ rmSync2(file, { force: true });
899
+ } catch {
900
+ }
901
+ }
1129
902
  }
1130
- var SECTION_TITLE = {
1131
- warning: "## What to avoid",
1132
- convention: "## What to do",
1133
- pattern: "## Patterns to follow",
1134
- fact: "## Project facts"
1135
- };
1136
- function renderLearnedInstructions(role, entries, capturedAt) {
1137
- const headerLines = [
1138
- `# Learned instructions for \`${role}\``,
903
+ function buildConsolidationInstruction(role, projectRoot) {
904
+ const normalizedRole = assertProjectAgentRole(role);
905
+ const entries = readRawLearnedEntries(normalizedRole, projectRoot);
906
+ const rawEntries = entries.map((entry) => {
907
+ const lines = [entry.what];
908
+ if (entry.how) lines.push(` Anchors: ${entry.how.split("\n").join(", ")}`);
909
+ const { applied, wins } = directiveTrials(entry);
910
+ if (applied > 0) lines.push(` Track record: applied ${applied}\xD7, ${wins} succeeded`);
911
+ return lines.join("\n");
912
+ });
913
+ const existingConsolidation = loadProjectAgentConsolidated(normalizedRole, projectRoot);
914
+ const rawBody = rawEntries.map((entry, i) => `### Entry ${i + 1}
915
+
916
+ ${entry}`).join("\n\n");
917
+ const sections = [
918
+ `You are reviewing and consolidating the captured learning entries for the "${normalizedRole}" agent role in this project.`,
1139
919
  "",
1140
- "> Project-specific learning data for the `" + role + "` agent. Each entry is a directive \u2014 read it as an instruction, not a journal entry. Entries are re-derived on every capture, so this file is always a current, structured snapshot of what this agent has learned.",
1141
- ""
920
+ "Your task is to synthesize the raw entries below into a single, narrowly-scoped document that represents what this agent has learned \u2014 specifically for its skills and role responsibilities. This document is the **instruction manual for future invocations** of this agent in this project. It is not a journal, not an exhaustive transcription, and not a memory store.",
921
+ "",
922
+ "## Requirements",
923
+ "",
924
+ "1. **Be selective.** Some raw entries are noise \u2014 narrative descriptions of single-session events, ephemeral references (commit SHAs, timestamps, line numbers, PR refs), or thin fragments that cannot be generalized. **Drop them.** The output must be smaller than the input; a consolidation that preserves every word is not a consolidation.",
925
+ '2. **Extract the directive.** When a raw entry mixes narrative framing with a buried lesson ("When I worked on X, I realized Y"), extract the lesson as a directive and discard the framing.',
926
+ "3. **Generic, not specific.** Rewrite entries so they apply across sessions. Replace session-specific anchors (commit SHAs, dates, line numbers) with general principles or concrete tools/commands that will still be valid next month.",
927
+ "4. **Self-contained bullets.** Each bullet must make sense standalone \u2014 understandable to a future agent that has no context about the session that produced it.",
928
+ "5. **Narrow scope.** Keep only knowledge that is genuinely durable and role-relevant. When in doubt, leave it out.",
929
+ "6. **Structured format.** Organize the content under clear markdown headings (##) by topic. Use bullet points for individual facts. Group by category (Conventions, Patterns, Warnings, Project Facts) when it helps scannability.",
930
+ "7. **Preserve actionable anchors.** Keep exact file paths, command names, package names, and configuration values that make a directive concrete and runnable.",
931
+ "8. **No filler.** Do not include meta-commentary about the consolidation process. The document should read as if it were always a single authoritative reference.",
932
+ "9. **Trust the track record over your own judgement of plausibility.** An entry may carry `Track record: applied N\xD7, M succeeded` \u2014 completed tasks that exercised it, and how many of those succeeded. Keep the ones that keep working, even when they read as obvious. An entry applied several times with few successes has been tested and has failed the test: drop it, or narrow it to the condition under which it actually holds. Entries with no track record are unproven rather than bad \u2014 judge them on merit, but do not let one displace a proven entry.",
933
+ "",
934
+ rawEntries.length > 0 ? `## Raw learned entries (${rawEntries.length} total)
935
+
936
+ ${rawBody}` : "## Raw learned entries\n\n(No raw entries yet \u2014 return an empty document.)"
1142
937
  ];
1143
- if (entries.length === 0) {
1144
- return headerLines.join("\n") + [
1145
- "_No learned entries yet._",
938
+ if (existingConsolidation) {
939
+ sections.push(
1146
940
  "",
1147
- "---",
1148
- `*Last capture: ${capturedAt} \xB7 0 entries*`,
1149
- ""
1150
- ].join("\n");
941
+ `## Existing consolidated document (for reference \u2014 improve upon it)
942
+
943
+ ${existingConsolidation}`
944
+ );
1151
945
  }
1152
- const buckets = {
1153
- warning: [],
1154
- convention: [],
1155
- pattern: [],
1156
- fact: []
1157
- };
1158
- for (const entry of entries) buckets[entry.category].push(entry);
1159
- const sections = [];
1160
- for (const category of ["warning", "convention", "pattern", "fact"]) {
1161
- const list = buckets[category];
1162
- if (list.length === 0) continue;
1163
- sections.push(SECTION_TITLE[category]);
1164
- sections.push("");
1165
- for (const entry of list) {
1166
- const trials = directiveTrials(entry);
1167
- const attributes = [
1168
- `category=${entry.category}`,
1169
- `capturedAt=${entry.capturedAt}`,
1170
- ...entry.skill ? [`skill=${entry.skill}`] : [],
1171
- // Zero counters are omitted so an entry with no track record renders
1172
- // exactly as it did before this field existed — the parse→render
1173
- // fixed point holds for every pre-existing buffer on disk.
1174
- ...trials.applied > 0 ? [`applied=${trials.applied}`, `wins=${trials.wins}`] : []
1175
- ].join("; ");
1176
- sections.push(`<!-- learned-stamp: ${attributes} -->`);
1177
- sections.push(`- **${entry.what}**`);
1178
- sections.push(` - *Why:* ${entry.why}`);
1179
- for (const line of entry.how.split("\n").map(cleanHowLine).filter(Boolean)) {
1180
- sections.push(` - *How:* ${line}`);
1181
- }
1182
- sections.push("");
1183
- }
946
+ const retired = retiredDirectivesToWarnAbout(
947
+ normalizedRole,
948
+ entries.map((entry) => entry.what),
949
+ projectRoot
950
+ );
951
+ if (retired.length > 0) {
952
+ sections.push(
953
+ "",
954
+ `## Retired \u2014 do not reinstate (${retired.length})
955
+
956
+ These were exercised repeatedly and kept correlating with failed tasks. If the existing document above still states any of them, drop it.
957
+
958
+ ${retired.map((what) => `- ${what}`).join("\n")}`
959
+ );
1184
960
  }
1185
- const footer = ["---", `*Last capture: ${capturedAt} \xB7 ${entries.length} entries*`, ""];
1186
- return [...headerLines, ...sections, ...footer].join("\n");
961
+ sections.push(
962
+ "",
963
+ "## Output",
964
+ "",
965
+ "Return ONLY the consolidated markdown document. Do not wrap it in code fences or add commentary."
966
+ );
967
+ return {
968
+ instruction: sections.join("\n"),
969
+ rawEntries,
970
+ hasExistingConsolidation: Boolean(existingConsolidation)
971
+ };
1187
972
  }
1188
- var DROP_PRIORITY = {
1189
- fact: 0,
1190
- pattern: 1,
1191
- convention: 2,
1192
- warning: 3
1193
- };
1194
- function enforceLearnedBudget(entries, capturedAt, maxBytes, role = "agent") {
1195
- const kept = [...entries];
1196
- const dropped = [];
1197
- const size = (list) => Buffer.byteLength(renderLearnedInstructions(role, list, capturedAt), "utf8");
1198
- const UTILITY_EPSILON = 1e-9;
1199
- while (kept.length > 1 && size(kept) > maxBytes) {
1200
- let victimIndex = 0;
1201
- for (let i = 1; i < kept.length; i++) {
1202
- const a = kept[i];
1203
- const b = kept[victimIndex];
1204
- const byPriority = DROP_PRIORITY[a.category] - DROP_PRIORITY[b.category];
1205
- if (byPriority !== 0) {
1206
- if (byPriority < 0) victimIndex = i;
1207
- continue;
973
+
974
+ // src/coordination/agents/project-agent-knowledge-manifests.ts
975
+ var BUILT_IN_KNOWLEDGE_MANIFESTS = {
976
+ android: {
977
+ role: "android",
978
+ // Only machine-readable JSON registries belong in liveQueries. Android
979
+ // release/API data has no JSON registry endpoint, so those checks stay as
980
+ // checklist prose below.
981
+ liveQueries: {
982
+ kotlin: {
983
+ registry: "https://registry.npmjs.org/kotlin/latest",
984
+ key: "version",
985
+ description: "Kotlin latest stable"
1208
986
  }
1209
- const byUtility = directiveUtility(a) - directiveUtility(b);
1210
- if (Math.abs(byUtility) > UTILITY_EPSILON) {
1211
- if (byUtility < 0) victimIndex = i;
1212
- continue;
987
+ },
988
+ checklist: [
989
+ "Current Android Gradle Plugin version (8.x+)",
990
+ "Current Kotlin version (2.x+)",
991
+ "Current compileSdk / targetSdk (\u2265 34, check Play Store policy)",
992
+ "Current Jetpack Compose stable version",
993
+ "All third-party dependencies are at latest compatible minor"
994
+ ],
995
+ verifyThreshold: 0.5
996
+ },
997
+ frontend: {
998
+ role: "frontend",
999
+ liveQueries: {
1000
+ react: {
1001
+ registry: "https://registry.npmjs.org/react/latest",
1002
+ key: "version",
1003
+ description: "React latest stable"
1004
+ },
1005
+ nextjs: {
1006
+ registry: "https://registry.npmjs.org/next/latest",
1007
+ key: "version",
1008
+ description: "Next.js latest stable"
1213
1009
  }
1214
- if (a.capturedAt < b.capturedAt) victimIndex = i;
1215
- }
1216
- dropped.push(...kept.splice(victimIndex, 1));
1010
+ },
1011
+ checklist: [
1012
+ "Current React version (pinned in package.json)",
1013
+ "Current Next.js / Vite / framework version",
1014
+ "Node.js runtime version (project .nvmrc or engines)",
1015
+ "All devDependencies are at latest compatible versions",
1016
+ "Browser compatibility targets from browserslist"
1017
+ ],
1018
+ verifyThreshold: 0.5
1019
+ },
1020
+ executor: {
1021
+ role: "executor",
1022
+ liveQueries: {
1023
+ node: {
1024
+ registry: "https://registry.npmjs.org/node/latest",
1025
+ key: "version",
1026
+ description: "Node.js latest release"
1027
+ }
1028
+ },
1029
+ checklist: [
1030
+ "Node.js version from .nvmrc or package.engines",
1031
+ "TypeScript version from package.json",
1032
+ "Package manager (pnpm \u2265 9) from packageManager",
1033
+ "Current toolchain: esbuild, vitest, etc."
1034
+ ],
1035
+ verifyThreshold: 0.6
1217
1036
  }
1218
- return { kept, dropped };
1219
- }
1037
+ };
1220
1038
 
1221
- // src/coordination/agents/project-agent-quarantine.ts
1222
- import { readFileSync as readFileSync3 } from "node:fs";
1223
- import * as path5 from "node:path";
1224
- var QUARANTINE_MAX_BYTES = 64 * 1024;
1225
- var RETIRED_MATCH_THRESHOLD = 0.55;
1226
- function quarantinePath(role, projectRoot) {
1227
- return path5.join(roleDir(role, projectRoot), "quarantine.md");
1039
+ // src/coordination/agents/project-agent-learning-policy.ts
1040
+ import { readFileSync as readFileSync4 } from "node:fs";
1041
+ var DEFAULT_LEARNING_POLICY = {
1042
+ enabled: true,
1043
+ lifetimeCaptureCount: 0
1044
+ };
1045
+ function loadProjectAgentLearningPolicy(role, projectRoot) {
1046
+ try {
1047
+ const parsed = JSON.parse(
1048
+ readFileSync4(learningPolicyPath(role, projectRoot), "utf8")
1049
+ );
1050
+ return {
1051
+ enabled: parsed.enabled !== false,
1052
+ lifetimeCaptureCount: typeof parsed.lifetimeCaptureCount === "number" && Number.isInteger(parsed.lifetimeCaptureCount) && parsed.lifetimeCaptureCount >= 0 ? parsed.lifetimeCaptureCount : 0,
1053
+ ...typeof parsed.lastCaptureAt === "string" ? { lastCaptureAt: parsed.lastCaptureAt } : {},
1054
+ ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {},
1055
+ ...typeof parsed.lastOptimizeAt === "string" ? { lastOptimizeAt: parsed.lastOptimizeAt } : {}
1056
+ };
1057
+ } catch {
1058
+ return { ...DEFAULT_LEARNING_POLICY };
1059
+ }
1228
1060
  }
1229
- function readTextOrEmpty(filePath) {
1061
+ function updateProjectAgentLearningPolicy(role, patch, projectRoot) {
1062
+ const defined = Object.fromEntries(
1063
+ Object.entries(patch).filter(([, value]) => value !== void 0)
1064
+ );
1065
+ const updated = {
1066
+ ...loadProjectAgentLearningPolicy(role, projectRoot),
1067
+ ...defined
1068
+ };
1069
+ writeTextAtomically(
1070
+ learningPolicyPath(role, projectRoot),
1071
+ `${JSON.stringify(updated, null, 2)}
1072
+ `
1073
+ );
1074
+ return updated;
1075
+ }
1076
+ function recordProjectAgentOptimizePass(role, projectRoot, at = (/* @__PURE__ */ new Date()).toISOString()) {
1230
1077
  try {
1231
- return readFileSync3(filePath, "utf8");
1078
+ updateProjectAgentLearningPolicy(role, { lastOptimizeAt: at }, projectRoot);
1232
1079
  } catch {
1233
- return "";
1234
1080
  }
1235
1081
  }
1236
- function appendQuarantine(role, retired, at, projectRoot) {
1237
- if (retired.length === 0) return;
1238
- const filePath = quarantinePath(role, projectRoot);
1239
- const existing = readTextOrEmpty(filePath);
1240
- const header = existing ? "" : `# Retired directives for \`${role}\`
1241
1082
 
1242
- > Directives that were exercised repeatedly and kept correlating with
1243
- > failure. They are no longer injected anywhere. Kept for audit \u2014 a directive
1244
- > can be right about a project that has since changed.
1083
+ // src/coordination/agents/project-agent-skill-layer.ts
1084
+ import { readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "node:fs";
1085
+ import * as path8 from "node:path";
1245
1086
 
1246
- `;
1247
- const block = retired.map((entry) => {
1248
- const { applied, wins, losses } = directiveTrials(entry);
1249
- const attributes = [
1250
- `at=${at}`,
1251
- `category=${entry.category}`,
1252
- ...entry.skill ? [`skill=${entry.skill}`] : [],
1253
- `applied=${applied}`,
1254
- `wins=${wins}`
1255
- ].join("; ");
1256
- return [
1257
- `<!-- quarantined: ${attributes} -->`,
1258
- `- **${entry.what}**`,
1259
- ` - *Record:* ${wins} succeeded / ${losses} failed of ${applied} applied`,
1260
- ""
1261
- ].join("\n");
1262
- }).join("\n");
1263
- let next = `${existing}${header}${block}`;
1264
- if (Buffer.byteLength(next, "utf8") > QUARANTINE_MAX_BYTES) {
1265
- const marker = "<!-- quarantined:";
1266
- const overflow = Buffer.byteLength(next, "utf8") - QUARANTINE_MAX_BYTES;
1267
- const cut = next.indexOf(marker, overflow);
1268
- next = cut === -1 ? block : next.slice(cut);
1269
- }
1270
- writeTextAtomically(filePath, next);
1087
+ // src/coordination/agents/project-agent-config-io.ts
1088
+ import { readFileSync as readFileSync5 } from "node:fs";
1089
+ import * as path6 from "node:path";
1090
+
1091
+ // src/coordination/agents/project-agent-config-validation.ts
1092
+ import * as path5 from "node:path";
1093
+ var PROJECT_AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
1094
+ "tools",
1095
+ "skillNames",
1096
+ "budget",
1097
+ "provider",
1098
+ "model",
1099
+ "allowedCapabilities",
1100
+ "modelPolicy",
1101
+ "fallbackProfile",
1102
+ "cwd",
1103
+ "worktree",
1104
+ "availability"
1105
+ ]);
1106
+ var PROJECT_AGENT_BUDGET_KEYS = /* @__PURE__ */ new Set([
1107
+ "timeoutMs",
1108
+ "idleTimeoutMs",
1109
+ "maxIterations",
1110
+ "maxToolCalls",
1111
+ "maxTokens",
1112
+ "maxCostUsd"
1113
+ ]);
1114
+ function isPlainRecord(value) {
1115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1271
1116
  }
1272
- function readQuarantinedDirectives(role, projectRoot) {
1273
- const raw = readTextOrEmpty(quarantinePath(role, projectRoot));
1274
- if (!raw) return [];
1275
- const out = [];
1276
- const stamps = [...raw.matchAll(/<!--\s*quarantined:\s*([^>]*?)\s*-->/g)];
1277
- for (const [index, stamp] of stamps.entries()) {
1278
- const attributes = {};
1279
- for (const part of (stamp[1] ?? "").split(";")) {
1280
- const eq = part.indexOf("=");
1281
- if (eq === -1) continue;
1282
- attributes[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
1283
- }
1284
- const start = (stamp.index ?? 0) + stamp[0].length;
1285
- const end = stamps[index + 1]?.index ?? raw.length;
1286
- const what = /^-\s+\*\*([\s\S]+?)\*\*\s*$/m.exec(raw.slice(start, end))?.[1]?.replace(/\s+/g, " ").trim();
1287
- if (!what) continue;
1288
- const skill = attributes["skill"];
1289
- out.push({ what, ...skill ? { skill } : {} });
1117
+ function assertStringList(value, field) {
1118
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
1119
+ throw new Error(`Project agent config "${field}" must be an array of non-empty strings.`);
1290
1120
  }
1291
- return out;
1292
- }
1293
- function bareClaim(line) {
1294
- return line.replace(/^\s*[-*+]\s+/, "").replace(/^\s*\d+[.)]\s+/, "").replace(/\*\*/g, "").replace(/`/g, "").trim();
1295
- }
1296
- function indentOf(line) {
1297
- return line.length - line.trimStart().length;
1298
1121
  }
1299
- function scrubRetiredLines(text, retired) {
1300
- if (!text.trim() || retired.length === 0) return text;
1301
- const keys = retired.map((what) => normalizeForComparison(what));
1302
- const lines = text.split("\n");
1303
- const kept = [];
1304
- for (let i = 0; i < lines.length; i++) {
1305
- const line = lines[i];
1306
- const claim = bareClaim(line);
1307
- if (claim.length >= MIN_INSTRUCTIVE_LENGTH && keys.some(
1308
- (key) => tokenOverlap(key, normalizeForComparison(claim)) >= RETIRED_MATCH_THRESHOLD
1309
- )) {
1310
- const base = indentOf(line);
1311
- while (i + 1 < lines.length) {
1312
- const next = lines[i + 1];
1313
- if (!next.trim() || indentOf(next) <= base) break;
1314
- i++;
1122
+ function validateProjectAgentConfig(value) {
1123
+ if (!isPlainRecord(value)) throw new Error("Project agent config must be an object.");
1124
+ const unknownKey = Object.keys(value).find((key) => !PROJECT_AGENT_CONFIG_KEYS.has(key));
1125
+ if (unknownKey) throw new Error(`Unknown project agent config field: "${unknownKey}".`);
1126
+ if (value.tools !== void 0) assertStringList(value.tools, "tools");
1127
+ if (value.skillNames !== void 0) assertStringList(value.skillNames, "skillNames");
1128
+ if (value.allowedCapabilities !== void 0) {
1129
+ assertStringList(value.allowedCapabilities, "allowedCapabilities");
1130
+ }
1131
+ for (const field of ["provider", "model", "fallbackProfile"]) {
1132
+ const fieldValue = value[field];
1133
+ if (fieldValue !== void 0 && (typeof fieldValue !== "string" || !fieldValue.trim())) {
1134
+ throw new Error(`Project agent config "${field}" must be a non-empty string.`);
1135
+ }
1136
+ }
1137
+ if (value.cwd !== void 0) {
1138
+ if (typeof value.cwd !== "string" || !value.cwd.trim()) {
1139
+ throw new Error('Project agent config "cwd" must be a non-empty relative path.');
1140
+ }
1141
+ const cwd = value.cwd.replace(/\\/g, "/").trim();
1142
+ if (path5.posix.isAbsolute(cwd) || /^[a-z]:\//i.test(cwd) || cwd.split("/").includes("..")) {
1143
+ throw new Error('Project agent config "cwd" must stay inside the assigned checkout.');
1144
+ }
1145
+ value.cwd = cwd;
1146
+ }
1147
+ if (value.worktree !== void 0 && value.worktree !== true && value.worktree !== false && value.worktree !== "auto" && value.worktree !== "required" && value.worktree !== "off") {
1148
+ throw new Error('Project agent config "worktree" must be auto, required, off, or boolean.');
1149
+ }
1150
+ if (value.modelPolicy !== void 0) validateProjectAgentModelPolicy(value.modelPolicy);
1151
+ if (value.availability !== void 0) validateProjectAgentAvailability(value.availability);
1152
+ if (value.budget !== void 0) {
1153
+ if (!isPlainRecord(value.budget)) {
1154
+ throw new Error('Project agent config "budget" must be an object.');
1155
+ }
1156
+ const unknownBudgetKey = Object.keys(value.budget).find(
1157
+ (key) => !PROJECT_AGENT_BUDGET_KEYS.has(key)
1158
+ );
1159
+ if (unknownBudgetKey) {
1160
+ throw new Error(`Unknown project agent budget field: "${unknownBudgetKey}".`);
1161
+ }
1162
+ for (const [field, amount] of Object.entries(value.budget)) {
1163
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
1164
+ throw new Error(`Project agent budget "${field}" must be a finite non-negative number.`);
1165
+ }
1166
+ if (field !== "maxCostUsd" && !Number.isInteger(amount)) {
1167
+ throw new Error(`Project agent budget "${field}" must be an integer.`);
1315
1168
  }
1316
- continue;
1317
1169
  }
1318
- kept.push(line);
1319
1170
  }
1320
- return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
1321
- }
1322
- function hasDirectiveContent(text) {
1323
- return text.split("\n").some((line) => {
1324
- const trimmed = line.trimStart();
1325
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(">")) return false;
1326
- if (/^-{3,}\s*$/.test(trimmed)) return false;
1327
- if (/^\*[^*].*\*$/.test(trimmed)) return false;
1328
- return bareClaim(line).length >= MIN_INSTRUCTIVE_LENGTH;
1329
- });
1330
- }
1331
- function retiredDirectivesToWarnAbout(role, currentDirectives, projectRoot, skill) {
1332
- const currentKeys = currentDirectives.map((text) => normalizeForComparison(text));
1333
- return readQuarantinedDirectives(role, projectRoot).filter((entry) => skill ? entry.skill === skill : true).map((entry) => entry.what).filter(
1334
- (what) => !currentKeys.some(
1335
- (key) => tokenOverlap(key, normalizeForComparison(what)) >= RETIRED_MATCH_THRESHOLD
1336
- )
1337
- );
1171
+ return value;
1338
1172
  }
1339
-
1340
- // src/coordination/agents/project-agent-consolidation.ts
1341
- function learnedPath(role, projectRoot) {
1342
- return path6.join(roleDir(role, projectRoot), "learned.md");
1173
+ function validateProjectAgentModelPolicy(value) {
1174
+ if (!isPlainRecord(value)) throw new Error("Project agent modelPolicy must be an object.");
1175
+ const allowedKeys = /* @__PURE__ */ new Set(["allowed", "fallbacks", "strict"]);
1176
+ const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key));
1177
+ if (unknownKey) throw new Error(`Unknown modelPolicy field: "${unknownKey}".`);
1178
+ const validateTargets = (targets, field, allowEmpty) => {
1179
+ if (!Array.isArray(targets) || !allowEmpty && targets.length === 0) {
1180
+ throw new Error(`Project agent modelPolicy "${field}" must be a non-empty array.`);
1181
+ }
1182
+ for (const target of targets) {
1183
+ if (!isPlainRecord(target) || typeof target.provider !== "string" || !target.provider.trim() || typeof target.model !== "string" || !target.model.trim()) {
1184
+ throw new Error(`Project agent modelPolicy "${field}" contains an invalid target.`);
1185
+ }
1186
+ }
1187
+ };
1188
+ validateTargets(value.allowed, "allowed", false);
1189
+ if (value.fallbacks !== void 0) {
1190
+ validateTargets(value.fallbacks, "fallbacks", true);
1191
+ const allowed = new Set(
1192
+ value.allowed.map(
1193
+ (target) => `${target.provider}\0${target.model}`
1194
+ )
1195
+ );
1196
+ for (const target of value.fallbacks) {
1197
+ if (!allowed.has(`${target.provider}\0${target.model}`)) {
1198
+ throw new Error("Every modelPolicy fallback must also appear in allowed.");
1199
+ }
1200
+ }
1201
+ }
1202
+ if (value.strict !== void 0 && typeof value.strict !== "boolean") {
1203
+ throw new Error('Project agent modelPolicy "strict" must be boolean.');
1204
+ }
1343
1205
  }
1344
- function loadProjectAgentLearnedText(role, projectRoot) {
1206
+ function validateProjectAgentAvailability(value) {
1207
+ if (!isPlainRecord(value)) throw new Error("Project agent availability must be an object.");
1208
+ const allowedKeys = /* @__PURE__ */ new Set(["timezone", "days", "start", "end", "mode"]);
1209
+ const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key));
1210
+ if (unknownKey) throw new Error(`Unknown availability field: "${unknownKey}".`);
1211
+ if (typeof value.timezone !== "string" || !value.timezone.trim()) {
1212
+ throw new Error("Project agent availability requires a timezone.");
1213
+ }
1345
1214
  try {
1346
- return readFileSync4(learnedPath(role, projectRoot), "utf8").trim();
1215
+ new Intl.DateTimeFormat("en-US", { timeZone: value.timezone }).format();
1347
1216
  } catch {
1348
- return "";
1217
+ throw new Error(`Invalid project agent availability timezone: "${value.timezone}".`);
1218
+ }
1219
+ if (!Array.isArray(value.days) || value.days.length === 0 || value.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) {
1220
+ throw new Error("Project agent availability days must contain weekday numbers 0-6.");
1221
+ }
1222
+ if (typeof value.start !== "string" || typeof value.end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(value.start) || !/^([01]\d|2[0-3]):[0-5]\d$/.test(value.end)) {
1223
+ throw new Error("Project agent availability start/end must use HH:MM.");
1224
+ }
1225
+ if (value.mode !== void 0 && value.mode !== "advisory" && value.mode !== "enforce") {
1226
+ throw new Error("Project agent availability mode must be advisory or enforce.");
1349
1227
  }
1350
1228
  }
1351
- function readRawLearnedEntries(role, projectRoot) {
1352
- const raw = loadProjectAgentLearnedText(role, projectRoot);
1353
- return parseStructuredLearnedEntriesFromContent(raw, splitLearnedEntries(raw));
1354
- }
1355
- function consolidationPath(role, projectRoot) {
1356
- return path6.join(roleDir(role, projectRoot), "consolidated.md");
1357
- }
1358
- function consolidatedDocumentPath(role, projectRoot) {
1359
- return consolidationPath(assertProjectAgentRole(role), projectRoot);
1360
- }
1361
- function consolidationMetaPath(role, projectRoot) {
1362
- return path6.join(roleDir(role, projectRoot), "consolidation.json");
1363
- }
1364
- function archivePath(role, at, projectRoot) {
1365
- return path6.join(roleDir(role, projectRoot), "archive", `learned-${at.replace(/[:.]/g, "-")}.md`);
1366
- }
1367
- function loadProjectAgentConsolidated(role, projectRoot) {
1229
+
1230
+ // src/coordination/agents/project-agent-config-io.ts
1231
+ function loadProjectAgentConfig(role, projectRoot) {
1232
+ const cfgPath = path6.join(roleDir(role, projectRoot), "config.json");
1368
1233
  try {
1369
- return readFileSync4(consolidationPath(role, projectRoot), "utf8").trim();
1234
+ return validateProjectAgentConfig(JSON.parse(readFileSync5(cfgPath, "utf8")));
1370
1235
  } catch {
1371
- return "";
1236
+ return void 0;
1372
1237
  }
1373
1238
  }
1374
- function loadConsolidationMetadata(role, projectRoot) {
1239
+
1240
+ // src/coordination/agents/project-agent-profile.ts
1241
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6 } from "node:fs";
1242
+ import * as path7 from "node:path";
1243
+ function slugifyProjectAgentRole(name) {
1244
+ const slug = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "").slice(0, 96);
1245
+ return assertProjectAgentRole(slug);
1246
+ }
1247
+ function loadProjectAgentProfile(role, projectRoot) {
1375
1248
  try {
1376
- const raw = readFileSync4(consolidationMetaPath(role, projectRoot), "utf8");
1377
- const parsed = JSON.parse(raw);
1378
- if (typeof parsed === "object" && parsed !== null && typeof parsed.consolidatedAt === "string" && typeof parsed.sourceEntryCount === "number") {
1379
- return parsed;
1249
+ const parsed = JSON.parse(
1250
+ readFileSync6(projectAgentProfilePath(role, projectRoot), "utf8")
1251
+ );
1252
+ const normalizedRole = assertProjectAgentRole(parsed.role ?? role);
1253
+ const baseRole = assertProjectAgentRole(parsed.baseRole ?? "generic");
1254
+ if (normalizedRole !== assertProjectAgentRole(role)) return void 0;
1255
+ if (typeof parsed.name !== "string" || !parsed.name.trim()) return void 0;
1256
+ if (typeof parsed.purpose !== "string" || !parsed.purpose.trim()) return void 0;
1257
+ if (!Array.isArray(parsed.taskTypes) || parsed.taskTypes.some((item) => typeof item !== "string")) {
1258
+ return void 0;
1380
1259
  }
1381
- return void 0;
1260
+ const createdAt = typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString();
1261
+ const updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : createdAt;
1262
+ return {
1263
+ role: normalizedRole,
1264
+ name: parsed.name.trim(),
1265
+ baseRole,
1266
+ purpose: parsed.purpose.trim(),
1267
+ taskTypes: parsed.taskTypes.map((item) => item.trim()).filter(Boolean),
1268
+ createdAt,
1269
+ updatedAt
1270
+ };
1382
1271
  } catch {
1383
1272
  return void 0;
1384
1273
  }
1385
1274
  }
1386
- function isConsolidated(role, projectRoot) {
1387
- return existsSync(consolidationMetaPath(role, projectRoot));
1388
- }
1389
- function saveProjectAgentConsolidated(role, content, projectRoot, options) {
1390
- const normalizedRole = assertProjectAgentRole(role);
1391
- if (!content.trim()) return consolidationPath(normalizedRole, projectRoot);
1392
- const dir = roleDir(normalizedRole, projectRoot);
1393
- mkdirSync2(dir, { recursive: true });
1394
- const fp = consolidationPath(normalizedRole, projectRoot);
1395
- writeTextAtomically(fp, content);
1396
- const consolidatedAt = (/* @__PURE__ */ new Date()).toISOString();
1397
- let archived;
1398
- if (options?.prune) {
1399
- const rawText = loadProjectAgentLearnedText(normalizedRole, projectRoot);
1400
- if (rawText) {
1401
- archived = archivePath(normalizedRole, consolidatedAt, projectRoot);
1402
- writeTextAtomically(archived, rawText);
1403
- }
1404
- writeTextAtomically(
1405
- learnedPath(normalizedRole, projectRoot),
1406
- renderLearnedInstructions(normalizedRole, [], consolidatedAt)
1407
- );
1275
+ function createProjectAgent(input, projectRoot) {
1276
+ const name = input.name.trim();
1277
+ const purpose = input.purpose.trim();
1278
+ if (!name || name.length > 120) throw new Error("Project agent name must be 1-120 characters.");
1279
+ if (purpose.length < 10 || purpose.length > 4e3) {
1280
+ throw new Error("Project agent purpose must be 10-4000 characters.");
1408
1281
  }
1409
- const rawEntries = readRawLearnedEntries(normalizedRole, projectRoot);
1410
- const rawBytes = Buffer.byteLength(
1411
- loadProjectAgentLearnedText(normalizedRole, projectRoot),
1412
- "utf8"
1282
+ const role = assertProjectAgentRole(
1283
+ (input.role?.trim() || slugifyProjectAgentRole(name)).toLowerCase()
1413
1284
  );
1414
- const meta = {
1415
- consolidatedAt,
1416
- sourceEntryCount: rawEntries.length,
1417
- sourceBytes: rawBytes,
1418
- consolidatedBytes: Buffer.byteLength(content, "utf8"),
1419
- trigger: options?.trigger ?? "manual",
1420
- ...options?.model ? { model: options.model } : {},
1421
- ...options?.prune ? { pruned: true } : {},
1422
- ...archived ? { archivePath: archived } : {},
1423
- ...options?.skills?.length ? { skills: [...options.skills] } : {}
1424
- };
1425
- writeTextAtomically(
1426
- consolidationMetaPath(normalizedRole, projectRoot),
1427
- `${JSON.stringify(meta, null, 2)}
1428
- `
1429
- );
1430
- return fp;
1431
- }
1432
- function clearProjectAgentConsolidated(role, projectRoot) {
1433
- const normalizedRole = assertProjectAgentRole(role);
1434
- for (const file of [
1435
- consolidationPath(normalizedRole, projectRoot),
1436
- consolidationMetaPath(normalizedRole, projectRoot)
1437
- ]) {
1438
- try {
1439
- rmSync2(file, { force: true });
1440
- } catch {
1441
- }
1285
+ const baseRole = assertProjectAgentRole((input.baseRole?.trim() || "generic").toLowerCase());
1286
+ const taskTypes = [...new Set(input.taskTypes.map((item) => item.trim()).filter(Boolean))];
1287
+ if (taskTypes.length === 0 || taskTypes.length > 32 || taskTypes.some((item) => item.length > 160)) {
1288
+ throw new Error("Project agent requires 1-32 task descriptions, each at most 160 characters.");
1442
1289
  }
1443
- }
1444
- function buildConsolidationInstruction(role, projectRoot) {
1445
- const normalizedRole = assertProjectAgentRole(role);
1446
- const entries = readRawLearnedEntries(normalizedRole, projectRoot);
1447
- const rawEntries = entries.map((entry) => {
1448
- const lines = [entry.what];
1449
- if (entry.how) lines.push(` Anchors: ${entry.how.split("\n").join(", ")}`);
1450
- const { applied, wins } = directiveTrials(entry);
1451
- if (applied > 0) lines.push(` Track record: applied ${applied}\xD7, ${wins} succeeded`);
1452
- return lines.join("\n");
1453
- });
1454
- const existingConsolidation = loadProjectAgentConsolidated(normalizedRole, projectRoot);
1455
- const rawBody = rawEntries.map((entry, i) => `### Entry ${i + 1}
1456
-
1457
- ${entry}`).join("\n\n");
1458
- const sections = [
1459
- `You are reviewing and consolidating the captured learning entries for the "${normalizedRole}" agent role in this project.`,
1290
+ const dir = roleDir(role, projectRoot);
1291
+ if (existsSync2(dir) && readdirSync(dir).length > 0) {
1292
+ throw new Error(`Project agent role "${role}" already exists.`);
1293
+ }
1294
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1295
+ const profile = {
1296
+ role,
1297
+ name,
1298
+ baseRole,
1299
+ purpose,
1300
+ taskTypes,
1301
+ createdAt: now,
1302
+ updatedAt: now
1303
+ };
1304
+ const identity = [
1305
+ `# ${name}`,
1460
1306
  "",
1461
- "Your task is to synthesize the raw entries below into a single, narrowly-scoped document that represents what this agent has learned \u2014 specifically for its skills and role responsibilities. This document is the **instruction manual for future invocations** of this agent in this project. It is not a journal, not an exhaustive transcription, and not a memory store.",
1307
+ `You are the project-specific "${name}" agent (role: ${role}).`,
1462
1308
  "",
1463
- "## Requirements",
1309
+ "## Purpose",
1464
1310
  "",
1465
- "1. **Be selective.** Some raw entries are noise \u2014 narrative descriptions of single-session events, ephemeral references (commit SHAs, timestamps, line numbers, PR refs), or thin fragments that cannot be generalized. **Drop them.** The output must be smaller than the input; a consolidation that preserves every word is not a consolidation.",
1466
- '2. **Extract the directive.** When a raw entry mixes narrative framing with a buried lesson ("When I worked on X, I realized Y"), extract the lesson as a directive and discard the framing.',
1467
- "3. **Generic, not specific.** Rewrite entries so they apply across sessions. Replace session-specific anchors (commit SHAs, dates, line numbers) with general principles or concrete tools/commands that will still be valid next month.",
1468
- "4. **Self-contained bullets.** Each bullet must make sense standalone \u2014 understandable to a future agent that has no context about the session that produced it.",
1469
- "5. **Narrow scope.** Keep only knowledge that is genuinely durable and role-relevant. When in doubt, leave it out.",
1470
- "6. **Structured format.** Organize the content under clear markdown headings (##) by topic. Use bullet points for individual facts. Group by category (Conventions, Patterns, Warnings, Project Facts) when it helps scannability.",
1471
- "7. **Preserve actionable anchors.** Keep exact file paths, command names, package names, and configuration values that make a directive concrete and runnable.",
1472
- "8. **No filler.** Do not include meta-commentary about the consolidation process. The document should read as if it were always a single authoritative reference.",
1473
- "9. **Trust the track record over your own judgement of plausibility.** An entry may carry `Track record: applied N\xD7, M succeeded` \u2014 completed tasks that exercised it, and how many of those succeeded. Keep the ones that keep working, even when they read as obvious. An entry applied several times with few successes has been tested and has failed the test: drop it, or narrow it to the condition under which it actually holds. Entries with no track record are unproven rather than bad \u2014 judge them on merit, but do not let one displace a proven entry.",
1311
+ purpose,
1474
1312
  "",
1475
- rawEntries.length > 0 ? `## Raw learned entries (${rawEntries.length} total)
1476
-
1477
- ${rawBody}` : "## Raw learned entries\n\n(No raw entries yet \u2014 return an empty document.)"
1478
- ];
1479
- if (existingConsolidation) {
1480
- sections.push(
1481
- "",
1482
- `## Existing consolidated document (for reference \u2014 improve upon it)
1483
-
1484
- ${existingConsolidation}`
1485
- );
1486
- }
1487
- const retired = retiredDirectivesToWarnAbout(
1488
- normalizedRole,
1489
- entries.map((entry) => entry.what),
1490
- projectRoot
1491
- );
1492
- if (retired.length > 0) {
1493
- sections.push(
1494
- "",
1495
- `## Retired \u2014 do not reinstate (${retired.length})
1496
-
1497
- These were exercised repeatedly and kept correlating with failed tasks. If the existing document above still states any of them, drop it.
1498
-
1499
- ${retired.map((what) => `- ${what}`).join("\n")}`
1500
- );
1501
- }
1502
- sections.push(
1313
+ "## Primary task types",
1503
1314
  "",
1504
- "## Output",
1315
+ ...taskTypes.map((item) => `- ${item}`),
1505
1316
  "",
1506
- "Return ONLY the consolidated markdown document. Do not wrap it in code fences or add commentary."
1317
+ "Learn durable project conventions from completed work, keep conclusions evidence-based, and stay within this assigned purpose.",
1318
+ ""
1319
+ ].join("\n");
1320
+ mkdirSync3(dir, { recursive: true });
1321
+ writeTextAtomically(path7.join(dir, "identity.md"), identity);
1322
+ writeTextAtomically(
1323
+ projectAgentProfilePath(role, projectRoot),
1324
+ `${JSON.stringify(profile, null, 2)}
1325
+ `
1507
1326
  );
1508
- return {
1509
- instruction: sections.join("\n"),
1510
- rawEntries,
1511
- hasExistingConsolidation: Boolean(existingConsolidation)
1512
- };
1327
+ return profile;
1513
1328
  }
1514
1329
 
1515
- // src/coordination/agents/project-agent-knowledge-manifests.ts
1516
- var BUILT_IN_KNOWLEDGE_MANIFESTS = {
1517
- android: {
1518
- role: "android",
1519
- // Only machine-readable JSON registries belong in liveQueries. Android
1520
- // release/API data has no JSON registry endpoint, so those checks stay as
1521
- // checklist prose below.
1522
- liveQueries: {
1523
- kotlin: {
1524
- registry: "https://registry.npmjs.org/kotlin/latest",
1525
- key: "version",
1526
- description: "Kotlin latest stable"
1527
- }
1528
- },
1529
- checklist: [
1530
- "Current Android Gradle Plugin version (8.x+)",
1531
- "Current Kotlin version (2.x+)",
1532
- "Current compileSdk / targetSdk (\u2265 34, check Play Store policy)",
1533
- "Current Jetpack Compose stable version",
1534
- "All third-party dependencies are at latest compatible minor"
1535
- ],
1536
- verifyThreshold: 0.5
1330
+ // src/types/runtime-capability-manifest.ts
1331
+ var PLAYWRIGHT_ALIASES = {
1332
+ playwright_navigate: "browser_navigate",
1333
+ playwright_screenshot: "browser_screenshot",
1334
+ playwright_click: "browser_click",
1335
+ playwright_type: "browser_type",
1336
+ playwright_evaluate: "browser_evaluate",
1337
+ playwright_select_option: "browser_select",
1338
+ playwright_hover: "browser_hover",
1339
+ playwright_fill_form: "browser_type",
1340
+ playwright_wait_for: "browser_wait",
1341
+ playwright_press_key: "browser_press",
1342
+ playwright_drag: "browser_drag"
1343
+ };
1344
+ var RUNTIME_CAPABILITY_MANIFEST = [
1345
+ {
1346
+ id: "filesystem.read",
1347
+ pack: "core",
1348
+ exposure: "direct",
1349
+ tools: ["read", "grep", "glob", "tree"]
1537
1350
  },
1538
- frontend: {
1539
- role: "frontend",
1540
- liveQueries: {
1541
- react: {
1542
- registry: "https://registry.npmjs.org/react/latest",
1543
- key: "version",
1544
- description: "React latest stable"
1545
- },
1546
- nextjs: {
1547
- registry: "https://registry.npmjs.org/next/latest",
1548
- key: "version",
1549
- description: "Next.js latest stable"
1550
- }
1551
- },
1552
- checklist: [
1553
- "Current React version (pinned in package.json)",
1554
- "Current Next.js / Vite / framework version",
1555
- "Node.js runtime version (project .nvmrc or engines)",
1556
- "All devDependencies are at latest compatible versions",
1557
- "Browser compatibility targets from browserslist"
1558
- ],
1559
- verifyThreshold: 0.5
1351
+ {
1352
+ id: "code.inspect",
1353
+ pack: "core",
1354
+ exposure: "direct",
1355
+ tools: [
1356
+ "codebase-stats",
1357
+ "codebase-search",
1358
+ "codebase-incoming-calls",
1359
+ "codebase-outgoing-calls",
1360
+ "codebase-index",
1361
+ "codebase-skeleton",
1362
+ "codebase-repo-map",
1363
+ "codebase-impact-analysis",
1364
+ "dead-code-scan",
1365
+ "diff",
1366
+ "json",
1367
+ "logs"
1368
+ ]
1560
1369
  },
1561
- executor: {
1562
- role: "executor",
1563
- liveQueries: {
1564
- node: {
1565
- registry: "https://registry.npmjs.org/node/latest",
1566
- key: "version",
1567
- description: "Node.js latest release"
1568
- }
1569
- },
1570
- checklist: [
1571
- "Node.js version from .nvmrc or package.engines",
1572
- "TypeScript version from package.json",
1573
- "Package manager (pnpm \u2265 9) from packageManager",
1574
- "Current toolchain: esbuild, vitest, etc."
1370
+ {
1371
+ id: "filesystem.write",
1372
+ pack: "development",
1373
+ exposure: "direct",
1374
+ tools: ["write", "edit", "replace", "patch", "codebase-ast-replace"]
1375
+ },
1376
+ {
1377
+ id: "execution.shell",
1378
+ pack: "development",
1379
+ exposure: "direct",
1380
+ tools: ["bash", "exec", "language", "language_info", "language_package"]
1381
+ },
1382
+ {
1383
+ id: "verification.run",
1384
+ pack: "development",
1385
+ exposure: "direct",
1386
+ tools: ["lint", "format", "typecheck", "test", "e2e_plan", "codebase-targeted-test"]
1387
+ },
1388
+ {
1389
+ id: "version-control.manage",
1390
+ pack: "development",
1391
+ exposure: "direct",
1392
+ tools: ["git"]
1393
+ },
1394
+ {
1395
+ id: "dependencies.manage",
1396
+ pack: "development",
1397
+ exposure: "on-demand",
1398
+ tools: ["install", "outdated", "audit"]
1399
+ },
1400
+ {
1401
+ id: "documentation.author",
1402
+ pack: "development",
1403
+ exposure: "on-demand",
1404
+ tools: ["document", "design", "scaffold"]
1405
+ },
1406
+ {
1407
+ id: "web.research",
1408
+ pack: "core",
1409
+ exposure: "direct",
1410
+ tools: ["search", "fetch"]
1411
+ },
1412
+ {
1413
+ id: "work.plan",
1414
+ pack: "core",
1415
+ exposure: "direct",
1416
+ tools: ["todo", "plan", "task", "kanban", "nextsteps"]
1417
+ },
1418
+ {
1419
+ id: "coordination.mailbox",
1420
+ pack: "fleet",
1421
+ exposure: "direct",
1422
+ tools: ["mailbox", "mail_send", "mail_inbox", "fleet_status"]
1423
+ },
1424
+ {
1425
+ id: "fleet.delegate",
1426
+ pack: "fleet",
1427
+ exposure: "on-demand",
1428
+ tools: [
1429
+ "delegate",
1430
+ "spawn_subagent",
1431
+ "assign_task",
1432
+ "kanban_queue",
1433
+ "await_tasks",
1434
+ "ask_subagent",
1435
+ "ask_result",
1436
+ "roll_up",
1437
+ "quality_gate",
1438
+ "terminate_subagent",
1439
+ "terminate_all",
1440
+ "work_complete",
1441
+ "collab_debug",
1442
+ "fleet_emit",
1443
+ "fleet"
1444
+ ]
1445
+ },
1446
+ {
1447
+ id: "reasoning.oneshot",
1448
+ pack: "core",
1449
+ exposure: "on-demand",
1450
+ tools: ["llm", "council"]
1451
+ },
1452
+ {
1453
+ id: "memory.manage",
1454
+ pack: "memory",
1455
+ exposure: "on-demand",
1456
+ tools: ["remember", "search_memory", "find_related_memories", "forget"]
1457
+ },
1458
+ {
1459
+ id: "memory.curate",
1460
+ pack: "memory",
1461
+ exposure: "on-demand",
1462
+ tools: [
1463
+ "memory_candidates",
1464
+ "memory_update",
1465
+ "memory_delete",
1466
+ "memory_recover",
1467
+ "memory_backfill_recoverable",
1468
+ "memory_for_file",
1469
+ "memory_for_path",
1470
+ "memory_search",
1471
+ "memory_graph",
1472
+ "memory_gather_batch",
1473
+ "memory_verify",
1474
+ "memory_hygiene"
1475
+ ]
1476
+ },
1477
+ {
1478
+ id: "browser.interact",
1479
+ pack: "browser",
1480
+ exposure: "on-demand",
1481
+ tools: [
1482
+ "browser_open",
1483
+ "browser_status",
1484
+ "browser_list",
1485
+ "browser_navigate",
1486
+ "browser_snapshot",
1487
+ "browser_screenshot",
1488
+ "browser_click",
1489
+ "browser_type",
1490
+ "browser_select",
1491
+ "browser_press",
1492
+ "browser_hover",
1493
+ "browser_drag",
1494
+ "browser_wait",
1495
+ "browser_evaluate",
1496
+ "browser_upload",
1497
+ "browser_close"
1575
1498
  ],
1576
- verifyThreshold: 0.6
1499
+ aliases: PLAYWRIGHT_ALIASES
1500
+ },
1501
+ {
1502
+ id: "mcp.dynamic",
1503
+ pack: "mcp",
1504
+ exposure: "on-demand",
1505
+ tools: ["mcp_use"]
1506
+ },
1507
+ {
1508
+ id: "automation.manage",
1509
+ pack: "admin",
1510
+ exposure: "on-demand",
1511
+ tools: ["cron_schedule", "cron_list", "cron_cancel", "watch_start", "watch_list", "watch_stop"]
1512
+ },
1513
+ {
1514
+ id: "context.pin",
1515
+ pack: "admin",
1516
+ exposure: "on-demand",
1517
+ tools: ["pin_add", "pin_list", "pin_remove"]
1518
+ },
1519
+ {
1520
+ id: "quality.analyze",
1521
+ pack: "development",
1522
+ exposure: "on-demand",
1523
+ tools: [
1524
+ "dead_code_scan",
1525
+ "detect_duplicate_code",
1526
+ "secret_scanner_test",
1527
+ "error_lens_history",
1528
+ "security-ast-scan"
1529
+ ]
1530
+ },
1531
+ {
1532
+ id: "release.manage",
1533
+ pack: "development",
1534
+ exposure: "on-demand",
1535
+ tools: ["git_autocommit", "semver_current", "semver_bump", "semver_changelog"]
1536
+ },
1537
+ {
1538
+ id: "messaging.telegram",
1539
+ pack: "fleet",
1540
+ exposure: "on-demand",
1541
+ tools: ["telegram_send", "telegram_read", "telegram_approve"]
1542
+ },
1543
+ {
1544
+ id: "tools.discover",
1545
+ pack: "admin",
1546
+ exposure: "direct",
1547
+ tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use", "clarify"]
1548
+ },
1549
+ {
1550
+ id: "runtime.admin",
1551
+ pack: "admin",
1552
+ exposure: "internal",
1553
+ tools: ["set_working_dir", "context_manager", "mode", "skill", "mcp_control"]
1577
1554
  }
1578
- };
1579
-
1580
- // src/coordination/agents/project-agent-learning-policy.ts
1581
- import { readFileSync as readFileSync5 } from "node:fs";
1582
- var DEFAULT_LEARNING_POLICY = {
1583
- enabled: true,
1584
- lifetimeCaptureCount: 0
1585
- };
1586
- function loadProjectAgentLearningPolicy(role, projectRoot) {
1587
- try {
1588
- const parsed = JSON.parse(
1589
- readFileSync5(learningPolicyPath(role, projectRoot), "utf8")
1590
- );
1591
- return {
1592
- enabled: parsed.enabled !== false,
1593
- lifetimeCaptureCount: typeof parsed.lifetimeCaptureCount === "number" && Number.isInteger(parsed.lifetimeCaptureCount) && parsed.lifetimeCaptureCount >= 0 ? parsed.lifetimeCaptureCount : 0,
1594
- ...typeof parsed.lastCaptureAt === "string" ? { lastCaptureAt: parsed.lastCaptureAt } : {},
1595
- ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {},
1596
- ...typeof parsed.lastOptimizeAt === "string" ? { lastOptimizeAt: parsed.lastOptimizeAt } : {}
1597
- };
1598
- } catch {
1599
- return { ...DEFAULT_LEARNING_POLICY };
1555
+ ];
1556
+ var CAPABILITY_BY_ID = new Map(
1557
+ RUNTIME_CAPABILITY_MANIFEST.map((definition) => [definition.id, definition])
1558
+ );
1559
+ var CAPABILITY_BY_TOOL = /* @__PURE__ */ new Map();
1560
+ var TOOL_ALIASES = /* @__PURE__ */ new Map();
1561
+ for (const definition of RUNTIME_CAPABILITY_MANIFEST) {
1562
+ for (const tool of definition.tools) {
1563
+ CAPABILITY_BY_TOOL.set(tool, definition.id);
1600
1564
  }
1565
+ for (const [alias, tool] of Object.entries(definition.aliases ?? {})) {
1566
+ TOOL_ALIASES.set(alias, tool);
1567
+ }
1568
+ }
1569
+ function toolsForRuntimeCapabilities(capabilityIds) {
1570
+ return [
1571
+ ...new Set(
1572
+ capabilityIds.flatMap((id) => {
1573
+ const definition = CAPABILITY_BY_ID.get(id);
1574
+ if (!definition) throw new Error(`Unknown runtime capability: "${id}"`);
1575
+ return definition.tools;
1576
+ })
1577
+ )
1578
+ ];
1579
+ }
1580
+ function normalizeRuntimeToolName(name) {
1581
+ return TOOL_ALIASES.get(name) ?? name;
1582
+ }
1583
+ function runtimeCapabilityForTool(name) {
1584
+ return CAPABILITY_BY_TOOL.get(normalizeRuntimeToolName(name));
1601
1585
  }
1602
- function updateProjectAgentLearningPolicy(role, patch, projectRoot) {
1603
- const defined = Object.fromEntries(
1604
- Object.entries(patch).filter(([, value]) => value !== void 0)
1605
- );
1606
- const updated = {
1607
- ...loadProjectAgentLearningPolicy(role, projectRoot),
1608
- ...defined
1609
- };
1610
- writeTextAtomically(
1611
- learningPolicyPath(role, projectRoot),
1612
- `${JSON.stringify(updated, null, 2)}
1613
- `
1614
- );
1615
- return updated;
1586
+ function isKnownRuntimeCapability(id) {
1587
+ return CAPABILITY_BY_ID.has(id);
1616
1588
  }
1617
- function recordProjectAgentOptimizePass(role, projectRoot, at = (/* @__PURE__ */ new Date()).toISOString()) {
1618
- try {
1619
- updateProjectAgentLearningPolicy(role, { lastOptimizeAt: at }, projectRoot);
1620
- } catch {
1589
+ function inferRuntimeCapabilities(toolNames) {
1590
+ const ids = /* @__PURE__ */ new Set();
1591
+ for (const rawName of toolNames) {
1592
+ const id = runtimeCapabilityForTool(rawName);
1593
+ if (id) ids.add(id);
1621
1594
  }
1595
+ return [...ids];
1622
1596
  }
1623
-
1624
- // src/coordination/agents/project-agent-skill-layer.ts
1625
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync3 } from "node:fs";
1626
- import * as path8 from "node:path";
1627
-
1628
- // src/coordination/agents/project-agent-profile.ts
1629
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6 } from "node:fs";
1630
- import * as path7 from "node:path";
1631
- function slugifyProjectAgentRole(name) {
1632
- const slug = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "").slice(0, 96);
1633
- return assertProjectAgentRole(slug);
1597
+ function missingRuntimeCapabilities(required, toolNames) {
1598
+ if (!required || required.length === 0) return [];
1599
+ const available = new Set(inferRuntimeCapabilities(toolNames));
1600
+ return required.filter((id) => !CAPABILITY_BY_ID.has(id) || !available.has(id));
1634
1601
  }
1635
- function loadProjectAgentProfile(role, projectRoot) {
1636
- try {
1637
- const parsed = JSON.parse(
1638
- readFileSync6(projectAgentProfilePath(role, projectRoot), "utf8")
1639
- );
1640
- const normalizedRole = assertProjectAgentRole(parsed.role ?? role);
1641
- const baseRole = assertProjectAgentRole(parsed.baseRole ?? "generic");
1642
- if (normalizedRole !== assertProjectAgentRole(role)) return void 0;
1643
- if (typeof parsed.name !== "string" || !parsed.name.trim()) return void 0;
1644
- if (typeof parsed.purpose !== "string" || !parsed.purpose.trim()) return void 0;
1645
- if (!Array.isArray(parsed.taskTypes) || parsed.taskTypes.some((item) => typeof item !== "string")) {
1646
- return void 0;
1647
- }
1648
- const createdAt = typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString();
1649
- const updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : createdAt;
1650
- return {
1651
- role: normalizedRole,
1652
- name: parsed.name.trim(),
1653
- baseRole,
1654
- purpose: parsed.purpose.trim(),
1655
- taskTypes: parsed.taskTypes.map((item) => item.trim()).filter(Boolean),
1656
- createdAt,
1657
- updatedAt
1658
- };
1659
- } catch {
1660
- return void 0;
1661
- }
1602
+ function missingRequiredRuntimeTools(required, toolNames) {
1603
+ if (!required || required.length === 0) return [];
1604
+ const available = new Set(toolNames.map(normalizeRuntimeToolName));
1605
+ return required.map(normalizeRuntimeToolName).filter((name) => !CAPABILITY_BY_TOOL.has(name) || !available.has(name));
1662
1606
  }
1663
- function createProjectAgent(input, projectRoot) {
1664
- const name = input.name.trim();
1665
- const purpose = input.purpose.trim();
1666
- if (!name || name.length > 120) throw new Error("Project agent name must be 1-120 characters.");
1667
- if (purpose.length < 10 || purpose.length > 4e3) {
1668
- throw new Error("Project agent purpose must be 10-4000 characters.");
1669
- }
1670
- const role = assertProjectAgentRole(
1671
- (input.role?.trim() || slugifyProjectAgentRole(name)).toLowerCase()
1672
- );
1673
- const baseRole = assertProjectAgentRole((input.baseRole?.trim() || "generic").toLowerCase());
1674
- const taskTypes = [...new Set(input.taskTypes.map((item) => item.trim()).filter(Boolean))];
1675
- if (taskTypes.length === 0 || taskTypes.length > 32 || taskTypes.some((item) => item.length > 160)) {
1676
- throw new Error("Project agent requires 1-32 task descriptions, each at most 160 characters.");
1607
+ function runtimeToolReferencesFromText(text) {
1608
+ const references = /* @__PURE__ */ new Set();
1609
+ for (const match of text.matchAll(/`([a-z][a-z0-9_-]+)`/gi)) {
1610
+ const name = normalizeRuntimeToolName(match[1] ?? "");
1611
+ if (CAPABILITY_BY_TOOL.has(name)) references.add(name);
1677
1612
  }
1678
- const dir = roleDir(role, projectRoot);
1679
- if (existsSync2(dir) && readdirSync(dir).length > 0) {
1680
- throw new Error(`Project agent role "${role}" already exists.`);
1613
+ for (const pattern of [
1614
+ /\b(?:call|use|invoke|run)\s+`([a-z][a-z0-9_-]+)`/gi,
1615
+ /`([a-z][a-z0-9_-]+)`\s+tool\b/gi
1616
+ ]) {
1617
+ for (const match of text.matchAll(pattern)) {
1618
+ references.add(normalizeRuntimeToolName(match[1] ?? ""));
1619
+ }
1681
1620
  }
1682
- const now = (/* @__PURE__ */ new Date()).toISOString();
1683
- const profile = {
1684
- role,
1685
- name,
1686
- baseRole,
1687
- purpose,
1688
- taskTypes,
1689
- createdAt: now,
1690
- updatedAt: now
1691
- };
1692
- const identity = [
1693
- `# ${name}`,
1694
- "",
1695
- `You are the project-specific "${name}" agent (role: ${role}).`,
1696
- "",
1697
- "## Purpose",
1698
- "",
1699
- purpose,
1700
- "",
1701
- "## Primary task types",
1702
- "",
1703
- ...taskTypes.map((item) => `- ${item}`),
1704
- "",
1705
- "Learn durable project conventions from completed work, keep conclusions evidence-based, and stay within this assigned purpose.",
1706
- ""
1707
- ].join("\n");
1708
- mkdirSync3(dir, { recursive: true });
1709
- writeTextAtomically(path7.join(dir, "identity.md"), identity);
1710
- writeTextAtomically(
1711
- projectAgentProfilePath(role, projectRoot),
1712
- `${JSON.stringify(profile, null, 2)}
1713
- `
1714
- );
1715
- return profile;
1621
+ return [...references];
1622
+ }
1623
+ function isDeprecatedRuntimeToolAlias(name) {
1624
+ return TOOL_ALIASES.has(name) || name.startsWith("mcp__");
1716
1625
  }
1717
1626
 
1718
1627
  // src/coordination/agents/role-skills.ts
@@ -2877,85 +2786,128 @@ function listProjectAgentRoles(projectRoot) {
2877
2786
  }
2878
2787
  }
2879
2788
 
2880
- // src/coordination/agents/project-agent-identity.ts
2881
- function createProjectAgentRoster(baseRoster, projectRoot) {
2882
- const target = { ...baseRoster };
2883
- const resolveRole = (role, resolving = /* @__PURE__ */ new Set()) => {
2884
- if (resolving.has(role)) return void 0;
2885
- const isBuiltIn = Object.hasOwn(target, role);
2886
- if (!isBuiltIn && !listProjectAgentRoles(projectRoot).includes(role)) return void 0;
2887
- resolving.add(role);
2888
- const profile = loadProjectAgentProfile(role, projectRoot);
2889
- const template = isBuiltIn ? target[role] : profile ? resolveRole(profile.baseRole, resolving) ?? target["generic"] : target["generic"];
2890
- resolving.delete(role);
2891
- if (!template) return void 0;
2892
- const base = isBuiltIn ? template : {
2893
- ...template,
2894
- id: role,
2895
- role,
2896
- name: profile?.name ?? role,
2897
- ...profile ? {
2898
- dispatch: {
2899
- summary: profile.purpose,
2900
- keywords: [
2901
- ...profile.taskTypes,
2902
- ...new Set(
2903
- `${profile.purpose} ${profile.taskTypes.join(" ")}`.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 3)
2904
- )
2905
- ]
2906
- }
2907
- } : {}
2908
- };
2909
- return applyProjectAgentConfig(base, loadProjectAgentConfig(role, projectRoot), {
2910
- protectSystemRole: isBuiltIn && !profile
2911
- });
2789
+ // src/security/capabilities.ts
2790
+ var ToolCapabilities = {
2791
+ /** Can execute arbitrary commands in the user's shell (the `bash` tool). */
2792
+ SHELL_ARBITRARY: "shell.arbitrary",
2793
+ /**
2794
+ * Can execute the `exec` tool's allowlisted commands.
2795
+ *
2796
+ * "Restricted" describes the command NAME check, not the resulting power.
2797
+ * Treat this as equivalent to {@link SHELL_ARBITRARY} when deciding what to
2798
+ * grant: the allowlist includes general-purpose interpreters (`node`,
2799
+ * `python`, `perl`, `ruby`) and, on Windows, the platform shells (`cmd`,
2800
+ * `powershell`, `pwsh`) deliberately, since without them `exec` cannot run
2801
+ * `dir`/`type`/any cmdlet. `node -e …` or `cmd /c …` therefore reaches
2802
+ * arbitrary execution, and per-argument denylisting cannot close that while
2803
+ * the shells remain (blocking `node -e` still leaves `cmd /c node -e`).
2804
+ *
2805
+ * What actually bounds this is downstream, not the name check: the permission
2806
+ * policy reconstructs the full command line and runs it through the
2807
+ * destructive classifier, so destructive calls still require confirmation.
2808
+ *
2809
+ * Granting `shell.restricted` while withholding `shell.arbitrary` therefore
2810
+ * buys no meaningful reduction in blast radius. Documented rather than
2811
+ * silently implied, because the two read as a narrow/wide pair (WS-083).
2812
+ */
2813
+ SHELL_RESTRICTED: "shell.restricted",
2814
+ /** Can run a restricted project formatter/linter-style command. */
2815
+ SHELL_EXEC: "shell.exec",
2816
+ /** Can read files inside the project (and possibly outside via symlinks if not guarded). */
2817
+ FS_READ: "fs.read",
2818
+ /** Can write / modify / delete files inside the project. */
2819
+ FS_WRITE: "fs.write",
2820
+ /** Can write files outside the current project root (very high risk). */
2821
+ FS_WRITE_OUTSIDE_PROJECT: "fs.write.outside-project",
2822
+ /** Can perform outbound network requests. */
2823
+ NET_OUTBOUND: "net.outbound",
2824
+ /** Can mutate in-memory session todos only. */
2825
+ SESSION_TODO: "session.todo",
2826
+ /** Can park after-task `<nextsteps>` suggestions for the current turn. */
2827
+ SESSION_NEXTSTEPS: "session.nextsteps",
2828
+ /** Can mutate in-memory session mode only. */
2829
+ SESSION_MODE: "session.mode",
2830
+ /** Can inspect registered tool metadata. */
2831
+ TOOL_META: "tool.meta",
2832
+ /** Can invoke arbitrary registered tools through a meta-tool. */
2833
+ TOOL_MUTATE_ANY: "tool.mutate.any",
2834
+ /** Can read persistent memory. */
2835
+ MEMORY_READ: "memory.read",
2836
+ /** Can write persistent memory. */
2837
+ MEMORY_WRITE: "memory.write",
2838
+ /** Can delete persistent memory. */
2839
+ MEMORY_DELETE: "memory.delete",
2840
+ /** Proxies tools from external MCP servers (unknown capability). */
2841
+ MCP_PROXY: "mcp.proxy",
2842
+ /** Can spawn or manage subagents / multi-agent tasks. */
2843
+ SUBAGENT_SPAWN: "subagent.spawn",
2844
+ /** Can inspect fleet/subagent coordination state without mutating it. */
2845
+ COORDINATION_FLEET_READ: "coordination.fleet.read",
2846
+ /** Can publish attributed, schema-checked events onto the fleet bus. */
2847
+ COORDINATION_FLEET_EMIT: "coordination.fleet.emit",
2848
+ /** Can submit a task-local structured result to the parent Director. */
2849
+ COORDINATION_RESULT_SUBMIT: "coordination.result.submit",
2850
+ /** Can read or write inter-agent mailbox messages. */
2851
+ COORDINATION_MAIL: "coordination.mail",
2852
+ /** Can schedule, inspect, or cancel in-session cron jobs. */
2853
+ COORDINATION_CRON: "coordination.cron",
2854
+ /** Can mutate global or session configuration / trust state. */
2855
+ CONFIG_MUTATE: "config.mutate",
2856
+ /** Can install packages or run package managers with side effects. */
2857
+ PACKAGE_INSTALL: "package.install"
2858
+ };
2859
+ var DANGEROUS_FOR_SUBAGENTS = [
2860
+ ToolCapabilities.SHELL_ARBITRARY,
2861
+ ToolCapabilities.SHELL_RESTRICTED,
2862
+ ToolCapabilities.SHELL_EXEC,
2863
+ ToolCapabilities.FS_WRITE,
2864
+ ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
2865
+ ToolCapabilities.TOOL_MUTATE_ANY,
2866
+ ToolCapabilities.MEMORY_WRITE,
2867
+ ToolCapabilities.MEMORY_DELETE,
2868
+ ToolCapabilities.MCP_PROXY,
2869
+ ToolCapabilities.SUBAGENT_SPAWN,
2870
+ ToolCapabilities.CONFIG_MUTATE,
2871
+ ToolCapabilities.PACKAGE_INSTALL
2872
+ ];
2873
+ var WIDE_SUBAGENT_CAPABILITIES = [
2874
+ ToolCapabilities.FS_READ,
2875
+ ToolCapabilities.FS_WRITE,
2876
+ ToolCapabilities.NET_OUTBOUND,
2877
+ ToolCapabilities.SESSION_TODO,
2878
+ ToolCapabilities.TOOL_META,
2879
+ ToolCapabilities.MEMORY_READ,
2880
+ ToolCapabilities.SHELL_ARBITRARY,
2881
+ ToolCapabilities.SHELL_RESTRICTED,
2882
+ ToolCapabilities.SHELL_EXEC,
2883
+ ToolCapabilities.PACKAGE_INSTALL,
2884
+ // Read-only fleet visibility (fleet_status) — peer awareness has no blast
2885
+ // radius and every fleet worker should coordinate around its peers.
2886
+ ToolCapabilities.COORDINATION_FLEET_READ,
2887
+ ToolCapabilities.COORDINATION_RESULT_SUBMIT
2888
+ ];
2889
+ function clampSubagentCapabilities(requested, ceiling = WIDE_SUBAGENT_CAPABILITIES) {
2890
+ const ceilingSet = new Set(ceiling);
2891
+ const requestedSet = new Set(requested);
2892
+ return {
2893
+ granted: ceiling.filter((capability) => requestedSet.has(capability)),
2894
+ dropped: [...requestedSet].filter((capability) => !ceilingSet.has(capability))
2912
2895
  };
2913
- return new Proxy(target, {
2914
- get(current, property, receiver) {
2915
- if (typeof property !== "string") return Reflect.get(current, property, receiver);
2916
- return resolveRole(property) ?? Reflect.get(current, property, receiver);
2917
- },
2918
- has(current, property) {
2919
- return typeof property === "string" ? Reflect.has(current, property) || resolveRole(property) !== void 0 : Reflect.has(current, property);
2920
- },
2921
- ownKeys(current) {
2922
- return [.../* @__PURE__ */ new Set([...Reflect.ownKeys(current), ...listProjectAgentRoles(projectRoot)])];
2923
- },
2924
- getOwnPropertyDescriptor(current, property) {
2925
- const descriptor = Reflect.getOwnPropertyDescriptor(current, property);
2926
- if (descriptor || typeof property !== "string" || !resolveRole(property)) return descriptor;
2927
- return {
2928
- configurable: true,
2929
- enumerable: true,
2930
- writable: false,
2931
- value: resolveRole(property)
2932
- };
2933
- }
2934
- });
2935
- }
2936
- function loadProjectAgentIdentity(role, projectRoot) {
2937
- const identityPath = path11.join(roleDir(role, projectRoot), "identity.md");
2938
- try {
2939
- return readFileSync9(identityPath, "utf8").trim();
2940
- } catch {
2941
- return "";
2942
- }
2943
- }
2944
- function loadProjectAgentLearned(role, projectRoot) {
2945
- const learnedPath3 = path11.join(roleDir(role, projectRoot), "learned.md");
2946
- try {
2947
- return readFileSync9(learnedPath3, "utf8").trim();
2948
- } catch {
2949
- return "";
2950
- }
2951
2896
  }
2952
- function loadRoleKnowledgeManifest(role, projectRoot) {
2953
- const projectPath = path11.join(roleDir(role, projectRoot), "knowledge.json");
2954
- try {
2955
- const raw = readFileSync9(projectPath, "utf8");
2956
- return JSON.parse(raw);
2957
- } catch {
2958
- return BUILT_IN_KNOWLEDGE_MANIFESTS[role];
2897
+
2898
+ // src/coordination/agents/project-agent-roster.ts
2899
+ var SYSTEM_AGENT_BUDGET_FLOORS = {
2900
+ timeoutMs: 3e5,
2901
+ idleTimeoutMs: 12e4,
2902
+ maxIterations: 20,
2903
+ maxToolCalls: 40,
2904
+ maxTokens: 8192,
2905
+ maxCostUsd: 0.25
2906
+ };
2907
+ function applySystemAgentBudgetFloors(config) {
2908
+ for (const [field, floor] of Object.entries(SYSTEM_AGENT_BUDGET_FLOORS)) {
2909
+ const value = config[field];
2910
+ if (typeof value === "number" && value < floor) config[field] = floor;
2959
2911
  }
2960
2912
  }
2961
2913
  function applyProjectAgentConfig(base, projectConfig, options = {}) {
@@ -2972,8 +2924,6 @@ function applyProjectAgentConfig(base, projectConfig, options = {}) {
2972
2924
  result.modelPolicy = {
2973
2925
  allowed: projectConfig.modelPolicy.allowed.map((target) => ({ ...target })),
2974
2926
  fallbacks: projectConfig.modelPolicy.fallbacks?.map((target) => ({ ...target })),
2975
- // System roles must remain recoverable even when a preferred model is
2976
- // temporarily unavailable. Custom roles may opt into a hard boundary.
2977
2927
  strict: options.protectSystemRole ? false : projectConfig.modelPolicy.strict ?? false
2978
2928
  };
2979
2929
  result.fallbackModels = (projectConfig.modelPolicy.fallbacks ?? []).map(
@@ -3014,177 +2964,82 @@ function applyProjectAgentConfig(base, projectConfig, options = {}) {
3014
2964
  if (options.protectSystemRole) applySystemAgentBudgetFloors(result);
3015
2965
  return result;
3016
2966
  }
3017
- var SYSTEM_AGENT_BUDGET_FLOORS = {
3018
- timeoutMs: 3e5,
3019
- idleTimeoutMs: 12e4,
3020
- maxIterations: 20,
3021
- maxToolCalls: 40,
3022
- maxTokens: 8192,
3023
- maxCostUsd: 0.25
3024
- };
3025
- function applySystemAgentBudgetFloors(config) {
3026
- for (const [field, floor] of Object.entries(SYSTEM_AGENT_BUDGET_FLOORS)) {
3027
- const value = config[field];
3028
- if (typeof value === "number" && value < floor) config[field] = floor;
3029
- }
3030
- }
3031
- function buildProjectContextualizedPrompt(basePrompt, role, projectRoot, options = {}) {
3032
- const contextStart = "<!-- wrongstack:project-agent-context:start -->";
3033
- const contextEnd = "<!-- wrongstack:project-agent-context:end -->";
3034
- if (process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"]) return basePrompt;
3035
- const cleanBase = basePrompt.replace(
3036
- /\s*<!-- wrongstack:project-agent-context:start -->[\s\S]*?<!-- wrongstack:project-agent-context:end -->\s*/g,
3037
- "\n"
3038
- ).trim();
3039
- const parts = [cleanBase];
3040
- const identity = options.identityOverride ?? loadProjectAgentIdentity(role, projectRoot);
3041
- if (identity) {
3042
- parts.push(`
3043
-
3044
- # Project custom identity
3045
-
3046
- ${identity}`);
3047
- }
3048
- const learningPolicy = loadProjectAgentLearningPolicy(role, projectRoot);
3049
- let learnedContent = "";
3050
- let learnedLabel = "";
3051
- if (learningPolicy.enabled) {
3052
- const consolidated = loadProjectAgentConsolidated(role, projectRoot);
3053
- const rawLearned = loadProjectAgentLearned(role, projectRoot);
3054
- if (consolidated) {
3055
- const meta = loadConsolidationMetadata(role, projectRoot);
3056
- const rawEntries = parseStructuredLearnedEntriesFromContent(
3057
- rawLearned,
3058
- splitLearnedEntries(rawLearned)
3059
- );
3060
- const rawBytes = Buffer.byteLength(rawLearned, "utf8");
3061
- const freshEntries = meta === void 0 ? [] : rawEntries.filter((entry) => entry.capturedAt > meta.consolidatedAt);
3062
- const stale = meta === void 0 || freshEntries.length > 0 || rawEntries.length > meta.sourceEntryCount || rawBytes > meta.sourceBytes;
3063
- if (!stale) {
3064
- learnedContent = consolidated;
3065
- learnedLabel = "Consolidated knowledge for this project";
3066
- } else if (freshEntries.length > 0) {
3067
- const delta = freshEntries.map((entry) => `- **${entry.what}**`).join("\n");
3068
- learnedContent = `${consolidated}
3069
-
3070
- ---
3071
-
3072
- ## Recently captured (pending next optimization)
3073
-
3074
- ${delta}`;
3075
- learnedLabel = "Consolidated knowledge for this project";
3076
- } else {
3077
- learnedContent = rawLearned;
3078
- learnedLabel = "Learned instructions for this project (structured: what / why / how)";
3079
- }
3080
- } else {
3081
- learnedContent = rawLearned;
3082
- learnedLabel = "Learned instructions for this project (structured: what / why / how)";
3083
- }
3084
- }
3085
- if (learnedContent) {
3086
- const meaningful = learnedContent.replace(/<!--[\s\S]*?-->/g, "").trim();
3087
- if (meaningful.length > 0) {
3088
- parts.push(`
3089
-
3090
- # ${learnedLabel}
3091
-
3092
- ${learnedContent}`);
3093
- }
3094
- }
3095
- const effectiveProjectRoot = projectRoot || process.cwd();
3096
- const learnedFilePath = path11.join(
3097
- effectiveProjectRoot,
3098
- ".wrongstack",
3099
- "agents",
3100
- role,
3101
- "learned.md"
3102
- );
3103
- if (learningPolicy.enabled) {
3104
- parts.push(
3105
- `
3106
-
3107
- ## Knowledge capture
3108
-
3109
- The file below stores **learning data for this project's "${role}" agent** \u2014 not your memory, not a session log. It is read back into the system prompt of every future "${role}" invocation, so each entry must teach a future agent how to act. On every capture the runtime **merges your new entry with every prior entry and rewrites the whole buffer as a structured instruction list** \u2014 grouped by category ("What to do", "What to avoid", "Patterns to follow", "Project facts"), with each item decomposed into **what** (the rule), **why** (the reason), and **how** (the concrete commands, file paths, or package names that anchor the rule). When you discover a durable principle that future invocations should follow, end your response with a \`## LEARNED\` block. The runtime persists it to:
3110
-
3111
- \`\`\`
3112
- ${learnedFilePath}
3113
- \`\`\`
3114
-
3115
- **Write directives, not narratives.** Each LEARNED entry should be:
3116
-
3117
- - A **rule or principle** that applies across sessions, not a description of what happened in this one.
3118
- - A short **imperative or statement** (1\u20133 sentences), starting with a verb like "Always verify\u2026", "Use X for Y", "Avoid\u2026", "Never assume\u2026".
3119
- - **Generic** \u2014 no commit SHAs, timestamps, specific line numbers, or PR/issue numbers. File paths, package names, and command names are fine when they anchor the lesson.
3120
- - **Self-contained** \u2014 understandable without the surrounding session context.
3121
- - **Front-load concrete anchors** \u2014 commands in backticks, package names like \`@wrongstack/core\`, file paths like \`packages/core/src/.../foo.ts\`. The structured-list renderer extracts these as the "how" for the entry.
3122
-
3123
- **Your directives are scored against real outcomes.** After every task the runtime checks which stored directives were actually exercised \u2014 it matches their anchors against the report \u2014 and folds that task's success or failure into each one's record. A directive that keeps correlating with success outlives newer arrivals and survives rewording; one that has been exercised repeatedly and kept correlating with failure is retired and stops being injected. Two consequences for how you write them: anchors are what make a directive *measurable*, not just runnable, so an anchorless directive can never earn a record; and a directive you are unsure about costs nothing to write, because the loop will find out.
3124
-
3125
- **Tag the skill you are developing.** When a directive refines one of your skills, mark it: \`## LEARNED [skill: testing]\`. Tagged directives are distilled into that skill's project addendum, so the lesson arrives as part of the skill itself on every future run instead of as a loose fact. Untagged directives are routed automatically when the wording makes the target obvious, and stay role-level otherwise.
3126
-
3127
- **Bad** (session log \u2014 rejected at capture time):
3128
-
3129
- \`\`\`
3130
- ## LEARNED
3131
- When I worked on the telegram plugin today, commit 9c7682b84 had a race condition in poll-lock at line 42 because writeFileSync wasn't using the 'wx' flag.
3132
- \`\`\`
3133
-
3134
- **Good** (directive \u2014 persists, then merges into the structured list):
3135
-
3136
- \`\`\`
3137
- ## LEARNED
3138
- Always use the 'wx' (exclusive create) flag with writeFileSync when implementing concurrent lock acquisition in \`packages/core/src/.../poll-lock.ts\` \u2014 filesystem-level atomicity guarantees only one writer wins.
3139
- \`\`\`
3140
-
3141
- Capture only durable, cross-session knowledge \u2014 never ephemeral task details.`
3142
- );
3143
- }
3144
- const knowledge = loadRoleKnowledgeManifest(role, projectRoot);
3145
- if (knowledge) {
3146
- const knowledgeSections = [];
3147
- if (knowledge.checklist.length > 0) {
3148
- knowledgeSections.push(
3149
- `Verify these before answering:
3150
- ${knowledge.checklist.map((item) => `- ${item}`).join("\n")}`
3151
- );
3152
- }
3153
- const liveQueries = Object.entries(knowledge.liveQueries ?? {});
3154
- if (liveQueries.length > 0) {
3155
- const threshold = Number.isFinite(knowledge.verifyThreshold) ? knowledge.verifyThreshold : 0.5;
3156
- knowledgeSections.push(
3157
- `When your confidence in any of the following is below ${threshold}, fetch the live value rather than recalling it:
3158
- ` + liveQueries.map(
3159
- ([topic, query]) => `- **${topic}** \u2014 ${query.description}: \`${query.registry}\` \u2192 field \`${query.key}\``
3160
- ).join("\n")
3161
- );
3162
- }
3163
- if (knowledgeSections.length > 0) {
3164
- parts.push(`
3165
-
3166
- ## Current knowledge requirements
3167
-
3168
- ${knowledgeSections.join("\n\n")}`);
2967
+ function createProjectAgentRoster(baseRoster, projectRoot) {
2968
+ const target = { ...baseRoster };
2969
+ const resolveRole = (role, resolving = /* @__PURE__ */ new Set()) => {
2970
+ if (resolving.has(role)) return void 0;
2971
+ const isBuiltIn = Object.hasOwn(target, role);
2972
+ if (!isBuiltIn && !listProjectAgentRoles(projectRoot).includes(role)) return void 0;
2973
+ resolving.add(role);
2974
+ const profile = loadProjectAgentProfile(role, projectRoot);
2975
+ const template = isBuiltIn ? target[role] : profile ? resolveRole(profile.baseRole, resolving) ?? target["generic"] : target["generic"];
2976
+ resolving.delete(role);
2977
+ if (!template) return void 0;
2978
+ const base = isBuiltIn ? template : {
2979
+ ...template,
2980
+ id: role,
2981
+ role,
2982
+ name: profile?.name ?? role,
2983
+ ...profile ? {
2984
+ dispatch: {
2985
+ summary: profile.purpose,
2986
+ keywords: [
2987
+ ...profile.taskTypes,
2988
+ ...new Set(
2989
+ `${profile.purpose} ${profile.taskTypes.join(" ")}`.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 3)
2990
+ )
2991
+ ]
2992
+ }
2993
+ } : {}
2994
+ };
2995
+ return applyProjectAgentConfig(base, loadProjectAgentConfig(role, projectRoot), {
2996
+ protectSystemRole: isBuiltIn && !profile
2997
+ });
2998
+ };
2999
+ return new Proxy(target, {
3000
+ get(current, property, receiver) {
3001
+ if (typeof property !== "string") return Reflect.get(current, property, receiver);
3002
+ return resolveRole(property) ?? Reflect.get(current, property, receiver);
3003
+ },
3004
+ has(current, property) {
3005
+ return typeof property === "string" ? Reflect.has(current, property) || resolveRole(property) !== void 0 : Reflect.has(current, property);
3006
+ },
3007
+ ownKeys(current) {
3008
+ return [.../* @__PURE__ */ new Set([...Reflect.ownKeys(current), ...listProjectAgentRoles(projectRoot)])];
3009
+ },
3010
+ getOwnPropertyDescriptor(current, property) {
3011
+ const descriptor = Reflect.getOwnPropertyDescriptor(current, property);
3012
+ if (descriptor || typeof property !== "string" || !resolveRole(property)) return descriptor;
3013
+ return {
3014
+ configurable: true,
3015
+ enumerable: true,
3016
+ writable: false,
3017
+ value: resolveRole(property)
3018
+ };
3169
3019
  }
3170
- }
3171
- const developedSkills = listProjectSkillAugmentations(role, projectRoot);
3172
- if (developedSkills.length > 0) {
3173
- parts.push(
3174
- `
3175
-
3176
- ## Project-developed skills
3020
+ });
3021
+ }
3177
3022
 
3178
- These skills have been extended with practice learned in this project: ${developedSkills.map((skill) => `\`${skill}\``).join(", ")}. Their project addendum is attached to the skill body \u2014 follow it over the generic method when the two differ.`
3179
- );
3023
+ // src/coordination/agents/project-agent-capture.ts
3024
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
3025
+ import * as path11 from "node:path";
3026
+ function loadProjectAgentLearned(role, projectRoot) {
3027
+ const learnedPath3 = path11.join(roleDir(role, projectRoot), "learned.md");
3028
+ try {
3029
+ return readFileSync9(learnedPath3, "utf8").trim();
3030
+ } catch {
3031
+ return "";
3180
3032
  }
3181
- const additions = parts.slice(1).join("\n\n").trim();
3182
- if (!additions) return cleanBase;
3183
- return `${cleanBase}
3184
-
3185
- ${contextStart}
3186
- ${additions}
3187
- ${contextEnd}`;
3033
+ }
3034
+ function listProjectAgentLearnedEntries(role, projectRoot) {
3035
+ return splitLearnedEntries(loadProjectAgentLearned(role, projectRoot));
3036
+ }
3037
+ function parseStructuredLearnedEntries(role, projectRoot) {
3038
+ const raw = loadProjectAgentLearned(role, projectRoot);
3039
+ return parseStructuredLearnedEntriesFromContent(
3040
+ raw,
3041
+ listProjectAgentLearnedEntries(role, projectRoot)
3042
+ );
3188
3043
  }
3189
3044
  function resolveCaptureKey(role, projectRoot) {
3190
3045
  return `${path11.resolve(projectRoot ?? process.cwd())}\0${assertProjectAgentRole(role)}`;
@@ -3286,16 +3141,6 @@ function detectLearnedConflicts(projectRoot) {
3286
3141
  }
3287
3142
  return conflicts.sort((a, b) => b.similarity - a.similarity);
3288
3143
  }
3289
- function listProjectAgentLearnedEntries(role, projectRoot) {
3290
- return splitLearnedEntries(loadProjectAgentLearned(role, projectRoot));
3291
- }
3292
- function parseStructuredLearnedEntries(role, projectRoot) {
3293
- const raw = loadProjectAgentLearned(role, projectRoot);
3294
- return parseStructuredLearnedEntriesFromContent(
3295
- raw,
3296
- listProjectAgentLearnedEntries(role, projectRoot)
3297
- );
3298
- }
3299
3144
  function captureLearnedFromAgentOutput(output, role, projectRoot, isManual = false) {
3300
3145
  return captureLearnedFromAgentOutputDetailed(output, role, projectRoot, isManual).captured;
3301
3146
  }
@@ -3443,11 +3288,188 @@ function hintLearnedNeedsSummarization(role, projectRoot) {
3443
3288
  return `Learned wisdom for role "${role}" is ${bytes} B (soft limit ${LEARNED_SOFT_LIMIT}). Schedule a low-priority consolidation pass.`;
3444
3289
  }
3445
3290
 
3291
+ // src/coordination/agents/project-agent-identity.ts
3292
+ function loadProjectAgentIdentity(role, projectRoot) {
3293
+ const identityPath = path12.join(roleDir(role, projectRoot), "identity.md");
3294
+ try {
3295
+ return readFileSync10(identityPath, "utf8").trim();
3296
+ } catch {
3297
+ return "";
3298
+ }
3299
+ }
3300
+ function loadRoleKnowledgeManifest(role, projectRoot) {
3301
+ const projectPath = path12.join(roleDir(role, projectRoot), "knowledge.json");
3302
+ try {
3303
+ const raw = readFileSync10(projectPath, "utf8");
3304
+ return JSON.parse(raw);
3305
+ } catch {
3306
+ return BUILT_IN_KNOWLEDGE_MANIFESTS[role];
3307
+ }
3308
+ }
3309
+ function buildProjectContextualizedPrompt(basePrompt, role, projectRoot, options = {}) {
3310
+ const contextStart = "<!-- wrongstack:project-agent-context:start -->";
3311
+ const contextEnd = "<!-- wrongstack:project-agent-context:end -->";
3312
+ if (process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"]) return basePrompt;
3313
+ const cleanBase = basePrompt.replace(
3314
+ /\s*<!-- wrongstack:project-agent-context:start -->[\s\S]*?<!-- wrongstack:project-agent-context:end -->\s*/g,
3315
+ "\n"
3316
+ ).trim();
3317
+ const parts = [cleanBase];
3318
+ const identity = options.identityOverride ?? loadProjectAgentIdentity(role, projectRoot);
3319
+ if (identity) {
3320
+ parts.push(`
3321
+
3322
+ # Project custom identity
3323
+
3324
+ ${identity}`);
3325
+ }
3326
+ const learningPolicy = loadProjectAgentLearningPolicy(role, projectRoot);
3327
+ let learnedContent = "";
3328
+ let learnedLabel = "";
3329
+ if (learningPolicy.enabled) {
3330
+ const consolidated = loadProjectAgentConsolidated(role, projectRoot);
3331
+ const rawLearned = loadProjectAgentLearned(role, projectRoot);
3332
+ if (consolidated) {
3333
+ const meta = loadConsolidationMetadata(role, projectRoot);
3334
+ const rawEntries = parseStructuredLearnedEntriesFromContent(
3335
+ rawLearned,
3336
+ splitLearnedEntries(rawLearned)
3337
+ );
3338
+ const rawBytes = Buffer.byteLength(rawLearned, "utf8");
3339
+ const freshEntries = meta === void 0 ? [] : rawEntries.filter((entry) => entry.capturedAt > meta.consolidatedAt);
3340
+ const stale = meta === void 0 || freshEntries.length > 0 || rawEntries.length > meta.sourceEntryCount || rawBytes > meta.sourceBytes;
3341
+ if (!stale) {
3342
+ learnedContent = consolidated;
3343
+ learnedLabel = "Consolidated knowledge for this project";
3344
+ } else if (freshEntries.length > 0) {
3345
+ const delta = freshEntries.map((entry) => `- **${entry.what}**`).join("\n");
3346
+ learnedContent = `${consolidated}
3347
+
3348
+ ---
3349
+
3350
+ ## Recently captured (pending next optimization)
3351
+
3352
+ ${delta}`;
3353
+ learnedLabel = "Consolidated knowledge for this project";
3354
+ } else {
3355
+ learnedContent = rawLearned;
3356
+ learnedLabel = "Learned instructions for this project (structured: what / why / how)";
3357
+ }
3358
+ } else {
3359
+ learnedContent = rawLearned;
3360
+ learnedLabel = "Learned instructions for this project (structured: what / why / how)";
3361
+ }
3362
+ }
3363
+ if (learnedContent) {
3364
+ const meaningful = learnedContent.replace(/<!--[\s\S]*?-->/g, "").trim();
3365
+ if (meaningful.length > 0) {
3366
+ parts.push(`
3367
+
3368
+ # ${learnedLabel}
3369
+
3370
+ ${learnedContent}`);
3371
+ }
3372
+ }
3373
+ const effectiveProjectRoot = projectRoot || process.cwd();
3374
+ const learnedFilePath = path12.join(
3375
+ effectiveProjectRoot,
3376
+ ".wrongstack",
3377
+ "agents",
3378
+ role,
3379
+ "learned.md"
3380
+ );
3381
+ if (learningPolicy.enabled) {
3382
+ parts.push(
3383
+ `
3384
+
3385
+ ## Knowledge capture
3386
+
3387
+ The file below stores **learning data for this project's "${role}" agent** \u2014 not your memory, not a session log. It is read back into the system prompt of every future "${role}" invocation, so each entry must teach a future agent how to act. On every capture the runtime **merges your new entry with every prior entry and rewrites the whole buffer as a structured instruction list** \u2014 grouped by category ("What to do", "What to avoid", "Patterns to follow", "Project facts"), with each item decomposed into **what** (the rule), **why** (the reason), and **how** (the concrete commands, file paths, or package names that anchor the rule). When you discover a durable principle that future invocations should follow, end your response with a \`## LEARNED\` block. The runtime persists it to:
3388
+
3389
+ \`\`\`
3390
+ ${learnedFilePath}
3391
+ \`\`\`
3392
+
3393
+ **Write directives, not narratives.** Each LEARNED entry should be:
3394
+
3395
+ - A **rule or principle** that applies across sessions, not a description of what happened in this one.
3396
+ - A short **imperative or statement** (1\u20133 sentences), starting with a verb like "Always verify\u2026", "Use X for Y", "Avoid\u2026", "Never assume\u2026".
3397
+ - **Generic** \u2014 no commit SHAs, timestamps, specific line numbers, or PR/issue numbers. File paths, package names, and command names are fine when they anchor the lesson.
3398
+ - **Self-contained** \u2014 understandable without the surrounding session context.
3399
+ - **Front-load concrete anchors** \u2014 commands in backticks, package names like \`@wrongstack/core\`, file paths like \`packages/core/src/.../foo.ts\`. The structured-list renderer extracts these as the "how" for the entry.
3400
+
3401
+ **Your directives are scored against real outcomes.** After every task the runtime checks which stored directives were actually exercised \u2014 it matches their anchors against the report \u2014 and folds that task's success or failure into each one's record. A directive that keeps correlating with success outlives newer arrivals and survives rewording; one that has been exercised repeatedly and kept correlating with failure is retired and stops being injected. Two consequences for how you write them: anchors are what make a directive *measurable*, not just runnable, so an anchorless directive can never earn a record; and a directive you are unsure about costs nothing to write, because the loop will find out.
3402
+
3403
+ **Tag the skill you are developing.** When a directive refines one of your skills, mark it: \`## LEARNED [skill: testing]\`. Tagged directives are distilled into that skill's project addendum, so the lesson arrives as part of the skill itself on every future run instead of as a loose fact. Untagged directives are routed automatically when the wording makes the target obvious, and stay role-level otherwise.
3404
+
3405
+ **Bad** (session log \u2014 rejected at capture time):
3406
+
3407
+ \`\`\`
3408
+ ## LEARNED
3409
+ When I worked on the telegram plugin today, commit 9c7682b84 had a race condition in poll-lock at line 42 because writeFileSync wasn't using the 'wx' flag.
3410
+ \`\`\`
3411
+
3412
+ **Good** (directive \u2014 persists, then merges into the structured list):
3413
+
3414
+ \`\`\`
3415
+ ## LEARNED
3416
+ Always use the 'wx' (exclusive create) flag with writeFileSync when implementing concurrent lock acquisition in \`packages/core/src/.../poll-lock.ts\` \u2014 filesystem-level atomicity guarantees only one writer wins.
3417
+ \`\`\`
3418
+
3419
+ Capture only durable, cross-session knowledge \u2014 never ephemeral task details.`
3420
+ );
3421
+ }
3422
+ const knowledge = loadRoleKnowledgeManifest(role, projectRoot);
3423
+ if (knowledge) {
3424
+ const knowledgeSections = [];
3425
+ if (knowledge.checklist.length > 0) {
3426
+ knowledgeSections.push(
3427
+ `Verify these before answering:
3428
+ ${knowledge.checklist.map((item) => `- ${item}`).join("\n")}`
3429
+ );
3430
+ }
3431
+ const liveQueries = Object.entries(knowledge.liveQueries ?? {});
3432
+ if (liveQueries.length > 0) {
3433
+ const threshold = Number.isFinite(knowledge.verifyThreshold) ? knowledge.verifyThreshold : 0.5;
3434
+ knowledgeSections.push(
3435
+ `When your confidence in any of the following is below ${threshold}, fetch the live value rather than recalling it:
3436
+ ` + liveQueries.map(
3437
+ ([topic, query]) => `- **${topic}** \u2014 ${query.description}: \`${query.registry}\` \u2192 field \`${query.key}\``
3438
+ ).join("\n")
3439
+ );
3440
+ }
3441
+ if (knowledgeSections.length > 0) {
3442
+ parts.push(`
3443
+
3444
+ ## Current knowledge requirements
3445
+
3446
+ ${knowledgeSections.join("\n\n")}`);
3447
+ }
3448
+ }
3449
+ const developedSkills = listProjectSkillAugmentations(role, projectRoot);
3450
+ if (developedSkills.length > 0) {
3451
+ parts.push(
3452
+ `
3453
+
3454
+ ## Project-developed skills
3455
+
3456
+ These skills have been extended with practice learned in this project: ${developedSkills.map((skill) => `\`${skill}\``).join(", ")}. Their project addendum is attached to the skill body \u2014 follow it over the generic method when the two differ.`
3457
+ );
3458
+ }
3459
+ const additions = parts.slice(1).join("\n\n").trim();
3460
+ if (!additions) return cleanBase;
3461
+ return `${cleanBase}
3462
+
3463
+ ${contextStart}
3464
+ ${additions}
3465
+ ${contextEnd}`;
3466
+ }
3467
+
3446
3468
  // src/coordination/agents/agent-prompts.ts
3447
3469
  var promptCache = /* @__PURE__ */ new Map();
3448
3470
  var candidateCache = /* @__PURE__ */ new Map();
3449
3471
  var TECH_VERSION_POLICY_KEY = "\0__tech_version_policy__";
3450
- var techVersionPolicyBody;
3472
+ var techVersionPolicyCache = /* @__PURE__ */ new Map();
3451
3473
  var INLINE_TECH_VERSION_POLICY = `# Mandatory modern technology policy
3452
3474
 
3453
3475
  You are bound to the current stable version of every library, framework and
@@ -3514,63 +3536,89 @@ package you use. The latest stable is preferred; the latest *published* is
3514
3536
  **"This package does not exist on the registry."** and the role must
3515
3537
  propose a real, maintained alternative.`;
3516
3538
  function loadTechVersionPolicy() {
3517
- if (techVersionPolicyBody !== void 0) return techVersionPolicyBody;
3518
- const fromDisk = policyBody();
3519
- techVersionPolicyBody = fromDisk || INLINE_TECH_VERSION_POLICY;
3520
- return techVersionPolicyBody;
3539
+ const envDir = process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"] ?? "";
3540
+ const cached = techVersionPolicyCache.get(envDir);
3541
+ if (cached !== void 0) return cached;
3542
+ const fromDisk = policyBody(envDir);
3543
+ const body = fromDisk || INLINE_TECH_VERSION_POLICY;
3544
+ techVersionPolicyCache.set(envDir, body);
3545
+ return body;
3521
3546
  }
3522
- function policyCandidateDirs() {
3523
- const here = path12.dirname(fileURLToPath(import.meta.url));
3547
+ function policyCandidateDirs(envDir) {
3548
+ const here = path13.dirname(fileURLToPath(import.meta.url));
3524
3549
  const profileInstructions = resolveWstackPaths({ projectRoot: process.cwd() }).globalInstructions;
3525
3550
  return [
3526
- path12.resolve(here, "../../../../instructions/agents"),
3527
- path12.resolve(here, "../../../instructions/agents"),
3528
- path12.resolve(here, "../../instructions/agents"),
3529
- path12.resolve(here, "../instructions/agents"),
3530
- path12.resolve(here, "instructions/agents"),
3531
- path12.join(profileInstructions, "agents"),
3551
+ ...envDir ? [path13.resolve(envDir)] : [],
3552
+ path13.resolve(here, "../../../../instructions/agents"),
3553
+ path13.resolve(here, "../../../instructions/agents"),
3554
+ path13.resolve(here, "../../instructions/agents"),
3555
+ path13.resolve(here, "../instructions/agents"),
3556
+ path13.resolve(here, "instructions/agents"),
3557
+ path13.join(profileInstructions, "agents"),
3532
3558
  profileInstructions
3533
3559
  ].filter((dir, index, all) => all.indexOf(dir) === index);
3534
3560
  }
3535
- function policyBody() {
3536
- const here = path12.dirname(fileURLToPath(import.meta.url));
3561
+ function policyBody(envDir) {
3562
+ const here = path13.dirname(fileURLToPath(import.meta.url));
3537
3563
  const roots = [
3538
- path12.resolve(here, "_policy/tech-version.md"),
3539
- path12.resolve(here, "../_policy/tech-version.md"),
3540
- path12.resolve(here, "../../../instructions/agents/_policy/tech-version.md"),
3541
- path12.resolve(here, "../../instructions/agents/_policy/tech-version.md"),
3542
- path12.resolve(here, "../instructions/agents/_policy/tech-version.md"),
3543
- path12.resolve(here, "../../../../instructions/agents/_policy/tech-version.md"),
3544
- path12.resolve(here, "instructions/agents/_policy/tech-version.md")
3564
+ path13.resolve(here, "_policy/tech-version.md"),
3565
+ path13.resolve(here, "../_policy/tech-version.md"),
3566
+ path13.resolve(here, "../../../instructions/agents/_policy/tech-version.md"),
3567
+ path13.resolve(here, "../../instructions/agents/_policy/tech-version.md"),
3568
+ path13.resolve(here, "../instructions/agents/_policy/tech-version.md"),
3569
+ path13.resolve(here, "../../../../instructions/agents/_policy/tech-version.md"),
3570
+ path13.resolve(here, "instructions/agents/_policy/tech-version.md")
3545
3571
  ];
3572
+ if (envDir) {
3573
+ roots.unshift(path13.resolve(envDir, "_policy/tech-version.md"));
3574
+ }
3546
3575
  for (const file of roots) {
3547
3576
  try {
3548
- return readFileSync10(file, "utf8").trim();
3577
+ return readFileSync11(file, "utf8").trim();
3549
3578
  } catch {
3550
3579
  }
3551
3580
  }
3552
- for (const dir of policyCandidateDirs()) {
3581
+ for (const dir of policyCandidateDirs(envDir)) {
3553
3582
  try {
3554
- return readFileSync10(path12.join(dir, "_policy", "tech-version.md"), "utf8").trim();
3583
+ return readFileSync11(path13.join(dir, "_policy", "tech-version.md"), "utf8").trim();
3555
3584
  } catch {
3556
3585
  }
3557
3586
  }
3558
3587
  return "";
3559
3588
  }
3589
+ var OVERLAY_FILES = ["identity.md", "learned.md", "consolidated.md"];
3590
+ function agentOverlayFingerprint(id, projectRoot) {
3591
+ if (process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"]) return "";
3592
+ let roleDir2;
3593
+ try {
3594
+ roleDir2 = path13.join(projectRoot, ".wrongstack", "agents", assertProjectAgentRole(id));
3595
+ } catch {
3596
+ return "";
3597
+ }
3598
+ let fp = "";
3599
+ for (const name of OVERLAY_FILES) {
3600
+ try {
3601
+ const st = statSync2(path13.join(roleDir2, name));
3602
+ fp += `${name}:${st.mtimeMs}:${st.size};`;
3603
+ } catch {
3604
+ }
3605
+ }
3606
+ return fp;
3607
+ }
3560
3608
  function agentPrompt(id) {
3561
3609
  if (id === TECH_VERSION_POLICY_KEY) return loadTechVersionPolicy();
3562
3610
  const envDir = process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"] ?? "";
3563
3611
  const policyOn = process.env["WRONGSTACK_AGENT_POLICY"] === "on";
3564
3612
  const cacheBypass = globalThis.__WS_DISABLE_PROMPT_CACHE__ === true;
3565
3613
  const promptProjectRoot = process.env["WRONGSTACK_PROJECT_ROOT"] || process.cwd();
3566
- const cacheKey = `${envDir}\0${policyOn ? 1 : 0}\0${promptProjectRoot}\0${id}`;
3614
+ const cacheKey = `${envDir}\0${policyOn ? 1 : 0}\0${promptProjectRoot}\0${agentOverlayFingerprint(id, promptProjectRoot)}\0${id}`;
3567
3615
  const cached = cacheBypass ? void 0 : promptCache.get(cacheKey);
3568
3616
  if (cached !== void 0) return cached;
3569
3617
  const fileName = `${id}.md`;
3570
3618
  let resolved = "";
3571
- for (const dir of agentPromptDirCandidates(envDir)) {
3619
+ for (const dir of agentPromptDirCandidates(envDir, promptProjectRoot)) {
3572
3620
  try {
3573
- resolved = readFileSync10(path12.join(dir, fileName), "utf8").trim();
3621
+ resolved = readFileSync11(path13.join(dir, fileName), "utf8").trim();
3574
3622
  break;
3575
3623
  } catch {
3576
3624
  }
@@ -3591,21 +3639,21 @@ ${policy}`;
3591
3639
  if (!cacheBypass) promptCache.set(cacheKey, resolved);
3592
3640
  return resolved;
3593
3641
  }
3594
- function agentPromptDirCandidates(envDir) {
3595
- const profileInstructions = resolveWstackPaths({ projectRoot: process.cwd() }).globalInstructions;
3596
- const candKey = `${envDir}\0${profileInstructions}`;
3642
+ function agentPromptDirCandidates(envDir, projectRoot) {
3643
+ const profileInstructions = resolveWstackPaths({ projectRoot }).globalInstructions;
3644
+ const candKey = `${envDir}\0${projectRoot}\0${profileInstructions}`;
3597
3645
  const cached = candidateCache.get(candKey);
3598
3646
  if (cached !== void 0) return cached;
3599
- const here = path12.dirname(fileURLToPath(import.meta.url));
3647
+ const here = path13.dirname(fileURLToPath(import.meta.url));
3600
3648
  const explicitDir = envDir || void 0;
3601
3649
  const candidates = [
3602
- ...explicitDir ? [path12.resolve(explicitDir)] : [],
3603
- path12.join(profileInstructions, "agents"),
3604
- path12.resolve(here, "../../../../instructions/agents"),
3605
- path12.resolve(here, "../../../instructions/agents"),
3606
- path12.resolve(here, "../../instructions/agents"),
3607
- path12.resolve(here, "../instructions/agents"),
3608
- path12.resolve(here, "instructions/agents")
3650
+ ...explicitDir ? [path13.resolve(explicitDir)] : [],
3651
+ path13.join(profileInstructions, "agents"),
3652
+ path13.resolve(here, "../../../../instructions/agents"),
3653
+ path13.resolve(here, "../../../instructions/agents"),
3654
+ path13.resolve(here, "../../instructions/agents"),
3655
+ path13.resolve(here, "../instructions/agents"),
3656
+ path13.resolve(here, "instructions/agents")
3609
3657
  ];
3610
3658
  const ordered = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
3611
3659
  candidateCache.set(candKey, ordered);
@@ -6227,7 +6275,7 @@ export {
6227
6275
  directiveWasApplied,
6228
6276
  enforceLearnedBudget,
6229
6277
  evaluateAutoOptimize,
6230
- existsSync4 as existsSync,
6278
+ existsSync5 as existsSync,
6231
6279
  getAgentDefinition,
6232
6280
  getProjectAgentLearnStats,
6233
6281
  hintLearnedNeedsSummarization,