kenaz 0.3.17 → 0.3.19

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 (316) hide show
  1. package/core/.claude/commands/agent-worker.md +1 -1
  2. package/core/.claude/commands/architect.md +1 -2
  3. package/core/.claude/commands/spec.md +1 -1
  4. package/core/.claude/commands//347/224/242/345/223/201.md +1 -1
  5. package/core/.claude/hooks/__tests__/task-1977-dedupe-guard.test.js +8 -8
  6. package/core/.claude/hooks/synapse-first-investigation.js +15 -0
  7. package/core/.claude/skills/architect_agent/SKILL.md +11 -92
  8. package/core/.claude/skills/assessment_agent/SKILL.md +0 -1
  9. package/core/.claude/skills/backend_engineer/SKILL.md +3 -6
  10. package/core/.claude/skills/code_reviewer/SKILL.md +1 -1
  11. package/core/.claude/skills/code_reviewer/patterns.yaml +1 -1
  12. package/core/.claude/skills/designer/SKILL.md +3 -6
  13. package/core/.claude/skills/dev_agent/SKILL.md +30 -491
  14. package/core/.claude/skills/developer/SKILL.md +3 -6
  15. package/core/.claude/skills/feature_agent/SKILL.md +3 -17
  16. package/core/.claude/skills/product_manager/SKILL.md +29 -1064
  17. package/core/.claude/skills/qa_engineer/SKILL.md +12 -17
  18. package/core/.claude/skills/spec_optimizer/SKILL.md +25 -275
  19. package/core/.claude/skills/sre_agent/SKILL.md +3 -6
  20. package/core/.collaboration/README.md +2 -3
  21. package/core/.collaboration/core/task-api.js +5 -138
  22. package/core/.collaboration/docs/CLI.md +0 -2
  23. package/core/.failure/INDEX.yaml +6 -40
  24. package/core/.failure/agent-execution/FAIL-013-search-cwd-drift-and-case-mismatch.yaml +1 -1
  25. package/core/.failure/tools/README.md +9 -227
  26. package/core/.formation/bifrost/exec.js +29 -4
  27. package/core/.formation/bifrost/launch.js +1 -6
  28. package/core/.formation/bifrost/launch.test.js +1 -1
  29. package/core/.formation/bifrost/provider-launch.js +18 -11
  30. package/core/.formation/bifrost/provider-launch.test.js +26 -2
  31. package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
  32. package/core/.formation/bifrost/shared/ansuz-boot-policy.test.js +1 -2
  33. package/core/.formation/tools/__tests__/dispatch-cwd-project.test.js +42 -0
  34. package/core/.formation/tools/__tests__/m1-hmap-convid.test.js +1 -124
  35. package/core/.formation/tools/__tests__/subagent-executor.test.js +3 -2
  36. package/core/.formation/tools/dispatch.js +15 -2
  37. package/core/.formation/tools/loader.js +17 -140
  38. package/core/.formation/tools/post-complete.js +0 -31
  39. package/core/.rule/INDEX.yaml +0 -18
  40. package/core/.rule/general/agent-workflow.yaml +14 -164
  41. package/core/.rule/general/project-self-containment.yaml +8 -19
  42. package/core/.rule/general/rust-verify-target.yaml +6 -5
  43. package/core/.rule/general/synapse-code-intelligence.yaml +3 -3
  44. package/core/.specialist/Amelia/capability.yaml +12 -12
  45. package/core/.specialist/Amelia/workflow.yaml +2 -2
  46. package/core/.specialist/Ansuz/SYSTEM_DOC.md +20 -578
  47. package/core/.specialist/Ansuz/cognitive_principles.yaml +2 -3
  48. package/core/.specialist/Ansuz/concepts/boot_rules_summary.yaml +0 -1
  49. package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +1 -2
  50. package/core/.specialist/Ansuz/core.yaml +6 -108
  51. package/core/.specialist/Ansuz/guards/lesson_guards.yaml +0 -3
  52. package/core/.specialist/Ansuz/identity_contract.yaml +0 -1
  53. package/core/.specialist/Ansuz/knowledge.yaml +0 -4
  54. package/core/.specialist/Ansuz/rule.yaml +0 -1
  55. package/core/.specialist/Ansuz/tools/handoff-session-identity.test.js +6 -150
  56. package/core/.specialist/tests/runtime-discovery/README.md +1 -1
  57. package/core/.specialist/tools/__tests__/task-2374-usage-counter.test.js +1 -12
  58. package/core/.specialist/tools/prompt-files.test.js +1 -0
  59. package/core/.specialist/tools/task-bridge.js +1 -66
  60. package/core/.system/README.md +11 -552
  61. package/core/.system/collaboration/README.md +10 -174
  62. package/core/CLAUDE.md +1 -6
  63. package/core/KENAZ_CORE_VERSION +1 -1
  64. package/core/dev/scripts/js-yaml-resolution.test.js +1 -1
  65. package/core/dev/scripts/run-rust-tests.js +30 -46
  66. package/core/dev/scripts/run-rust-tests.test.js +17 -0
  67. package/core/dev/scripts/rust-verify-worktree.js +4 -4
  68. package/core/dev/scripts/verify-target-dir.js +6 -6
  69. package/core/plugins/kenaz/INVENTORY.md +8 -8
  70. package/core/plugins/kenaz/commands/architect.md +1 -2
  71. package/core/plugins/kenaz/commands/spec.md +1 -1
  72. package/core/plugins/kenaz/commands//347/224/242/345/223/201.md +1 -1
  73. package/core/plugins/kenaz/guidance.md +1 -1
  74. package/core/plugins/kenaz/scripts/sync.js +1 -1
  75. package/core/plugins/kenaz/scripts/ygg.js +6 -0
  76. package/core/plugins/kenaz/skills/architect_agent/SKILL.md +11 -92
  77. package/core/plugins/kenaz/skills/assessment_agent/SKILL.md +0 -1
  78. package/core/plugins/kenaz/skills/backend_engineer/SKILL.md +3 -6
  79. package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +1 -1
  80. package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +1 -1
  81. package/core/plugins/kenaz/skills/designer/SKILL.md +3 -6
  82. package/core/plugins/kenaz/skills/dev_agent/SKILL.md +30 -491
  83. package/core/plugins/kenaz/skills/developer/SKILL.md +3 -6
  84. package/core/plugins/kenaz/skills/feature_agent/SKILL.md +3 -17
  85. package/core/plugins/kenaz/skills/product_manager/SKILL.md +29 -1064
  86. package/core/plugins/kenaz/skills/qa_engineer/SKILL.md +12 -17
  87. package/core/plugins/kenaz/skills/spec_optimizer/SKILL.md +25 -275
  88. package/core/plugins/kenaz/skills/sre_agent/SKILL.md +3 -6
  89. package/package.json +6 -6
  90. package/core/.claude/commands/archive.md +0 -24
  91. package/core/.claude/commands/init.md +0 -112
  92. package/core/.claude/commands/legacy_dev.md +0 -18
  93. package/core/.claude/commands/product-review.md +0 -131
  94. package/core/.claude/commands/strategy.md +0 -141
  95. package/core/.claude/commands/sunny.md +0 -1
  96. package/core/.claude/commands//347/224/242/345/223/201/346/252/242/350/246/226.md +0 -129
  97. package/core/.claude/commands//351/200/262/345/272/246.md +0 -15
  98. package/core/.claude/hooks/__tests__/substrate.test.js +0 -122
  99. package/core/.claude/hooks/_substrate-paths.js +0 -95
  100. package/core/.claude/hooks/context-check.js +0 -51
  101. package/core/.claude/hooks/fatigue-check.js +0 -52
  102. package/core/.claude/hooks/handoff-snapshot-on-commit.js +0 -154
  103. package/core/.claude/hooks/handoff-snapshot-on-question.js +0 -158
  104. package/core/.claude/hooks/handoff-staleness.js +0 -197
  105. package/core/.claude/hooks/jev-thalamus-context.js +0 -17
  106. package/core/.claude/hooks/jev-thalamus-context.test.js +0 -41
  107. package/core/.claude/hooks/lease-release.js +0 -119
  108. package/core/.claude/hooks/lease-touch.js +0 -96
  109. package/core/.claude/hooks/reflex-feedback.js +0 -5
  110. package/core/.claude/hooks/session-init.ps1 +0 -49
  111. package/core/.claude/hooks/sound-event.js +0 -70
  112. package/core/.claude/hooks/sound-event.sh +0 -37
  113. package/core/.claude/hooks/stream-delta.js +0 -169
  114. package/core/.claude/no-hooks.settings.json +0 -3
  115. package/core/.claude/skills/legacy_dev/SKILL.md +0 -202
  116. package/core/.claude/skills/sunny/SKILL.md +0 -74
  117. package/core/.collaboration/cli/commands/assign.js +0 -39
  118. package/core/.collaboration/cli/commands/claim.js +0 -33
  119. package/core/.collaboration/cli/commands/complete.js +0 -44
  120. package/core/.collaboration/cli/commands/create.js +0 -110
  121. package/core/.collaboration/cli/commands/get.js +0 -83
  122. package/core/.collaboration/cli/commands/index.js +0 -52
  123. package/core/.collaboration/cli/commands/list.js +0 -51
  124. package/core/.collaboration/cli/commands/lock.js +0 -45
  125. package/core/.collaboration/cli/commands/rebuild.js +0 -39
  126. package/core/.collaboration/cli/commands/search.js +0 -61
  127. package/core/.collaboration/cli/commands/stats.js +0 -61
  128. package/core/.collaboration/cli/commands/transfer.js +0 -41
  129. package/core/.collaboration/cli/commands/unlock.js +0 -48
  130. package/core/.collaboration/cli/formatters/help-formatter.js +0 -70
  131. package/core/.collaboration/cli/formatters/index.js +0 -7
  132. package/core/.collaboration/cli/task-cli.js +0 -46
  133. package/core/.collaboration/cli/utils/arg-parser.js +0 -74
  134. package/core/.collaboration/cli/utils/git-user.js +0 -25
  135. package/core/.collaboration/cli/utils/index.js +0 -9
  136. package/core/.collaboration/cli/utils/output.js +0 -10
  137. package/core/.collaboration/config/default.yaml +0 -189
  138. package/core/.collaboration/core/conflict-resolver.js +0 -461
  139. package/core/.collaboration/core/conflict-resolver.test.js +0 -389
  140. package/core/.collaboration/core/migrate-legacy.js +0 -260
  141. package/core/.collaboration/docs/API.md +0 -787
  142. package/core/.collaboration/docs/ARCHITECTURE.md +0 -596
  143. package/core/.collaboration/docs/FAQ.md +0 -695
  144. package/core/.collaboration/docs/MIGRATION.md +0 -804
  145. package/core/.collaboration/docs/examples/README.md +0 -320
  146. package/core/.collaboration/docs/examples/advanced-queries.js +0 -261
  147. package/core/.collaboration/docs/examples/basic-usage.js +0 -184
  148. package/core/.collaboration/docs/examples/team-workflow.js +0 -352
  149. package/core/.failure/agent-execution/FAIL-001-agent-no-output.yaml +0 -391
  150. package/core/.failure/agent-execution/FAIL-007-pm-lead-wrong-team-invocation.yaml +0 -0
  151. package/core/.failure/agent-execution/FAIL-010-phase0-poem-format-skipped.yaml +0 -28
  152. package/core/.failure/agent-execution/FAIL-010-skipping-cognitive-checkpoint-after-verification.yaml +0 -59
  153. package/core/.failure/agent-execution/FAIL-012-grep-shortcut-during-investigation.yaml +0 -85
  154. package/core/.failure/architecture/FAIL-006-python-version-compatibility.yaml +0 -445
  155. package/core/.failure/pipeline/FAIL-003-queue-pollution.yaml +0 -335
  156. package/core/.failure/pipeline/README.md +0 -95
  157. package/core/.failure/retired/FAIL-009-skipping-synapse-pulse-before-code-change.yaml +0 -49
  158. package/core/.failure/tools/USAGE_GUIDE.md +0 -323
  159. package/core/.failure/tools/check_failures.ps1 +0 -387
  160. package/core/.failure/tools/log_failure.ps1 +0 -474
  161. package/core/.formation/.episode-input.json +0 -10
  162. package/core/.formation/.merge-lock +0 -1
  163. package/core/.formation/lead.yaml +0 -118
  164. package/core/.formation/legacy.bak/designer.yaml +0 -57
  165. package/core/.formation/legacy.bak/pm.yaml +0 -57
  166. package/core/.formation/legacy.bak/qa.yaml +0 -57
  167. package/core/.formation/legacy.bak/review.yaml +0 -62
  168. package/core/.formation/legacy.bak/scheduler.yaml +0 -129
  169. package/core/.formation/legacy.bak/sre.yaml +0 -57
  170. package/core/.formation/role-map.yaml +0 -274
  171. package/core/.formation/roles/_shared/modules/code_review.yaml +0 -98
  172. package/core/.formation/roles/_shared/modules/security.yaml +0 -103
  173. package/core/.formation/roles/_shared/modules/testing.yaml +0 -14
  174. package/core/.formation/roles/backend/role.yaml +0 -93
  175. package/core/.formation/roles/code_reviewer/role.yaml +0 -102
  176. package/core/.formation/roles/database_optimizer/role.yaml +0 -107
  177. package/core/.formation/roles/designer/role.yaml +0 -66
  178. package/core/.formation/roles/developer/role.yaml +0 -75
  179. package/core/.formation/roles/frontend/role.yaml +0 -134
  180. package/core/.formation/roles/incident_response/role.yaml +0 -89
  181. package/core/.formation/roles/qa/role.yaml +0 -89
  182. package/core/.formation/roles/security/role.yaml +0 -87
  183. package/core/.formation/roles/sre/role.yaml +0 -66
  184. package/core/.formation/skills/coordinator.yaml +0 -52
  185. package/core/.formation/solo/diagnosis.yaml +0 -78
  186. package/core/.formation/solo/strategy.yaml +0 -121
  187. package/core/.formation/tests/hello.txt +0 -2
  188. package/core/.formation/tools/coordinator.js +0 -350
  189. package/core/.formation/tools/jev-correction.js +0 -214
  190. package/core/.formation/tools/jev-correction.test.js +0 -178
  191. package/core/.formation/tools/log-reader.js +0 -232
  192. package/core/.formation/tools/revert-daemon-state.js +0 -162
  193. package/core/.formation/tools/test-drift.js +0 -148
  194. package/core/.formation/unit/heimdall.yaml +0 -96
  195. package/core/.formation/unit/plan.yaml +0 -67
  196. package/core/.formation/unit/think.yaml +0 -58
  197. package/core/.kenaz/MUNINN_SNAPSHOT9_EPISODE.json +0 -32
  198. package/core/.kenaz/MUNINN_SNAPSHOT9_RUNE.json +0 -23
  199. package/core/.kenaz/MUNINN_SNAPSHOT9_SUMMARY.md +0 -89
  200. package/core/.kenaz/ash_TASK_1428-20260507.json +0 -14
  201. package/core/.kenaz/muninn_episode_write.ps1 +0 -17
  202. package/core/.kenaz/muninn_snapshot11_rune_candidates.md +0 -63
  203. package/core/.kenaz/muninn_snapshot1_rune_candidates.md +0 -49
  204. package/core/.kenaz/runes_TASK_1428.json +0 -57
  205. package/core/.kenaz/tmp_heimdall_proposal_8d3e392.json +0 -13
  206. package/core/.kenaz/tmp_props.json +0 -1
  207. package/core/.kenaz/tmp_sounddrop_t1_prompt.txt +0 -371
  208. package/core/.rule/MECHANIZED.md +0 -61
  209. package/core/.rule/general/cortex-memory-recall.yaml +0 -446
  210. package/core/.rule/general/pipeline.yaml +0 -246
  211. package/core/.rule/general/sunny-access-control.yaml +0 -60
  212. package/core/.rule/general/system-invariants.yaml +0 -231
  213. package/core/.rule/pipeline/PPL-002-completed-task-archival.yaml +0 -528
  214. package/core/.rule/pipeline/auto-update.yaml +0 -282
  215. package/core/.rule/pipeline/task-execution.yaml +0 -365
  216. package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +0 -65
  217. package/core/.specialist/Ansuz/experiments/boot_ablation_replay.js +0 -197
  218. package/core/.specialist/Ansuz/tools/bootstrap-anchors.js +0 -289
  219. package/core/.specialist/Ansuz/workflow.yaml +0 -1225
  220. package/core/.specialist/experiments/rg_benchmark.js +0 -523
  221. package/core/.specialist/experiments/rg_benchmark_results.json +0 -27
  222. package/core/.specialist/tools/jev-optional-context.js +0 -107
  223. package/core/.specialist/tools/jev-optional-context.test.js +0 -152
  224. package/core/.specialist/tools/memory-action-log.js +0 -839
  225. package/core/.specialist/tools/phase-gate.js +0 -3534
  226. package/core/.specialist/tools/phase-gate.test.js +0 -85
  227. package/core/.specialist/tools/precompute-runner.js +0 -444
  228. package/core/.specialist/tools/rule-engine.js +0 -621
  229. package/core/.specialist/tools/task-bridge-jev.test.js +0 -170
  230. package/core/.synapse/memory-shared.json.sync +0 -1
  231. package/core/.system/agent-skills/README.md +0 -724
  232. package/core/.system/cli-tools/README.md +0 -724
  233. package/core/.system/cortex/MODULES.md +0 -1046
  234. package/core/.system/dashboard/FRONTEND.md +0 -812
  235. package/core/.system/dashboard/README.md +0 -737
  236. package/core/.system/failure/README.md +0 -723
  237. package/core/.system/formation/LEGACY_SKILLS.md +0 -974
  238. package/core/.system/formation/POST_COMPLETE.md +0 -602
  239. package/core/.system/formation/README.md +0 -1094
  240. package/core/.system/formation/TOOLS_REFERENCE.md +0 -1145
  241. package/core/.system/kenaz/README.md +0 -787
  242. package/core/.system/memory/README.md +0 -719
  243. package/core/.system/pipeline/README.md +0 -712
  244. package/core/.system/rule/README.md +0 -794
  245. package/core/.system/sunny/README.md +0 -476
  246. package/core/.system/sunny/RULES.json +0 -89
  247. package/core/.system/sunny/knowledge/INDEX.md +0 -37
  248. package/core/.system/sunny/knowledge/agent-teams-api.md +0 -0
  249. package/core/.system/sunny/knowledge/ansuz-specialist.md +0 -500
  250. package/core/.system/sunny/knowledge/claude-cli-spawn.md +0 -177
  251. package/core/.system/sunny/knowledge/collaboration-api.md +0 -925
  252. package/core/.system/sunny/teammates/README.md +0 -107
  253. package/core/.system/sunny/teammates/how_to_use.yaml +0 -101
  254. package/core/.system/sunny/teammates/tools/headless/launcher.js +0 -318
  255. package/core/.system/sunny/teammates/tools/powershell/launcher.js +0 -241
  256. package/core/.system/sunny/teammates/tools/powershell/start.bat +0 -50
  257. package/core/.system/sunny/teammates/tools/utils/env.js +0 -96
  258. package/core/.system/sunny/teammates/tools/utils/format-output.js +0 -329
  259. package/core/.system/sunny/tools.json +0 -101
  260. package/core/dev/scripts/cleanup-queue.js +0 -184
  261. package/core/dev/scripts/cleanup_queue.js +0 -86
  262. package/core/dev/scripts/legacy/README.md +0 -39
  263. package/core/dev/scripts/legacy/dev_agent_scheduler.ps1.bak +0 -536
  264. package/core/dev/scripts/legacy/multi_agent_scheduler.ps1.bak +0 -1978
  265. package/core/dev/scripts/matrix/CLI_README.md +0 -214
  266. package/core/dev/scripts/matrix/README.md +0 -310
  267. package/core/dev/scripts/matrix/package-lock.json +0 -4266
  268. package/core/dev/scripts/matrix/package.json +0 -55
  269. package/core/dev/scripts/matrix/src/abbreviations.ts +0 -295
  270. package/core/dev/scripts/matrix/src/cli/commands/convert.ts +0 -211
  271. package/core/dev/scripts/matrix/src/cli/commands/create.ts +0 -95
  272. package/core/dev/scripts/matrix/src/cli/commands/delete.ts +0 -102
  273. package/core/dev/scripts/matrix/src/cli/commands/list-tags.ts.wip +0 -140
  274. package/core/dev/scripts/matrix/src/cli/commands/manage-tags.ts.wip +0 -173
  275. package/core/dev/scripts/matrix/src/cli/commands/query.ts +0 -261
  276. package/core/dev/scripts/matrix/src/cli/commands/read.ts +0 -77
  277. package/core/dev/scripts/matrix/src/cli/commands/stats.ts.wip +0 -168
  278. package/core/dev/scripts/matrix/src/cli/commands/tokens.ts.wip +0 -126
  279. package/core/dev/scripts/matrix/src/cli/commands/update.ts +0 -147
  280. package/core/dev/scripts/matrix/src/cli/commands/validate.ts +0 -275
  281. package/core/dev/scripts/matrix/src/cli/index.ts +0 -350
  282. package/core/dev/scripts/matrix/src/cli/utils/file-handler.ts +0 -72
  283. package/core/dev/scripts/matrix/src/cli/utils/logger.ts +0 -65
  284. package/core/dev/scripts/matrix/src/index.ts +0 -62
  285. package/core/dev/scripts/matrix/src/indexer/tag-indexer.ts +0 -165
  286. package/core/dev/scripts/matrix/src/parser.ts +0 -281
  287. package/core/dev/scripts/matrix/src/serializer.ts +0 -151
  288. package/core/dev/scripts/matrix/src/validator.ts +0 -301
  289. package/core/dev/scripts/matrix/tests/abbreviations.test.ts +0 -122
  290. package/core/dev/scripts/matrix/tests/cli.test.ts +0 -177
  291. package/core/dev/scripts/matrix/tests/convert.test.ts +0 -143
  292. package/core/dev/scripts/matrix/tests/parser.test.ts +0 -203
  293. package/core/dev/scripts/matrix/tests/query.test.ts +0 -85
  294. package/core/dev/scripts/matrix/tests/serializer.test.ts +0 -197
  295. package/core/dev/scripts/matrix/tests/validate.test.ts +0 -141
  296. package/core/dev/scripts/matrix/tests/validator.test.ts +0 -167
  297. package/core/dev/scripts/matrix/tsconfig.json +0 -20
  298. package/core/dev/scripts/matrix/types/index.d.ts +0 -111
  299. package/core/dev/scripts/matrix/types/index.d.ts.map +0 -1
  300. package/core/dev/scripts/matrix/types/index.js +0 -8
  301. package/core/dev/scripts/matrix/types/index.js.map +0 -1
  302. package/core/dev/scripts/matrix/types/index.ts +0 -124
  303. package/core/dev/scripts/memory-usage-report.js +0 -152
  304. package/core/dev/scripts/probe/ansuz-scaffold-ablation.js +0 -579
  305. package/core/dev/scripts/probe/ansuz-scaffold-ablation.test.js +0 -244
  306. package/core/dev/scripts/probe/hook-latency-probe.js +0 -315
  307. package/core/dev/scripts/probe/hook-latency-probe.test.js +0 -105
  308. package/core/dev/scripts/probe/prediction-signal-auc.js +0 -483
  309. package/core/dev/scripts/probe/prediction-signal-auc.test.js +0 -200
  310. package/core/dev/scripts/test-pty-ws-tunnel.js +0 -229
  311. package/core/dev/scripts/validate_pipeline.js +0 -197
  312. package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +0 -56
  313. package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
  314. package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
  315. package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +0 -1
  316. package/core/plugins/kenaz/commands/strategy.md +0 -141
@@ -1,839 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * memory-action-log.js — Unified Memory-to-Action instrumentation (TASK_1072 + TASK_1078)
5
- *
6
- * Tracks whether a recalled memory item (handoff, episode, rune, sigil,
7
- * self-model lesson, lesson guard) later influenced a design, dispatch,
8
- * validation or fix-loop decision.
9
- *
10
- * Why this exists:
11
- * Recall has been a silent operation — we had no way to distinguish
12
- * "loaded into context" from "actually changed the agent's behaviour".
13
- * This module turns memory from passive context into a measurable
14
- * decision-influence signal.
15
- *
16
- * Relationship to existing infrastructure:
17
- * - Runes: synapse rune_usage_log (TASK_589) — we delegate; our
18
- * log cross-references rune_id but does not duplicate
19
- * the rune table.
20
- * - Lesson guards: lesson-guard.js (TASK_1071/1076) — we call its module
21
- * API so the legacy lesson_guard_acted_on.jsonl and the
22
- * new unified log stay in sync.
23
- * - Handoff/episode/sigil/self-model: NEW — no prior acted-on tracking.
24
- *
25
- * Storage:
26
- * .kenaz/specialist/memory_action_log.jsonl (append-only, JSONL)
27
- *
28
- * Schema (one JSON object per line):
29
- * {
30
- * "timestamp": "<ISO-8601>",
31
- * "event": "recall" | "acted_on",
32
- * "recall_id": "<stable ID, e.g. recall_1712345_abc123>", // always present
33
- * "source": "handoff" | "episode" | "rune" | "sigil" | "self_model" | "lesson_guard",
34
- * "source_id": "<stable id: rune_id | episode_id | sigil_id | B-XXX | L-XXX | handoff session_id>",
35
- * "task_id": "<TASK_XXXX or session id>",
36
- * "phase": <int phase> | null,
37
- * "action_type": "design_decision" | "dispatch_choice" | "validation_behavior"
38
- * | "fix_loop_decision" | "scope_adjustment" | "blocked_exit"
39
- * | null,
40
- * "reason": "<evidence text, >=20 chars for acted_on>",
41
- * "conflict_with": "<winner_source_id>" | null,
42
- * "priority_rank": <int from memory_source_priority.yaml> | null,
43
- * "recalled_from_ids": ["<recall_id>", ...] // for acted_on events: IDs of recall events that led to this decision
44
- * }
45
- *
46
- * CLI:
47
- * node memory-action-log.js record-recall --source=rune --source-id=rune_X --task=TASK_XXX [--phase=N] [--reason="text"]
48
- * node memory-action-log.js record-acted --source=rune --source-id=rune_X --task=TASK_XXX --action-type=design_decision --reason="why" [--recalled-from=recall_id1,recall_id2]
49
- * node memory-action-log.js report [--window-days=30] [--task=TASK_XXX] [--json]
50
- * node memory-action-log.js priority [--source-type=rune] [--type=decision] [--confidence=0.85]
51
- * node memory-action-log.js list-recalls --task=TASK_XXX [--phase=N] [--json]
52
- *
53
- * Module exports (for phase-gate.js and other callers):
54
- * recordRecall(entry) → { ok, recall_id, line?, error? }
55
- * recordActedOn(entry) → { ok, line?, error? }
56
- * getMetrics(opts) → { by_source, totals, window_days, link_rate }
57
- * getPriorityRank(descriptor) → { rank, tier, mode }
58
- * loadPriorityRules() → parsed yaml object (cached)
59
- * recordActedOnBatch(items, taskId, phase) → { recorded, failed[] }
60
- * listRecalls(taskId, phase?) → [recall entries]
61
- *
62
- * Graceful degradation contract:
63
- * Every write path is try/catch — a failure in the memory-action-log
64
- * must never block a phase-gate exit or cause a specialist to crash.
65
- * On failure the module returns { ok: false, error } and never throws.
66
- *
67
- * Exit codes (CLI):
68
- * 0 = success
69
- * 1 = fatal error (bad args, missing required field)
70
- */
71
-
72
- 'use strict';
73
-
74
- const fs = require('fs');
75
- const path = require('path');
76
- const crypto = require('crypto');
77
-
78
- // ═══════════════════════════════════════
79
- // Path resolution
80
- // ═══════════════════════════════════════
81
-
82
- function findProjectRoot() {
83
- let dir = __dirname;
84
- while (dir !== path.dirname(dir)) {
85
- const parent = path.dirname(dir);
86
- if (fs.existsSync(path.join(parent, '.specialist'))) return parent;
87
- dir = parent;
88
- }
89
- return process.cwd();
90
- }
91
-
92
- const BASE_DIR = findProjectRoot();
93
- const LOG_PATH = path.join(BASE_DIR, '.kenaz', 'specialist', 'memory_action_log.jsonl');
94
- const PRIORITY_PATH = path.join(BASE_DIR, '.kenaz', 'specialist', 'memory_source_priority.yaml');
95
- const LESSON_GUARD_JS = path.join(BASE_DIR, '.specialist', 'tools', 'lesson-guard.js');
96
-
97
- const VALID_SOURCES = new Set([
98
- 'handoff', 'episode', 'rune', 'sigil', 'self_model', 'lesson_guard',
99
- ]);
100
-
101
- const VALID_ACTION_TYPES = new Set([
102
- 'design_decision',
103
- 'dispatch_choice',
104
- 'validation_behavior',
105
- 'fix_loop_decision',
106
- 'scope_adjustment',
107
- 'blocked_exit',
108
- ]);
109
-
110
- const MIN_REASON_CHARS = 20;
111
-
112
- // ═══════════════════════════════════════
113
- // recall_id generation (TASK_1078)
114
- // ═══════════════════════════════════════
115
-
116
- /**
117
- * Generate a stable, unique recall ID.
118
- * Format: recall_<timestamp_ms>_<6-hex-chars>
119
- * Stable within the same millisecond window; globally unique enough to
120
- * cross-reference within a session log. Not a cryptographic guarantee.
121
- */
122
- function generateRecallId() {
123
- const ts = Date.now();
124
- const rnd = crypto.randomBytes(3).toString('hex');
125
- return `recall_${ts}_${rnd}`;
126
- }
127
-
128
- // ═══════════════════════════════════════
129
- // Minimal YAML parser for memory_source_priority.yaml
130
- // (same pattern as lesson-guard.js — no external deps)
131
- // ═══════════════════════════════════════
132
-
133
- let _priorityCache = null;
134
-
135
- /**
136
- * Parse the priority yaml into a JS object. We only implement the subset
137
- * needed to answer getPriorityRank() — nested maps with scalar leaves and
138
- * small lists. If the file is missing or unparseable we fall back to the
139
- * hard-coded defaults below.
140
- */
141
- function loadPriorityRules() {
142
- if (_priorityCache) return _priorityCache;
143
- const fallback = {
144
- version: 1,
145
- sources: {
146
- user_session_context: { priority_rank: 100, conflict_mode: 'strict' },
147
- verified_decision_rune: { priority_rank: 90, conflict_mode: 'strict', source_type: 'rune' },
148
- session_handoff: { priority_rank: 80, conflict_mode: 'advisory', source_type: 'handoff' },
149
- self_model_lesson: { priority_rank: 70, conflict_mode: 'advisory', source_type: 'self_model' },
150
- risk_or_fix_rune: { priority_rank: 60, conflict_mode: 'strict', source_type: 'rune' },
151
- sigil: { priority_rank: 50, conflict_mode: 'advisory', source_type: 'sigil' },
152
- insight_or_pattern_rune: { priority_rank: 40, conflict_mode: 'advisory', source_type: 'rune' },
153
- episode: { priority_rank: 30, conflict_mode: 'advisory', source_type: 'episode' },
154
- summary_or_ash_rune: { priority_rank: 20, conflict_mode: 'advisory', source_type: 'rune' },
155
- lesson_guard: { priority_rank: 10, conflict_mode: 'advisory', source_type: 'lesson_guard' },
156
- },
157
- acted_on_weights: {
158
- verified_decision_rune: 1.0, session_handoff: 1.0, self_model_lesson: 1.5,
159
- risk_or_fix_rune: 1.2, sigil: 0.8, insight_or_pattern_rune: 0.7,
160
- episode: 0.5, summary_or_ash_rune: 0.5, lesson_guard: 0.4,
161
- },
162
- };
163
-
164
- if (!fs.existsSync(PRIORITY_PATH)) {
165
- _priorityCache = fallback;
166
- return _priorityCache;
167
- }
168
-
169
- try {
170
- const raw = fs.readFileSync(PRIORITY_PATH, 'utf8');
171
- const parsed = parsePriorityYaml(raw);
172
- _priorityCache = parsed || fallback;
173
- return _priorityCache;
174
- } catch {
175
- _priorityCache = fallback;
176
- return _priorityCache;
177
- }
178
- }
179
-
180
- function parsePriorityYaml(content) {
181
- const out = { version: 1, sources: {}, acted_on_weights: {}, tie_breakers: [], acted_on_evidence_requirements: {} };
182
- const lines = content.split('\n');
183
-
184
- let section = null; // 'sources' | 'weights' | 'tiebreakers' | 'evidence'
185
- let currentSource = null;
186
- let currentNested = null; // 'rune_filter' | 'allowed_action_types' | etc.
187
-
188
- for (let i = 0; i < lines.length; i++) {
189
- const line = lines[i];
190
- const trimmed = line.trim();
191
- if (!trimmed || trimmed.startsWith('#')) continue;
192
-
193
- // Top-level: version
194
- const v = line.match(/^version:\s*(\d+)/);
195
- if (v) { out.version = parseInt(v[1], 10); continue; }
196
-
197
- // Top-level section markers
198
- if (/^sources:\s*$/.test(line)) { section = 'sources'; currentSource = null; continue; }
199
- if (/^acted_on_weights:\s*$/.test(line)) { section = 'weights'; currentSource = null; continue; }
200
- if (/^tie_breakers:\s*$/.test(line)) { section = 'tiebreakers'; currentSource = null; continue; }
201
- if (/^acted_on_evidence_requirements:\s*$/.test(line)) { section = 'evidence'; currentSource = null; continue; }
202
-
203
- if (section === 'sources') {
204
- // Source key: " name_of_source:"
205
- const srcMatch = line.match(/^ {2}([a-z_]+):\s*$/);
206
- if (srcMatch) {
207
- currentSource = srcMatch[1];
208
- out.sources[currentSource] = {};
209
- currentNested = null;
210
- continue;
211
- }
212
- if (!currentSource) continue;
213
-
214
- // Nested rune_filter block
215
- if (/^ {4}rune_filter:\s*$/.test(line)) {
216
- out.sources[currentSource].rune_filter = {};
217
- currentNested = 'rune_filter';
218
- continue;
219
- }
220
-
221
- // Scalar fields at 4-space indent
222
- const scalarMatch = line.match(/^ {4}([a-z_]+):\s*(.+?)\s*$/);
223
- if (scalarMatch && currentNested !== 'rune_filter') {
224
- const key = scalarMatch[1];
225
- let val = scalarMatch[2];
226
- // Strip trailing "> description" multi-line block continuations
227
- if (val === '>') {
228
- // Gather subsequent indented lines
229
- let buf = [];
230
- while (i + 1 < lines.length && /^ {6}/.test(lines[i + 1])) {
231
- buf.push(lines[++i].trim());
232
- }
233
- out.sources[currentSource][key] = buf.join(' ');
234
- continue;
235
- }
236
- val = val.replace(/^"|"$/g, '');
237
- if (/^-?\d+$/.test(val)) out.sources[currentSource][key] = parseInt(val, 10);
238
- else if (/^-?\d+\.\d+$/.test(val)) out.sources[currentSource][key] = parseFloat(val);
239
- else if (val === 'true' || val === 'false') out.sources[currentSource][key] = (val === 'true');
240
- else out.sources[currentSource][key] = val;
241
- continue;
242
- }
243
-
244
- // Inside rune_filter (6-space indent)
245
- if (currentNested === 'rune_filter') {
246
- const nested = line.match(/^ {6}([a-z_]+):\s*(.+?)\s*$/);
247
- if (nested) {
248
- const key = nested[1];
249
- let val = nested[2];
250
- if (/^\[.*\]$/.test(val)) {
251
- val = val.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
252
- } else if (/^-?\d+\.\d+$/.test(val)) {
253
- val = parseFloat(val);
254
- } else {
255
- val = val.replace(/^"|"$/g, '');
256
- }
257
- out.sources[currentSource].rune_filter[key] = val;
258
- continue;
259
- }
260
- // End of nested if indent decreases
261
- if (/^ {4}[a-z_]/.test(line)) currentNested = null;
262
- }
263
- }
264
-
265
- if (section === 'weights') {
266
- const m = line.match(/^ {2}([a-z_]+):\s*([\d.]+)/);
267
- if (m) out.acted_on_weights[m[1]] = parseFloat(m[2]);
268
- }
269
- }
270
-
271
- return out;
272
- }
273
-
274
- /**
275
- * Return the priority tier that best matches a recalled item.
276
- *
277
- * descriptor = { source, rune_type?, confidence?, verified? }
278
- *
279
- * The match order is:
280
- * - Exact source_type tiers first (rune/handoff/episode/...).
281
- * - Within runes: match rune_filter (type + min_confidence + verified_not).
282
- * - Highest priority_rank wins on tie.
283
- * Returns { rank, tier, mode } or null if no tier matched.
284
- */
285
- function getPriorityRank(descriptor) {
286
- const rules = loadPriorityRules();
287
- const source = descriptor && descriptor.source;
288
- if (!source) return null;
289
-
290
- const matches = [];
291
- for (const [tierName, tier] of Object.entries(rules.sources)) {
292
- if (tier.source_type && tier.source_type !== source) continue;
293
- // For generic source tiers (no source_type), only match by name
294
- if (!tier.source_type && tierName !== source) continue;
295
-
296
- if (source === 'rune' && tier.rune_filter) {
297
- const f = tier.rune_filter;
298
- if (f.type && descriptor.rune_type !== f.type) continue;
299
- if (f.type_in && Array.isArray(f.type_in) && !f.type_in.includes(descriptor.rune_type)) continue;
300
- if (typeof f.min_confidence === 'number' && (descriptor.confidence || 0) < f.min_confidence) continue;
301
- if (f.verified_not && descriptor.verified === f.verified_not) continue;
302
- }
303
-
304
- matches.push({ tier: tierName, rank: tier.priority_rank || 0, mode: tier.conflict_mode || 'advisory' });
305
- }
306
-
307
- if (matches.length === 0) return null;
308
- matches.sort((a, b) => b.rank - a.rank);
309
- return matches[0];
310
- }
311
-
312
- // ═══════════════════════════════════════
313
- // Append-only JSONL writer
314
- // ═══════════════════════════════════════
315
-
316
- function ensureLogDir() {
317
- const dir = path.dirname(LOG_PATH);
318
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
319
- }
320
-
321
- function appendEntry(entry) {
322
- try {
323
- ensureLogDir();
324
- fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n', 'utf8');
325
- return { ok: true };
326
- } catch (e) {
327
- return { ok: false, error: e.message };
328
- }
329
- }
330
-
331
- // ═══════════════════════════════════════
332
- // recordRecall — module entry point
333
- // ═══════════════════════════════════════
334
-
335
- /**
336
- * Record a "memory was recalled into context" event.
337
- *
338
- * Minimal required fields: { source, source_id, task_id }
339
- * Accepted fields: phase, reason, rune_type, confidence, verified
340
- *
341
- * Does NOT enforce min-reason-chars — recall events are cheap, we log
342
- * everything. Action events have the stricter gate.
343
- *
344
- * Returns { ok, recall_id, line? } — recall_id is the stable ID for this
345
- * event, which can be passed into later recordActedOn calls via recalled_from_ids.
346
- */
347
- function recordRecall(input) {
348
- if (!input || typeof input !== 'object') {
349
- return { ok: false, error: 'input must be an object' };
350
- }
351
- const { source, source_id: sourceId, task_id: taskId } = input;
352
-
353
- if (!VALID_SOURCES.has(source)) {
354
- return { ok: false, error: `invalid source '${source}' (expected one of: ${[...VALID_SOURCES].join(', ')})` };
355
- }
356
- if (!sourceId) return { ok: false, error: 'source_id is required' };
357
- if (!taskId) return { ok: false, error: 'task_id is required' };
358
-
359
- const prio = getPriorityRank({
360
- source,
361
- rune_type: input.rune_type || null,
362
- confidence: input.confidence || null,
363
- verified: input.verified || null,
364
- });
365
-
366
- const recallId = generateRecallId();
367
-
368
- const entry = {
369
- timestamp: new Date().toISOString(),
370
- event: 'recall',
371
- recall_id: recallId,
372
- source,
373
- source_id: String(sourceId),
374
- task_id: String(taskId),
375
- phase: Number.isFinite(input.phase) ? input.phase : null,
376
- action_type: null,
377
- reason: input.reason ? String(input.reason) : null,
378
- conflict_with: null,
379
- priority_rank: prio ? prio.rank : null,
380
- priority_tier: prio ? prio.tier : null,
381
- conflict_mode: prio ? prio.mode : null,
382
- recalled_from_ids: [],
383
- };
384
-
385
- const write = appendEntry(entry);
386
- if (!write.ok) return write;
387
- return { ok: true, recall_id: recallId, line: entry };
388
- }
389
-
390
- // ═══════════════════════════════════════
391
- // recordActedOn — module entry point
392
- // ═══════════════════════════════════════
393
-
394
- /**
395
- * Record a "recalled memory influenced a concrete decision" event.
396
- *
397
- * Required: { source, source_id, task_id, action_type, reason }
398
- * Optional: recalled_from_ids (array of recall_ids from prior recordRecall calls
399
- * that directly led to this decision — enables linking recall→action)
400
- * reason must be >= MIN_REASON_CHARS to filter out rubber-stamping.
401
- *
402
- * Side effects:
403
- * - Appends to memory_action_log.jsonl
404
- * - If source=lesson_guard and source_id matches /^L-\d+$/, also calls
405
- * lesson-guard.js recordActedOn() so the legacy per-guard counter and
406
- * lesson_guard_acted_on.jsonl stay in sync.
407
- * - If source=rune, the Synapse rune_usage_log acted_on bit should be
408
- * set by the caller via synapse_inscribe related_rune_ids. We do NOT
409
- * attempt a direct HTTP call here because the MCP layer is outside
410
- * this tool's scope — we rely on the caller having already done it
411
- * when they inscribed the follow-up rune. If they have not, our log
412
- * still captures the event for reporting.
413
- */
414
- function recordActedOn(input) {
415
- if (!input || typeof input !== 'object') {
416
- return { ok: false, error: 'input must be an object' };
417
- }
418
- const { source, source_id: sourceId, task_id: taskId, action_type: actionType, reason } = input;
419
-
420
- if (!VALID_SOURCES.has(source)) {
421
- return { ok: false, error: `invalid source '${source}'` };
422
- }
423
- if (!sourceId) return { ok: false, error: 'source_id is required' };
424
- if (!taskId) return { ok: false, error: 'task_id is required' };
425
- if (!VALID_ACTION_TYPES.has(actionType)) {
426
- return { ok: false, error: `invalid action_type '${actionType}' (expected one of: ${[...VALID_ACTION_TYPES].join(', ')})` };
427
- }
428
- if (!reason || String(reason).trim().length < MIN_REASON_CHARS) {
429
- return { ok: false, error: `reason must be >= ${MIN_REASON_CHARS} chars (got ${reason ? String(reason).length : 0})` };
430
- }
431
-
432
- const prio = getPriorityRank({
433
- source,
434
- rune_type: input.rune_type || null,
435
- confidence: input.confidence || null,
436
- verified: input.verified || null,
437
- });
438
-
439
- // recalled_from_ids: array of recall_id strings that led to this decision
440
- const recalledFromIds = Array.isArray(input.recalled_from_ids)
441
- ? input.recalled_from_ids.filter(id => typeof id === 'string' && id.startsWith('recall_'))
442
- : [];
443
-
444
- const entry = {
445
- timestamp: new Date().toISOString(),
446
- event: 'acted_on',
447
- recall_id: generateRecallId(), // stable ID for this acted_on event too
448
- source,
449
- source_id: String(sourceId),
450
- task_id: String(taskId),
451
- phase: Number.isFinite(input.phase) ? input.phase : null,
452
- action_type: actionType,
453
- reason: String(reason),
454
- conflict_with: input.conflict_with || null,
455
- priority_rank: prio ? prio.rank : null,
456
- priority_tier: prio ? prio.tier : null,
457
- conflict_mode: prio ? prio.mode : null,
458
- recalled_from_ids: recalledFromIds,
459
- };
460
-
461
- const write = appendEntry(entry);
462
- if (!write.ok) return write;
463
-
464
- // Bridge to lesson-guard.js for L-XXX guards so the two logs agree.
465
- if (source === 'lesson_guard' && /^L-\d+$/.test(String(sourceId))) {
466
- try {
467
- const lg = require(LESSON_GUARD_JS);
468
- if (lg && typeof lg.recordActedOn === 'function') {
469
- lg.recordActedOn(String(sourceId), String(taskId), String(reason));
470
- }
471
- } catch { /* graceful degradation */ }
472
- }
473
-
474
- return { ok: true, line: entry };
475
- }
476
-
477
- /**
478
- * Batch helper used by phase-gate.js exit. Accepts an array of items the
479
- * specialist reports as having influenced the current phase's decisions,
480
- * records them all, and returns per-item status.
481
- *
482
- * Input shape:
483
- * items = [{ source, source_id, action_type, reason, phase?, rune_type?,
484
- * recalled_from_ids?: string[] }]
485
- * taskId = string
486
- * phase = int (default applied to items lacking phase)
487
- */
488
- function recordActedOnBatch(items, taskId, phase) {
489
- const result = { recorded: 0, failed: [] };
490
- if (!Array.isArray(items) || items.length === 0) return result;
491
- for (const raw of items) {
492
- if (!raw || typeof raw !== 'object') {
493
- result.failed.push({ item: raw, error: 'not an object' });
494
- continue;
495
- }
496
- const item = Object.assign({ task_id: taskId, phase }, raw);
497
- const r = recordActedOn(item);
498
- if (r.ok) result.recorded += 1;
499
- else result.failed.push({ item, error: r.error });
500
- }
501
- return result;
502
- }
503
-
504
- // ═══════════════════════════════════════
505
- // listRecalls — session recall registry (TASK_1078)
506
- // ═══════════════════════════════════════
507
-
508
- /**
509
- * Return all recall events for a given task_id, optionally filtered by phase.
510
- * Useful for an agent to enumerate recall_ids that can be passed into
511
- * recordActedOn calls when linking decisions back to the recalls that shaped them.
512
- *
513
- * Returns array of recall entries (event='recall') sorted by timestamp ascending.
514
- * Graceful on missing log (returns empty array).
515
- */
516
- function listRecalls(taskId, phase) {
517
- if (!taskId) return [];
518
- if (!fs.existsSync(LOG_PATH)) return [];
519
-
520
- let raw;
521
- try { raw = fs.readFileSync(LOG_PATH, 'utf8'); }
522
- catch { return []; }
523
-
524
- const results = [];
525
- for (const line of raw.split('\n')) {
526
- if (!line.trim()) continue;
527
- let entry;
528
- try { entry = JSON.parse(line); } catch { continue; }
529
- if (!entry || entry.event !== 'recall') continue;
530
- if (entry.task_id !== String(taskId)) continue;
531
- if (phase !== undefined && entry.phase !== phase) continue;
532
- results.push(entry);
533
- }
534
- return results;
535
- }
536
-
537
- // ═══════════════════════════════════════
538
- // Metrics / report
539
- // ═══════════════════════════════════════
540
-
541
- /**
542
- * Stream the jsonl log and aggregate recall-vs-impact counters.
543
- * Graceful on missing log (returns zeroed report).
544
- *
545
- * opts = { windowDays?: number, taskId?: string }
546
- * returns:
547
- * {
548
- * window_days, task_id, total_recalls, total_acted_on,
549
- * impact_rate, weighted_impact_score,
550
- * linked_acts, // acted_on events with non-empty recalled_from_ids
551
- * link_rate, // linked_acts / total_acted_on (traceability)
552
- * by_source: {
553
- * <source>: { recalls, acted_on, impact_rate, weighted_score, linked_acts }
554
- * },
555
- * by_action_type: { <action>: count },
556
- * unique_items_recalled: N,
557
- * unique_items_acted_on: N
558
- * }
559
- */
560
- function getMetrics(opts = {}) {
561
- const windowDays = Number.isFinite(opts.windowDays) ? opts.windowDays : 30;
562
- const taskFilter = opts.taskId || null;
563
- const cutoff = Date.now() - windowDays * 86400 * 1000;
564
-
565
- const report = {
566
- window_days: windowDays,
567
- task_id: taskFilter,
568
- total_recalls: 0,
569
- total_acted_on: 0,
570
- impact_rate: 0,
571
- weighted_impact_score: 0,
572
- linked_acts: 0,
573
- link_rate: 0,
574
- by_source: {},
575
- by_action_type: {},
576
- unique_items_recalled: 0,
577
- unique_items_acted_on: 0,
578
- };
579
-
580
- if (!fs.existsSync(LOG_PATH)) return report;
581
-
582
- const rules = loadPriorityRules();
583
- const weights = rules.acted_on_weights || {};
584
- const recalledSet = new Set();
585
- const actedSet = new Set();
586
-
587
- let raw;
588
- try { raw = fs.readFileSync(LOG_PATH, 'utf8'); }
589
- catch { return report; }
590
-
591
- for (const line of raw.split('\n')) {
592
- if (!line.trim()) continue;
593
- let entry;
594
- try { entry = JSON.parse(line); } catch { continue; }
595
- if (!entry || !entry.source) continue;
596
-
597
- const ts = Date.parse(entry.timestamp || '');
598
- if (!Number.isFinite(ts) || ts < cutoff) continue;
599
- if (taskFilter && entry.task_id !== taskFilter) continue;
600
-
601
- const srcBucket = report.by_source[entry.source] || {
602
- recalls: 0, acted_on: 0, impact_rate: 0, weighted_score: 0, linked_acts: 0,
603
- };
604
- const key = `${entry.source}:${entry.source_id}`;
605
-
606
- if (entry.event === 'recall') {
607
- report.total_recalls += 1;
608
- srcBucket.recalls += 1;
609
- recalledSet.add(key);
610
- } else if (entry.event === 'acted_on') {
611
- report.total_acted_on += 1;
612
- srcBucket.acted_on += 1;
613
- actedSet.add(key);
614
- if (entry.action_type) {
615
- report.by_action_type[entry.action_type] = (report.by_action_type[entry.action_type] || 0) + 1;
616
- }
617
- // linked_acts: this acted_on was explicitly traced to prior recall events
618
- if (Array.isArray(entry.recalled_from_ids) && entry.recalled_from_ids.length > 0) {
619
- report.linked_acts += 1;
620
- srcBucket.linked_acts += 1;
621
- }
622
- // Weighted score: use the tier weight if available, else source weight, else 1.0.
623
- const tierWeight = entry.priority_tier && weights[entry.priority_tier];
624
- const srcWeight = weights[entry.source];
625
- const w = Number.isFinite(tierWeight) ? tierWeight
626
- : Number.isFinite(srcWeight) ? srcWeight
627
- : 1.0;
628
- report.weighted_impact_score += w;
629
- srcBucket.weighted_score += w;
630
- }
631
-
632
- report.by_source[entry.source] = srcBucket;
633
- }
634
-
635
- report.unique_items_recalled = recalledSet.size;
636
- report.unique_items_acted_on = actedSet.size;
637
- report.impact_rate = report.total_recalls > 0
638
- ? +(report.total_acted_on / report.total_recalls).toFixed(4)
639
- : 0;
640
- report.link_rate = report.total_acted_on > 0
641
- ? +(report.linked_acts / report.total_acted_on).toFixed(4)
642
- : 0;
643
-
644
- for (const src of Object.keys(report.by_source)) {
645
- const b = report.by_source[src];
646
- b.impact_rate = b.recalls > 0 ? +(b.acted_on / b.recalls).toFixed(4) : 0;
647
- b.weighted_score = +b.weighted_score.toFixed(3);
648
- }
649
- report.weighted_impact_score = +report.weighted_impact_score.toFixed(3);
650
-
651
- return report;
652
- }
653
-
654
- // ═══════════════════════════════════════
655
- // CLI
656
- // ═══════════════════════════════════════
657
-
658
- function parseArgs(argv) {
659
- const args = { _: [] };
660
- for (const arg of argv.slice(2)) {
661
- const m = arg.match(/^--([^=]+)(?:=(.*))?$/);
662
- if (m) args[m[1]] = m[2] !== undefined ? m[2] : true;
663
- else args._.push(arg);
664
- }
665
- return args;
666
- }
667
-
668
- function cmdRecordRecall(args) {
669
- const entry = {
670
- source: args.source,
671
- source_id: args['source-id'],
672
- task_id: args.task,
673
- phase: args.phase !== undefined ? parseInt(args.phase, 10) : null,
674
- reason: args.reason || null,
675
- rune_type: args['rune-type'] || null,
676
- confidence: args.confidence !== undefined ? parseFloat(args.confidence) : null,
677
- verified: args.verified || null,
678
- };
679
- const r = recordRecall(entry);
680
- if (!r.ok) { console.error(JSON.stringify(r)); process.exit(1); }
681
- console.log(JSON.stringify(r, null, 2));
682
- }
683
-
684
- function cmdRecordActed(args) {
685
- // --recalled-from=id1,id2,id3 → array
686
- const recalledFrom = args['recalled-from']
687
- ? String(args['recalled-from']).split(',').map(s => s.trim()).filter(Boolean)
688
- : [];
689
-
690
- const entry = {
691
- source: args.source,
692
- source_id: args['source-id'],
693
- task_id: args.task,
694
- phase: args.phase !== undefined ? parseInt(args.phase, 10) : null,
695
- action_type: args['action-type'],
696
- reason: args.reason || '',
697
- rune_type: args['rune-type'] || null,
698
- confidence: args.confidence !== undefined ? parseFloat(args.confidence) : null,
699
- conflict_with: args['conflict-with'] || null,
700
- recalled_from_ids: recalledFrom,
701
- };
702
- const r = recordActedOn(entry);
703
- if (!r.ok) { console.error(JSON.stringify(r)); process.exit(1); }
704
- console.log(JSON.stringify(r, null, 2));
705
- }
706
-
707
- function cmdReport(args) {
708
- const windowDays = args['window-days'] !== undefined ? parseInt(args['window-days'], 10) : 30;
709
- const taskId = args.task || null;
710
- const report = getMetrics({ windowDays, taskId });
711
- if (args.json) {
712
- console.log(JSON.stringify(report, null, 2));
713
- return;
714
- }
715
- // Plain report
716
- const lines = [];
717
- lines.push(`Memory-to-Action report (window: ${windowDays}d${taskId ? `, task: ${taskId}` : ''})`);
718
- lines.push('');
719
- lines.push(` total_recalls: ${report.total_recalls}`);
720
- lines.push(` total_acted_on: ${report.total_acted_on}`);
721
- lines.push(` impact_rate: ${(report.impact_rate * 100).toFixed(1)}%`);
722
- lines.push(` weighted_impact_score: ${report.weighted_impact_score}`);
723
- lines.push(` linked_acts: ${report.linked_acts} (acted_on events with explicit recall linkage)`);
724
- lines.push(` link_rate: ${(report.link_rate * 100).toFixed(1)}% (traceability: linked_acts / total_acted_on)`);
725
- lines.push(` unique_items_recalled: ${report.unique_items_recalled}`);
726
- lines.push(` unique_items_acted_on: ${report.unique_items_acted_on}`);
727
- lines.push('');
728
- lines.push(' by source:');
729
- const sources = Object.keys(report.by_source).sort();
730
- if (sources.length === 0) lines.push(' (none)');
731
- for (const src of sources) {
732
- const b = report.by_source[src];
733
- lines.push(` ${src.padEnd(14)} recalls=${String(b.recalls).padStart(4)} acted_on=${String(b.acted_on).padStart(4)} rate=${(b.impact_rate * 100).toFixed(1).padStart(5)}% linked=${b.linked_acts} weight=${b.weighted_score}`);
734
- }
735
- lines.push('');
736
- lines.push(' by action_type:');
737
- const types = Object.keys(report.by_action_type).sort();
738
- if (types.length === 0) lines.push(' (none)');
739
- for (const t of types) lines.push(` ${t.padEnd(22)} ${report.by_action_type[t]}`);
740
- console.log(lines.join('\n'));
741
- }
742
-
743
- function cmdPriority(args) {
744
- const rank = getPriorityRank({
745
- source: args['source-type'] || args.source,
746
- rune_type: args.type || args['rune-type'],
747
- confidence: args.confidence !== undefined ? parseFloat(args.confidence) : null,
748
- verified: args.verified || null,
749
- });
750
- console.log(JSON.stringify({ ok: true, ...(rank || { rank: null, tier: null, mode: null }) }, null, 2));
751
- }
752
-
753
- function cmdListRecalls(args) {
754
- const taskId = args.task;
755
- const phase = args.phase !== undefined ? parseInt(args.phase, 10) : undefined;
756
- if (!taskId) { console.error(JSON.stringify({ error: '--task is required' })); process.exit(1); }
757
- const recalls = listRecalls(taskId, phase);
758
- if (args.json) {
759
- console.log(JSON.stringify(recalls, null, 2));
760
- return;
761
- }
762
- if (recalls.length === 0) {
763
- console.log(`No recall events found for task=${taskId}${phase !== undefined ? `, phase=${phase}` : ''}`);
764
- return;
765
- }
766
- console.log(`Recall events for task=${taskId}${phase !== undefined ? `, phase=${phase}` : ''} (${recalls.length} total):`);
767
- for (const r of recalls) {
768
- const phaseStr = r.phase !== null ? ` phase=${r.phase}` : '';
769
- console.log(` ${r.recall_id} source=${r.source}:${r.source_id}${phaseStr}`);
770
- }
771
- }
772
-
773
- // ═══════════════════════════════════════
774
- // Module exports
775
- // ═══════════════════════════════════════
776
-
777
- module.exports = {
778
- recordRecall,
779
- recordActedOn,
780
- recordActedOnBatch,
781
- getMetrics,
782
- listRecalls,
783
- getPriorityRank,
784
- loadPriorityRules,
785
- // internal paths exported for tests
786
- _LOG_PATH: LOG_PATH,
787
- _PRIORITY_PATH: PRIORITY_PATH,
788
- _VALID_SOURCES: VALID_SOURCES,
789
- _VALID_ACTION_TYPES: VALID_ACTION_TYPES,
790
- _MIN_REASON_CHARS: MIN_REASON_CHARS,
791
- };
792
-
793
- // ═══════════════════════════════════════
794
- // CLI entry point
795
- // ═══════════════════════════════════════
796
-
797
- if (require.main === module) {
798
- const args = parseArgs(process.argv);
799
- const cmd = args._[0];
800
- switch (cmd) {
801
- case 'record-recall': cmdRecordRecall(args); break;
802
- case 'record-acted': cmdRecordActed(args); break;
803
- case 'report': cmdReport(args); break;
804
- case 'priority': cmdPriority(args); break;
805
- case 'list-recalls': cmdListRecalls(args); break;
806
- default:
807
- console.error([
808
- 'Usage: memory-action-log.js <command> [options]',
809
- '',
810
- 'Commands:',
811
- ' record-recall --source=<src> --source-id=<id> --task=TASK_XXX',
812
- ' [--phase=N] [--reason="text"]',
813
- ' [--rune-type=<type>] [--confidence=0.85] [--verified=<state>]',
814
- ' Returns: { ok, recall_id } — save recall_id for later linkage',
815
- '',
816
- ' record-acted --source=<src> --source-id=<id> --task=TASK_XXX',
817
- ' --action-type=<type> --reason="why this recall changed the plan (>=20 chars)"',
818
- ' [--phase=N] [--conflict-with=<other_source_id>]',
819
- ' [--recalled-from=recall_id1,recall_id2,...]',
820
- '',
821
- ' list-recalls --task=TASK_XXX [--phase=N] [--json]',
822
- ' List all recall events for a task (shows recall_ids for linkage)',
823
- '',
824
- ' report [--window-days=30] [--task=TASK_XXX] [--json]',
825
- '',
826
- ' priority --source-type=<src> [--type=<rune_type>] [--confidence=N] [--verified=<state>]',
827
- '',
828
- 'Sources:',
829
- ` ${[...VALID_SOURCES].join(', ')}`,
830
- '',
831
- 'Action types:',
832
- ` ${[...VALID_ACTION_TYPES].join(', ')}`,
833
- '',
834
- 'Contract: .kenaz/specialist/memory_source_priority.yaml',
835
- 'Log: .kenaz/specialist/memory_action_log.jsonl',
836
- ].join('\n'));
837
- process.exit(1);
838
- }
839
- }