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.
- 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-2288-followups.test.js +2 -37
- package/core/.collaboration/core/task-2421-replica.test.js +229 -0
- package/core/.collaboration/core/task-api.js +223 -230
- package/core/.collaboration/core/task-api.test.js +1 -138
- package/core/.collaboration/core/task-replica.js +189 -0
- package/core/.formation/.merge-lock +1 -0
- package/core/.formation/bifrost/codex-doctor.js +12 -2
- package/core/.formation/bifrost/codex-doctor.test.js +15 -0
- package/core/.formation/bifrost/launch.js +2 -2
- package/core/.formation/bifrost/launch.test.js +2 -2
- package/core/.formation/bifrost/setup-codex.js +84 -16
- 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/task2420.test.js +129 -0
- package/core/.formation/tools/post-complete/testing.js +3 -21
- package/core/.formation/tools/post-complete.js +131 -436
- package/core/.formation/tools/post-complete.test.js +0 -135
- package/core/.formation/tools/route-audit.test.js +8 -20
- package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
- package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
- 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/__tests__/rea-detect.test.js +10 -0
- 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/rea-detect.js +6 -2
- package/core/.specialist/tools/session.js +2 -89
- package/core/.synapse/memory-shared.json.sync +1 -0
- 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 +75 -83
- package/core/dev/specs/peer/CODEX_MANAGED_DAEMON.md +54 -0
- package/core/dev/specs/peer/setup-codex-hooks.test.js +38 -0
- package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +56 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +1 -0
- package/core/plugins/kenaz/INVENTORY.md +1 -1
- package/core/plugins/kenaz/scripts/kenaz.test.js +70 -1
- package/core/plugins/kenaz/scripts/ygg.js +45 -4
- 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 +17 -9
- package/lib/cli.test.js +10 -0
- package/lib/dashboard.test.js +3 -3
- package/package.json +6 -6
- package/vendor/install-kenaz-cli.js +167 -16
- package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
- package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
- package/core/.collaboration/core/task-2264-single-after-capture.test.js +0 -61
- 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}
|
|
@@ -9,11 +9,10 @@ const { spawnSync } = require('child_process');
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const os = require('os');
|
|
12
|
-
const cp = require('child_process');
|
|
13
12
|
|
|
14
13
|
const TASKAPI = path.join(__dirname, 'task-api.js');
|
|
15
14
|
const TASKJSON = path.join(__dirname, 'task-api-json.js');
|
|
16
|
-
const {
|
|
15
|
+
const { toYAML } = require('./task-api.js');
|
|
17
16
|
|
|
18
17
|
const SB = fs.mkdtempSync(path.join(os.tmpdir(), 't2288_'));
|
|
19
18
|
const tasksDir = path.join(SB, '.kenaz', 'tasks');
|
|
@@ -37,20 +36,6 @@ const field = (id, k) => (findCard(id).text.match(new RegExp('^' + k + ':\\s*(.*
|
|
|
37
36
|
const mkActive = (id) => fs.writeFileSync(path.join(tasksDir, 'active', id + '_t.yaml'),
|
|
38
37
|
toYAML({ id, title: 't2288 ' + id, priority: 'P2', status: 'in_progress' }));
|
|
39
38
|
|
|
40
|
-
// --- spawn counting seam (same as TASK_2264) ---
|
|
41
|
-
let phases = [];
|
|
42
|
-
const realSpawn = cp.spawn;
|
|
43
|
-
cp.spawn = function (cmd, argv, opts) {
|
|
44
|
-
const script = Array.isArray(argv) ? argv.find(a => typeof a === 'string' && a.includes('const CFG')) : null;
|
|
45
|
-
if (script) {
|
|
46
|
-
const m = script.match(/"phase":"(before|after)"/);
|
|
47
|
-
phases.push(m ? m[1] : '?');
|
|
48
|
-
return { unref() {}, on() {} };
|
|
49
|
-
}
|
|
50
|
-
return realSpawn.apply(this, arguments);
|
|
51
|
-
};
|
|
52
|
-
const afters = () => phases.filter(p => p === 'after').length;
|
|
53
|
-
|
|
54
39
|
(async () => {
|
|
55
40
|
try {
|
|
56
41
|
// ---- (1) actual-hours validation (CLI, subprocess so spawn patch is irrelevant) ----
|
|
@@ -90,30 +75,10 @@ const afters = () => phases.filter(p => p === 'after').length;
|
|
|
90
75
|
check('Z3 task-api-json emits estimated_hours 0 as 0', p2 && p2.estimated_hours === 0, JSON.stringify(p2 && p2.estimated_hours) + j2.stderr);
|
|
91
76
|
check('Z4 task-api-json emits actual_hours 0 as 0', p2 && p2.actual_hours === 0, JSON.stringify(p2 && p2.actual_hours));
|
|
92
77
|
}
|
|
93
|
-
|
|
94
|
-
// ---- (3) single 'after' capture ----
|
|
95
|
-
const api = new TaskAPI({ tasksDir, projectPath: SB });
|
|
96
|
-
for (const envName of ['FORMATION_HEADLESS', 'FORMATION_SCHEDULER']) {
|
|
97
|
-
const id = envName === 'FORMATION_HEADLESS' ? 'TASK_9111' : 'TASK_9112';
|
|
98
|
-
mkActive(id); phases = [];
|
|
99
|
-
process.env[envName] = '1';
|
|
100
|
-
try { await api.completeTask(id, {}); } finally { delete process.env[envName]; }
|
|
101
|
-
check(`R1 ${envName}: caller's post-complete owns 'after'; moveTask schedules none`, afters() === 0, JSON.stringify(phases));
|
|
102
|
-
}
|
|
103
|
-
mkActive('TASK_9113'); phases = [];
|
|
104
|
-
process.env.FORMATION_HEADLESS = '1';
|
|
105
|
-
try { await api.completeTask('TASK_9113', { skipPostComplete: true }); } finally { delete process.env.FORMATION_HEADLESS; }
|
|
106
|
-
check('R2 control: headless + skipPostComplete (nobody else measures) keeps exactly 1 capture', afters() === 1, JSON.stringify(phases));
|
|
107
|
-
mkActive('TASK_9114'); phases = [];
|
|
108
|
-
await api.moveTask('TASK_9114', 'done');
|
|
109
|
-
check('R3 control: bare move done captures exactly once', afters() === 1, JSON.stringify(phases));
|
|
110
|
-
mkActive('TASK_9115'); phases = [];
|
|
111
|
-
await api.completeTask('TASK_9115', { dryRun: true, skipGit: true, skipArchive: true });
|
|
112
|
-
check('R4 control: in-process complete (TASK_2264) still schedules none', afters() === 0, JSON.stringify(phases));
|
|
78
|
+
// (3) the single 'after' memory-health capture checks were removed with the capture itself (TASK_2411)
|
|
113
79
|
} catch (e) {
|
|
114
80
|
check('no exception', false, e && e.stack);
|
|
115
81
|
} finally {
|
|
116
|
-
cp.spawn = realSpawn;
|
|
117
82
|
try { fs.rmSync(SB, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
118
83
|
}
|
|
119
84
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TASK_2421 -- shared-room card replicas, unknown-id lookup, import, alias,
|
|
4
|
+
* local_fallback warning. Uses a local stub relay (never the real relay) and
|
|
5
|
+
* temp project dirs. YGGDRASIL_PUBLISHABLE_KEY is set so ~/.kenaz is never read.
|
|
6
|
+
*/
|
|
7
|
+
const assert = require('assert');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const http = require('http');
|
|
12
|
+
const { execFile } = require('child_process');
|
|
13
|
+
const { TaskAPI } = require('./task-api.js');
|
|
14
|
+
|
|
15
|
+
process.env.YGGDRASIL_PUBLISHABLE_KEY = 'test-key';
|
|
16
|
+
const CLI = path.join(__dirname, 'task-api.js');
|
|
17
|
+
|
|
18
|
+
function startRelay({ failAllocate = false } = {}) {
|
|
19
|
+
const state = { rows: new Map(), nextId: 9000, posts: [], patches: [], authSeen: [] };
|
|
20
|
+
const server = http.createServer((req, res) => {
|
|
21
|
+
let body = '';
|
|
22
|
+
req.on('data', (c) => { body += c; });
|
|
23
|
+
req.on('end', () => {
|
|
24
|
+
state.authSeen.push(req.headers.authorization);
|
|
25
|
+
const url = new URL(req.url, 'http://x');
|
|
26
|
+
const json = (code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
|
|
27
|
+
if (req.method === 'POST' && url.pathname === '/api/tasks/allocate-ids') {
|
|
28
|
+
if (failAllocate) return json(500, { error: 'down' });
|
|
29
|
+
const start = state.nextId; state.nextId += 20;
|
|
30
|
+
return json(200, { start, end: start + 19 });
|
|
31
|
+
}
|
|
32
|
+
if (req.method === 'POST' && url.pathname === '/api/tasks') {
|
|
33
|
+
const b = JSON.parse(body); state.posts.push(b);
|
|
34
|
+
state.rows.set(b.task_id, { ...b, claimed_by: null, assigned_to: b.assigned_to || null });
|
|
35
|
+
return json(200, state.rows.get(b.task_id));
|
|
36
|
+
}
|
|
37
|
+
if (req.method === 'PATCH' && url.pathname.startsWith('/api/tasks/')) {
|
|
38
|
+
const id = decodeURIComponent(url.pathname.split('/').pop());
|
|
39
|
+
const b = JSON.parse(body); state.patches.push({ id, ...b });
|
|
40
|
+
const row = state.rows.get(id);
|
|
41
|
+
if (!row) return json(409, { error: 'conflict' });
|
|
42
|
+
for (const k of ['status', 'title', 'description', 'priority', 'full_yaml']) if (b[k] != null) row[k] = b[k];
|
|
43
|
+
return json(200, row);
|
|
44
|
+
}
|
|
45
|
+
if (req.method === 'GET' && url.pathname === '/api/tasks') {
|
|
46
|
+
const wanted = (url.searchParams.get('status') || 'pool').split(',');
|
|
47
|
+
return json(200, [...state.rows.values()].filter(r => wanted.includes(r.status)));
|
|
48
|
+
}
|
|
49
|
+
json(404, {});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => {
|
|
53
|
+
resolve({ state, server, url: `ws://127.0.0.1:${server.address().port}` });
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mkProject(yamlText) {
|
|
58
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'task2421-'));
|
|
59
|
+
fs.mkdirSync(path.join(root, '.kenaz', 'tasks'), { recursive: true });
|
|
60
|
+
if (yamlText) fs.writeFileSync(path.join(root, '.kenaz', 'yggdrasil.yaml'), yamlText);
|
|
61
|
+
return { root, api: new TaskAPI({ projectPath: root, tasksDir: path.join(root, '.kenaz', 'tasks') }) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cfg = (url, extra = '') => `enabled: true\nrelay_url: "${url}"\nyggdrasil_project: room\nnode_name: node-A\n${extra}`;
|
|
65
|
+
const card = (extra = {}) => ({ title: 'Replica me', description: 'desc', priority: 'P1', requirements: ['r1'], acceptance_criteria: ['a1'], force: true, ...extra });
|
|
66
|
+
|
|
67
|
+
function cli(root, args) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
execFile(process.execPath, [CLI, '--project-path', root, ...args], { env: process.env, timeout: 30000 },
|
|
70
|
+
(err, stdout, stderr) => resolve({ code: err ? (err.code || 1) : 0, stdout, stderr }));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tests = [];
|
|
75
|
+
const test = (n, f) => tests.push([n, f]);
|
|
76
|
+
|
|
77
|
+
test('shared room: create publishes a non-pool read-only replica', async () => {
|
|
78
|
+
const relay = await startRelay();
|
|
79
|
+
const { api } = mkProject(cfg(relay.url));
|
|
80
|
+
const t = await api.createTask(card({ alias: '791-17' }));
|
|
81
|
+
assert.strictEqual(relay.state.posts.length, 1);
|
|
82
|
+
const p = relay.state.posts[0];
|
|
83
|
+
assert.strictEqual(p.task_id, t.id);
|
|
84
|
+
assert.strictEqual(p.status, 'assigned', 'must not be claimable pool work');
|
|
85
|
+
assert.ok(!p.assigned_to, 'no assigned_to so creator inbox (pool_pull) stays clean');
|
|
86
|
+
assert.strictEqual(p.created_by, 'node-A');
|
|
87
|
+
assert.ok(/replica: true/.test(p.full_yaml) && /791-17/.test(p.full_yaml) && /origin_node: node-A/.test(p.full_yaml));
|
|
88
|
+
assert.ok(relay.state.authSeen.every(a => a === 'Bearer test-key'));
|
|
89
|
+
assert.strictEqual(t.replica_published, true);
|
|
90
|
+
relay.server.close();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('opted out (share_cards: false) -> no replica; ids still from relay', async () => {
|
|
94
|
+
const relay = await startRelay();
|
|
95
|
+
const { api } = mkProject(cfg(relay.url, 'share_cards: false\n'));
|
|
96
|
+
const t = await api.createTask(card());
|
|
97
|
+
assert.strictEqual(relay.state.posts.length, 0);
|
|
98
|
+
assert.ok(/^TASK_9\d{3}$/.test(t.id));
|
|
99
|
+
relay.server.close();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('not a shared room (no yggdrasil config) -> no relay traffic', async () => {
|
|
103
|
+
const relay = await startRelay();
|
|
104
|
+
const { api } = mkProject(null);
|
|
105
|
+
await api.createTask(card());
|
|
106
|
+
assert.strictEqual(relay.state.posts.length, 0);
|
|
107
|
+
relay.server.close();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('relay down: local create still succeeds with one replica warning', async () => {
|
|
111
|
+
const relay = await startRelay();
|
|
112
|
+
const { api } = mkProject(cfg(relay.url));
|
|
113
|
+
const first = await api.createTask(card()); // gets a lease while relay is up
|
|
114
|
+
await new Promise(r => relay.server.close(r));
|
|
115
|
+
const warns = [];
|
|
116
|
+
const orig = console.warn; console.warn = (...a) => warns.push(a.join(' '));
|
|
117
|
+
let t;
|
|
118
|
+
try { t = await api.createTask(card({ title: 'Second' })); } finally { console.warn = orig; }
|
|
119
|
+
assert.ok(t && t.id && t.id !== first.id);
|
|
120
|
+
assert.ok(fs.existsSync(t._file_path), 'local card written');
|
|
121
|
+
assert.strictEqual(t.replica_published, false);
|
|
122
|
+
assert.strictEqual(warns.filter(w => /replica not published/.test(w)).length, 1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('local_fallback in a shared room warns loudly (stderr + create output)', async () => {
|
|
126
|
+
const { api, root } = mkProject(cfg('ws://127.0.0.1:1')); // unreachable relay, no lease
|
|
127
|
+
const r = await cli(root, ['create', '--title', 'Fallback', '--force']);
|
|
128
|
+
assert.strictEqual(r.code, 0, r.stderr);
|
|
129
|
+
assert.ok(/ID COLLISION RISK/.test(r.stderr), 'stderr warning');
|
|
130
|
+
assert.ok(/⚠️.*locally because the relay is unreachable/.test(r.stdout), 'create output warning');
|
|
131
|
+
assert.ok(api);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('solo project local id allocation prints no collision warning', async () => {
|
|
135
|
+
const { root } = mkProject(null);
|
|
136
|
+
const r = await cli(root, ['create', '--title', 'Solo', '--force']);
|
|
137
|
+
assert.strictEqual(r.code, 0, r.stderr);
|
|
138
|
+
assert.ok(!/COLLISION/.test(r.stderr + r.stdout));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('get unknown id shows replica with owner node; mutations explain, create no local file', async () => {
|
|
142
|
+
const relay = await startRelay();
|
|
143
|
+
const a = mkProject(cfg(relay.url));
|
|
144
|
+
const t = await a.api.createTask(card());
|
|
145
|
+
// node B: same room, empty local store
|
|
146
|
+
const b = mkProject(cfg(relay.url).replace('node-A', 'node-B'));
|
|
147
|
+
const g = await cli(b.root, ['get', t.id]);
|
|
148
|
+
assert.strictEqual(g.code, 0, g.stderr);
|
|
149
|
+
const out = JSON.parse(g.stdout);
|
|
150
|
+
assert.strictEqual(out.read_only_replica, true);
|
|
151
|
+
assert.strictEqual(out.title, 'Replica me');
|
|
152
|
+
assert.ok(/read-only replica; card lives on node node-A/.test(out.replica_notice));
|
|
153
|
+
assert.ok(/task-api\.js pull /.test(out.replica_notice));
|
|
154
|
+
for (const args of [['move', t.id, 'active'], ['update', t.id, '--title', 'x'], ['complete', t.id]]) {
|
|
155
|
+
const r = await cli(b.root, args);
|
|
156
|
+
assert.notStrictEqual(r.code, 0, args.join(' '));
|
|
157
|
+
assert.ok(/read-only replica; card lives on node node-A/.test(r.stderr + r.stdout), args.join(' ') + ': ' + r.stderr);
|
|
158
|
+
}
|
|
159
|
+
const files = fs.readdirSync(path.join(b.root, '.kenaz', 'tasks'), { recursive: true }).filter(f => /TASK_/.test(String(f)));
|
|
160
|
+
assert.strictEqual(files.length, 0, 'no broken local file: ' + files.join(','));
|
|
161
|
+
// truly unknown id still says not found
|
|
162
|
+
const miss = await cli(b.root, ['get', 'TASK_424242']);
|
|
163
|
+
assert.notStrictEqual(miss.code, 0);
|
|
164
|
+
assert.ok(/任務不存在/.test(miss.stderr));
|
|
165
|
+
relay.server.close();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('status changes on the owner keep the replica current', async () => {
|
|
169
|
+
const relay = await startRelay();
|
|
170
|
+
const { api } = mkProject(cfg(relay.url));
|
|
171
|
+
const t = await api.createTask(card());
|
|
172
|
+
await api.moveTask(t.id, 'active');
|
|
173
|
+
assert.strictEqual(relay.state.rows.get(t.id).status, 'in_progress');
|
|
174
|
+
await api.updateTask(t.id, { title: 'Renamed' });
|
|
175
|
+
assert.strictEqual(relay.state.rows.get(t.id).title, 'Renamed');
|
|
176
|
+
relay.server.close();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('pull imports a local copy marked with origin node', async () => {
|
|
180
|
+
const relay = await startRelay();
|
|
181
|
+
const a = mkProject(cfg(relay.url));
|
|
182
|
+
const t = await a.api.createTask(card({ alias: '791-18' }));
|
|
183
|
+
const b = mkProject(cfg(relay.url).replace('node-A', 'node-B'));
|
|
184
|
+
const r = await cli(b.root, ['pull', t.id]);
|
|
185
|
+
assert.strictEqual(r.code, 0, r.stderr);
|
|
186
|
+
const local = await b.api.getTask(t.id);
|
|
187
|
+
assert.ok(local, 'local copy exists');
|
|
188
|
+
assert.strictEqual(local.origin_node, 'node-A');
|
|
189
|
+
assert.strictEqual(local.title, 'Replica me');
|
|
190
|
+
assert.strictEqual(local.alias, '791-18');
|
|
191
|
+
const again = await cli(b.root, ['pull', t.id]);
|
|
192
|
+
assert.notStrictEqual(again.code, 0);
|
|
193
|
+
relay.server.close();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('alias: create/update/get/search resolve to the TASK id', async () => {
|
|
197
|
+
const { api, root } = mkProject(null);
|
|
198
|
+
const t = await api.createTask(card({ alias: '791-17' }));
|
|
199
|
+
const g = await cli(root, ['get', '791-17']);
|
|
200
|
+
assert.strictEqual(g.code, 0, g.stderr);
|
|
201
|
+
assert.strictEqual(JSON.parse(g.stdout).id, t.id);
|
|
202
|
+
const s = await cli(root, ['search', '791-17']);
|
|
203
|
+
assert.ok(s.stdout.includes(t.id));
|
|
204
|
+
const u = await cli(root, ['update', t.id, '--alias', '791-99']);
|
|
205
|
+
assert.strictEqual(u.code, 0, u.stderr);
|
|
206
|
+
const g2 = await cli(root, ['get', '791-99']);
|
|
207
|
+
assert.strictEqual(JSON.parse(g2.stdout).id, t.id);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('get by alias of a replica that exists only on the relay', async () => {
|
|
211
|
+
const relay = await startRelay();
|
|
212
|
+
const a = mkProject(cfg(relay.url));
|
|
213
|
+
const t = await a.api.createTask(card({ alias: '791-20' }));
|
|
214
|
+
const b = mkProject(cfg(relay.url).replace('node-A', 'node-B'));
|
|
215
|
+
const g = await cli(b.root, ['get', '791-20']);
|
|
216
|
+
assert.strictEqual(g.code, 0, g.stderr);
|
|
217
|
+
assert.strictEqual(JSON.parse(g.stdout).id, t.id);
|
|
218
|
+
relay.server.close();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
(async () => {
|
|
222
|
+
let failed = 0;
|
|
223
|
+
for (const [name, fn] of tests) {
|
|
224
|
+
try { await fn(); console.log(` ok ${name}`); }
|
|
225
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e && e.stack || e}`); }
|
|
226
|
+
}
|
|
227
|
+
console.log(`\n${tests.length - failed}/${tests.length} passed`);
|
|
228
|
+
process.exit(failed ? 1 : 0);
|
|
229
|
+
})();
|