kenaz 0.3.10 → 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.
- package/core/.claude/skills/code_reviewer/SKILL.md +0 -1
- package/core/.claude/skills/code_reviewer/patterns.yaml +0 -8
- package/core/.claude/skills/dev_agent/SKILL.md +3 -6
- package/core/.claude/skills/heimdall/SKILL.md +5 -6
- package/core/.claude/skills/huginn/SKILL.md +11 -38
- package/core/.collaboration/core/task-api.js +2 -218
- package/core/.collaboration/core/task-api.test.js +1 -138
- package/core/.formation/bifrost/launch.js +2 -2
- package/core/.formation/bifrost/launch.test.js +2 -2
- package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
- package/core/.formation/bifrost/shared/codex-injection.js +3 -17
- package/core/.formation/bifrost/shared/consolidate.js +1 -51
- package/core/.formation/bifrost/sync.js +4 -5
- package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +1 -1
- package/core/.formation/tools/dispatch.js +2 -2
- package/core/.formation/tools/kenaz-stats.js +1 -1
- package/core/.formation/tools/mcp-allowlist.test.js +5 -3
- package/core/.formation/tools/post-complete/bounded-http.js +1 -40
- package/core/.formation/tools/post-complete/bounded-http.test.js +2 -89
- package/core/.formation/tools/post-complete/cognitive.js +2 -119
- package/core/.formation/tools/post-complete/testing.js +3 -21
- package/core/.formation/tools/post-complete.js +2 -428
- package/core/.formation/tools/post-complete.test.js +0 -135
- package/core/.formation/tools/route-audit.test.js +8 -20
- package/core/.rule/INDEX.yaml +3 -3
- package/core/.rule/general/ddx-synapse-stack.yaml +11 -11
- package/core/.rule/general/specialist-identity-contract.yaml +5 -5
- package/core/.rule/general/synapse-code-intelligence.yaml +25 -111
- package/core/.rule/general/worktree-isolation.yaml +2 -2
- package/core/.rule/pipeline/task-execution.yaml +6 -9
- package/core/.specialist/Amelia/README.md +5 -5
- package/core/.specialist/Amelia/capability.yaml +10 -18
- package/core/.specialist/Amelia/knowledge.yaml +15 -66
- package/core/.specialist/Amelia/workflow.yaml +12 -21
- package/core/.specialist/Ansuz/capability.yaml +12 -87
- package/core/.specialist/Ansuz/concepts/autonomous_agenda.yaml +1 -1
- package/core/.specialist/Ansuz/concepts/autonomy_contract.yaml +2 -3
- package/core/.specialist/Ansuz/concepts/capability_pointers.yaml +1 -3
- package/core/.specialist/Ansuz/concepts/cognitive_fluency_contract.yaml +8 -21
- package/core/.specialist/Ansuz/concepts/cognitive_systems_overview.yaml +0 -4
- package/core/.specialist/Ansuz/concepts/cognitive_systems_turn_context.yaml +1 -1
- package/core/.specialist/Ansuz/concepts/completion_tracking.yaml +0 -1
- package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +0 -3
- package/core/.specialist/Ansuz/concepts/evolvable_guardrails.yaml +3 -3
- package/core/.specialist/Ansuz/concepts/fatigue_handoff.yaml +1 -7
- package/core/.specialist/Ansuz/concepts/guardrails_immutable.yaml +3 -4
- package/core/.specialist/Ansuz/concepts/identity.yaml +1 -2
- package/core/.specialist/Ansuz/concepts/phase9_verification.yaml +0 -1
- package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +3 -4
- package/core/.specialist/Ansuz/core.yaml +3 -6
- package/core/.specialist/Ansuz/elixir.yaml +6 -6
- package/core/.specialist/Ansuz/exo_cortex_tools.yaml +5 -9
- package/core/.specialist/Ansuz/guards/blind_spot_guards.yaml +2 -2
- package/core/.specialist/Ansuz/guards/lesson_guards.yaml +6 -19
- package/core/.specialist/Ansuz/identity_contract.yaml +4 -4
- package/core/.specialist/Ansuz/knowledge.yaml +13 -40
- package/core/.specialist/Ansuz/modules/ddx.yaml +2 -2
- package/core/.specialist/Ansuz/modules/prophecy.yaml +19 -19
- package/core/.specialist/Ansuz/modules/pulse_empty.yaml +6 -17
- package/core/.specialist/Ansuz/tools/ansuz-handoff-snapshot.js +3 -102
- package/core/.specialist/Ansuz/tools/ansuz-sdk-runner.js +2 -44
- package/core/.specialist/Ansuz/workflow.yaml +32 -76
- package/core/.specialist/Forseti/core.yaml +2 -7
- package/core/.specialist/Forseti/rule.yaml +1 -1
- package/core/.specialist/Heimdall/SKILL.md +2 -3
- package/core/.specialist/Heimdall/capability.yaml +22 -45
- package/core/.specialist/Heimdall/core.yaml +24 -126
- package/core/.specialist/Heimdall/principles.yaml +8 -11
- package/core/.specialist/Heimdall/rule.yaml +2 -2
- package/core/.specialist/Huginn/capability.yaml +31 -52
- package/core/.specialist/Huginn/core.yaml +43 -123
- package/core/.specialist/Huginn/principles.yaml +5 -5
- package/core/.specialist/Huginn/rule.yaml +3 -9
- package/core/.specialist/REFLECTION_TO_CONTROL.md +3 -4
- package/core/.specialist/shared/guard_schema.json +1 -1
- package/core/.specialist/shared/principles.yaml +6 -6
- package/core/.specialist/tools/phase-gate.js +10 -230
- package/core/.specialist/tools/phase-gate.test.js +2 -2
- package/core/.specialist/tools/prompt-files.test.js +1 -1
- package/core/.specialist/tools/session.js +2 -89
- package/core/.system/memory/README.md +3 -0
- package/core/CLAUDE.md +6 -7
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +26 -0
- package/core/plugins/kenaz/INVENTORY.md +1 -1
- package/core/plugins/kenaz/scripts/kenaz.test.js +2 -1
- package/core/plugins/kenaz/scripts/ygg.js +2 -0
- package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +0 -1
- package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +0 -8
- package/core/plugins/kenaz/skills/dev_agent/SKILL.md +3 -6
- package/lib/cli.js +0 -2
- package/package.json +6 -6
- package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
- package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
- package/core/.formation/tools/alaya-gate.js +0 -198
- package/core/.formation/tools/post-complete/health.js +0 -464
- package/core/.formation/tools/post-complete/task2038.test.js +0 -246
- package/core/.formation/tools/post-complete/task2341.test.js +0 -74
- package/core/.rule/general/cortex-tools.yaml +0 -181
- package/core/.rule/general/memory-language.yaml +0 -67
- package/core/.rule/general/memory-source-priority.yaml +0 -207
- package/core/.rule/general/synapse-cognitive-fusion.yaml +0 -301
- package/core/.specialist/Ansuz/concepts/cognitive_systems_hippocampus.yaml +0 -14
- package/core/.specialist/Ansuz/concepts/write_verification.yaml +0 -26
- package/core/.specialist/Ansuz/modules/recall.yaml +0 -45
- package/core/.specialist/tools/MEMORY_RECALL_TRIGGERS.md +0 -156
- package/core/.specialist/tools/context-injection.js +0 -395
- package/core/dev/scripts/probe/hippocampus-concurrency.js +0 -247
|
@@ -232,7 +232,6 @@ description: 程式碼審查、三級問題分類(Blocker/Suggestion/Nit)、
|
|
|
232
232
|
|
|
233
233
|
| Mutation | Counterpart | 缺失嚴重度 |
|
|
234
234
|
|----------|-------------|-----------|
|
|
235
|
-
| `hippocampus_episode_start` | `hippocampus_episode_close` | leak(episode 永不歸檔) |
|
|
236
235
|
| `task-api.js claim` | `task-api.js complete` 或 `move blocked` | orphan(task 卡在 active/) |
|
|
237
236
|
| `addEventListener` | `removeEventListener` | leak(listener 記憶體洩漏) |
|
|
238
237
|
| `setInterval` | `clearInterval` | leak(定時器持續觸發) |
|
|
@@ -17,14 +17,6 @@ mutation_counterpart:
|
|
|
17
17
|
# orphan = logical state / lifecycle not terminated
|
|
18
18
|
|
|
19
19
|
pairs:
|
|
20
|
-
- pair: hippocampus_episode
|
|
21
|
-
mutation: 'hippocampus_episode_start'
|
|
22
|
-
counterpart: 'hippocampus_episode_close'
|
|
23
|
-
severity: leak
|
|
24
|
-
rationale: 'episode never closed -> consolidation never fires, unarchived memory accumulates'
|
|
25
|
-
grep_mutation: 'hippocampus_episode_start'
|
|
26
|
-
grep_counterpart: 'hippocampus_episode_close'
|
|
27
|
-
|
|
28
20
|
- pair: task_lifecycle
|
|
29
21
|
mutation: 'task-api.js claim'
|
|
30
22
|
counterpart: 'task-api.js complete | task-api.js move'
|
|
@@ -256,8 +256,6 @@ description: 開發協調、Pipeline管理、任務執行、進度追蹤、Agent
|
|
|
256
256
|
| `kenaz_lsp_references` | 所有引用 / 修改影響範圍 | 修改前評估 blast radius |
|
|
257
257
|
| `kenaz_lsp_definition` | 定義位置 | 跳到宣告處 |
|
|
258
258
|
| `kenaz_lsp_symbols` | 檔案內 symbol 列表 | 了解檔案結構 |
|
|
259
|
-
| `synapse_recall` | 歷史 rune / 已知風險 | 修改前召回 |
|
|
260
|
-
| `synapse_status` | 記憶系統狀態 | 任務開始前檢查 |
|
|
261
259
|
|
|
262
260
|
文字與概念搜尋用 Grep / Glob。
|
|
263
261
|
|
|
@@ -281,8 +279,7 @@ description: 開發協調、Pipeline管理、任務執行、進度追蹤、Agent
|
|
|
281
279
|
**在開始任何開發任務前,建議執行:**
|
|
282
280
|
|
|
283
281
|
1. **獲取專案 Context**:
|
|
284
|
-
-
|
|
285
|
-
- 使用 `synapse_recall` 召回相關已知風險與決策
|
|
282
|
+
- 讀取任務卡與 `kenaz learn list` 的相關教訓
|
|
286
283
|
|
|
287
284
|
2. **搜索相關代碼**:
|
|
288
285
|
- 使用 Grep / Glob 找到相關實現
|
|
@@ -335,10 +332,10 @@ description: 開發協調、Pipeline管理、任務執行、進度追蹤、Agent
|
|
|
335
332
|
2. 重新讀取 Skill 文件
|
|
336
333
|
3. 如果問題持續,直接應用該 Agent 的核心職責邏輯
|
|
337
334
|
|
|
338
|
-
### LSP
|
|
335
|
+
### LSP 查詢失敗時
|
|
339
336
|
1. 繼續執行任務(降級為 Grep/Glob)
|
|
340
337
|
2. 確認 Dashboard 已啟動(依賴 Rust HTTP server on port 18080)
|
|
341
|
-
3. 使用 `
|
|
338
|
+
3. 使用 `kenaz_ping` 檢查服務狀態
|
|
342
339
|
|
|
343
340
|
---
|
|
344
341
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Heimdall
|
|
3
|
-
description: 代碼疆域守望者 — git 驅動的代碼健康審查(kenaz_lsp_callers
|
|
3
|
+
description: 代碼疆域守望者 — git 驅動的代碼健康審查(kenaz_lsp_callers),生成 OptimizationProposal;記憶健康僅記錄趨勢,不做 KEEP/REVERT
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Heimdall ᚺ — 守望者
|
|
@@ -39,17 +39,16 @@ trigger=manual target_symbol=<SYMBOL> project_path=<PATH>
|
|
|
39
39
|
|
|
40
40
|
### Attention Trigger
|
|
41
41
|
|
|
42
|
-
`trigger=attention`(舊的注意力聚合工具已移除):改以 git 近期變更檔 +
|
|
42
|
+
`trigger=attention`(舊的注意力聚合工具已移除):改以 git 近期變更檔 + 作為調查起點。
|
|
43
43
|
|
|
44
44
|
1. **取得焦點**:`git log --since=7.days --name-only` 統計最常被修改的檔案(top 15)→ `kenaz_lsp_symbols(file)` 取得其中的 symbols
|
|
45
45
|
2. **深度調查每個 symbol**:
|
|
46
46
|
- `kenaz_lsp_references(symbol, depth=2)` → 爆炸半徑
|
|
47
|
-
- `synapse_recall(query=symbol)` → 已知風險/歷史 rune
|
|
48
47
|
- `kenaz_lsp_callers(symbol)` → 呼叫者
|
|
49
|
-
3. **交叉比對**:合成 blast_radius +
|
|
50
|
-
4. **嚴重性分級**:references >= 20
|
|
48
|
+
3. **交叉比對**:合成 blast_radius + callers → 產生 finding
|
|
49
|
+
4. **嚴重性分級**:references >= 20 → HIGH,>= 8 → MEDIUM,else → LOW
|
|
51
50
|
5. **空結果**:若無近期變更焦點 → 輸出 `ᚺ Heimdall: clean scan. No attention signals.` 並結束
|
|
52
|
-
6. **降級**:LSP 不可用 → 降級到 `scheduled_trigger` 流程(Grep/Glob
|
|
51
|
+
6. **降級**:LSP 不可用 → 降級到 `scheduled_trigger` 流程(Grep/Glob)
|
|
53
52
|
|
|
54
53
|
---
|
|
55
54
|
|
|
@@ -25,58 +25,40 @@ Converting an advisory into a task is **Ansuz exclusive decision**, and Ansuz MU
|
|
|
25
25
|
|
|
26
26
|
1. Parse context block: `trigger`, `project_path`, `focus` (optional), `query` (optional)
|
|
27
27
|
2. Confirm `project_path` is set -- abort if missing
|
|
28
|
-
3.
|
|
29
|
-
`Huginn: Synapse unavailable. Cannot think without perception. Abort.` and exit
|
|
28
|
+
3. (memory probe removed in TASK_2411 -- Huginn perceives code, tasks, Heimdall and wishes only)
|
|
30
29
|
4. **Read loop mode** -- read `{project_path}/.kenaz/loop/loop.json` (fallback: `.kenaz/loop.json`, then `.collaboration/loop.json`).
|
|
31
|
-
Canonical writer: `readLoopConfig` in `.formation/tools/post-complete/health.js:28`.
|
|
32
30
|
If no file found, default to `{ mode: "manual" }`.
|
|
33
31
|
Store as `loop_mode` ("manual" | "auto"). Loop mode read is harmless; output routing is identical for both branches -- advisory file only.
|
|
34
32
|
5. **Sanity cap** -- no quota API call needed. Huginn targets <= 5 advisories per run (quality cap, not a hard abort).
|
|
35
|
-
6. Output: `trigger_type`, `loop_mode
|
|
33
|
+
6. Output: `trigger_type`, `loop_mode`
|
|
36
34
|
|
|
37
35
|
---
|
|
38
36
|
|
|
39
37
|
## Phase 1 - Perceive (parallel data collection)
|
|
40
38
|
|
|
41
|
-
Collect raw signals from
|
|
39
|
+
Collect raw signals from 5 sources. All MCP calls MUST include `projectPath`.
|
|
42
40
|
|
|
43
41
|
### 1a. Code Perception
|
|
44
42
|
git log hot files (last 14 days), Grep for TODO/FIXME/HACK, kenaz_lsp_references on the top hot symbols
|
|
45
43
|
Output: graph_signals (name kept for downstream phases)
|
|
46
44
|
|
|
47
|
-
### 1b.
|
|
48
|
-
synapse_recall, synapse_sigils
|
|
49
|
-
- **COGNITIVE DEBT pre-emption**: For each hot symbol from 1a with zero runes returned by synapse_recall AND a high kenaz_lsp_references count:
|
|
50
|
-
tag it as COGNITIVE DEBT in cognitive_signals. In Phase 4, draft an insight rune at confidence 0.5
|
|
51
|
-
for the top-1 COGNITIVE DEBT symbol (draft only). Do NOT re-propose these symbols if they appear in
|
|
52
|
-
`.kenaz/huginn-proposed-symbols.json` within the last 30 days (see Phase 3 Repeat Guard).
|
|
53
|
-
Output: cognitive_signals
|
|
54
|
-
|
|
55
|
-
### 1c. Episodic Perception
|
|
56
|
-
hippocampus_episode_recall difficulties, hippocampus_episode_recall successes
|
|
57
|
-
Output: episodic_signals
|
|
58
|
-
|
|
59
|
-
### 1d. Cross-Project Perception (if Cortex available)
|
|
60
|
-
cortex_recall architecture patterns
|
|
61
|
-
Output: cross_project_signals
|
|
62
|
-
|
|
63
|
-
### 1e. Task Perception
|
|
45
|
+
### 1b. Task Perception
|
|
64
46
|
- Read .kenaz/tasks/done/ or .collaboration/tasks/done/ last 30 days
|
|
65
47
|
- Read blocked tasks
|
|
66
48
|
- Cluster by scope_paths
|
|
67
49
|
Output: task_signals
|
|
68
50
|
|
|
69
|
-
###
|
|
51
|
+
### 1c. Heimdall Perception
|
|
70
52
|
- **Health snapshots**: `Bash: ls -t {project_path}/.kenaz/health_snapshots/*_after.json 2>/dev/null | head -5`
|
|
71
53
|
- Read each JSON -> extract `score`, `sub_metrics`, `delta`
|
|
72
|
-
- **Health trend**: post-complete only records memory health (`post_complete_verdict: RECORDED`, never reverts). Use the `score` / `delta` series from the snapshots above to spot a declining trend; a falling score
|
|
54
|
+
- **Health trend**: post-complete only records memory health (`post_complete_verdict: RECORDED`, never reverts). Use the `score` / `delta` series from the snapshots above to spot a declining trend; a falling score is a memory-store signal, not a code signal.
|
|
73
55
|
- **OptimizationProposals**: `Bash: curl -s "http://127.0.0.1:18080/api/proposals/list?projectPath={project_path}&status=pending" 2>/dev/null`
|
|
74
56
|
- Each proposal becomes a finding with `source: "heimdall"` -- Huginn should refine + prioritize alongside other signals
|
|
75
57
|
- Dismiss processed proposals via /ignore endpoint (permanently dismissed, no task created).
|
|
76
58
|
- If no verdict yet, skip marking -- GC auto-expires Huginn-generated pending proposals after 30 days.
|
|
77
59
|
Output: heimdall_signals: { health_trend, optimization_proposals[] }
|
|
78
60
|
|
|
79
|
-
###
|
|
61
|
+
### 1d. Wish Perception
|
|
80
62
|
- Fetch pending wishes: `Bash: curl -s http://127.0.0.1:18080/api/wishes/list?status=pending&project_path={project_path}`
|
|
81
63
|
- If pending wishes exist:
|
|
82
64
|
- Mark each as picked_up via `PUT /api/wishes/{id}`
|
|
@@ -155,7 +137,7 @@ Each finding is appended as one JSON line (JSONL format, English only -- MEM-LAN
|
|
|
155
137
|
- `ts` -- ISO8601 timestamp of this Huginn run
|
|
156
138
|
- `title` -- < 80 chars, English, descriptive; no "Create task" / "Open PR" phrasing
|
|
157
139
|
- `insight` -- What was observed and why it matters (1-2 sentences, English)
|
|
158
|
-
- `evidence` -- `{ sources: [tool_name,...],
|
|
140
|
+
- `evidence` -- `{ sources: [tool_name,...], metrics: {} }`
|
|
159
141
|
- `confidence` -- 0.0-1.0 from scoring step
|
|
160
142
|
- `source_signals` -- array of signal categories that contributed
|
|
161
143
|
- `status` -- always "unread" on write; Ansuz sets to "read" / "converted" / "dismissed"
|
|
@@ -169,18 +151,9 @@ Example line (compact, one object per line):
|
|
|
169
151
|
|
|
170
152
|
**Rotation rule (bounded file)**: On every write, the combined array of existing + new entries is sliced to the 100 most recent entries (`.slice(-100)`) before writing back. This keeps the file bounded at <= 100 lines regardless of run frequency. File size stays under ~200 KB under normal operation. Huginn itself performs the rotation inline -- no external cron needed.
|
|
171
153
|
|
|
172
|
-
## Phase 4 -
|
|
154
|
+
## Phase 4 - Record
|
|
173
155
|
|
|
174
|
-
|
|
175
|
-
- synapse_recall to check existing runes
|
|
176
|
-
- synapse_inscribe type=insight, confidence >= 0.7
|
|
177
|
-
|
|
178
|
-
If 3+ symbols form unnamed concept: synapse_forge Sigil
|
|
179
|
-
|
|
180
|
-
Rules:
|
|
181
|
-
- English only (MEM-LANG-001)
|
|
182
|
-
- confidence >= 0.7 (higher than standard 0.6 -- derived knowledge)
|
|
183
|
-
- Do NOT inscribe every finding -- only long-term value
|
|
156
|
+
Huginn is advisory-only: findings go to the advisory file (Phase 3). Durable lessons are recorded by Ansuz with `kenaz learn add`; Huginn does not write memory.
|
|
184
157
|
|
|
185
158
|
---
|
|
186
159
|
|
|
@@ -193,7 +166,7 @@ Output session report (<= 20 lines):
|
|
|
193
166
|
Trigger: {scheduled | milestone | manual}
|
|
194
167
|
Loop Mode: {manual | auto}
|
|
195
168
|
Duration: {thinking time}
|
|
196
|
-
Sources:
|
|
169
|
+
Sources: code: OK/FAIL, tasks: OK/FAIL, heimdall: OK/FAIL, wishes: OK/FAIL
|
|
197
170
|
|
|
198
171
|
Advisories written: {N} (file: .kenaz/specialist/huginn_advisory.jsonl)
|
|
199
172
|
Drafts skipped: {N}
|
|
@@ -170,101 +170,6 @@ function releaseClaimMarker(taskId, projectRoot) {
|
|
|
170
170
|
try { require('fs').unlinkSync(markerPath); } catch { /* already gone */ }
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// ─────────────────────────────────────────
|
|
174
|
-
// TASK_1861: Dispatch-path-agnostic health snapshot capture
|
|
175
|
-
// _before at activation (move/claim/update→active), _after + observe-only verdict
|
|
176
|
-
// at completion (complete AND bare `move X done`). Fires a detached child so the
|
|
177
|
-
// short-lived CLI never blocks, fails, or slows on Dashboard state. Every capture
|
|
178
|
-
// attempt is audited in .kenaz/health_snapshots/snapshot_events.jsonl (never silent).
|
|
179
|
-
// Reuses .formation/tools/post-complete/health.js (measureHealthScoreSync etc.) —
|
|
180
|
-
// no third implementation. Does NOT touch guardian_counter.json (TASK_1720 periodic
|
|
181
|
-
// counter semantics unchanged; verdict stays observe-only, no git action).
|
|
182
|
-
// ─────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
function _appendSnapshotEvent(projectRoot, taskId, phase, action, reason) {
|
|
185
|
-
try {
|
|
186
|
-
const dir = path.join(projectRoot, '.kenaz', 'health_snapshots');
|
|
187
|
-
fsSync.mkdirSync(dir, { recursive: true });
|
|
188
|
-
const line = JSON.stringify({
|
|
189
|
-
task_id: taskId,
|
|
190
|
-
ts: new Date().toISOString(),
|
|
191
|
-
snapshot_type: phase,
|
|
192
|
-
action,
|
|
193
|
-
...(reason ? { reason } : {}),
|
|
194
|
-
});
|
|
195
|
-
fsSync.appendFileSync(path.join(dir, 'snapshot_events.jsonl'), line + '\n', 'utf8');
|
|
196
|
-
} catch { /* audit is best-effort — must never block task ops */ }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function scheduleHealthSnapshotCapture(taskId, phase, projectRoot) {
|
|
200
|
-
try {
|
|
201
|
-
if (!taskId || !projectRoot) return;
|
|
202
|
-
const healthJs = path.join(__dirname, '..', '..', '.formation', 'tools', 'post-complete', 'health.js');
|
|
203
|
-
if (!fsSync.existsSync(healthJs)) {
|
|
204
|
-
_appendSnapshotEvent(projectRoot, taskId, phase, 'skipped', 'health_module_missing');
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const cfg = { taskId, phase, projectRoot, healthJs };
|
|
208
|
-
// ASCII-only inline script (Windows argv codepage safety). Child owns the full
|
|
209
|
-
// outcome audit: written | skipped(reason). measureHealthScoreSync already
|
|
210
|
-
// handles Dashboard-down gracefully (returns null within its own timeout).
|
|
211
|
-
const script = [
|
|
212
|
-
`const CFG = ${JSON.stringify(cfg)};`,
|
|
213
|
-
"const fs = require('fs');",
|
|
214
|
-
"const path = require('path');",
|
|
215
|
-
"function logEvent(action, reason) {",
|
|
216
|
-
" try {",
|
|
217
|
-
" const dir = path.join(CFG.projectRoot, '.kenaz', 'health_snapshots');",
|
|
218
|
-
" fs.mkdirSync(dir, { recursive: true });",
|
|
219
|
-
" const line = JSON.stringify({ task_id: CFG.taskId, ts: new Date().toISOString(), snapshot_type: CFG.phase, action, ...(reason ? { reason } : {}) });",
|
|
220
|
-
" fs.appendFileSync(path.join(dir, 'snapshot_events.jsonl'), line + '\\n', 'utf8');",
|
|
221
|
-
" } catch (_e) { /* best-effort */ }",
|
|
222
|
-
"}",
|
|
223
|
-
"try {",
|
|
224
|
-
" const snapFile = path.join(CFG.projectRoot, '.kenaz', 'health_snapshots', CFG.taskId + '_' + CFG.phase + '.json');",
|
|
225
|
-
" if (fs.existsSync(snapFile)) { logEvent('skipped', 'snapshot_already_exists'); process.exit(0); }",
|
|
226
|
-
" const h = require(CFG.healthJs);",
|
|
227
|
-
" const health = h.measureHealthScoreSync(CFG.projectRoot);",
|
|
228
|
-
" if (!health || typeof health.score !== 'number') { logEvent('skipped', 'dashboard_unreachable'); process.exit(0); }",
|
|
229
|
-
// TASK_1891: capture the written payload (delta_from_previous) for
|
|
230
|
-
// observability threading into computeHealthVerdict below -- see
|
|
231
|
-
// health.js's computeHealthVerdict doc comment for why that field
|
|
232
|
-
// never gates the verdict itself.
|
|
233
|
-
" const afterPayload = h.saveTaskHealthSnapshot(CFG.taskId, CFG.phase, health, CFG.projectRoot);",
|
|
234
|
-
" logEvent('written');",
|
|
235
|
-
" if (CFG.phase === 'after') {",
|
|
236
|
-
" const pre = h.readPreTaskHealthSnapshot(CFG.taskId, CFG.projectRoot);",
|
|
237
|
-
" if (pre && typeof pre.score === 'number') {",
|
|
238
|
-
" const loopCfg = h.readLoopConfig(CFG.projectRoot);",
|
|
239
|
-
" const threshold = typeof loopCfg.revert_threshold === 'number' ? loopCfg.revert_threshold : h.DEFAULT_REVERT_THRESHOLD;",
|
|
240
|
-
// TASK_1890/1891: same computeHealthVerdict as post-complete.js's
|
|
241
|
-
// auto/manual arms -- no third re-implementation of the delta/trust-gate
|
|
242
|
-
// logic. No git access here (no isScopeUnchanged callback), so guard 3
|
|
243
|
-
// (stale snapshot + zero scope diff) is a safe no-op for this call
|
|
244
|
-
// site, same as it always was before TASK_1890. TASK_1891: pass the
|
|
245
|
-
// full `pre` object (not pre.score) so ordering + symmetric trust can
|
|
246
|
-
// be checked -- this is one of the two call sites that actually runs
|
|
247
|
-
// in production (readLoopConfig's TASK_1885 note).
|
|
248
|
-
" const vr = h.computeHealthVerdict(pre, health, threshold, { projectPath: CFG.projectRoot, trendDeltaFromPrevious: afterPayload ? afterPayload.delta_from_previous : null });",
|
|
249
|
-
" const reason = vr.verdict === 'REVERT'",
|
|
250
|
-
" ? vr.reason + ' - observe-only capture at task completion, no git action taken'",
|
|
251
|
-
" : vr.reason;",
|
|
252
|
-
" h.writeVerdictToTask(CFG.taskId, vr.verdict, vr.delta, reason, CFG.projectRoot);",
|
|
253
|
-
" }",
|
|
254
|
-
" }",
|
|
255
|
-
"} catch (e) { logEvent('skipped', 'error: ' + String(e && e.message || e)); }",
|
|
256
|
-
].join('\n');
|
|
257
|
-
const { spawn } = require('child_process');
|
|
258
|
-
const child = spawn(process.execPath, ['-e', script], {
|
|
259
|
-
detached: true,
|
|
260
|
-
stdio: 'ignore',
|
|
261
|
-
windowsHide: true,
|
|
262
|
-
});
|
|
263
|
-
child.unref();
|
|
264
|
-
} catch (e) {
|
|
265
|
-
_appendSnapshotEvent(projectRoot, taskId, phase, 'skipped', 'spawn_failed: ' + String(e && e.message || e));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
173
|
const yaml = (() => {
|
|
269
174
|
try { return require('js-yaml'); } catch {
|
|
270
175
|
// Fallback: resolve .vendor/js-yaml.cjs relative to this script's real location.
|
|
@@ -1770,9 +1675,7 @@ class TaskAPI {
|
|
|
1770
1675
|
*/
|
|
1771
1676
|
async moveTask(taskId, newStatus, taskData = null, options = {}) {
|
|
1772
1677
|
await this._ensureStatusDirs();
|
|
1773
|
-
|
|
1774
|
-
// save the 'after' snapshot itself, so scheduling a second detached measurement is pure waste.
|
|
1775
|
-
const { skipValidation = false, forceClaim = false, skipHealthAfter = false } = options;
|
|
1678
|
+
const { skipValidation = false, forceClaim = false } = options;
|
|
1776
1679
|
|
|
1777
1680
|
// 取得任務資料
|
|
1778
1681
|
let task = taskData;
|
|
@@ -1919,20 +1822,6 @@ class TaskAPI {
|
|
|
1919
1822
|
releaseClaimMarker(taskId, _projectRootForClaim);
|
|
1920
1823
|
}
|
|
1921
1824
|
|
|
1922
|
-
// TASK_1861: dispatch-path-agnostic health capture. moveTask is the single choke
|
|
1923
|
-
// point every activation/completion path funnels through (claim → moveTask,
|
|
1924
|
-
// update --status → moveTask, complete → moveTask, bare move). Fire-and-forget
|
|
1925
|
-
// detached child; idempotent only against snapshots that ALREADY exist when the child
|
|
1926
|
-
// starts (it can race a concurrent writer). TASK_2264: completeTask therefore passes
|
|
1927
|
-
// skipHealthAfter when its own in-process postComplete will measure 'after', so that
|
|
1928
|
-
// path is measured exactly once; paths with no post-complete (bare move, update --status,
|
|
1929
|
-
// complete --skip-post-complete) keep this capture as their only one.
|
|
1930
|
-
if (targetDir === 'active') {
|
|
1931
|
-
scheduleHealthSnapshotCapture(taskId, 'before', _projectRootForClaim);
|
|
1932
|
-
} else if (targetDir === 'done' && !skipHealthAfter) {
|
|
1933
|
-
scheduleHealthSnapshotCapture(taskId, 'after', _projectRootForClaim);
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
1825
|
// Yggdrasil: sync status change to kenaz_tasks
|
|
1937
1826
|
const sync = getTaskSync();
|
|
1938
1827
|
if (sync) {
|
|
@@ -2126,13 +2015,8 @@ class TaskAPI {
|
|
|
2126
2015
|
updatedTask.actual_hours = options.actual_hours;
|
|
2127
2016
|
}
|
|
2128
2017
|
|
|
2129
|
-
// TASK_2264: decide up front whether post-complete (below) will measure 'after' in-process.
|
|
2130
|
-
// TASK_2288 single-winner rule: whenever a post-complete run is expected for this completion,
|
|
2131
|
-
// post-complete's own measurement is the ONE 'after' capture (it runs after merge and feeds the
|
|
2132
|
-
// verdict), so moveTask must not also spawn one. Expected = in-process (below).
|
|
2133
|
-
// moveTask's detached capture stays the sole one only when nobody measures: --skip-post-complete.
|
|
2134
2018
|
const postCompleteRuns = !options.skipPostComplete;
|
|
2135
|
-
const result = await this.moveTask(taskId, 'done', updatedTask
|
|
2019
|
+
const result = await this.moveTask(taskId, 'done', updatedTask);
|
|
2136
2020
|
|
|
2137
2021
|
try {
|
|
2138
2022
|
this._autoPopThoughtsForCompletedTask(taskId, now);
|
|
@@ -2520,100 +2404,6 @@ class TaskAPI {
|
|
|
2520
2404
|
}
|
|
2521
2405
|
}
|
|
2522
2406
|
|
|
2523
|
-
// ============================================================================
|
|
2524
|
-
// TASK_1992/TASK_2104 — Alaya process-lessons echo (best-effort, create-only)
|
|
2525
|
-
// ============================================================================
|
|
2526
|
-
// Mirrors prompt.js's appendAlayaLessons: same query, same alaya_recall
|
|
2527
|
-
// shape, and (TASK_2104) the same TASK_2101 cosine-floor + stack-match
|
|
2528
|
-
// gate — loaded from the dependency-free .formation/tools/alaya-gate.js
|
|
2529
|
-
// module (require() resolved relative to __dirname; GEN-008) instead of
|
|
2530
|
-
// the hand-copied query builder that used to live here with no gate at all.
|
|
2531
|
-
// Never affects create's exit code: any failure is swallowed silently.
|
|
2532
|
-
// Opt-out: KENAZ_NO_ALAYA_ECHO=1.
|
|
2533
|
-
|
|
2534
|
-
// Best-effort: null (not a throw) when unavailable — e.g. a sub-project
|
|
2535
|
-
// with no .formation/ checkout — so the caller prints nothing rather than
|
|
2536
|
-
// falling back to ungated results.
|
|
2537
|
-
function loadAlayaGate() {
|
|
2538
|
-
try {
|
|
2539
|
-
return require('../../.formation/tools/alaya-gate');
|
|
2540
|
-
} catch {
|
|
2541
|
-
return null;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
// fetchImpl/gateImpl: injectable test seams; default to the real fetch and loadAlayaGate().
|
|
2546
|
-
async function echoAlayaLessons(taskData, fetchImpl, gateImpl) {
|
|
2547
|
-
if (process.env.KENAZ_NO_ALAYA_ECHO === '1') return;
|
|
2548
|
-
|
|
2549
|
-
const doFetch = fetchImpl || (typeof fetch !== 'undefined' ? fetch : null);
|
|
2550
|
-
if (!doFetch) return;
|
|
2551
|
-
|
|
2552
|
-
const gate = gateImpl !== undefined ? gateImpl : loadAlayaGate();
|
|
2553
|
-
if (!gate) return; // shared gate unavailable — never print ungated results
|
|
2554
|
-
|
|
2555
|
-
const query = gate.buildTaskContextQuery(taskData);
|
|
2556
|
-
if (!query) return;
|
|
2557
|
-
|
|
2558
|
-
const taskStack = gate.deriveTaskStack((taskData && taskData.scope_paths) || []);
|
|
2559
|
-
|
|
2560
|
-
const controller = new AbortController();
|
|
2561
|
-
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
2562
|
-
try {
|
|
2563
|
-
const payload = JSON.stringify({
|
|
2564
|
-
jsonrpc: '2.0',
|
|
2565
|
-
id: 1,
|
|
2566
|
-
method: 'tools/call',
|
|
2567
|
-
params: {
|
|
2568
|
-
name: 'alaya_recall',
|
|
2569
|
-
arguments: {
|
|
2570
|
-
query, mode: 'hybrid', seed_types: ['failure_seed', 'lesson_seed'],
|
|
2571
|
-
limit: gate.ALAYA_CANDIDATE_POOL, min_cosine: gate.ALAYA_MIN_COSINE_DEFAULT,
|
|
2572
|
-
},
|
|
2573
|
-
},
|
|
2574
|
-
});
|
|
2575
|
-
const res = await doFetch('http://127.0.0.1:18080/api/mcp', {
|
|
2576
|
-
method: 'POST',
|
|
2577
|
-
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' },
|
|
2578
|
-
body: payload,
|
|
2579
|
-
signal: controller.signal,
|
|
2580
|
-
});
|
|
2581
|
-
if (!res || !res.ok) return;
|
|
2582
|
-
const result = await res.json();
|
|
2583
|
-
if (!result || result.error) return;
|
|
2584
|
-
|
|
2585
|
-
const content = result.result && result.result.content;
|
|
2586
|
-
if (!content || !Array.isArray(content)) return;
|
|
2587
|
-
const textPart = content.find(c => c.type === 'text');
|
|
2588
|
-
if (!textPart || !textPart.text) return;
|
|
2589
|
-
|
|
2590
|
-
let recallData;
|
|
2591
|
-
try { recallData = JSON.parse(textPart.text); } catch { return; }
|
|
2592
|
-
if (!recallData) return;
|
|
2593
|
-
|
|
2594
|
-
let bijas = recallData.bijas || [];
|
|
2595
|
-
if (bijas.length === 0) return;
|
|
2596
|
-
|
|
2597
|
-
bijas = gate.dedupBijas(bijas);
|
|
2598
|
-
// TASK_2104: same relevance gate as prompt.js's appendAlayaLessons —
|
|
2599
|
-
// the server already applied min_cosine, but re-checking (+ the
|
|
2600
|
-
// stack-match half, which the server does not apply) keeps this
|
|
2601
|
-
// correct against a stub/older server and drops stack mismatches.
|
|
2602
|
-
bijas = bijas.filter(b => gate.passesAlayaRelevanceGate(b, taskStack, gate.ALAYA_MIN_COSINE_DEFAULT)).slice(0, 3);
|
|
2603
|
-
if (bijas.length === 0) return;
|
|
2604
|
-
|
|
2605
|
-
console.log(' Alaya lessons for this card:');
|
|
2606
|
-
for (const b of bijas) {
|
|
2607
|
-
const preview = (b.content || '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
2608
|
-
console.log(` - [${b.seed_type || 'seed'}/${b.origin || 'unknown'}] ${preview}`);
|
|
2609
|
-
}
|
|
2610
|
-
} catch {
|
|
2611
|
-
// Best-effort: MCP down, timeout, network error, malformed response — silent.
|
|
2612
|
-
} finally {
|
|
2613
|
-
clearTimeout(timeoutId);
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
2407
|
// TASK_2257: estimated_hours is a finite number >= 0 or null. '' (flag present, empty value)
|
|
2618
2408
|
// CLEARS the field (null) -- same "clearable" contract as --description/--notes. Anything
|
|
2619
2409
|
// non-numeric or negative exits 1 instead of persisting NaN / a nonsense value.
|
|
@@ -3198,8 +2988,6 @@ Create 選項:
|
|
|
3198
2988
|
if (task.estimated_hours != null) console.log(`預估工時: ${task.estimated_hours}h`);
|
|
3199
2989
|
if (task.branch) console.log(`分支: ${task.branch}`);
|
|
3200
2990
|
|
|
3201
|
-
await echoAlayaLessons(taskData);
|
|
3202
|
-
|
|
3203
2991
|
} else if (command === 'claim') {
|
|
3204
2992
|
const taskId = args[1];
|
|
3205
2993
|
if (!taskId) {
|
|
@@ -3737,10 +3525,6 @@ module.exports = {
|
|
|
3737
3525
|
parseYAML,
|
|
3738
3526
|
toYAML,
|
|
3739
3527
|
resolveTasksDir,
|
|
3740
|
-
// TASK_1992/TASK_2104: exported for direct unit-testing with a mocked
|
|
3741
|
-
// fetch (and an injectable alaya-gate module) — no live MCP dependency.
|
|
3742
|
-
loadAlayaGate,
|
|
3743
|
-
echoAlayaLessons,
|
|
3744
3528
|
// TASK_2142 R2: test seam for injecting a fake pool-status-push module.
|
|
3745
3529
|
setPoolStatusPush,
|
|
3746
3530
|
};
|
|
@@ -17,11 +17,10 @@ const assert = require('assert');
|
|
|
17
17
|
const fs = require('fs').promises;
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const os = require('os');
|
|
20
|
-
const { TaskAPI, CONFIG, parseYAML, toYAML
|
|
20
|
+
const { TaskAPI, CONFIG, parseYAML, toYAML } = require('./task-api.js');
|
|
21
21
|
// TASK_2104: same shared gate task-api.js loads at runtime — used here only
|
|
22
22
|
// to read its constants for assertions (query building + gate behavior
|
|
23
23
|
// itself is covered by prompt.test.js against the one implementation).
|
|
24
|
-
const alayaGate = require('../../.formation/tools/alaya-gate.js');
|
|
25
24
|
|
|
26
25
|
// ============================================================================
|
|
27
26
|
// 測試工具
|
|
@@ -1129,140 +1128,4 @@ runner.test('shared-mode: id_alloc stamp on task when local_fallback', async ()
|
|
|
1129
1128
|
});
|
|
1130
1129
|
|
|
1131
1130
|
|
|
1132
|
-
// ============================================================================
|
|
1133
|
-
// 測試案例: TASK_1992 — Alaya process-lessons echo (create command)
|
|
1134
|
-
// ============================================================================
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Captures console.log calls made during fn() and returns them as an array
|
|
1138
|
-
* of joined-argument strings, restoring console.log afterward.
|
|
1139
|
-
*/
|
|
1140
|
-
async function captureLogs(fn) {
|
|
1141
|
-
const original = console.log;
|
|
1142
|
-
const logs = [];
|
|
1143
|
-
console.log = (...a) => logs.push(a.join(' '));
|
|
1144
|
-
try {
|
|
1145
|
-
await fn();
|
|
1146
|
-
} finally {
|
|
1147
|
-
console.log = original;
|
|
1148
|
-
}
|
|
1149
|
-
return logs;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function mcpTextResponse(bodyObj) {
|
|
1153
|
-
return {
|
|
1154
|
-
ok: true,
|
|
1155
|
-
json: async () => ({
|
|
1156
|
-
jsonrpc: '2.0', id: 1,
|
|
1157
|
-
result: { content: [{ type: 'text', text: JSON.stringify(bodyObj) }] },
|
|
1158
|
-
}),
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const ALAYA_TASK_DATA = { title: 'Fix appendCortexContext dedup bug', scope_paths: ['.formation/tools/prompt.js'] };
|
|
1163
|
-
|
|
1164
|
-
runner.test('alaya-gate buildTaskContextQuery (shared with prompt.js): builds a query from title + scope_paths, null when empty', () => {
|
|
1165
|
-
const q = alayaGate.buildTaskContextQuery(ALAYA_TASK_DATA);
|
|
1166
|
-
assert.ok(q && q.includes('prompt'));
|
|
1167
|
-
assert.strictEqual(alayaGate.buildTaskContextQuery({}), null);
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
runner.test('echoAlayaLessons: requests the candidate pool with min_cosine, prints up to 3 gated seeds from a mocked fetch', async () => {
|
|
1171
|
-
let calledUrl = null;
|
|
1172
|
-
let calledOpts = null;
|
|
1173
|
-
const fetchMock = async (url, opts) => {
|
|
1174
|
-
calledUrl = url;
|
|
1175
|
-
calledOpts = opts;
|
|
1176
|
-
return mcpTextResponse({
|
|
1177
|
-
// js/.formation stack overlap with ALAYA_TASK_DATA's scope_paths, and
|
|
1178
|
-
// cosine clears ALAYA_MIN_COSINE_DEFAULT — both must pass the gate.
|
|
1179
|
-
bijas: [
|
|
1180
|
-
{ id: 'a1', seed_type: 'failure_seed', origin: 'local', content: 'Root cause one.', cosine: 0.75, tags: 'js' },
|
|
1181
|
-
{ id: 'a2', seed_type: 'lesson_seed', origin: 'remote', content: 'Lesson two.', cosine: 0.72, tags: 'js' },
|
|
1182
|
-
],
|
|
1183
|
-
mode_effective: 'hybrid',
|
|
1184
|
-
});
|
|
1185
|
-
};
|
|
1186
|
-
|
|
1187
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
|
|
1188
|
-
|
|
1189
|
-
assert.strictEqual(calledUrl, 'http://127.0.0.1:18080/api/mcp');
|
|
1190
|
-
const body = JSON.parse(calledOpts.body);
|
|
1191
|
-
assert.strictEqual(body.params.name, 'alaya_recall');
|
|
1192
|
-
assert.strictEqual(body.params.arguments.mode, 'hybrid');
|
|
1193
|
-
assert.deepStrictEqual(body.params.arguments.seed_types, ['failure_seed', 'lesson_seed']);
|
|
1194
|
-
assert.strictEqual(body.params.arguments.limit, alayaGate.ALAYA_CANDIDATE_POOL);
|
|
1195
|
-
assert.strictEqual(body.params.arguments.min_cosine, alayaGate.ALAYA_MIN_COSINE_DEFAULT);
|
|
1196
|
-
|
|
1197
|
-
assert.ok(logs.some(l => l.includes('Alaya lessons for this card:')));
|
|
1198
|
-
assert.ok(logs.some(l => l.includes('[failure_seed/local] Root cause one.')));
|
|
1199
|
-
assert.ok(logs.some(l => l.includes('[lesson_seed/remote] Lesson two.')));
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
runner.test('echoAlayaLessons: TASK_2104 gate drops a below-floor bija and never prints it', async () => {
|
|
1203
|
-
const fetchMock = async () => mcpTextResponse({
|
|
1204
|
-
bijas: [
|
|
1205
|
-
// Below ALAYA_MIN_COSINE_DEFAULT (0.70) — must be dropped even though
|
|
1206
|
-
// it stack-matches (tags: 'js') and would render fine otherwise.
|
|
1207
|
-
{ id: 'b1', seed_type: 'failure_seed', origin: 'local', content: 'Stale noise seed.', cosine: 0.5, tags: 'js' },
|
|
1208
|
-
],
|
|
1209
|
-
mode_effective: 'hybrid',
|
|
1210
|
-
});
|
|
1211
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
|
|
1212
|
-
assert.strictEqual(logs.length, 0, 'a below-floor bija must print nothing, not fall back to ungated output');
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
runner.test('echoAlayaLessons: TASK_2104 gate keeps an above-floor stack-matching bija', async () => {
|
|
1216
|
-
const fetchMock = async () => mcpTextResponse({
|
|
1217
|
-
bijas: [
|
|
1218
|
-
{ id: 'c1', seed_type: 'lesson_seed', origin: 'local', content: 'On-topic lesson.', cosine: 0.8, tags: 'js' },
|
|
1219
|
-
],
|
|
1220
|
-
mode_effective: 'hybrid',
|
|
1221
|
-
});
|
|
1222
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
|
|
1223
|
-
assert.ok(logs.some(l => l.includes('[lesson_seed/local] On-topic lesson.')));
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
runner.test('echoAlayaLessons: gate module unavailable (e.g. sub-project with no .formation/) silently skips, never falls back to ungated output', async () => {
|
|
1227
|
-
let fetchCalled = false;
|
|
1228
|
-
const fetchMock = async () => { fetchCalled = true; return mcpTextResponse({ bijas: [{ id: 'd1', seed_type: 'failure_seed', origin: 'local', content: 'never printed' }] }); };
|
|
1229
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock, null));
|
|
1230
|
-
assert.strictEqual(logs.length, 0);
|
|
1231
|
-
assert.strictEqual(fetchCalled, false, 'must return before ever calling fetch when the gate cannot be loaded');
|
|
1232
|
-
});
|
|
1233
|
-
|
|
1234
|
-
runner.test('echoAlayaLessons: silent when fetch rejects (network error / timeout)', async () => {
|
|
1235
|
-
const fetchMock = async () => { throw new Error('AbortError: The operation was aborted'); };
|
|
1236
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
|
|
1237
|
-
assert.strictEqual(logs.length, 0);
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
runner.test('echoAlayaLessons: silent when fetch resolves non-ok', async () => {
|
|
1241
|
-
const fetchMock = async () => ({ ok: false, status: 500 });
|
|
1242
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
|
|
1243
|
-
assert.strictEqual(logs.length, 0);
|
|
1244
|
-
});
|
|
1245
|
-
|
|
1246
|
-
runner.test('echoAlayaLessons: silent when the MCP result has zero bijas', async () => {
|
|
1247
|
-
const fetchMock = async () => mcpTextResponse({ bijas: [], mode_effective: 'hybrid' });
|
|
1248
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
|
|
1249
|
-
assert.strictEqual(logs.length, 0);
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
runner.test('echoAlayaLessons: KENAZ_NO_ALAYA_ECHO=1 suppresses the echo entirely (fetch never called)', async () => {
|
|
1253
|
-
let fetchCalled = false;
|
|
1254
|
-
const fetchMock = async () => { fetchCalled = true; return mcpTextResponse({ bijas: [{ id: 'x', seed_type: 'failure_seed', origin: 'local', content: 'never printed' }] }); };
|
|
1255
|
-
|
|
1256
|
-
const prev = process.env.KENAZ_NO_ALAYA_ECHO;
|
|
1257
|
-
process.env.KENAZ_NO_ALAYA_ECHO = '1';
|
|
1258
|
-
try {
|
|
1259
|
-
const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
|
|
1260
|
-
assert.strictEqual(logs.length, 0);
|
|
1261
|
-
assert.strictEqual(fetchCalled, false);
|
|
1262
|
-
} finally {
|
|
1263
|
-
if (prev === undefined) delete process.env.KENAZ_NO_ALAYA_ECHO;
|
|
1264
|
-
else process.env.KENAZ_NO_ALAYA_ECHO = prev;
|
|
1265
|
-
}
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
1131
|
runner.run();
|
|
@@ -276,7 +276,7 @@ function renderMemoryStartupDoc() {
|
|
|
276
276
|
|
|
277
277
|
function renderOverride(prompt, bootContext, { policy = BOOT_POLICY, projectPath = PROJECT_PATH, memoryStartupDoc = '' } = {}) {
|
|
278
278
|
const projectInstruction = projectPath
|
|
279
|
-
? `You are working on sub-project: ${projectPath}\nAll task-api.js commands must include --project-path="${projectPath}".\nAll MCP tool calls (
|
|
279
|
+
? `You are working on sub-project: ${projectPath}\nAll task-api.js commands must include --project-path="${projectPath}".\nAll MCP tool calls (kenaz_*) must include projectPath="${projectPath}".\n\n`
|
|
280
280
|
: '';
|
|
281
281
|
const codexDdxInjection = renderCodexAntiDrift();
|
|
282
282
|
|
|
@@ -491,7 +491,7 @@ async function main() {
|
|
|
491
491
|
? runFastBoot()
|
|
492
492
|
: await fetchBootContext({ specialist: SPECIALIST, projectRoot: ROOT, projectPath: PROJECT_PATH || ROOT, includeCanary: false });
|
|
493
493
|
const offlineNote = probe.reachable ? ''
|
|
494
|
-
: '# Boot context availability\nInitial Kenaz MCP probe failed;
|
|
494
|
+
: '# Boot context availability\nInitial Kenaz MCP probe failed; server availability is not established. Local fast boot context is not proof of a successful kenaz_ping check.\n\n';
|
|
495
495
|
writeOverride(prompt, offlineNote + bootCtx);
|
|
496
496
|
// ── SessionStart hook (TASK_1230) ─────────────────────────────────────────
|
|
497
497
|
// Fires after prompt is written (AGENTS.override.md exists) but before Codex
|
|
@@ -23,7 +23,7 @@ function mockRequest(scenario = 'offline') {
|
|
|
23
23
|
if (scenario === 'incomplete') { res.complete = false; res.emit('close'); return; }
|
|
24
24
|
if (scenario === 'oversized') { res.emit('data', 'x'.repeat(1024 * 1024 + 1)); return; }
|
|
25
25
|
const body = {
|
|
26
|
-
ok: JSON.stringify({ jsonrpc: '2.0', id: 1, result: { tools: [{ name: '
|
|
26
|
+
ok: JSON.stringify({ jsonrpc: '2.0', id: 1, result: { tools: [{ name: 'kenaz_ping' }] } }),
|
|
27
27
|
malformed: '{', empty: '{"result":{"tools":[]}}',
|
|
28
28
|
'rpc-error': '{"error":{"code":-1}}', null: 'null',
|
|
29
29
|
}[scenario] || '{}';
|
|
@@ -135,7 +135,7 @@ test('offline default Ansuz continues once with elixir, truthful status and pinn
|
|
|
135
135
|
assert.deepEqual(Array.from(effects.promptCalls[0].args.slice(-2)), ['--project-path', project]);
|
|
136
136
|
assert.ok(effects.promptCalls[0].args.includes('--external-boot-context'));
|
|
137
137
|
const override = effects.writes.find(write => write.name.endsWith('AGENTS.override.md'));
|
|
138
|
-
assert.match(override.content, /
|
|
138
|
+
assert.match(override.content, /server availability is not established/);
|
|
139
139
|
assert.match(override.content, /Fixture fast context banner/);
|
|
140
140
|
assert.equal((override.content.match(/## Codex Bootstrap Policy \(fast\)/g) || []).length, 1);
|
|
141
141
|
assert.ok(override.content.includes(`projectPath="${project}"`));
|