Learning Curator 3.1 — 規格 vs 實作審查報告

日期:2026-05-21(PR-A 修正 2026-05-22) · 分支:feat/learning-curator-pivot · 審查範圍:Layer 0-8 規格 vs 實作 · 修正分支:fix/learning-orphan-eval-test (PR #46) · Spec realignment:docs/learning-curator-pr-a-spec-realignment (PR-A)

TL;DR

檢查事項列表

我把 9 個 layer 的規格逐一對照實作(每 layer 都讀規格 + 讀對應的 code + 跑必要的指令)。下面是逐項清單。

Layer 0 — Enablement / Scope Gate

狀態檢查項目證據
PASSshouldObserve() 串接 eval-trial skip + ARCFORGE_OBSERVE_SKIP_PATHShooks/observe/main.js:75-88
PASSEval-trial 路徑 regex (segment + suffix)main.js:57-58 Decision 7
PASSDisabled-by-default — defaultScopeConfig() 回傳 enabled: falsescripts/lib/learning.js:75-76
PASSLayer 0 不持久化任何東西(fail-closed)未發現 fs.write
DRIFT 接受沒有 ARCFORGE_SKIP_OBSERVE=1 自迴避 guard(daemon self-loop)arcforge dev repo 因 plugin disabled 不會觸發;其他環境風險低

Layer 1 — Observation Collection

狀態檢查項目證據
已修正持久化記錄缺 schema_version: 1source: { collector, phase }PR #46 已補上
PASSSkeleton 必填欄位(ts/event/session/project/project_id/tool)main.js:405-412
PASS不持久化原始 tool_input全部走 buildObservedEvidence
PASSSkill args 不持久化(只記 skill name)extractSkillName
PASSPostToolUse 只記 outcome + output_bytesmain.js:439-444
DRIFT 接受頂層 observation.skill 在 pre + post 都設(規格只描述 tool_start)低影響,移除可能破壞消費者

Layer 2 — Sanitization + Derived Semantic View

狀態檢查項目證據
PASSEVIDENCE_STATUS 凍結常數 + 全部 4 個值sanitize-observation.js:174-179
PASSomitted_no_input vs omitted_safety 語意正確classifyOmission + 每個 per-tool branch
PASSDecision 5 全部 keyword + value form 覆蓋(14 regex / 42 tests)sanitize-observation.js:60-94
PASSPer-tool persistence contract(Bash/Read/Edit/Write/Grep/Glob/NotebookEdit/Skill/Web/PostToolUse)SafeEvidencePatch 每個分支
PASSoperation_kind 欄位名(PR #31 reconcile 1.1)全部正確
PASSsummarizeToolInput 是 read-time only(不持久化)learning-observation-view.js 純函式
PASSFail-closed:raw fallback 不允許outer catch fallback 用 OMITTED_NO_INPUT

Layer 3 — Curator Batch Assembly

狀態檢查項目證據
PASSOne-way — 不讀 Layer 5-8 狀態batch-assembler.js 沒有相關 import
PASSCuratorBatchManifest 持久化到正確路徑~/.arcforge/learning/curator-batches/<id>.manifest.json
PASSbatch_idbatch_hash 格式正確SHA-256 truncated
PASSSafety metadata 全部 raw_*_included = falsebatch-assembler.js:426-432
PASSAtomic write(atomicWriteFile避免半寫入 manifest 毒化 Layer 4
BLOCKERReflect + Recall 來源完全沒讀(規格 line 49-56 列為 approved input)max_reflections: 0, max_recalls: 0 硬寫死
DRIFTDiaries 是 raw string 不是 DiaryEvidenceItem(沒有 evidence_id,Layer 4 沒法 cite)readRecentDiaries() 回傳 string[]
DRIFTsource_windows.diaries 從 manifest 缺漏只填了 observations + transcript_summaries

Layer 4 — LLM Curator Analysis

狀態檢查項目證據
PASSDaemon 不直接讀 observations.jsonl只透過 batch manifest
PASSBash daemon + Node CLI 分工(plan §3)observer-daemon.sh:158,296-298
PASSbody_source: "llm_curator" 三層強制(prompt / record builder / validator)PR #31 reconcile 1.2 全部到位
PASSFirst-slice allowed_artifact_types: ["instinct"]observer-prompt.md:18-20
PASSCuratorRunManifest 每次必寫(含失敗路徑)atomic + 9 個 parse_status branch
PASS失敗模式(timeout/transport_error/malformed_json/empty)都不會建 queue state每個 fail branch 都 early return
PASSsanitizer_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 修正

Layer 5 — Candidate Queue + Lifecycle

狀態檢查項目證據
PASSvalidateCandidateV1 覆蓋全 spec 必填欄位schema.js:140-479
PASSbody_source 4-value enumPR #31 reconcile 1.2
PASSpromoted_from_* 在 scope 上會被 rejectPR #31 reconcile 1.3
PASSREJECTION_CODES 完整 18-code canonical unionPR #31 reconcile 1.4
PASSAction × Status matrix 跟規格表完全一致(8×6)PR #31 reconcile 1.5 + Slice G 加的 deactivate 欄
PASSapplyTransition 對 promote/evolve 會 throwcandidate-producing actions
PASSevidence_quality v1 只用 project_obs_countPR #31 reconcile 1.11
PASSQueue + rejections + lock 路徑正確 + sanitizer 套用PR #31 reconcile 1.9
PASS事件 replay 容忍 corrupted trailing linetry/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
BLOCKERevidence_ref_omitted_upstream rejection code 定義了但 never emit(Layer 5 從來不去讀上游 evidence_status)詳見 Blocker #2
DRIFTdedupe.dedupe_basis ad-hoc keys vs spec 規範 shape沒做主動 dedup enforcement
DRIFTevidence_quality_metadata.rule_version 跟 sanitizer policy version 混用同個值是兩個不同的版本空間

Layer 6 — Dashboard Review Control Plane

狀態檢查項目證據
PASSDashboardCandidateCard 全部必填欄位(包括 PR #31 reconcile 1.5 的 available_actions)learning-dashboard.js:113-136
PASSPrivacy invariant — 對抗 4 種 fixture(API key / Bearer / JWT / private key)都 redactedtests:247-307
PASSproject_id 從 wire model 剔除buildCardScope
PASSServer-side enforce Action × Status matrix(用 Slice D 的 isLegalActionhandleDashboardAction:266-268
PASSactions.jsonl audit log(accept + reject 都記)多處 writeAuditEntry
PASSLayer 6 不 call LLM、不寫 skill、不寫 CLAUDE.md無相關 import
PASSPromote 建新 global candidate,source status 不變跟 Slice D applyTransition rule 一致
PASSToken-gated POST(24-byte random token)hasDashboardWriteHeader
DRIFT 接受Wire model 沒輸出 evidence_quality_chip / relationshipsFirst-slice UI 沒用到
DRIFT 接受Request envelope 丟掉 expected_current_status / actor / safety_ack / reasonFirst-slice 簡化(樂觀並發)
DRIFT 接受Detail view 缺 evidence_summaries / llm_assessment / materialization / activation blocksFirst-slice 只給 rationale + body_preview

Layer 7 — Materialization

狀態檢查項目證據
PASS只接受 approved + deactivated(matrix 一致)materialize.js:327-330
PASS只寫 inactive draft — 不碰 active skill/instinct 路徑draft root containment check
PASSPath-containment 防止逃逸materialize.js:399-403
PASSMaterializationRecord 先寫完再回報 Layer 5atomicWriteFile 在 transition event 之前
PASSAtomic + lock-protectedatomicWriteFile + draft lock
PASSIdempotent on (candidate_hash, render_policy_version)findExistingMaterialization
PASSBody secret scan(strict equality check)redactObservationText 比對
DRIFT 接受render_policy.include_evidence_summaries: false 硬寫死First-slice rendering policy

Layer 8 — Activation / Runtime Influence Surface

狀態檢查項目證據
PASS只接受 materialized + deactivatedactivate.js:229-232
PASSMaterialization record 完整性驗證(hash check)activate.js:235-237
PASSActivate 路徑要求 reviewer_ackactivate.js:240-243
PASSPer-target-kind overwrite policy(instinct=supersede_with_backup, 其他=forbidden)PR #31 reconcile 1.10
PASSclaude_md_addition 不會 auto-apply(雙層 block)FIRST_SLICE_TARGET_KINDS + 顯式 check
PASSAllowed roots 顯式 allowlist + path-resolve containment checkactivate.js:38-54,285-300
PASSAtomic write + lock + supersede 時備份.backups/<id>-<ts>.md
PASSActivationRecord 先寫完再回報 Layer 5 transitionatomicWriteFile 在 appendTransitionEvent 之前
PASSSessionStart 不會 auto-load instinctsinject-context.js:271-273 有顯式 comment + e2e test
PASS失敗不會建 activated event所有 fail() 在 transition 之前 return
BLOCKERdeactivate() 不驗證 reviewer_ack(spec 要求 activate AND deactivate 都要)詳見 Blocker #3
DRIFTactive_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」)功能等價

Blockers(需要你決定)

下面這些是真實的 spec drift,但修正會動到資料 shape 或行為,不適合 orchestrator 自動處理。請逐項判斷要不要修。

Blocker #1 — Layer 5 接受的 candidate safety metadata 缺欄位 需要決定

檔案:scripts/lib/learning-curator/proposal-ingestor.js:167-174

規格要求(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 測試會更新。

Blocker #2 — evidence_ref_omitted_upstream 定義了但 never emit 需要決定

檔案:scripts/lib/learning-curator/proposal-ingestor.js evidence ref 驗證段

規格要求(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。

Blocker #3 — Layer 8 deactivate() 不驗證 reviewer_ack 需要決定

檔案:scripts/lib/learning-curator/activate.js:423-462

規格要求(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。

Blocker #4 — Layer 3 reflect/recall 來源未讀;diaries 不是 typed evidence 需要決定

檔案:scripts/lib/learning-curator/batch-assembler.js

規格要求(layer-3 spec 第 49-56 行):Approved evidence sources 包含 observation / diary / reflect / recall / transcript_summary,每筆要有 evidence_id + evidence_type + source_ref 以便 Layer 4 cite。

實作現況

影響: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)。

已修正項目(PR #46)

項目檔案說明
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。

接受為 first-slice drift(已記錄、不處理)

整體 verdict

實作 v.s. 規格整體 PASS 程度約 90%

Critical path 沒有 P0 bug。Blockers #1 跟 #2 是「未來會默默降質」的型,不是「今天會炸」。Blocker #3 是 UX 決策。Blocker #4 是 v1 scope 決策。

建議的下一步

  1. Review + merge PR #46(3 個低風險修正)— 在 merge 後 main 是 spec-clean 的。
  2. 對 4 個 blockers 逐項判斷:
  3. 選定要處理的 blockers → 我可以另開 follow-up PR 實作(不在這個審查週期內,避免 scope 失控)。