agent-working-memory 0.10.0 → 0.12.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 (136) hide show
  1. package/README.md +118 -19
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +63 -3
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adapters/common.d.ts.map +1 -1
  6. package/dist/adapters/common.js +329 -302
  7. package/dist/adapters/common.js.map +1 -1
  8. package/dist/api/routes.d.ts.map +1 -1
  9. package/dist/api/routes.js +31 -8
  10. package/dist/api/routes.js.map +1 -1
  11. package/dist/cli/migrate.js +29 -29
  12. package/dist/cli.js +82 -2
  13. package/dist/cli.js.map +1 -1
  14. package/dist/coordination/circuit-breaker.js +23 -23
  15. package/dist/coordination/routes.d.ts.map +1 -1
  16. package/dist/coordination/routes.js +174 -170
  17. package/dist/coordination/routes.js.map +1 -1
  18. package/dist/core/embeddings.d.ts.map +1 -1
  19. package/dist/core/embeddings.js +3 -0
  20. package/dist/core/embeddings.js.map +1 -1
  21. package/dist/core/entity-extract.d.ts +3 -0
  22. package/dist/core/entity-extract.d.ts.map +1 -0
  23. package/dist/core/entity-extract.js +47 -0
  24. package/dist/core/entity-extract.js.map +1 -0
  25. package/dist/core/salience.d.ts.map +1 -1
  26. package/dist/core/salience.js +14 -2
  27. package/dist/core/salience.js.map +1 -1
  28. package/dist/core/whoami.d.ts +24 -0
  29. package/dist/core/whoami.d.ts.map +1 -0
  30. package/dist/core/whoami.js +66 -0
  31. package/dist/core/whoami.js.map +1 -0
  32. package/dist/core/write-pipeline.d.ts +9 -0
  33. package/dist/core/write-pipeline.d.ts.map +1 -1
  34. package/dist/core/write-pipeline.js +109 -68
  35. package/dist/core/write-pipeline.js.map +1 -1
  36. package/dist/core/write-telemetry.d.ts +33 -0
  37. package/dist/core/write-telemetry.d.ts.map +1 -0
  38. package/dist/core/write-telemetry.js +110 -0
  39. package/dist/core/write-telemetry.js.map +1 -0
  40. package/dist/engine/activation.d.ts +22 -12
  41. package/dist/engine/activation.d.ts.map +1 -1
  42. package/dist/engine/activation.js +133 -17
  43. package/dist/engine/activation.js.map +1 -1
  44. package/dist/engine/consolidation-scheduler.d.ts +1 -1
  45. package/dist/engine/consolidation-scheduler.js +1 -1
  46. package/dist/engine/consolidation.d.ts +1 -0
  47. package/dist/engine/consolidation.d.ts.map +1 -1
  48. package/dist/engine/consolidation.js +18 -0
  49. package/dist/engine/consolidation.js.map +1 -1
  50. package/dist/engine/eval.d.ts.map +1 -1
  51. package/dist/engine/eval.js +5 -1
  52. package/dist/engine/eval.js.map +1 -1
  53. package/dist/index.js +21 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/mcp.d.ts +2 -1
  56. package/dist/mcp.d.ts.map +1 -1
  57. package/dist/mcp.js +212 -97
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/onboard/index.d.ts +68 -0
  60. package/dist/onboard/index.d.ts.map +1 -0
  61. package/dist/onboard/index.js +265 -0
  62. package/dist/onboard/index.js.map +1 -0
  63. package/dist/recipes/index.d.ts +57 -0
  64. package/dist/recipes/index.d.ts.map +1 -0
  65. package/dist/recipes/index.js +81 -0
  66. package/dist/recipes/index.js.map +1 -0
  67. package/dist/storage/pglite-schema.d.ts.map +1 -1
  68. package/dist/storage/pglite-schema.js +170 -143
  69. package/dist/storage/pglite-schema.js.map +1 -1
  70. package/dist/storage/pglite.d.ts +5 -0
  71. package/dist/storage/pglite.d.ts.map +1 -1
  72. package/dist/storage/pglite.js +180 -138
  73. package/dist/storage/pglite.js.map +1 -1
  74. package/dist/storage/postgres.d.ts +5 -0
  75. package/dist/storage/postgres.d.ts.map +1 -1
  76. package/dist/storage/postgres.js +44 -2
  77. package/dist/storage/postgres.js.map +1 -1
  78. package/dist/storage/sqlite.d.ts +9 -0
  79. package/dist/storage/sqlite.d.ts.map +1 -1
  80. package/dist/storage/sqlite.js +394 -326
  81. package/dist/storage/sqlite.js.map +1 -1
  82. package/dist/types/engram.d.ts +14 -0
  83. package/dist/types/engram.d.ts.map +1 -1
  84. package/dist/types/engram.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +27 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +9 -1
  90. package/src/adapters/claude-code.ts +66 -3
  91. package/src/adapters/common.ts +538 -511
  92. package/src/api/index.ts +3 -3
  93. package/src/api/routes.ts +999 -970
  94. package/src/cli/migrate.ts +307 -307
  95. package/src/cli.ts +77 -2
  96. package/src/coordination/circuit-breaker.ts +83 -83
  97. package/src/coordination/failure-modes.ts +50 -50
  98. package/src/coordination/routes.ts +2155 -2150
  99. package/src/core/decay.ts +63 -63
  100. package/src/core/embeddings.ts +113 -110
  101. package/src/core/entity-extract.ts +47 -0
  102. package/src/core/index.ts +5 -5
  103. package/src/core/logger.ts +36 -36
  104. package/src/core/ml-worker-entry.ts +194 -194
  105. package/src/core/ml-worker.ts +281 -281
  106. package/src/core/query-expander.ts +122 -122
  107. package/src/core/reranker.ts +119 -119
  108. package/src/core/salience.ts +529 -514
  109. package/src/core/whoami.ts +92 -0
  110. package/src/core/write-pipeline.ts +60 -8
  111. package/src/core/write-telemetry.ts +131 -0
  112. package/src/engine/activation.ts +1468 -1369
  113. package/src/engine/confidence.ts +120 -120
  114. package/src/engine/consolidation-scheduler.ts +242 -242
  115. package/src/engine/consolidation.ts +887 -869
  116. package/src/engine/eval.ts +107 -102
  117. package/src/engine/eviction.ts +101 -101
  118. package/src/engine/index.ts +8 -8
  119. package/src/engine/retraction.ts +366 -366
  120. package/src/engine/staging.ts +74 -74
  121. package/src/index.ts +248 -226
  122. package/src/mcp.ts +1341 -1211
  123. package/src/onboard/index.ts +298 -0
  124. package/src/recipes/index.ts +125 -0
  125. package/src/storage/index.ts +3 -3
  126. package/src/storage/pglite-schema.ts +193 -166
  127. package/src/storage/pglite.ts +1420 -1372
  128. package/src/storage/postgres.ts +50 -2
  129. package/src/storage/sqlite.ts +1936 -1861
  130. package/src/storage/store.ts +80 -80
  131. package/src/types/agent.ts +67 -67
  132. package/src/types/checkpoint.ts +46 -46
  133. package/src/types/engram.ts +22 -0
  134. package/src/types/eval.ts +100 -100
  135. package/src/types/index.ts +6 -6
  136. package/src/version.ts +26 -0
package/src/cli.ts CHANGED
@@ -13,10 +13,12 @@
13
13
  */
14
14
 
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
16
- import { resolve, join, dirname } from 'node:path';
16
+ import { resolve, join, dirname, basename } from 'node:path';
17
17
  import { execSync } from 'node:child_process';
18
18
  import { randomUUID } from 'node:crypto';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { VERSION } from './version.js';
21
+ import { runOnboard, ONBOARD_SKILL } from './onboard/index.js';
20
22
 
21
23
  const __filename = fileURLToPath(import.meta.url);
22
24
  const __dirname = dirname(__filename);
@@ -139,6 +141,9 @@ async function setup() {
139
141
  const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
140
142
  const hooksAction = adapter.writeHooks(ctx, skipHooks);
141
143
 
144
+ // Seed the onboarding skill so a cold store can teach the agent how to warm itself.
145
+ const skillAction = await seedOnboardSkill(ctx.dbPath, ctx.agentId);
146
+
142
147
  console.log(`
143
148
  AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
144
149
 
@@ -147,6 +152,7 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
147
152
  ${configAction}
148
153
  ${instructionsAction}
149
154
  ${hooksAction}
155
+ ${skillAction}
150
156
 
151
157
  Next steps:
152
158
  1. Restart ${adapter.name} to pick up the MCP server
@@ -352,7 +358,7 @@ async function exportMemories() {
352
358
  }
353
359
 
354
360
  const exportData = {
355
- version: '0.9.2',
361
+ version: VERSION,
356
362
  exported_at: new Date().toISOString(),
357
363
  source_backend: backend,
358
364
  agent_filter: agentFilter,
@@ -711,6 +717,72 @@ async function migrateCmd() {
711
717
  }
712
718
  }
713
719
 
720
+ // ─── ONBOARD ──────────────────────────────────────
721
+
722
+ /**
723
+ * Seed the onboarding skill as a canonical memory (idempotent). This is what lets
724
+ * a cold store teach the host agent how to warm-start itself — the agent recalls
725
+ * the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
726
+ */
727
+ async function seedOnboardSkill(dbPath: string, agentId: string): Promise<string> {
728
+ try {
729
+ const { store, close } = await openCliStore(dbPath);
730
+ try {
731
+ const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
732
+ if (existing) return 'Onboarding skill: already present';
733
+ await store.createEngram({
734
+ agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
735
+ tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
736
+ });
737
+ return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
738
+ } finally {
739
+ await close();
740
+ }
741
+ } catch (e: any) {
742
+ return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
743
+ }
744
+ }
745
+
746
+ function onboardCmd() {
747
+ const docs: string[] = [];
748
+ let repo: string | undefined;
749
+ let project = '';
750
+ let agentId = '';
751
+ let purpose: string | undefined;
752
+ let outDir = resolve(process.cwd(), '.awm');
753
+
754
+ for (let i = 1; i < args.length; i++) {
755
+ const a = args[i];
756
+ if (a === '--repo' && args[i + 1]) repo = args[++i];
757
+ else if (a === '--project' && args[i + 1]) project = args[++i];
758
+ else if (a === '--agent' && args[i + 1]) agentId = args[++i];
759
+ else if (a === '--purpose' && args[i + 1]) purpose = args[++i];
760
+ else if (a === '--out' && args[i + 1]) outDir = resolve(args[++i]);
761
+ else if (!a.startsWith('--')) docs.push(a);
762
+ }
763
+
764
+ // Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
765
+ if (docs.length === 0) docs.push(repo ?? process.cwd());
766
+ if (!project) project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
767
+ if (!agentId) agentId = project;
768
+
769
+ const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
770
+ console.log(`
771
+ AWM onboard — warm-start pack for "${project}"
772
+
773
+ Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
774
+ Extracted: ${count} candidate memories (agent: ${agentId})
775
+
776
+ Review: ${reviewPath}
777
+ Pack: ${packPath}
778
+
779
+ Next:
780
+ 1. Edit the review file / pack as needed (delete noise, answer the interview questions).
781
+ 2. Load it: awm import ${packPath} --db <path> --dedupe
782
+ (embeddings backfill on the first consolidation — recall is warm immediately after)
783
+ `.trimEnd());
784
+ }
785
+
714
786
  // ─── Dispatch ──────────────────────────────────────
715
787
 
716
788
  switch (command) {
@@ -741,6 +813,9 @@ switch (command) {
741
813
  case 'migrate':
742
814
  await migrateCmd();
743
815
  break;
816
+ case 'onboard':
817
+ onboardCmd();
818
+ break;
744
819
  case '--help':
745
820
  case '-h':
746
821
  case undefined:
@@ -1,83 +1,83 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Per-worker CircuitBreaker for the coordination control layer.
5
- * Prevents chronically-stale workers from poisoning the assignment queue.
6
- * Part of AWM 0.8.1 — additive, no breaking changes.
7
- *
8
- * States:
9
- * closed — normal operation
10
- * open — worker blocked after FAILURE_THRESHOLD consecutive failures
11
- * half_open — probe window (30s after open), allows one assignment attempt
12
- */
13
-
14
- import type Database from 'better-sqlite3';
15
-
16
- export type CircuitState = 'closed' | 'open' | 'half_open';
17
-
18
- const FAILURE_THRESHOLD = 5;
19
- const HALF_OPEN_DELAY_MS = 30_000;
20
-
21
- /** Record a worker failure. Opens the circuit when consecutive failures hit the threshold. */
22
- export function recordFailure(db: Database.Database, agentId: string): void {
23
- db.prepare(`
24
- INSERT INTO coord_circuit_state (agent_id, consecutive_failures, last_transition_at)
25
- VALUES (?, 1, datetime('now'))
26
- ON CONFLICT(agent_id) DO UPDATE SET
27
- consecutive_failures = consecutive_failures + 1,
28
- state = CASE
29
- WHEN consecutive_failures + 1 >= ${FAILURE_THRESHOLD} THEN 'open'
30
- ELSE state
31
- END,
32
- opened_at = CASE
33
- WHEN consecutive_failures + 1 >= ${FAILURE_THRESHOLD} AND (state != 'open' OR opened_at IS NULL)
34
- THEN datetime('now')
35
- ELSE opened_at
36
- END,
37
- last_transition_at = datetime('now')
38
- `).run(agentId);
39
- }
40
-
41
- /** Record a worker success. Resets to closed regardless of prior state. */
42
- export function recordSuccess(db: Database.Database, agentId: string): void {
43
- db.prepare(`
44
- INSERT INTO coord_circuit_state (agent_id, state, consecutive_failures, last_transition_at)
45
- VALUES (?, 'closed', 0, datetime('now'))
46
- ON CONFLICT(agent_id) DO UPDATE SET
47
- state = 'closed',
48
- consecutive_failures = 0,
49
- opened_at = NULL,
50
- last_transition_at = datetime('now')
51
- `).run(agentId);
52
- }
53
-
54
- /**
55
- * Get current circuit state for a worker.
56
- * If the circuit has been open for >30s, auto-transitions to half_open.
57
- */
58
- export function getState(db: Database.Database, agentId: string): CircuitState {
59
- const row = db.prepare(
60
- `SELECT state, opened_at FROM coord_circuit_state WHERE agent_id = ?`
61
- ).get(agentId) as { state: string; opened_at: string | null } | undefined;
62
-
63
- if (!row || row.state === 'closed') return 'closed';
64
- if (row.state === 'half_open') return 'half_open';
65
-
66
- // open — check if half-open window has elapsed
67
- if (row.state === 'open' && row.opened_at) {
68
- const openedAt = new Date(row.opened_at.endsWith('Z') ? row.opened_at : row.opened_at + 'Z').getTime();
69
- if (Date.now() - openedAt > HALF_OPEN_DELAY_MS) {
70
- db.prepare(
71
- `UPDATE coord_circuit_state SET state = 'half_open', last_transition_at = datetime('now') WHERE agent_id = ?`
72
- ).run(agentId);
73
- return 'half_open';
74
- }
75
- }
76
-
77
- return 'open';
78
- }
79
-
80
- /** Returns true when the worker is eligible to receive an assignment. */
81
- export function isAvailable(db: Database.Database, agentId: string): boolean {
82
- return getState(db, agentId) !== 'open';
83
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Per-worker CircuitBreaker for the coordination control layer.
5
+ * Prevents chronically-stale workers from poisoning the assignment queue.
6
+ * Part of AWM 0.8.1 — additive, no breaking changes.
7
+ *
8
+ * States:
9
+ * closed — normal operation
10
+ * open — worker blocked after FAILURE_THRESHOLD consecutive failures
11
+ * half_open — probe window (30s after open), allows one assignment attempt
12
+ */
13
+
14
+ import type Database from 'better-sqlite3';
15
+
16
+ export type CircuitState = 'closed' | 'open' | 'half_open';
17
+
18
+ const FAILURE_THRESHOLD = 5;
19
+ const HALF_OPEN_DELAY_MS = 30_000;
20
+
21
+ /** Record a worker failure. Opens the circuit when consecutive failures hit the threshold. */
22
+ export function recordFailure(db: Database.Database, agentId: string): void {
23
+ db.prepare(`
24
+ INSERT INTO coord_circuit_state (agent_id, consecutive_failures, last_transition_at)
25
+ VALUES (?, 1, datetime('now'))
26
+ ON CONFLICT(agent_id) DO UPDATE SET
27
+ consecutive_failures = consecutive_failures + 1,
28
+ state = CASE
29
+ WHEN consecutive_failures + 1 >= ${FAILURE_THRESHOLD} THEN 'open'
30
+ ELSE state
31
+ END,
32
+ opened_at = CASE
33
+ WHEN consecutive_failures + 1 >= ${FAILURE_THRESHOLD} AND (state != 'open' OR opened_at IS NULL)
34
+ THEN datetime('now')
35
+ ELSE opened_at
36
+ END,
37
+ last_transition_at = datetime('now')
38
+ `).run(agentId);
39
+ }
40
+
41
+ /** Record a worker success. Resets to closed regardless of prior state. */
42
+ export function recordSuccess(db: Database.Database, agentId: string): void {
43
+ db.prepare(`
44
+ INSERT INTO coord_circuit_state (agent_id, state, consecutive_failures, last_transition_at)
45
+ VALUES (?, 'closed', 0, datetime('now'))
46
+ ON CONFLICT(agent_id) DO UPDATE SET
47
+ state = 'closed',
48
+ consecutive_failures = 0,
49
+ opened_at = NULL,
50
+ last_transition_at = datetime('now')
51
+ `).run(agentId);
52
+ }
53
+
54
+ /**
55
+ * Get current circuit state for a worker.
56
+ * If the circuit has been open for >30s, auto-transitions to half_open.
57
+ */
58
+ export function getState(db: Database.Database, agentId: string): CircuitState {
59
+ const row = db.prepare(
60
+ `SELECT state, opened_at FROM coord_circuit_state WHERE agent_id = ?`
61
+ ).get(agentId) as { state: string; opened_at: string | null } | undefined;
62
+
63
+ if (!row || row.state === 'closed') return 'closed';
64
+ if (row.state === 'half_open') return 'half_open';
65
+
66
+ // open — check if half-open window has elapsed
67
+ if (row.state === 'open' && row.opened_at) {
68
+ const openedAt = new Date(row.opened_at.endsWith('Z') ? row.opened_at : row.opened_at + 'Z').getTime();
69
+ if (Date.now() - openedAt > HALF_OPEN_DELAY_MS) {
70
+ db.prepare(
71
+ `UPDATE coord_circuit_state SET state = 'half_open', last_transition_at = datetime('now') WHERE agent_id = ?`
72
+ ).run(agentId);
73
+ return 'half_open';
74
+ }
75
+ }
76
+
77
+ return 'open';
78
+ }
79
+
80
+ /** Returns true when the worker is eligible to receive an assignment. */
81
+ export function isAvailable(db: Database.Database, agentId: string): boolean {
82
+ return getState(db, agentId) !== 'open';
83
+ }
@@ -1,50 +1,50 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * FailureMode taxonomy and mutation-hint map for the coordination control layer.
5
- * Part of AWM 0.8.1 — additive, no breaking changes.
6
- */
7
-
8
- export enum FailureMode {
9
- AGENT_STALE = 'agent_stale',
10
- TIMEOUT = 'timeout',
11
- OUTPUT_INVALID = 'output_invalid',
12
- TEST_FAIL = 'test_fail',
13
- LINT_FAIL = 'lint_fail',
14
- MERGE_CONFLICT = 'merge_conflict',
15
- UNKNOWN = 'unknown',
16
- }
17
-
18
- /** Classify a failure result string into one of the known modes. */
19
- export function classifyFailure(result: string | null): FailureMode {
20
- if (!result) return FailureMode.UNKNOWN;
21
- const r = result.toLowerCase();
22
- if (r.includes('stale') || r.includes('disconnected')) return FailureMode.AGENT_STALE;
23
- if (r.includes('timeout') || r.includes('timed out')) return FailureMode.TIMEOUT;
24
- if (r.includes('json') || r.includes('schema') || r.includes('parse')) return FailureMode.OUTPUT_INVALID;
25
- if (r.includes('test fail') || r.includes('vitest') || r.includes('jest')) return FailureMode.TEST_FAIL;
26
- if (r.includes('lint') || r.includes('eslint') || r.includes('typecheck')) return FailureMode.LINT_FAIL;
27
- if (r.includes('conflict')) return FailureMode.MERGE_CONFLICT;
28
- return FailureMode.UNKNOWN;
29
- }
30
-
31
- /**
32
- * Corrective guidance injected into the task description on retry.
33
- * Each hint is written in the vocabulary the next worker will read.
34
- */
35
- export const MUTATION_HINTS: Record<FailureMode, string> = {
36
- [FailureMode.AGENT_STALE]:
37
- 'Previous worker disconnected before completion. Resume from last known state; check git status before re-running destructive commands.',
38
- [FailureMode.TIMEOUT]:
39
- 'Previous attempt timed out. Break work into smaller commits; report progress every 5 minutes.',
40
- [FailureMode.OUTPUT_INVALID]:
41
- 'Previous output failed validation. Return a single fenced code block; verify JSON parses before submitting.',
42
- [FailureMode.TEST_FAIL]:
43
- 'Previous attempt left tests failing. Run vitest before completion; do NOT mark complete if any test fails.',
44
- [FailureMode.LINT_FAIL]:
45
- 'Previous attempt had lint/typecheck errors. Run pnpm typecheck and pnpm lint before completion.',
46
- [FailureMode.MERGE_CONFLICT]:
47
- 'Previous attempt left merge conflicts unresolved. git pull --rebase, resolve, then re-attempt.',
48
- [FailureMode.UNKNOWN]:
49
- 'Previous attempt failed for an unclassified reason. Investigate the prior result before re-running.',
50
- };
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * FailureMode taxonomy and mutation-hint map for the coordination control layer.
5
+ * Part of AWM 0.8.1 — additive, no breaking changes.
6
+ */
7
+
8
+ export enum FailureMode {
9
+ AGENT_STALE = 'agent_stale',
10
+ TIMEOUT = 'timeout',
11
+ OUTPUT_INVALID = 'output_invalid',
12
+ TEST_FAIL = 'test_fail',
13
+ LINT_FAIL = 'lint_fail',
14
+ MERGE_CONFLICT = 'merge_conflict',
15
+ UNKNOWN = 'unknown',
16
+ }
17
+
18
+ /** Classify a failure result string into one of the known modes. */
19
+ export function classifyFailure(result: string | null): FailureMode {
20
+ if (!result) return FailureMode.UNKNOWN;
21
+ const r = result.toLowerCase();
22
+ if (r.includes('stale') || r.includes('disconnected')) return FailureMode.AGENT_STALE;
23
+ if (r.includes('timeout') || r.includes('timed out')) return FailureMode.TIMEOUT;
24
+ if (r.includes('json') || r.includes('schema') || r.includes('parse')) return FailureMode.OUTPUT_INVALID;
25
+ if (r.includes('test fail') || r.includes('vitest') || r.includes('jest')) return FailureMode.TEST_FAIL;
26
+ if (r.includes('lint') || r.includes('eslint') || r.includes('typecheck')) return FailureMode.LINT_FAIL;
27
+ if (r.includes('conflict')) return FailureMode.MERGE_CONFLICT;
28
+ return FailureMode.UNKNOWN;
29
+ }
30
+
31
+ /**
32
+ * Corrective guidance injected into the task description on retry.
33
+ * Each hint is written in the vocabulary the next worker will read.
34
+ */
35
+ export const MUTATION_HINTS: Record<FailureMode, string> = {
36
+ [FailureMode.AGENT_STALE]:
37
+ 'Previous worker disconnected before completion. Resume from last known state; check git status before re-running destructive commands.',
38
+ [FailureMode.TIMEOUT]:
39
+ 'Previous attempt timed out. Break work into smaller commits; report progress every 5 minutes.',
40
+ [FailureMode.OUTPUT_INVALID]:
41
+ 'Previous output failed validation. Return a single fenced code block; verify JSON parses before submitting.',
42
+ [FailureMode.TEST_FAIL]:
43
+ 'Previous attempt left tests failing. Run vitest before completion; do NOT mark complete if any test fails.',
44
+ [FailureMode.LINT_FAIL]:
45
+ 'Previous attempt had lint/typecheck errors. Run pnpm typecheck and pnpm lint before completion.',
46
+ [FailureMode.MERGE_CONFLICT]:
47
+ 'Previous attempt left merge conflicts unresolved. git pull --rebase, resolve, then re-attempt.',
48
+ [FailureMode.UNKNOWN]:
49
+ 'Previous attempt failed for an unclassified reason. Investigate the prior result before re-running.',
50
+ };