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.
- package/core/.claude/commands/agent-worker.md +1 -1
- package/core/.claude/commands/architect.md +1 -2
- package/core/.claude/commands/spec.md +1 -1
- package/core/.claude/commands//347/224/242/345/223/201.md +1 -1
- package/core/.claude/hooks/__tests__/task-1977-dedupe-guard.test.js +8 -8
- package/core/.claude/hooks/synapse-first-investigation.js +15 -0
- package/core/.claude/skills/architect_agent/SKILL.md +11 -92
- package/core/.claude/skills/assessment_agent/SKILL.md +0 -1
- package/core/.claude/skills/backend_engineer/SKILL.md +3 -6
- package/core/.claude/skills/code_reviewer/SKILL.md +1 -1
- package/core/.claude/skills/code_reviewer/patterns.yaml +1 -1
- package/core/.claude/skills/designer/SKILL.md +3 -6
- package/core/.claude/skills/dev_agent/SKILL.md +30 -491
- package/core/.claude/skills/developer/SKILL.md +3 -6
- package/core/.claude/skills/feature_agent/SKILL.md +3 -17
- package/core/.claude/skills/product_manager/SKILL.md +29 -1064
- package/core/.claude/skills/qa_engineer/SKILL.md +12 -17
- package/core/.claude/skills/spec_optimizer/SKILL.md +25 -275
- package/core/.claude/skills/sre_agent/SKILL.md +3 -6
- package/core/.collaboration/README.md +2 -3
- package/core/.collaboration/core/task-api.js +5 -138
- package/core/.collaboration/docs/CLI.md +0 -2
- package/core/.failure/INDEX.yaml +6 -40
- package/core/.failure/agent-execution/FAIL-013-search-cwd-drift-and-case-mismatch.yaml +1 -1
- package/core/.failure/tools/README.md +9 -227
- package/core/.formation/bifrost/exec.js +29 -4
- package/core/.formation/bifrost/launch.js +1 -6
- package/core/.formation/bifrost/launch.test.js +1 -1
- package/core/.formation/bifrost/provider-launch.js +18 -11
- package/core/.formation/bifrost/provider-launch.test.js +26 -2
- package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
- package/core/.formation/bifrost/shared/ansuz-boot-policy.test.js +1 -2
- package/core/.formation/tools/__tests__/dispatch-cwd-project.test.js +42 -0
- package/core/.formation/tools/__tests__/m1-hmap-convid.test.js +1 -124
- package/core/.formation/tools/__tests__/subagent-executor.test.js +3 -2
- package/core/.formation/tools/dispatch.js +15 -2
- package/core/.formation/tools/loader.js +17 -140
- package/core/.formation/tools/post-complete.js +0 -31
- package/core/.rule/INDEX.yaml +0 -18
- package/core/.rule/general/agent-workflow.yaml +14 -164
- package/core/.rule/general/project-self-containment.yaml +8 -19
- package/core/.rule/general/rust-verify-target.yaml +6 -5
- package/core/.rule/general/synapse-code-intelligence.yaml +3 -3
- package/core/.specialist/Amelia/capability.yaml +12 -12
- package/core/.specialist/Amelia/workflow.yaml +2 -2
- package/core/.specialist/Ansuz/SYSTEM_DOC.md +20 -578
- package/core/.specialist/Ansuz/cognitive_principles.yaml +2 -3
- package/core/.specialist/Ansuz/concepts/boot_rules_summary.yaml +0 -1
- package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +1 -2
- package/core/.specialist/Ansuz/core.yaml +6 -108
- package/core/.specialist/Ansuz/guards/lesson_guards.yaml +0 -3
- package/core/.specialist/Ansuz/identity_contract.yaml +0 -1
- package/core/.specialist/Ansuz/knowledge.yaml +0 -4
- package/core/.specialist/Ansuz/rule.yaml +0 -1
- package/core/.specialist/Ansuz/tools/handoff-session-identity.test.js +6 -150
- package/core/.specialist/tests/runtime-discovery/README.md +1 -1
- package/core/.specialist/tools/__tests__/task-2374-usage-counter.test.js +1 -12
- package/core/.specialist/tools/prompt-files.test.js +1 -0
- package/core/.specialist/tools/task-bridge.js +1 -66
- package/core/.system/README.md +11 -552
- package/core/.system/collaboration/README.md +10 -174
- package/core/CLAUDE.md +1 -6
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/scripts/js-yaml-resolution.test.js +1 -1
- package/core/dev/scripts/run-rust-tests.js +30 -46
- package/core/dev/scripts/run-rust-tests.test.js +17 -0
- package/core/dev/scripts/rust-verify-worktree.js +4 -4
- package/core/dev/scripts/verify-target-dir.js +6 -6
- package/core/plugins/kenaz/INVENTORY.md +8 -8
- package/core/plugins/kenaz/commands/architect.md +1 -2
- package/core/plugins/kenaz/commands/spec.md +1 -1
- package/core/plugins/kenaz/commands//347/224/242/345/223/201.md +1 -1
- package/core/plugins/kenaz/guidance.md +1 -1
- package/core/plugins/kenaz/scripts/sync.js +1 -1
- package/core/plugins/kenaz/scripts/ygg.js +6 -0
- package/core/plugins/kenaz/skills/architect_agent/SKILL.md +11 -92
- package/core/plugins/kenaz/skills/assessment_agent/SKILL.md +0 -1
- package/core/plugins/kenaz/skills/backend_engineer/SKILL.md +3 -6
- package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +1 -1
- package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +1 -1
- package/core/plugins/kenaz/skills/designer/SKILL.md +3 -6
- package/core/plugins/kenaz/skills/dev_agent/SKILL.md +30 -491
- package/core/plugins/kenaz/skills/developer/SKILL.md +3 -6
- package/core/plugins/kenaz/skills/feature_agent/SKILL.md +3 -17
- package/core/plugins/kenaz/skills/product_manager/SKILL.md +29 -1064
- package/core/plugins/kenaz/skills/qa_engineer/SKILL.md +12 -17
- package/core/plugins/kenaz/skills/spec_optimizer/SKILL.md +25 -275
- package/core/plugins/kenaz/skills/sre_agent/SKILL.md +3 -6
- package/package.json +6 -6
- package/core/.claude/commands/archive.md +0 -24
- package/core/.claude/commands/init.md +0 -112
- package/core/.claude/commands/legacy_dev.md +0 -18
- package/core/.claude/commands/product-review.md +0 -131
- package/core/.claude/commands/strategy.md +0 -141
- package/core/.claude/commands/sunny.md +0 -1
- package/core/.claude/commands//347/224/242/345/223/201/346/252/242/350/246/226.md +0 -129
- package/core/.claude/commands//351/200/262/345/272/246.md +0 -15
- package/core/.claude/hooks/__tests__/substrate.test.js +0 -122
- package/core/.claude/hooks/_substrate-paths.js +0 -95
- package/core/.claude/hooks/context-check.js +0 -51
- package/core/.claude/hooks/fatigue-check.js +0 -52
- package/core/.claude/hooks/handoff-snapshot-on-commit.js +0 -154
- package/core/.claude/hooks/handoff-snapshot-on-question.js +0 -158
- package/core/.claude/hooks/handoff-staleness.js +0 -197
- package/core/.claude/hooks/jev-thalamus-context.js +0 -17
- package/core/.claude/hooks/jev-thalamus-context.test.js +0 -41
- package/core/.claude/hooks/lease-release.js +0 -119
- package/core/.claude/hooks/lease-touch.js +0 -96
- package/core/.claude/hooks/reflex-feedback.js +0 -5
- package/core/.claude/hooks/session-init.ps1 +0 -49
- package/core/.claude/hooks/sound-event.js +0 -70
- package/core/.claude/hooks/sound-event.sh +0 -37
- package/core/.claude/hooks/stream-delta.js +0 -169
- package/core/.claude/no-hooks.settings.json +0 -3
- package/core/.claude/skills/legacy_dev/SKILL.md +0 -202
- package/core/.claude/skills/sunny/SKILL.md +0 -74
- package/core/.collaboration/cli/commands/assign.js +0 -39
- package/core/.collaboration/cli/commands/claim.js +0 -33
- package/core/.collaboration/cli/commands/complete.js +0 -44
- package/core/.collaboration/cli/commands/create.js +0 -110
- package/core/.collaboration/cli/commands/get.js +0 -83
- package/core/.collaboration/cli/commands/index.js +0 -52
- package/core/.collaboration/cli/commands/list.js +0 -51
- package/core/.collaboration/cli/commands/lock.js +0 -45
- package/core/.collaboration/cli/commands/rebuild.js +0 -39
- package/core/.collaboration/cli/commands/search.js +0 -61
- package/core/.collaboration/cli/commands/stats.js +0 -61
- package/core/.collaboration/cli/commands/transfer.js +0 -41
- package/core/.collaboration/cli/commands/unlock.js +0 -48
- package/core/.collaboration/cli/formatters/help-formatter.js +0 -70
- package/core/.collaboration/cli/formatters/index.js +0 -7
- package/core/.collaboration/cli/task-cli.js +0 -46
- package/core/.collaboration/cli/utils/arg-parser.js +0 -74
- package/core/.collaboration/cli/utils/git-user.js +0 -25
- package/core/.collaboration/cli/utils/index.js +0 -9
- package/core/.collaboration/cli/utils/output.js +0 -10
- package/core/.collaboration/config/default.yaml +0 -189
- package/core/.collaboration/core/conflict-resolver.js +0 -461
- package/core/.collaboration/core/conflict-resolver.test.js +0 -389
- package/core/.collaboration/core/migrate-legacy.js +0 -260
- package/core/.collaboration/docs/API.md +0 -787
- package/core/.collaboration/docs/ARCHITECTURE.md +0 -596
- package/core/.collaboration/docs/FAQ.md +0 -695
- package/core/.collaboration/docs/MIGRATION.md +0 -804
- package/core/.collaboration/docs/examples/README.md +0 -320
- package/core/.collaboration/docs/examples/advanced-queries.js +0 -261
- package/core/.collaboration/docs/examples/basic-usage.js +0 -184
- package/core/.collaboration/docs/examples/team-workflow.js +0 -352
- package/core/.failure/agent-execution/FAIL-001-agent-no-output.yaml +0 -391
- package/core/.failure/agent-execution/FAIL-007-pm-lead-wrong-team-invocation.yaml +0 -0
- package/core/.failure/agent-execution/FAIL-010-phase0-poem-format-skipped.yaml +0 -28
- package/core/.failure/agent-execution/FAIL-010-skipping-cognitive-checkpoint-after-verification.yaml +0 -59
- package/core/.failure/agent-execution/FAIL-012-grep-shortcut-during-investigation.yaml +0 -85
- package/core/.failure/architecture/FAIL-006-python-version-compatibility.yaml +0 -445
- package/core/.failure/pipeline/FAIL-003-queue-pollution.yaml +0 -335
- package/core/.failure/pipeline/README.md +0 -95
- package/core/.failure/retired/FAIL-009-skipping-synapse-pulse-before-code-change.yaml +0 -49
- package/core/.failure/tools/USAGE_GUIDE.md +0 -323
- package/core/.failure/tools/check_failures.ps1 +0 -387
- package/core/.failure/tools/log_failure.ps1 +0 -474
- package/core/.formation/.episode-input.json +0 -10
- package/core/.formation/.merge-lock +0 -1
- package/core/.formation/lead.yaml +0 -118
- package/core/.formation/legacy.bak/designer.yaml +0 -57
- package/core/.formation/legacy.bak/pm.yaml +0 -57
- package/core/.formation/legacy.bak/qa.yaml +0 -57
- package/core/.formation/legacy.bak/review.yaml +0 -62
- package/core/.formation/legacy.bak/scheduler.yaml +0 -129
- package/core/.formation/legacy.bak/sre.yaml +0 -57
- package/core/.formation/role-map.yaml +0 -274
- package/core/.formation/roles/_shared/modules/code_review.yaml +0 -98
- package/core/.formation/roles/_shared/modules/security.yaml +0 -103
- package/core/.formation/roles/_shared/modules/testing.yaml +0 -14
- package/core/.formation/roles/backend/role.yaml +0 -93
- package/core/.formation/roles/code_reviewer/role.yaml +0 -102
- package/core/.formation/roles/database_optimizer/role.yaml +0 -107
- package/core/.formation/roles/designer/role.yaml +0 -66
- package/core/.formation/roles/developer/role.yaml +0 -75
- package/core/.formation/roles/frontend/role.yaml +0 -134
- package/core/.formation/roles/incident_response/role.yaml +0 -89
- package/core/.formation/roles/qa/role.yaml +0 -89
- package/core/.formation/roles/security/role.yaml +0 -87
- package/core/.formation/roles/sre/role.yaml +0 -66
- package/core/.formation/skills/coordinator.yaml +0 -52
- package/core/.formation/solo/diagnosis.yaml +0 -78
- package/core/.formation/solo/strategy.yaml +0 -121
- package/core/.formation/tests/hello.txt +0 -2
- package/core/.formation/tools/coordinator.js +0 -350
- package/core/.formation/tools/jev-correction.js +0 -214
- package/core/.formation/tools/jev-correction.test.js +0 -178
- package/core/.formation/tools/log-reader.js +0 -232
- package/core/.formation/tools/revert-daemon-state.js +0 -162
- package/core/.formation/tools/test-drift.js +0 -148
- package/core/.formation/unit/heimdall.yaml +0 -96
- package/core/.formation/unit/plan.yaml +0 -67
- package/core/.formation/unit/think.yaml +0 -58
- package/core/.kenaz/MUNINN_SNAPSHOT9_EPISODE.json +0 -32
- package/core/.kenaz/MUNINN_SNAPSHOT9_RUNE.json +0 -23
- package/core/.kenaz/MUNINN_SNAPSHOT9_SUMMARY.md +0 -89
- package/core/.kenaz/ash_TASK_1428-20260507.json +0 -14
- package/core/.kenaz/muninn_episode_write.ps1 +0 -17
- package/core/.kenaz/muninn_snapshot11_rune_candidates.md +0 -63
- package/core/.kenaz/muninn_snapshot1_rune_candidates.md +0 -49
- package/core/.kenaz/runes_TASK_1428.json +0 -57
- package/core/.kenaz/tmp_heimdall_proposal_8d3e392.json +0 -13
- package/core/.kenaz/tmp_props.json +0 -1
- package/core/.kenaz/tmp_sounddrop_t1_prompt.txt +0 -371
- package/core/.rule/MECHANIZED.md +0 -61
- package/core/.rule/general/cortex-memory-recall.yaml +0 -446
- package/core/.rule/general/pipeline.yaml +0 -246
- package/core/.rule/general/sunny-access-control.yaml +0 -60
- package/core/.rule/general/system-invariants.yaml +0 -231
- package/core/.rule/pipeline/PPL-002-completed-task-archival.yaml +0 -528
- package/core/.rule/pipeline/auto-update.yaml +0 -282
- package/core/.rule/pipeline/task-execution.yaml +0 -365
- package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +0 -65
- package/core/.specialist/Ansuz/experiments/boot_ablation_replay.js +0 -197
- package/core/.specialist/Ansuz/tools/bootstrap-anchors.js +0 -289
- package/core/.specialist/Ansuz/workflow.yaml +0 -1225
- package/core/.specialist/experiments/rg_benchmark.js +0 -523
- package/core/.specialist/experiments/rg_benchmark_results.json +0 -27
- package/core/.specialist/tools/jev-optional-context.js +0 -107
- package/core/.specialist/tools/jev-optional-context.test.js +0 -152
- package/core/.specialist/tools/memory-action-log.js +0 -839
- package/core/.specialist/tools/phase-gate.js +0 -3534
- package/core/.specialist/tools/phase-gate.test.js +0 -85
- package/core/.specialist/tools/precompute-runner.js +0 -444
- package/core/.specialist/tools/rule-engine.js +0 -621
- package/core/.specialist/tools/task-bridge-jev.test.js +0 -170
- package/core/.synapse/memory-shared.json.sync +0 -1
- package/core/.system/agent-skills/README.md +0 -724
- package/core/.system/cli-tools/README.md +0 -724
- package/core/.system/cortex/MODULES.md +0 -1046
- package/core/.system/dashboard/FRONTEND.md +0 -812
- package/core/.system/dashboard/README.md +0 -737
- package/core/.system/failure/README.md +0 -723
- package/core/.system/formation/LEGACY_SKILLS.md +0 -974
- package/core/.system/formation/POST_COMPLETE.md +0 -602
- package/core/.system/formation/README.md +0 -1094
- package/core/.system/formation/TOOLS_REFERENCE.md +0 -1145
- package/core/.system/kenaz/README.md +0 -787
- package/core/.system/memory/README.md +0 -719
- package/core/.system/pipeline/README.md +0 -712
- package/core/.system/rule/README.md +0 -794
- package/core/.system/sunny/README.md +0 -476
- package/core/.system/sunny/RULES.json +0 -89
- package/core/.system/sunny/knowledge/INDEX.md +0 -37
- package/core/.system/sunny/knowledge/agent-teams-api.md +0 -0
- package/core/.system/sunny/knowledge/ansuz-specialist.md +0 -500
- package/core/.system/sunny/knowledge/claude-cli-spawn.md +0 -177
- package/core/.system/sunny/knowledge/collaboration-api.md +0 -925
- package/core/.system/sunny/teammates/README.md +0 -107
- package/core/.system/sunny/teammates/how_to_use.yaml +0 -101
- package/core/.system/sunny/teammates/tools/headless/launcher.js +0 -318
- package/core/.system/sunny/teammates/tools/powershell/launcher.js +0 -241
- package/core/.system/sunny/teammates/tools/powershell/start.bat +0 -50
- package/core/.system/sunny/teammates/tools/utils/env.js +0 -96
- package/core/.system/sunny/teammates/tools/utils/format-output.js +0 -329
- package/core/.system/sunny/tools.json +0 -101
- package/core/dev/scripts/cleanup-queue.js +0 -184
- package/core/dev/scripts/cleanup_queue.js +0 -86
- package/core/dev/scripts/legacy/README.md +0 -39
- package/core/dev/scripts/legacy/dev_agent_scheduler.ps1.bak +0 -536
- package/core/dev/scripts/legacy/multi_agent_scheduler.ps1.bak +0 -1978
- package/core/dev/scripts/matrix/CLI_README.md +0 -214
- package/core/dev/scripts/matrix/README.md +0 -310
- package/core/dev/scripts/matrix/package-lock.json +0 -4266
- package/core/dev/scripts/matrix/package.json +0 -55
- package/core/dev/scripts/matrix/src/abbreviations.ts +0 -295
- package/core/dev/scripts/matrix/src/cli/commands/convert.ts +0 -211
- package/core/dev/scripts/matrix/src/cli/commands/create.ts +0 -95
- package/core/dev/scripts/matrix/src/cli/commands/delete.ts +0 -102
- package/core/dev/scripts/matrix/src/cli/commands/list-tags.ts.wip +0 -140
- package/core/dev/scripts/matrix/src/cli/commands/manage-tags.ts.wip +0 -173
- package/core/dev/scripts/matrix/src/cli/commands/query.ts +0 -261
- package/core/dev/scripts/matrix/src/cli/commands/read.ts +0 -77
- package/core/dev/scripts/matrix/src/cli/commands/stats.ts.wip +0 -168
- package/core/dev/scripts/matrix/src/cli/commands/tokens.ts.wip +0 -126
- package/core/dev/scripts/matrix/src/cli/commands/update.ts +0 -147
- package/core/dev/scripts/matrix/src/cli/commands/validate.ts +0 -275
- package/core/dev/scripts/matrix/src/cli/index.ts +0 -350
- package/core/dev/scripts/matrix/src/cli/utils/file-handler.ts +0 -72
- package/core/dev/scripts/matrix/src/cli/utils/logger.ts +0 -65
- package/core/dev/scripts/matrix/src/index.ts +0 -62
- package/core/dev/scripts/matrix/src/indexer/tag-indexer.ts +0 -165
- package/core/dev/scripts/matrix/src/parser.ts +0 -281
- package/core/dev/scripts/matrix/src/serializer.ts +0 -151
- package/core/dev/scripts/matrix/src/validator.ts +0 -301
- package/core/dev/scripts/matrix/tests/abbreviations.test.ts +0 -122
- package/core/dev/scripts/matrix/tests/cli.test.ts +0 -177
- package/core/dev/scripts/matrix/tests/convert.test.ts +0 -143
- package/core/dev/scripts/matrix/tests/parser.test.ts +0 -203
- package/core/dev/scripts/matrix/tests/query.test.ts +0 -85
- package/core/dev/scripts/matrix/tests/serializer.test.ts +0 -197
- package/core/dev/scripts/matrix/tests/validate.test.ts +0 -141
- package/core/dev/scripts/matrix/tests/validator.test.ts +0 -167
- package/core/dev/scripts/matrix/tsconfig.json +0 -20
- package/core/dev/scripts/matrix/types/index.d.ts +0 -111
- package/core/dev/scripts/matrix/types/index.d.ts.map +0 -1
- package/core/dev/scripts/matrix/types/index.js +0 -8
- package/core/dev/scripts/matrix/types/index.js.map +0 -1
- package/core/dev/scripts/matrix/types/index.ts +0 -124
- package/core/dev/scripts/memory-usage-report.js +0 -152
- package/core/dev/scripts/probe/ansuz-scaffold-ablation.js +0 -579
- package/core/dev/scripts/probe/ansuz-scaffold-ablation.test.js +0 -244
- package/core/dev/scripts/probe/hook-latency-probe.js +0 -315
- package/core/dev/scripts/probe/hook-latency-probe.test.js +0 -105
- package/core/dev/scripts/probe/prediction-signal-auc.js +0 -483
- package/core/dev/scripts/probe/prediction-signal-auc.test.js +0 -200
- package/core/dev/scripts/test-pty-ws-tunnel.js +0 -229
- package/core/dev/scripts/validate_pipeline.js +0 -197
- package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +0 -56
- package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +0 -1
- 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
|
-
}
|