@kylecheng3146/agent-ops 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/packages/cli/src/args.js +8 -43
  2. package/dist/packages/cli/src/bin.js +28 -29
  3. package/dist/packages/cli/src/cli.js +12 -14
  4. package/dist/packages/cli/src/codex-loop-process.js +0 -6
  5. package/dist/packages/cli/src/commands/review.js +5 -10
  6. package/dist/packages/cli/src/commands/run.js +3 -1
  7. package/dist/packages/cli/src/commands/task.js +6 -50
  8. package/dist/packages/cli/src/commands/verify.js +7 -3
  9. package/dist/packages/cli/src/default-base.js +26 -0
  10. package/dist/packages/cli/src/hook-process.js +19 -83
  11. package/dist/packages/cli/src/run-entry.js +66 -21
  12. package/dist/runtime/src/adapters/agy/config.js +2 -3
  13. package/dist/runtime/src/adapters/claude/config.js +2 -2
  14. package/dist/runtime/src/adapters/codex/config.js +2 -5
  15. package/dist/runtime/src/hooks/completion-gate.js +0 -15
  16. package/dist/runtime/src/install/harness.js +3 -3
  17. package/dist/runtime/src/install/plan.js +3 -3
  18. package/dist/runtime/src/install/profiles.js +0 -2
  19. package/dist/runtime/src/office/snapshot.js +4 -1
  20. package/dist/runtime/src/parallel/manage.js +0 -3
  21. package/dist/runtime/src/review/execute.js +68 -4
  22. package/dist/runtime/src/run/verification.js +2 -2
  23. package/dist/runtime/src/schema/validate.js +10 -5
  24. package/dist/runtime/src/task/completion.js +4 -42
  25. package/dist/runtime/src/task/contract.js +11 -0
  26. package/dist/runtime/src/task/service.js +34 -57
  27. package/dist/runtime/src/task/store.js +3 -7
  28. package/dist/runtime/src/verify/acceptance-coverage.js +2 -3
  29. package/dist/runtime/src/verify/acceptance-protocol.js +4 -3
  30. package/dist/runtime/src/verify/acceptance-replay.js +4 -7
  31. package/dist/runtime/src/verify/service.js +6 -23
  32. package/dist/runtime/src/verify/source-fingerprint.js +17 -4
  33. package/docs/en/guides/task-debug.md +39 -133
  34. package/docs/en/spec/acceptance-and-evidence.md +31 -23
  35. package/docs/en/spec/harness-adapters.md +9 -11
  36. package/docs/en/spec/loop-engineering.md +11 -5
  37. package/docs/zh-TW/guides/task-debug.md +27 -73
  38. package/docs/zh-TW/spec/acceptance-and-evidence.md +18 -18
  39. package/docs/zh-TW/spec/harness-adapters.md +7 -8
  40. package/docs/zh-TW/spec/loop-engineering.md +9 -4
  41. package/package.json +1 -1
  42. package/schemas/config.schema.json +1 -21
  43. package/dist/packages/cli/src/debug-intake.js +0 -245
  44. package/dist/runtime/src/task/debug-intake.js +0 -15
  45. package/dist/runtime/src/task/debug-state.js +0 -135
  46. package/dist/runtime/src/task/debug-user-event.js +0 -160
  47. package/dist/runtime/src/task/debug.js +0 -632
@@ -12,10 +12,11 @@
12
12
 
13
13
  - `behavioral`:指定 red checks 在 baseline 都因斷言失敗,candidate 的必要 checks 全部通過。`invariant`:candidate 的必要 checks 通過,baseline 原本 PASS 有效。`review-only`:兩輪 fresh review 對照原始目標驗收,不宣稱機械證明。
14
14
  - repo 授權的 `verification.acceptanceRunners` 定義 argv、setup、產品與 test build。task 提供穩定 check IDs 與明列且已 commit 的 test/fixture/helper;兩側各自安裝並 build 其版本,再覆蓋 candidate 材料。task 不能改 runner 命令。
15
- - generic runner 輸出的 check IDs 必須恰好等於共用相同 runner、baseline 與材料之 criteria 所要求的 `checkIds` 聯集;多出的列為 UNKNOWN。若 runner 固定輸出整套 checks,就綁定完整輸出集合,並將 `redCheckIds` 限於實際回歸斷言。
15
+ - generic runner 必須輸出共用相同 runner、baseline 與材料之 criteria 所要求的每個 `checkId`,且各恰好一次;缺少或重複為 UNKNOWN。其他列會被忽略,因此 binding 可以只列 red checks。未要求的列若失敗,runner 的 exit code 仍與 PASS 彙總矛盾,candidate 會成為 UNKNOWN,絕不會 green。
16
16
  - fixture、collection、build、timeout、取消、輸出截斷、缺少或重複 ID、retry、skip MUST NOT 算 red 或 green。完整 execution artifacts 以摘要綁定契約、材料與 commits。
17
17
  - 原生 Jest JSON 與 Vitest task errors 無法完整區分 test-call 斷言與 hook/fixture cleanup。除非已 review 的 repo collector 提供明確斷言分類,失敗列 MUST 保持 UNKNOWN;AssertionError 名稱、matcher 資料與 pass 的 hook 狀態都不足。green 需要明確的單次 attempt 證據:attempts、Jest invocations,或 Vitest retryCount 與 repeatCount 兩者。原生 Vitest JSON 省略這些 counters,不能單獨提供機械證明。pytest 每個 nodeid 必須有一組依序的 setup/call/teardown;Rust protocol 必須提供 attempts。缺 metadata MUST NOT 預設為一次成功。
18
- - baseline 固定為建立 commit;finding pin 使用被 FAIL 的 candidate。revise/replan 保留原始目標、全部映射要求與先前契約/證據。新契約必須重新取證並取得兩輪 fresh review。
18
+ - baseline 為建立 commit;finding pin 使用被 FAIL 的 candidate。`task revise --baseline <ref>` 會一次把所有 typed criterion 的 baseline(以及 task 的建立 commit)移到 HEAD 的某個祖先 commit,例如 task 開在錯的分支時,並使其驗證失效。revise/replan 保留原始目標、全部映射要求與先前契約/證據。
19
+ - verification evidence 只綁定 criteria 的 execution hash。只改 surface 或 origin 的 revise 會保留它;改 criteria 或設定則使其失效。0.11 之前的證據綁定完整 contract hash,契約未變時仍相符。每個新契約都需要兩輪 fresh review。
19
20
  - `task pin-finding` 驗證已保存 report 的 digest/index 來源。recurrence 明確引用既有 pin。第六條回傳 replan 義務且不修改狀態;映射後必須保留 pin 與全部要求。
20
21
  - reviewer packet 包含原始目標、修訂歷史、coverage 與 verifier 材料,審查斷言本身與明確的 fallback 理由。機械 green 不會省略必要 policy commands 或任一 fresh review。
21
22
 
@@ -47,7 +48,7 @@ English source version: 2026-10-20. Revalidate: when the English specification c
47
48
 
48
49
  每個新 task MUST 在寫驗收條件前記錄變更還會影響哪些位置(surface audit);discovery 找到未分類位置時,review MUST NOT 執行。
49
50
 
50
- - Trigger: 建立 task(`task create`、run plan,或 DEBUG task 第一次修復之前)、修訂 surface,或審查 task。
51
+ - Trigger: 建立 task(`task create`,含 `--debug`,或 run plan)、修訂 surface,或審查 task。
51
52
  - Action: 以記錄下來的固定字串查詢或 `n/a: 理由` 搜尋四個軸向,把每個命中分類為 included、excluded(glob + 理由)或 UNKNOWN,每列 surface 都寫出可觀察的 `behavior`,並宣告每個共享契約為 compatible 或 breaking。以 `task create|revise --surface-file <json>` 提交。
52
53
  - Evidence: surface 存在 task 上、綁進 contract hash 並保留修訂歷史,review 前以 `git grep -F` 重放,並以 `surface-coverage` 條件交給兩位 reviewer。
53
54
  - Positive: `diff token 命中三個複製貼上的同類頁面;review 前 audit 已把它們連同行為列入 included。`
@@ -83,7 +84,8 @@ English source version: 2026-10-20. Revalidate: when the English specification c
83
84
  至多 128 筆。Reviewer 會看到每筆排除與其命中數,若它隱藏了先前
84
85
  `SURFACE_UNCLASSIFIED` 阻擋所列的檔案則會標示出來。
85
86
  結果以 HEAD、base、diff、查詢集合與 config hash 快取。
86
- - Review 以 NOT_RUN 回報:`SURFACE_MISSING`(沒有或無法解析的 surface;非 DEBUG
87
+ - `verify` 在驗證後執行同一個 gate,並以建議性質的輸出列印結果(`Surface: PASS`,或錯誤碼與未分類路徑),讓 surface 在 review 前修好;只有 review 會強制。
88
+ - Review 以 NOT_RUN 回報:`SURFACE_MISSING`(沒有或無法解析的 surface;
87
89
  task 建立時也會拒絕)、`SURFACE_UNCLASSIFIED`、`SURFACE_QUERY_TOO_BROAD`(單一
88
90
  記錄查詢超過 200 個檔案)、`SURFACE_EXCLUSION_TOO_BROAD`(單一 glob 超過 10 個
89
91
  檔案,或包含 diff 內的檔案)、`SURFACE_DISMISSAL_TOO_BROAD`(超過 128 筆排除)、
@@ -100,17 +102,15 @@ English source version: 2026-10-20. Revalidate: when the English specification c
100
102
 
101
103
  ## EVIDENCE-ESCAPE-001
102
104
 
103
- 每個 DEBUG 修復 MUST 在 review 前宣告 bug 的來源;宣告的逃逸 MUST 以機械方式連結、標記並記錄。
105
+ bug 修復在已知來源時 SHOULD 宣告 bug 的來源;宣告的逃逸 MUST 以機械方式連結、標記並記錄。此宣告屬建議性質(advisory),絕不阻擋 finish。
104
106
 
105
- - Trigger: DEBUG 假設已確認,或一般 task 修復一個通過了先前關卡的 bug。
106
- - Action: 以 `task debug --debug-action origin --payload-file <json>` 宣告 `origin`:`{"kind": "escape-of", "commit": "<commit>", "label": "<label>"}`(可選 `"receipt"`),或 `{"kind": "pre-existing" | "external", "reason": "..."}`。一般 task 可用 `task create|revise --escape-of <commit> --escape-label <label> [--escape-receipt <id>]` 選擇加入。把 `.agent-ops-escapes/<task-id>.json` 記錄與修復一起 commit。
107
- - Evidence: `origin` 綁進 task contract hash;本規則上線後建立的 DEBUG task 沒有它時,finish、`task complete` 與 completion gate 以 `ESCAPE_ORIGIN_MISSING` 拒絕;兩位 reviewer 在 `escape-label` 條件下收到連結狀態、標記、事實與 residual risks。
108
- - Positive: `假設已確認;origin escape-of 864dbbe2 label surface;重放把漏掉的頁面標為 surface-unseen;記錄與修復一起 commit。`
109
- - Negative: `DEBUG 修復沒有 origin 就 finish,或把 surface-excluded 的漏網改標成 review,理由是 reviewer「應該注意到」。`
107
+ - Trigger: DEBUG task(`task create --debug`)或任何 task 修復一個通過了先前關卡的 bug,且已知其來源。
108
+ - Action: 以 `task create|revise --escape-of <commit> --escape-label <label> [--escape-receipt <id>]` 宣告 `origin`,DEBUG 與一般 task 相同。0.10 `task debug` 記錄的 `pre-existing` 與 `external` origin(附 `reason`)仍可讀取;未宣告的 origin 另行計數。把 `.agent-ops-escapes/<task-id>.json` 記錄與修復一起 commit。
109
+ - Evidence: `origin` 綁進 task contract hash;兩位 reviewer 在 `escape-label` 條件下收到連結狀態、標記、事實與 residual risks。沒有 origin 的 task 仍可 finish;0.10 標記 `originRequired` 的 task 不再被拒絕(`ESCAPE_ORIGIN_MISSING` 已於 0.11.0 移除)。
110
+ - Positive: `回歸在 baseline 為紅、修復後為綠;origin escape-of 864dbbe2 label surface;重放把漏掉的頁面標為 surface-unseen;記錄與修復一起 commit。`
111
+ - Negative: `把 surface-excluded 的漏網改標成 review,理由是 reviewer「應該注意到」。`
110
112
 
111
- - 宣告必須在假設確認之後(否則 `ESCAPE_ORIGIN_EARLY`),並計入 DEBUG 預算。它是一次
112
- contract revision:重新 verify、以新證據重新記錄已確認的假設,然後 review。重新宣告會
113
- 取代記錄。輸出會列出修復所改或刪行的 `git blame` 提示 commit;blame 只是提示,因為遺漏
113
+ - 宣告是一次 contract revision:重新 verify,然後 review。重新宣告會取代記錄。輸出會列出修復所改或刪行的 `git blame` 提示 commit;blame 只是提示,因為遺漏
114
114
  型 bug 存在於沒人寫過的行。
115
115
  - finish receipt 留在本機 Git common directory。Schema 2 記錄 target..candidate 的每個
116
116
  commit 與逐檔 diff fingerprint(去掉 blob id 與 hunk 位置,squash merge 後仍相同);
@@ -134,13 +134,13 @@ English source version: 2026-10-20. Revalidate: when the English specification c
134
134
 
135
135
  ## EVIDENCE-ESCAPE-002
136
136
 
137
- surface 逃逸 MUST 在修復 finish 前變成已證明、受追蹤的規則;學到的規則 MUST 只能收緊 surface gate。
137
+ surface 逃逸 SHOULD 變成已證明、受追蹤的規則;學到的規則 MUST 只能收緊 surface gate。此證明屬建議性質,絕不阻擋 finish。
138
138
 
139
139
  - Trigger: 已宣告的 escape-of origin(EVIDENCE-ESCAPE-001),或以 `task create --escape-of <commit> --escape-label <label> --escape-fix <fix-commit>` 回填的過去逃逸。
140
- - Action: 標記為 `surface`、`surface-unseen` 或 `surface-excluded` 時,commit 一條規則 `.agent-ops-escapes/rules/<rule-id>.json`,形狀為 `{"id", "escape", "label", "trigger": {"paths", "tokens"}, "expand": {"paths", "queries"}, "noExclude"}`,並重新宣告 origin。`criteria` 或 `verify` 以 `--escape-promote <runnerId:checkId>`(DEBUG payload `"promote"`)指名修復 task 由紅轉綠的 check。其他標記記錄為 lesson。
141
- - Evidence: finish、`task complete` 與 completion gate 會重跑證明,並以 `ESCAPE_RULE_UNPROVEN`、`ESCAPE_RULE_TOO_BROAD` 或 `SURFACE_BUDGET_EXCEEDED` 拒絕;ledger 記錄保存證明用規則、其內容 hash 與回測的 fires、commits 與比率。
140
+ - Action: 標記為 `surface`、`surface-unseen` 或 `surface-excluded` 時,commit 一條規則 `.agent-ops-escapes/rules/<rule-id>.json`,形狀為 `{"id", "escape", "label", "trigger": {"paths", "tokens"}, "expand": {"paths", "queries"}, "noExclude"}`,並重新宣告 origin。`criteria` 或 `verify` 以 `--escape-promote <runnerId:checkId>`(origin 欄位 `"promote"`)指名修復 task 由紅轉綠的 check。其他標記記錄為 lesson。
141
+ - Evidence: 宣告 origin 時執行證明,失敗時印出 `ESCAPE_RULE_UNPROVEN`、`ESCAPE_RULE_TOO_BROAD` 或 `SURFACE_BUDGET_EXCEEDED`;自 0.11.0 起 finish 不再因此拒絕。ledger 記錄保存證明用規則、其內容 hash 與回測的 fires、commits 與比率。
142
142
  - Positive: `逃逸 surface-unseen;規則 image-size-siblings 在被逃逸 commit 上觸發,其查詢在該 commit 命中兩個漏掉的兄弟頁面,且在最近 200 個 commit 中只觸發 2 次。`
143
- - Negative: `規則從未在被逃逸 commit 上觸發就 finish surface 逃逸,或新增可排除路徑的規則欄位。`
143
+ - Negative: `規則從未在被逃逸 commit 上觸發卻把 surface 逃逸報為已證明,或新增可排除路徑的規則欄位。`
144
144
 
145
145
  - 規則只含資料且不需要 trust:trigger 與 expand 變成 argv `git grep -F` 查詢與路徑
146
146
  glob,絕不經過 shell 或 regex。嚴格 schema 沒有任何能移除查詢、新增排除或放寬關卡的
@@ -203,7 +203,7 @@ surface 逃逸 MUST 在修復 finish 前變成已證明、受追蹤的規則;
203
203
 
204
204
  既有 task 檔案仍可直接讀取,不需資料格式遷移。缺少 config baseline、使用任意文字證據,或含有未完成但已封存子任務的舊任務,必須重建並完成驗證與審查。重複執行 complete 也會重新驗證證據;舊的 completed 狀態本身不夠。Runtime 呼叫端必須提供 `TaskServiceOptions.completion`;缺少此 repository context 時,complete 會拒絕執行,但仍可讀取任務。
205
205
 
206
- Commit 前,verify、review、complete 使用預設 worktree scope。Commit 後,三個命令都使用相同的 `--base <git-ref>`,為已提交範圍重新產生證據;runtime 呼叫端使用 `TaskServiceOptions.completion.base`。Base 模式要求 worktree 乾淨。沒有 worktree 變更又未指定 base 時,complete 會回傳 `TASK_COMPLETION_SCOPE_REQUIRED`。agy Stop gate 仍檢查 worktree scope 的證據:請在 commit 前完成並停止,或由使用者檢查已完成的 base-scope 任務後,核准一次性的 Stop permit。
206
+ Commit 前,verify、review、complete 使用預設 worktree scope。Commit 後,三個命令都使用相同的 `--base <git-ref>`,為已提交範圍重新產生證據。未指定 `--base` 且 worktree 乾淨時,verify 與 review 自動選擇:session worktree 使用與目標分支的 merge-base(finish rebase 後會跟著移動),其他情況使用 task 的建立 commit(須為 HEAD 的祖先)。Base 模式的 source fingerprint 忽略 escape ledger 的 task 記錄(`.agent-ops-escapes/task-*.json`;rules 仍納入 fingerprint),因此 commit ledger 不會使驗證與 review 失效;0.10 計算的 fingerprint 不同,升級後需重新 verify 一次;runtime 呼叫端使用 `TaskServiceOptions.completion.base`。Base 模式要求 worktree 乾淨。沒有 worktree 變更又未指定 base 時,complete 會回傳 `TASK_COMPLETION_SCOPE_REQUIRED`。agy Stop gate 仍檢查 worktree scope 的證據:請在 commit 前完成並停止,或由使用者檢查已完成的 base-scope 任務後,核准一次性的 Stop permit。
207
207
 
208
208
  這項機制強制的是 toolkit 的完成命令,無法阻止 host 直接用自然語言宣稱完成,也無法阻止具有相同檔案權限的 process 直接改寫 task/evidence 檔案;它不會鎖住 Git 或其他來源檔案寫入者。
209
209
 
@@ -101,14 +101,11 @@ Runtime-failure 處理中,只有 `command-policy` 為 fail-closed。當已安
101
101
  OpenCode `tool.execute.before` plugin 可在其支援的 Bash surface 上 throw 文件化的
102
102
  denial 或 unavailable-runtime error。Codex 維持 `unknown` 且絕不輸出 denial。
103
103
  Fixture test 只斷言這些 wire 與 plugin shape;它們不證明 host 會實際遵守 denial。
104
- `SessionStart` 與一般 `Stop` failure 維持 fail-open。active DEBUG Stop 故障則
105
- 針對 task/config 重新載入一次,再明確回報 UNKNOWN/未完成並保留原狀態,才允許非成功停止。
106
-
107
- Codex DEBUG lifecycle 語意於 2026-10-11 依[官方 hook 文件](https://learn.chatgpt.com/docs/hooks#stop)
108
- 重新檢查:Stop 的 `decision: "block"` 加 `reason` 會排程續跑;`continue: false`
109
- 會停止。`stop_hook_active` 表示已經續跑過,不能繞過 DEBUG 檢查。
110
- 原生 `Interrupt` 暫停採三秒 timeout,不得阻擋或重啟人類中斷。
111
- 此 DEBUG 專用政策不會啟用 Codex 的一般 completion gate 或 optional Stop verifier。
104
+ `SessionStart` 與 `Stop` failure 維持 fail-open。
105
+
106
+ 自 0.11.0 起沒有 DEBUG 專用 hook:prompt 不會開 DEBUG task,Stop 不會因 DEBUG 擋住
107
+ session,Codex `Interrupt` 不再註冊(舊安裝殘留的註冊在 `update` 前為 no-op)。Codex
108
+ 仍沒有一般 completion gate 或 optional Stop verifier。
112
109
  Codex 另以每個原生 hook 的精確定義保存信任。更新受管理 hook 後,必須在 `/hooks`
113
110
  審核新增或變更的定義;agent-ops repository trust 不會啟用尚未審核的原生 hook。
114
111
 
@@ -169,6 +166,8 @@ unknown)、有界且遮蔽敏感資料的原始診斷、sealed proof(`record
169
166
  僅表示紀錄存在,不代表驗證通過)、cleanup 狀態、能否 resume 與具體修復方式。
170
167
  相同 cleanup blocker 保留原始 cause。Trust、native identity、generation 與 denial
171
168
  仍 fail-closed;sealed proof 不得抹除 native 或組織的明確拒絕。
169
+ `run status` 與 `run logs` 的 JSON `state` 省略 event 歷史與 goal 全文,保留
170
+ `eventCount` 與 `goalHash`,讓重複輪詢維持精簡;event 本身由 `run logs` 回傳。
172
171
 
173
172
  人工 resume 已寫入 receipt 的 integration,必須先驗證原 sealed proof、receipt
174
173
  digest 與 transaction、owner/generation、candidate/target ancestry 及 Git note。
@@ -11,7 +11,7 @@
11
11
  - Negative: 只因 native goal completion 就把 run 標為 complete。
12
12
 
13
13
  - macOS supervisor 在啟動 goal 前保存 Claude/Codex native 身份。同 host writers 使用獨立 worktrees 與目前 generation lease;coordinator 計入最多兩個 writer。依賴以固定交付提供,child baseline 在依賴 commits 進入 checkout 後建立。
14
- - 新 run 不設總時間上限(`budget.limitMs: null`),仍以 active intervals 聯集記錄 setup、verify、review、整合的消耗。舊有限 run 僅在下次人工 resume 升級,保留舊上限、intervals、消耗、usage epochs 與 blocker 歷史;讀取或安裝不會升級或喚醒舊 run。單次命令/review timeout、provider quota、DEBUG 預算、無進度與 pause/stop 控制保持獨立有效。明確指定 --time-budget 會被拒絕。缺少計數維持 UNKNOWN,partial usage 不算完整成本。
14
+ - 新 run 不設總時間上限(`budget.limitMs: null`),仍以 active intervals 聯集記錄 setup、verify、review、整合的消耗。舊有限 run 僅在下次人工 resume 升級,保留舊上限、intervals、消耗、usage epochs 與 blocker 歷史;讀取或安裝不會升級或喚醒舊 run。單次命令/review timeout、provider quota、無進度與 pause/stop 控制保持獨立有效。明確指定 --time-budget 會被拒絕。缺少計數維持 UNKNOWN,partial usage 不算完整成本。
15
15
  - 實質疑問暫停並要求明確回答。修復保留目標與失敗紀錄;連續兩次相同失敗且無進展停止該 worker 及其依賴,獨立 task 仍可執行。review findings 在下次 final gate 前必須有回歸 pin 或記錄 review-only fallback。
16
16
  - native completion 僅是觀察。完成必須有必要驗證、兩輪 fresh review、完成的 task state、綁定 target 的 receipt。整合在移動 target 前封存 candidate,記錄 task、receipt、note、cleanup 進度;恢復驗證封存證據,不重做成功的 target mutation。不一致的 target 維持 blocked。
17
17
  - Stop 先保存禁止續跑,再確認受管 process group 死亡。crash recovery 不得取代仍存活或身份不明的 writer。dirty checkout、版本漂移、restart storm 保留現場。重開機/login 改變必須明確 resume;不修改全域 host policy。
@@ -24,9 +24,14 @@
24
24
  - 當次人工 resume 的初始恢復 MAY 忽略啟用前收到、且 native 更新秒數嚴格早於該次 resume 的本執行緒 Codex 額度快照。自動重啟、未知/同秒/未來時間、時鐘倒退,以及啟用開始後收到的所有額度事件仍有效。Native 啟用 MUST 確認 goal 為 active,才能啟動或轉向 turn。人工 resume 可解除衍生的 `RUN_POLICY_DISABLED` task 阻擋,但保留 ID、耗用、歷史及其他產品失敗。
25
25
  - 收斂判斷比較實際 per-check 狀態及未解除的 finding pins;source fingerprint、時間或診斷文字本身不能算進展。
26
26
 
27
- `jobs=1` 時,child MUST 等 coordinator 程序確認死亡後才執行。coordinator 保留
28
- task 與 native session identity,在 child 交付後以新 generation 接續。native
29
- 註冊、恢復與啟動 MUST 都限制最多一個 active agent;僅扣除 task slot 不足以符合。
27
+ 不論 `jobs` 為何,coordinator MUST 只在規劃完 child 後送出明確的 `action=wait`
28
+ request 才讓出;supervisor 不會停止仍在規劃的 coordinator。wait 會確認 coordinator
29
+ 程序死亡、釋出其 slot 並捨棄其 native thread,使 coordinator 在 child 工作期間
30
+ 不再輪詢 `run status`。恢復流程讓等待中的 coordinator 保持停止;所有 child 皆為
31
+ delivered、complete 或 blocked 後,才以新 generation 在全新 native session 中接續,
32
+ instructions 會列出每個 child 的狀態與交付 commit。`jobs=1` 時,child MUST 等該
33
+ 程序確認死亡後才執行。native 註冊、恢復與啟動 MUST 都限制最多 `jobs` 個 active
34
+ agent;僅扣除 task slot 不足以符合。
30
35
 
31
36
  English source version: 2026-07-23. Revalidate: when the English specification changes.
32
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "Evidence-driven development loops for agy, Codex, Claude Code, and opencode",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -282,27 +282,7 @@
282
282
  },
283
283
  "debug": {
284
284
  "type": "object",
285
- "additionalProperties": false,
286
- "required": [
287
- "enabled",
288
- "budgetMs",
289
- "maxRounds"
290
- ],
291
- "properties": {
292
- "enabled": {
293
- "type": "boolean"
294
- },
295
- "budgetMs": {
296
- "type": "integer",
297
- "minimum": 1,
298
- "maximum": 86400000
299
- },
300
- "maxRounds": {
301
- "type": "integer",
302
- "minimum": 1,
303
- "maximum": 100
304
- }
305
- }
285
+ "description": "Pre-0.11 DEBUG budget settings; accepted, ignored and dropped by update."
306
286
  }
307
287
  }
308
288
  },
@@ -1,245 +0,0 @@
1
- import { join } from "node:path";
2
- import { TaskService } from "../../../runtime/src/task/service.js";
3
- import { FileTaskStore } from "../../../runtime/src/task/store.js";
4
- import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
5
- import { evaluateGuardrail } from "../../../runtime/src/guardrails/evaluate.js";
6
- import { isDebugIssue } from "../../../runtime/src/task/debug-intake.js";
7
- import { ensureSessionWorktree, readWorktreeRecord, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
8
- import { listWorktrees, resumeWorktree } from "../../../runtime/src/parallel/manage.js";
9
- import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
10
- import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
11
- import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
12
- import { loadEffectiveConfig } from "./context.js";
13
- import { gitRunner, worktreeDependencies } from "./parallel-deps.js";
14
- const foregroundClockSupported = (host) => host !== "opencode";
15
- function nativeSessionId(input) {
16
- const fromInput = [input.session_id, input.sessionId, input.conversationId, input.thread_id, input.threadId]
17
- .find((value) => typeof value === "string" && value.length > 0);
18
- return fromInput ?? [process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID]
19
- .find((value) => value !== undefined && value.length > 0);
20
- }
21
- /** Start only an already attached DEBUG issue; native hooks never create work. */
22
- export async function startDebugSession(root, sessionId, host) {
23
- if (!foregroundClockSupported(host) || sessionId.length === 0)
24
- return;
25
- const config = (await loadEffectiveConfig(root, "project")).config;
26
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
27
- const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
28
- const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
29
- const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
30
- const attached = await tasks.status({ sessionId }).catch(() => null);
31
- if (attached?.status === "active" && attached.task.debug !== undefined)
32
- await tasks.debugService().foregroundStart(attached.task.id, sessionId);
33
- }
34
- export async function routeDebugPrompt(root, input, host = "claude") {
35
- if (typeof input !== "object" || input === null || Array.isArray(input))
36
- return null;
37
- const fields = input;
38
- if (typeof fields.prompt !== "string")
39
- return null;
40
- const continuation = /^(?:繼續|继续|continue)[。.!!]?$/iu.test(fields.prompt.trim());
41
- const debugIssue = isDebugIssue(fields.prompt);
42
- const config = (await loadEffectiveConfig(root, "project")).config;
43
- if (config.features.debug?.enabled !== true)
44
- return null;
45
- if (evaluateGuardrail({ kind: "content", content: fields.prompt, scope: root }).action === "block")
46
- return null;
47
- const session = nativeSessionId(fields);
48
- if (session === undefined) {
49
- if (!debugIssue && !continuation)
50
- return null;
51
- return "DEBUG intake unsupported: this hook has no reliable native session identity. Use task create --debug --goal with the original issue in the native conversation.";
52
- }
53
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(session)));
54
- const contextRoot = saved?.mainRoot === root && saved.sessionId === session ? saved.path : root;
55
- const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
56
- const original = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
57
- const attached = await original.status({ sessionId: session }).catch(() => null);
58
- if (attached?.status === "active" && attached.task.debug !== undefined) {
59
- await original.debugService().syncNativeControl(attached.task.id, session);
60
- const control = (await original.debugService().root(attached.task.id)).debug.control;
61
- if (control?.paused === true)
62
- return `DEBUG original issue ${attached.task.id} is paused by the human. Preserve its task, evidence and remaining budget; stop after saving progress. Only a native human continue event resumes it.`;
63
- if (continuation) {
64
- let message = `DEBUG continue preserved original issue ${attached.task.id}: native user event did not extend time; other limits remain authoritative.`;
65
- try {
66
- await original.debugService().extend(attached.task.id, session, "latest");
67
- message = `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
68
- }
69
- catch (error) {
70
- message = `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
71
- }
72
- if (foregroundClockSupported(host)) {
73
- try {
74
- await original.debugService().foregroundStart(attached.task.id, session);
75
- }
76
- catch (error) {
77
- message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
78
- }
79
- }
80
- else
81
- message += " Foreground clock unsupported on this host; preserve the issue and use a host with a trustworthy end-of-turn signal.";
82
- if (control?.mode === "diagnose")
83
- message += " Diagnosis-only scope remains active; continue discriminating evidence without product repairs until a proven diagnosis or runtime blocker. Only a native human resume repair event restores the full loop.";
84
- return message;
85
- }
86
- if (foregroundClockSupported(host)) {
87
- try {
88
- await original.debugService().foregroundStart(attached.task.id, session);
89
- }
90
- catch (error) {
91
- return `DEBUG original issue ${attached.task.id}: foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
92
- }
93
- }
94
- if (control?.mode === "diagnose")
95
- return `DEBUG original issue ${attached.task.id}: diagnosis only. Continue reproduction and hypothesis evidence until a supported diagnosis or a runtime blocker. Do not repair product behavior. A failure to reproduce is not a stop condition; a human resume repair event restores the full loop.`;
96
- if (!debugIssue)
97
- return null;
98
- return `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
99
- }
100
- if (!debugIssue && !continuation)
101
- return null;
102
- if (continuation) {
103
- if (attached?.status !== "active" || attached.task.debug === undefined)
104
- return null;
105
- try {
106
- await original.debugService().extend(attached.task.id, session, "latest");
107
- return `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
108
- }
109
- catch (error) {
110
- return `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
111
- }
112
- }
113
- if (attached?.status === "active")
114
- return attached.task.debug === undefined ? null : `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
115
- // Another session's worktree may already hold this issue; reusing it keeps
116
- // its consumed budget and rounds instead of starting a fresh task.
117
- const holders = await listWorktrees(worktreeDependencies(), root).catch(() => []);
118
- for (const { record: held } of holders) {
119
- if (held.sessionId === session)
120
- continue;
121
- const heldConfig = (await loadEffectiveConfig(held.path, "project").catch(() => null))?.config;
122
- if (heldConfig === undefined)
123
- continue;
124
- const issue = await new TaskService(new FileTaskStore(join(held.path, ".agent-ops/tasks/state.json"), held.path), { completion: { root: held.path, gitRunner: gitRunner(held.path), loadConfig: async () => heldConfig } }).activeDebugRoot(fields.prompt).catch(() => null);
125
- if (issue === null)
126
- continue;
127
- try {
128
- await resumeWorktree(worktreeDependencies(), { cwd: root, name: held.name, sessionId: session });
129
- }
130
- catch (error) {
131
- return `DEBUG original issue ${issue.task.id} is still active in ${held.path}; a new task would reset its budget. ${error instanceof Error ? error.message : "Resume needs confirmation."} After the user confirms, run agent-ops worktree resume ${held.name} --session ${session}.`;
132
- }
133
- return `Continue original DEBUG issue ${issue.task.id} in ${held.path}; its budget, rounds and evidence are preserved.`;
134
- }
135
- await original.assertDebugBaseline();
136
- const record = config.worktree?.mode === "auto" && await readWorktreeRecord(root) === null ?
137
- await ensureSessionWorktree(worktreeDependencies(), { cwd: root, sessionId: session }) : null;
138
- const path = record?.path ?? root;
139
- const effective = (await loadEffectiveConfig(path, "project")).config;
140
- const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => effective } });
141
- const existing = await tasks.status({ sessionId: session }).catch(() => null);
142
- if (existing?.status === "active")
143
- return existing.task.debug === undefined ? null : `Continue original DEBUG issue ${existing.task.id}.`;
144
- const held = await tasks.activeDebugRoot(fields.prompt);
145
- if (held !== null) {
146
- await tasks.attach(session, held.task.id);
147
- let message = `Continue original DEBUG issue ${held.task.id}; its budget, rounds and evidence are preserved.`;
148
- if (foregroundClockSupported(host)) {
149
- try {
150
- await tasks.debugService().foregroundStart(held.task.id, session);
151
- }
152
- catch (error) {
153
- message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
154
- }
155
- }
156
- return message;
157
- }
158
- const ids = effective.verification.commands.filter(c => c.required).map(c => c.id);
159
- const baseline = (await gitRunner(path).run(["rev-parse", "--verify", "HEAD"])).stdout.toString().trim();
160
- const criteria = ["issue-regression", "related-checks"].map(id => ({ id, description: id === "issue-regression" ? "Reproduce and fix the original reported issue with relevant regression evidence." : "Pass necessary related checks and two fresh independent reviews.", verifierIds: ids,
161
- acceptance: { mode: "review-only", baselineCommit: baseline, bindings: [], reviewOnlyReason: "Provisional investigation intake only. Identify and revise to executable original-issue regression before debug success; this criterion cannot prove reproduction." } }));
162
- const created = await tasks.create({ title: fields.prompt.split("\n")[0].slice(0, 160), goal: fields.prompt, intent: "Investigate and fix the original issue through authoritative task debug evidence.", debug: { facts: ["User-reported symptom is unverified."], unknowns: ["Baseline reproduction", "Root cause", "Relevant regression and related checks"] }, criteria, sessionId: session, policyConfigHash: calculateConfigHash(effective), requireSurface: true });
163
- let clockNote = "";
164
- if (foregroundClockSupported(host)) {
165
- try {
166
- await tasks.debugService().foregroundStart(created.task.id, session);
167
- }
168
- catch (error) {
169
- return `DEBUG issue ${created.task.id} created in ${path}, but its foreground clock is unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}. Preserve the issue and do not claim timed completion.`;
170
- }
171
- }
172
- else
173
- clockNote = " Native foreground timing is unsupported on this host; controlled operations remain evidence-timed and do not claim a complete active-time budget.";
174
- return `DEBUG issue ${created.task.id} created in ${path}. Preserve the original goal. Investigate, record falsifiable hypotheses, revise provisional criteria to relevant behavioral regression, add the surface audit (task revise --surface-file) before the first repair, repair, declare the origin (task debug --debug-action origin) once a hypothesis is confirmed, verify, obtain two fresh independent reviews, then finish. A clean HEAD baseline is fixed automatically; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Defaults: 60 active minutes / 5 material repair candidates. No user-authored debug plan is required.${clockNote}`;
175
- }
176
- export async function interruptDebugSession(root, sessionId, nativeInput) {
177
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
178
- const path = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
179
- const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => (await loadEffectiveConfig(path, "project")).config } });
180
- const attached = await tasks.status({ sessionId }).catch(error => {
181
- if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
182
- return null;
183
- throw error;
184
- });
185
- if (attached?.status === "active" && attached.task.debug !== undefined)
186
- await tasks.debugService().interrupt(attached.task.id, sessionId, nativeInput);
187
- }
188
- /** Close only the task attached to a native host's verified session. */
189
- export async function closeDebugSession(root, sessionId, host = "claude") {
190
- if (!foregroundClockSupported(host))
191
- return;
192
- if (sessionId.length === 0)
193
- return;
194
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
195
- const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
196
- const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => (await loadEffectiveConfig(contextRoot, "project")).config } });
197
- const attached = await tasks.status({ sessionId }).catch(() => null);
198
- if (attached?.status === "active" && attached.task.debug !== undefined)
199
- await tasks.debugService().foregroundStop(attached.task.id, sessionId);
200
- }
201
- /**
202
- * Without the completion gate, an active DEBUG issue still cannot end a turn
203
- * unless a current runtime stop checkpoint exists. Sessions without one pass.
204
- */
205
- export function debugStopGate(root, rootConfig) {
206
- return { handle: async (event) => {
207
- if (event.event !== "stop" || event.sessionId === undefined || event.sessionId.length === 0 ||
208
- event.terminationReason !== "model_stop" || event.fullyIdle !== true)
209
- return null;
210
- const sessionId = event.sessionId;
211
- const evaluate = async (attempt) => {
212
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId))).catch(() => null);
213
- const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
214
- const config = contextRoot === root && attempt === 0 ? rootConfig : (await loadEffectiveConfig(contextRoot, "project")).config;
215
- const runner = gitRunner(contextRoot);
216
- const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: runner, loadConfig: async () => config } });
217
- const attached = await tasks.status({ sessionId }).catch(error => {
218
- if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
219
- return null;
220
- throw error;
221
- });
222
- if (attached?.status !== "active" || attached.task.debug === undefined)
223
- return null;
224
- await tasks.debugService().prepareStop(attached.task.id, sessionId);
225
- const surface = await collectChangeSurface(runner);
226
- const fingerprint = await calculateSourceFingerprint(contextRoot, { mode: "worktree", changedFiles: surface.paths }, runner);
227
- return await tasks.debugService().validCheckpoint(attached.task.id, sessionId, fingerprint)
228
- ? { action: "continue", status: "UNKNOWN", code: "COMPLETION_GATE_TASK_DEBUG_STOPPED", remedy: "Original issue remains active with a runtime-bound non-success checkpoint." }
229
- : { action: "block", status: "FAIL", code: "COMPLETION_GATE_TASK_DEBUG_ACTIVE", remedy: "Continue the original DEBUG investigation, repair, verification and review loop. Failure to reproduce and an arbitrary checkpoint do not permit stopping. Preserve the task and budgets; record a current runtime stop checkpoint only for a proven blocker or exhausted limits." };
230
- };
231
- // Reload the task/config once for a transient gate fault; never rewrite bad evidence.
232
- for (let attempt = 0; attempt < 2; attempt++) {
233
- try {
234
- return await evaluate(attempt);
235
- }
236
- catch (error) {
237
- if (attempt === 0)
238
- continue;
239
- const code = error instanceof AgentOpsError ? error.code : "IO_ERROR";
240
- return { action: "continue", status: "UNKNOWN", code: "TASK_DEBUG_GATE_UNAVAILABLE", remedy: `DEBUG remains incomplete: gate ${code} failed after one targeted reload. Preserve the original task, evidence and budgets; run agent-ops doctor and restore readable task/config evidence before resuming. No completion is attested.` };
241
- }
242
- }
243
- return null;
244
- } };
245
- }
@@ -1,15 +0,0 @@
1
- /** Conservative intake only; the existing native Agent reasons about the issue. */
2
- export function isDebugIssue(prompt) {
3
- if (prompt.length === 0 || prompt.length > 64 * 1024 || prompt.includes("\0"))
4
- return false;
5
- // Harness-injected turns (subagent hand-backs, task notifications) are not a human report.
6
- if (/^\s*<(?:agent-message|task-notification|teammate-message)\b/u.test(prompt))
7
- return false;
8
- const value = prompt.normalize("NFKC").toLowerCase();
9
- if (/\b(?:explain|describe|what is|how does|refactor|feature|implement|design)\b|說明|解释|解釋|重構|新增功能|設計|實作功能/u.test(value))
10
- return false;
11
- return /\b(?:bug|error|fail(?:s|ed|ure)?|exception|crash|regression|500)\b|錯誤|错误|失敗|失败|異常|异常|問題|问题|回傳\s*500/u.test(value) &&
12
- /\b(?:fix|debug|investigate|reproduce|diagnose|resolve|repair)\b|修復|修复|調查|调查|重現|重现|診斷|诊断|查明|找出原因|解決|解决/u.test(value);
13
- }
14
- /** One issue or cause in different spacing or case is still the same one. */
15
- export const sameDebugText = (left, right) => left.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase() === right.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase();
@@ -1,135 +0,0 @@
1
- import { canonicalJson } from "../config/hash.js";
2
- import { sha256 } from "../fs/hash.js";
3
- import { AgentOpsError } from "../fs/paths.js";
4
- export const DEBUG_DEFAULTS = { enabled: true, budgetMs: 60 * 60_000, maxRounds: 5 };
5
- export const DEBUG_EXTENSION_MS = 20 * 60_000;
6
- const object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7
- const text = (v) => typeof v === "string" && v.length > 0 && v.length <= 4096 && !v.includes("\0");
8
- const hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/u.test(v);
9
- const integer = (v) => Number.isSafeInteger(v) && v >= 0;
10
- const strings = (v) => Array.isArray(v) && v.length <= 128 && v.every(text) && new Set(v).size === v.length;
11
- const exact = (v, names) => object(v) && Object.keys(v).length === names.length && names.every(k => Object.hasOwn(v, k));
12
- const statuses = ["proposed", "investigating", "confirmed", "rejected", "inconclusive"];
13
- export const debugCauses = ["budget", "iterations", "strategy-required", "stagnation", "environment", "permission", "user-pause", "diagnosed"];
14
- export function validDebugSettings(v) {
15
- return exact(v, ["enabled", "budgetMs", "maxRounds"]) && typeof v.enabled === "boolean" &&
16
- integer(v.budgetMs) && v.budgetMs > 0 && v.budgetMs <= 86_400_000 &&
17
- integer(v.maxRounds) && v.maxRounds > 0 && v.maxRounds <= 100;
18
- }
19
- export function validDebugIntake(v) {
20
- return exact(v, ["rootTaskId", "facts", "unknowns"]) && text(v.rootTaskId) && strings(v.facts) && strings(v.unknowns);
21
- }
22
- export function validDebugState(v) {
23
- const baseKeys = ["schemaVersion", "rootTaskId", "ownerRoot", "baselineCommit", "budgetMs", "maxRounds", "consumedMs", "regressionChecks", "hypotheses", "receipts", "strategy", "grants", "operation", "checkpoint"];
24
- if (!object(v) || Object.keys(v).some(k => !baseKeys.includes(k) && k !== "foreground" && k !== "control") || !baseKeys.every(k => Object.hasOwn(v, k)) ||
25
- v.schemaVersion !== 1 || !text(v.rootTaskId) || !text(v.ownerRoot) || typeof v.baselineCommit !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(v.baselineCommit) ||
26
- !validDebugSettings({ enabled: true, budgetMs: v.budgetMs, maxRounds: v.maxRounds }) || !integer(v.consumedMs) || !strings(v.regressionChecks) ||
27
- !Array.isArray(v.hypotheses) || v.hypotheses.length > 32 || !Array.isArray(v.receipts) || v.receipts.length > 256 ||
28
- !Array.isArray(v.grants) || v.grants.length > 128)
29
- return false;
30
- if (Object.hasOwn(v, "control") && (!exact(v.control, ["mode", "paused", "sessionId", "eventId", "sourceDigest", "timestamp"]) ||
31
- !["full", "diagnose"].includes(String(v.control.mode)) || typeof v.control.paused !== "boolean" || !text(v.control.sessionId) ||
32
- v.control.sessionId.length > 256 || !hash(v.control.eventId) || !hash(v.control.sourceDigest) || !integer(v.control.timestamp)))
33
- return false;
34
- for (const h of v.hypotheses) {
35
- if (!exact(h, ["id", "statement", "method", "expected", "checkId", "expectedStatus", "status", "supporting", "opposing", "actual", "history"]) ||
36
- ![h.id, h.statement, h.method, h.expected, h.checkId].every(text) || !["PASS", "FAIL"].includes(String(h.expectedStatus)) || !statuses.includes(String(h.status)) ||
37
- !strings(h.supporting) || !strings(h.opposing) || typeof h.actual !== "string" || h.actual.length > 4096 ||
38
- !Array.isArray(h.history) || h.history.length > 128 || h.history.some(entry => !exact(entry, ["status", "actual", "references", "reason"]) ||
39
- !statuses.includes(String(entry.status)) || typeof entry.actual !== "string" || entry.actual.length > 4096 || !strings(entry.references) || !text(entry.reason)))
40
- return false;
41
- }
42
- if (new Set(v.hypotheses.map(h => h.id)).size !== v.hypotheses.length)
43
- return false;
44
- for (const r of v.receipts)
45
- if (!exact(r, ["id", "taskId", "kind", "contractHash", "configHash", "sourceFingerprint", "productDigest", "durationMs", "finishedAt", "round", "progress", "references", "outcome", "cause"]) ||
46
- !text(r.id) || !text(r.taskId) || !["investigate", "verify", "review"].includes(String(r.kind)) ||
47
- ![r.contractHash, r.configHash, r.sourceFingerprint, r.productDigest].every(hash) || !integer(r.durationMs) || !integer(r.finishedAt) || typeof r.round !== "boolean" ||
48
- !strings(r.progress) || !strings(r.references) || !["PASS", "FAIL", "UNKNOWN"].includes(String(r.outcome)) || ![null, "environment", "permission"].includes(r.cause))
49
- return false;
50
- if (new Set(v.receipts.map(r => r.id)).size !== v.receipts.length || v.receipts.filter(r => r.round).length > v.maxRounds)
51
- return false;
52
- for (const g of v.grants)
53
- if (!(exact(g, ["eventId", "sessionId", "sourceDigest", "grantedMs"]) || (exact(g, ["eventId", "sessionId", "sourceDigest", "grantedMs", "spentAtGrantMs"]) && integer(g.spentAtGrantMs))) || !text(g.eventId) || !text(g.sessionId) || !hash(g.sourceDigest) || (g.grantedMs !== DEBUG_EXTENSION_MS && g.grantedMs !== null))
54
- return false;
55
- if (new Set(v.grants.map(g => g.eventId)).size !== v.grants.length)
56
- return false;
57
- if (v.strategy !== null && (!exact(v.strategy, ["from", "to", "reason", "references", "afterRound"]) ||
58
- ![v.strategy.from, v.strategy.to, v.strategy.reason].every(text) || v.strategy.from === v.strategy.to ||
59
- !strings(v.strategy.references) || !integer(v.strategy.afterRound) || v.strategy.afterRound < 2))
60
- return false;
61
- if (v.foreground !== undefined && v.foreground !== null && (!exact(v.foreground, ["sessionId", "startedAt"]) || !text(v.foreground.sessionId) || v.foreground.sessionId.length > 256 || !integer(v.foreground.startedAt)))
62
- return false;
63
- if (v.operation !== null && (!((exact(v.operation, ["id", "startedAt", "taskId", "contractHash", "configHash", "sourceFingerprint", "productDigest", "kind"])) ||
64
- exact(v.operation, ["id", "startedAt", "taskId", "contractHash", "configHash", "sourceFingerprint", "productDigest", "kind", "processId", "processIdentity"])) ||
65
- !text(v.operation.id) || !integer(v.operation.startedAt) || !text(v.operation.taskId) ||
66
- ![v.operation.contractHash, v.operation.configHash, v.operation.sourceFingerprint, v.operation.productDigest].every(hash) ||
67
- !["investigate", "verify", "review"].includes(String(v.operation.kind)) ||
68
- (Object.hasOwn(v.operation, "processId") && (!integer(v.operation.processId) || v.operation.processId < 1)) ||
69
- (Object.hasOwn(v.operation, "processIdentity") && (!text(v.operation.processIdentity) || v.operation.processIdentity.length > 256))))
70
- return false;
71
- if (v.checkpoint !== null && (!exact(v.checkpoint, ["taskId", "sessionId", "root", "sourceFingerprint", "contractHash", "configHash", "cause", "receiptId"]) ||
72
- ![v.checkpoint.taskId, v.checkpoint.sessionId, v.checkpoint.root, v.checkpoint.receiptId].every(text) ||
73
- ![v.checkpoint.sourceFingerprint, v.checkpoint.contractHash, v.checkpoint.configHash].every(hash) || (v.checkpoint.cause !== null && !debugCauses.includes(v.checkpoint.cause))))
74
- return false;
75
- return true;
76
- }
77
- export function createDebugState(rootTaskId, baselineCommit, settings = DEBUG_DEFAULTS, ownerRoot = ".") {
78
- return { schemaVersion: 1, rootTaskId, ownerRoot, baselineCommit, budgetMs: settings.budgetMs, maxRounds: settings.maxRounds, consumedMs: 0, regressionChecks: [],
79
- hypotheses: [], receipts: [], strategy: null, grants: [], foreground: null, operation: null, checkpoint: null };
80
- }
81
- export function debugRounds(state) { return state.receipts.filter(r => r.round).length; }
82
- export function debugSpentMs(state, now = Date.now()) {
83
- const starts = [state.foreground?.startedAt, state.operation?.startedAt].filter((startedAt) => startedAt !== undefined);
84
- const active = starts.length === 0 ? 0 : Math.max(0, now - Math.min(...starts));
85
- return state.consumedMs + active;
86
- }
87
- export function debugRemainingMs(state, now = Date.now()) {
88
- if (state.grants.some(g => g.grantedMs === null))
89
- return Number.POSITIVE_INFINITY;
90
- const limit = state.grants.reduce((n, g) => Math.max(n, g.spentAtGrantMs ?? n) + (g.grantedMs ?? 0), state.budgetMs);
91
- return Math.max(0, limit - debugSpentMs(state, now));
92
- }
93
- export function debugStopCause(state, now = Date.now()) {
94
- if (state.control?.paused === true)
95
- return "user-pause";
96
- if (debugRemainingMs(state, now) === 0)
97
- return "budget";
98
- if (debugRounds(state) >= state.maxRounds)
99
- return "iterations";
100
- const rounds = state.receipts.filter(r => r.round);
101
- const since = rounds.slice(state.strategy?.afterRound ?? 0);
102
- let stagnant = 0;
103
- for (const round of [...since].reverse()) {
104
- if (round.progress.length > 0)
105
- break;
106
- stagnant++;
107
- }
108
- if (stagnant >= 2)
109
- return state.strategy === null ? "strategy-required" : "stagnation";
110
- return state.receipts.at(-1)?.cause ?? null;
111
- }
112
- /** Only reviewed reasoning/proof belongs in the digest; operations and clocks do not. */
113
- export function debugProofDigest(state) {
114
- return sha256(canonicalJson({ rootTaskId: state.rootTaskId, baselineCommit: state.baselineCommit, regressionChecks: state.regressionChecks, hypotheses: state.hypotheses,
115
- strategy: state.strategy, proof: state.receipts.filter(r => r.kind === "verify").map(r => ({ taskId: r.taskId, contractHash: r.contractHash,
116
- configHash: r.configHash, sourceFingerprint: r.sourceFingerprint, productDigest: r.productDigest, references: r.references, outcome: r.outcome })) }));
117
- }
118
- export function assertDebugState(value) {
119
- if (!validDebugState(value))
120
- throw new AgentOpsError("TASK_DEBUG_STATE_INVALID", "Debug state must be strict, bounded and root-bound.");
121
- }
122
- export async function loadDebugProof(root, rootTaskId) {
123
- const { FileTaskStore } = await import("./store.js");
124
- const { join } = await import("node:path");
125
- const record = (await new FileTaskStore(join(root, ".agent-ops/tasks/state.json"), root).read()).tasks.find(r => r.task.id === rootTaskId);
126
- if (record?.debug === undefined || record.task.debug?.rootTaskId !== rootTaskId)
127
- throw new AgentOpsError("TASK_DEBUG_ROOT_MISMATCH", "Original issue proof is unavailable; do not reset its limits.");
128
- if (record.debug.ownerRoot !== root) {
129
- const owned = (await new FileTaskStore(join(record.debug.ownerRoot, ".agent-ops/tasks/state.json"), record.debug.ownerRoot).read()).tasks.find(r => r.task.id === rootTaskId);
130
- if (owned?.debug === undefined || owned.debug.ownerRoot !== record.debug.ownerRoot || owned.task.goal !== record.task.goal)
131
- throw new AgentOpsError("TASK_DEBUG_ROOT_MISMATCH", "Original owned issue state is unavailable; do not recreate its budget.");
132
- return owned.debug;
133
- }
134
- return record.debug;
135
- }