@kylecheng3146/agent-ops 0.2.8 → 0.3.1
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/dist/packages/cli/src/args.js +48 -2
- package/dist/packages/cli/src/bin.js +55 -35
- package/dist/packages/cli/src/cli.js +31 -0
- package/dist/packages/cli/src/commands/hook.js +4 -1
- package/dist/packages/cli/src/commands/parallel.js +88 -0
- package/dist/packages/cli/src/commands/review.js +6 -4
- package/dist/packages/cli/src/commands/task.js +27 -3
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +34 -7
- package/dist/packages/cli/src/parallel-deps.js +89 -0
- package/dist/runtime/src/adapters/agy/output.js +2 -1
- package/dist/runtime/src/adapters/claude/config.js +6 -2
- package/dist/runtime/src/adapters/claude/input.js +24 -0
- package/dist/runtime/src/adapters/claude/output.js +2 -1
- package/dist/runtime/src/config/merge.js +10 -2
- package/dist/runtime/src/hooks/codex-loop.js +11 -1
- package/dist/runtime/src/hooks/completion-gate.js +141 -7
- package/dist/runtime/src/hooks/dispatch.js +5 -0
- package/dist/runtime/src/install/doctor.js +4 -1
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/plan.js +4 -1
- package/dist/runtime/src/parallel/finish.js +303 -0
- package/dist/runtime/src/parallel/guard.js +76 -0
- package/dist/runtime/src/parallel/manage.js +147 -0
- package/dist/runtime/src/parallel/service.js +281 -0
- package/dist/runtime/src/review/attestation.js +125 -50
- package/dist/runtime/src/review/execute.js +32 -5
- package/dist/runtime/src/schema/validate.js +50 -1
- package/dist/runtime/src/task/completion.js +1 -1
- package/dist/runtime/src/task/service.js +20 -4
- package/dist/runtime/src/task/store.js +7 -2
- package/docs/en/guides/configuration.md +63 -0
- package/docs/en/spec/delegation.md +10 -0
- package/docs/zh-TW/guides/configuration.md +55 -0
- package/docs/zh-TW/spec/delegation.md +11 -1
- package/package.json +1 -1
- package/schemas/config.schema.json +35 -0
|
@@ -38,7 +38,7 @@ export async function checkTaskCompletionEvidence(stored, options) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
const attestation = await findReviewAttestation(root, sourceFingerprint);
|
|
41
|
+
const attestation = await findReviewAttestation(root, sourceFingerprint, stored.task.id);
|
|
42
42
|
if (attestation === null || attestation.taskId !== stored.task.id) {
|
|
43
43
|
return { status: "FAIL", code: "REVIEW_REQUIRED", remedy: `Run agent-ops review --task ${stored.task.id} --yes for the whole task and current source.` };
|
|
44
44
|
}
|
|
@@ -130,9 +130,17 @@ export class TaskService {
|
|
|
130
130
|
state.tasks.push(record);
|
|
131
131
|
if (input.sessionId !== undefined) {
|
|
132
132
|
const currentIndex = state.sessions.findIndex(({ sessionId }) => sessionId === input.sessionId);
|
|
133
|
+
// A subtask keeps the session on the top of its tree: the completion
|
|
134
|
+
// gate checks the attached task and everything under it, so attaching
|
|
135
|
+
// the subtask would let a Stop through with its parent unfinished.
|
|
136
|
+
let rootId = record.task.id;
|
|
137
|
+
for (let parentId = record.task.parentTaskId; parentId !== undefined;) {
|
|
138
|
+
rootId = parentId;
|
|
139
|
+
parentId = state.tasks.find(({ task }) => task.id === parentId)?.task.parentTaskId;
|
|
140
|
+
}
|
|
133
141
|
const attachment = {
|
|
134
142
|
sessionId: input.sessionId,
|
|
135
|
-
taskId:
|
|
143
|
+
taskId: rootId,
|
|
136
144
|
attachedAt: now
|
|
137
145
|
};
|
|
138
146
|
if (currentIndex === -1) {
|
|
@@ -304,7 +312,12 @@ export class TaskService {
|
|
|
304
312
|
* task accepts evidence: a completed record must stay exactly as it was
|
|
305
313
|
* verified.
|
|
306
314
|
*/
|
|
307
|
-
async recordEvidence(taskId, evidenceInput
|
|
315
|
+
async recordEvidence(taskId, evidenceInput,
|
|
316
|
+
/**
|
|
317
|
+
* A PASS review's base: set records it, null clears it (the review covered
|
|
318
|
+
* uncommitted work), undefined leaves the record's value alone.
|
|
319
|
+
*/
|
|
320
|
+
reviewBase) {
|
|
308
321
|
const now = assertTimestamp(this.#now());
|
|
309
322
|
return await this.#store.mutate((state) => {
|
|
310
323
|
const current = findTask(state, taskId);
|
|
@@ -328,10 +341,13 @@ export class TaskService {
|
|
|
328
341
|
...new Set([...(evidence[criterionId] ?? []), ...references])
|
|
329
342
|
];
|
|
330
343
|
}
|
|
344
|
+
const { reviewBase: previousBase, ...rest } = current;
|
|
345
|
+
const base = reviewBase === undefined ? previousBase : reviewBase ?? undefined;
|
|
331
346
|
const updated = {
|
|
332
|
-
...
|
|
347
|
+
...rest,
|
|
333
348
|
evidence,
|
|
334
|
-
updatedAt: now
|
|
349
|
+
updatedAt: now,
|
|
350
|
+
...(base === undefined ? {} : { reviewBase: base })
|
|
335
351
|
};
|
|
336
352
|
replaceTask(state, updated);
|
|
337
353
|
return cloneRecord(updated);
|
|
@@ -66,7 +66,7 @@ function parseTaskRecord(value) {
|
|
|
66
66
|
];
|
|
67
67
|
// Every record written before an optional field existed must still parse, so
|
|
68
68
|
// the accepted shapes are every combination of them.
|
|
69
|
-
const optionalKeys = ["completionBase", "failureFingerprint", "policyConfigHash"];
|
|
69
|
+
const optionalKeys = ["completionBase", "failureFingerprint", "policyConfigHash", "reviewBase"];
|
|
70
70
|
const allowedKeys = new Set(Array.from({ length: 1 << optionalKeys.length }, (_unused, mask) => [
|
|
71
71
|
...baseKeys,
|
|
72
72
|
...optionalKeys.filter((_key, position) => (mask & (1 << position)) !== 0)
|
|
@@ -146,6 +146,10 @@ function parseTaskRecord(value) {
|
|
|
146
146
|
(typeof completionBase !== "string" || !/^[a-f0-9]{40,64}$/u.test(completionBase))) {
|
|
147
147
|
return invalidState("Task state contains an invalid completion base.");
|
|
148
148
|
}
|
|
149
|
+
if (value.reviewBase !== undefined &&
|
|
150
|
+
(typeof value.reviewBase !== "string" || !/^[a-f0-9]{40,64}$/u.test(value.reviewBase))) {
|
|
151
|
+
return invalidState("Task state contains an invalid review base.");
|
|
152
|
+
}
|
|
149
153
|
return {
|
|
150
154
|
task: task.value,
|
|
151
155
|
status,
|
|
@@ -156,7 +160,8 @@ function parseTaskRecord(value) {
|
|
|
156
160
|
archivedAt: value.archivedAt,
|
|
157
161
|
failureFingerprint,
|
|
158
162
|
policyConfigHash,
|
|
159
|
-
completionBase
|
|
163
|
+
completionBase,
|
|
164
|
+
...(value.reviewBase === undefined ? {} : { reviewBase: value.reviewBase })
|
|
160
165
|
};
|
|
161
166
|
}
|
|
162
167
|
function parseSession(value) {
|
|
@@ -132,6 +132,69 @@ agent-ops doctor --check-auth # one real print call per target
|
|
|
132
132
|
reports what to do but never fixes it: every target authenticates through
|
|
133
133
|
interactive OAuth, so there is no `--fix`. Run `<target> login` yourself.
|
|
134
134
|
|
|
135
|
+
### Parallel sessions in worktrees
|
|
136
|
+
|
|
137
|
+
Verification, review and the completion gate all fingerprint the whole Git
|
|
138
|
+
change surface, so two conversations editing one checkout void each other's
|
|
139
|
+
evidence. Give each editing conversation its own worktree instead:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"worktree": {
|
|
144
|
+
"mode": "auto",
|
|
145
|
+
"setup": [{ "command": "pnpm", "args": ["install", "--frozen-lockfile"] }]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `mode: "auto"` routes every editing conversation through
|
|
151
|
+
`agent-ops worktree add <name> --session <id>`, run from the main checkout.
|
|
152
|
+
On Claude Code a direct Edit or Write of the main checkout outside
|
|
153
|
+
`.worktrees/` is denied (run `agent-ops update` once so the PreToolUse hook
|
|
154
|
+
matches the file tools). Absent or `"off"` keeps today's single checkout.
|
|
155
|
+
- `agent-ops task create` run from the main checkout with a known session
|
|
156
|
+
creates that session's worktree first (or reuses it) and records the task
|
|
157
|
+
there, so the work starts in the worktree. Its output names the path to
|
|
158
|
+
enter and the `--base` to verify against.
|
|
159
|
+
- As a fallback on Claude Code, the first denied edit creates that worktree:
|
|
160
|
+
`.worktrees/session-<first eight characters of the session id>`, setup
|
|
161
|
+
included, bound to the session. The denial names the path to enter with
|
|
162
|
+
EnterWorktree; a later denial for the same session reuses it. If creation
|
|
163
|
+
fails, the denial says why and falls back to the manual `worktree add`.
|
|
164
|
+
Run `agent-ops update` once so the PreToolUse hook gets the 600-second
|
|
165
|
+
timeout setup needs.
|
|
166
|
+
- `add` creates `.worktrees/<name>` on branch `agent-ops/<name>` from the main
|
|
167
|
+
checkout's HEAD and excludes `/.worktrees/` through `.git/info/exclude`,
|
|
168
|
+
never `.gitignore`. It copies the ignored files agent-ops installed and any
|
|
169
|
+
file matched by a root `.worktreeinclude` (gitignore syntax, for example
|
|
170
|
+
`.env` or `local.properties`); a file Git already checked out is never
|
|
171
|
+
overwritten. Trust is inherited only when the main checkout is trusted and
|
|
172
|
+
the worktree's effective config is identical.
|
|
173
|
+
- `setup` commands run in the new worktree (default timeout ten minutes) for
|
|
174
|
+
what Git does not carry, such as installed dependencies. They are part of
|
|
175
|
+
the trusted config, so they need no separate approval; a failing step
|
|
176
|
+
removes the worktree again.
|
|
177
|
+
- The session's completion gate follows it: Claude Code enters the worktree
|
|
178
|
+
with EnterWorktree, and a host that cannot move a session (agy) is judged by
|
|
179
|
+
the worktree's gate through a redirect the main checkout records.
|
|
180
|
+
- In a worktree, verify and review each task but do not run `task complete`
|
|
181
|
+
(Claude Code refuses it inside an isolated worktree session). `agent-ops
|
|
182
|
+
worktree finish <name>` completes every non-archived task in the worktree,
|
|
183
|
+
subtasks before their parents, each against the base its PASS review
|
|
184
|
+
recorded (the worktree's base when none was recorded), before any rebase.
|
|
185
|
+
If one cannot complete, nothing merges and the error names the task.
|
|
186
|
+
Creating a subtask keeps the session attached to the top of its tree, and
|
|
187
|
+
each task keeps its own review record, so a parent and a subtask reviewed
|
|
188
|
+
on the same source do not overwrite each other.
|
|
189
|
+
- `agent-ops worktree finish <name>` merges by fast-forward only, one finish
|
|
190
|
+
at a time. If the target moved, it rebases; a clean rebase whose own patch
|
|
191
|
+
is unchanged is re-verified before merging, and a conflict is reported with
|
|
192
|
+
the `refs/notes/agent-ops` intent of the work that landed first. The note
|
|
193
|
+
records every task of the worktree, subtasks indented under their parent.
|
|
194
|
+
- `worktree list`, `resume <name> --session <id>` and `remove <name>` manage
|
|
195
|
+
what is left; `remove --force` discards work and asks the user first.
|
|
196
|
+
`doctor` reports worktrees idle for more than seven days.
|
|
197
|
+
|
|
135
198
|
### Project-local loop profile
|
|
136
199
|
|
|
137
200
|
`--profile loop` is an opt-in project-scope profile. Select `codex`, `claude`,
|
|
@@ -29,3 +29,13 @@ Concurrent delegated writers MUST NOT share one working tree.
|
|
|
29
29
|
- Evidence: `agent-ops verify` returns a status other than `UNKNOWN` for the run.
|
|
30
30
|
- Positive: `Read-only scans run in parallel; the single edit runs in the coordinator worktree.`
|
|
31
31
|
- Negative: `Two subagents edit the same files while verification runs.`
|
|
32
|
+
|
|
33
|
+
## DELEGATE-ISOLATION-002
|
|
34
|
+
|
|
35
|
+
Parallel editing conversations in one repository MUST each work in their own agent-ops worktree.
|
|
36
|
+
|
|
37
|
+
- Trigger: `worktree.mode` is `auto`, or a second conversation will edit the repository while another is still open.
|
|
38
|
+
- Action: Run `agent-ops worktree add <name> --session <id>` from the main checkout before the first edit, work only in the printed path, and merge with `agent-ops worktree finish <name>` after the task completes against the printed `--base`.
|
|
39
|
+
- Evidence: `agent-ops worktree list` shows one worktree per editing session, and each finish reports a fast-forward merge.
|
|
40
|
+
- Positive: `Two conversations edit .worktrees/login and .worktrees/cart; each verifies, reviews and finishes on its own.`
|
|
41
|
+
- Negative: `Two conversations edit the main checkout at once, so each one's verification and review is voided by the other's writes.`
|
|
@@ -116,6 +116,61 @@ agent-ops doctor --check-auth # 每個目標一次真實 print 呼叫
|
|
|
116
116
|
不會代為修復:所有目標都經由互動式 OAuth 認證,因此沒有 `--fix`。
|
|
117
117
|
請自行執行 `<target> login`。
|
|
118
118
|
|
|
119
|
+
### 在 worktree 中並行 session
|
|
120
|
+
|
|
121
|
+
Verification、review 與 completion gate 都對整個 Git change surface 計算
|
|
122
|
+
fingerprint,因此兩個對話修改同一個 checkout 會互相作廢對方的 evidence。
|
|
123
|
+
請讓每個修改中的對話使用自己的 worktree:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"worktree": {
|
|
128
|
+
"mode": "auto",
|
|
129
|
+
"setup": [{ "command": "pnpm", "args": ["install", "--frozen-lockfile"] }]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- `mode: "auto"` 讓每個修改中的對話都先在主 checkout 執行
|
|
135
|
+
`agent-ops worktree add <name> --session <id>`。Claude Code 直接以 Edit 或
|
|
136
|
+
Write 修改主 checkout 中 `.worktrees/` 以外的檔案會被拒絕(需先執行一次
|
|
137
|
+
`agent-ops update`,讓 PreToolUse hook 比對檔案工具)。未設定或 `"off"` 則維持
|
|
138
|
+
目前的單一 checkout。
|
|
139
|
+
- 在主 checkout、已知 session 的情況下執行 `agent-ops task create`,會先建立(或重用)
|
|
140
|
+
該 session 的 worktree,並把 task 記錄在那裡,讓工作一開始就在 worktree 內。輸出會列出
|
|
141
|
+
要進入的路徑與 verify 用的 `--base`。
|
|
142
|
+
- 作為 Claude Code 上的後備,第一次被拒絕的編輯會自動建立該 worktree:
|
|
143
|
+
`.worktrees/session-<session id 前八個字元>`,包含 setup,並綁定該 session。
|
|
144
|
+
拒絕訊息會列出要以 EnterWorktree 進入的路徑;同一 session 之後被拒絕時會重用它。
|
|
145
|
+
建立失敗時,拒絕訊息會說明原因並退回手動 `worktree add`。需先執行一次
|
|
146
|
+
`agent-ops update`,讓 PreToolUse hook 取得 setup 所需的 600 秒逾時。
|
|
147
|
+
- `add` 從主 checkout 的 HEAD 建立 `.worktrees/<name>` 與 branch
|
|
148
|
+
`agent-ops/<name>`,並透過 `.git/info/exclude`(而非 `.gitignore`)排除
|
|
149
|
+
`/.worktrees/`。它會複製 agent-ops 安裝的 ignored 檔案,以及根目錄
|
|
150
|
+
`.worktreeinclude`(gitignore 語法,例如 `.env` 或 `local.properties`)比對到
|
|
151
|
+
的檔案;Git 已 checkout 的檔案一律不覆寫。只有主 checkout 已 trusted 且
|
|
152
|
+
worktree 的 effective config 完全相同時才繼承 trust。
|
|
153
|
+
- `setup` 指令在新 worktree 中執行(預設 timeout 十分鐘),處理 Git 不會帶過去
|
|
154
|
+
的東西,例如已安裝的相依套件。它屬於 trusted config 的一部分,不需另外核准;
|
|
155
|
+
任一步驟失敗會移除該 worktree。
|
|
156
|
+
- Session 的 completion gate 會跟著它:Claude Code 以 EnterWorktree 進入
|
|
157
|
+
worktree;無法移動 session 的 host(agy)則透過主 checkout 記錄的 redirect,
|
|
158
|
+
由 worktree 的 gate 判定。
|
|
159
|
+
- 在 worktree 裡對每個 task 執行 verify 與 review,但不要執行 `task complete`
|
|
160
|
+
(Claude Code 在隔離的 worktree session 中會拒絕它)。`agent-ops worktree finish
|
|
161
|
+
<name>` 會在任何 rebase 之前 complete worktree 內每個未封存的 task,先 subtask
|
|
162
|
+
後 parent,各自使用其 PASS review 記錄的 base(沒有記錄時使用 worktree 的 base)。
|
|
163
|
+
任一 task 無法完成時不會合併,錯誤訊息會指出是哪個 task。建立 subtask 時 session
|
|
164
|
+
維持附著在 task 樹最上層,且每個 task 各自保留 review 紀錄,所以在同一份原始碼上
|
|
165
|
+
review 的 parent 與 subtask 不會互相覆蓋。
|
|
166
|
+
- `agent-ops worktree finish <name>` 只以 fast-forward 合併,一次只執行一個
|
|
167
|
+
finish。Target 若已前進會先 rebase;沒有衝突且自身 patch 不變的 rebase 會先
|
|
168
|
+
重新驗證再合併;衝突則連同先合併那份工作在 `refs/notes/agent-ops` 的意圖一起
|
|
169
|
+
回報。Note 會記錄 worktree 的每個 task,subtask 縮排在其 parent 之下。
|
|
170
|
+
- `worktree list`、`resume <name> --session <id>` 與 `remove <name>` 管理剩下的
|
|
171
|
+
worktree;`remove --force` 會丟棄工作,執行前會先詢問使用者。`doctor` 會回報
|
|
172
|
+
閒置超過七天的 worktree。
|
|
173
|
+
|
|
119
174
|
### Project-local loop profile
|
|
120
175
|
|
|
121
176
|
`--profile loop` 是明確 opt-in 的 project-scope profile。請選擇 `codex`、
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 委派
|
|
2
2
|
|
|
3
|
-
English source version: 2026-
|
|
3
|
+
English source version: 2026-09-24. Revalidate: when the English specification changes.
|
|
4
4
|
|
|
5
5
|
## DELEGATE-SCOPE-001
|
|
6
6
|
|
|
@@ -31,3 +31,13 @@ English source version: 2026-07-23. Revalidate: when the English specification c
|
|
|
31
31
|
- Evidence: `agent-ops verify` 該次執行回傳非 `UNKNOWN` 的狀態。
|
|
32
32
|
- Positive: `唯讀掃描並行執行;唯一的修改在協調者工作區進行。`
|
|
33
33
|
- Negative: `兩個 subagent 在驗證執行期間修改相同檔案。`
|
|
34
|
+
|
|
35
|
+
## DELEGATE-ISOLATION-002
|
|
36
|
+
|
|
37
|
+
同一個 repository 中並行修改檔案的對話 MUST 各自在自己的 agent-ops worktree 中工作。
|
|
38
|
+
|
|
39
|
+
- Trigger: `worktree.mode` 為 `auto`,或另一個對話仍開著時,第二個對話也要修改這個 repository。
|
|
40
|
+
- Action: 第一次修改前,在主 checkout 執行 `agent-ops worktree add <name> --session <id>`,只在印出的路徑中工作;task 以印出的 `--base` 完成後,用 `agent-ops worktree finish <name>` 合併。
|
|
41
|
+
- Evidence: `agent-ops worktree list` 顯示每個修改中的 session 各有一個 worktree,且每次 finish 都回報 fast-forward 合併。
|
|
42
|
+
- Positive: `兩個對話分別修改 .worktrees/login 與 .worktrees/cart;各自驗證、review 並 finish。`
|
|
43
|
+
- Negative: `兩個對話同時修改主 checkout,彼此的寫入讓對方的驗證與 review 全部作廢。`
|
package/package.json
CHANGED
|
@@ -119,6 +119,41 @@
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
},
|
|
122
|
+
"worktree": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"additionalProperties": false,
|
|
125
|
+
"required": ["mode"],
|
|
126
|
+
"properties": {
|
|
127
|
+
"mode": {
|
|
128
|
+
"enum": ["auto", "off"]
|
|
129
|
+
},
|
|
130
|
+
"setup": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"items": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"additionalProperties": false,
|
|
135
|
+
"required": ["command", "args"],
|
|
136
|
+
"properties": {
|
|
137
|
+
"command": {
|
|
138
|
+
"$ref": "#/$defs/nonEmptyString"
|
|
139
|
+
},
|
|
140
|
+
"args": {
|
|
141
|
+
"type": "array",
|
|
142
|
+
"items": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"pattern": "^[^\\u0000]*$"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"timeoutMs": {
|
|
148
|
+
"type": "integer",
|
|
149
|
+
"minimum": 1,
|
|
150
|
+
"maximum": 2147483647
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
122
157
|
"securityExceptions": {
|
|
123
158
|
"type": "array",
|
|
124
159
|
"items": {
|