@kylecheng3146/agent-ops 0.3.2 → 0.3.3
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/runtime/src/adapters/opencode/config.js +1 -1
- package/dist/runtime/src/adapters/opencode/input.js +67 -1
- package/dist/runtime/src/parallel/guard.js +2 -2
- package/dist/runtime/src/review/probe.js +26 -2
- package/docs/en/guides/configuration.md +4 -3
- package/docs/zh-TW/guides/configuration.md +4 -3
- package/package.json +1 -1
|
@@ -131,7 +131,7 @@ export function buildOpencodePlugin(capabilities, runtimePath) {
|
|
|
131
131
|
lines.push(" // opencode initializes plugins once per app, not once per session.", " await runManagedHook($, \"SessionStart\", {", " event: \"SessionStart\",", " projectRoot", " });");
|
|
132
132
|
}
|
|
133
133
|
if (events.includes("PreToolUse")) {
|
|
134
|
-
lines.push(' hooks["tool.execute.before"] = async (input, output) => {', '
|
|
134
|
+
lines.push(' const WRITE_TOOLS = ["bash", "edit", "write", "patch", "apply_patch"];', ' hooks["tool.execute.before"] = async (input, output) => {', ' const tool = typeof input?.tool === "string" ? input.tool.toLowerCase() : "";', " if (!WRITE_TOOLS.includes(tool)) return;", " await runManagedHook($, \"PreToolUse\", {", " event: \"PreToolUse\",", " projectRoot,", " input,", " output", " });", " };");
|
|
135
135
|
}
|
|
136
136
|
if (events.includes("Stop")) {
|
|
137
137
|
lines.push(" hooks.event = async ({ event }) => {", ' if (event?.type !== "session.idle") return;', " await runManagedHook($, \"Stop\", { event: \"Stop\", projectRoot });", " };");
|
|
@@ -1,8 +1,54 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { normalizeHookEvent } from "../../hooks/normalize.js";
|
|
2
3
|
import { normalizeShellHookEvent } from "../../hooks/shell.js";
|
|
4
|
+
/**
|
|
5
|
+
* opencode tools that write a file without a shell (lowercase tool names).
|
|
6
|
+
* `apply_patch` is the GPT-model replacement for `edit`/`write`; older
|
|
7
|
+
* servers still report `patch`.
|
|
8
|
+
*/
|
|
9
|
+
export const OPENCODE_FILE_TOOLS = new Set([
|
|
10
|
+
"edit",
|
|
11
|
+
"write",
|
|
12
|
+
"patch",
|
|
13
|
+
"apply_patch"
|
|
14
|
+
]);
|
|
3
15
|
function isRecord(value) {
|
|
4
16
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
17
|
}
|
|
18
|
+
function sessionIdOf(value) {
|
|
19
|
+
const sessionId = value.sessionID;
|
|
20
|
+
return typeof sessionId === "string" && sessionId.length > 0
|
|
21
|
+
? sessionId
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Paths embedded in `apply_patch` marker lines, relative to the project root:
|
|
26
|
+
* `*** Add File:`, `*** Update File:`, `*** Move to:`, `*** Delete File:`.
|
|
27
|
+
*/
|
|
28
|
+
function patchTextPaths(patchText) {
|
|
29
|
+
const paths = [];
|
|
30
|
+
const marker = /^\*\*\*\s+(?:Add File|Update File|Move to|Delete File):\s*(.+?)\s*$/gmu;
|
|
31
|
+
for (const match of patchText.matchAll(marker)) {
|
|
32
|
+
const target = match[1]?.trim();
|
|
33
|
+
if (target !== undefined && target.length > 0 && !target.includes("\0")) {
|
|
34
|
+
paths.push(target);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return paths;
|
|
38
|
+
}
|
|
39
|
+
function fileWritePaths(args, projectRoot) {
|
|
40
|
+
const direct = args.filePath ?? args.file_path ?? args.path;
|
|
41
|
+
if (typeof direct === "string" && direct.length > 0 && !direct.includes("\0")) {
|
|
42
|
+
return [resolve(projectRoot, direct)];
|
|
43
|
+
}
|
|
44
|
+
if (typeof args.patchText === "string") {
|
|
45
|
+
const marked = patchTextPaths(args.patchText);
|
|
46
|
+
if (marked.length > 0) {
|
|
47
|
+
return marked.map((target) => resolve(projectRoot, target));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
6
52
|
/**
|
|
7
53
|
* The managed plugin forwards opencode's own hook payload untouched under
|
|
8
54
|
* `input`, so every field this adapter reads is documented plugin input rather
|
|
@@ -21,7 +67,27 @@ export function normalizeOpencodeHookInput(input) {
|
|
|
21
67
|
}
|
|
22
68
|
if (input.event === "PreToolUse" &&
|
|
23
69
|
isRecord(input.input) &&
|
|
24
|
-
input.input.tool === "
|
|
70
|
+
typeof input.input.tool === "string" &&
|
|
71
|
+
OPENCODE_FILE_TOOLS.has(input.input.tool.toLowerCase()) &&
|
|
72
|
+
isRecord(input.output) &&
|
|
73
|
+
isRecord(input.output.args) &&
|
|
74
|
+
typeof projectRoot === "string" &&
|
|
75
|
+
projectRoot.length > 0) {
|
|
76
|
+
const paths = fileWritePaths(input.output.args, projectRoot);
|
|
77
|
+
if (paths !== null) {
|
|
78
|
+
const sessionId = sessionIdOf(input.input);
|
|
79
|
+
return {
|
|
80
|
+
event: "file-write",
|
|
81
|
+
projectRoot,
|
|
82
|
+
paths,
|
|
83
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (input.event === "PreToolUse" &&
|
|
88
|
+
isRecord(input.input) &&
|
|
89
|
+
typeof input.input.tool === "string" &&
|
|
90
|
+
input.input.tool.toLowerCase() === "bash" &&
|
|
25
91
|
isRecord(input.output) &&
|
|
26
92
|
isRecord(input.output.args) &&
|
|
27
93
|
typeof input.output.args.command === "string" &&
|
|
@@ -56,7 +56,7 @@ ensureWorktree) {
|
|
|
56
56
|
action: "block",
|
|
57
57
|
status: "FAIL",
|
|
58
58
|
code: "WORKTREE_CREATED",
|
|
59
|
-
remedy: `worktree.mode is auto, so this session now has its own worktree at ${worktree}. Enter it (Claude Code: EnterWorktree with path ${worktree}) and redo this edit there, at ${join(worktree, relative(mainRoot, target))}.`
|
|
59
|
+
remedy: `worktree.mode is auto, so this session now has its own worktree at ${worktree}. Enter it (Claude Code: EnterWorktree with path ${worktree}; otherwise use the worktree path as every command's working directory) and redo this edit there, at ${join(worktree, relative(mainRoot, target))}.`
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
catch (error) {
|
|
@@ -68,7 +68,7 @@ ensureWorktree) {
|
|
|
68
68
|
action: "block",
|
|
69
69
|
status: "FAIL",
|
|
70
70
|
code: "WORKTREE_REQUIRED",
|
|
71
|
-
remedy: `worktree.mode is auto, so the main checkout is shared and not edited directly.${failure} Run \`agent-ops worktree add <name> --session ${session}\` in ${mainRoot}, enter the printed path (Claude Code: EnterWorktree with that path), and edit there.`
|
|
71
|
+
remedy: `worktree.mode is auto, so the main checkout is shared and not edited directly.${failure} Run \`agent-ops worktree add <name> --session ${session}\` in ${mainRoot}, enter the printed path (Claude Code: EnterWorktree with that path; otherwise use it as every command's working directory), and edit there.`
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -34,6 +34,30 @@ export async function probeReviewTarget(target, options) {
|
|
|
34
34
|
? await mkdtemp(join(tmpdir(), "agent-ops-review-probe-"))
|
|
35
35
|
: options.cwd;
|
|
36
36
|
try {
|
|
37
|
+
// agy's sandbox only answers inside a project-shaped directory: an empty
|
|
38
|
+
// temp dir hangs or denies file access, which this probe then misreads as
|
|
39
|
+
// unauthenticated. Cloning first matches the review attempt, which already
|
|
40
|
+
// runs inside a disposable clone; a failed clone falls back to the plain
|
|
41
|
+
// temp dir, which is exactly today's behavior.
|
|
42
|
+
let invocationCwd = directory;
|
|
43
|
+
if (deep && target === "agy") {
|
|
44
|
+
const clone = join(directory, "repository");
|
|
45
|
+
const cloned = await runVerificationCommand({
|
|
46
|
+
id: "review-probe-clone",
|
|
47
|
+
command: "git",
|
|
48
|
+
args: ["clone", "--no-hardlinks", "--quiet", "--", options.cwd, clone],
|
|
49
|
+
cwd: directory,
|
|
50
|
+
required: true,
|
|
51
|
+
evidence: { kind: "exit-code" },
|
|
52
|
+
timeoutMs: Math.min(probeTimeoutMs(options.timeoutMs), 30_000)
|
|
53
|
+
}, {
|
|
54
|
+
cwd: directory,
|
|
55
|
+
...(options.runner === undefined ? {} : { runner: options.runner })
|
|
56
|
+
});
|
|
57
|
+
if (cloned.status === "PASS") {
|
|
58
|
+
invocationCwd = clone;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
37
61
|
const invocation = buildProbeInvocation({
|
|
38
62
|
target,
|
|
39
63
|
prompt: PROBE_PROMPT,
|
|
@@ -48,12 +72,12 @@ export async function probeReviewTarget(target, options) {
|
|
|
48
72
|
id: `review-probe-${target}`,
|
|
49
73
|
command: invocation.command,
|
|
50
74
|
args: deep ? [...invocation.args] : ["--version"],
|
|
51
|
-
cwd:
|
|
75
|
+
cwd: invocationCwd,
|
|
52
76
|
required: true,
|
|
53
77
|
evidence: { kind: "exit-code" },
|
|
54
78
|
timeoutMs: probeTimeoutMs(options.timeoutMs)
|
|
55
79
|
}, {
|
|
56
|
-
cwd:
|
|
80
|
+
cwd: invocationCwd,
|
|
57
81
|
...(options.runner === undefined ? {} : { runner: options.runner }),
|
|
58
82
|
...(deep
|
|
59
83
|
? {
|
|
@@ -149,17 +149,18 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
149
149
|
|
|
150
150
|
- `mode: "auto"` routes every editing conversation through
|
|
151
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
|
|
152
|
+
On Claude Code and opencode a direct Edit or Write of the main checkout outside
|
|
153
153
|
`.worktrees/` is denied (run `agent-ops update` once so the PreToolUse hook
|
|
154
154
|
matches the file tools). Absent or `"off"` keeps today's single checkout.
|
|
155
155
|
- `agent-ops task create` run from the main checkout with a known session
|
|
156
156
|
creates that session's worktree first (or reuses it) and records the task
|
|
157
157
|
there, so the work starts in the worktree. Its output names the path to
|
|
158
158
|
enter and the `--base` to verify against.
|
|
159
|
-
- As a fallback on Claude Code, the first denied edit creates that worktree:
|
|
159
|
+
- As a fallback on Claude Code and opencode, the first denied edit creates that worktree:
|
|
160
160
|
`.worktrees/session-<first eight characters of the session id>`, setup
|
|
161
161
|
included, bound to the session. The denial names the path to enter with
|
|
162
|
-
EnterWorktree
|
|
162
|
+
EnterWorktree on Claude Code, or to use as every command's working directory
|
|
163
|
+
otherwise; a later denial for the same session reuses it. If creation
|
|
163
164
|
fails, the denial says why and falls back to the manual `worktree add`.
|
|
164
165
|
Run `agent-ops update` once so the PreToolUse hook gets the 600-second
|
|
165
166
|
timeout setup needs.
|
|
@@ -132,16 +132,17 @@ fingerprint,因此兩個對話修改同一個 checkout 會互相作廢對方
|
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
- `mode: "auto"` 讓每個修改中的對話都先在主 checkout 執行
|
|
135
|
-
`agent-ops worktree add <name> --session <id>`。Claude Code 直接以 Edit 或
|
|
135
|
+
`agent-ops worktree add <name> --session <id>`。Claude Code 與 opencode 直接以 Edit 或
|
|
136
136
|
Write 修改主 checkout 中 `.worktrees/` 以外的檔案會被拒絕(需先執行一次
|
|
137
137
|
`agent-ops update`,讓 PreToolUse hook 比對檔案工具)。未設定或 `"off"` 則維持
|
|
138
138
|
目前的單一 checkout。
|
|
139
139
|
- 在主 checkout、已知 session 的情況下執行 `agent-ops task create`,會先建立(或重用)
|
|
140
140
|
該 session 的 worktree,並把 task 記錄在那裡,讓工作一開始就在 worktree 內。輸出會列出
|
|
141
141
|
要進入的路徑與 verify 用的 `--base`。
|
|
142
|
-
- 作為 Claude Code 上的後備,第一次被拒絕的編輯會自動建立該 worktree:
|
|
142
|
+
- 作為 Claude Code 與 opencode 上的後備,第一次被拒絕的編輯會自動建立該 worktree:
|
|
143
143
|
`.worktrees/session-<session id 前八個字元>`,包含 setup,並綁定該 session。
|
|
144
|
-
|
|
144
|
+
拒絕訊息會列出要進入的路徑(Claude Code 以 EnterWorktree 進入,其他 harness
|
|
145
|
+
則作為每條命令的工作目錄);同一 session 之後被拒絕時會重用它。
|
|
145
146
|
建立失敗時,拒絕訊息會說明原因並退回手動 `worktree add`。需先執行一次
|
|
146
147
|
`agent-ops update`,讓 PreToolUse hook 取得 setup 所需的 600 秒逾時。
|
|
147
148
|
- `add` 從主 checkout 的 HEAD 建立 `.worktrees/<name>` 與 branch
|