@kylecheng3146/agent-ops 0.5.3 → 0.7.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/README.md +103 -1
- package/dist/packages/cli/src/args.js +183 -3
- package/dist/packages/cli/src/bin.js +76 -19
- package/dist/packages/cli/src/cli.js +53 -8
- package/dist/packages/cli/src/codex-loop-process.js +19 -2
- package/dist/packages/cli/src/commands/advance.js +42 -5
- package/dist/packages/cli/src/commands/init.js +2 -0
- package/dist/packages/cli/src/commands/office.js +20 -0
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +100 -9
- package/dist/packages/cli/src/commands/run.js +115 -0
- package/dist/packages/cli/src/commands/task.js +61 -17
- package/dist/packages/cli/src/commands/update.js +8 -0
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +64 -1
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +54 -3
- package/dist/packages/cli/src/loop-entry.js +2 -1
- package/dist/packages/cli/src/office-entry.js +230 -0
- package/dist/packages/cli/src/owned-run-step.js +65 -0
- package/dist/packages/cli/src/parallel-deps.js +18 -5
- package/dist/packages/cli/src/run-deps.js +321 -0
- package/dist/packages/cli/src/run-entry.js +952 -0
- package/dist/packages/cli/src/run-setup-entry.js +32 -0
- package/dist/packages/cli/src/run-step-entry.js +58 -0
- package/dist/packages/cli/src/wizard.js +14 -2
- package/dist/runtime/src/adapters/agy/config.js +4 -3
- package/dist/runtime/src/adapters/agy/events.js +9 -0
- package/dist/runtime/src/adapters/claude/config.js +3 -3
- package/dist/runtime/src/adapters/claude/events.js +8 -0
- package/dist/runtime/src/adapters/codex/config.js +3 -3
- package/dist/runtime/src/adapters/codex/events.js +8 -0
- package/dist/runtime/src/adapters/opencode/config.js +5 -4
- package/dist/runtime/src/adapters/opencode/events.js +8 -0
- package/dist/runtime/src/config/explain.js +11 -0
- package/dist/runtime/src/config/hash.js +1 -1
- package/dist/runtime/src/config/merge.js +9 -1
- package/dist/runtime/src/contracts.js +2 -2
- package/dist/runtime/src/hooks/codex-loop.js +4 -0
- package/dist/runtime/src/hooks/completion-gate.js +8 -2
- package/dist/runtime/src/install/doctor.js +2 -0
- package/dist/runtime/src/install/harness.js +44 -1
- package/dist/runtime/src/install/plan.js +16 -11
- package/dist/runtime/src/install/preauth.js +44 -18
- package/dist/runtime/src/install/profiles.js +8 -2
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/office/collect.js +233 -0
- package/dist/runtime/src/office/page.js +873 -0
- package/dist/runtime/src/office/scene.js +168 -0
- package/dist/runtime/src/office/server.js +139 -0
- package/dist/runtime/src/office/sessions.js +115 -0
- package/dist/runtime/src/office/snapshot.js +107 -0
- package/dist/runtime/src/parallel/finish.js +85 -22
- package/dist/runtime/src/parallel/guard.js +12 -2
- package/dist/runtime/src/parallel/integrate.js +288 -10
- package/dist/runtime/src/parallel/manage.js +4 -1
- package/dist/runtime/src/parallel/receipt.js +71 -5
- package/dist/runtime/src/parallel/service.js +41 -1
- package/dist/runtime/src/review/attestation.js +36 -11
- package/dist/runtime/src/review/execute.js +24 -1
- package/dist/runtime/src/review/packet.js +35 -1
- package/dist/runtime/src/review/runner.js +12 -1
- package/dist/runtime/src/review/scope.js +18 -1
- package/dist/runtime/src/review/slots.js +35 -10
- package/dist/runtime/src/run/authorization.js +60 -0
- package/dist/runtime/src/run/budget.js +30 -0
- package/dist/runtime/src/run/controls.js +139 -0
- package/dist/runtime/src/run/convergence.js +177 -0
- package/dist/runtime/src/run/hosts/claude.js +180 -0
- package/dist/runtime/src/run/hosts/codex.js +279 -0
- package/dist/runtime/src/run/hosts/index.js +4 -0
- package/dist/runtime/src/run/hosts/protocol.js +254 -0
- package/dist/runtime/src/run/hosts/types.js +1 -0
- package/dist/runtime/src/run/hosts/util.js +136 -0
- package/dist/runtime/src/run/integration.js +204 -0
- package/dist/runtime/src/run/macOS.js +189 -0
- package/dist/runtime/src/run/ownership.js +22 -0
- package/dist/runtime/src/run/phase.js +37 -0
- package/dist/runtime/src/run/planning.js +25 -0
- package/dist/runtime/src/run/policy-transition.js +1076 -0
- package/dist/runtime/src/run/policy.js +176 -0
- package/dist/runtime/src/run/ratchet.js +71 -0
- package/dist/runtime/src/run/scheduler.js +260 -0
- package/dist/runtime/src/run/service.js +433 -0
- package/dist/runtime/src/run/supervisor.js +635 -0
- package/dist/runtime/src/run/transport.js +159 -0
- package/dist/runtime/src/run/types.js +1 -0
- package/dist/runtime/src/run/usage.js +81 -0
- package/dist/runtime/src/run/verification.js +64 -0
- package/dist/runtime/src/run/worker-gate.js +48 -0
- package/dist/runtime/src/schema/acceptance.js +91 -0
- package/dist/runtime/src/schema/validate.js +80 -9
- package/dist/runtime/src/task/completion.js +14 -2
- package/dist/runtime/src/task/contract.js +17 -0
- package/dist/runtime/src/task/pin-finding.js +81 -0
- package/dist/runtime/src/task/service.js +316 -13
- package/dist/runtime/src/task/store.js +57 -7
- package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
- package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
- package/dist/runtime/src/verify/acceptance-replay.js +239 -0
- package/dist/runtime/src/verify/adapters/index.js +5 -0
- package/dist/runtime/src/verify/adapters/jest.js +142 -0
- package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
- package/dist/runtime/src/verify/adapters/node.js +254 -0
- package/dist/runtime/src/verify/adapters/pytest.js +188 -0
- package/dist/runtime/src/verify/adapters/rust.js +84 -0
- package/dist/runtime/src/verify/adapters/shared.js +82 -0
- package/dist/runtime/src/verify/adapters/vitest.js +218 -0
- package/dist/runtime/src/verify/evidence.js +5 -2
- package/dist/runtime/src/verify/service.js +24 -8
- package/dist/runtime/src/verify/source-fingerprint.js +1 -1
- package/dist/runtime/src/verify/spawn.js +13 -2
- package/docs/en/spec/acceptance-and-evidence.md +48 -0
- package/docs/en/spec/harness-adapters.md +113 -1
- package/docs/en/spec/loop-engineering.md +49 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
- package/docs/zh-TW/spec/harness-adapters.md +93 -1
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +230 -19
- package/schemas/evidence.schema.json +129 -6
- package/schemas/task.schema.json +265 -6
- package/templates/acceptance/agent_ops_acceptance.rs +36 -0
- package/templates/acceptance/agent_ops_pytest.py +46 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FileRunRepository } from "../../../runtime/src/run/service.js";
|
|
2
|
+
import { readRunPolicy, runRuntimeHash } from "../../../runtime/src/run/policy.js";
|
|
3
|
+
import { activeWallTimeMs } from "../../../runtime/src/run/scheduler.js";
|
|
4
|
+
import { runVerificationCommand, NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
5
|
+
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { realpath } from "node:fs/promises";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
// This entry is released by run-step-entry only after its process group is registered.
|
|
10
|
+
const [commonDir, runId, root, index, expectedPolicy] = process.argv.slice(2);
|
|
11
|
+
if ([commonDir, runId, root, index, expectedPolicy].some(v => v === undefined))
|
|
12
|
+
throw new Error("Incomplete run setup identity.");
|
|
13
|
+
const repository = new FileRunRepository(join(commonDir, "agent-ops/runs"), commonDir);
|
|
14
|
+
const state = await repository.read(runId);
|
|
15
|
+
const group = process.env.AGENT_OPS_RUN_PROOF_PID;
|
|
16
|
+
const actualCommon = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8" }).trim();
|
|
17
|
+
if (state === null || state.status !== "active" || state.disableRestart || state.proofProcess?.processId !== Number(group) ||
|
|
18
|
+
state.policyBinding?.artifactDigest !== expectedPolicy || await realpath(actualCommon) !== await realpath(state.commonDir))
|
|
19
|
+
throw new Error("Stale run setup authorization.");
|
|
20
|
+
const policy = await readRunPolicy(state, await runRuntimeHash());
|
|
21
|
+
const step = /^(?:0|[1-9][0-9]*)$/u.test(index) ? policy?.config.worktree?.setup?.[Number(index)] : undefined;
|
|
22
|
+
if (step === undefined)
|
|
23
|
+
throw new Error("Setup must select an authorized fixed policy command.");
|
|
24
|
+
const remaining = state.budget.limitMs - activeWallTimeMs(state.budget.activeIntervals, Date.now());
|
|
25
|
+
if (remaining <= 0)
|
|
26
|
+
throw new Error("Run budget is exhausted.");
|
|
27
|
+
const result = await runVerificationCommand({ id: "run-setup", ...step, cwd: ".", required: true,
|
|
28
|
+
timeoutMs: Math.min(step.timeoutMs ?? 600000, remaining), evidence: { kind: "exit-code" } }, {
|
|
29
|
+
cwd: root, runner: new NodeVerificationProcessRunner({ processGroupId: Number(group) })
|
|
30
|
+
});
|
|
31
|
+
process.stdout.write(JSON.stringify({ exitCode: result.status === "PASS" ? 0 : result.exitCode ?? 1,
|
|
32
|
+
output: redactSecrets(result.stdout + result.stderr) }) + "\n");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
/** Dormant process group: the supervisor records this PID before releasing the CLI. */
|
|
4
|
+
const cli = fileURLToPath(new URL("./bin.js", import.meta.url));
|
|
5
|
+
const setup = fileURLToPath(new URL("./run-setup-entry.js", import.meta.url));
|
|
6
|
+
const target = process.argv[2];
|
|
7
|
+
if (target !== cli && target !== setup)
|
|
8
|
+
throw new Error("Run proof entry must use its own installed execution entry.");
|
|
9
|
+
let input = "";
|
|
10
|
+
let started = false;
|
|
11
|
+
process.stdin.setEncoding("utf8");
|
|
12
|
+
process.stdin.on("data", chunk => {
|
|
13
|
+
if (started)
|
|
14
|
+
return;
|
|
15
|
+
input += chunk;
|
|
16
|
+
if (input.length > 16 || (input.includes("\n") && input !== "start\n"))
|
|
17
|
+
process.exit(1);
|
|
18
|
+
if (input !== "start\n")
|
|
19
|
+
return;
|
|
20
|
+
started = true;
|
|
21
|
+
// Keep this identity alive until the CLI finishes its signal cleanup.
|
|
22
|
+
process.on("SIGTERM", () => { });
|
|
23
|
+
process.on("SIGINT", () => { });
|
|
24
|
+
process.stdin.pause();
|
|
25
|
+
process.env.AGENT_OPS_RUN_PROOF_PID = String(process.pid);
|
|
26
|
+
const child = spawn(process.execPath, [target, ...process.argv.slice(3)], { env: process.env, stdio: ["ignore", "inherit", "inherit"] });
|
|
27
|
+
child.once("error", () => process.exit(1));
|
|
28
|
+
child.once("close", (code, signal) => {
|
|
29
|
+
const deadline = Date.now() + 3000;
|
|
30
|
+
const reconcile = () => {
|
|
31
|
+
const listing = spawn("ps", ["-axo", "pid=,pgid="], { detached: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
32
|
+
let output = "";
|
|
33
|
+
listing.stdout.on("data", chunk => { output += chunk.toString(); });
|
|
34
|
+
listing.once("error", () => process.kill(-process.pid, "SIGKILL"));
|
|
35
|
+
listing.once("close", status => {
|
|
36
|
+
if (status !== 0)
|
|
37
|
+
process.kill(-process.pid, "SIGKILL");
|
|
38
|
+
const remaining = output.split("\n").some(line => {
|
|
39
|
+
const [pid, group] = line.trim().split(/\s+/u).map(Number);
|
|
40
|
+
return group === process.pid && pid !== process.pid;
|
|
41
|
+
});
|
|
42
|
+
if (remaining && Date.now() < deadline) {
|
|
43
|
+
setTimeout(reconcile, 50);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (remaining)
|
|
47
|
+
process.kill(-process.pid, "SIGKILL");
|
|
48
|
+
else if (signal !== null)
|
|
49
|
+
process.exit(1);
|
|
50
|
+
else
|
|
51
|
+
process.exit(code ?? 1);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
reconcile();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
process.stdin.on("end", () => { if (!started)
|
|
58
|
+
process.exit(1); });
|
|
@@ -3,7 +3,7 @@ import { DEFAULT_REVIEW_TARGETS, REVIEW_TARGET_ORDER } from "../../../runtime/sr
|
|
|
3
3
|
import { HARNESS_IDS, resolveHarnessSelection } from "../../../runtime/src/install/harness.js";
|
|
4
4
|
import { selectOption, selectOptions } from "./ui.js";
|
|
5
5
|
const SCOPES = new Set(["project", "user"]);
|
|
6
|
-
const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
|
|
6
|
+
const PROFILES = new Set(["advisory", "core", "guardrails", "loop", "run"]);
|
|
7
7
|
const DEFAULT_HARNESS = [];
|
|
8
8
|
const REVIEW_TARGET_SET = new Set(REVIEW_TARGET_ORDER);
|
|
9
9
|
const REVIEW_TARGET_CHOICES = DEFAULT_REVIEW_TARGETS.map((id) => ({
|
|
@@ -77,6 +77,11 @@ const PROFILE_CHOICES = [
|
|
|
77
77
|
label: "loop",
|
|
78
78
|
value: "loop",
|
|
79
79
|
description: "Installs the local loop; agy uses its supported lifecycle subset and is reported as degraded."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: "run",
|
|
83
|
+
value: "run",
|
|
84
|
+
description: "Adds loop; Claude Code and Codex hand changes needing more than five acceptance criteria to agent-ops run."
|
|
80
85
|
}
|
|
81
86
|
];
|
|
82
87
|
const WIZARD_SUBTITLE = "Safe setup for agy, Codex, Claude Code, and opencode with profile-driven rules, verification, and hooks.";
|
|
@@ -207,12 +212,17 @@ export async function completeInitChoices(args, io, setup = {}) {
|
|
|
207
212
|
}
|
|
208
213
|
], selectorIo))
|
|
209
214
|
: undefined);
|
|
215
|
+
const office = args.office ?? (await selectOption("Enable Office (Preview)?", [
|
|
216
|
+
{ label: "no", value: "off", description: "Default. Keep the pixel office disabled." },
|
|
217
|
+
{ label: "yes", value: "on", description: "Show sessions and teams in a local, read-only pixel office." }
|
|
218
|
+
], selectorIo));
|
|
210
219
|
await probeReviewTargets(reviewTargets, setup);
|
|
211
220
|
return {
|
|
212
221
|
...args,
|
|
213
222
|
scope,
|
|
214
223
|
harness,
|
|
215
224
|
profiles,
|
|
225
|
+
office,
|
|
216
226
|
...(completionGate ? { completionGate: true } : {}),
|
|
217
227
|
...(reviewTargets.length === 0 ? {} : { reviewTargets }),
|
|
218
228
|
...(worktree !== undefined ? { worktree } : {})
|
|
@@ -226,7 +236,7 @@ export async function completeInitChoices(args, io, setup = {}) {
|
|
|
226
236
|
selectHarness(await session.question(`Harness (${HARNESS_IDS.join(",")}): `));
|
|
227
237
|
const profiles = args.profiles.length > 0
|
|
228
238
|
? args.profiles
|
|
229
|
-
: selectProfiles(await session.question("Profiles (core,advisory,guardrails,loop) [core]: "));
|
|
239
|
+
: selectProfiles(await session.question("Profiles (core,advisory,guardrails,loop,run) [core]: "));
|
|
230
240
|
const completionGate = args.completionGate ?? (completionGateEligible(scope, harness, profiles)
|
|
231
241
|
? !/^(n|no)$/i.test((await session.question("Enable the recommended agy completion gate? [Y/n]: ")).trim())
|
|
232
242
|
: false);
|
|
@@ -238,11 +248,13 @@ export async function completeInitChoices(args, io, setup = {}) {
|
|
|
238
248
|
const worktree = args.worktree ?? (scope === "project"
|
|
239
249
|
? (!/^(n|no)$/i.test((await session.question("Enable Git worktree isolation for parallel agent sessions? [Y/n]: ")).trim()) ? "auto" : "off")
|
|
240
250
|
: undefined);
|
|
251
|
+
const office = args.office ?? (affirmative(await session.question("Enable Office (Preview)? [y/N]: ")) ? "on" : "off");
|
|
241
252
|
return {
|
|
242
253
|
...args,
|
|
243
254
|
scope,
|
|
244
255
|
harness,
|
|
245
256
|
profiles,
|
|
257
|
+
office,
|
|
246
258
|
...(completionGate ? { completionGate: true } : {}),
|
|
247
259
|
...(reviewTargets.length === 0 ? {} : { reviewTargets }),
|
|
248
260
|
...(worktree !== undefined ? { worktree } : {})
|
|
@@ -54,13 +54,14 @@ export function buildAgyHookSettings(capabilities, runtimePath, platform = proce
|
|
|
54
54
|
throw new AgentOpsError("AGY_HOOK_PATH_INVALID", "agy hook runtime path is invalid.");
|
|
55
55
|
}
|
|
56
56
|
const hooks = {};
|
|
57
|
-
if (capabilities.includes("lifecycle-summary") || capabilities.includes("project-loop")) {
|
|
57
|
+
if ((capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence")) || capabilities.includes("project-loop")) {
|
|
58
58
|
hooks.PreInvocation = [handler(runtimePath, "SessionStart", platform)];
|
|
59
59
|
}
|
|
60
|
-
if (capabilities.includes("command-policy") || capabilities.includes("project-loop")) {
|
|
60
|
+
if ((capabilities.includes("command-policy") || capabilities.includes("office-presence")) || capabilities.includes("project-loop")) {
|
|
61
61
|
hooks.PreToolUse = [{ matcher: "run_command", hooks: [handler(runtimePath, "PreToolUse", platform)] }];
|
|
62
62
|
}
|
|
63
|
-
if (capabilities.includes("
|
|
63
|
+
if (capabilities.includes("office-presence") ||
|
|
64
|
+
capabilities.includes("optional-stop-verify") ||
|
|
64
65
|
capabilities.includes("completion-gate")) {
|
|
65
66
|
hooks.Stop = [handler(runtimePath, "Stop", platform, capabilities.includes("completion-gate"))];
|
|
66
67
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export const AGY_CAPABILITY_REGISTRATIONS = [
|
|
2
|
+
{
|
|
3
|
+
capability: "office-presence",
|
|
4
|
+
normalizedEvent: "session-start",
|
|
5
|
+
nativeEvent: "SessionStart",
|
|
6
|
+
hostEvent: "PreInvocation",
|
|
7
|
+
surfaceId: "agy-hooks",
|
|
8
|
+
support: "degraded",
|
|
9
|
+
runtimeFailure: "fail-open"
|
|
10
|
+
},
|
|
2
11
|
{
|
|
3
12
|
capability: "lifecycle-summary",
|
|
4
13
|
normalizedEvent: "session-start",
|
|
@@ -110,10 +110,10 @@ export function buildClaudeHookSettings(capabilities, runtimePath, platform = pr
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
|
-
if (capabilities.includes("lifecycle-summary")) {
|
|
113
|
+
if (capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence")) {
|
|
114
114
|
hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
|
|
115
115
|
}
|
|
116
|
-
if (capabilities.includes("command-policy")) {
|
|
116
|
+
if (capabilities.includes("command-policy") || capabilities.includes("office-presence")) {
|
|
117
117
|
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -122,7 +122,7 @@ export function buildClaudeHookSettings(capabilities, runtimePath, platform = pr
|
|
|
122
122
|
if (capabilities.includes("completion-gate")) {
|
|
123
123
|
hooks.Stop = [matcherGroup("Stop", runtimePath, true)];
|
|
124
124
|
}
|
|
125
|
-
else if (capabilities.includes("optional-stop-verify")) {
|
|
125
|
+
else if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
126
126
|
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
127
127
|
}
|
|
128
128
|
return { hooks };
|
|
@@ -14,6 +14,14 @@ export function claudeNonInteractiveTrust(printMode) {
|
|
|
14
14
|
return printMode ? "dialog-skipped" : "interactive-dialog";
|
|
15
15
|
}
|
|
16
16
|
export const CLAUDE_CAPABILITY_REGISTRATIONS = [
|
|
17
|
+
{
|
|
18
|
+
capability: "office-presence",
|
|
19
|
+
normalizedEvent: "session-start",
|
|
20
|
+
nativeEvent: "SessionStart",
|
|
21
|
+
surfaceId: "claude-settings",
|
|
22
|
+
support: "supported",
|
|
23
|
+
runtimeFailure: "fail-open"
|
|
24
|
+
},
|
|
17
25
|
{
|
|
18
26
|
capability: "lifecycle-summary",
|
|
19
27
|
normalizedEvent: "session-start",
|
|
@@ -79,14 +79,14 @@ export function buildCodexHookConfig(capabilities, runtimePath) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
else {
|
|
82
|
-
if (capabilities.includes("lifecycle-summary")) {
|
|
82
|
+
if (capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence")) {
|
|
83
83
|
hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
|
|
84
84
|
}
|
|
85
|
-
if (capabilities.includes("command-policy")) {
|
|
85
|
+
if (capabilities.includes("command-policy") || capabilities.includes("office-presence")) {
|
|
86
86
|
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
if (capabilities.includes("optional-stop-verify")) {
|
|
89
|
+
if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
90
90
|
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
91
91
|
}
|
|
92
92
|
return {
|
|
@@ -14,6 +14,14 @@ export function codexMatcherSupport(event) {
|
|
|
14
14
|
return event === "PreToolUse" ? "tool-name" : "unsupported";
|
|
15
15
|
}
|
|
16
16
|
export const CODEX_CAPABILITY_REGISTRATIONS = [
|
|
17
|
+
{
|
|
18
|
+
capability: "office-presence",
|
|
19
|
+
normalizedEvent: "session-start",
|
|
20
|
+
nativeEvent: "SessionStart",
|
|
21
|
+
surfaceId: "codex-hooks",
|
|
22
|
+
support: "degraded",
|
|
23
|
+
runtimeFailure: "fail-open"
|
|
24
|
+
},
|
|
17
25
|
{
|
|
18
26
|
capability: "lifecycle-summary",
|
|
19
27
|
normalizedEvent: "session-start",
|
|
@@ -67,13 +67,13 @@ function assertRuntimePath(runtimePath) {
|
|
|
67
67
|
}
|
|
68
68
|
export function opencodePluginEvents(capabilities) {
|
|
69
69
|
const events = [];
|
|
70
|
-
if (capabilities.includes("lifecycle-summary")) {
|
|
70
|
+
if (capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence")) {
|
|
71
71
|
events.push("SessionStart");
|
|
72
72
|
}
|
|
73
|
-
if (capabilities.includes("command-policy")) {
|
|
73
|
+
if (capabilities.includes("command-policy") || capabilities.includes("office-presence")) {
|
|
74
74
|
events.push("PreToolUse");
|
|
75
75
|
}
|
|
76
|
-
if (capabilities.includes("optional-stop-verify")) {
|
|
76
|
+
if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
77
77
|
events.push("Stop");
|
|
78
78
|
}
|
|
79
79
|
return events;
|
|
@@ -91,6 +91,7 @@ export function buildOpencodePlugin(capabilities, runtimePath) {
|
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
93
|
assertRuntimePath(runtimePath);
|
|
94
|
+
const displayOnly = capabilities.includes("office-presence") && !capabilities.includes("command-policy");
|
|
94
95
|
const lines = [
|
|
95
96
|
"// Managed by agent-ops. Do not edit: `agent-ops update` rewrites this file.",
|
|
96
97
|
`const RUNTIME_PATH = ${JSON.stringify(runtimePath)};`,
|
|
@@ -115,7 +116,7 @@ export function buildOpencodePlugin(capabilities, runtimePath) {
|
|
|
115
116
|
"async function runManagedHook($, event, payload) {",
|
|
116
117
|
" const decision = await invokeRuntime($, event, payload, payload.projectRoot);",
|
|
117
118
|
" if (decision === null) {",
|
|
118
|
-
" if (event === \"PreToolUse\") throw new Error(\"agent-ops: command policy is unavailable\");",
|
|
119
|
+
...(displayOnly ? [] : [" if (event === \"PreToolUse\") throw new Error(\"agent-ops: command policy is unavailable\");"]),
|
|
119
120
|
" return;",
|
|
120
121
|
" }",
|
|
121
122
|
" if (decision.decision === \"deny\") {",
|
|
@@ -22,6 +22,14 @@ export const OPENCODE_STOP_BLOCKING = "UNKNOWN";
|
|
|
22
22
|
*/
|
|
23
23
|
export const OPENCODE_SESSION_START_FIDELITY = "app-init";
|
|
24
24
|
export const OPENCODE_CAPABILITY_REGISTRATIONS = [
|
|
25
|
+
{
|
|
26
|
+
capability: "office-presence",
|
|
27
|
+
normalizedEvent: "session-start",
|
|
28
|
+
nativeEvent: "SessionStart",
|
|
29
|
+
surfaceId: "opencode-plugin",
|
|
30
|
+
support: "degraded",
|
|
31
|
+
runtimeFailure: "fail-open"
|
|
32
|
+
},
|
|
25
33
|
{
|
|
26
34
|
capability: "lifecycle-summary",
|
|
27
35
|
normalizedEvent: "session-start",
|
|
@@ -2,6 +2,11 @@ export function explainConfig(merged) {
|
|
|
2
2
|
return {
|
|
3
3
|
schemaVersion: merged.config.schemaVersion,
|
|
4
4
|
features: {
|
|
5
|
+
office: {
|
|
6
|
+
enabled: merged.config.features.office?.enabled === true,
|
|
7
|
+
source: merged.provenance.features.source,
|
|
8
|
+
sourcePath: merged.provenance.features.sourcePath
|
|
9
|
+
},
|
|
5
10
|
completionGate: {
|
|
6
11
|
enabled: merged.config.features.completionGate.enabled,
|
|
7
12
|
source: merged.provenance.features.source,
|
|
@@ -29,6 +34,12 @@ export function explainConfig(merged) {
|
|
|
29
34
|
source: entry.source,
|
|
30
35
|
sourcePath: entry.sourcePath
|
|
31
36
|
})),
|
|
37
|
+
...(merged.provenance.acceptanceRunners === undefined ? {} : {
|
|
38
|
+
acceptanceRunners: merged.provenance.acceptanceRunners.map(entry => ({
|
|
39
|
+
id: entry.value.id, adapter: entry.value.adapter,
|
|
40
|
+
source: entry.source, sourcePath: entry.sourcePath
|
|
41
|
+
}))
|
|
42
|
+
}),
|
|
32
43
|
pathMappings: merged.provenance.pathMappings.map((entry) => ({
|
|
33
44
|
path: entry.value.path,
|
|
34
45
|
verifierIds: [...entry.value.verifierIds],
|
|
@@ -105,6 +105,7 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
105
105
|
const layers = [...inputLayers].sort((left, right) => SOURCE_RANK[left.source] - SOURCE_RANK[right.source]);
|
|
106
106
|
const profiles = new Map();
|
|
107
107
|
const commands = new Map();
|
|
108
|
+
const acceptanceRunners = new Map();
|
|
108
109
|
const mappings = new Map();
|
|
109
110
|
const exceptions = new Map();
|
|
110
111
|
const reviewRoles = new Map();
|
|
@@ -128,6 +129,9 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
128
129
|
}
|
|
129
130
|
commands.set(command.id, effective(command, layer));
|
|
130
131
|
}
|
|
132
|
+
for (const runner of layer.config.verification.acceptanceRunners ?? []) {
|
|
133
|
+
acceptanceRunners.set(runner.id, effective(runner, layer));
|
|
134
|
+
}
|
|
131
135
|
for (const mapping of layer.config.pathMappings) {
|
|
132
136
|
const key = mappingKey(mapping);
|
|
133
137
|
if (layer.source === "project") {
|
|
@@ -162,6 +166,7 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
162
166
|
features,
|
|
163
167
|
profiles: [...profiles.values()],
|
|
164
168
|
verificationCommands: [...commands.values()],
|
|
169
|
+
...(acceptanceRunners.size === 0 ? {} : { acceptanceRunners: [...acceptanceRunners.values()] }),
|
|
165
170
|
pathMappings: [...mappings.values()],
|
|
166
171
|
securityExceptions: [...exceptions.values()],
|
|
167
172
|
reviewRoles: [...reviewRoles.values()],
|
|
@@ -171,7 +176,10 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
171
176
|
schemaVersion: schemaVersion.value,
|
|
172
177
|
profiles: provenance.profiles.map(({ value }) => value),
|
|
173
178
|
verification: {
|
|
174
|
-
commands: provenance.verificationCommands.map(({ value }) => value)
|
|
179
|
+
commands: provenance.verificationCommands.map(({ value }) => value),
|
|
180
|
+
...(provenance.acceptanceRunners === undefined ? {} : {
|
|
181
|
+
acceptanceRunners: provenance.acceptanceRunners.map(({ value }) => value)
|
|
182
|
+
})
|
|
175
183
|
},
|
|
176
184
|
features: provenance.features.value,
|
|
177
185
|
pathMappings: provenance.pathMappings.map(({ value }) => value),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const CONFIG_SCHEMA_VERSION = 3;
|
|
2
|
-
export const TASK_SCHEMA_VERSION =
|
|
3
|
-
export const EVIDENCE_SCHEMA_VERSION =
|
|
2
|
+
export const TASK_SCHEMA_VERSION = 2;
|
|
3
|
+
export const EVIDENCE_SCHEMA_VERSION = 4;
|
|
4
4
|
/** @deprecated Use the document-specific schema version constants. */
|
|
5
5
|
export const SCHEMA_VERSION = CONFIG_SCHEMA_VERSION;
|
|
6
6
|
/**
|
|
@@ -278,6 +278,10 @@ async function resolveLoopRoot(input, fallback, harness) {
|
|
|
278
278
|
}
|
|
279
279
|
return null;
|
|
280
280
|
}
|
|
281
|
+
/** Validated project-local loop root for display-only observers. */
|
|
282
|
+
export async function resolveProjectLoopRoot(input, fallback, harness) {
|
|
283
|
+
return await resolveLoopRoot(input, fallback, harness);
|
|
284
|
+
}
|
|
281
285
|
function loopPath(root, harness, name) {
|
|
282
286
|
return join(root, `.${harness}`, name);
|
|
283
287
|
}
|
|
@@ -6,6 +6,7 @@ import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../secur
|
|
|
6
6
|
import { checkTaskCompletionEvidence, findIncompleteSubtask } from "../task/completion.js";
|
|
7
7
|
import { collectBaseChangePaths, collectChangeSurface } from "../verify/change-surface.js";
|
|
8
8
|
import { calculateSourceFingerprint } from "../verify/source-fingerprint.js";
|
|
9
|
+
import { resolveReviewScope } from "../review/scope.js";
|
|
9
10
|
import { NOTES_REF, noteSessionLine, readWorktreeRecord } from "../parallel/service.js";
|
|
10
11
|
const FINGERPRINT = /^[a-f0-9]{64}$/u;
|
|
11
12
|
const SESSION = /^[^\0\r\n]{1,256}$/u;
|
|
@@ -192,7 +193,7 @@ export class CompletionGateService {
|
|
|
192
193
|
* range is recomputed here rather than trusted, so evidence for a range that
|
|
193
194
|
* no longer ends at HEAD still fails.
|
|
194
195
|
*/
|
|
195
|
-
async #evidenceFingerprint(completionBase) {
|
|
196
|
+
async #evidenceFingerprint(completionBase, noChangePaths) {
|
|
196
197
|
const worktree = await this.#fingerprint();
|
|
197
198
|
if (completionBase === null) {
|
|
198
199
|
return worktree;
|
|
@@ -202,6 +203,11 @@ export class CompletionGateService {
|
|
|
202
203
|
return worktree;
|
|
203
204
|
}
|
|
204
205
|
try {
|
|
206
|
+
if (noChangePaths !== undefined) {
|
|
207
|
+
const scope = await resolveReviewScope({ root: this.#options.root, runner: this.#options.gitRunner,
|
|
208
|
+
base: completionBase, noChangePaths });
|
|
209
|
+
return await calculateSourceFingerprint(this.#options.root, scope, this.#options.gitRunner);
|
|
210
|
+
}
|
|
205
211
|
const changedFiles = await collectBaseChangePaths(this.#options.gitRunner, completionBase);
|
|
206
212
|
if (changedFiles.length === 0) {
|
|
207
213
|
return worktree;
|
|
@@ -367,7 +373,7 @@ export class CompletionGateService {
|
|
|
367
373
|
if (unfinished !== undefined) {
|
|
368
374
|
return gateResult("block", "FAIL", "COMPLETION_GATE_SUBTASK_INCOMPLETE", `Complete subtask ${unfinished.task.id} before its parent.`);
|
|
369
375
|
}
|
|
370
|
-
const sourceFingerprint = await this.#evidenceFingerprint(stored.completionBase);
|
|
376
|
+
const sourceFingerprint = await this.#evidenceFingerprint(stored.completionBase, stored.noChangePaths);
|
|
371
377
|
const problem = await checkTaskCompletionEvidence(stored, {
|
|
372
378
|
...this.#options,
|
|
373
379
|
sourceFingerprint
|
|
@@ -548,6 +548,8 @@ export async function doctorInstallation(options) {
|
|
|
548
548
|
? []
|
|
549
549
|
: [await checkProbe("root-ghost-files", options.probes.rootGhostFiles)])
|
|
550
550
|
];
|
|
551
|
+
if (options.probes?.acceptanceCoverage !== undefined)
|
|
552
|
+
checks.push(await checkProbe("acceptance-coverage", options.probes.acceptanceCoverage));
|
|
551
553
|
return {
|
|
552
554
|
checks,
|
|
553
555
|
surfaces: surfaceInventory.surfaces,
|
|
@@ -305,6 +305,49 @@ function subagentInstruction(descriptor) {
|
|
|
305
305
|
return "Delegate it with this harness's subagent or task tool when it has one; otherwise run the same bounded steps sequentially in this session.";
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* The `run` profile's hand-off of feature-sized work to `agent-ops run`.
|
|
310
|
+
* agy keeps the subtask flow: `run` launches only Claude Code or Codex.
|
|
311
|
+
* AGENTS.md is shared by Codex and opencode, so it carries the Codex form.
|
|
312
|
+
*/
|
|
313
|
+
function autoRunInstruction(descriptor, context) {
|
|
314
|
+
const file = descriptor.control.instructionFile;
|
|
315
|
+
if (!context.capabilities.includes("auto-run") || file === "GEMINI.md") {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
const host = file === "CLAUDE.md" ? "claude" : "codex";
|
|
319
|
+
const start = `agent-ops run --goal-file .agent-ops/state/run-goal.md --host ${host} --wait`;
|
|
320
|
+
return [
|
|
321
|
+
"With the `run` profile, a change that needs more than five acceptance",
|
|
322
|
+
"criteria goes to `agent-ops run` instead of being split here; five or",
|
|
323
|
+
"fewer stay in this session. From the main checkout, before any `task",
|
|
324
|
+
"create` (its worktree makes `run` refuse with `WORKTREE_NESTED`), write",
|
|
325
|
+
"the user's prompt verbatim, then your proposed acceptance criteria marked",
|
|
326
|
+
"as proposals, to `.agent-ops/state/run-goal.md` (gitignored), and start",
|
|
327
|
+
...(host === "claude"
|
|
328
|
+
? [`\`${start}\``, "as a background shell command."]
|
|
329
|
+
: [
|
|
330
|
+
"it as a background shell command through the trusted outer host",
|
|
331
|
+
"runner, like review: the outer request uses",
|
|
332
|
+
"`sandbox_permissions: \"require_escalated\"`, and the shell form is",
|
|
333
|
+
`\`env -u CODEX_SANDBOX_NETWORK_DISABLED ${start}\`.`
|
|
334
|
+
]),
|
|
335
|
+
"When it returns awaiting input, relay the question to the user, run",
|
|
336
|
+
"`agent-ops run respond <id> --question-id <q> --answer <text>`, and poll",
|
|
337
|
+
"`agent-ops run status <id>` until it finishes or asks again; on",
|
|
338
|
+
"completion report the result. If the start is refused with",
|
|
339
|
+
"`RUN_TARGET_REQUIRED`, `RUN_BACKGROUND_UNSUPPORTED`, `RUN_TARGET_DIRTY` or",
|
|
340
|
+
"`WORKTREE_NESTED`, say so in one line naming the code and fall back to",
|
|
341
|
+
"the subtask flow (`task create --parent`, then batch or advance).",
|
|
342
|
+
"`RUN_REPO_UNTRUSTED` means stop and ask the user; never work",
|
|
343
|
+
"around trust. If the run ends without completing (blocked, budget",
|
|
344
|
+
"exhausted, stopped, failing review), report the `agent-ops run status",
|
|
345
|
+
"<id>` state and code, the last `agent-ops run logs <id>` events, and the",
|
|
346
|
+
"exact `agent-ops run resume <id>` and `agent-ops run stop <id>` commands,",
|
|
347
|
+
"then stop: never resume automatically or take over the run's work.",
|
|
348
|
+
""
|
|
349
|
+
];
|
|
350
|
+
}
|
|
308
351
|
export function managedRules(descriptor, context) {
|
|
309
352
|
const lines = [
|
|
310
353
|
"# Loop Engineering",
|
|
@@ -322,7 +365,7 @@ export function managedRules(descriptor, context) {
|
|
|
322
365
|
lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain complete independent review before claiming completion, via", " `agent-ops review --task <task-id> --yes` (or the CLI's equivalent invocation).", " Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", " Start this command through the trusted outer host runner as its first", " and only invocation. On Codex, the outer tool request must use", " `sandbox_permissions: \"require_escalated\"` so network and loopback", " are available; in that same request, unset", " `CODEX_SANDBOX_NETWORK_DISABLED` and set `AGENT_OPS_HOST` when three", " targets are configured. The Codex shell form is", " `env -u CODEX_SANDBOX_NETWORK_DISABLED AGENT_OPS_HOST=<current-host> agent-ops review --task <task-id> --yes`.", " Keep it a plain command line: to keep the output add `--output <file>`", " (the JSON envelope is written there and one line is printed) instead of", " a shell redirect, which the host can only allow by matching the whole", " command string, task id included.", " Do not run it once in the restricted sandbox and retry; changing the", " variable inside that sandbox is not elevation.", " Review runs exactly two fresh sessions: with three configured targets,", " set `AGENT_OPS_HOST` to the current CLI id so it is excluded; with two", " targets both run in configured order; with one target it runs twice.", " The first session is necessary; a PASS then requires an adversarial", " re-check carrying the first report. A first FAIL or NOT_RUN stops.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "exact matching trust record. Confirmed project init/update grants it", "automatically when verification commands are configured.", "");
|
|
323
366
|
}
|
|
324
367
|
if (context.capabilities.includes("task")) {
|
|
325
|
-
lines.push("Split a change that exceeds five acceptance criteria into subtasks:", "`agent-ops task create --parent <task-id>` records one, and", "`agent-ops task status --parent <task-id>` lists them. Each subtask", "carries its own criteria and verification; final review covers all", "tasks together when their worktrees are integrated;", "completing one never completes its parent.", "", "To review every active subtask of a parent together, run", "`agent-ops batch --parent <task-id> --yes` instead of one review per", "subtask. It needs what review needs: start it through the trusted outer", "host runner as its first and only invocation, with the same Codex", "elevation, and keep `--output <file>` instead of a shell redirect. It", "verifies, one task at a time, each task that lacks fresh evidence, and", "reviews a few tasks at once. Do not commit or edit the worktree while it", "runs: a moved HEAD voids every verify and review in the batch.", "For subagents in separate worktrees, use `task advance` instead.", "", "Delegate a subtask whose scope spans unknown files, broad scans, or an", "independent workstream instead of doing it inline. Name the bounded", "scope, the artifact to return, and the acceptance criteria it serves.", subagentInstruction(descriptor), "Read every returned artifact back yourself: integration, verification,", "and review stay with the coordinator.", "", "One writer at a time. `agent-ops verify` and `agent-ops review`", "fingerprint the working tree and report UNKNOWN when anything else", "writes it during the run, so delegate edits to one subagent at a time,", "or give each writer its own Git worktree and merge before verifying.", "Read-only delegation has no such limit and may run in parallel.", "", "Parallel conversations in one repository each need their own worktree.", "When `.agent-ops/config.json` sets `worktree.mode` to `auto`, run", "`agent-ops task create` from the main checkout before your first edit:", "it creates this session's worktree and puts the task there (`agent-ops", "worktree add <name> --session <session-id>` does the same without a", "task). Work only inside the printed path", "(Claude Code: EnterWorktree with that path; otherwise use it as every", "command's working directory). Commit, then run verify and review for", "each task with the printed `--base`; do not run `task complete` in the", "worktree. Leave it (Claude Code: ExitWorktree with action keep) and run", "`agent-ops worktree finish <name>` from the main checkout: it completes", "every task in the worktree, subtasks first, then merges. A finish that", "reports an incomplete task names it; verify or review that task again", "in the worktree and finish again. A finish that reports a conflict is", "resolved once, as a new task whose criteria cover both intents; if its", "verification or review fails, reset as instructed and report.", "", "For a session with writing subagents, record each child task's complete", "modification intent with `task create --intent <text>` before editing.", "Each child commits and runs local `verify --task` in its own worktree.", "From the main checkout run `agent-ops task advance --task <parent-id>", "--session <session-id> --yes` through the trusted outer host runner,", "unsetting `CODEX_SANDBOX_NETWORK_DISABLED` on Codex. Advance integrates", "child commits and intent into the coordinator worktree, verifies every", "task on the final candidate, then runs one two-round tree review.", "Only `NOT_RUN / scope-too-large` falls back to per-task two-round review;", "a substantive FAIL must be fixed. After PASS, advance writes a local", "receipt under the main checkout's Git common directory and calls finish.", "If the target moves, it retries the final gate once. An unresolved", "failure preserves worktrees and cannot be reported as finished.", "");
|
|
368
|
+
lines.push("Split a change that exceeds five acceptance criteria into subtasks:", "`agent-ops task create --parent <task-id>` records one, and", "`agent-ops task status --parent <task-id>` lists them. Each subtask", "carries its own criteria and verification; final review covers all", "tasks together when their worktrees are integrated;", "completing one never completes its parent.", "", ...autoRunInstruction(descriptor, context), "To review every active subtask of a parent together, run", "`agent-ops batch --parent <task-id> --yes` instead of one review per", "subtask. It needs what review needs: start it through the trusted outer", "host runner as its first and only invocation, with the same Codex", "elevation, and keep `--output <file>` instead of a shell redirect. It", "verifies, one task at a time, each task that lacks fresh evidence, and", "reviews a few tasks at once. Do not commit or edit the worktree while it", "runs: a moved HEAD voids every verify and review in the batch.", "For subagents in separate worktrees, use `task advance` instead.", "", "Delegate a subtask whose scope spans unknown files, broad scans, or an", "independent workstream instead of doing it inline. Name the bounded", "scope, the artifact to return, and the acceptance criteria it serves.", subagentInstruction(descriptor), "Read every returned artifact back yourself: integration, verification,", "and review stay with the coordinator.", "", "One writer at a time. `agent-ops verify` and `agent-ops review`", "fingerprint the working tree and report UNKNOWN when anything else", "writes it during the run, so delegate edits to one subagent at a time,", "or give each writer its own Git worktree and merge before verifying.", "Read-only delegation has no such limit and may run in parallel.", "", "Parallel conversations in one repository each need their own worktree.", "When `.agent-ops/config.json` sets `worktree.mode` to `auto`, run", "`agent-ops task create` from the main checkout before your first edit:", "it creates this session's worktree and puts the task there (`agent-ops", "worktree add <name> --session <session-id>` does the same without a", "task). Work only inside the printed path", "(Claude Code: EnterWorktree with that path; otherwise use it as every", "command's working directory). Commit, then run verify and review for", "each task with the printed `--base`; do not run `task complete` in the", "worktree. Leave it (Claude Code: ExitWorktree with action keep) and run", "`agent-ops worktree finish <name>` from the main checkout: it completes", "every task in the worktree, subtasks first, then merges. A finish that", "reports an incomplete task names it; verify or review that task again", "in the worktree and finish again. A finish that reports a conflict is", "resolved once, as a new task whose criteria cover both intents; if its", "verification or review fails, reset as instructed and report.", "", "For a session with writing subagents, record each child task's complete", "modification intent with `task create --intent <text>` before editing.", "Each child commits and runs local `verify --task` in its own worktree.", "From the main checkout run `agent-ops task advance --task <parent-id>", "--session <session-id> --yes` through the trusted outer host runner,", "unsetting `CODEX_SANDBOX_NETWORK_DISABLED` on Codex. Advance integrates", "child commits and intent into the coordinator worktree, verifies every", "task on the final candidate, then runs one two-round tree review.", "Only `NOT_RUN / scope-too-large` falls back to per-task two-round review;", "a substantive FAIL must be fixed. After PASS, advance writes a local", "receipt under the main checkout's Git common directory and calls finish.", "If the target moves, it retries the final gate once. An unresolved", "failure preserves worktrees and cannot be reported as finished.", "");
|
|
326
369
|
}
|
|
327
370
|
if (context.capabilities.includes("lifecycle-summary")) {
|
|
328
371
|
lines.push("Advisory lifecycle summaries and local logs are informational. Advisory", "failures must remain fail-open and cannot become verification evidence.", "");
|
|
@@ -79,7 +79,7 @@ async function detectVerificationCommands(root) {
|
|
|
79
79
|
blockers: discovery.decisions.map((decision) => `${decision.adapter}: ${decision.message}`)
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
function buildConfig(profiles, existing, reviewTargets = [], detectedCommands = [], completionGateEnabled = false, worktree) {
|
|
82
|
+
function buildConfig(profiles, existing, reviewTargets = [], detectedCommands = [], completionGateEnabled = false, worktree, officeEnabled) {
|
|
83
83
|
// Absent reviewRoles means external review is disabled; an empty selection
|
|
84
84
|
// must therefore omit the field rather than write an empty array.
|
|
85
85
|
const reviewRoles = reviewTargets.length > 0
|
|
@@ -96,13 +96,14 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
96
96
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
97
97
|
profiles: [...profiles],
|
|
98
98
|
verification,
|
|
99
|
-
features:
|
|
100
|
-
|
|
101
|
-
enabled: false
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
features: {
|
|
100
|
+
...(existing?.features ?? {
|
|
101
|
+
stopVerification: { enabled: false },
|
|
102
|
+
completionGate: { enabled: completionGateEnabled }
|
|
103
|
+
}),
|
|
104
|
+
...(officeEnabled === undefined
|
|
105
|
+
? (existing === undefined ? { office: { enabled: false } } : {})
|
|
106
|
+
: { office: { enabled: officeEnabled } })
|
|
106
107
|
},
|
|
107
108
|
pathMappings: existing?.pathMappings ?? [],
|
|
108
109
|
securityExceptions: existing?.securityExceptions ?? [],
|
|
@@ -110,7 +111,7 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
110
111
|
...(effectiveWorktree === undefined ? {} : { worktree: effectiveWorktree })
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
|
-
async function planConfig(root, profiles, existingManifest, suppliedConfig, reviewTargets = [], completionGateEnabled = false, worktree) {
|
|
114
|
+
async function planConfig(root, profiles, existingManifest, suppliedConfig, reviewTargets = [], completionGateEnabled = false, worktree, officeEnabled) {
|
|
114
115
|
const current = await readCurrentFile(root, CONFIG_PATH);
|
|
115
116
|
const owned = findOwnedArtifact(existingManifest, CONFIG_PATH);
|
|
116
117
|
if (current !== null && owned === undefined) {
|
|
@@ -146,7 +147,7 @@ async function planConfig(root, profiles, existingManifest, suppliedConfig, revi
|
|
|
146
147
|
existingConfig.verification.commands.length === 0
|
|
147
148
|
? await detectVerificationCommands(root)
|
|
148
149
|
: { commands: [], blockers: [] };
|
|
149
|
-
const config = buildConfig(profiles, existingConfig, reviewTargets, detected.commands, completionGateEnabled, worktree);
|
|
150
|
+
const config = buildConfig(profiles, existingConfig, reviewTargets, detected.commands, completionGateEnabled, worktree, officeEnabled);
|
|
150
151
|
const content = `${JSON.stringify(config, null, 2)}\n`;
|
|
151
152
|
return {
|
|
152
153
|
operation: {
|
|
@@ -437,6 +438,10 @@ export async function createInstallPlan(options) {
|
|
|
437
438
|
await assertCodexLoopConfiguration(options.root, options.harness, resolved.capabilities);
|
|
438
439
|
const existing = await readExistingManifest(options.root);
|
|
439
440
|
assertCompatibleManifest(existing?.manifest ?? null, options.scope, options.harness, options.allowHarnessChange === true);
|
|
441
|
+
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig, options.reviewTargets ?? [], completionGateEnabled, options.worktree, options.officeEnabled);
|
|
442
|
+
resolved.capabilities = resolved.capabilities.filter(capability => capability !== "office-presence");
|
|
443
|
+
if (config.config.features.office?.enabled === true)
|
|
444
|
+
resolved.capabilities.push("office-presence");
|
|
440
445
|
const existingOpencodePluginPath = existing?.manifest.artifacts.find(({ id }) => id === "opencode-plugin")?.path;
|
|
441
446
|
const explicitHookTargets = new Map();
|
|
442
447
|
for (const target of options.hookTargets ?? []) {
|
|
@@ -520,7 +525,6 @@ export async function createInstallPlan(options) {
|
|
|
520
525
|
: [];
|
|
521
526
|
const operations = [];
|
|
522
527
|
const artifacts = [];
|
|
523
|
-
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig, options.reviewTargets ?? [], completionGateEnabled, options.worktree);
|
|
524
528
|
operations.push(config.operation);
|
|
525
529
|
artifacts.push(config.record);
|
|
526
530
|
const repaired = [];
|
|
@@ -634,6 +638,7 @@ export async function createInstallPlan(options) {
|
|
|
634
638
|
scope: options.scope,
|
|
635
639
|
harness: options.harness,
|
|
636
640
|
desired: config.config.worktree?.mode === "auto",
|
|
641
|
+
run: resolved.profiles.includes("run"),
|
|
637
642
|
...(options.codexHome === undefined ? {} : { codexHome: options.codexHome })
|
|
638
643
|
});
|
|
639
644
|
return {
|