@kylecheng3146/agent-ops 0.3.6 → 0.4.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.
- package/dist/packages/cli/src/args.js +49 -5
- package/dist/packages/cli/src/bin.js +17 -35
- package/dist/packages/cli/src/cli.js +27 -2
- package/dist/packages/cli/src/commands/hook.js +4 -1
- package/dist/packages/cli/src/commands/init.js +1 -0
- package/dist/packages/cli/src/commands/parallel.js +16 -3
- package/dist/packages/cli/src/commands/uninstall.js +3 -2
- package/dist/packages/cli/src/commands/update.js +1 -0
- package/dist/packages/cli/src/context.js +28 -3
- package/dist/packages/cli/src/hook-process.js +14 -3
- package/dist/packages/cli/src/public-plan.js +32 -2
- package/dist/runtime/src/adapters/claude/config.js +1 -1
- package/dist/runtime/src/adapters/claude/input.js +17 -0
- package/dist/runtime/src/adapters/claude/output.js +9 -0
- package/dist/runtime/src/hooks/codex-loop.js +67 -7
- package/dist/runtime/src/hooks/completion-gate.js +27 -2
- package/dist/runtime/src/hooks/dispatch.js +5 -0
- package/dist/runtime/src/install/apply.js +2 -0
- package/dist/runtime/src/install/doctor.js +12 -1
- package/dist/runtime/src/install/harness.js +16 -5
- package/dist/runtime/src/install/plan.js +9 -0
- package/dist/runtime/src/install/preauth.js +265 -0
- package/dist/runtime/src/install/probes.js +32 -0
- package/dist/runtime/src/install/uninstall.js +29 -11
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/parallel/enter.js +43 -0
- package/dist/runtime/src/parallel/finish.js +2 -1
- package/dist/runtime/src/parallel/guard.js +20 -1
- package/dist/runtime/src/parallel/manage.js +38 -0
- package/dist/runtime/src/parallel/service.js +3 -1
- package/dist/runtime/src/review/execute.js +15 -7
- package/dist/runtime/src/review/host-sandbox.js +45 -1
- package/dist/runtime/src/review/probe.js +44 -12
- package/dist/runtime/src/review/render.js +9 -0
- package/docs/en/guides/configuration.md +61 -4
- package/docs/zh-TW/guides/configuration.md +49 -1
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ const SCOPES = new Set(["project", "user"]);
|
|
|
17
17
|
const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
|
|
18
18
|
// opencode is absent: it has no read-only flag, so it cannot review.
|
|
19
19
|
const REVIEW_TARGETS = new Set(["agy", "claude", "codex"]);
|
|
20
|
-
export const WORKTREE_ACTIONS = ["add", "finish", "list", "resume", "remove"];
|
|
20
|
+
export const WORKTREE_ACTIONS = ["add", "commit", "finish", "list", "resume", "remove"];
|
|
21
21
|
export class CliArgumentError extends Error {
|
|
22
22
|
code;
|
|
23
23
|
option;
|
|
@@ -87,6 +87,8 @@ export function parseArgs(argv) {
|
|
|
87
87
|
let worktreeName;
|
|
88
88
|
let force = false;
|
|
89
89
|
let worktreeFrom;
|
|
90
|
+
let worktreeMessage;
|
|
91
|
+
let output;
|
|
90
92
|
let worktreeTargetBranch;
|
|
91
93
|
let worktree;
|
|
92
94
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -276,6 +278,23 @@ export function parseArgs(argv) {
|
|
|
276
278
|
index += 1;
|
|
277
279
|
break;
|
|
278
280
|
}
|
|
281
|
+
case "-m":
|
|
282
|
+
case "--message": {
|
|
283
|
+
if (worktreeMessage !== undefined) {
|
|
284
|
+
duplicate(token);
|
|
285
|
+
}
|
|
286
|
+
worktreeMessage = readOptionValue(argv, index, token);
|
|
287
|
+
index += 1;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "--output": {
|
|
291
|
+
if (output !== undefined) {
|
|
292
|
+
duplicate(token);
|
|
293
|
+
}
|
|
294
|
+
output = readOptionValue(argv, index, token);
|
|
295
|
+
index += 1;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
279
298
|
case "--target-branch": {
|
|
280
299
|
if (worktreeTargetBranch !== undefined) {
|
|
281
300
|
duplicate(token);
|
|
@@ -339,7 +358,11 @@ export function parseArgs(argv) {
|
|
|
339
358
|
action = token;
|
|
340
359
|
break;
|
|
341
360
|
}
|
|
342
|
-
if (command === "worktree" &&
|
|
361
|
+
if (command === "worktree" &&
|
|
362
|
+
action !== undefined &&
|
|
363
|
+
action !== "list" &&
|
|
364
|
+
action !== "commit" &&
|
|
365
|
+
worktreeName === undefined) {
|
|
343
366
|
worktreeName = token;
|
|
344
367
|
break;
|
|
345
368
|
}
|
|
@@ -400,6 +423,8 @@ export function parseArgs(argv) {
|
|
|
400
423
|
rerun ||
|
|
401
424
|
worktree !== undefined ||
|
|
402
425
|
worktreeFrom !== undefined ||
|
|
426
|
+
worktreeMessage !== undefined ||
|
|
427
|
+
output !== undefined ||
|
|
403
428
|
worktreeTargetBranch !== undefined ||
|
|
404
429
|
force) {
|
|
405
430
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
|
|
@@ -415,9 +440,15 @@ export function parseArgs(argv) {
|
|
|
415
440
|
if (command === "worktree" && action === undefined) {
|
|
416
441
|
throw new CliArgumentError("CLI_ACTION_REQUIRED", `The worktree command requires one of: ${WORKTREE_ACTIONS.join(", ")}.`);
|
|
417
442
|
}
|
|
418
|
-
if (command === "worktree" && action !== "list" && worktreeName === undefined) {
|
|
443
|
+
if (command === "worktree" && action !== "list" && action !== "commit" && worktreeName === undefined) {
|
|
419
444
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `worktree ${action} requires a worktree name.`);
|
|
420
445
|
}
|
|
446
|
+
if (command === "worktree" && action === "commit" && (worktreeMessage === undefined || worktreeMessage.trim() === "")) {
|
|
447
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "worktree commit requires --message (-m).", "--message");
|
|
448
|
+
}
|
|
449
|
+
if (worktreeMessage !== undefined && !(command === "worktree" && action === "commit")) {
|
|
450
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--message may be used only with worktree commit.", "--message");
|
|
451
|
+
}
|
|
421
452
|
if (command === "task" && action === undefined) {
|
|
422
453
|
throw new CliArgumentError("CLI_ACTION_REQUIRED", "The task command requires one of: create, status, attach, complete, archive, export.");
|
|
423
454
|
}
|
|
@@ -556,7 +587,13 @@ export function parseArgs(argv) {
|
|
|
556
587
|
dryRun ||
|
|
557
588
|
yes ||
|
|
558
589
|
rerun)) {
|
|
559
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "worktree accepts only a name, --session, --from, --target-branch and --json.");
|
|
590
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "worktree accepts only a name, --session, --from, --target-branch, --message and --json.");
|
|
591
|
+
}
|
|
592
|
+
if (output !== undefined && command !== "review" && command !== "doctor") {
|
|
593
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--output may be used only with review or doctor.", "--output");
|
|
594
|
+
}
|
|
595
|
+
if (output !== undefined && (output.trim() === "" || output.includes("\0"))) {
|
|
596
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", "Invalid value for --output.", "--output");
|
|
560
597
|
}
|
|
561
598
|
if (force && !(command === "worktree" && action === "remove")) {
|
|
562
599
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--force may be used only with worktree remove.");
|
|
@@ -572,7 +609,12 @@ export function parseArgs(argv) {
|
|
|
572
609
|
!/^[A-Za-z0-9._/-]{1,128}$/u.test(worktreeTargetBranch)) {
|
|
573
610
|
throw new CliArgumentError("CLI_INVALID_VALUE", `Invalid value for --target-branch: ${worktreeTargetBranch}`, "--target-branch");
|
|
574
611
|
}
|
|
575
|
-
|
|
612
|
+
// commit acts as a session (it refuses another's worktree), so it may be told which.
|
|
613
|
+
if (command === "worktree" &&
|
|
614
|
+
action !== "add" &&
|
|
615
|
+
action !== "resume" &&
|
|
616
|
+
action !== "commit" &&
|
|
617
|
+
sessionId !== undefined) {
|
|
576
618
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `worktree ${action} takes the session from the worktree; omit --session.`);
|
|
577
619
|
}
|
|
578
620
|
return {
|
|
@@ -595,6 +637,8 @@ export function parseArgs(argv) {
|
|
|
595
637
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
596
638
|
...(base === undefined ? {} : { base }),
|
|
597
639
|
...(worktreeFrom === undefined ? {} : { worktreeFrom }),
|
|
640
|
+
...(worktreeMessage === undefined ? {} : { worktreeMessage }),
|
|
641
|
+
...(output === undefined ? {} : { output }),
|
|
598
642
|
...(worktreeTargetBranch === undefined ? {} : { worktreeTargetBranch }),
|
|
599
643
|
...(checkAuth ? { checkAuth } : {}),
|
|
600
644
|
...(checkAuthTargets.length === 0 ? {} : { checkAuthTargets }),
|
|
@@ -6,9 +6,10 @@ import { join } from "node:path";
|
|
|
6
6
|
import { execFileSync, spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { commonHarnessAdapters, harnessHookPath, HARNESS_IDS } from "../../../runtime/src/install/harness.js";
|
|
9
|
-
import { agyRuntimeStatus, agyVersionSupported, hookRegistrationDrift,
|
|
9
|
+
import { agyRuntimeStatus, agyVersionSupported, hookRegistrationDrift, repositoryTrustProbe, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
|
|
10
10
|
import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
|
|
11
|
-
import { readRecordedSessionId } from "../../../runtime/src/hooks/codex-loop.js";
|
|
11
|
+
import { readRecordedSessionId, resolveCommandSessionId, sessionIdFromEnvironment } from "../../../runtime/src/hooks/codex-loop.js";
|
|
12
|
+
import { codexHomeDirectory } from "../../../runtime/src/install/preauth.js";
|
|
12
13
|
import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
|
|
13
14
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
14
15
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
@@ -31,7 +32,7 @@ import { formatUninstallPlan, runUninstallCommand } from "./commands/uninstall.j
|
|
|
31
32
|
import { runTaskCommand } from "./commands/task.js";
|
|
32
33
|
import { runReviewCommand } from "./commands/review.js";
|
|
33
34
|
import { createReviewExecutor, ReviewInterruptedError } from "../../../runtime/src/review/execute.js";
|
|
34
|
-
import {
|
|
35
|
+
import { probeReviewTargetDetailed } from "../../../runtime/src/review/probe.js";
|
|
35
36
|
import { resolveReviewScope } from "../../../runtime/src/review/scope.js";
|
|
36
37
|
import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
|
|
37
38
|
import { runTrustCommand } from "./commands/trust.js";
|
|
@@ -234,6 +235,7 @@ else {
|
|
|
234
235
|
isTTY,
|
|
235
236
|
toolkitVersion: CLI_VERSION,
|
|
236
237
|
hookRuntimePath: HOOK_RUNTIME_PATH,
|
|
238
|
+
codexHome: codexHomeDirectory(),
|
|
237
239
|
agyWarning: () => {
|
|
238
240
|
try {
|
|
239
241
|
const output = runAgy(["--version"], { timeout: 5_000 });
|
|
@@ -304,34 +306,7 @@ else {
|
|
|
304
306
|
};
|
|
305
307
|
},
|
|
306
308
|
repositoryTrust: async () => {
|
|
307
|
-
|
|
308
|
-
const status = repositoryTrustStatus(trust);
|
|
309
|
-
if (trust === "STALE") {
|
|
310
|
-
return {
|
|
311
|
-
status,
|
|
312
|
-
message: "Repository trust binding is stale.",
|
|
313
|
-
code: "TRUST_REQUIRED",
|
|
314
|
-
remediation: "Run `agent-ops trust grant`."
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
if (trust === "UNTRUSTED") {
|
|
318
|
-
const verificationConfigured = config.verification.commands.length > 0;
|
|
319
|
-
return {
|
|
320
|
-
status,
|
|
321
|
-
message: verificationConfigured
|
|
322
|
-
? "Repository is not trusted; Stop verification will not run."
|
|
323
|
-
: "Repository is not trusted.",
|
|
324
|
-
...(verificationConfigured
|
|
325
|
-
? {
|
|
326
|
-
code: "TRUST_REQUIRED",
|
|
327
|
-
remediation: "Run `agent-ops trust grant`."
|
|
328
|
-
}
|
|
329
|
-
: {
|
|
330
|
-
remediation: "No action needed; trust is only required once verification.commands is set."
|
|
331
|
-
})
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
return { status };
|
|
309
|
+
return repositoryTrustProbe(await repositoryTrust(root, config, CLI_VERSION), config.verification.commands.length > 0);
|
|
335
310
|
},
|
|
336
311
|
smokeAvailability: () => {
|
|
337
312
|
const status = smokeAvailabilityStatus(config);
|
|
@@ -343,7 +318,7 @@ else {
|
|
|
343
318
|
}
|
|
344
319
|
: { status };
|
|
345
320
|
},
|
|
346
|
-
reviewTarget: async (target, deep) => await
|
|
321
|
+
reviewTarget: async (target, deep) => await probeReviewTargetDetailed(target, { cwd: root, deep }),
|
|
347
322
|
worktrees: async () => await worktreeDoctorProbe(root),
|
|
348
323
|
worktreeBranchLock: async () => await worktreeBranchLockProbe(root),
|
|
349
324
|
rootGhostFiles: async () => await rootGhostFilesProbe(root)
|
|
@@ -362,6 +337,7 @@ else {
|
|
|
362
337
|
args,
|
|
363
338
|
root,
|
|
364
339
|
isTTY,
|
|
340
|
+
codexHome: codexHomeDirectory(),
|
|
365
341
|
trustStore: store,
|
|
366
342
|
calculateTrustBinding: async () => await plannedTrustBinding(root),
|
|
367
343
|
confirm: async (plan, trust) => await confirmPlan(formatUninstallPlan(plan, trust))
|
|
@@ -387,6 +363,7 @@ else {
|
|
|
387
363
|
isTTY,
|
|
388
364
|
toolkitVersion: CLI_VERSION,
|
|
389
365
|
hookRuntimePath: HOOK_RUNTIME_PATH,
|
|
366
|
+
codexHome: codexHomeDirectory(),
|
|
390
367
|
...(updateArgs.hookTargets === undefined
|
|
391
368
|
? {}
|
|
392
369
|
: { hookTargets: updateArgs.hookTargets }),
|
|
@@ -422,8 +399,13 @@ else {
|
|
|
422
399
|
// Explicit `--session` wins, then an injected identity, then the
|
|
423
400
|
// id a SessionStart hook recorded for this checkout: a command
|
|
424
401
|
// run inside a session is never told which session it is in.
|
|
425
|
-
|
|
426
|
-
|
|
402
|
+
// Only create, attach and status act as a session; a contested
|
|
403
|
+
// record refuses those rather than guess, and leaves the rest.
|
|
404
|
+
const actsAsSession = args.action === "create" ||
|
|
405
|
+
args.action === "attach" || args.action === "status";
|
|
406
|
+
const sessionId = args.sessionId ?? (actsAsSession
|
|
407
|
+
? await resolveCommandSessionId(root)
|
|
408
|
+
: sessionIdFromEnvironment() ?? await readRecordedSessionId(root));
|
|
427
409
|
const createConfig = args.action === "create"
|
|
428
410
|
? (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config
|
|
429
411
|
: undefined;
|
|
@@ -507,7 +489,7 @@ else {
|
|
|
507
489
|
...(reviewRole?.timeoutMs === undefined
|
|
508
490
|
? {}
|
|
509
491
|
: { timeoutMs: reviewRole.timeoutMs }),
|
|
510
|
-
preflightTarget: async (target, budget) => await
|
|
492
|
+
preflightTarget: async (target, budget) => await probeReviewTargetDetailed(target, {
|
|
511
493
|
cwd: root,
|
|
512
494
|
deep: true,
|
|
513
495
|
// The probe answers within the chain's remaining budget
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { CliArgumentError, parseArgs } from "./args.js";
|
|
2
4
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
3
5
|
import { errorEnvelope, okEnvelope, writeEnvelope } from "./output.js";
|
|
@@ -106,6 +108,9 @@ Options:
|
|
|
106
108
|
--scope <project|user>
|
|
107
109
|
--harness <all|both|agy|claude|codex|opencode|comma-separated>
|
|
108
110
|
--check-auth Probe each review target's authentication with one real call
|
|
111
|
+
--output <file>
|
|
112
|
+
Write the JSON envelope to <file> (mode 0600) and print one
|
|
113
|
+
summary line, so no shell redirect is needed
|
|
109
114
|
--json
|
|
110
115
|
`,
|
|
111
116
|
update: `Usage: agent-ops update [options]
|
|
@@ -172,6 +177,8 @@ fresh adversarial reviewer from the configured pair.
|
|
|
172
177
|
Options:
|
|
173
178
|
--task <id> Required task whose original criteria are reviewed
|
|
174
179
|
--base <git-ref> Review a clean committed range
|
|
180
|
+
--output <file> Write the JSON envelope to <file> (mode 0600) and print
|
|
181
|
+
one summary line, so no shell redirect is needed
|
|
175
182
|
--json
|
|
176
183
|
--yes Required authorization for both reviewer sessions
|
|
177
184
|
`,
|
|
@@ -186,6 +193,7 @@ Options:
|
|
|
186
193
|
--json
|
|
187
194
|
`,
|
|
188
195
|
worktree: `Usage: agent-ops worktree <add|finish|resume|remove> <name> [options]
|
|
196
|
+
agent-ops worktree commit --message <text> [--json]
|
|
189
197
|
agent-ops worktree list [--json]
|
|
190
198
|
|
|
191
199
|
Give one writing session its own Git worktree, so parallel sessions stop
|
|
@@ -205,6 +213,9 @@ voiding each other's verification and review. Run it from the main checkout.
|
|
|
205
213
|
branch to the worktree's (after a clean rebase and
|
|
206
214
|
re-verification if the branch moved), record the task in
|
|
207
215
|
git notes, and remove the worktree. One finish runs at a time.
|
|
216
|
+
commit Stage every change in the current worktree and commit it with
|
|
217
|
+
--message. Refuses the main checkout and a worktree another
|
|
218
|
+
session owns, and needs no Git in the shell.
|
|
208
219
|
list Every agent-ops worktree: session, commits ahead, uncommitted
|
|
209
220
|
changes, task status and last activity.
|
|
210
221
|
resume <name>
|
|
@@ -221,6 +232,7 @@ Options:
|
|
|
221
232
|
add: branch finish merges back into (defaults to the current
|
|
222
233
|
branch; required when the main checkout is detached)
|
|
223
234
|
--force remove: discard uncommitted or unmerged work
|
|
235
|
+
--message <text> commit: the commit message (also -m)
|
|
224
236
|
--json
|
|
225
237
|
`
|
|
226
238
|
};
|
|
@@ -275,11 +287,24 @@ export async function runCli(argv, io, services) {
|
|
|
275
287
|
return writeAndReturn(io, errorEnvelope("CLI_COMMAND_UNAVAILABLE", `Command is not implemented yet: ${args.command}`), args.json, 1);
|
|
276
288
|
}
|
|
277
289
|
const result = await execute(args);
|
|
278
|
-
|
|
290
|
+
const exitCode = result.status === "ok"
|
|
279
291
|
? 0
|
|
280
292
|
: result.code === "REVIEW_NOT_RUN"
|
|
281
293
|
? 2
|
|
282
|
-
: 1
|
|
294
|
+
: 1;
|
|
295
|
+
if (args.output !== undefined) {
|
|
296
|
+
// The envelope goes to the file whole; stdout carries one line, so an
|
|
297
|
+
// agent needs neither a shell redirect nor the whole report in context.
|
|
298
|
+
try {
|
|
299
|
+
await writeFile(resolve(args.output), `${JSON.stringify(result.data === undefined ? { ...result, data: null } : result)}\n`, { mode: 0o600 });
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
throw new AgentOpsError("CLI_OUTPUT_WRITE_FAILED", `Could not write ${args.output}.`);
|
|
303
|
+
}
|
|
304
|
+
io.writeStdout(`agent-ops: ${result.code} (${result.status}); envelope written to ${args.output}\n`);
|
|
305
|
+
return exitCode;
|
|
306
|
+
}
|
|
307
|
+
return writeAndReturn(io, result, args.json, exitCode);
|
|
283
308
|
}
|
|
284
309
|
catch (error) {
|
|
285
310
|
if (error instanceof CliArgumentError) {
|
|
@@ -50,7 +50,10 @@ export async function runHookCommand(options) {
|
|
|
50
50
|
: { completionGate: options.completionGate }),
|
|
51
51
|
...(options.worktreeGuard === undefined
|
|
52
52
|
? {}
|
|
53
|
-
: { worktreeGuard: options.worktreeGuard })
|
|
53
|
+
: { worktreeGuard: options.worktreeGuard }),
|
|
54
|
+
...(options.worktreeEnter === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: { worktreeEnter: options.worktreeEnter })
|
|
54
57
|
});
|
|
55
58
|
return descriptor.runtime.formatOutput(options.event, result);
|
|
56
59
|
}
|
|
@@ -119,6 +119,7 @@ export async function runInitCommand(options) {
|
|
|
119
119
|
...(args.completionGate === undefined
|
|
120
120
|
? {}
|
|
121
121
|
: { completionGateEnabled: args.completionGate }),
|
|
122
|
+
...(options.codexHome === undefined ? {} : { codexHome: options.codexHome }),
|
|
122
123
|
...(worktreeConfig === undefined
|
|
123
124
|
? {}
|
|
124
125
|
: { worktree: worktreeConfig })
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { okEnvelope } from "../output.js";
|
|
2
|
+
import { resolveCommandSessionId, sessionIdFromEnvironment } from "../../../../runtime/src/hooks/codex-loop.js";
|
|
2
3
|
import { addWorktree } from "../../../../runtime/src/parallel/service.js";
|
|
3
4
|
import { finishWorktree } from "../../../../runtime/src/parallel/finish.js";
|
|
4
|
-
import { idleWorktrees, listWorktrees, removeWorktree, resumeWorktree } from "../../../../runtime/src/parallel/manage.js";
|
|
5
|
+
import { commitWorktree, idleWorktrees, listWorktrees, removeWorktree, resumeWorktree } from "../../../../runtime/src/parallel/manage.js";
|
|
5
6
|
function listText(statuses, now) {
|
|
6
7
|
if (statuses.length === 0)
|
|
7
8
|
return "No agent-ops worktrees.";
|
|
@@ -49,11 +50,23 @@ export async function runWorktreeCommand(options) {
|
|
|
49
50
|
const worktrees = await listWorktrees(options.deps, options.cwd);
|
|
50
51
|
return okEnvelope("WORKTREE_LIST", { worktrees, text: listText(worktrees, Date.now()) });
|
|
51
52
|
}
|
|
53
|
+
if (args.action === "commit") {
|
|
54
|
+
const committed = await commitWorktree(options.deps, {
|
|
55
|
+
cwd: options.cwd,
|
|
56
|
+
sessionId: args.sessionId ?? await resolveCommandSessionId(options.cwd),
|
|
57
|
+
message: args.worktreeMessage ?? ""
|
|
58
|
+
});
|
|
59
|
+
return okEnvelope("WORKTREE_COMMITTED", {
|
|
60
|
+
...committed,
|
|
61
|
+
text: `Committed ${committed.commit} in ${committed.record.path}.\n` +
|
|
62
|
+
`Verify and review with --base ${committed.record.base} (or the commit before your first change), then finish from ${committed.record.mainRoot}.`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
52
65
|
if (args.action === "resume") {
|
|
53
66
|
const record = await resumeWorktree(options.deps, {
|
|
54
67
|
cwd: options.cwd,
|
|
55
68
|
name: args.worktreeName,
|
|
56
|
-
sessionId: args.sessionId ??
|
|
69
|
+
sessionId: args.sessionId ?? sessionIdFromEnvironment()
|
|
57
70
|
});
|
|
58
71
|
return okEnvelope("WORKTREE_RESUMED", {
|
|
59
72
|
record,
|
|
@@ -78,7 +91,7 @@ export async function runWorktreeCommand(options) {
|
|
|
78
91
|
const finished = await finishWorktree(options.deps, { cwd: options.cwd, name: args.worktreeName });
|
|
79
92
|
return okEnvelope("WORKTREE_FINISHED", { ...finished, text: finishText(finished) });
|
|
80
93
|
}
|
|
81
|
-
const sessionId = args.sessionId ??
|
|
94
|
+
const sessionId = args.sessionId ?? sessionIdFromEnvironment();
|
|
82
95
|
const result = await addWorktree(options.deps, {
|
|
83
96
|
cwd: options.cwd,
|
|
84
97
|
name: args.worktreeName,
|
|
@@ -56,7 +56,8 @@ async function trustChange(options, plan) {
|
|
|
56
56
|
: await planTrustRevoke(binding, options.trustStore);
|
|
57
57
|
}
|
|
58
58
|
export async function runUninstallCommand(options) {
|
|
59
|
-
const
|
|
59
|
+
const planOptions = options.codexHome === undefined ? {} : { codexHome: options.codexHome };
|
|
60
|
+
const plan = await createUninstallPlan(options.root, options.args.harness, planOptions);
|
|
60
61
|
if (!plan.installed) {
|
|
61
62
|
return okEnvelope("UNINSTALL_NOT_INSTALLED", {
|
|
62
63
|
applied: false,
|
|
@@ -80,7 +81,7 @@ export async function runUninstallCommand(options) {
|
|
|
80
81
|
!(await options.confirm(plan, trust))) {
|
|
81
82
|
return uninstallError("UNINSTALL_CANCELLED", "Uninstall was cancelled; no files were changed.", plan, trust);
|
|
82
83
|
}
|
|
83
|
-
await applyUninstallPlan(options.root, plan);
|
|
84
|
+
await applyUninstallPlan(options.root, plan, planOptions);
|
|
84
85
|
if (trust.action === "revoke") {
|
|
85
86
|
try {
|
|
86
87
|
if (options.trustStore === undefined ||
|
|
@@ -112,6 +112,7 @@ export async function runUpdateCommand(options) {
|
|
|
112
112
|
...((options.hookTargets ?? options.args.hookTargets) === undefined
|
|
113
113
|
? {}
|
|
114
114
|
: { hookTargets: options.hookTargets ?? options.args.hookTargets }),
|
|
115
|
+
...(options.codexHome === undefined ? {} : { codexHome: options.codexHome }),
|
|
115
116
|
...(worktree === undefined ? {} : { worktree })
|
|
116
117
|
});
|
|
117
118
|
const trust = await trustChange(options, plan);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
4
5
|
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
5
6
|
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
6
7
|
import { loadConfigFile } from "../../../runtime/src/config/load.js";
|
|
@@ -23,6 +24,30 @@ export const DEFAULT_CONFIG = {
|
|
|
23
24
|
pathMappings: [],
|
|
24
25
|
securityExceptions: []
|
|
25
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Where a checkout's project config lives. Inside an agent-ops worktree it is
|
|
29
|
+
* the main checkout's: a branch must not change the config that decides its
|
|
30
|
+
* own gate, verifiers and trust before that change is merged, and a copy of a
|
|
31
|
+
* gitignored file can be missing or stale. Anything git cannot confirm is a
|
|
32
|
+
* worktree of this repository falls back to the checkout itself.
|
|
33
|
+
*/
|
|
34
|
+
export function projectConfigRoot(root) {
|
|
35
|
+
try {
|
|
36
|
+
const common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
37
|
+
if (basename(common) !== ".git") {
|
|
38
|
+
return root;
|
|
39
|
+
}
|
|
40
|
+
const main = realpathSync(dirname(common));
|
|
41
|
+
const here = realpathSync(root);
|
|
42
|
+
return here.startsWith(`${join(main, ".worktrees")}${sep}`) &&
|
|
43
|
+
existsSync(join(main, ".agent-ops", "config.json"))
|
|
44
|
+
? main
|
|
45
|
+
: root;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
26
51
|
function defaultConfigLayer() {
|
|
27
52
|
return {
|
|
28
53
|
source: "default",
|
|
@@ -49,7 +74,7 @@ async function loadOptionalConfig(path) {
|
|
|
49
74
|
export async function loadEffectiveConfig(root, scope, projectOverride) {
|
|
50
75
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
51
76
|
const userPath = join(home, ".agent-ops", "config.json");
|
|
52
|
-
const projectPath = join(root, ".agent-ops", "config.json");
|
|
77
|
+
const projectPath = join(scope === "user" ? root : projectConfigRoot(root), ".agent-ops", "config.json");
|
|
53
78
|
const layers = [defaultConfigLayer()];
|
|
54
79
|
if (scope === "user") {
|
|
55
80
|
const user = await loadOptionalConfig(userPath);
|
|
@@ -92,7 +117,7 @@ export async function loadEffectiveConfig(root, scope, projectOverride) {
|
|
|
92
117
|
export async function loadProjectHookConfig(root) {
|
|
93
118
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
94
119
|
const userPath = join(home, ".agent-ops", "config.json");
|
|
95
|
-
const projectPath = join(root, ".agent-ops", "config.json");
|
|
120
|
+
const projectPath = join(projectConfigRoot(root), ".agent-ops", "config.json");
|
|
96
121
|
const layers = [defaultConfigLayer()];
|
|
97
122
|
let project;
|
|
98
123
|
try {
|
|
@@ -9,6 +9,7 @@ import { resolveContainedPath } from "../../../runtime/src/fs/paths.js";
|
|
|
9
9
|
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
10
10
|
import { claudeStopRecursionMarker } from "../../../runtime/src/adapters/claude/input.js";
|
|
11
11
|
import { HARNESS_IDS, harnessDescriptor } from "../../../runtime/src/install/harness.js";
|
|
12
|
+
import { evaluateWorktreeEnter } from "../../../runtime/src/parallel/enter.js";
|
|
12
13
|
import { evaluateWorktreeWrite, resolveMainRoot } from "../../../runtime/src/parallel/guard.js";
|
|
13
14
|
import { ensureSessionWorktree, recordSessionOrigin } from "../../../runtime/src/parallel/service.js";
|
|
14
15
|
import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
|
|
@@ -214,11 +215,12 @@ function shouldBuildStopVerification(harness, event, config, rawInput) {
|
|
|
214
215
|
* The gate of one checkout. A session redirected into a worktree is judged by
|
|
215
216
|
* that worktree's gate, built the same way from the worktree's own config.
|
|
216
217
|
*/
|
|
217
|
-
function completionGateFor(root, config, gitRunner) {
|
|
218
|
+
function completionGateFor(root, config, gitRunner, harness) {
|
|
218
219
|
return new CompletionGateService({
|
|
219
220
|
root,
|
|
220
221
|
config,
|
|
221
222
|
gitRunner,
|
|
223
|
+
...(harness === undefined ? {} : { harness }),
|
|
222
224
|
taskService: new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)),
|
|
223
225
|
evidenceStore: new FileEvidenceStore(root, root),
|
|
224
226
|
forRoot: async (worktree) => {
|
|
@@ -226,7 +228,7 @@ function completionGateFor(root, config, gitRunner) {
|
|
|
226
228
|
if (outcome.kind !== "loaded" || !outcome.config.features.completionGate.enabled) {
|
|
227
229
|
throw new Error("The redirected worktree has no enabled completion gate.");
|
|
228
230
|
}
|
|
229
|
-
return completionGateFor(worktree, outcome.config, defaultGitRunner(worktree));
|
|
231
|
+
return completionGateFor(worktree, outcome.config, defaultGitRunner(worktree), harness);
|
|
230
232
|
}
|
|
231
233
|
});
|
|
232
234
|
}
|
|
@@ -341,7 +343,7 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
341
343
|
const completionGate = (harnessId === "agy" || harnessId === "claude") &&
|
|
342
344
|
config.features.completionGate.enabled
|
|
343
345
|
? dependencies.completionGate ?? {
|
|
344
|
-
handle: async (normalized) => await completionGateFor(root, config, gitRunner).handle(normalized)
|
|
346
|
+
handle: async (normalized) => await completionGateFor(root, config, gitRunner, harnessId).handle(normalized)
|
|
345
347
|
}
|
|
346
348
|
: undefined;
|
|
347
349
|
const worktreeGuard = config.worktree?.mode === "auto"
|
|
@@ -354,6 +356,14 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
354
356
|
: async (sessionId) => (await ensureSessionWorktree(dependencies.worktree, { cwd: mainRoot, sessionId })).path);
|
|
355
357
|
}
|
|
356
358
|
: undefined;
|
|
359
|
+
const worktreeEnter = config.worktree?.mode === "auto"
|
|
360
|
+
? async (enter) => {
|
|
361
|
+
const mainRoot = await resolveMainRoot(gitRunner);
|
|
362
|
+
return mainRoot === null
|
|
363
|
+
? { action: "continue", status: "UNKNOWN", code: "WORKTREE_GUARD_UNAVAILABLE" }
|
|
364
|
+
: await evaluateWorktreeEnter(mainRoot, enter.path, enter.sessionId);
|
|
365
|
+
}
|
|
366
|
+
: undefined;
|
|
357
367
|
if (hookEvent === "SessionStart" && config.worktree?.mode === "auto" &&
|
|
358
368
|
dependencies.worktree !== undefined && typeof parsedInput === "object" && parsedInput !== null) {
|
|
359
369
|
const fields = parsedInput;
|
|
@@ -370,6 +380,7 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
370
380
|
config,
|
|
371
381
|
trusted,
|
|
372
382
|
...(worktreeGuard === undefined ? {} : { worktreeGuard }),
|
|
383
|
+
...(worktreeEnter === undefined ? {} : { worktreeEnter }),
|
|
373
384
|
...(dependencies.advisory === undefined
|
|
374
385
|
? {}
|
|
375
386
|
: { advisory: dependencies.advisory }),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
2
3
|
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
3
4
|
const OPAQUE_SUMMARY = "Opaque managed settings content withheld.";
|
|
@@ -35,6 +36,29 @@ export function toPublicOperation(operation) {
|
|
|
35
36
|
export function toPublicOperations(operations) {
|
|
36
37
|
return operations.map(toPublicOperation);
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Pre-authorization changes join the ordinary operations, so every preview and
|
|
41
|
+
* confirmation shows them. A file in Codex's home is shown by absolute path;
|
|
42
|
+
* a file agent-ops does not wholly own is described, never printed.
|
|
43
|
+
*/
|
|
44
|
+
export function toPublicPreauthorization(changes) {
|
|
45
|
+
return (changes ?? []).map((change) => {
|
|
46
|
+
const path = publicPath(join(change.root, change.operation.path));
|
|
47
|
+
const operation = change.operation;
|
|
48
|
+
if (operation.kind === "remove") {
|
|
49
|
+
return { kind: "remove", path, expectedHash: operation.expectedHash };
|
|
50
|
+
}
|
|
51
|
+
return change.ownedContent
|
|
52
|
+
? { kind: "write", path, expectedHash: operation.expectedHash, content: operation.content }
|
|
53
|
+
: {
|
|
54
|
+
kind: "write",
|
|
55
|
+
path,
|
|
56
|
+
expectedHash: operation.expectedHash,
|
|
57
|
+
contentHash: sha256(operation.content),
|
|
58
|
+
summary: redactSecrets(change.summary)
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
38
62
|
export function toPublicInstallPlan(plan, trust) {
|
|
39
63
|
return {
|
|
40
64
|
scope: plan.scope,
|
|
@@ -42,7 +66,10 @@ export function toPublicInstallPlan(plan, trust) {
|
|
|
42
66
|
profiles: plan.profiles,
|
|
43
67
|
capabilities: plan.capabilities,
|
|
44
68
|
manifest: plan.manifest,
|
|
45
|
-
operations:
|
|
69
|
+
operations: [
|
|
70
|
+
...toPublicOperations(plan.operations),
|
|
71
|
+
...toPublicPreauthorization(plan.preauthorization)
|
|
72
|
+
],
|
|
46
73
|
detectedVerification: plan.detectedVerification,
|
|
47
74
|
...(trust === undefined ? {} : { trust })
|
|
48
75
|
};
|
|
@@ -59,7 +86,10 @@ export function toPublicUninstallPlan(plan, trust) {
|
|
|
59
86
|
installed: plan.installed,
|
|
60
87
|
manifest: plan.manifest,
|
|
61
88
|
manifestHash: plan.manifestHash,
|
|
62
|
-
operations:
|
|
89
|
+
operations: [
|
|
90
|
+
...toPublicOperations(plan.operations),
|
|
91
|
+
...toPublicPreauthorization(plan.preauthorization)
|
|
92
|
+
],
|
|
63
93
|
...(trust === undefined ? {} : { trust })
|
|
64
94
|
};
|
|
65
95
|
}
|
|
@@ -11,7 +11,7 @@ const CLAUDE_LOOP_EVENTS = [
|
|
|
11
11
|
"SubagentStop"
|
|
12
12
|
];
|
|
13
13
|
const CLAUDE_HOOK_MARKER = "--managed-by=agent-ops";
|
|
14
|
-
export const CLAUDE_PRE_TOOL_MATCHER = "Bash|Edit|MultiEdit|NotebookEdit|Write";
|
|
14
|
+
export const CLAUDE_PRE_TOOL_MATCHER = "Bash|Edit|MultiEdit|NotebookEdit|Write|EnterWorktree";
|
|
15
15
|
const CLAUDE_LOOP_LAUNCHER = "${CLAUDE_PROJECT_DIR}/.claude/hooks/agent-ops-loop.sh";
|
|
16
16
|
const CLAUDE_WINDOWS_LOOP_LAUNCHER = "${CLAUDE_PROJECT_DIR}/.claude/hooks/agent-ops-loop.ps1";
|
|
17
17
|
export function claudeSettingsTarget(scope) {
|
|
@@ -63,6 +63,23 @@ export function normalizeClaudeHookInput(input) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
if (input.hook_event_name === "PreToolUse" &&
|
|
67
|
+
input.tool_name === "EnterWorktree" &&
|
|
68
|
+
isRecord(input.tool_input) &&
|
|
69
|
+
typeof projectRoot === "string" &&
|
|
70
|
+
projectRoot.length > 0) {
|
|
71
|
+
const target = input.tool_input.path;
|
|
72
|
+
// A `name` asks for a new worktree of Claude's own, which is not this
|
|
73
|
+
// hook's business.
|
|
74
|
+
if (typeof target === "string" && target.length > 0 && !target.includes("\0")) {
|
|
75
|
+
return {
|
|
76
|
+
event: "enter-worktree",
|
|
77
|
+
projectRoot,
|
|
78
|
+
path: resolve(projectRoot, target),
|
|
79
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
66
83
|
if (input.hook_event_name === "PreToolUse" &&
|
|
67
84
|
input.tool_name === "Bash" &&
|
|
68
85
|
isRecord(input.tool_input) &&
|
|
@@ -32,6 +32,15 @@ export function claudeHookOutput(event, result) {
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
if (event === "PreToolUse" && result.decision === "allow") {
|
|
36
|
+
return json({
|
|
37
|
+
hookSpecificOutput: {
|
|
38
|
+
hookEventName: "PreToolUse",
|
|
39
|
+
permissionDecision: "allow",
|
|
40
|
+
permissionDecisionReason: denialReason
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
35
44
|
if (event === "Stop" && result.evidence !== undefined) {
|
|
36
45
|
if (result.status === "FAIL") {
|
|
37
46
|
const failed = result.evidence.commandResults
|