kenaz 0.3.10 → 0.3.12

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 (131) hide show
  1. package/core/.claude/skills/code_reviewer/SKILL.md +0 -1
  2. package/core/.claude/skills/code_reviewer/patterns.yaml +0 -8
  3. package/core/.claude/skills/dev_agent/SKILL.md +3 -6
  4. package/core/.claude/skills/heimdall/SKILL.md +5 -6
  5. package/core/.claude/skills/huginn/SKILL.md +11 -38
  6. package/core/.collaboration/core/task-2288-followups.test.js +2 -37
  7. package/core/.collaboration/core/task-2421-replica.test.js +229 -0
  8. package/core/.collaboration/core/task-api.js +223 -230
  9. package/core/.collaboration/core/task-api.test.js +1 -138
  10. package/core/.collaboration/core/task-replica.js +189 -0
  11. package/core/.formation/.merge-lock +1 -0
  12. package/core/.formation/bifrost/codex-doctor.js +12 -2
  13. package/core/.formation/bifrost/codex-doctor.test.js +15 -0
  14. package/core/.formation/bifrost/launch.js +2 -2
  15. package/core/.formation/bifrost/launch.test.js +2 -2
  16. package/core/.formation/bifrost/setup-codex.js +84 -16
  17. package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
  18. package/core/.formation/bifrost/shared/codex-injection.js +3 -17
  19. package/core/.formation/bifrost/shared/consolidate.js +1 -51
  20. package/core/.formation/bifrost/sync.js +4 -5
  21. package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +1 -1
  22. package/core/.formation/tools/dispatch.js +2 -2
  23. package/core/.formation/tools/kenaz-stats.js +1 -1
  24. package/core/.formation/tools/mcp-allowlist.test.js +5 -3
  25. package/core/.formation/tools/post-complete/bounded-http.js +1 -40
  26. package/core/.formation/tools/post-complete/bounded-http.test.js +2 -89
  27. package/core/.formation/tools/post-complete/cognitive.js +2 -119
  28. package/core/.formation/tools/post-complete/task2420.test.js +129 -0
  29. package/core/.formation/tools/post-complete/testing.js +3 -21
  30. package/core/.formation/tools/post-complete.js +131 -436
  31. package/core/.formation/tools/post-complete.test.js +0 -135
  32. package/core/.formation/tools/route-audit.test.js +8 -20
  33. package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
  34. package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
  35. package/core/.rule/INDEX.yaml +3 -3
  36. package/core/.rule/general/ddx-synapse-stack.yaml +11 -11
  37. package/core/.rule/general/specialist-identity-contract.yaml +5 -5
  38. package/core/.rule/general/synapse-code-intelligence.yaml +25 -111
  39. package/core/.rule/general/worktree-isolation.yaml +2 -2
  40. package/core/.rule/pipeline/task-execution.yaml +6 -9
  41. package/core/.specialist/Amelia/README.md +5 -5
  42. package/core/.specialist/Amelia/capability.yaml +10 -18
  43. package/core/.specialist/Amelia/knowledge.yaml +15 -66
  44. package/core/.specialist/Amelia/workflow.yaml +12 -21
  45. package/core/.specialist/Ansuz/capability.yaml +12 -87
  46. package/core/.specialist/Ansuz/concepts/autonomous_agenda.yaml +1 -1
  47. package/core/.specialist/Ansuz/concepts/autonomy_contract.yaml +2 -3
  48. package/core/.specialist/Ansuz/concepts/capability_pointers.yaml +1 -3
  49. package/core/.specialist/Ansuz/concepts/cognitive_fluency_contract.yaml +8 -21
  50. package/core/.specialist/Ansuz/concepts/cognitive_systems_overview.yaml +0 -4
  51. package/core/.specialist/Ansuz/concepts/cognitive_systems_turn_context.yaml +1 -1
  52. package/core/.specialist/Ansuz/concepts/completion_tracking.yaml +0 -1
  53. package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +0 -3
  54. package/core/.specialist/Ansuz/concepts/evolvable_guardrails.yaml +3 -3
  55. package/core/.specialist/Ansuz/concepts/fatigue_handoff.yaml +1 -7
  56. package/core/.specialist/Ansuz/concepts/guardrails_immutable.yaml +3 -4
  57. package/core/.specialist/Ansuz/concepts/identity.yaml +1 -2
  58. package/core/.specialist/Ansuz/concepts/phase9_verification.yaml +0 -1
  59. package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +3 -4
  60. package/core/.specialist/Ansuz/core.yaml +3 -6
  61. package/core/.specialist/Ansuz/elixir.yaml +6 -6
  62. package/core/.specialist/Ansuz/exo_cortex_tools.yaml +5 -9
  63. package/core/.specialist/Ansuz/guards/blind_spot_guards.yaml +2 -2
  64. package/core/.specialist/Ansuz/guards/lesson_guards.yaml +6 -19
  65. package/core/.specialist/Ansuz/identity_contract.yaml +4 -4
  66. package/core/.specialist/Ansuz/knowledge.yaml +13 -40
  67. package/core/.specialist/Ansuz/modules/ddx.yaml +2 -2
  68. package/core/.specialist/Ansuz/modules/prophecy.yaml +19 -19
  69. package/core/.specialist/Ansuz/modules/pulse_empty.yaml +6 -17
  70. package/core/.specialist/Ansuz/tools/ansuz-handoff-snapshot.js +3 -102
  71. package/core/.specialist/Ansuz/tools/ansuz-sdk-runner.js +2 -44
  72. package/core/.specialist/Ansuz/workflow.yaml +32 -76
  73. package/core/.specialist/Forseti/core.yaml +2 -7
  74. package/core/.specialist/Forseti/rule.yaml +1 -1
  75. package/core/.specialist/Heimdall/SKILL.md +2 -3
  76. package/core/.specialist/Heimdall/capability.yaml +22 -45
  77. package/core/.specialist/Heimdall/core.yaml +24 -126
  78. package/core/.specialist/Heimdall/principles.yaml +8 -11
  79. package/core/.specialist/Heimdall/rule.yaml +2 -2
  80. package/core/.specialist/Huginn/capability.yaml +31 -52
  81. package/core/.specialist/Huginn/core.yaml +43 -123
  82. package/core/.specialist/Huginn/principles.yaml +5 -5
  83. package/core/.specialist/Huginn/rule.yaml +3 -9
  84. package/core/.specialist/REFLECTION_TO_CONTROL.md +3 -4
  85. package/core/.specialist/shared/guard_schema.json +1 -1
  86. package/core/.specialist/shared/principles.yaml +6 -6
  87. package/core/.specialist/tools/__tests__/rea-detect.test.js +10 -0
  88. package/core/.specialist/tools/phase-gate.js +10 -230
  89. package/core/.specialist/tools/phase-gate.test.js +2 -2
  90. package/core/.specialist/tools/prompt-files.test.js +1 -1
  91. package/core/.specialist/tools/rea-detect.js +6 -2
  92. package/core/.specialist/tools/session.js +2 -89
  93. package/core/.synapse/memory-shared.json.sync +1 -0
  94. package/core/.system/memory/README.md +3 -0
  95. package/core/CLAUDE.md +6 -7
  96. package/core/KENAZ_CORE_VERSION +1 -1
  97. package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +75 -83
  98. package/core/dev/specs/peer/CODEX_MANAGED_DAEMON.md +54 -0
  99. package/core/dev/specs/peer/setup-codex-hooks.test.js +38 -0
  100. package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +56 -0
  101. package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
  102. package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
  103. package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +1 -0
  104. package/core/plugins/kenaz/INVENTORY.md +1 -1
  105. package/core/plugins/kenaz/scripts/kenaz.test.js +70 -1
  106. package/core/plugins/kenaz/scripts/ygg.js +45 -4
  107. package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +0 -1
  108. package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +0 -8
  109. package/core/plugins/kenaz/skills/dev_agent/SKILL.md +3 -6
  110. package/lib/cli.js +17 -9
  111. package/lib/cli.test.js +10 -0
  112. package/lib/dashboard.test.js +3 -3
  113. package/package.json +6 -6
  114. package/vendor/install-kenaz-cli.js +167 -16
  115. package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
  116. package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
  117. package/core/.collaboration/core/task-2264-single-after-capture.test.js +0 -61
  118. package/core/.formation/tools/alaya-gate.js +0 -198
  119. package/core/.formation/tools/post-complete/health.js +0 -464
  120. package/core/.formation/tools/post-complete/task2038.test.js +0 -246
  121. package/core/.formation/tools/post-complete/task2341.test.js +0 -74
  122. package/core/.rule/general/cortex-tools.yaml +0 -181
  123. package/core/.rule/general/memory-language.yaml +0 -67
  124. package/core/.rule/general/memory-source-priority.yaml +0 -207
  125. package/core/.rule/general/synapse-cognitive-fusion.yaml +0 -301
  126. package/core/.specialist/Ansuz/concepts/cognitive_systems_hippocampus.yaml +0 -14
  127. package/core/.specialist/Ansuz/concepts/write_verification.yaml +0 -26
  128. package/core/.specialist/Ansuz/modules/recall.yaml +0 -45
  129. package/core/.specialist/tools/MEMORY_RECALL_TRIGGERS.md +0 -156
  130. package/core/.specialist/tools/context-injection.js +0 -395
  131. package/core/dev/scripts/probe/hippocampus-concurrency.js +0 -247
@@ -1,84 +0,0 @@
1
- # synapse_maintain
2
-
3
- Unified Synapse maintenance checkpoint for agent-facing health reviews.
4
-
5
- ## when_to_call
6
-
7
- - Phase 2 research: run before planning when the task depends on current codebase health, stale cognition, or cleanup priority.
8
- - Cognitive checkpoint: run after a risky implementation burst to see whether new gaps, conflicts, or stale runes appeared.
9
- - Session end: run before handoff so the next agent receives a single dashboard instead of seven separate tool calls.
10
-
11
- ## tool
12
-
13
- Call `synapse_maintain` with:
14
-
15
- ```json
16
- {
17
- "projectPath": "<worktree-or-project-path>",
18
- "top_n": 5
19
- }
20
- ```
21
-
22
- ## sections
23
-
24
- ### orphans
25
-
26
- - Meaning: code symbols that look unreachable and may be removable.
27
- - Read it as: high count + high removal impact means cleanup can simplify the graph quickly.
28
- - Follow-up: inspect candidates before deletion; route handlers and command entry points can still be legitimate.
29
- - Rune guidance: inscribe `fix` after a confirmed deletion or `risk` when a false-positive pattern is discovered.
30
-
31
- ### stale_runes
32
-
33
- - Meaning: runes whose attached symbols drifted heavily or disappeared from the graph.
34
- - Read it as: `auto_rejected` is urgent because memory was already invalidated; `needs_review` means knowledge may be stale.
35
- - Follow-up: refresh or supersede outdated runes before using them in planning.
36
- - Rune guidance: inscribe `decision` when you intentionally keep a flagged rune valid, or `fix` when you replace stale guidance.
37
-
38
- ### conflicts
39
-
40
- - Meaning: contradictory runes on the same symbol or within the same sigil.
41
- - Read it as: any `high` conflict blocks blind trust in recall results.
42
- - Follow-up: verify, reject, or supersede conflicting runes before further automation.
43
- - Rune guidance: inscribe `decision` with the resolved interpretation after adjudicating the conflict.
44
-
45
- ### gaps
46
-
47
- - Meaning: heavily used symbols with no cognitive memory.
48
- - Read it as: these are high-leverage documentation targets, especially when danger score is high.
49
- - Follow-up: inspect the top hotspots and inscribe only non-obvious behavior, risks, or design choices.
50
- - Rune guidance: inscribe `insight`, `risk`, or `summary` depending on the symbol and what was learned.
51
-
52
- ### pain_paths
53
-
54
- - Meaning: long call chains crossing too many community boundaries.
55
- - Read it as: repeated community crossings usually signal architecture drag rather than a single bug.
56
- - Follow-up: use the chain preview to decide where a facade, shortcut, or boundary cleanup would reduce coupling.
57
- - Rune guidance: inscribe `risk` when a path is fragile, or `decision` when you adopt a refactoring boundary.
58
-
59
- ### overdue_episodes
60
-
61
- - Meaning: closed episodes with high consolidation scores that still need memory compression or review.
62
- - Read it as: these sessions contain learnings that are likely to be lost unless consolidated.
63
- - Follow-up: consolidate, summarize key facts, or extract repeatable lessons.
64
- - Rune guidance: inscribe `ash` only after a stable cross-session lesson is clear.
65
-
66
- ### audit_summary
67
-
68
- - Meaning: anomaly rule findings from the structural audit engine.
69
- - Read it as: `high` findings are active breakage or architectural risk; `medium` usually deserves planned follow-up.
70
- - Follow-up: treat the file paths as the first investigation slice, not as automatic proof.
71
- - Rune guidance: inscribe `fix` after resolving a confirmed audit issue, or `risk` if a known anomaly must remain temporarily.
72
-
73
- ## interpretation
74
-
75
- - `severity=high` means act before more feature work if the section affects active scope.
76
- - `severity=medium` means plan remediation or inscribe context before handing off.
77
- - `severity=low` means no urgent action, but keep the diagnosis in the task summary if it influenced decisions.
78
- - If a section is `degraded`, treat the diagnosis as tool coverage failure, not proof that the codebase is healthy.
79
-
80
- ## output_discipline
81
-
82
- - Do not summarize the dashboard as "all green" unless every section is both `status=ok` and `severity=low`.
83
- - If `items` is empty but `status=degraded`, report the error explicitly; degraded sections are not passes.
84
- - Prefer one follow-up rune with high confidence over bulk inscription from raw dashboard findings.
@@ -1,61 +0,0 @@
1
- // TASK_2264 — completeTask must schedule exactly ONE 'after' health capture.
2
- // moveTask used to schedule a detached 'after' capture on every ->done move even when
3
- // completeTask's own postComplete was about to measure the same snapshot.
4
- // Counting seam: child_process.spawn is patched (task-api requires it at call time) and
5
- // calls whose inline script carries phase "after" are counted. Isolated temp project.
6
- // Usage: node .collaboration/core/task-2264-single-after-capture.test.js
7
- const fs = require('fs');
8
- const path = require('path');
9
- const os = require('os');
10
- const cp = require('child_process');
11
- const { TaskAPI, toYAML } = require('./task-api.js');
12
-
13
- const SB = fs.mkdtempSync(path.join(os.tmpdir(), 't2264_'));
14
- const tasksDir = path.join(SB, '.kenaz', 'tasks');
15
- for (const d of ['backlog', 'active', 'done', 'blocked', 'cancel', 'archived']) fs.mkdirSync(path.join(tasksDir, d), { recursive: true });
16
- delete process.env.FORMATION_SCHEDULER; delete process.env.FORMATION_HEADLESS;
17
-
18
- let pass = 0, fail = 0;
19
- const check = (n, c, d) => { console.log((c ? 'PASS' : 'FAIL') + ' | ' + n + (c ? '' : ' | ' + d)); c ? pass++ : fail++; };
20
-
21
- let phases = [];
22
- const realSpawn = cp.spawn;
23
- cp.spawn = function (cmd, argv, opts) {
24
- const script = Array.isArray(argv) ? argv.find(a => typeof a === 'string' && a.includes('const CFG')) : null;
25
- if (script) {
26
- const m = script.match(/"phase":"(before|after)"/);
27
- phases.push(m ? m[1] : '?');
28
- return { unref() {}, on() {} };
29
- }
30
- return realSpawn.apply(this, arguments);
31
- };
32
-
33
- const mkActive = (id) => fs.writeFileSync(path.join(tasksDir, 'active', id + '_t.yaml'),
34
- toYAML({ id, title: 't2264 ' + id, priority: 'P2', status: 'in_progress' }));
35
-
36
- (async () => {
37
- const api = new TaskAPI({ tasksDir, projectPath: SB });
38
- try {
39
- // A: complete with post-complete running in-process (dry run, no git/archive) -> post-complete owns 'after'
40
- mkActive('TASK_9001'); phases = [];
41
- await api.completeTask('TASK_9001', { dryRun: true, skipGit: true, skipArchive: true });
42
- check('A complete w/ post-complete: moveTask schedules NO after capture', phases.filter(p => p === 'after').length === 0, JSON.stringify(phases));
43
-
44
- // B control: post-complete skipped -> nobody else measures, moveTask must still capture exactly once
45
- mkActive('TASK_9002'); phases = [];
46
- await api.completeTask('TASK_9002', { skipPostComplete: true });
47
- check('B control: complete w/ skipPostComplete schedules exactly 1 after capture', phases.filter(p => p === 'after').length === 1, JSON.stringify(phases));
48
-
49
- // C control: bare move done (no post-complete) keeps its single capture
50
- mkActive('TASK_9003'); phases = [];
51
- await api.moveTask('TASK_9003', 'done');
52
- check('C control: bare move done schedules exactly 1 after capture', phases.filter(p => p === 'after').length === 1, JSON.stringify(phases));
53
- } catch (e) {
54
- check('no exception', false, e && e.stack);
55
- } finally {
56
- cp.spawn = realSpawn;
57
- try { fs.rmSync(SB, { recursive: true, force: true }); } catch { /* best effort */ }
58
- }
59
- console.log(`\n${pass} passed, ${fail} failed`);
60
- process.exit(fail ? 1 : 0);
61
- })();
@@ -1,198 +0,0 @@
1
- // TASK_2104: shared, dependency-free Alaya relevance gate — extracted out
2
- // of prompt.js so task-api.js's echoAlayaLessons (TASK_1992) applies the
3
- // same TASK_2101 cosine-floor + stack-match gate instead of a hand-copied
4
- // near-duplicate that had neither. Zero deps (no memory-loader/core-resolve)
5
- // so it still require()s from a sub-project with no .formation/ checkout —
6
- // callers must treat a require() failure as "gate unavailable" and degrade
7
- // gracefully (see task-api.js echoAlayaLessons).
8
- // Shared by: prompt.js appendAlayaLessons(), task-api.js echoAlayaLessons().
9
-
10
- 'use strict';
11
-
12
- /**
13
- * Build the cross-project context query shared by Cortex and Alaya lookups:
14
- * scope_paths basenames (extension stripped) + up to 4 non-stop-word
15
- * keywords from the task title.
16
- *
17
- * @param {string|object} taskData - task JSON string or parsed object
18
- * @returns {string|null} the query string, or null when nothing to query
19
- */
20
- function buildTaskContextQuery(taskData) {
21
- let scopePaths = [];
22
- let taskTitle = '';
23
- try {
24
- const parsed = typeof taskData === 'string' ? JSON.parse(taskData) : taskData;
25
- if (parsed) {
26
- scopePaths = parsed.scope_paths || [];
27
- taskTitle = parsed.title || '';
28
- }
29
- } catch { /* taskData not JSON, skip */ }
30
-
31
- if (scopePaths.length === 0 && !taskTitle) return null;
32
-
33
- // Build query from scope_paths file names + task title keywords
34
- const queryParts = [];
35
- for (const sp of scopePaths) {
36
- const parts = String(sp).replace(/\\/g, '/').split('/');
37
- const last = parts[parts.length - 1].replace(/\.[^.]+$/, '');
38
- if (last && last.length > 2) queryParts.push(last);
39
- }
40
- if (taskTitle) {
41
- const stopWords = new Set(['the','a','an','is','are','to','of','and','in','for','on','at','by','with','fix','task','add','update']);
42
- const words = taskTitle.replace(/[^a-zA-Z0-9_\s]/g, ' ').split(/\s+/).filter(w => w.length > 2 && !stopWords.has(w.toLowerCase()));
43
- queryParts.push(...words.slice(0, 4));
44
- }
45
- if (queryParts.length === 0) return null;
46
-
47
- return [...new Set(queryParts)].join(' ');
48
- }
49
-
50
- // TASK_2101: absolute relevance gate for Alaya lesson injection.
51
- //
52
- // Why: alaya_recall's only pre-2101 score is RRF rank fusion (~0.016-0.032),
53
- // which carries no absolute relevance — lexical overlap on a single common
54
- // word ("checkpoint", "gate") was enough to rank a bija first regardless of
55
- // domain. Every bija also had source_count=1 (hub distiller not implemented),
56
- // so nothing self-corrects via aggregation either. The fix is two gates that
57
- // must BOTH pass before a bija is even a candidate for injection:
58
- // 1. cosine floor — the RAW semantic cosine (never the RRF score) must
59
- // clear ALAYA_MIN_COSINE_DEFAULT. A bija the semantic arm never scored
60
- // (cosine=null: fts-only hit, or bija has no embedding yet) fails the
61
- // floor by construction — an unverifiable relevance is not a floor pass.
62
- // 2. stack match — the bija's coarse tech stack (source_symbol's file
63
- // extension/dir, plus tags) must overlap the task's (scope_paths'
64
- // extensions/dirs), UNLESS the cosine is very high
65
- // (>= ALAYA_STACK_OVERRIDE_COSINE) — a strong enough semantic match can
66
- // override a stack that a coarse heuristic failed to resolve.
67
- //
68
- // Calibration (R4, measured 2026-09-28 against the live backend, 5 real
69
- // dispatch queries x top-10 semantic hits = 50 bijas, hand-labelled):
70
- // - ArcticEmbedXs cosines are compressed high: clearly irrelevant noise
71
- // (payment/CRM/UI-colour/backtest seeds on Rust and JS infra cards)
72
- // scored 0.656-0.697; on-topic bijas scored 0.709-0.810. 0.45 (the
73
- // earlier placeholder) passed all 50, i.e. it was not a filter at all.
74
- // - Confusion at 0.45: 20 on-topic kept, 30 noise kept. At 0.70: 20 kept,
75
- // 0 noise. The margin is only ~0.012, so treat 0.70 as fitted to a small
76
- // sample and re-measure when the bija population changes.
77
- // - The stack half of the gate currently has no real signal: every live
78
- // bija has source_symbol='' (hub predates the pass-through) and `tags`
79
- // are content word-bags, not stack labels. In practice a bija on a task
80
- // with scope_paths passes only at >= ALAYA_STACK_OVERRIDE_COSINE (0.8),
81
- // so today's effective floor for scoped tasks is 0.8 and 0.70 applies to
82
- // unscoped tasks. A3 check (dispatch --dry-run on TASK_2096/2097/2100):
83
- // 10/10 candidates dropped on each; no noise injected.
84
- // - Also seen: the on-topic hits were near-duplicates (8-9 copies of one
85
- // "community detection void" / "KuzuDB graph broken" observation) and
86
- // months stale (the conditions they describe are fixed). Relevance is
87
- // not the only gap: dedup (distiller) and retraction are next.
88
- const ALAYA_MIN_COSINE_DEFAULT = 0.70;
89
- const ALAYA_STACK_OVERRIDE_COSINE = 0.8;
90
- // Candidate pool requested from alaya_recall — wider than the render cap (3)
91
- // so post-filtering by the two gates still has real candidates to keep.
92
- const ALAYA_CANDIDATE_POOL = 10;
93
-
94
- /**
95
- * Coarse stack tokens (lowercase file extensions + 1-2 top path segments)
96
- * from a list of paths. Shared by the task side (scope_paths) and the bija
97
- * side (source_symbol's `{file_path}:{symbol}:{kind}` shape, split on ':').
98
- *
99
- * @param {string} normPath - a single '/'-normalised path
100
- * @param {Set<string>} tokens - mutated in place
101
- */
102
- function addStackTokens(normPath, tokens) {
103
- if (!normPath) return;
104
- const extMatch = normPath.match(/\.([a-zA-Z0-9]+)$/);
105
- if (extMatch) tokens.add(extMatch[1].toLowerCase());
106
- const parts = normPath.split('/').filter(Boolean);
107
- if (parts.length > 0) tokens.add(parts[0].toLowerCase());
108
- if (parts.length > 1) tokens.add(`${parts[0]}/${parts[1]}`.toLowerCase());
109
- }
110
-
111
- /**
112
- * Derive the task's coarse stack from its scope_paths (file extensions +
113
- * top-level dirs, e.g. {'rs', 'js', 'dashboard', '.formation/tools'}).
114
- * @param {string[]} scopePaths
115
- * @returns {Set<string>}
116
- */
117
- function deriveTaskStack(scopePaths) {
118
- const tokens = new Set();
119
- for (const sp of scopePaths || []) {
120
- addStackTokens(String(sp).replace(/\\/g, '/'), tokens);
121
- }
122
- return tokens;
123
- }
124
-
125
- /**
126
- * Derive a bija's coarse stack from its tags (comma-separated string, as
127
- * returned by alaya_recall) and its source_symbol (TASK_2101 — hub
128
- * pass-through of the originating rune's symbol_stable_id; empty for hubs
129
- * that predate the field, or project-level seeds with no single symbol).
130
- * @param {object} bija
131
- * @returns {Set<string>}
132
- */
133
- function deriveBijaStack(bija) {
134
- const tokens = new Set();
135
- for (const t of String(bija.tags || '').split(',')) {
136
- const tag = t.trim().toLowerCase();
137
- if (tag) tokens.add(tag);
138
- }
139
- const sourceSymbol = String(bija.source_symbol || '');
140
- if (sourceSymbol) {
141
- const filePath = sourceSymbol.split(':')[0].replace(/\\/g, '/');
142
- addStackTokens(filePath, tokens);
143
- }
144
- return tokens;
145
- }
146
-
147
- /**
148
- * Whether a bija passes the TASK_2101 relevance gate: cosine floor AND
149
- * (stack overlap OR a cosine high enough to override a stack miss).
150
- * A task with no derivable stack (empty scope_paths) cannot be judged on
151
- * stack at all, so the stack half of the gate is skipped — the floor alone
152
- * still applies.
153
- *
154
- * @param {object} bija - one alaya_recall result (cosine, tags, source_symbol, ...)
155
- * @param {Set<string>} taskStack - from deriveTaskStack
156
- * @param {number} floor - ALAYA_MIN_COSINE_DEFAULT (or an override)
157
- * @returns {boolean}
158
- */
159
- function passesAlayaRelevanceGate(bija, taskStack, floor) {
160
- const cosine = typeof bija.cosine === 'number' ? bija.cosine : null;
161
- if (cosine === null || cosine < floor) return false;
162
- if (taskStack.size === 0) return true; // no task-side stack signal to judge against
163
- if (cosine >= ALAYA_STACK_OVERRIDE_COSINE) return true;
164
- const bijaStack = deriveBijaStack(bija);
165
- for (const tok of bijaStack) {
166
- if (taskStack.has(tok)) return true;
167
- }
168
- return false;
169
- }
170
-
171
- /**
172
- * Dedup a list of alaya_recall bijas: key = id, else first 100 chars of
173
- * content. Shared by prompt.js's appendAlayaLessons and task-api.js's
174
- * echoAlayaLessons — both queried the same hub, so the same duplicate hubs
175
- * (near-identical seeds re-inscribed by different sources) can show up.
176
- *
177
- * @param {object[]} bijas
178
- * @returns {object[]}
179
- */
180
- function dedupBijas(bijas) {
181
- const seen = new Set();
182
- return (bijas || []).filter(b => {
183
- const key = b.id || (b.content || '').slice(0, 100);
184
- if (!key || seen.has(key)) return false;
185
- seen.add(key); return true;
186
- });
187
- }
188
-
189
- module.exports = {
190
- buildTaskContextQuery,
191
- deriveTaskStack,
192
- deriveBijaStack,
193
- passesAlayaRelevanceGate,
194
- dedupBijas,
195
- ALAYA_MIN_COSINE_DEFAULT,
196
- ALAYA_STACK_OVERRIDE_COSINE,
197
- ALAYA_CANDIDATE_POOL,
198
- };