kenaz 0.3.9 → 0.3.11

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 (117) hide show
  1. package/core/.claude/commands/Ansuz.md +12 -16
  2. package/core/.claude/hooks/memory-guard.js +13 -0
  3. package/core/.claude/skills/code_reviewer/SKILL.md +0 -1
  4. package/core/.claude/skills/code_reviewer/patterns.yaml +0 -8
  5. package/core/.claude/skills/dev_agent/SKILL.md +3 -6
  6. package/core/.claude/skills/heimdall/SKILL.md +5 -6
  7. package/core/.claude/skills/huginn/SKILL.md +11 -38
  8. package/core/.collaboration/core/task-api.js +2 -218
  9. package/core/.collaboration/core/task-api.test.js +1 -138
  10. package/core/.formation/bifrost/launch.js +2 -2
  11. package/core/.formation/bifrost/launch.test.js +2 -2
  12. package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
  13. package/core/.formation/bifrost/shared/codex-injection.js +3 -17
  14. package/core/.formation/bifrost/shared/consolidate.js +1 -51
  15. package/core/.formation/bifrost/sync.js +4 -5
  16. package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +1 -1
  17. package/core/.formation/tools/dispatch.js +2 -2
  18. package/core/.formation/tools/kenaz-stats.js +259 -0
  19. package/core/.formation/tools/kenaz-stats.test.js +180 -0
  20. package/core/.formation/tools/mcp-allowlist.test.js +5 -3
  21. package/core/.formation/tools/post-complete/bounded-http.js +1 -40
  22. package/core/.formation/tools/post-complete/bounded-http.test.js +2 -89
  23. package/core/.formation/tools/post-complete/cognitive.js +2 -119
  24. package/core/.formation/tools/post-complete/testing.js +3 -21
  25. package/core/.formation/tools/post-complete.js +2 -428
  26. package/core/.formation/tools/post-complete.test.js +0 -135
  27. package/core/.formation/tools/prompt.js +14 -0
  28. package/core/.formation/tools/route-audit.test.js +8 -20
  29. package/core/.rule/INDEX.yaml +3 -3
  30. package/core/.rule/general/ddx-synapse-stack.yaml +11 -11
  31. package/core/.rule/general/specialist-identity-contract.yaml +5 -5
  32. package/core/.rule/general/synapse-code-intelligence.yaml +25 -111
  33. package/core/.rule/general/worktree-isolation.yaml +2 -2
  34. package/core/.rule/pipeline/task-execution.yaml +6 -9
  35. package/core/.specialist/Amelia/README.md +5 -5
  36. package/core/.specialist/Amelia/capability.yaml +10 -18
  37. package/core/.specialist/Amelia/knowledge.yaml +15 -66
  38. package/core/.specialist/Amelia/workflow.yaml +12 -21
  39. package/core/.specialist/Ansuz/RULES.md +6 -0
  40. package/core/.specialist/Ansuz/capability.yaml +12 -87
  41. package/core/.specialist/Ansuz/concepts/autonomous_agenda.yaml +1 -1
  42. package/core/.specialist/Ansuz/concepts/autonomy_contract.yaml +2 -3
  43. package/core/.specialist/Ansuz/concepts/capability_pointers.yaml +1 -3
  44. package/core/.specialist/Ansuz/concepts/cognitive_fluency_contract.yaml +8 -21
  45. package/core/.specialist/Ansuz/concepts/cognitive_systems_overview.yaml +0 -4
  46. package/core/.specialist/Ansuz/concepts/cognitive_systems_turn_context.yaml +1 -1
  47. package/core/.specialist/Ansuz/concepts/completion_tracking.yaml +0 -1
  48. package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +0 -3
  49. package/core/.specialist/Ansuz/concepts/evolvable_guardrails.yaml +3 -3
  50. package/core/.specialist/Ansuz/concepts/fatigue_handoff.yaml +1 -7
  51. package/core/.specialist/Ansuz/concepts/guardrails_immutable.yaml +3 -4
  52. package/core/.specialist/Ansuz/concepts/identity.yaml +1 -2
  53. package/core/.specialist/Ansuz/concepts/phase9_verification.yaml +0 -1
  54. package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +3 -4
  55. package/core/.specialist/Ansuz/core.yaml +3 -6
  56. package/core/.specialist/Ansuz/elixir.yaml +6 -6
  57. package/core/.specialist/Ansuz/exo_cortex_tools.yaml +5 -9
  58. package/core/.specialist/Ansuz/guards/blind_spot_guards.yaml +2 -2
  59. package/core/.specialist/Ansuz/guards/lesson_guards.yaml +6 -19
  60. package/core/.specialist/Ansuz/identity_contract.yaml +4 -4
  61. package/core/.specialist/Ansuz/knowledge.yaml +13 -40
  62. package/core/.specialist/Ansuz/modules/ddx.yaml +2 -2
  63. package/core/.specialist/Ansuz/modules/prophecy.yaml +19 -19
  64. package/core/.specialist/Ansuz/modules/pulse_empty.yaml +6 -17
  65. package/core/.specialist/Ansuz/tools/ansuz-handoff-snapshot.js +3 -102
  66. package/core/.specialist/Ansuz/tools/ansuz-sdk-runner.js +2 -44
  67. package/core/.specialist/Ansuz/workflow.yaml +32 -76
  68. package/core/.specialist/Forseti/core.yaml +2 -7
  69. package/core/.specialist/Forseti/rule.yaml +1 -1
  70. package/core/.specialist/Heimdall/SKILL.md +2 -3
  71. package/core/.specialist/Heimdall/capability.yaml +22 -45
  72. package/core/.specialist/Heimdall/core.yaml +24 -126
  73. package/core/.specialist/Heimdall/principles.yaml +8 -11
  74. package/core/.specialist/Heimdall/rule.yaml +2 -2
  75. package/core/.specialist/Huginn/capability.yaml +31 -52
  76. package/core/.specialist/Huginn/core.yaml +43 -123
  77. package/core/.specialist/Huginn/principles.yaml +5 -5
  78. package/core/.specialist/Huginn/rule.yaml +3 -9
  79. package/core/.specialist/REFLECTION_TO_CONTROL.md +3 -4
  80. package/core/.specialist/shared/guard_schema.json +1 -1
  81. package/core/.specialist/shared/principles.yaml +6 -6
  82. package/core/.specialist/tools/__tests__/task-2409-lessons.test.js +95 -0
  83. package/core/.specialist/tools/ansuz-boot-fast.js +1 -0
  84. package/core/.specialist/tools/lessons.js +234 -0
  85. package/core/.specialist/tools/phase-gate.js +10 -230
  86. package/core/.specialist/tools/phase-gate.test.js +2 -2
  87. package/core/.specialist/tools/prompt-files.test.js +1 -1
  88. package/core/.specialist/tools/session.js +2 -89
  89. package/core/.system/memory/README.md +3 -0
  90. package/core/CLAUDE.md +6 -7
  91. package/core/KENAZ_CORE_VERSION +1 -1
  92. package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +26 -0
  93. package/core/plugins/kenaz/INVENTORY.md +1 -1
  94. package/core/plugins/kenaz/commands/Ansuz.md +12 -16
  95. package/core/plugins/kenaz/scripts/kenaz.test.js +2 -1
  96. package/core/plugins/kenaz/scripts/ygg.js +2 -0
  97. package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +0 -1
  98. package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +0 -8
  99. package/core/plugins/kenaz/skills/dev_agent/SKILL.md +3 -6
  100. package/lib/cli.js +13 -3
  101. package/package.json +6 -6
  102. package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
  103. package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
  104. package/core/.formation/tools/alaya-gate.js +0 -198
  105. package/core/.formation/tools/post-complete/health.js +0 -464
  106. package/core/.formation/tools/post-complete/task2038.test.js +0 -246
  107. package/core/.formation/tools/post-complete/task2341.test.js +0 -74
  108. package/core/.rule/general/cortex-tools.yaml +0 -181
  109. package/core/.rule/general/memory-language.yaml +0 -67
  110. package/core/.rule/general/memory-source-priority.yaml +0 -207
  111. package/core/.rule/general/synapse-cognitive-fusion.yaml +0 -301
  112. package/core/.specialist/Ansuz/concepts/cognitive_systems_hippocampus.yaml +0 -14
  113. package/core/.specialist/Ansuz/concepts/write_verification.yaml +0 -26
  114. package/core/.specialist/Ansuz/modules/recall.yaml +0 -45
  115. package/core/.specialist/tools/MEMORY_RECALL_TRIGGERS.md +0 -156
  116. package/core/.specialist/tools/context-injection.js +0 -395
  117. package/core/dev/scripts/probe/hippocampus-concurrency.js +0 -247
@@ -1,156 +0,0 @@
1
- # Memory Recall Trigger Map (TASK_1077)
2
-
3
- Canonical mapping of specialist decision points to required recall operations,
4
- source types, and conflict resolution. This document is the authoritative trigger
5
- contract. Telemetry instrumentation (`memory-action-log.js`, `acted_on` events)
6
- is built on top of this contract by TASK_1072 — do not mix the layers.
7
-
8
- **Machine-consumable counterpart:**
9
- `.kenaz/specialist/memory_source_priority.yaml`
10
-
11
- **Rule enforcement:**
12
- `.rule/general/memory-source-priority.yaml` (MSP-001 through MSP-005)
13
-
14
- ---
15
-
16
- ## Core Principle
17
-
18
- > Memory is only valuable when it changes a decision.
19
-
20
- Every recall call has a **decision point** on the other side. If you cannot
21
- name the decision the recall will inform, you are loading context for show.
22
- The trigger map below makes this concrete: for each phase or moment, it
23
- specifies _what_ to recall, _from which source_, and _what happens next_.
24
-
25
- ---
26
-
27
- ## Trigger Map
28
-
29
- | Phase / Moment | Tool | Sources Surfaced | Required vs Optional | Conflict Mode |
30
- |---|---|---|---|---|
31
- | **Phase 0 — Boot** | `cortex_recall(type=ash, limit=5)` | `summary_or_ash_rune` | Required | advisory |
32
- | **Phase 0 — Boot** | `synapse_sigils(projectPath)` | `sigil` | Required | advisory |
33
- | **Phase 0 — Boot** | `hippocampus_episode_recall(limit=3)` | `episode`, `session_handoff` | Conditional (prior session exists) | advisory |
34
- | **Phase 0 — Boot** | Read `ansuz_self_model.json` | `self_model_lesson` | Required | advisory |
35
- | **Phase 0 — Boot** | Auto-load handoff slot | `session_handoff` | Conditional (fatigue_warning in prior session) | advisory |
36
- | **Phase 1 — Requirements** | `synapse_recall(query=task_title+scope)` | `insight_or_pattern_rune`, `risk_or_fix_rune`, `episode` | Required | advisory |
37
- | **Before editing a symbol** | `synapse_recall(symbol_name)` + `kenaz_lsp_callers` | `risk_or_fix_rune`, `verified_decision_rune` | Required (non-trivial changes) | strict (risk/fix) |
38
- | **Phase 3 — Decomposition** | `kenaz_lsp_callers(scope symbols)` | `risk_or_fix_rune`, `sigil` | Required | strict (risk/fix) |
39
- | **Phase 8 — Dispatch** | `synapse_recall(dispatch patterns)` | `sigil`, `self_model_lesson`, `session_handoff` | Optional | advisory |
40
- | **Phase 9 — Verification** | `git diff --stat` + `synapse_recall(symbols)` | `risk_or_fix_rune`, `sigil` | Required | strict (risk) |
41
- | **Retry / Bail decision** | `synapse_recall(error signature)` | `risk_or_fix_rune`, `episode` | Required | strict (fix) |
42
- | **Phase-gate blocked exit** | `lesson_guards` auto-surfaced | `lesson_guard`, `session_handoff` | Auto (phase-gate handles) | advisory |
43
-
44
- ### Conditional triggers (detail)
45
-
46
- - **Handoff load (Phase 0):** Only if a per-session slot exists under
47
- `.kenaz/specialist/handoffs/` (newest `generated_at` wins — resolved via
48
- `handoff-resolver.js readLatestHandoff`; the legacy singular
49
- `ansuz_session_handoff.json` is retired read-only fallback, M3/TASK_1760)
50
- AND its `generated_at` timestamp is within the `ttl_hours: 72` window. Stale handoffs
51
- are ignored (they contain context from a cycle too far in the past).
52
-
53
- - **Episode recall (Phase 0):** Only when `hippocampus_episode_recall` returns ≥ 1 result.
54
- If the project has no episodes yet (cold start), skip silently — do not error.
55
-
56
- - **Phase 1 deep episode drill:** If Phase 0 episode summary mentions a `task_id` relevant
57
- to current requirements, call `hippocampus_episode_recall(task_id=that_id)` to get full entries.
58
-
59
- - **synapse_recall before edit:** May skip for trivial changes (config value, typo, < 5 lines).
60
- Judgment required. If skipping, note it explicitly (FAIL-009 records past recall-skips that
61
- caused regressions; pulse itself is retired, recall is the replacement).
62
-
63
- ---
64
-
65
- ## Decision-Point Checklist
66
-
67
- Before a recalled item can be treated as **acted-on** (influencing a decision),
68
- confirm all three:
69
-
70
- 1. **Name the specific recalled item.**
71
- `source_id` = rune_id, episode_id, sigil_id, guard id (L-XXX), blind-spot id (B-XXX),
72
- handoff `session_id`, or self_model `growth` entry hash.
73
-
74
- 2. **The recall changed what you were about to do.**
75
- State the "before" plan and the "after" plan in one sentence each. If the plan
76
- did not change, the recall is informational only — still worth noting, but not acted-on.
77
-
78
- 3. **The reason explains the delta, not the lookup.**
79
- "Recalled L-003" is not evidence.
80
- "L-003 flagged scope expansion requires a sub-task — created TASK_1077.1" is evidence.
81
-
82
- ---
83
-
84
- ## Conflict Resolution Contract
85
-
86
- When two sources disagree (e.g. an episode says "retry", a rune says "bail"):
87
-
88
- 1. Apply `priority_rank` from `memory_source_priority.yaml` — higher rank wins.
89
- 2. If same rank: apply `tie_breakers` in order (confidence DESC → freshness ASC → source_id ASC).
90
- 3. Apply the winner's `conflict_mode`:
91
- - **strict** → hide the loser from the agent; log with `conflict_with: <winner_source_id>`
92
- - **advisory** → show both; label the loser as "competing view (rank: N)"
93
- 4. If the agent disagrees with the winner's guidance, it must document the override reason
94
- before proceeding (no silent overrides).
95
-
96
- ### Conflict examples
97
-
98
- | Winner | Loser | Mode | What happens |
99
- |---|---|---|---|
100
- | `risk_or_fix_rune` (rank 60) | `episode` (rank 30) says "proceed" | strict | Agent follows rune; episode suppressed; override requires user statement |
101
- | `session_handoff` (rank 80) | `insight_or_pattern_rune` (rank 40) | advisory | Both surfaced; agent chooses with documented reason |
102
- | `verified_decision_rune` (rank 90) | `self_model_lesson` (rank 70) | strict | Agent follows decision rune; self-model view logged only |
103
-
104
- ---
105
-
106
- ## Anti-Patterns
107
-
108
- | Pattern | Why it fails |
109
- |---|---|
110
- | Recalling every source at every phase | Phase 0 boot cost grows unbounded; irrelevant context degrades signal |
111
- | Skipping recall at phase boundaries | Misses the decision point; FAIL-009 shows this causes regressions |
112
- | Treating lesson_guards as primary evidence | They are the *lowest* rank (10); trace back to the originating rune/episode |
113
- | Loading Phase 0 handoff even when > 72h old | Stale handoffs inject obsolete context; TTL exists for this reason |
114
- | "Recalling" from a source not actually queried | Incoherent audit trail; metrics show recall=0 / acted_on=1 |
115
-
116
- ---
117
-
118
- ## Minimal Integration Contract for New Specialists
119
-
120
- A specialist flow integrating with this system needs exactly two behaviors:
121
-
122
- 1. **At each trigger boundary** (see Trigger Map above), call the listed tool.
123
- Pass `projectPath` to every Synapse/Hippocampus call — omitting it causes
124
- multi-session collision on the global default project.
125
-
126
- 2. **When a recalled item changes a decision**, mark it as acted-on via the
127
- `memory_acted_on` field on phase-gate exit OR via `memory-action-log.js`
128
- `recordActedOn()` directly (TASK_1072 adds this layer).
129
-
130
- Both behaviors are independently useful: #1 is the contract defined here;
131
- #2 is observability added by TASK_1072. Do not depend on #2 being present.
132
-
133
- ---
134
-
135
- ## Rule References
136
-
137
- | Rule | File | Scope |
138
- |---|---|---|
139
- | MSP-001 through MSP-005 | `.rule/general/memory-source-priority.yaml` | Priority ordering, conflict modes, required triggers |
140
- | SYN-001 through SYN-007 | `.rule/general/synapse-code-intelligence.yaml` | Which Synapse tool at which moment |
141
- | SYN-008 through SYN-011 | `.rule/general/synapse-cognitive-fusion.yaml` | When to promote recalled patterns to runes/sigils |
142
- | MEM-LANG-001 | `.rule/general/memory-language.yaml` | All memory writes must be in English |
143
- | DDx stack | `.rule/general/ddx-synapse-stack.yaml` | Recall before writing code (pulse-or-recall gate) |
144
-
145
- ---
146
-
147
- ## File Locations
148
-
149
- | File | Purpose |
150
- |---|---|
151
- | `.kenaz/specialist/memory_source_priority.yaml` | Machine-consumable priority + conflict contract |
152
- | `.specialist/tools/MEMORY_RECALL_TRIGGERS.md` | This file — human-readable trigger map |
153
- | `.specialist/tools/memory-action-log.js` | Telemetry layer (TASK_1072) — `recordRecall` / `recordActedOn` |
154
- | `.specialist/Ansuz/modules/recall.yaml` | Ansuz-specific recall protocol (Tier 1/2/3 boot sequence) |
155
- | `.specialist/Ansuz/guards/lesson_guards.yaml` | L-XXX lesson guards surfaced by phase-gate |
156
- | `.kenaz/specialist/memory_action_log.jsonl` | Append-only event log (TASK_1072) |
@@ -1,395 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * context-injection.js
4
- * Phase-Aware Synapse 注入工具
5
- *
6
- * 根據當前 phase 輸出具體的 Synapse 查詢建議,把「執行 synapse_recall」
7
- * 從模糊提醒變成具體查詢目標。
8
- *
9
- * CLI:
10
- * node .specialist/tools/context-injection.js --phase=N --task=TASK_XXX [--project-path=PATH]
11
- *
12
- * Output: JSON to stdout
13
- * { phase, task_id, synapse_suggestions: { ... } }
14
- *
15
- * Exit codes:
16
- * 0 = success (including graceful degradation)
17
- * 1 = task not found or fatal error
18
- */
19
-
20
- 'use strict';
21
-
22
- const path = require('path');
23
- const fs = require('fs');
24
- const { execSync } = require('child_process');
25
-
26
- // ═══════════════════════════════════════
27
- // Path resolution
28
- // ═══════════════════════════════════════
29
-
30
- function findProjectRoot() {
31
- let dir = __dirname;
32
- while (dir !== path.dirname(dir)) {
33
- const parent = path.dirname(dir);
34
- if (fs.existsSync(path.join(parent, '.specialist'))) return parent;
35
- dir = parent;
36
- }
37
- return process.cwd();
38
- }
39
-
40
- const BASE_DIR = findProjectRoot();
41
- const TASK_API = path.join(BASE_DIR, '.collaboration', 'core', 'task-api.js');
42
- // TASK_2163: chart files are keyed by project identity + task id (sub-projects
43
- // reuse bare TASK_ ids and previously collided in the flat core sessions dir).
44
- const chartPathLib = require('./lib/chart-path');
45
-
46
- // ═══════════════════════════════════════
47
- // CLI arg parsing
48
- // ═══════════════════════════════════════
49
-
50
- function parseArgs(argv) {
51
- const args = {};
52
- for (const arg of argv.slice(2)) {
53
- const m = arg.match(/^--([^=]+)(?:=(.*))?$/);
54
- if (m) args[m[1]] = m[2] !== undefined ? m[2] : true;
55
- }
56
- return args;
57
- }
58
-
59
- // ═══════════════════════════════════════
60
- // Task loading via task-api.js
61
- // ═══════════════════════════════════════
62
-
63
- function loadTask(taskId, projectPath) {
64
- const ppFlag = projectPath ? ` --project-path="${projectPath}"` : '';
65
- const cmd = `node "${TASK_API}" get ${taskId}${ppFlag}`;
66
- try {
67
- const raw = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
68
- return JSON.parse(raw.trim());
69
- } catch (err) {
70
- // task-api exits non-zero when not found, or JSON parse fails
71
- return null;
72
- }
73
- }
74
-
75
- // ═══════════════════════════════════════
76
- // Chart loading
77
- // ═══════════════════════════════════════
78
-
79
- function loadChart(taskId, projectPath) {
80
- const chartPath = chartPathLib.getChartReadPath(taskId, projectPath ? { projectPath } : {});
81
- try {
82
- const raw = fs.readFileSync(chartPath, 'utf8');
83
- return JSON.parse(raw);
84
- } catch {
85
- return null;
86
- }
87
- }
88
-
89
- // ═══════════════════════════════════════
90
- // Symbol name inference
91
- // ═══════════════════════════════════════
92
-
93
- const GENERIC_FILENAMES = new Set(['mod', 'index', 'main', 'lib', 'utils', 'helpers', 'types', 'constants']);
94
-
95
- function inferSymbolName(scopePath) {
96
- // Normalize slashes
97
- const normalized = scopePath.replace(/\\/g, '/');
98
- const parts = normalized.split('/').filter(Boolean);
99
- if (parts.length === 0) return null;
100
-
101
- const lastPart = parts[parts.length - 1];
102
- const stem = lastPart.replace(/\.[^.]+$/, ''); // remove extension
103
-
104
- const nameSource = GENERIC_FILENAMES.has(stem) && parts.length >= 2
105
- ? parts[parts.length - 2].replace(/\.[^.]+$/, '')
106
- : stem;
107
-
108
- // underscore_case or kebab-case → PascalCase
109
- return nameSource
110
- .split(/[_\-]/)
111
- .filter(Boolean)
112
- .map(s => s.charAt(0).toUpperCase() + s.slice(1))
113
- .join('');
114
- }
115
-
116
- function inferSymbolsFromPaths(scopePaths) {
117
- if (!scopePaths || scopePaths.length === 0) return [];
118
- return scopePaths
119
- .map(inferSymbolName)
120
- .filter(Boolean)
121
- .filter((v, i, a) => a.indexOf(v) === i); // deduplicate
122
- }
123
-
124
- // ═══════════════════════════════════════
125
- // Research notes token extraction (Phase 3)
126
- // ═══════════════════════════════════════
127
-
128
- function extractFilePathTokens(text) {
129
- if (!text) return [];
130
- // Matches words containing / or . (file-path-like tokens)
131
- const matches = text.match(/\b[\w\-]+[./][\w\-./]+\b/g) || [];
132
- return matches
133
- .filter(t => !t.match(/^\d+\.\d+/)) // skip version numbers like 1.2.3
134
- .filter((v, i, a) => a.indexOf(v) === i);
135
- }
136
-
137
- function extractResearchNoteTokens(chart) {
138
- if (!chart) return [];
139
- const notes = chart?.history?.research_notes || '';
140
- const text = Array.isArray(notes) ? notes.join(' ') : String(notes);
141
- return extractFilePathTokens(text);
142
- }
143
-
144
- // ═══════════════════════════════════════
145
- // Phase handlers
146
- // ═══════════════════════════════════════
147
-
148
- function buildPhase1Suggestions(task) {
149
- const scopePaths = task.scope_paths || [];
150
- const tags = task.tags || [];
151
- const description = task.description || task.title || '';
152
-
153
- // Extract keywords: tags + meaningful words from description
154
- const descWords = description
155
- .toLowerCase()
156
- .replace(/[^\w\s\-_]/g, ' ')
157
- .split(/\s+/)
158
- .filter(w => w.length >= 4 && !STOPWORDS.has(w));
159
-
160
- const keywords = [...new Set([...tags, ...descWords])].slice(0, 10);
161
-
162
- const query = keywords.join(' ') || task.title || task.id;
163
-
164
- return {
165
- recall_query: query,
166
- recall_keywords: keywords,
167
- hint: scopePaths.length === 0
168
- ? 'No scope_paths defined — using description/tags as recall seeds'
169
- : `Recall before touching: ${scopePaths.join(', ')}`,
170
- };
171
- }
172
-
173
- function buildPhase2Suggestions(task) {
174
- const scopePaths = task.scope_paths || [];
175
- if (scopePaths.length === 0) {
176
- return {
177
- pulse_targets: [],
178
- hint: 'scope_paths is empty - Grep/Glob for the task title keywords, then synapse_recall on what you find',
179
- fallback_query: task.title || task.id,
180
- };
181
- }
182
- const pulseTargets = inferSymbolsFromPaths(scopePaths);
183
- return {
184
- pulse_targets: pulseTargets,
185
- scope_paths: scopePaths,
186
- hint: 'synapse_recall on scope_paths-inferred targets: '
187
- + (pulseTargets.length > 0 ? pulseTargets.join(', ') : 'Grep/Glob the scope_paths filenames first'),
188
- };
189
- }
190
-
191
- function buildPhase3Suggestions(task, chart) {
192
- const scopePaths = task.scope_paths || [];
193
- const fromPaths = inferSymbolsFromPaths(scopePaths);
194
- const fromNotes = extractResearchNoteTokens(chart);
195
-
196
- // Combine: path-derived symbols + file-path-like tokens from research notes
197
- const impactTargets = [...fromPaths, ...fromNotes]
198
- .filter((v, i, a) => a.indexOf(v) === i);
199
-
200
- return {
201
- impact_targets: impactTargets,
202
- scope_paths: scopePaths,
203
- research_note_tokens: fromNotes,
204
- hint: impactTargets.length > 0
205
- ? `Run kenaz_lsp_callers for each: ${impactTargets.join(', ')}`
206
- : 'No impact targets inferred - Grep the scope_paths filenames, then kenaz_lsp_callers on the symbols found',
207
- };
208
- }
209
-
210
- function buildPhase9Suggestions(task) {
211
- const scopePaths = task.scope_paths || [];
212
- return {
213
- detect_changes_scope: scopePaths,
214
- hint: scopePaths.length === 0
215
- ? 'scope_paths is empty - review the full diff with git diff --stat'
216
- : `Review the diff for: ${scopePaths.join(', ')} (git diff --stat -- <paths>), then synapse_recall on the touched symbols`,
217
- };
218
- }
219
-
220
- // ═══════════════════════════════════════
221
- // Recall telemetry (TASK_1078)
222
- // ═══════════════════════════════════════
223
-
224
- /**
225
- * Per-phase source types to emit recall telemetry for.
226
- * Derived from .kenaz/specialist/memory_source_priority.yaml phase_bindings.
227
- * Each entry: { source, rune_type?, label }
228
- */
229
- const PHASE_RECALL_SOURCES = {
230
- 1: [
231
- { source: 'rune', rune_type: 'insight', label: 'insight_or_pattern_rune' },
232
- { source: 'rune', rune_type: 'pattern', label: 'insight_or_pattern_rune' },
233
- { source: 'sigil', label: 'sigil' },
234
- ],
235
- 2: [
236
- { source: 'rune', rune_type: 'risk', label: 'risk_or_fix_rune' },
237
- { source: 'rune', rune_type: 'fix', label: 'risk_or_fix_rune' },
238
- { source: 'sigil', label: 'sigil' },
239
- ],
240
- 3: [
241
- { source: 'rune', rune_type: 'risk', label: 'risk_or_fix_rune' },
242
- { source: 'rune', rune_type: 'fix', label: 'risk_or_fix_rune' },
243
- { source: 'sigil', label: 'sigil' },
244
- { source: 'rune', rune_type: 'decision', label: 'verified_decision_rune' },
245
- ],
246
- 9: [
247
- { source: 'rune', rune_type: 'risk', label: 'risk_or_fix_rune' },
248
- { source: 'rune', rune_type: 'fix', label: 'risk_or_fix_rune' },
249
- { source: 'episode', label: 'episode' },
250
- ],
251
- };
252
-
253
- /**
254
- * Emit recall telemetry events for the memory sources required by this phase.
255
- * Uses a deterministic synthetic source_id so the same phase+task combination
256
- * always generates the same recall records (idempotent within a session slot).
257
- *
258
- * Returns:
259
- * { recalled: N, recall_ids: string[], sources: [{source, source_id, recall_id}] }
260
- * or { recalled: 0, recall_ids: [], sources: [], error?: string } on failure.
261
- */
262
- function emitRecallTelemetry(phase, taskId) {
263
- const telemetry = { recalled: 0, recall_ids: [], sources: [] };
264
- if (!taskId) return telemetry;
265
-
266
- const phaseSources = PHASE_RECALL_SOURCES[phase];
267
- if (!phaseSources || phaseSources.length === 0) return telemetry;
268
-
269
- let memLog;
270
- try {
271
- const memLogPath = path.join(BASE_DIR, '.specialist', 'tools', 'memory-action-log.js');
272
- if (!fs.existsSync(memLogPath)) return telemetry;
273
- memLog = require(memLogPath);
274
- } catch {
275
- return telemetry;
276
- }
277
-
278
- for (const src of phaseSources) {
279
- try {
280
- // Synthetic source_id: deterministic per phase+task+source type so
281
- // list-recalls can group these as "context-injection-suggested sources"
282
- const sourceId = `ctx-inject:phase${phase}:${src.label}`;
283
- const entry = {
284
- source: src.source,
285
- source_id: sourceId,
286
- task_id: taskId,
287
- phase,
288
- reason: `context-injection phase ${phase} loaded ${src.label} sources for task ${taskId}`,
289
- rune_type: src.rune_type || null,
290
- };
291
- const r = memLog.recordRecall(entry);
292
- if (r.ok) {
293
- telemetry.recalled += 1;
294
- telemetry.recall_ids.push(r.recall_id);
295
- telemetry.sources.push({
296
- source: src.source,
297
- source_id: sourceId,
298
- label: src.label,
299
- recall_id: r.recall_id,
300
- });
301
- }
302
- } catch { /* graceful degradation per source */ }
303
- }
304
-
305
- return telemetry;
306
- }
307
-
308
- // ═══════════════════════════════════════
309
- // Stopwords (for phase 1 keyword extraction)
310
- // ═══════════════════════════════════════
311
-
312
- const STOPWORDS = new Set([
313
- 'this', 'that', 'with', 'from', 'will', 'have', 'been', 'they',
314
- 'when', 'what', 'each', 'into', 'then', 'than', 'also', 'more',
315
- 'some', 'such', 'like', 'task', 'file', 'code', 'tool', 'node',
316
- 'spec', 'test', 'make', 'does', 'just', 'them', 'your', 'over',
317
- 'output', 'input', 'return', 'value', 'params', 'param', 'args',
318
- ]);
319
-
320
- // ═══════════════════════════════════════
321
- // Main
322
- // ═══════════════════════════════════════
323
-
324
- function main() {
325
- const args = parseArgs(process.argv);
326
-
327
- const phaseRaw = args['phase'];
328
- const taskId = args['task'];
329
- const projectPath = args['project-path'] || null;
330
-
331
- const phase = phaseRaw !== undefined ? parseInt(phaseRaw, 10) : NaN;
332
-
333
- if (!taskId) {
334
- console.error(JSON.stringify({ error: 'missing --task argument', phase: phase || null }));
335
- process.exit(1);
336
- }
337
-
338
- if (isNaN(phase)) {
339
- console.error(JSON.stringify({ error: 'missing or invalid --phase argument', task_id: taskId }));
340
- process.exit(1);
341
- }
342
-
343
- // Load task
344
- const task = loadTask(taskId, projectPath);
345
-
346
- if (!task) {
347
- process.stdout.write(JSON.stringify({ error: 'task not found', phase, task_id: taskId }) + '\n');
348
- process.exit(1);
349
- }
350
-
351
- // Load chart (optional)
352
- const chart = loadChart(taskId, projectPath);
353
-
354
- // Build suggestions by phase
355
- let synapse_suggestions = {};
356
-
357
- switch (phase) {
358
- case 1:
359
- synapse_suggestions = { phase_1: buildPhase1Suggestions(task) };
360
- break;
361
- case 2:
362
- synapse_suggestions = { phase_2: buildPhase2Suggestions(task) };
363
- break;
364
- case 3:
365
- synapse_suggestions = { phase_3: buildPhase3Suggestions(task, chart) };
366
- break;
367
- case 9:
368
- synapse_suggestions = { phase_9: buildPhase9Suggestions(task) };
369
- break;
370
- default:
371
- // Other phases: empty suggestions, exit 0
372
- break;
373
- }
374
-
375
- // Emit recall telemetry for memory sources required by this phase (TASK_1078).
376
- // Graceful — failure never blocks context-injection output.
377
- const recallTelemetry = emitRecallTelemetry(phase, task.id || taskId);
378
-
379
- const result = {
380
- phase,
381
- task_id: task.id || taskId,
382
- task_title: task.title,
383
- chart_loaded: chart !== null,
384
- synapse_suggestions,
385
- // recall_telemetry: stable recall_ids for this phase's required memory sources.
386
- // Pass any of these recall_ids to memory-action-log.js record-acted --recalled-from=...
387
- // to link a later decision back to the memory sources that shaped it.
388
- recall_telemetry: recallTelemetry,
389
- };
390
-
391
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
392
- process.exit(0);
393
- }
394
-
395
- main();