gsd-pi 2.24.0 → 2.25.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 (113) hide show
  1. package/README.md +2 -1
  2. package/dist/models-resolver.d.ts +0 -11
  3. package/dist/models-resolver.js +0 -15
  4. package/dist/resource-loader.d.ts +0 -1
  5. package/dist/resource-loader.js +0 -9
  6. package/dist/resources/GSD-WORKFLOW.md +12 -9
  7. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  8. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  9. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  10. package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
  11. package/dist/resources/extensions/gsd/auto-worktree.ts +119 -1
  12. package/dist/resources/extensions/gsd/auto.ts +184 -36
  13. package/dist/resources/extensions/gsd/cache.ts +3 -1
  14. package/dist/resources/extensions/gsd/doctor.ts +2 -0
  15. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  16. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +34 -12
  18. package/dist/resources/extensions/gsd/index.ts +14 -1
  19. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  20. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  21. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  22. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  24. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
  25. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  29. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  31. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  32. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  33. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  34. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  35. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  36. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  37. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  38. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  39. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  40. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  41. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  42. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  43. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  44. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  45. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  46. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  47. package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
  48. package/package.json +1 -1
  49. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  50. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  51. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  52. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  53. package/packages/pi-ai/dist/providers/anthropic.js +39 -0
  54. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  55. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  56. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  57. package/packages/pi-ai/dist/types.d.ts +23 -1
  58. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/types.js.map +1 -1
  60. package/packages/pi-ai/src/providers/anthropic.ts +38 -1
  61. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  62. package/packages/pi-ai/src/types.ts +19 -1
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  67. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  71. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
  72. package/src/resources/GSD-WORKFLOW.md +12 -9
  73. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  74. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  75. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  76. package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
  77. package/src/resources/extensions/gsd/auto-worktree.ts +119 -1
  78. package/src/resources/extensions/gsd/auto.ts +184 -36
  79. package/src/resources/extensions/gsd/cache.ts +3 -1
  80. package/src/resources/extensions/gsd/doctor.ts +2 -0
  81. package/src/resources/extensions/gsd/git-service.ts +74 -14
  82. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  83. package/src/resources/extensions/gsd/guided-flow.ts +34 -12
  84. package/src/resources/extensions/gsd/index.ts +14 -1
  85. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  86. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  87. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  88. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  89. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  90. package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
  91. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  92. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  93. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  94. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  95. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  96. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  97. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  98. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  99. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  100. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  102. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  103. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  104. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  105. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  106. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  107. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  108. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  109. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  110. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  111. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  112. package/src/resources/extensions/gsd/worktree.ts +9 -2
  113. package/src/resources/extensions/search-the-web/native-search.ts +15 -5
@@ -153,6 +153,64 @@ async function main(): Promise<void> {
153
153
  // After teardown, originalBase should be null
154
154
  assertEq(getAutoWorktreeOriginalBase(), null, "no split-brain: originalBase cleared");
155
155
 
156
+ // ─── #778: reconcile plan checkboxes on re-attach ─────────────────
157
+ console.log("\n=== #778: reconcile plan checkboxes on re-attach ===");
158
+ {
159
+ // Simulate: T01 [x] was committed to milestone branch, T02 [x] was
160
+ // written to project root by syncStateToProjectRoot() but the
161
+ // auto-commit crashed before it fired. On restart the worktree is
162
+ // re-created from the milestone branch HEAD (T02 still [ ]).
163
+ // reconcilePlanCheckboxes should forward-apply T02 [x] from the root.
164
+
165
+ const planRelPath = join(".gsd", "milestones", "M004", "slices", "S01", "S01-PLAN.md");
166
+ const planDir = join(tempDir, ".gsd", "milestones", "M004", "slices", "S01");
167
+ const { mkdirSync: mkdir, writeFileSync: write, readFileSync: read } = await import("node:fs");
168
+
169
+ // Plan on integration branch (project root): T01 [x], T02 [x]
170
+ mkdir(planDir, { recursive: true });
171
+ write(
172
+ join(tempDir, planRelPath),
173
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
174
+ );
175
+
176
+ // Write integration-branch plan to git so milestone branch starts from it
177
+ run(`git add .`, tempDir);
178
+ run(`git commit -m "add plan with T01 and T02 checked" --allow-empty`, tempDir);
179
+
180
+ // Create milestone branch with only T01 [x] (simulating crash before T02 commit)
181
+ const milestoneBranch = "milestone/M004";
182
+ run(`git checkout -b ${milestoneBranch}`, tempDir);
183
+ mkdir(planDir, { recursive: true });
184
+ write(
185
+ join(tempDir, planRelPath),
186
+ "# S01 Plan\n- [x] **T01:** task one\n- [ ] **T02:** task two\n- [ ] **T03:** task three\n",
187
+ );
188
+ run(`git add .`, tempDir);
189
+ run(`git commit -m "milestone: only T01 checked"`, tempDir);
190
+ run(`git checkout main`, tempDir);
191
+
192
+ // Restore project root plan (T01+T02 [x]) — simulates syncStateToProjectRoot
193
+ write(
194
+ join(tempDir, planRelPath),
195
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
196
+ );
197
+
198
+ // Create worktree re-attached to existing milestone branch (T02 still [ ] in branch)
199
+ const wtPath = createAutoWorktree(tempDir, "M004");
200
+
201
+ try {
202
+ const wtPlanPath = join(wtPath, planRelPath);
203
+ assertTrue(existsSync(wtPlanPath), "plan file exists in worktree after re-attach");
204
+
205
+ const wtPlan = read(wtPlanPath, "utf-8");
206
+ assertTrue(wtPlan.includes("- [x] **T02:"), "T02 should be [x] after reconciliation (was [ ] on branch)");
207
+ assertTrue(wtPlan.includes("- [x] **T01:"), "T01 stays [x]");
208
+ assertTrue(wtPlan.includes("- [ ] **T03:"), "T03 stays [ ] (not in root either)");
209
+ } finally {
210
+ teardownAutoWorktree(tempDir, "M004");
211
+ }
212
+ }
213
+
156
214
  } finally {
157
215
  // Always restore cwd and clean up
158
216
  process.chdir(savedCwd);
@@ -5,6 +5,7 @@ import { execSync } from "node:child_process";
5
5
 
6
6
  import {
7
7
  inferCommitType,
8
+ buildTaskCommitMessage,
8
9
  GitServiceImpl,
9
10
  RUNTIME_EXCLUSION_PATHS,
10
11
  VALID_BRANCH_NAME,
@@ -14,6 +15,7 @@ import {
14
15
  type GitPreferences,
15
16
  type CommitOptions,
16
17
  type PreMergeCheckResult,
18
+ type TaskCommitContext,
17
19
  } from "../git-service.ts";
18
20
  import { createTestContext } from './test-helpers.ts';
19
21
 
@@ -188,6 +190,58 @@ async function main(): Promise<void> {
188
190
  "'prefix' does not match 'fix' — word boundary prevents partial match"
189
191
  );
190
192
 
193
+ // ─── inferCommitType with oneLiner ──────────────────────────────────────
194
+
195
+ console.log("\n=== inferCommitType with oneLiner ===");
196
+
197
+ assertEq(
198
+ inferCommitType("implement dashboard", "Fixed rendering bug in sidebar"),
199
+ "fix",
200
+ "one-liner with 'fixed' overrides generic title → fix"
201
+ );
202
+
203
+ assertEq(
204
+ inferCommitType("add search", "Optimized query performance with caching"),
205
+ "perf",
206
+ "one-liner with 'performance' and 'caching' → perf"
207
+ );
208
+
209
+ // ─── buildTaskCommitMessage ─────────────────────────────────────────────
210
+
211
+ console.log("\n=== buildTaskCommitMessage ===");
212
+
213
+ {
214
+ const msg = buildTaskCommitMessage({
215
+ taskId: "S01/T02",
216
+ taskTitle: "implement user authentication",
217
+ oneLiner: "Added JWT-based auth with refresh token rotation",
218
+ keyFiles: ["src/auth.ts", "src/middleware/jwt.ts"],
219
+ });
220
+ assertTrue(msg.startsWith("feat(S01/T02):"), "message starts with type(scope)");
221
+ assertTrue(msg.includes("JWT-based auth"), "message includes one-liner content");
222
+ assertTrue(msg.includes("- src/auth.ts"), "message body includes key files");
223
+ assertTrue(msg.includes("- src/middleware/jwt.ts"), "message body includes second key file");
224
+ }
225
+
226
+ {
227
+ const msg = buildTaskCommitMessage({
228
+ taskId: "S02/T01",
229
+ taskTitle: "fix login redirect bug",
230
+ });
231
+ assertTrue(msg.startsWith("fix(S02/T01):"), "infers fix type from title");
232
+ assertTrue(msg.includes("fix login redirect bug"), "uses task title when no one-liner");
233
+ assertTrue(!msg.includes("\n"), "no body when no key files");
234
+ }
235
+
236
+ {
237
+ const msg = buildTaskCommitMessage({
238
+ taskId: "S01/T03",
239
+ taskTitle: "add tests",
240
+ oneLiner: "Unit tests for auth module with coverage",
241
+ });
242
+ assertTrue(msg.startsWith("test(S01/T03):"), "infers test type");
243
+ }
244
+
191
245
  // ─── RUNTIME_EXCLUSION_PATHS ───────────────────────────────────────────
192
246
 
193
247
  console.log("\n=== RUNTIME_EXCLUSION_PATHS ===");
@@ -430,13 +484,25 @@ async function main(): Promise<void> {
430
484
  const svc = new GitServiceImpl(repo);
431
485
 
432
486
  createFile(repo, "src/new-feature.ts", "export const x = 1;");
433
- const msg = svc.autoCommit("task", "T01");
434
487
 
435
- assertEq(msg, "chore(T01): auto-commit after task", "autoCommit returns correct message format");
488
+ // Without task context, autoCommit uses generic chore message
489
+ const msg = svc.autoCommit("task", "T01");
490
+ assertEq(msg, "chore(T01): auto-commit after task", "autoCommit returns generic format without task context");
436
491
 
437
- // Verify the commit exists
438
492
  const log = run("git log --oneline -1", repo);
439
- assertTrue(log.includes("chore(T01): auto-commit after task"), "commit message is in git log");
493
+ assertTrue(log.includes("chore(T01): auto-commit after task"), "generic commit message is in git log");
494
+
495
+ // With task context, autoCommit uses meaningful message
496
+ createFile(repo, "src/auth.ts", "export function login() {}");
497
+ const msg2 = svc.autoCommit("task", "S01/T02", [], {
498
+ taskId: "S01/T02",
499
+ taskTitle: "implement user authentication endpoint",
500
+ oneLiner: "Added JWT-based auth with refresh token rotation",
501
+ keyFiles: ["src/auth.ts"],
502
+ });
503
+ assertTrue(msg2 !== null, "autoCommit with task context returns a message");
504
+ assertTrue(msg2!.startsWith("feat(S01/T02):"), "meaningful commit uses feat type and scope");
505
+ assertTrue(msg2!.includes("JWT-based auth"), "meaningful commit includes one-liner content");
440
506
 
441
507
  rmSync(repo, { recursive: true, force: true });
442
508
  }
@@ -65,8 +65,8 @@ console.log('\n=== gsd-db: fresh DB schema init (memory) ===');
65
65
 
66
66
  // Check schema_version table
67
67
  const adapter = _getAdapter()!;
68
- const version = adapter.prepare('SELECT version FROM schema_version').get();
69
- assertEq(version?.['version'], 2, 'schema version should be 2');
68
+ const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
69
+ assertEq(version?.['version'], 3, 'schema version should be 3');
70
70
 
71
71
  // Check tables exist by querying them
72
72
  const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
@@ -350,12 +350,11 @@ console.log('=== md-importer: missing file handling ===');
350
350
  console.log('=== md-importer: schema v1→v2 migration ===');
351
351
 
352
352
  {
353
- // This test verifies that opening a v1 DB auto-migrates to v2
354
- // (The actual migration is tested via the gsd-db.test.ts schema version assertion = 2)
353
+ // This test verifies that opening a fresh DB auto-migrates to current schema version
355
354
  openDatabase(':memory:');
356
355
  const adapter = _getAdapter();
357
356
  const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
358
- assertEq(version?.v, 2, 'new DB should be at schema version 2');
357
+ assertEq(version?.v, 3, 'new DB should be at schema version 3');
359
358
 
360
359
  // Artifacts table should exist
361
360
  const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
@@ -0,0 +1,180 @@
1
+ import { createTestContext } from './test-helpers.ts';
2
+ import { parseMemoryResponse, _resetExtractionState } from '../memory-extractor.ts';
3
+ import {
4
+ openDatabase,
5
+ closeDatabase,
6
+ } from '../gsd-db.ts';
7
+ import {
8
+ getActiveMemories,
9
+ applyMemoryActions,
10
+ getActiveMemoriesRanked,
11
+ } from '../memory-store.ts';
12
+ import type { MemoryAction } from '../memory-store.ts';
13
+
14
+ const { assertEq, assertTrue, report } = createTestContext();
15
+
16
+ // ═══════════════════════════════════════════════════════════════════════════
17
+ // memory-extractor: parse valid JSON response
18
+ // ═══════════════════════════════════════════════════════════════════════════
19
+
20
+ console.log('\n=== memory-extractor: parse valid JSON ===');
21
+ {
22
+ const response = JSON.stringify([
23
+ { action: 'CREATE', category: 'gotcha', content: 'esbuild drops binaries', confidence: 0.85 },
24
+ { action: 'REINFORCE', id: 'MEM001' },
25
+ { action: 'UPDATE', id: 'MEM002', content: 'revised content' },
26
+ { action: 'SUPERSEDE', id: 'MEM003', superseded_by: 'MEM004' },
27
+ ]);
28
+
29
+ const actions = parseMemoryResponse(response);
30
+ assertEq(actions.length, 4, 'should parse 4 actions');
31
+ assertEq(actions[0].action, 'CREATE', 'first action should be CREATE');
32
+ assertEq((actions[0] as any).category, 'gotcha', 'CREATE category');
33
+ assertEq((actions[0] as any).confidence, 0.85, 'CREATE confidence');
34
+ assertEq(actions[1].action, 'REINFORCE', 'second action should be REINFORCE');
35
+ assertEq(actions[2].action, 'UPDATE', 'third action should be UPDATE');
36
+ assertEq(actions[3].action, 'SUPERSEDE', 'fourth action should be SUPERSEDE');
37
+ }
38
+
39
+ // ═══════════════════════════════════════════════════════════════════════════
40
+ // memory-extractor: parse fenced JSON response
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+
43
+ console.log('\n=== memory-extractor: parse fenced JSON ===');
44
+ {
45
+ const response = '```json\n[\n {"action": "CREATE", "category": "convention", "content": "test memory"}\n]\n```';
46
+
47
+ const actions = parseMemoryResponse(response);
48
+ assertEq(actions.length, 1, 'should parse 1 action from fenced JSON');
49
+ assertEq(actions[0].action, 'CREATE', 'action should be CREATE');
50
+ }
51
+
52
+ // ═══════════════════════════════════════════════════════════════════════════
53
+ // memory-extractor: parse empty array response
54
+ // ═══════════════════════════════════════════════════════════════════════════
55
+
56
+ console.log('\n=== memory-extractor: parse empty array ===');
57
+ {
58
+ const actions = parseMemoryResponse('[]');
59
+ assertEq(actions.length, 0, 'empty array should parse to empty actions');
60
+ }
61
+
62
+ // ═══════════════════════════════════════════════════════════════════════════
63
+ // memory-extractor: parse malformed response
64
+ // ═══════════════════════════════════════════════════════════════════════════
65
+
66
+ console.log('\n=== memory-extractor: malformed responses ===');
67
+ {
68
+ assertEq(parseMemoryResponse('not json at all'), [], 'garbage text should return []');
69
+ assertEq(parseMemoryResponse('{"action": "CREATE"}'), [], 'non-array should return []');
70
+ assertEq(parseMemoryResponse(''), [], 'empty string should return []');
71
+ assertEq(parseMemoryResponse('```\nbroken\n```'), [], 'fenced non-JSON should return []');
72
+ }
73
+
74
+ // ═══════════════════════════════════════════════════════════════════════════
75
+ // memory-extractor: validation of required fields
76
+ // ═══════════════════════════════════════════════════════════════════════════
77
+
78
+ console.log('\n=== memory-extractor: field validation ===');
79
+ {
80
+ const response = JSON.stringify([
81
+ // Valid CREATE
82
+ { action: 'CREATE', category: 'gotcha', content: 'valid' },
83
+ // Invalid CREATE — missing content
84
+ { action: 'CREATE', category: 'gotcha' },
85
+ // Invalid CREATE — missing category
86
+ { action: 'CREATE', content: 'no category' },
87
+ // Valid REINFORCE
88
+ { action: 'REINFORCE', id: 'MEM001' },
89
+ // Invalid REINFORCE — missing id
90
+ { action: 'REINFORCE' },
91
+ // Valid UPDATE
92
+ { action: 'UPDATE', id: 'MEM002', content: 'new content' },
93
+ // Invalid UPDATE — missing content
94
+ { action: 'UPDATE', id: 'MEM002' },
95
+ // Valid SUPERSEDE
96
+ { action: 'SUPERSEDE', id: 'MEM001', superseded_by: 'MEM002' },
97
+ // Invalid SUPERSEDE — missing superseded_by
98
+ { action: 'SUPERSEDE', id: 'MEM001' },
99
+ // Unknown action
100
+ { action: 'DELETE', id: 'MEM001' },
101
+ // Null entry
102
+ null,
103
+ ]);
104
+
105
+ const actions = parseMemoryResponse(response);
106
+ assertEq(actions.length, 4, 'should only accept 4 valid actions');
107
+ assertEq(actions[0].action, 'CREATE', 'first valid is CREATE');
108
+ assertEq(actions[1].action, 'REINFORCE', 'second valid is REINFORCE');
109
+ assertEq(actions[2].action, 'UPDATE', 'third valid is UPDATE');
110
+ assertEq(actions[3].action, 'SUPERSEDE', 'fourth valid is SUPERSEDE');
111
+ }
112
+
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+ // Integration: applyMemoryActions with mixed actions
115
+ // ═══════════════════════════════════════════════════════════════════════════
116
+
117
+ console.log('\n=== integration: mixed action lifecycle ===');
118
+ {
119
+ openDatabase(':memory:');
120
+
121
+ // Phase 1: Create initial memories
122
+ applyMemoryActions([
123
+ { action: 'CREATE', category: 'gotcha', content: 'npm run build needs tsc first', confidence: 0.7 },
124
+ { action: 'CREATE', category: 'convention', content: 'all DB queries use named params', confidence: 0.8 },
125
+ { action: 'CREATE', category: 'architecture', content: 'extensions loaded from two paths', confidence: 0.85 },
126
+ ], 'plan-slice', 'M001/S01');
127
+
128
+ let active = getActiveMemoriesRanked(30);
129
+ assertEq(active.length, 3, 'phase 1: 3 active memories');
130
+
131
+ // Phase 2: Reinforce one, update another, create new
132
+ applyMemoryActions([
133
+ { action: 'REINFORCE', id: 'MEM002' },
134
+ { action: 'UPDATE', id: 'MEM001', content: 'npm run build requires tsc --noEmit first' },
135
+ { action: 'CREATE', category: 'pattern', content: 'use INSERT OR IGNORE for idempotency', confidence: 0.75 },
136
+ ], 'execute-task', 'M001/S01/T01');
137
+
138
+ active = getActiveMemoriesRanked(30);
139
+ assertEq(active.length, 4, 'phase 2: 4 active memories');
140
+ assertEq(
141
+ active.find(m => m.id === 'MEM001')?.content,
142
+ 'npm run build requires tsc --noEmit first',
143
+ 'MEM001 content should be updated',
144
+ );
145
+ assertEq(active.find(m => m.id === 'MEM002')?.hit_count, 1, 'MEM002 should be reinforced');
146
+
147
+ // Phase 3: Supersede MEM001 with MEM005
148
+ applyMemoryActions([
149
+ { action: 'CREATE', category: 'gotcha', content: 'build script handles tsc automatically now', confidence: 0.9 },
150
+ { action: 'SUPERSEDE', id: 'MEM001', superseded_by: 'MEM005' },
151
+ ], 'execute-task', 'M001/S01/T02');
152
+
153
+ active = getActiveMemoriesRanked(30);
154
+ assertEq(active.length, 4, 'phase 3: 4 active (1 superseded, 1 created)');
155
+ assertTrue(!active.find(m => m.id === 'MEM001'), 'MEM001 should be superseded');
156
+ assertTrue(!!active.find(m => m.id === 'MEM005'), 'MEM005 should be active');
157
+
158
+ // Verify ranking: MEM003 (0.85) > MEM005 (0.9) but MEM002 has 1 hit
159
+ // MEM002: 0.8 * (1 + 1*0.1) = 0.88
160
+ // MEM003: 0.85 * 1.0 = 0.85
161
+ // MEM005: 0.9 * 1.0 = 0.9
162
+ // MEM004: 0.75 * 1.0 = 0.75
163
+ assertEq(active[0].id, 'MEM005', 'MEM005 should rank first (0.9)');
164
+ assertEq(active[1].id, 'MEM002', 'MEM002 should rank second (0.88)');
165
+
166
+ closeDatabase();
167
+ }
168
+
169
+ // ═══════════════════════════════════════════════════════════════════════════
170
+ // memory-extractor: _resetExtractionState
171
+ // ═══════════════════════════════════════════════════════════════════════════
172
+
173
+ console.log('\n=== memory-extractor: reset extraction state ===');
174
+ {
175
+ // Just verify it doesn't throw
176
+ _resetExtractionState();
177
+ assertTrue(true, '_resetExtractionState should not throw');
178
+ }
179
+
180
+ report();