forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7

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 (268) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/bin/forge.js +12 -1
  6. package/docs/guides/MIGRATION.md +3 -3
  7. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  8. package/docs/reference/INSTALL.md +4 -0
  9. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  10. package/docs/reference/RELEASE.md +4 -4
  11. package/docs/reference/github-accounts.md +134 -0
  12. package/docs/reference/shepherd.md +63 -13
  13. package/lib/adapters/pr-state-adapter.js +15 -2
  14. package/lib/base-remote.js +138 -0
  15. package/lib/beta5-compatibility-evidence.js +1093 -0
  16. package/lib/bun-lockfile-proof.js +413 -0
  17. package/lib/bun-workflow-pins.js +461 -0
  18. package/lib/capabilities/index.js +9 -0
  19. package/lib/capabilities/model.js +141 -0
  20. package/lib/capabilities/probes.js +347 -0
  21. package/lib/codex-skills.js +2 -2
  22. package/lib/commands/_manifest.js +1 -0
  23. package/lib/commands/_registry.js +48 -18
  24. package/lib/commands/clean.js +57 -1
  25. package/lib/commands/doctor.js +37 -6
  26. package/lib/commands/gate.js +197 -27
  27. package/lib/commands/github.js +215 -0
  28. package/lib/commands/hooks.js +54 -6
  29. package/lib/commands/memory.js +66 -2
  30. package/lib/commands/merge.js +720 -73
  31. package/lib/commands/plan.js +33 -2
  32. package/lib/commands/pr.js +2 -0
  33. package/lib/commands/preflight.js +10 -2
  34. package/lib/commands/push.js +120 -9
  35. package/lib/commands/recall.js +95 -61
  36. package/lib/commands/release.js +23 -2
  37. package/lib/commands/remember.js +28 -4
  38. package/lib/commands/serve.js +26 -9
  39. package/lib/commands/setup.js +132 -4
  40. package/lib/commands/shepherd.js +578 -72
  41. package/lib/commands/ship.js +15 -69
  42. package/lib/commands/skill.js +8 -0
  43. package/lib/commands/team.js +47 -8
  44. package/lib/commands/test.js +163 -4
  45. package/lib/commands/validate.js +78 -22
  46. package/lib/commands/worktree.js +155 -19
  47. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  48. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  49. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  50. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  51. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  52. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  53. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  54. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  55. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  56. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  57. package/lib/forge-issues.js +78 -0
  58. package/lib/gate-events.js +98 -10
  59. package/lib/github-context.js +308 -0
  60. package/lib/global-flags.js +1 -0
  61. package/lib/hook-renderer.js +29 -1
  62. package/lib/issue-render.js +19 -0
  63. package/lib/kernel/broker.js +723 -31
  64. package/lib/kernel/claim-reconciler.js +238 -0
  65. package/lib/kernel/lease-enforcer.js +9 -4
  66. package/lib/kernel/legacy-claim-repair.js +442 -0
  67. package/lib/kernel/live-claim-projection.js +26 -0
  68. package/lib/kernel/migrations.js +118 -3
  69. package/lib/kernel/readiness-model.js +184 -12
  70. package/lib/kernel/schema.js +49 -1
  71. package/lib/kernel/sqlite-driver.js +3322 -183
  72. package/lib/kernel/taxonomy-validator.js +4 -1
  73. package/lib/kernel/windows-private-acl.js +239 -0
  74. package/lib/memory/hygiene.js +191 -0
  75. package/lib/memory/router.js +94 -27
  76. package/lib/memory/usage-evidence.js +4 -0
  77. package/lib/memory-digest.js +59 -0
  78. package/lib/merge-rules.js +135 -17
  79. package/lib/npm-publish-workflow.js +233 -40
  80. package/lib/package-root.js +2 -0
  81. package/lib/pr-monitor/auto-actions.js +169 -28
  82. package/lib/pr-monitor/differ.js +110 -4
  83. package/lib/pr-monitor/events.js +0 -0
  84. package/lib/pr-monitor/flow-monitor.js +1424 -0
  85. package/lib/pr-monitor/gather.js +251 -44
  86. package/lib/pr-monitor/journal.js +0 -37
  87. package/lib/pr-monitor/monitor.js +117 -10
  88. package/lib/pr-monitor/process-identity.js +117 -0
  89. package/lib/pr-monitor/reconcile-executor.js +1101 -625
  90. package/lib/pr-monitor/reconcile.js +0 -0
  91. package/lib/pr-monitor/render-summary.js +121 -24
  92. package/lib/pr-monitor/review-preflight.js +269 -0
  93. package/lib/pr-monitor/shepherd-lease.js +28 -19
  94. package/lib/pr-monitor/verdict.js +438 -0
  95. package/lib/pr-monitor/watch-lifecycle.js +144 -38
  96. package/lib/pr-monitor/watch-owner.js +1414 -0
  97. package/lib/pr-monitor/watch.js +129 -58
  98. package/lib/pr-shepherd.js +17 -3
  99. package/lib/project-memory.js +145 -3
  100. package/lib/protected-state-authority.js +799 -4
  101. package/lib/protected-state-surfaces.js +181 -3
  102. package/lib/release-readiness.js +2 -3
  103. package/lib/review-adapter.js +65 -0
  104. package/lib/skills-sync.js +65 -32
  105. package/lib/validation/risk-manifest.js +339 -0
  106. package/lib/validation-receipt.js +190 -0
  107. package/lib/workflow/enforce-stage.js +44 -0
  108. package/lib/workflow/plan-authority.js +225 -0
  109. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  110. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  111. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  112. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  113. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  114. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  115. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  116. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  117. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  118. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  119. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  120. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  121. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  122. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  123. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  124. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  125. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  126. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  127. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  128. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  129. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  130. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  131. package/node_modules/@forge/contracts/index.js +32 -0
  132. package/node_modules/@forge/contracts/package.json +35 -0
  133. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  134. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  135. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  136. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  137. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  138. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  139. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  140. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  141. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  142. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  143. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  144. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  145. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  146. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  147. package/node_modules/@forge/contracts/src/identity.js +42 -0
  148. package/node_modules/@forge/contracts/src/schema.js +72 -0
  149. package/node_modules/@forge/contracts/src/validate.js +305 -0
  150. package/node_modules/@forge/flow/index.js +114 -0
  151. package/node_modules/@forge/flow/package.json +34 -0
  152. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  153. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  154. package/node_modules/@forge/flow/src/executor.js +279 -0
  155. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  156. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  157. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  158. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  159. package/node_modules/@forge/memory/index.js +331 -0
  160. package/node_modules/@forge/memory/package.json +34 -0
  161. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  162. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  163. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  164. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  165. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  166. package/package.json +16 -4
  167. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  168. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  169. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  170. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  171. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  172. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  173. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  174. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  175. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  176. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  177. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  178. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  179. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  180. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  181. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  182. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  183. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  184. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  185. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  186. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  187. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  188. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  189. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  190. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  191. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  192. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  193. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  194. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  195. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  196. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  197. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  198. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  199. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  200. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  201. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  202. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  203. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  204. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  205. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  206. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  207. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  208. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  209. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  210. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  211. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  212. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  213. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  214. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  215. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  216. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  217. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  218. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  219. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  220. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  221. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  222. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  223. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  224. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  225. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  226. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  227. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  228. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  229. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  230. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  231. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  232. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  233. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  234. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  235. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  236. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  237. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  238. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  239. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  240. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  241. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  242. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  243. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  244. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  245. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  246. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  247. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  248. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  249. package/scripts/commitlint.js +13 -15
  250. package/scripts/generate-risk-manifest.js +91 -0
  251. package/scripts/github-context-bridge.sh +10 -0
  252. package/scripts/legacy-claim-repair.js +145 -0
  253. package/scripts/lib/behavioral-eval-runtime.js +3 -2
  254. package/scripts/process-tree.js +14 -2
  255. package/scripts/protected-state-check.js +440 -17
  256. package/scripts/sync-agent-skills.js +333 -34
  257. package/scripts/test-full-suite.js +704 -18
  258. package/scripts/test-profile.js +13 -3
  259. package/scripts/test.js +96 -15
  260. package/skills/coverage.json +1 -0
  261. package/skills/review/SKILL.md +2 -0
  262. package/skills/review/evals/scorecard.json +2 -2
  263. package/skills/setup/SKILL.md +18 -0
  264. package/skills/setup/evals/scorecard.json +3 -3
  265. package/skills/shepherd/SKILL.md +19 -2
  266. package/skills/shepherd/evals/scorecard.json +3 -3
  267. package/skills/validate/SKILL.md +3 -0
  268. package/skills/validate/evals/scorecard.json +1 -1
@@ -29,7 +29,10 @@ const STATUS_TRANSITIONS = Object.freeze({
29
29
  backlog: Object.freeze(['open', 'cancelled']),
30
30
  });
31
31
 
32
- const CLAIM_STATES = Object.freeze(['active', 'released', 'expired']);
32
+ // `reclaimable` is the historical row left behind when an expired active lease is
33
+ // superseded. It is distinct from `expired`: both are non-authoritative history,
34
+ // but reclaimable records that the slot was deliberately freed for another owner.
35
+ const CLAIM_STATES = Object.freeze(['active', 'released', 'expired', 'reclaimable']);
33
36
 
34
37
  const DEFAULT_PRIORITY_RANK = 2;
35
38
  const MAX_DISPLAY_PRIORITY_RANK = 4;
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ /* global ActiveXObject, Enumerator, VBArray, WScript */
4
+
5
+ // This file is executed by Windows Script Host and loaded by Bun/Node tests.
6
+ // Keep its syntax compatible with the system JScript engine.
7
+ var ADS_PATH_FILE = 1;
8
+ var ADS_SD_FORMAT_IID = 1;
9
+ var ADS_SD_FORMAT_RAW = 2;
10
+ var ADS_SECURITY_INFO_OWNER = 1;
11
+ var ADS_SECURITY_INFO_DACL = 4;
12
+ var PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000;
13
+ var SE_DACL_PRESENT = 0x0004;
14
+ var SE_DACL_PROTECTED = 0x1000;
15
+ var SE_SELF_RELATIVE = 0x8000;
16
+ var FILE_ALL_ACCESS = 0x001f01ff;
17
+ var ACCESS_ALLOWED_ACE_TYPE = 0;
18
+ var OBJECT_INHERIT_ACE = 1;
19
+ var CONTAINER_INHERIT_ACE = 2;
20
+ var MAX_TARGETS = 128;
21
+
22
+ function invariant(condition, message) {
23
+ if (!condition) throw new Error(message);
24
+ }
25
+
26
+ function atStage(code, action) {
27
+ try {
28
+ return action();
29
+ } catch (error) {
30
+ error.forgeExitCode = code;
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ function bytesFromSafeArray(value) {
36
+ var values = new VBArray(value).toArray();
37
+ var bytes = [];
38
+ for (var i = 0; i < values.length; i++) bytes.push(values[i] & 0xff);
39
+ return bytes;
40
+ }
41
+
42
+ function u16(bytes, offset) {
43
+ invariant(offset >= 0 && offset + 2 <= bytes.length, 'truncated uint16');
44
+ return bytes[offset] | (bytes[offset + 1] << 8);
45
+ }
46
+
47
+ function u32(bytes, offset) {
48
+ invariant(offset >= 0 && offset + 4 <= bytes.length, 'truncated uint32');
49
+ return (bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24)) >>> 0;
50
+ }
51
+
52
+ function sidAt(bytes, offset) {
53
+ invariant(offset > 0 && offset + 8 <= bytes.length, 'missing SID');
54
+ var revision = bytes[offset];
55
+ var count = bytes[offset + 1];
56
+ var end = offset + 8 + (count * 4);
57
+ invariant(revision === 1 && end <= bytes.length, 'malformed SID');
58
+ var authority = 0;
59
+ for (var i = 0; i < 6; i++) authority = (authority * 256) + bytes[offset + 2 + i];
60
+ invariant(authority <= 9007199254740991, 'unsupported SID authority');
61
+ var parts = ['S', String(revision), String(authority)];
62
+ for (var j = 0; j < count; j++) parts.push(String(u32(bytes, offset + 8 + (j * 4))));
63
+ return { end: end, value: parts.join('-') };
64
+ }
65
+
66
+ function inspectDescriptor(bytes, expectedOwnerSid, expectedAceFlags) {
67
+ invariant(bytes && typeof bytes.length === 'number' && bytes.length >= 20, 'malformed security descriptor');
68
+ invariant(/^S-1-[0-9]+(?:-[0-9]+)+$/.test(expectedOwnerSid), 'invalid expected SID');
69
+ invariant(bytes[0] === 1, 'unsupported security descriptor revision');
70
+ var control = u16(bytes, 2);
71
+ invariant((control & SE_SELF_RELATIVE) !== 0, 'security descriptor is not self-relative');
72
+ invariant((control & SE_DACL_PRESENT) !== 0, 'DACL is absent');
73
+ invariant((control & SE_DACL_PROTECTED) !== 0, 'DACL is not protected');
74
+ var owner = sidAt(bytes, u32(bytes, 4));
75
+ invariant(owner.value === expectedOwnerSid, 'owner changed');
76
+ var daclOffset = u32(bytes, 16);
77
+ invariant(daclOffset > 0 && daclOffset + 8 <= bytes.length, 'DACL is malformed');
78
+ var aclRevision = bytes[daclOffset];
79
+ var aclSize = u16(bytes, daclOffset + 2);
80
+ var aceCount = u16(bytes, daclOffset + 4);
81
+ invariant(aclRevision === 2, 'unsupported ACL revision');
82
+ invariant(aclSize >= 8 && daclOffset + aclSize <= bytes.length, 'ACL size is malformed');
83
+ invariant(aceCount === 1, 'DACL must contain one ACE');
84
+ var aceOffset = daclOffset + 8;
85
+ invariant(aceOffset + 8 <= daclOffset + aclSize, 'ACE is truncated');
86
+ var aceType = bytes[aceOffset];
87
+ var aceFlags = bytes[aceOffset + 1];
88
+ var aceSize = u16(bytes, aceOffset + 2);
89
+ invariant(aceType === ACCESS_ALLOWED_ACE_TYPE, 'ACE must allow access');
90
+ invariant(aceFlags === expectedAceFlags, 'ACE flags are not exact');
91
+ invariant(aceSize >= 16 && aceOffset + aceSize === daclOffset + aclSize, 'ACE size is not exact');
92
+ invariant(u32(bytes, aceOffset + 4) === FILE_ALL_ACCESS, 'ACE is not full control');
93
+ var trustee = sidAt(bytes, aceOffset + 8);
94
+ invariant(trustee.value === expectedOwnerSid, 'ACE trustee is not the owner');
95
+ invariant(trustee.end === aceOffset + aceSize, 'ACE contains trailing data');
96
+ return { aceFlags: aceFlags, ownerSid: owner.value };
97
+ }
98
+
99
+ function inspectIidDescriptor(descriptor, expectedOwner, expectedAceFlags, enumeratorFactory) {
100
+ invariant(descriptor && descriptor.Owner, 'IID owner is absent');
101
+ if (expectedOwner !== null) invariant(String(descriptor.Owner) === expectedOwner, 'IID owner changed');
102
+ invariant((descriptor.Control & SE_DACL_PRESENT) !== 0, 'IID DACL is absent');
103
+ invariant((descriptor.Control & SE_DACL_PROTECTED) !== 0, 'IID DACL is not protected');
104
+ var dacl = descriptor.DiscretionaryAcl;
105
+ invariant(dacl && dacl.AceCount === 1, 'IID DACL must contain one ACE');
106
+ var entries = enumeratorFactory ? enumeratorFactory(dacl) : new Enumerator(dacl);
107
+ invariant(!entries.atEnd(), 'IID ACE is absent');
108
+ var ace = entries.item();
109
+ invariant(ace.AceType === ACCESS_ALLOWED_ACE_TYPE, 'IID ACE must allow access');
110
+ invariant((ace.AccessMask >>> 0) === FILE_ALL_ACCESS, 'IID ACE is not full control');
111
+ invariant(ace.AceFlags === expectedAceFlags, 'IID ACE flags are not exact');
112
+ entries.moveNext();
113
+ invariant(entries.atEnd(), 'IID DACL contains extra ACEs');
114
+ }
115
+
116
+ function rawDescriptor(utility, targetPath) {
117
+ utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL;
118
+ return bytesFromSafeArray(utility.GetSecurityDescriptor(targetPath, ADS_PATH_FILE, ADS_SD_FORMAT_RAW));
119
+ }
120
+
121
+ function iidDescriptor(utility, targetPath) {
122
+ utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL;
123
+ return utility.GetSecurityDescriptor(targetPath, ADS_PATH_FILE, ADS_SD_FORMAT_IID);
124
+ }
125
+
126
+ function ownerSid(bytes) {
127
+ invariant(bytes && typeof bytes.length === 'number' && bytes.length >= 20, 'malformed security descriptor');
128
+ invariant(bytes[0] === 1, 'unsupported security descriptor revision');
129
+ invariant((u16(bytes, 2) & SE_SELF_RELATIVE) !== 0, 'security descriptor is not self-relative');
130
+ return sidAt(bytes, u32(bytes, 4)).value;
131
+ }
132
+
133
+ function readTargets(environment, fso) {
134
+ var countText = String(environment('FORGE_PRIVATE_ACL_COUNT'));
135
+ invariant(/^[1-9][0-9]*$/.test(countText), 'invalid target count');
136
+ var count = Number(countText);
137
+ invariant(count <= MAX_TARGETS, 'too many ACL targets');
138
+ var targets = [];
139
+ for (var i = 0; i < count; i++) {
140
+ var targetPath = String(environment('FORGE_PRIVATE_ACL_TARGET_' + i));
141
+ invariant(targetPath.length > 0 && targetPath.indexOf('\0') === -1, 'invalid ACL target');
142
+ var isDirectory = fso.FolderExists(targetPath);
143
+ var isFile = fso.FileExists(targetPath);
144
+ invariant(isDirectory !== isFile, 'ACL target does not exist');
145
+ targets.push({ isDirectory: isDirectory, path: targetPath });
146
+ }
147
+ return targets;
148
+ }
149
+
150
+ function applyOwnerOnlyDacl(utility, target, currentSid, createObject, descriptor) {
151
+ var dacl = createObject('AccessControlList');
152
+ dacl.AclRevision = 2;
153
+ var ace = createObject('AccessControlEntry');
154
+ ace.Trustee = currentSid;
155
+ ace.AccessMask = FILE_ALL_ACCESS;
156
+ ace.AceType = ACCESS_ALLOWED_ACE_TYPE;
157
+ ace.AceFlags = target.isDirectory ? (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE) : 0;
158
+ dacl.AddAce(ace);
159
+ descriptor.Owner = currentSid;
160
+ descriptor.DiscretionaryAcl = dacl;
161
+ descriptor.Control = descriptor.Control | SE_DACL_PRESENT | SE_DACL_PROTECTED;
162
+ utility.SecurityMask = ADS_SECURITY_INFO_OWNER | ADS_SECURITY_INFO_DACL | PROTECTED_DACL_SECURITY_INFORMATION;
163
+ utility.SetSecurityDescriptor(target.path, ADS_PATH_FILE, descriptor, ADS_SD_FORMAT_IID);
164
+ }
165
+
166
+ function runWithDependencies(dependencies) {
167
+ var environment = dependencies.environment;
168
+ var fso = dependencies.fso;
169
+ var utility = dependencies.utility;
170
+ var createObject = dependencies.createObject;
171
+ var enumeratorFactory = dependencies.enumeratorFactory;
172
+ var currentSid = String(environment('FORGE_PRIVATE_ACL_SID'));
173
+ invariant(/^S-1-[0-9]+(?:-[0-9]+)+$/.test(currentSid), 'invalid current SID');
174
+ var targets = readTargets(environment, fso);
175
+ var owners = [];
176
+ var iidOwners = [];
177
+ var i;
178
+
179
+ // Prove every target and descriptor are readable before the first mutation.
180
+ for (i = 0; i < targets.length; i++) {
181
+ var beforeRaw = atStage(20, function () { return rawDescriptor(utility, targets[i].path); });
182
+ var beforeIid = atStage(21, function () { return iidDescriptor(utility, targets[i].path); });
183
+ owners.push(atStage(22, function () { return ownerSid(beforeRaw); }));
184
+ iidOwners.push(atStage(23, function () {
185
+ invariant(beforeIid && beforeIid.Owner, 'precheck IID owner is absent');
186
+ return String(beforeIid.Owner);
187
+ }));
188
+ }
189
+
190
+ for (i = 0; i < targets.length; i++) {
191
+ // Recheck immediately before changing the owner and DACL together.
192
+ var currentIid = atStage(30, function () {
193
+ var isDirectory = fso.FolderExists(targets[i].path);
194
+ var isFile = fso.FileExists(targets[i].path);
195
+ invariant(isDirectory === targets[i].isDirectory && isFile !== isDirectory, 'target type changed before mutation');
196
+ invariant(ownerSid(rawDescriptor(utility, targets[i].path)) === owners[i], 'owner changed before mutation');
197
+ var descriptor = iidDescriptor(utility, targets[i].path);
198
+ invariant(descriptor && descriptor.Owner, 'IID owner is absent before mutation');
199
+ invariant(String(descriptor.Owner) === iidOwners[i], 'IID owner changed before mutation');
200
+ return descriptor;
201
+ });
202
+ atStage(31, function () { applyOwnerOnlyDacl(utility, targets[i], currentSid, createObject, currentIid); });
203
+ var expectedFlags = targets[i].isDirectory ? (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE) : 0;
204
+ atStage(32, function () {
205
+ inspectDescriptor(rawDescriptor(utility, targets[i].path), currentSid, expectedFlags);
206
+ });
207
+ atStage(33, function () {
208
+ inspectIidDescriptor(iidDescriptor(utility, targets[i].path), null, expectedFlags, enumeratorFactory);
209
+ });
210
+ }
211
+ return true;
212
+ }
213
+
214
+ function main() {
215
+ try {
216
+ var shell = new ActiveXObject('WScript.Shell');
217
+ runWithDependencies({
218
+ createObject: function (name) { return new ActiveXObject(name); },
219
+ environment: shell.Environment('PROCESS'),
220
+ fso: new ActiveXObject('Scripting.FileSystemObject'),
221
+ utility: new ActiveXObject('ADsSecurityUtility')
222
+ });
223
+ WScript.Quit(0);
224
+ } catch (_error) {
225
+ var exitCode = _error && typeof _error.forgeExitCode === 'number' ? _error.forgeExitCode : 1;
226
+ WScript.Quit(exitCode);
227
+ }
228
+ }
229
+
230
+ if (typeof module !== 'undefined' && module.exports) {
231
+ module.exports = {
232
+ FILE_ALL_ACCESS: FILE_ALL_ACCESS,
233
+ inspectDescriptor: inspectDescriptor,
234
+ inspectIidDescriptor: inspectIidDescriptor,
235
+ runWithDependencies: runWithDependencies
236
+ };
237
+ } else {
238
+ main();
239
+ }
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+
5
+ const MEMORY_REVIEW_ENTRY_LIMIT = 200;
6
+ const DEFAULT_FINDING_LIMIT = 50;
7
+ const STALE_AFTER_DAYS = 90;
8
+ const STALE_AFTER_MS = STALE_AFTER_DAYS * 24 * 60 * 60 * 1000;
9
+ const VOLATILE_FIELDS = new Set([
10
+ 'id', 'key', 'memory_id', 'created_at', 'updated_at', 'timestamp', 'revision',
11
+ 'createdAt', 'updatedAt',
12
+ ]);
13
+ const NEGATION_PREFIXES = Object.freeze(['do not ', "don't ", 'never ']);
14
+
15
+ function compareCodeUnits(left, right) {
16
+ if (left < right) return -1;
17
+ if (left > right) return 1;
18
+ return 0;
19
+ }
20
+
21
+ function normalizeText(value) {
22
+ return String(value)
23
+ .normalize('NFKC')
24
+ .toLowerCase()
25
+ .replace(/[’']/g, "'")
26
+ .replace(/[^\p{L}\p{N}'\s]/gu, ' ')
27
+ .replace(/\s+/g, ' ')
28
+ .trim();
29
+ }
30
+
31
+ function canonicalize(value) {
32
+ if (typeof value === 'string') return normalizeText(value);
33
+ if (Array.isArray(value)) {
34
+ return value.map(canonicalize).sort((left, right) =>
35
+ compareCodeUnits(JSON.stringify(left), JSON.stringify(right)));
36
+ }
37
+ if (!value || typeof value !== 'object') return value;
38
+ return Object.fromEntries(Object.keys(value)
39
+ .filter(key => !VOLATILE_FIELDS.has(key))
40
+ .sort(compareCodeUnits)
41
+ .map(key => [key, canonicalize(value[key])]));
42
+ }
43
+
44
+ function payloadFor(entry) {
45
+ if (entry && Object.hasOwn(entry, 'note')) return entry.note;
46
+ if (entry && Object.hasOwn(entry, 'value')) return entry.value;
47
+ return entry;
48
+ }
49
+
50
+ function fingerprint(value) {
51
+ return createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
52
+ }
53
+
54
+ function reviewId(kind, parts) {
55
+ const digest = createHash('sha256')
56
+ .update(`${kind}\0${[...parts].sort(compareCodeUnits).join('\0')}`)
57
+ .digest('hex')
58
+ .slice(0, 16);
59
+ return `memory-${kind}-${digest}`;
60
+ }
61
+
62
+ function claimFor(entry) {
63
+ const payload = payloadFor(entry);
64
+ if (typeof payload !== 'string') return null;
65
+ let text = normalizeText(payload);
66
+ let negative = false;
67
+ const prefix = NEGATION_PREFIXES.find(candidate => text.startsWith(candidate));
68
+ if (prefix) {
69
+ negative = true;
70
+ text = text.slice(prefix.length).trim();
71
+ }
72
+ return text ? { basis: text, negative } : null;
73
+ }
74
+
75
+ function readOwnDataProperty(value, fieldName, subject) {
76
+ let descriptor;
77
+ try {
78
+ descriptor = Object.getOwnPropertyDescriptor(value, fieldName);
79
+ } catch {
80
+ throw new TypeError(`${subject} ${fieldName} must be an own data property`);
81
+ }
82
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
83
+ throw new TypeError(`${subject} ${fieldName} must be an own data property`);
84
+ }
85
+ return descriptor.value;
86
+ }
87
+
88
+ function stalenessForMemory(memory, usageProjection, options = {}) {
89
+ if (!memory || typeof memory !== 'object') {
90
+ throw new TypeError('memory must be an object');
91
+ }
92
+ const assertCanonicalIso = (value, fieldName) => {
93
+ if (typeof value !== 'string' || Number.isNaN(Date.parse(value))
94
+ || new Date(value).toISOString() !== value) {
95
+ throw new TypeError(`${fieldName} must be a canonical ISO timestamp`);
96
+ }
97
+ };
98
+ const createdAt = readOwnDataProperty(memory, 'created_at', 'memory');
99
+ assertCanonicalIso(createdAt, 'memory created_at');
100
+ let useCount = 0;
101
+ let baselineAt = createdAt;
102
+ if (usageProjection !== null && usageProjection !== undefined) {
103
+ if (!usageProjection || typeof usageProjection !== 'object') {
104
+ throw new TypeError('durable usage projection must be an object');
105
+ }
106
+ const lastUsedAt = readOwnDataProperty(usageProjection, 'last_used_at', 'durable usage projection');
107
+ const projectedUseCount = readOwnDataProperty(usageProjection, 'use_count', 'durable usage projection');
108
+ assertCanonicalIso(lastUsedAt, 'durable last_used_at');
109
+ if (!Number.isSafeInteger(projectedUseCount) || projectedUseCount < 0) {
110
+ throw new TypeError('durable usage projection use_count must be a nonnegative safe integer');
111
+ }
112
+ baselineAt = lastUsedAt;
113
+ useCount = projectedUseCount;
114
+ }
115
+ if (!options || typeof options !== 'object') {
116
+ throw new TypeError('staleness review options must be an object');
117
+ }
118
+ const now = readOwnDataProperty(options, 'now', 'staleness review options');
119
+ assertCanonicalIso(now, 'staleness review now');
120
+ const ageMs = Date.parse(now) - Date.parse(baselineAt);
121
+ const stale = ageMs > STALE_AFTER_MS;
122
+ return {
123
+ baseline_at: baselineAt,
124
+ use_count: useCount,
125
+ stale,
126
+ demote: stale,
127
+ age_days: Math.max(0, Math.floor(ageMs / (24 * 60 * 60 * 1000))),
128
+ };
129
+ }
130
+
131
+ function reviewMemories(entries, options = {}) {
132
+ const source = Array.isArray(entries) ? entries : [];
133
+ const entryLimit = Math.max(1, Math.min(options.entryLimit || MEMORY_REVIEW_ENTRY_LIMIT, MEMORY_REVIEW_ENTRY_LIMIT));
134
+ const findingLimit = Math.max(1, Math.min(options.findingLimit || DEFAULT_FINDING_LIMIT, DEFAULT_FINDING_LIMIT));
135
+ const selected = source.slice(0, entryLimit);
136
+ const findings = [];
137
+
138
+ const duplicateGroups = new Map();
139
+ for (const entry of selected) {
140
+ const payload = payloadFor(entry);
141
+ const canonical = JSON.stringify(canonicalize(payload));
142
+ const group = duplicateGroups.get(canonical) || [];
143
+ group.push(fingerprint(payload));
144
+ duplicateGroups.set(canonical, group);
145
+ }
146
+ for (const [canonical, members] of duplicateGroups) {
147
+ if (members.length < 2) continue;
148
+ findings.push({
149
+ review_id: reviewId('duplicate', [canonical]),
150
+ kind: 'duplicate',
151
+ count: members.length,
152
+ members: [...new Set(members)].sort(compareCodeUnits),
153
+ });
154
+ }
155
+
156
+ const claims = new Map();
157
+ for (const entry of selected) {
158
+ const claim = claimFor(entry);
159
+ if (!claim) continue;
160
+ const sides = claims.get(claim.basis) || { positive: [], negative: [] };
161
+ sides[claim.negative ? 'negative' : 'positive'].push(fingerprint(payloadFor(entry)));
162
+ claims.set(claim.basis, sides);
163
+ }
164
+ for (const [basis, sides] of claims) {
165
+ if (!sides.positive.length || !sides.negative.length) continue;
166
+ findings.push({
167
+ review_id: reviewId('contradiction', [basis]),
168
+ kind: 'contradiction',
169
+ claim: basis,
170
+ members: [...new Set([...sides.positive, ...sides.negative])].sort(compareCodeUnits),
171
+ });
172
+ }
173
+
174
+ findings.sort((left, right) => compareCodeUnits(left.review_id, right.review_id));
175
+ const total = Number.isInteger(options.total) && options.total >= source.length
176
+ ? options.total
177
+ : source.length;
178
+ return {
179
+ total,
180
+ scanned: selected.length,
181
+ truncated: total > selected.length || findings.length > findingLimit,
182
+ findings: findings.slice(0, findingLimit),
183
+ };
184
+ }
185
+
186
+ module.exports = {
187
+ MEMORY_REVIEW_ENTRY_LIMIT,
188
+ STALE_AFTER_DAYS,
189
+ reviewMemories,
190
+ stalenessForMemory,
191
+ };
@@ -29,11 +29,16 @@ const crypto = require('node:crypto');
29
29
  const fs = require('node:fs');
30
30
  const path = require('node:path');
31
31
  const projectMemory = require('../project-memory');
32
+ const {
33
+ assertBackend,
34
+ createMemoryBackendRegistry,
35
+ } = require('@forge/memory');
32
36
 
33
37
  /** Supported PUBLIC backends, default first. */
34
38
  const MEMORY_BACKENDS = ['local', 'graphiti'];
35
39
  const DEFAULT_MEMORY_BACKEND = 'local';
36
40
  const ENV_VAR = 'FORGE_MEMORY_BACKEND';
41
+ const registeredMemoryBackends = new Map();
37
42
 
38
43
  /** Default recall cap — newest-N, so `recall` with no query never dumps the whole store. */
39
44
  const DEFAULT_RECALL_LIMIT = 20;
@@ -126,7 +131,7 @@ function resolveMemoryBackend({
126
131
  if (!value) return DEFAULT_MEMORY_BACKEND;
127
132
 
128
133
  const normalized = value.toLowerCase();
129
- if (MEMORY_BACKENDS.includes(normalized)) return normalized;
134
+ if (MEMORY_BACKENDS.includes(normalized) || registeredMemoryBackends.has(normalized)) return normalized;
130
135
 
131
136
  warn(
132
137
  `Unknown memory backend "${value}" from ${source}; `
@@ -135,6 +140,22 @@ function resolveMemoryBackend({
135
140
  return DEFAULT_MEMORY_BACKEND;
136
141
  }
137
142
 
143
+ function registerMemoryBackend(name, adapter) {
144
+ const normalized = typeof name === 'string' ? name.trim().toLowerCase() : '';
145
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(normalized) || MEMORY_BACKENDS.includes(normalized)) {
146
+ throw new TypeError('custom memory backend must have a unique lowercase name');
147
+ }
148
+ if (registeredMemoryBackends.has(normalized)) {
149
+ throw new Error(`custom memory backend "${normalized}" is already registered`);
150
+ }
151
+ assertBackend(adapter, normalized);
152
+ registeredMemoryBackends.set(normalized, adapter);
153
+ }
154
+
155
+ function unregisterMemoryBackend(name) {
156
+ registeredMemoryBackends.delete(String(name || '').trim().toLowerCase());
157
+ }
158
+
138
159
  /**
139
160
  * Strict validation for the resolved backend. Unlike `resolveMemoryBackend`
140
161
  * (which soft-falls-back), this THROWS a clear, actionable error when the
@@ -145,6 +166,17 @@ function resolveMemoryBackend({
145
166
  */
146
167
  function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, config } = {}) {
147
168
  const memory = config || readMemoryConfig(projectRoot);
169
+ const signal = collectBackendSignal({ deps, env, projectRoot, config: memory });
170
+ const requested = signal.value?.toLowerCase();
171
+ if (requested && !MEMORY_BACKENDS.includes(requested) && !registeredMemoryBackends.has(requested)) {
172
+ const error = new Error(
173
+ `memory backend "${signal.value}" from ${signal.source} is not registered. `
174
+ + `Available backends: ${[...MEMORY_BACKENDS, ...registeredMemoryBackends.keys()]
175
+ .sort((left, right) => left.localeCompare(right)).join(', ')}.`,
176
+ );
177
+ error.backend = signal.value;
178
+ throw error;
179
+ }
148
180
  const backend = resolveMemoryBackend({ deps, env, projectRoot, config: memory, warn: () => {} });
149
181
 
150
182
  if (backend !== 'graphiti') {
@@ -182,19 +214,32 @@ function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, co
182
214
  * never-hang guarantee — the emitter, not this seam, owns preventing that.
183
215
  *
184
216
  * @param {object} [emitter] - optional `{ emit(entry) }` injected by callers.
185
- * @param {object} entry - the persisted local entry.
217
+ * @param {Function} entryProvider - returns the persisted local entry.
186
218
  */
187
- function fireAndForgetGraphitiEmit(emitter, entry) {
188
- if (!emitter || typeof emitter.emit !== 'function') return;
189
- try {
190
- // Do not await: fire-and-forget. If it returns a promise, swallow rejection.
191
- const maybePromise = emitter.emit(entry);
192
- if (maybePromise && typeof maybePromise.then === 'function') {
193
- maybePromise.then(() => {}, () => {});
194
- }
195
- } catch {
196
- // Hard fallback: the local write already succeeded. Never surface emit errors.
197
- }
219
+ // The implementation now lives in the adapter below so synchronous failures can
220
+ // be represented in the operation receipt while rejected promises stay consumed.
221
+
222
+ function graphitiAdapter(emitter, entryProvider) {
223
+ const unavailable = () => [];
224
+ const freeze = (value, seen = new WeakSet()) => {
225
+ if (!value || typeof value !== 'object' || seen.has(value)) return value;
226
+ seen.add(value);
227
+ for (const child of Object.values(value)) freeze(child, seen);
228
+ return Object.freeze(value);
229
+ };
230
+ return {
231
+ add() {
232
+ if (!emitter || typeof emitter.emit !== 'function') return;
233
+ const maybePromise = emitter.emit(freeze(structuredClone(entryProvider())));
234
+ if (maybePromise && typeof maybePromise.then === 'function') {
235
+ maybePromise.then(() => {}, () => {});
236
+ }
237
+ },
238
+ recall: unavailable,
239
+ search: unavailable,
240
+ capture: unavailable,
241
+ digest: unavailable,
242
+ };
198
243
  }
199
244
 
200
245
  /**
@@ -336,20 +381,38 @@ function migrateJsonlNotesOnce(projectRoot, options = {}) {
336
381
  * @param {object} [options] - { tags, deps, env, config, graphitiEmitter, store }
337
382
  * @returns {{ id: string, note: string, timestamp: string, tags: string[] }}
338
383
  */
339
- function append(projectRoot, note, options = {}) {
384
+ function appendWithReceipt(projectRoot, note, options = {}) {
340
385
  migrateJsonlNotesOnce(projectRoot, options);
341
386
  const backend = resolveMemoryBackend({ ...options, projectRoot });
342
- const written = projectMemory.write(projectRoot, {
343
- key: crypto.randomUUID(),
344
- value: note,
345
- sourceAgent: REMEMBER_SOURCE_AGENT,
346
- tags: Array.isArray(options.tags) ? options.tags : [],
347
- }, options);
348
- const entry = toNote(written);
387
+ let entry;
388
+ const local = {
389
+ add() {
390
+ const written = projectMemory.write(projectRoot, {
391
+ key: crypto.randomUUID(),
392
+ value: note,
393
+ sourceAgent: REMEMBER_SOURCE_AGENT,
394
+ tags: Array.isArray(options.tags) ? options.tags : [],
395
+ }, options);
396
+ entry = toNote(written);
397
+ return entry;
398
+ },
399
+ recall: () => [],
400
+ search: () => [],
401
+ capture: () => null,
402
+ digest: () => null,
403
+ };
404
+ const registry = createMemoryBackendRegistry({ local });
349
405
  if (backend === 'graphiti') {
350
- fireAndForgetGraphitiEmit(options.graphitiEmitter, entry);
406
+ registry.register('graphiti', graphitiAdapter(options.graphitiEmitter, () => entry));
407
+ } else if (backend !== 'local') {
408
+ registry.register(backend, registeredMemoryBackends.get(backend));
351
409
  }
352
- return entry;
410
+ registry.select(backend);
411
+ return registry.add(note);
412
+ }
413
+
414
+ function append(projectRoot, note, options = {}) {
415
+ return appendWithReceipt(projectRoot, note, options).value;
353
416
  }
354
417
 
355
418
  /**
@@ -363,7 +426,7 @@ function append(projectRoot, note, options = {}) {
363
426
  * widens it to every stored memory.
364
427
  *
365
428
  * @param {string} projectRoot
366
- * @param {object} [selection] - { query, limit, all }
429
+ * @param {object} [selection] - { query, limit, all, kind }
367
430
  * @param {object} [options] - forwarded to the project-memory read paths (e.g. a store).
368
431
  * @returns {{ notes: object[], total: number, capped: boolean, query: string, limit: number, scope: string }}
369
432
  */
@@ -373,15 +436,16 @@ function recall(projectRoot, selection = {}, options = {}) {
373
436
  const limit = Number.isInteger(requested) && requested > 0 ? requested : DEFAULT_RECALL_LIMIT;
374
437
  const query = String(selection.query || '').trim();
375
438
  const includeAll = Boolean(selection.all);
439
+ const kind = typeof selection.kind === 'string' ? selection.kind.trim() : '';
440
+ const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
441
+ const readOptions = { ...options, agents, kind };
376
442
 
377
443
  if (query) {
378
- const notes = projectMemory.searchRanked(projectRoot, query, limit, options).map(toNote);
444
+ const notes = projectMemory.searchRanked(projectRoot, query, limit, readOptions).map(toNote);
379
445
  // BM25 returns at most `limit`; a full result set signals there may be more.
380
446
  return { notes, total: notes.length, capped: notes.length >= limit, query, limit, scope: 'all' };
381
447
  }
382
448
 
383
- const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
384
- const readOptions = { ...options, agents };
385
449
  const notes = projectMemory.recent(projectRoot, limit, readOptions).map(toNote);
386
450
  const total = projectMemory.count(projectRoot, readOptions);
387
451
  return { notes, total, capped: total > notes.length, query: '', limit, scope: includeAll ? 'all' : 'remembered' };
@@ -395,8 +459,11 @@ module.exports = {
395
459
  readMemoryConfig,
396
460
  resolveMemoryBackend,
397
461
  assertMemoryConfigValid,
462
+ registerMemoryBackend,
463
+ unregisterMemoryBackend,
398
464
  migrateJsonlNotesOnce,
399
465
  toNote,
400
466
  append,
467
+ appendWithReceipt,
401
468
  recall,
402
469
  };
@@ -0,0 +1,4 @@
1
+ 'use strict';
2
+
3
+ // Compatibility shim: the durable usage contract belongs to @forge/memory.
4
+ module.exports = require('@forge/memory');