@kylecheng3146/agent-ops 0.3.0 → 0.3.2
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 +19 -1
- package/dist/packages/cli/src/bin.js +34 -77
- package/dist/packages/cli/src/cli.js +3 -0
- package/dist/packages/cli/src/commands/init.js +27 -3
- package/dist/packages/cli/src/commands/parallel.js +5 -4
- 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/commands/update.js +47 -3
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +4 -1
- package/dist/packages/cli/src/parallel-deps.js +89 -0
- package/dist/packages/cli/src/wizard.js +21 -2
- package/dist/runtime/src/adapters/claude/config.js +2 -1
- package/dist/runtime/src/hooks/completion-gate.js +1 -1
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/parallel-setup.js +63 -0
- package/dist/runtime/src/install/plan.js +8 -7
- package/dist/runtime/src/install/update.js +4 -1
- package/dist/runtime/src/parallel/finish.js +47 -7
- package/dist/runtime/src/parallel/guard.js +20 -3
- package/dist/runtime/src/parallel/service.js +18 -0
- package/dist/runtime/src/review/attestation.js +125 -50
- package/dist/runtime/src/review/execute.js +32 -5
- 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 +22 -1
- package/docs/zh-TW/guides/configuration.md +16 -1
- package/package.json +1 -1
|
@@ -86,6 +86,7 @@ export function parseArgs(argv) {
|
|
|
86
86
|
let versionSeen = false;
|
|
87
87
|
let worktreeName;
|
|
88
88
|
let force = false;
|
|
89
|
+
let worktree;
|
|
89
90
|
for (let index = 0; index < argv.length; index += 1) {
|
|
90
91
|
const token = argv[index];
|
|
91
92
|
if (token === undefined) {
|
|
@@ -229,6 +230,18 @@ export function parseArgs(argv) {
|
|
|
229
230
|
}
|
|
230
231
|
completionGate = true;
|
|
231
232
|
break;
|
|
233
|
+
case "--worktree": {
|
|
234
|
+
if (worktree !== undefined) {
|
|
235
|
+
duplicate(token);
|
|
236
|
+
}
|
|
237
|
+
const value = readOptionValue(argv, index, token);
|
|
238
|
+
if (value !== "auto" && value !== "off") {
|
|
239
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", `Worktree mode must be auto or off: ${value}`, token);
|
|
240
|
+
}
|
|
241
|
+
worktree = value;
|
|
242
|
+
index += 1;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
232
245
|
case "--dry-run":
|
|
233
246
|
if (dryRun) {
|
|
234
247
|
duplicate(token);
|
|
@@ -367,6 +380,7 @@ export function parseArgs(argv) {
|
|
|
367
380
|
dryRun ||
|
|
368
381
|
yes ||
|
|
369
382
|
rerun ||
|
|
383
|
+
worktree !== undefined ||
|
|
370
384
|
force) {
|
|
371
385
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
|
|
372
386
|
}
|
|
@@ -432,6 +446,9 @@ export function parseArgs(argv) {
|
|
|
432
446
|
if (completionGate !== undefined && command !== "init") {
|
|
433
447
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--completion-gate may be used only with init.");
|
|
434
448
|
}
|
|
449
|
+
if (worktree !== undefined && command !== "init" && command !== "update") {
|
|
450
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--worktree may be used only with init or update.");
|
|
451
|
+
}
|
|
435
452
|
if (command === "task") {
|
|
436
453
|
if (harness !== undefined ||
|
|
437
454
|
profiles.length > 0 ||
|
|
@@ -551,6 +568,7 @@ export function parseArgs(argv) {
|
|
|
551
568
|
rerun,
|
|
552
569
|
dryRun,
|
|
553
570
|
json,
|
|
554
|
-
yes
|
|
571
|
+
yes,
|
|
572
|
+
...(worktree === undefined ? {} : { worktree })
|
|
555
573
|
};
|
|
556
574
|
}
|
|
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
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
9
|
import { agyRuntimeStatus, agyVersionSupported, hookRegistrationDrift, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
|
|
@@ -12,8 +12,6 @@ import { readRecordedSessionId } from "../../../runtime/src/hooks/codex-loop.js"
|
|
|
12
12
|
import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
|
|
13
13
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
14
14
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
15
|
-
import { FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
16
|
-
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
17
15
|
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
18
16
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
19
17
|
import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
|
|
@@ -22,6 +20,7 @@ import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn
|
|
|
22
20
|
import { runCli } from "./cli.js";
|
|
23
21
|
import { loadEffectiveConfig, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
24
22
|
import { runHookProcess } from "./hook-process.js";
|
|
23
|
+
import { gitRunner, trustStore, worktreeDependencies } from "./parallel-deps.js";
|
|
25
24
|
import { selectYesNo, writeBanner } from "./ui.js";
|
|
26
25
|
import { CLI_VERSION } from "./version.js";
|
|
27
26
|
import { createCommandRegistry } from "./commands/index.js";
|
|
@@ -43,6 +42,7 @@ import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
|
43
42
|
import { errorEnvelope } from "./output.js";
|
|
44
43
|
import { runAgyHeadless } from "./agy-headless.js";
|
|
45
44
|
import { runWorktreeCommand } from "./commands/parallel.js";
|
|
45
|
+
import { ensureSessionWorktree, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
46
46
|
import { listWorktrees, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
|
|
47
47
|
const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
|
|
48
48
|
async function readOptionalText(path) {
|
|
@@ -78,29 +78,6 @@ async function installedManifest(root) {
|
|
|
78
78
|
async function installedHarness(root) {
|
|
79
79
|
return (await installedManifest(root))?.harness ?? [...HARNESS_IDS];
|
|
80
80
|
}
|
|
81
|
-
function gitRunner(root) {
|
|
82
|
-
return {
|
|
83
|
-
run: async (gitArgs) => {
|
|
84
|
-
try {
|
|
85
|
-
return {
|
|
86
|
-
exitCode: 0,
|
|
87
|
-
stdout: execFileSync("git", [...gitArgs], {
|
|
88
|
-
cwd: root,
|
|
89
|
-
encoding: "buffer",
|
|
90
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
91
|
-
})
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
const failure = error;
|
|
96
|
-
return {
|
|
97
|
-
exitCode: failure.status ?? 1,
|
|
98
|
-
stdout: failure.stdout ?? new Uint8Array()
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
81
|
async function confirmInit(plan, trust, warnings = []) {
|
|
105
82
|
writeBanner({
|
|
106
83
|
isTTY: process.stdout.isTTY === true,
|
|
@@ -113,59 +90,12 @@ async function confirmPlan(text) {
|
|
|
113
90
|
process.stdout.write(text);
|
|
114
91
|
return await selectYesNo("Apply this installation plan?", { input: process.stdin, output: process.stdout }, false);
|
|
115
92
|
}
|
|
116
|
-
function trustStore() {
|
|
117
|
-
const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
|
|
118
|
-
return new FileTrustStore(state.trustStore, state.anchorDirectory);
|
|
119
|
-
}
|
|
120
93
|
async function plannedTrustBinding(root, projectConfig) {
|
|
121
94
|
const config = (await loadEffectiveConfig(root, "project", projectConfig)).config;
|
|
122
95
|
return config.verification.commands.length === 0
|
|
123
96
|
? null
|
|
124
97
|
: await repositoryTrustBinding(root, config, CLI_VERSION);
|
|
125
98
|
}
|
|
126
|
-
function worktreeDependencies() {
|
|
127
|
-
const gateFor = async (root, config) => new CompletionGateService({
|
|
128
|
-
root,
|
|
129
|
-
config,
|
|
130
|
-
gitRunner: gitRunner(root),
|
|
131
|
-
taskService: new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)),
|
|
132
|
-
evidenceStore: new FileEvidenceStore(root, root)
|
|
133
|
-
});
|
|
134
|
-
return {
|
|
135
|
-
git: async (cwd, gitArgs) => await new Promise((resolve) => {
|
|
136
|
-
execFile("git", [...gitArgs], { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => resolve({
|
|
137
|
-
exitCode: error === null ? 0 : typeof error.code === "number" ? error.code : 1,
|
|
138
|
-
stdout,
|
|
139
|
-
stderr
|
|
140
|
-
}));
|
|
141
|
-
}),
|
|
142
|
-
loadConfig: async (root) => (await loadEffectiveConfig(root, "project")).config,
|
|
143
|
-
trust: {
|
|
144
|
-
status: async (root, config) => await repositoryTrust(root, config, CLI_VERSION),
|
|
145
|
-
grant: async (root, config) => await trustStore().grant(await repositoryTrustBinding(root, config, CLI_VERSION)),
|
|
146
|
-
revoke: async (root, config) => {
|
|
147
|
-
await trustStore().revoke(await repositoryTrustBinding(root, config, CLI_VERSION));
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
gate: gateFor,
|
|
151
|
-
tasks: (root) => new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)),
|
|
152
|
-
processRunner: new NodeVerificationProcessRunner(),
|
|
153
|
-
runSetup: async (cwd, step) => await new Promise((resolve) => {
|
|
154
|
-
execFile(step.command, [...step.args], {
|
|
155
|
-
cwd,
|
|
156
|
-
encoding: "utf8",
|
|
157
|
-
timeout: step.timeoutMs,
|
|
158
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
159
|
-
...(process.platform === "win32" ? { shell: true } : {})
|
|
160
|
-
}, (error, stdout, stderr) => resolve({
|
|
161
|
-
exitCode: error === null
|
|
162
|
-
? 0
|
|
163
|
-
: typeof error.code === "number" && error.killed !== true ? error.code : null,
|
|
164
|
-
output: `${stdout}${stderr}`
|
|
165
|
-
}));
|
|
166
|
-
})
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
99
|
async function worktreeDoctorProbe(root) {
|
|
170
100
|
let statuses;
|
|
171
101
|
try {
|
|
@@ -222,7 +152,7 @@ else if (argv[0] === "hook") {
|
|
|
222
152
|
stdin: process.stdin,
|
|
223
153
|
writeStdout: (value) => process.stdout.write(value),
|
|
224
154
|
writeStderr: (value) => process.stderr.write(value)
|
|
225
|
-
}, CLI_VERSION);
|
|
155
|
+
}, CLI_VERSION, { worktree: worktreeDependencies() });
|
|
226
156
|
}
|
|
227
157
|
else {
|
|
228
158
|
process.exitCode = await runCli(argv, {
|
|
@@ -418,6 +348,7 @@ else {
|
|
|
418
348
|
trustStore: store,
|
|
419
349
|
calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
|
|
420
350
|
confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
|
|
351
|
+
promptWorktree: async (message) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, true),
|
|
421
352
|
...(updateArgs.targetVersion === undefined
|
|
422
353
|
? {}
|
|
423
354
|
: { targetVersion: updateArgs.targetVersion })
|
|
@@ -448,14 +379,40 @@ else {
|
|
|
448
379
|
// run inside a session is never told which session it is in.
|
|
449
380
|
const sessionId = process.env.AGENT_OPS_SESSION_ID ??
|
|
450
381
|
await readRecordedSessionId(root);
|
|
451
|
-
const
|
|
452
|
-
?
|
|
382
|
+
const createConfig = args.action === "create"
|
|
383
|
+
? (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config
|
|
453
384
|
: undefined;
|
|
385
|
+
const policyConfigHash = createConfig === undefined
|
|
386
|
+
? undefined
|
|
387
|
+
: calculateConfigHash(createConfig);
|
|
388
|
+
// Auto mode starts the work where it belongs: a task created from
|
|
389
|
+
// the main checkout lands in the session's own worktree.
|
|
390
|
+
const worktreeDeps = worktreeDependencies();
|
|
391
|
+
const fromMain = createConfig?.worktree?.mode === "auto" &&
|
|
392
|
+
args.scope !== "user" &&
|
|
393
|
+
await resolveCheckouts(worktreeDeps, root)
|
|
394
|
+
.then(({ mainRoot, currentRoot }) => mainRoot === currentRoot)
|
|
395
|
+
.catch(() => false);
|
|
454
396
|
return await runTaskCommand({
|
|
455
397
|
args,
|
|
456
398
|
service: taskService,
|
|
457
399
|
...(policyConfigHash === undefined ? {} : { policyConfigHash }),
|
|
458
|
-
...(sessionId === undefined ? {} : { sessionId })
|
|
400
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
401
|
+
...(fromMain
|
|
402
|
+
? {
|
|
403
|
+
sessionWorktree: async (session) => {
|
|
404
|
+
const record = await ensureSessionWorktree(worktreeDeps, {
|
|
405
|
+
cwd: root,
|
|
406
|
+
sessionId: session
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
path: record.path,
|
|
410
|
+
base: record.base,
|
|
411
|
+
service: new TaskService(new FileTaskStore(join(record.path, ".agent-ops", "tasks", "state.json"), record.path))
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
: {})
|
|
459
416
|
});
|
|
460
417
|
}
|
|
461
418
|
if (args.command === "review") {
|
|
@@ -39,6 +39,7 @@ Options:
|
|
|
39
39
|
--profile <core|advisory|guardrails|loop> Repeatable
|
|
40
40
|
--review-target <codex|agy|claude> Repeatable init option; review pair order
|
|
41
41
|
--completion-gate Init only: enable the project-loop completion gate
|
|
42
|
+
--worktree <auto|off> Init/update: configure session worktree isolation
|
|
42
43
|
--check-auth Doctor only: probe selected review targets'
|
|
43
44
|
authentication with one real call
|
|
44
45
|
--check-auth-target <target> Repeatable doctor filter with --check-auth
|
|
@@ -75,6 +76,7 @@ Options:
|
|
|
75
76
|
--profile <core|advisory|guardrails|loop> Repeatable
|
|
76
77
|
--review-target <codex|agy|claude> Repeatable, in fallback-chain order
|
|
77
78
|
--completion-gate Enable the project-loop completion gate
|
|
79
|
+
--worktree <auto|off> Configure session worktree isolation
|
|
78
80
|
--dry-run Print the plan without writing
|
|
79
81
|
--json
|
|
80
82
|
--yes
|
|
@@ -114,6 +116,7 @@ Options:
|
|
|
114
116
|
--scope <project|user>
|
|
115
117
|
--harness <all|both|agy|claude|codex|opencode|comma-separated>
|
|
116
118
|
--target-version <version> Offline-capable update target
|
|
119
|
+
--worktree <auto|off> Configure session worktree isolation
|
|
117
120
|
--dry-run
|
|
118
121
|
--json
|
|
119
122
|
--yes
|
|
@@ -5,6 +5,7 @@ import { okEnvelope } from "../output.js";
|
|
|
5
5
|
import { formatOperationPlan } from "../plan-output.js";
|
|
6
6
|
import { toPublicInstallPlan } from "../public-plan.js";
|
|
7
7
|
import { formatTrustChange, planTrustGrant } from "./trust.js";
|
|
8
|
+
import { checkWorktreeEnvWarning, detectWorktreeSetup } from "../../../../runtime/src/install/parallel-setup.js";
|
|
8
9
|
export function formatInstallPlan(plan, trust, warnings = []) {
|
|
9
10
|
const hooks = plan.manifest.hooks ?? [];
|
|
10
11
|
const detectedVerification = plan.detectedVerification;
|
|
@@ -14,6 +15,13 @@ export function formatInstallPlan(plan, trust, warnings = []) {
|
|
|
14
15
|
`Scope: ${plan.scope}`,
|
|
15
16
|
`Harness: ${plan.harness}`,
|
|
16
17
|
`Profiles: ${plan.profiles.join(", ")}`,
|
|
18
|
+
...(plan.config.worktree === undefined
|
|
19
|
+
? []
|
|
20
|
+
: [
|
|
21
|
+
`Worktree: mode=${plan.config.worktree.mode}${plan.config.worktree.setup && plan.config.worktree.setup.length > 0
|
|
22
|
+
? `, setup=${plan.config.worktree.setup.map((s) => `${s.command} ${s.args.join(" ")}`.trim()).join("; ")}`
|
|
23
|
+
: ""}`
|
|
24
|
+
]),
|
|
17
25
|
...(plan.scope === "user" && plan.harness.includes("agy")
|
|
18
26
|
? ["Notice: .gemini/GEMINI.md is a shared Gemini rule surface."]
|
|
19
27
|
: []),
|
|
@@ -85,6 +93,11 @@ export async function runInitCommand(options) {
|
|
|
85
93
|
args.profiles.length === 0) {
|
|
86
94
|
throw new AgentOpsError("INIT_CHOICES_REQUIRED", "Init requires complete scope, harness, and profile choices.");
|
|
87
95
|
}
|
|
96
|
+
const worktreeConfig = args.worktree === "auto"
|
|
97
|
+
? { mode: "auto", setup: await detectWorktreeSetup(options.root) }
|
|
98
|
+
: args.worktree === "off"
|
|
99
|
+
? { mode: "off" }
|
|
100
|
+
: undefined;
|
|
88
101
|
const plan = await createInstallPlan({
|
|
89
102
|
root: options.root,
|
|
90
103
|
scope: args.scope,
|
|
@@ -105,7 +118,10 @@ export async function runInitCommand(options) {
|
|
|
105
118
|
: { reviewTargets: args.reviewTargets }),
|
|
106
119
|
...(args.completionGate === undefined
|
|
107
120
|
? {}
|
|
108
|
-
: { completionGateEnabled: args.completionGate })
|
|
121
|
+
: { completionGateEnabled: args.completionGate }),
|
|
122
|
+
...(worktreeConfig === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { worktree: worktreeConfig })
|
|
109
125
|
});
|
|
110
126
|
const trust = await trustChange(options, plan);
|
|
111
127
|
// An installation with no verifier looks finished and is not: every task
|
|
@@ -121,7 +137,14 @@ export async function runInitCommand(options) {
|
|
|
121
137
|
: ` Detection stopped because — ${plan.verificationBlockers.join("; ")}`)
|
|
122
138
|
]
|
|
123
139
|
: [];
|
|
124
|
-
const
|
|
140
|
+
const worktreeEnvWarning = plan.config.worktree?.mode === "auto"
|
|
141
|
+
? await checkWorktreeEnvWarning(options.root)
|
|
142
|
+
: undefined;
|
|
143
|
+
const worktreeWarnings = worktreeEnvWarning === undefined ? [] : [worktreeEnvWarning];
|
|
144
|
+
const warnings = [
|
|
145
|
+
...verificationWarnings,
|
|
146
|
+
...worktreeWarnings,
|
|
147
|
+
...(plan.harness.includes("agy") && options.agyWarning !== undefined
|
|
125
148
|
? (() => {
|
|
126
149
|
try {
|
|
127
150
|
const warning = options.agyWarning();
|
|
@@ -131,7 +154,8 @@ export async function runInitCommand(options) {
|
|
|
131
154
|
return ["agy could not be probed; run `agent-ops doctor` to verify it."];
|
|
132
155
|
}
|
|
133
156
|
})()
|
|
134
|
-
: [])
|
|
157
|
+
: [])
|
|
158
|
+
];
|
|
135
159
|
if (args.dryRun) {
|
|
136
160
|
return okEnvelope("INIT_PLAN_READY", {
|
|
137
161
|
applied: false,
|
|
@@ -28,16 +28,17 @@ function addText(result) {
|
|
|
28
28
|
"Work only inside that path from now on:",
|
|
29
29
|
`- Claude Code: EnterWorktree with path ${record.path}`,
|
|
30
30
|
`- Codex and agy: run every command with ${record.path} as its working directory`,
|
|
31
|
-
`Finish with: commit, agent-ops verify
|
|
32
|
-
`then agent-ops worktree finish ${record.name} from ${record.mainRoot}`,
|
|
33
|
-
"
|
|
31
|
+
`Finish with: commit, agent-ops verify and review with --base ${record.base},`,
|
|
32
|
+
`then agent-ops worktree finish ${record.name} from ${record.mainRoot}, which`,
|
|
33
|
+
"completes every task in the worktree and merges",
|
|
34
|
+
"(Claude Code: ExitWorktree with action keep before finishing; do not run task complete in the worktree)."
|
|
34
35
|
].join("\n");
|
|
35
36
|
}
|
|
36
37
|
function finishText(result) {
|
|
37
38
|
const { record } = result;
|
|
38
39
|
return [
|
|
39
40
|
`Merged ${record.branch} into ${record.targetBranch} at ${result.mergedHead}${result.rebased ? " after a clean rebase and re-verification" : ""}.`,
|
|
40
|
-
`
|
|
41
|
+
`Completed and recorded in git notes (refs/notes/agent-ops): ${result.taskIds.join(", ")}.`,
|
|
41
42
|
`Removed ${record.path}.`,
|
|
42
43
|
...result.warnings.map((warning) => `Warning: ${warning}`)
|
|
43
44
|
].join("\n");
|
|
@@ -273,7 +273,7 @@ async function reusableReview(options, sourceFingerprint, context) {
|
|
|
273
273
|
if (options.root === undefined) {
|
|
274
274
|
return null;
|
|
275
275
|
}
|
|
276
|
-
const attestation = await findReviewAttestation(options.root, sourceFingerprint);
|
|
276
|
+
const attestation = await findReviewAttestation(options.root, sourceFingerprint, context?.taskId);
|
|
277
277
|
if (attestation === null || attestation.taskId !== context?.taskId) {
|
|
278
278
|
return null;
|
|
279
279
|
}
|
|
@@ -284,7 +284,7 @@ async function reusableReview(options, sourceFingerprint, context) {
|
|
|
284
284
|
context?.policyConfigHash !== options.policyConfigHash) {
|
|
285
285
|
return null;
|
|
286
286
|
}
|
|
287
|
-
const artifact = await readReviewReportArtifact(options.root, sourceFingerprint);
|
|
287
|
+
const artifact = await readReviewReportArtifact(options.root, sourceFingerprint, attestation.taskId);
|
|
288
288
|
if (artifact === null ||
|
|
289
289
|
artifact.taskId !== attestation.taskId ||
|
|
290
290
|
artifact.report === undefined ||
|
|
@@ -488,7 +488,7 @@ export async function runReviewCommand(options) {
|
|
|
488
488
|
})
|
|
489
489
|
});
|
|
490
490
|
}
|
|
491
|
-
await invalidateReviewAttestation(options.root, sourceFingerprint);
|
|
491
|
+
await invalidateReviewAttestation(options.root, sourceFingerprint, context?.taskId);
|
|
492
492
|
pendingInvalidation = false;
|
|
493
493
|
}
|
|
494
494
|
}
|
|
@@ -605,7 +605,9 @@ export async function runReviewCommand(options) {
|
|
|
605
605
|
await options.tasks.recordEvidence(context.taskId, Object.fromEntries(finalResult.results.map((item) => [
|
|
606
606
|
item.criterionId,
|
|
607
607
|
item.evidence.map((reference) => `review:${finalResult.harness}:${reference}`)
|
|
608
|
-
]))
|
|
608
|
+
])),
|
|
609
|
+
// What `worktree finish` completes this task against.
|
|
610
|
+
scope?.mode === "base" ? scope.resolvedBase : null);
|
|
609
611
|
}
|
|
610
612
|
const message = finalResult.status === "PASS"
|
|
611
613
|
? "Independent review passed."
|
|
@@ -87,7 +87,10 @@ export async function runTaskCommand(options) {
|
|
|
87
87
|
if (options.args.title === undefined) {
|
|
88
88
|
throw new AgentOpsError("TASK_TITLE_REQUIRED", "Task creation requires --title.");
|
|
89
89
|
}
|
|
90
|
-
const
|
|
90
|
+
const worktree = sessionId === undefined || options.sessionWorktree === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: await options.sessionWorktree(sessionId);
|
|
93
|
+
const record = await (worktree?.service ?? options.service).create({
|
|
91
94
|
title: options.args.title,
|
|
92
95
|
criteria: (options.args.criteria ?? []).map(parseCriterion),
|
|
93
96
|
...(options.policyConfigHash === undefined
|
|
@@ -98,9 +101,30 @@ export async function runTaskCommand(options) {
|
|
|
98
101
|
: { parentTaskId: options.args.parentTaskId }),
|
|
99
102
|
...(sessionId === undefined ? {} : { sessionId })
|
|
100
103
|
});
|
|
101
|
-
|
|
104
|
+
const created = taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
102
105
|
? `Created task ${record.task.id}.`
|
|
103
|
-
:
|
|
106
|
+
: record.task.parentTaskId === undefined
|
|
107
|
+
? `Created and attached task ${record.task.id}.`
|
|
108
|
+
: `Created subtask ${record.task.id}; the session stays on the top of its task tree.`, record);
|
|
109
|
+
if (worktree === undefined || created.data === null)
|
|
110
|
+
return created;
|
|
111
|
+
return {
|
|
112
|
+
...created,
|
|
113
|
+
data: {
|
|
114
|
+
...created.data,
|
|
115
|
+
text: [
|
|
116
|
+
`Created in this session's worktree ${worktree.path}.`,
|
|
117
|
+
"Work only inside that path from now on:",
|
|
118
|
+
`- Claude Code: EnterWorktree with path ${worktree.path}`,
|
|
119
|
+
`- Codex and agy: run every command with ${worktree.path} as its working directory`,
|
|
120
|
+
`Commit, then run verify and review there with --base ${worktree.base};`,
|
|
121
|
+
"then leave the worktree (Claude Code: ExitWorktree with action keep) and run",
|
|
122
|
+
"agent-ops worktree finish from the main checkout, which completes the task and merges.",
|
|
123
|
+
"",
|
|
124
|
+
created.data.text
|
|
125
|
+
].join("\n")
|
|
126
|
+
}
|
|
127
|
+
};
|
|
104
128
|
}
|
|
105
129
|
if (action === "status") {
|
|
106
130
|
if (options.args.taskId === undefined && sessionId === undefined) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectWorktreeSetup } from "../../../../runtime/src/install/parallel-setup.js";
|
|
1
4
|
import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
|
|
2
5
|
import { okEnvelope } from "../output.js";
|
|
3
6
|
import { formatOperationPlan } from "../plan-output.js";
|
|
@@ -10,6 +13,13 @@ export function formatUpdatePlan(plan, trust) {
|
|
|
10
13
|
metadata: [
|
|
11
14
|
`Target version: ${plan.targetVersion}`,
|
|
12
15
|
`Harness: ${plan.installation.harness.join(", ")}`,
|
|
16
|
+
...(plan.installation.config.worktree === undefined
|
|
17
|
+
? []
|
|
18
|
+
: [
|
|
19
|
+
`Worktree: mode=${plan.installation.config.worktree.mode}${plan.installation.config.worktree.setup && plan.installation.config.worktree.setup.length > 0
|
|
20
|
+
? `, setup=${plan.installation.config.worktree.setup.map((s) => `${s.command} ${s.args.join(" ")}`.trim()).join("; ")}`
|
|
21
|
+
: ""}`
|
|
22
|
+
]),
|
|
13
23
|
...(trust === undefined ? [] : formatTrustChange(trust)),
|
|
14
24
|
`Schema migrations: ${plan.migrationSteps.length === 0
|
|
15
25
|
? "none"
|
|
@@ -47,7 +57,40 @@ async function trustChange(options, plan) {
|
|
|
47
57
|
? { action: "skipped", reason: "no-verification-commands" }
|
|
48
58
|
: await planTrustGrant(binding, options.trustStore);
|
|
49
59
|
}
|
|
60
|
+
async function existingConfigHasWorktree(root) {
|
|
61
|
+
try {
|
|
62
|
+
const content = await readFile(join(root, ".agent-ops", "config.json"), "utf8");
|
|
63
|
+
const parsed = JSON.parse(content);
|
|
64
|
+
return parsed.worktree !== undefined;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
50
70
|
export async function runUpdateCommand(options) {
|
|
71
|
+
let worktree;
|
|
72
|
+
if (options.args.worktree === "auto") {
|
|
73
|
+
worktree = {
|
|
74
|
+
mode: "auto",
|
|
75
|
+
setup: await detectWorktreeSetup(options.root)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
else if (options.args.worktree === "off") {
|
|
79
|
+
worktree = { mode: "off" };
|
|
80
|
+
}
|
|
81
|
+
else if (options.args.worktree === undefined &&
|
|
82
|
+
options.promptWorktree !== undefined &&
|
|
83
|
+
options.isTTY &&
|
|
84
|
+
!options.args.yes &&
|
|
85
|
+
!(await existingConfigHasWorktree(options.root))) {
|
|
86
|
+
const shouldEnable = await options.promptWorktree("Enable Git worktree isolation for parallel agent sessions?");
|
|
87
|
+
if (shouldEnable) {
|
|
88
|
+
worktree = {
|
|
89
|
+
mode: "auto",
|
|
90
|
+
setup: await detectWorktreeSetup(options.root)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
51
94
|
const plan = await createUpdatePlan({
|
|
52
95
|
root: options.root,
|
|
53
96
|
adapters: options.adapters,
|
|
@@ -57,9 +100,9 @@ export async function runUpdateCommand(options) {
|
|
|
57
100
|
...(options.registry === undefined
|
|
58
101
|
? {}
|
|
59
102
|
: { registry: options.registry }),
|
|
60
|
-
...(options.targetVersion === undefined
|
|
103
|
+
...((options.targetVersion ?? options.args.targetVersion) === undefined
|
|
61
104
|
? {}
|
|
62
|
-
: { targetVersion: options.targetVersion }),
|
|
105
|
+
: { targetVersion: options.targetVersion ?? options.args.targetVersion }),
|
|
63
106
|
...(options.toolkitVersion === undefined
|
|
64
107
|
? {}
|
|
65
108
|
: { toolkitVersion: options.toolkitVersion }),
|
|
@@ -68,7 +111,8 @@ export async function runUpdateCommand(options) {
|
|
|
68
111
|
: { hookRuntimePath: options.hookRuntimePath }),
|
|
69
112
|
...((options.hookTargets ?? options.args.hookTargets) === undefined
|
|
70
113
|
? {}
|
|
71
|
-
: { hookTargets: options.hookTargets ?? options.args.hookTargets })
|
|
114
|
+
: { hookTargets: options.hookTargets ?? options.args.hookTargets }),
|
|
115
|
+
...(worktree === undefined ? {} : { worktree })
|
|
72
116
|
});
|
|
73
117
|
const trust = await trustChange(options, plan);
|
|
74
118
|
if (options.args.dryRun) {
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// node <this file> <harness> <event> --managed-by=agent-ops
|
|
4
4
|
import { CLI_VERSION } from "./version.js";
|
|
5
5
|
import { runHookProcess } from "./hook-process.js";
|
|
6
|
+
import { worktreeDependencies } from "./parallel-deps.js";
|
|
6
7
|
process.exitCode = await runHookProcess(process.argv.slice(2), {
|
|
7
8
|
stdin: process.stdin,
|
|
8
9
|
writeStdout: (value) => process.stdout.write(value),
|
|
9
10
|
writeStderr: (value) => process.stderr.write(value)
|
|
10
|
-
}, CLI_VERSION);
|
|
11
|
+
}, CLI_VERSION, { worktree: worktreeDependencies() });
|
|
@@ -10,6 +10,7 @@ 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
12
|
import { evaluateWorktreeWrite, resolveMainRoot } from "../../../runtime/src/parallel/guard.js";
|
|
13
|
+
import { ensureSessionWorktree } from "../../../runtime/src/parallel/service.js";
|
|
13
14
|
import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
|
|
14
15
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
15
16
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
@@ -348,7 +349,9 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
348
349
|
const mainRoot = await resolveMainRoot(gitRunner);
|
|
349
350
|
return mainRoot === null
|
|
350
351
|
? { action: "continue", status: "UNKNOWN", code: "WORKTREE_GUARD_UNAVAILABLE" }
|
|
351
|
-
: await evaluateWorktreeWrite(mainRoot, write.paths, write.sessionId
|
|
352
|
+
: await evaluateWorktreeWrite(mainRoot, write.paths, write.sessionId, dependencies.worktree === undefined
|
|
353
|
+
? undefined
|
|
354
|
+
: async (sessionId) => (await ensureSessionWorktree(dependencies.worktree, { cwd: mainRoot, sessionId })).path);
|
|
352
355
|
}
|
|
353
356
|
: undefined;
|
|
354
357
|
const output = await runHookCommand({
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
|
|
5
|
+
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
6
|
+
import { FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
7
|
+
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
8
|
+
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
9
|
+
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
10
|
+
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
11
|
+
import { loadEffectiveConfig, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
12
|
+
import { CLI_VERSION } from "./version.js";
|
|
13
|
+
// Shared by the CLI and the hook entry point: Claude Code runs hooks through
|
|
14
|
+
// hook-entry.js, never bin.js, and its worktree guard needs these too.
|
|
15
|
+
export function gitRunner(root) {
|
|
16
|
+
return {
|
|
17
|
+
run: async (gitArgs) => {
|
|
18
|
+
try {
|
|
19
|
+
return {
|
|
20
|
+
exitCode: 0,
|
|
21
|
+
stdout: execFileSync("git", [...gitArgs], {
|
|
22
|
+
cwd: root,
|
|
23
|
+
encoding: "buffer",
|
|
24
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const failure = error;
|
|
30
|
+
return {
|
|
31
|
+
exitCode: failure.status ?? 1,
|
|
32
|
+
stdout: failure.stdout ?? new Uint8Array()
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function trustStore() {
|
|
39
|
+
const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
|
|
40
|
+
return new FileTrustStore(state.trustStore, state.anchorDirectory);
|
|
41
|
+
}
|
|
42
|
+
export function worktreeDependencies() {
|
|
43
|
+
const gateFor = async (root, config) => new CompletionGateService({
|
|
44
|
+
root,
|
|
45
|
+
config,
|
|
46
|
+
gitRunner: gitRunner(root),
|
|
47
|
+
taskService: new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)),
|
|
48
|
+
evidenceStore: new FileEvidenceStore(root, root)
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
git: async (cwd, gitArgs) => await new Promise((resolve) => {
|
|
52
|
+
execFile("git", [...gitArgs], { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => resolve({
|
|
53
|
+
exitCode: error === null ? 0 : typeof error.code === "number" ? error.code : 1,
|
|
54
|
+
stdout,
|
|
55
|
+
stderr
|
|
56
|
+
}));
|
|
57
|
+
}),
|
|
58
|
+
loadConfig: async (root) => (await loadEffectiveConfig(root, "project")).config,
|
|
59
|
+
trust: {
|
|
60
|
+
status: async (root, config) => await repositoryTrust(root, config, CLI_VERSION),
|
|
61
|
+
grant: async (root, config) => await trustStore().grant(await repositoryTrustBinding(root, config, CLI_VERSION)),
|
|
62
|
+
revoke: async (root, config) => {
|
|
63
|
+
await trustStore().revoke(await repositoryTrustBinding(root, config, CLI_VERSION));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
gate: gateFor,
|
|
67
|
+
tasks: (root, base) => new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root), base === undefined ? {} : { completion: {
|
|
68
|
+
root,
|
|
69
|
+
gitRunner: gitRunner(root),
|
|
70
|
+
base,
|
|
71
|
+
loadConfig: async () => (await loadEffectiveConfig(root, "project")).config
|
|
72
|
+
} }),
|
|
73
|
+
processRunner: new NodeVerificationProcessRunner(),
|
|
74
|
+
runSetup: async (cwd, step) => await new Promise((resolve) => {
|
|
75
|
+
execFile(step.command, [...step.args], {
|
|
76
|
+
cwd,
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
timeout: step.timeoutMs,
|
|
79
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
80
|
+
...(process.platform === "win32" ? { shell: true } : {})
|
|
81
|
+
}, (error, stdout, stderr) => resolve({
|
|
82
|
+
exitCode: error === null
|
|
83
|
+
? 0
|
|
84
|
+
: typeof error.code === "number" && error.killed !== true ? error.code : null,
|
|
85
|
+
output: `${stdout}${stderr}`
|
|
86
|
+
}));
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
}
|