@ijfw/memory-server 1.4.4 → 1.5.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 (232) hide show
  1. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  2. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  3. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  4. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  5. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  6. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  7. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  8. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  9. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  23. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  24. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  26. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  27. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  28. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  29. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  47. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  48. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  49. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  50. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  52. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  53. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  54. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  68. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  69. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  71. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  72. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  73. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  74. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  92. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  93. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  94. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  95. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  96. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  97. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  98. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  99. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  115. package/package.json +1 -1
  116. package/src/active-extension-writer.js +144 -64
  117. package/src/api-client.js +43 -5
  118. package/src/audit-roster.js +80 -5
  119. package/src/blackboard.js +298 -6
  120. package/src/cli-run.js +33 -5
  121. package/src/codex-agents.js +96 -5
  122. package/src/cost/aggregator.js +39 -9
  123. package/src/cost/pricing.js +57 -0
  124. package/src/cost/readers/gemini.js +1 -1
  125. package/src/cross-audit-chunker.js +189 -0
  126. package/src/cross-dispatcher.js +124 -21
  127. package/src/cross-orchestrator-cli.js +550 -14
  128. package/src/cross-orchestrator.js +1016 -17
  129. package/src/cross-project-search.js +195 -9
  130. package/src/dashboard-client-waves.html +304 -0
  131. package/src/dashboard-client.html +5 -1
  132. package/src/dashboard-server.js +73 -0
  133. package/src/deploy-alerts.js +150 -0
  134. package/src/design/iframe-bridge.js +242 -0
  135. package/src/design-companion.js +144 -0
  136. package/src/dispatch/checkpoint-cli.js +97 -0
  137. package/src/dispatch/colon-syntax.js +81 -1
  138. package/src/dispatch/extension.js +26 -2
  139. package/src/dispatch/registry-cli.js +4 -1
  140. package/src/dispatch/wave-cli.js +201 -6
  141. package/src/dispatch/worktree-cli.js +40 -0
  142. package/src/dispatch-planner.js +97 -2
  143. package/src/dream/runner.mjs +47 -11
  144. package/src/dream/stage-runner.js +40 -0
  145. package/src/dream/state-file.js +102 -0
  146. package/src/extension-installer.js +70 -24
  147. package/src/extension-quota-tracker.js +4 -2
  148. package/src/extension-registry.js +289 -35
  149. package/src/feedback-detector.js +26 -0
  150. package/src/fs-lock.js +259 -7
  151. package/src/gate-result.js +95 -1
  152. package/src/hero-line.js +86 -5
  153. package/src/intent-router.js +35 -0
  154. package/src/lib/a11y-contract.js +117 -0
  155. package/src/lib/atomic-io.js +29 -8
  156. package/src/lib/cache-keepalive.js +150 -0
  157. package/src/lib/jsonl-rotation.js +104 -0
  158. package/src/lib/lighthouse-pillar.js +121 -0
  159. package/src/lib/llm-call.js +121 -0
  160. package/src/lib/playwright-baseline.js +205 -0
  161. package/src/lib/rekor-bridge.js +221 -0
  162. package/src/lib/repo-map.js +392 -0
  163. package/src/lib/shasum-verify.js +164 -0
  164. package/src/lib/sketches-gc.js +132 -0
  165. package/src/lib/tmp-suffix.js +62 -0
  166. package/src/lib/ui-review-runner.js +554 -0
  167. package/src/lib/uispec-drift.js +301 -0
  168. package/src/lib/uispec-intake.js +381 -0
  169. package/src/lib/worktree-guards.js +118 -0
  170. package/src/lib/worktree-recovery.js +100 -0
  171. package/src/memory/auto-linker.js +152 -0
  172. package/src/memory/benchmark.js +498 -0
  173. package/src/memory/dedup.js +126 -0
  174. package/src/memory/embedding-cache.js +136 -0
  175. package/src/memory/fact-extractor.js +168 -0
  176. package/src/memory/fts5.js +65 -1
  177. package/src/memory/migrations/004-bitemporal.js +91 -0
  178. package/src/memory/migrations/005-vector-cache.js +61 -0
  179. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  180. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  181. package/src/memory/migrations/008-write-provenance.js +41 -0
  182. package/src/memory/obsidian-parser.js +91 -0
  183. package/src/memory/query-dataview.js +86 -0
  184. package/src/memory/search.js +10 -0
  185. package/src/memory/temporal.js +529 -0
  186. package/src/memory/tokenize.js +10 -0
  187. package/src/memory-facts-handler.js +37 -0
  188. package/src/memory-feedback.js +260 -2
  189. package/src/model-refresh.js +292 -0
  190. package/src/observability/cost-anomaly.js +166 -0
  191. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  192. package/src/observability/trace-id.js +163 -0
  193. package/src/orchestrator/agents-md-blackboard.js +152 -0
  194. package/src/orchestrator/checkpoint-contract.md +140 -0
  195. package/src/orchestrator/debug-trident.js +570 -0
  196. package/src/orchestrator/merge-block-aware.js +350 -0
  197. package/src/orchestrator/plan-checker.js +475 -0
  198. package/src/orchestrator/post-done-runner.js +249 -0
  199. package/src/orchestrator/review.js +38 -3
  200. package/src/orchestrator/runtime-loop.js +430 -0
  201. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  202. package/src/orchestrator/skill-telemetry.js +37 -0
  203. package/src/orchestrator/state-events.js +459 -0
  204. package/src/orchestrator/state-sdk.js +1764 -0
  205. package/src/orchestrator/status-protocol.js +84 -17
  206. package/src/orchestrator/subagent-telemetry.js +452 -0
  207. package/src/orchestrator/termination.js +160 -0
  208. package/src/orchestrator/verification-gate.js +200 -16
  209. package/src/orchestrator/wave-state.js +332 -23
  210. package/src/orchestrator/worktree-provision.js +77 -0
  211. package/src/override-use-registry.js +111 -5
  212. package/src/receipts.js +36 -4
  213. package/src/recovery/checkpoint.js +56 -3
  214. package/src/recovery/code-fixer.js +656 -0
  215. package/src/recovery/truncation.js +317 -0
  216. package/src/redactor.js +75 -6
  217. package/src/runtime-mediator.js +15 -0
  218. package/src/sanitizer.js +10 -0
  219. package/src/search-hybrid.js +139 -0
  220. package/src/server.js +603 -59
  221. package/src/swarm/worktree.js +27 -4
  222. package/src/swarm-config.js +94 -17
  223. package/src/team/domain-templates/book.json +51 -0
  224. package/src/team/domain-templates/business.json +41 -0
  225. package/src/team/domain-templates/content.json +50 -0
  226. package/src/team/domain-templates/design.json +44 -0
  227. package/src/team/domain-templates/research.json +41 -0
  228. package/src/team/domain-templates/software.json +40 -0
  229. package/src/team/generator.js +278 -3
  230. package/src/team/modify.js +203 -0
  231. package/src/team/schemas.js +48 -0
  232. package/src/update-apply.js +19 -3
@@ -0,0 +1,118 @@
1
+ /**
2
+ * worktree-guards.js — v1.5.0-major S08: incident-driven worktree safety guards.
3
+ *
4
+ * Lifted from GSD executor (battle-tested across many incidents):
5
+ * #3097 — cwd-drift assertion
6
+ * #3099 — absolute-path containment check
7
+ * #2924 — protected-ref deny-list (refuse commit to main/master/develop/trunk/release/*)
8
+ *
9
+ * We hit ALL THREE during v1.5.0-major dispatch (multiple subagents drifted
10
+ * cwd into other worktrees; W11-A0 accidentally committed to main because
11
+ * branch creation failed). Three small functions, large blast-radius reduction.
12
+ */
13
+
14
+ import { execFileSync } from 'node:child_process';
15
+ import { realpathSync } from 'node:fs';
16
+ import { resolve, isAbsolute, relative } from 'node:path';
17
+
18
+ // r15-H2: resolve symlinks before containment check. Without this, a path like
19
+ // <toplevel>/escape-link/foo (where escape-link → /etc) trivially escapes the
20
+ // toplevel even though `relative(toplevel, path)` reports a non-`..` result for
21
+ // the lexical path. realpathSync resolves the link first. If the path does not
22
+ // exist yet (e.g. a not-yet-created file we're about to write), realpathSync
23
+ // throws ENOENT — we fall back to the input path since a not-yet-existing
24
+ // target is still a valid containment surface to check lexically.
25
+ function safeRealpath(p) {
26
+ try { return realpathSync(p); } catch { return p; }
27
+ }
28
+
29
+ const PROTECTED_REF_PATTERNS = [
30
+ /^main$/, /^master$/, /^develop$/, /^trunk$/,
31
+ /^release\//, /^prod$/, /^production$/,
32
+ ];
33
+
34
+ /**
35
+ * #3097 — Assert that current working directory matches the captured spawn-time
36
+ * git toplevel. Returns the captured toplevel for later use. Throws if drift detected.
37
+ *
38
+ * @param {string} capturedToplevel - what was `git rev-parse --show-toplevel` at spawn
39
+ * @param {string} cwd - current working directory (default: process.cwd())
40
+ * @returns {string} the captured toplevel (passthrough on success)
41
+ * @throws Error if cwd drifted off the captured toplevel
42
+ */
43
+ export function assertNoCwdDrift(capturedToplevel, cwd = process.cwd()) {
44
+ if (!capturedToplevel) throw new Error('worktree-guards: capturedToplevel required (call captureSpawnToplevel() at spawn)');
45
+ let currentTop;
46
+ try {
47
+ currentTop = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' }).trim();
48
+ } catch {
49
+ throw new Error(`worktree-guards: cwd is not a git tree: ${cwd}`);
50
+ }
51
+ if (currentTop !== capturedToplevel) {
52
+ throw new Error(`worktree-guards: cwd drift detected! spawn=${capturedToplevel}, now=${currentTop}`);
53
+ }
54
+ return capturedToplevel;
55
+ }
56
+
57
+ /**
58
+ * Capture the spawn-time toplevel for later drift assertion.
59
+ */
60
+ export function captureSpawnToplevel(cwd = process.cwd()) {
61
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' }).trim();
62
+ }
63
+
64
+ /**
65
+ * #3099 — Assert that an absolute file path is contained within the toplevel.
66
+ * Refuses operations that would read/write outside the expected worktree.
67
+ *
68
+ * @param {string} absolutePath - path to check
69
+ * @param {string} toplevel - git toplevel (from captureSpawnToplevel)
70
+ * @returns {string} the path (passthrough on success)
71
+ * @throws if path escapes toplevel
72
+ */
73
+ export function assertPathWithinToplevel(absolutePath, toplevel) {
74
+ if (!isAbsolute(absolutePath)) {
75
+ throw new Error(`worktree-guards: path must be absolute (got: ${absolutePath})`);
76
+ }
77
+ // r15-H2: resolve symlinks on BOTH path AND toplevel before comparing.
78
+ // A path that lexically starts with toplevel can still escape via a symlink
79
+ // (e.g. <toplevel>/escape-link/foo where escape-link → /etc). Resolving
80
+ // both ends through realpath catches the real-fs destination.
81
+ const resolvedPath = safeRealpath(absolutePath);
82
+ const resolvedTop = safeRealpath(toplevel);
83
+ const rel = relative(resolvedTop, resolvedPath);
84
+ if (rel.startsWith('..') || isAbsolute(rel)) {
85
+ throw new Error(`worktree-guards: path escapes toplevel! path=${absolutePath} (resolved: ${resolvedPath}), toplevel=${toplevel} (resolved: ${resolvedTop})`);
86
+ }
87
+ return absolutePath;
88
+ }
89
+
90
+ /**
91
+ * #2924 — Assert the current HEAD is NOT pointing at a protected ref before commit.
92
+ * Subagents must commit to a wave branch (wave/W*-*), never directly to main/master/etc.
93
+ *
94
+ * @param {string} cwd
95
+ * @returns {string} the current branch name (passthrough on success)
96
+ * @throws if HEAD is on a protected ref
97
+ */
98
+ export function assertNotProtectedRef(cwd = process.cwd()) {
99
+ const branch = execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim();
100
+ if (!branch) {
101
+ throw new Error('worktree-guards: detached HEAD — cannot commit safely');
102
+ }
103
+ for (const pattern of PROTECTED_REF_PATTERNS) {
104
+ if (pattern.test(branch)) {
105
+ throw new Error(`worktree-guards: refuse to commit to protected ref: ${branch}. Switch to a wave/feature branch first.`);
106
+ }
107
+ }
108
+ return branch;
109
+ }
110
+
111
+ /**
112
+ * Convenience: run all 3 guards in sequence before a commit. Returns the verified branch.
113
+ */
114
+ export function preCommitGuards(capturedToplevel, paths = []) {
115
+ assertNoCwdDrift(capturedToplevel);
116
+ for (const p of paths) assertPathWithinToplevel(resolve(p), capturedToplevel);
117
+ return assertNotProtectedRef();
118
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * worktree-recovery.js — v1.5.0-major S10: transactional cleanup tail for worktrees.
3
+ *
4
+ * Pattern lifted from GSD code-fixer (crash-safe recovery on next run):
5
+ * 1. BEFORE opening the crash window (git worktree remove), write a sentinel
6
+ * file at .planning/<phase>/.worktree-recovery-pending.json with:
7
+ * { worktreePath, branch, phase, ts, op: 'remove' }
8
+ * 2. Run the destructive op.
9
+ * 3. On success, DELETE the sentinel.
10
+ *
11
+ * Next run scans for orphan sentinels and offers/runs cleanup. Survives:
12
+ * - Process SIGKILL between steps 1+2
13
+ * - Power loss / OOM between steps 2+3
14
+ * - Network filesystem partition during step 2
15
+ *
16
+ * Use this for ANY destructive op where a partial state on disk is worse
17
+ * than a fully-committed or fully-rolled-back state.
18
+ */
19
+
20
+ import { writeFile, readFile, unlink, readdir, mkdir } from 'node:fs/promises';
21
+ import { existsSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { execFileSync } from 'node:child_process';
24
+ import { randomBytes } from 'node:crypto';
25
+
26
+ const SENTINEL_PREFIX = '.worktree-recovery-pending.';
27
+
28
+ /**
29
+ * Write a recovery sentinel, run the op, delete on success.
30
+ * @param {object} sentinelData - serialised into JSON; should include worktreePath + op
31
+ * @param {function} op - async function to execute; if it throws, sentinel remains
32
+ * @param {string} projectRoot - parent project root
33
+ * @returns the op's return value
34
+ */
35
+ export async function withRecoverySentinel(sentinelData, op, projectRoot) {
36
+ const sentinelDir = join(projectRoot, '.planning', 'worktree-recovery');
37
+ await mkdir(sentinelDir, { recursive: true });
38
+ const sentinelId = randomBytes(6).toString('hex');
39
+ const sentinelPath = join(sentinelDir, `${SENTINEL_PREFIX}${sentinelId}.json`);
40
+ const payload = { ...sentinelData, sentinel_id: sentinelId, ts: new Date().toISOString() };
41
+
42
+ await writeFile(sentinelPath, JSON.stringify(payload, null, 2), 'utf8');
43
+ try {
44
+ const result = await op();
45
+ try { await unlink(sentinelPath); } catch { /* missing → ok */ }
46
+ return result;
47
+ } catch (err) {
48
+ // Leave sentinel in place for next run to recover.
49
+ err.sentinelPath = sentinelPath;
50
+ throw err;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Scan for orphan sentinels (op didn't complete on previous run).
56
+ * @returns array of sentinel objects with full payload + path
57
+ */
58
+ export async function listPendingRecoveries(projectRoot) {
59
+ const sentinelDir = join(projectRoot, '.planning', 'worktree-recovery');
60
+ try {
61
+ const entries = await readdir(sentinelDir);
62
+ const pending = [];
63
+ for (const name of entries) {
64
+ if (!name.startsWith(SENTINEL_PREFIX)) continue;
65
+ const fullPath = join(sentinelDir, name);
66
+ try {
67
+ const raw = await readFile(fullPath, 'utf8');
68
+ pending.push({ ...JSON.parse(raw), path: fullPath });
69
+ } catch { /* malformed, skip */ }
70
+ }
71
+ return pending;
72
+ } catch (err) {
73
+ if (err.code === 'ENOENT') return [];
74
+ throw err;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Attempt to complete a pending recovery. Currently handles op:'remove' for worktrees.
80
+ * Returns { ok, action, details? }.
81
+ */
82
+ export async function recoverPending(sentinel, projectRoot) {
83
+ if (sentinel.op === 'remove' && sentinel.worktreePath) {
84
+ try {
85
+ // Best-effort: if the worktree dir still exists, finish the remove.
86
+ if (existsSync(sentinel.worktreePath)) {
87
+ execFileSync('git', ['worktree', 'remove', '--force', sentinel.worktreePath], {
88
+ cwd: projectRoot, encoding: 'utf8',
89
+ });
90
+ }
91
+ // Always prune to clean up dangling .git/worktrees/ refs.
92
+ execFileSync('git', ['worktree', 'prune'], { cwd: projectRoot, encoding: 'utf8' });
93
+ await unlink(sentinel.path);
94
+ return { ok: true, action: 'completed-remove', worktreePath: sentinel.worktreePath };
95
+ } catch (err) {
96
+ return { ok: false, action: 'remove-failed', error: err.message };
97
+ }
98
+ }
99
+ return { ok: false, action: 'unknown-op', op: sentinel.op };
100
+ }
@@ -0,0 +1,152 @@
1
+ // IJFW v1.5.0 -- A-Mem-style auto-linking on memory store.
2
+ //
3
+ // On every memory store call (post-dedup, pre-fact-extraction), this module:
4
+ // 1. Selects top-k neighbors via body lexical match (production
5
+ // memory_entries has columns id/body/source/session_id/created_at;
6
+ // no title, so neighbor scoring is body-only).
7
+ // 2. Asks the LLM (one call, env-gated) to return a JSON payload:
8
+ // classification: "ADD" | "UPDATE" | "NOOP"
9
+ // links: [{ target }]
10
+ // neighbor_edits: [{ id, add_tags: [...] }]
11
+ // 3. Applies the proposal to memory_links / memory_tags atomically.
12
+ //
13
+ // All steps are gated by llm-call.js env vars (IJFW_AUTOLINK_OFF=1,
14
+ // IJFW_AUTOLINK_BUDGET_USD=0, missing API key). When skipped, no writes.
15
+ //
16
+ // Test injections:
17
+ // { neighborsOnly: true } -> select neighbors, no LLM, no writes
18
+ // { dryProposal: <obj> } -> skip LLM, apply the supplied proposal
19
+ //
20
+ // Reference: arxiv 2502.12110 (A-Mem, NeurIPS 2025). This is the
21
+ // academically-validated "smarter with use" keystone.
22
+
23
+ import { llmCall, parseLlmJsonResponse } from '../lib/llm-call.js';
24
+
25
+ const DEFAULT_TOPK = 5;
26
+ const SYSTEM_PROMPT = [
27
+ 'You are the IJFW memory auto-linker. Given a NEW MEMORY and TOP-K NEIGHBORS,',
28
+ 'return STRICT JSON only (no prose). Schema:',
29
+ '{',
30
+ ' "classification": "ADD" | "UPDATE" | "NOOP",',
31
+ ' "links": [{ "target": "<lowercase-dash-collapsed-slug>" }],',
32
+ ' "neighbor_edits": [{ "id": "<neighbor-id>", "add_tags": ["..."] }]',
33
+ '}',
34
+ 'classification = UPDATE if the new memory directly supersedes a neighbor.',
35
+ 'classification = NOOP if the new memory is fully duplicate.',
36
+ 'links = neighbors the new memory should reference (max 3).',
37
+ 'neighbor_edits = small tag additions to neighbors (max 2 per neighbor; max 3 total).',
38
+ 'NEVER invent neighbors not in TOP-K. NEVER rewrite neighbor body.',
39
+ ].join('\n');
40
+
41
+ function selectNeighbors(db, entry, k = DEFAULT_TOPK) {
42
+ // Body-only lexical proximity via tokenized LIKE -- light, deterministic,
43
+ // no FTS dependency. Production memory_entries has no title column.
44
+ //
45
+ // Tokenization: split on non-word chars, filter to tokens >=4 chars
46
+ // (skips stop-words and noise), keep first 3. Build OR-LIKE so we find
47
+ // any neighbor that shares at least one significant token. Ranking is
48
+ // implicit via the LIMIT; if multi-token scoring matters in the future,
49
+ // wrap in a CASE-WHEN sum.
50
+ const tokens = (entry.body || entry.title || '')
51
+ .toLowerCase()
52
+ .split(/[^a-z0-9]+/i)
53
+ .filter((t) => t.length >= 4)
54
+ .slice(0, 3);
55
+ if (tokens.length === 0) return [];
56
+
57
+ const orClauses = tokens.map(() => 'body LIKE ?').join(' OR ');
58
+ const params = tokens.map((t) => `%${t}%`);
59
+ let sql = `SELECT CAST(id AS TEXT) AS id, body, source
60
+ FROM memory_entries
61
+ WHERE (${orClauses})`;
62
+ if (entry.id != null) {
63
+ sql += ' AND CAST(id AS TEXT) != ?';
64
+ params.push(String(entry.id));
65
+ }
66
+ sql += ' ORDER BY created_at DESC LIMIT ?';
67
+ params.push(k);
68
+ return db.prepare(sql).all(...params);
69
+ }
70
+
71
+ function applyProposal(db, entry, proposal) {
72
+ const insLink = db.prepare(
73
+ 'INSERT OR IGNORE INTO memory_links (from_id, to_target, line) VALUES (?, ?, 0)',
74
+ );
75
+ const insTag = db.prepare(
76
+ 'INSERT OR IGNORE INTO memory_tags (memory_id, tag_path, depth) VALUES (?, ?, ?)',
77
+ );
78
+ let linksAdded = 0;
79
+ let neighborTagsAdded = 0;
80
+ const tx = db.transaction(() => {
81
+ for (const l of (proposal.links || []).slice(0, 3)) {
82
+ if (l && typeof l.target === 'string' && l.target) {
83
+ const res = insLink.run(String(entry.id), l.target);
84
+ if (res.changes) linksAdded++;
85
+ }
86
+ }
87
+ let total = 0;
88
+ for (const ne of (proposal.neighbor_edits || []).slice(0, 5)) {
89
+ if (!ne || typeof ne.id !== 'string') continue;
90
+ for (const t of (ne.add_tags || []).slice(0, 2)) {
91
+ if (total >= 3) break;
92
+ if (typeof t === 'string' && t) {
93
+ const path = t.toLowerCase().replace(/^\/+|\/+$/g, '');
94
+ if (!path) continue;
95
+ const res = insTag.run(ne.id, path, path.split('/').length);
96
+ if (res.changes) {
97
+ neighborTagsAdded++;
98
+ total++;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ });
104
+ tx();
105
+ return { links_added: linksAdded, neighbor_tags_added: neighborTagsAdded };
106
+ }
107
+
108
+ export async function autoLink(db, entry, opts = {}) {
109
+ // Env-gate check FIRST — before any DB work — so the off-path is a
110
+ // true no-op (no SELECT against memory_entries, no race against
111
+ // fire-and-forget db.close calls in test harnesses).
112
+ if (!opts.dryProposal && !opts.neighborsOnly) {
113
+ if (process.env.IJFW_AUTOLINK_OFF === '1') {
114
+ return { skipped: true, reason: 'autolink_off' };
115
+ }
116
+ const budget = process.env.IJFW_AUTOLINK_BUDGET_USD;
117
+ if (budget !== undefined && Number(budget) <= 0) {
118
+ return { skipped: true, reason: 'budget_exhausted' };
119
+ }
120
+ const hasKey = !!(process.env.IJFW_AUTOLINK_API_KEY || process.env.ANTHROPIC_API_KEY);
121
+ if (!hasKey) {
122
+ return { skipped: true, reason: 'no_key' };
123
+ }
124
+ }
125
+ const neighbors = selectNeighbors(db, entry, opts.k || DEFAULT_TOPK);
126
+ if (opts.neighborsOnly) return { skipped: true, neighbors };
127
+
128
+ let proposal = opts.dryProposal;
129
+ if (!proposal) {
130
+ const userPayload = JSON.stringify({
131
+ new_memory: { id: String(entry.id), body: entry.body },
132
+ top_k_neighbors: neighbors.map((n) => ({ id: n.id, body: (n.body || '').slice(0, 200) })),
133
+ });
134
+ const llm = await llmCall({
135
+ system: SYSTEM_PROMPT,
136
+ user: userPayload,
137
+ maxTokens: 512,
138
+ });
139
+ if (llm.skipped) {
140
+ return { skipped: true, reason: llm.reason, neighbors };
141
+ }
142
+ try {
143
+ proposal = parseLlmJsonResponse(llm.text);
144
+ } catch (e) {
145
+ return { skipped: true, reason: 'parse_failed', neighbors, error: e.message };
146
+ }
147
+ }
148
+ const applied = applyProposal(db, entry, proposal);
149
+ return { skipped: false, neighbors, proposal, applied };
150
+ }
151
+
152
+ export default { autoLink };