我把 9 個 layer 的規格逐一對照實作(每 layer 都讀規格 + 讀對應的 code + 跑必要的指令)。下面是逐項清單。
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | shouldObserve() 串接 eval-trial skip + ARCFORGE_OBSERVE_SKIP_PATHS | hooks/observe/main.js:75-88 |
| PASS | Eval-trial 路徑 regex (segment + suffix) | main.js:57-58 Decision 7 |
| PASS | Disabled-by-default — defaultScopeConfig() 回傳 enabled: false | scripts/lib/learning.js:75-76 |
| PASS | Layer 0 不持久化任何東西(fail-closed) | 未發現 fs.write |
| DRIFT 接受 | 沒有 ARCFORGE_SKIP_OBSERVE=1 自迴避 guard(daemon self-loop) | arcforge dev repo 因 plugin disabled 不會觸發;其他環境風險低 |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| 已修正 | 持久化記錄缺 schema_version: 1 跟 source: { collector, phase } | PR #46 已補上 |
| PASS | Skeleton 必填欄位(ts/event/session/project/project_id/tool) | main.js:405-412 |
| PASS | 不持久化原始 tool_input | 全部走 buildObservedEvidence |
| PASS | Skill args 不持久化(只記 skill name) | extractSkillName |
| PASS | PostToolUse 只記 outcome + output_bytes | main.js:439-444 |
| DRIFT 接受 | 頂層 observation.skill 在 pre + post 都設(規格只描述 tool_start) | 低影響,移除可能破壞消費者 |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | EVIDENCE_STATUS 凍結常數 + 全部 4 個值 | sanitize-observation.js:174-179 |
| PASS | omitted_no_input vs omitted_safety 語意正確 | classifyOmission + 每個 per-tool branch |
| PASS | Decision 5 全部 keyword + value form 覆蓋(14 regex / 42 tests) | sanitize-observation.js:60-94 |
| PASS | Per-tool persistence contract(Bash/Read/Edit/Write/Grep/Glob/NotebookEdit/Skill/Web/PostToolUse) | SafeEvidencePatch 每個分支 |
| PASS | operation_kind 欄位名(PR #31 reconcile 1.1) | 全部正確 |
| PASS | summarizeToolInput 是 read-time only(不持久化) | learning-observation-view.js 純函式 |
| PASS | Fail-closed:raw fallback 不允許 | outer catch fallback 用 OMITTED_NO_INPUT |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | One-way — 不讀 Layer 5-8 狀態 | batch-assembler.js 沒有相關 import |
| PASS | CuratorBatchManifest 持久化到正確路徑 | ~/.arcforge/learning/curator-batches/<id>.manifest.json |
| PASS | batch_id 跟 batch_hash 格式正確 | SHA-256 truncated |
| PASS | Safety metadata 全部 raw_*_included = false | batch-assembler.js:426-432 |
| PASS | Atomic write(atomicWriteFile) | 避免半寫入 manifest 毒化 Layer 4 |
| BLOCKER | Reflect + Recall 來源完全沒讀(規格 line 49-56 列為 approved input) | max_reflections: 0, max_recalls: 0 硬寫死 |
| DRIFT | Diaries 是 raw string 不是 DiaryEvidenceItem(沒有 evidence_id,Layer 4 沒法 cite) | readRecentDiaries() 回傳 string[] |
| DRIFT | source_windows.diaries 從 manifest 缺漏 | 只填了 observations + transcript_summaries |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | Daemon 不直接讀 observations.jsonl | 只透過 batch manifest |
| PASS | Bash daemon + Node CLI 分工(plan §3) | observer-daemon.sh:158,296-298 |
| PASS | body_source: "llm_curator" 三層強制(prompt / record builder / validator) | PR #31 reconcile 1.2 全部到位 |
| PASS | First-slice allowed_artifact_types: ["instinct"] | observer-prompt.md:18-20 |
| PASS | CuratorRunManifest 每次必寫(含失敗路徑) | atomic + 9 個 parse_status branch |
| PASS | 失敗模式(timeout/transport_error/malformed_json/empty)都不會建 queue state | 每個 fail branch 都 early return |
| PASS | sanitizer_module + policy version 記錄在 CuratorPromptPolicy metadata(layer-4 lines 109-110, 124-135) | Spec 要求記在 metadata(已 wired),未要求印在 prompt 文字內 — 2026-05-21 audit 措辭誤導,2026-05-22 PR-A 修正 |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | validateCandidateV1 覆蓋全 spec 必填欄位 | schema.js:140-479 |
| PASS | body_source 4-value enum | PR #31 reconcile 1.2 |
| PASS | promoted_from_* 在 scope 上會被 reject | PR #31 reconcile 1.3 |
| PASS | REJECTION_CODES 完整 18-code canonical union | PR #31 reconcile 1.4 |
| PASS | Action × Status matrix 跟規格表完全一致(8×6) | PR #31 reconcile 1.5 + Slice G 加的 deactivate 欄 |
| PASS | applyTransition 對 promote/evolve 會 throw | candidate-producing actions |
| PASS | evidence_quality v1 只用 project_obs_count | PR #31 reconcile 1.11 |
| PASS | Queue + rejections + lock 路徑正確 + sanitizer 套用 | PR #31 reconcile 1.9 |
| PASS | 事件 replay 容忍 corrupted trailing line | try/catch + continue |
| 已修正 | LIFECYCLE_STATUSES 重複定義 | PR #46 改成 import from lifecycle.js |
| BLOCKER | 接受的 candidate safety metadata 缺 spec 必填欄位(validator_version / sanitizer_policy_version / secret_scan / activation_claim_scan / file_write_claim_scan) | proposal-ingestor.js:167-174 — 詳見 Blocker #1 |
| BLOCKER | evidence_ref_omitted_upstream rejection code 定義了但 never emit(Layer 5 從來不去讀上游 evidence_status) | 詳見 Blocker #2 |
| DRIFT | dedupe.dedupe_basis ad-hoc keys vs spec 規範 shape | 沒做主動 dedup enforcement |
| DRIFT | evidence_quality_metadata.rule_version 跟 sanitizer policy version 混用同個值 | 是兩個不同的版本空間 |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | DashboardCandidateCard 全部必填欄位(包括 PR #31 reconcile 1.5 的 available_actions) | learning-dashboard.js:113-136 |
| PASS | Privacy invariant — 對抗 4 種 fixture(API key / Bearer / JWT / private key)都 redacted | tests:247-307 |
| PASS | project_id 從 wire model 剔除 | buildCardScope |
| PASS | Server-side enforce Action × Status matrix(用 Slice D 的 isLegalAction) | handleDashboardAction:266-268 |
| PASS | actions.jsonl audit log(accept + reject 都記) | 多處 writeAuditEntry |
| PASS | Layer 6 不 call LLM、不寫 skill、不寫 CLAUDE.md | 無相關 import |
| PASS | Promote 建新 global candidate,source status 不變 | 跟 Slice D applyTransition rule 一致 |
| PASS | Token-gated POST(24-byte random token) | hasDashboardWriteHeader |
| DRIFT 接受 | Wire model 沒輸出 evidence_quality_chip / relationships | First-slice UI 沒用到 |
| DRIFT 接受 | Request envelope 丟掉 expected_current_status / actor / safety_ack / reason | First-slice 簡化(樂觀並發) |
| DRIFT 接受 | Detail view 缺 evidence_summaries / llm_assessment / materialization / activation blocks | First-slice 只給 rationale + body_preview |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | 只接受 approved + deactivated(matrix 一致) | materialize.js:327-330 |
| PASS | 只寫 inactive draft — 不碰 active skill/instinct 路徑 | draft root containment check |
| PASS | Path-containment 防止逃逸 | materialize.js:399-403 |
| PASS | MaterializationRecord 先寫完再回報 Layer 5 | atomicWriteFile 在 transition event 之前 |
| PASS | Atomic + lock-protected | atomicWriteFile + draft lock |
| PASS | Idempotent on (candidate_hash, render_policy_version) | findExistingMaterialization |
| PASS | Body secret scan(strict equality check) | redactObservationText 比對 |
| DRIFT 接受 | render_policy.include_evidence_summaries: false 硬寫死 | First-slice rendering policy |
| 狀態 | 檢查項目 | 證據 |
|---|---|---|
| PASS | 只接受 materialized + deactivated | activate.js:229-232 |
| PASS | Materialization record 完整性驗證(hash check) | activate.js:235-237 |
| PASS | Activate 路徑要求 reviewer_ack | activate.js:240-243 |
| PASS | Per-target-kind overwrite policy(instinct=supersede_with_backup, 其他=forbidden) | PR #31 reconcile 1.10 |
| PASS | claude_md_addition 不會 auto-apply(雙層 block) | FIRST_SLICE_TARGET_KINDS + 顯式 check |
| PASS | Allowed roots 顯式 allowlist + path-resolve containment check | activate.js:38-54,285-300 |
| PASS | Atomic write + lock + supersede 時備份 | .backups/<id>-<ts>.md |
| PASS | ActivationRecord 先寫完再回報 Layer 5 transition | atomicWriteFile 在 appendTransitionEvent 之前 |
| PASS | SessionStart 不會 auto-load instincts | inject-context.js:271-273 有顯式 comment + e2e test |
| PASS | 失敗不會建 activated event | 所有 fail() 在 transition 之前 return |
| BLOCKER | deactivate() 不驗證 reviewer_ack(spec 要求 activate AND deactivate 都要) | 詳見 Blocker #3 |
| DRIFT | active_path_summary(audit 誤稱 target_path_summary)含 project_id,2026-05-22 PR-A codify redaction rule 進 Layer 8 spec;PR-B 同步 impl 改 hash 化 | activate.js:345 |
| DRIFT 接受 | Hash verify 順序(先讀 draft 再 hash vs spec 「verify before reading」) | 功能等價 |
下面這些是真實的 spec drift,但修正會動到資料 shape 或行為,不適合 orchestrator 自動處理。請逐項判斷要不要修。
safety metadata 缺欄位 需要決定規格要求(layer-5 spec 第 937-963 行):每筆接受的 candidate safety 區塊要記錄 validator_version, sanitizer_policy_version, sanitizer_module, secret_scan {status, rule_version}, activation_claim_scan, file_write_claim_scan,以及 6 個 raw_*_included: false flags。
實作現況:sanitizer 確實有套用(queue-writer.js:134-153),但接受的 candidate 只記 6 個 raw flags,其他 provenance 欄位通通沒寫。Validator 不檢查這些欄位(schema 允許缺)所以驗證會通過。
影響:PR #31 reconcile 1.9 講的「Layer 4↔5 共用 sanitizer、兩邊記同個 rule_version」契約 — 從 persisted artifacts 無法重建。未來做安全稽核、回放、版本升級時,你拿到的 candidate 不知道是用哪個 sanitizer 版本驗的。
修法:proposal-ingestor.js 接受的 candidate 在 safety block 加上完整欄位,從 SANITIZER_POLICY_VERSION 跟新增的 VALIDATOR_VERSION 取值。
風險:改了 candidate 持久化 shape,所有未來新增的 candidate 都會多帶這些欄位。不影響既有 queue 因為是 additive,但 dashboard 跟 materialize 如果有 snapshot 測試會更新。
evidence_ref_omitted_upstream 定義了但 never emit 需要決定規格要求(PR #31 reconcile 1.4 + layer-5 spec 第 257-268 行):當 proposal cite 的 evidence_id 對應到 batch 裡 evidence_status ≠ "present" 的 item 時,要 reject with evidence_ref_omitted_upstream。
實作現況:Code 只 check evidence_id 是否存在於 batch(evidence_ref_missing),但從不讀那個 item 的 evidence_status。所以如果 Layer 3 batch 含有 omitted-upstream 項目而 LLM cite 了它,會被當成 valid 收下來。
影響:**今天沒有 bug** — Layer 3 目前不會把 non-present 項目放進 batch(只有 'present' 的 observation 才進去)。但 Layer 3 一旦開始把 omitted 項目也帶入 batch(為了讓 LLM 知道「這裡有東西但被遮蔽了」),這個檢查就會默默失靈。
修法:proposal-ingestor.js 在驗 evidence_ref 時,從 batch manifest 讀對應 item 的 evidence_status,非 present 的就 reject with 新 code。
風險:今天 100% 不會觸發。是一個防禦欄位。但如果未來改 Layer 3 行為而忘了補這條,會在 production 默默放行不該收的 candidate。
deactivate() 不驗證 reviewer_ack 需要決定規格要求(layer-8 spec §Inputs):Activate AND Deactivate 都要 reviewer_ack。
實作現況:Activate 路徑有檢查(activate.js:240-243)。Deactivate 路徑完全沒查 — 任何送過來的 deactivate 請求都會被執行。
影響:Dashboard 的 "Deactivate" 按鈕一鍵就生效,沒有確認對話框。對 single-user dashboard 可能是 acceptable UX 簡化;但如果未來改成 multi-user 或有自動化 caller,可能會意外停用真實 active artifact。
修法:Deactivate 入口加同樣的 reviewer_ack 檢查。Dashboard 端的 deactivate POST 也要送 ack。
請判斷:(a) 維持現狀(first-slice UX 簡化);(b) 加 ack 檢查 + dashboard 加確認 modal。
規格要求(layer-3 spec 第 49-56 行):Approved evidence sources 包含 observation / diary / reflect / recall / transcript_summary,每筆要有 evidence_id + evidence_type + source_ref 以便 Layer 4 cite。
實作現況:
max_reflections: 0, max_recalls: 0 硬寫死 — reflect 跟 recall 完全沒讀readRecentDiaries() 讀成 string[],塞到 prompt 文字但沒包裝成 DiaryEvidenceItem — Layer 4 沒法用 evidence_id citesource_windows.diaries 從 manifest 缺漏影響:Layer 4 的 curator 可以看到 diary 內容(透過 prompt),但 proposal 沒法用 evidence_id 引用。所以 candidate 的 evidence[] 永遠只有 observation 來源。降低了 candidate 的訊號品質。
請判斷:(a) first-slice 接受 — diary/reflect/recall 全部留到 v2;(b) 把 diary 改成 typed evidence;(c) 完整支援三個來源(含 evidence_id)。
| 項目 | 檔案 | 說明 |
|---|---|---|
| Orphan pytest 測試 | tests/skills/test_optional_learning_release_eval.py |
Slice I.3a (#43) 刪了 scenario 但沒刪驗證它的 test — 每次 npm test 直接炸 FileNotFoundError。已刪除。 |
| Layer 1 ObservationSkeleton 缺欄位 | hooks/observe/main.js |
加上 schema_version: 1 + source: { collector, phase }。Additive,207 hook tests 全綠。 |
| LIFECYCLE_STATUSES 重複定義 | scripts/lib/learning-curator/schema.js |
從 lifecycle.js import 替代本地 8-string array。Single source of truth。 |
evidence_quality_chip / relationships — 第一版 UI 沒消費expected_current_status / actor / safety_ack / reason)— 樂觀並發 first-slice OKevidence_summaries / llm_assessment / materialization / activation blocks — 只給 rationale + body_previewrender_policy.include_evidence_summaries 硬寫 false — first-slice rendering policyobservation.skill 在 pre + post 都設 — 規格只描述 tool_start,但移除會破壞既有消費者ARCFORGE_SKIP_OBSERVE=1 daemon 自迴避 guard — arcforge dev repo 已 plugin disabled,其他環境風險低sanitizer_module + version — sanitizer 確實 wired,只是 prompt 文字沒提dedupe_basis ad-hoc keys;無主動 dedup enforcement — first-slice 沒有用戶會手動建重複的 candidateevidence_quality_metadata.rule_version 跟 sanitizer policy version 混用 — 命名空間混亂但目前無 user-visible bug實作 v.s. 規格整體 PASS 程度約 90%。
Critical path 沒有 P0 bug。Blockers #1 跟 #2 是「未來會默默降質」的型,不是「今天會炸」。Blocker #3 是 UX 決策。Blocker #4 是 v1 scope 決策。