@kylecheng3146/agent-ops 0.6.0 → 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 +55 -1
- package/dist/packages/cli/src/args.js +19 -2
- package/dist/packages/cli/src/bin.js +14 -4
- package/dist/packages/cli/src/cli.js +20 -2
- package/dist/packages/cli/src/codex-loop-process.js +19 -2
- package/dist/packages/cli/src/commands/advance.js +19 -2
- 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/run.js +6 -2
- package/dist/packages/cli/src/commands/update.js +8 -0
- package/dist/packages/cli/src/context.js +1 -0
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +23 -2
- 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/run-deps.js +9 -2
- package/dist/packages/cli/src/run-entry.js +9 -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 +5 -0
- package/dist/runtime/src/hooks/codex-loop.js +4 -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/guard.js +7 -1
- package/dist/runtime/src/review/slots.js +1 -1
- package/dist/runtime/src/run/phase.js +37 -0
- package/dist/runtime/src/run/service.js +8 -0
- package/dist/runtime/src/schema/validate.js +13 -2
- package/docs/en/spec/harness-adapters.md +113 -1
- package/docs/zh-TW/spec/harness-adapters.md +93 -1
- package/package.json +1 -1
- package/schemas/config.schema.json +10 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { collectOfficeInput } from "../../../runtime/src/office/collect.js";
|
|
6
|
+
import { claimOffice, createOfficeServer, ensureOffice, readLiveOffice, officeUrl, releaseOffice } from "../../../runtime/src/office/server.js";
|
|
7
|
+
import { buildOfficeSnapshot } from "../../../runtime/src/office/snapshot.js";
|
|
8
|
+
import { recordOfficeSession } from "../../../runtime/src/office/sessions.js";
|
|
9
|
+
import { resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
10
|
+
import { createLaunchdDescriptor, LaunchdController } from "../../../runtime/src/run/macOS.js";
|
|
11
|
+
import { ensurePrivateDirectory } from "../../../runtime/src/security/permissions.js";
|
|
12
|
+
import { worktreeDependencies } from "./parallel-deps.js";
|
|
13
|
+
import { normalizeShellHookEvent } from "../../../runtime/src/hooks/shell.js";
|
|
14
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
15
|
+
import { loadEffectiveConfig } from "./context.js";
|
|
16
|
+
const entry = fileURLToPath(import.meta.url);
|
|
17
|
+
/** Missing or invalid config keeps this Preview disabled. */
|
|
18
|
+
export async function officeEnabled(cwd) {
|
|
19
|
+
if (process.env.AGENT_OPS_DISABLE === "1")
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
return (await loadEffectiveConfig(cwd, "project")).config.features.office?.enabled === true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function requireOffice(cwd) {
|
|
29
|
+
if (!(await officeEnabled(cwd))) {
|
|
30
|
+
throw new AgentOpsError("OFFICE_PREVIEW_DISABLED", "Office (Preview) is disabled. Enable it with agent-ops update --office on.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function recordValue(input, key) {
|
|
34
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
35
|
+
return undefined;
|
|
36
|
+
return input[key];
|
|
37
|
+
}
|
|
38
|
+
function observationInput(input) {
|
|
39
|
+
return typeof input === "object" && input !== null && !Array.isArray(input);
|
|
40
|
+
}
|
|
41
|
+
function firstText(...values) {
|
|
42
|
+
return values.find((value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\0\r\n]/u.test(value));
|
|
43
|
+
}
|
|
44
|
+
function sessionIdentity(options) {
|
|
45
|
+
const nested = recordValue(options.input, "input");
|
|
46
|
+
return firstText(options.sessionId, recordValue(options.input, "session_id"), recordValue(options.input, "sessionId"), recordValue(options.input, "conversationId"), recordValue(options.input, "thread_id"), recordValue(options.input, "threadId"), recordValue(nested, "sessionID"), recordValue(nested, "session_id"), process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID);
|
|
47
|
+
}
|
|
48
|
+
function officeEvent(event) {
|
|
49
|
+
return event === "SessionStart" ? "start" : event === "Stop" ? "stop" : "activity";
|
|
50
|
+
}
|
|
51
|
+
/** Phase hints come only from normalized native event kinds and exact agent-ops argv. */
|
|
52
|
+
export function officePhaseHint(event) {
|
|
53
|
+
if (event.event === "file-write" || event.event === "content")
|
|
54
|
+
return "implementing";
|
|
55
|
+
const commands = event.event === "command"
|
|
56
|
+
? [event]
|
|
57
|
+
: event.event === "command-batch"
|
|
58
|
+
? event.commands
|
|
59
|
+
: [];
|
|
60
|
+
let phase;
|
|
61
|
+
for (const command of commands) {
|
|
62
|
+
if (basename(command.command) !== "agent-ops")
|
|
63
|
+
continue;
|
|
64
|
+
const action = command.args[0];
|
|
65
|
+
const next = action === "verify"
|
|
66
|
+
? "verifying"
|
|
67
|
+
: action === "review"
|
|
68
|
+
? "reviewing"
|
|
69
|
+
: action === "worktree" && command.args[1] === "finish"
|
|
70
|
+
? "integrating"
|
|
71
|
+
: action === "task" && command.args[1] === "complete"
|
|
72
|
+
? "integrating"
|
|
73
|
+
: action === "task"
|
|
74
|
+
? "planning"
|
|
75
|
+
: undefined;
|
|
76
|
+
if (next === "integrating" || next === "reviewing" || next === "verifying")
|
|
77
|
+
phase = next;
|
|
78
|
+
else if (phase === undefined && next !== undefined)
|
|
79
|
+
phase = next;
|
|
80
|
+
}
|
|
81
|
+
return phase;
|
|
82
|
+
}
|
|
83
|
+
/** Loop adapters expose Bash and native edit events with the same safe shape. */
|
|
84
|
+
export function officeLoopPhaseHint(input, root) {
|
|
85
|
+
if (!observationInput(input))
|
|
86
|
+
return undefined;
|
|
87
|
+
const tool = input.tool_name;
|
|
88
|
+
if (tool === "Edit" || tool === "Write" || tool === "NotebookEdit" || tool === "FileWrite")
|
|
89
|
+
return "implementing";
|
|
90
|
+
if (tool !== "Bash")
|
|
91
|
+
return undefined;
|
|
92
|
+
const toolInput = recordValue(input, "tool_input");
|
|
93
|
+
const command = recordValue(toolInput, "command");
|
|
94
|
+
return typeof command === "string" ? officePhaseHint(normalizeShellHookEvent(command, root)) : undefined;
|
|
95
|
+
}
|
|
96
|
+
/** Records hook presence and starts Office at the production process boundary. */
|
|
97
|
+
export async function observeOfficeSession(options) {
|
|
98
|
+
if (process.env.AGENT_OPS_DISABLE === "1")
|
|
99
|
+
return;
|
|
100
|
+
if (options.validated !== true)
|
|
101
|
+
return;
|
|
102
|
+
// Host hooks with malformed or absent input are fail-open and must not
|
|
103
|
+
// create a server from an unrelated process cwd.
|
|
104
|
+
if (!observationInput(options.input))
|
|
105
|
+
return;
|
|
106
|
+
const identity = sessionIdentity(options);
|
|
107
|
+
if (identity === undefined)
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
if (!(await officeEnabled(options.root)))
|
|
111
|
+
return;
|
|
112
|
+
const { mainRoot, commonDir } = await checkouts(options.root);
|
|
113
|
+
if (identity !== undefined) {
|
|
114
|
+
await recordOfficeSession({
|
|
115
|
+
sessionId: identity,
|
|
116
|
+
harness: options.harness,
|
|
117
|
+
projectRoot: mainRoot,
|
|
118
|
+
commonDir,
|
|
119
|
+
event: officeEvent(options.event),
|
|
120
|
+
...(options.agentId === undefined ? {} : { agentId: options.agentId }),
|
|
121
|
+
...(process.env.AGENT_OPS_RUN_ID === undefined ? {} : { runId: process.env.AGENT_OPS_RUN_ID }),
|
|
122
|
+
...(process.env.AGENT_OPS_WORKER_ID === undefined ? {} : { workerId: process.env.AGENT_OPS_WORKER_ID }),
|
|
123
|
+
...(process.env.AGENT_OPS_TASK_ID === undefined ? {} : { taskId: process.env.AGENT_OPS_TASK_ID }),
|
|
124
|
+
...(options.phase === undefined ? {} : { phase: options.phase }),
|
|
125
|
+
host: options.harness
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// A resumed session may outlive the ten-minute idle shutdown. Reusing the
|
|
129
|
+
// same ensure path revives that repository server without creating a tab
|
|
130
|
+
// unless this call actually wins a new-server start claim.
|
|
131
|
+
if (options.event !== "Stop")
|
|
132
|
+
await ensureBackgroundOffice(options.root, undefined, openOfficeBrowser);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Office is display-only; a missing Git checkout, lock or launcher never blocks a host hook.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function checkouts(cwd) {
|
|
139
|
+
return await resolveCheckouts(worktreeDependencies(), cwd);
|
|
140
|
+
}
|
|
141
|
+
/** Serve the office until no run has been active for the idle window. */
|
|
142
|
+
export async function serveOffice(cwd, onUrl = () => { }, openBrowser) {
|
|
143
|
+
await requireOffice(cwd);
|
|
144
|
+
const { mainRoot, commonDir } = await checkouts(cwd);
|
|
145
|
+
// ponytail: one shared 1s snapshot so several polling tabs cost one git scan
|
|
146
|
+
let cached = null;
|
|
147
|
+
const snapshot = () => {
|
|
148
|
+
if (cached === null || Date.now() - cached.at > 1000)
|
|
149
|
+
cached = { at: Date.now(), value: collectOfficeInput(mainRoot, commonDir).then(buildOfficeSnapshot) };
|
|
150
|
+
return cached.value;
|
|
151
|
+
};
|
|
152
|
+
let finish = () => { };
|
|
153
|
+
const idle = new Promise((resolve) => { finish = resolve; });
|
|
154
|
+
const office = createOfficeServer({ snapshot, onIdle: () => finish() });
|
|
155
|
+
const record = await claimOffice(commonDir, office);
|
|
156
|
+
if (record === null) {
|
|
157
|
+
const live = await readLiveOffice(commonDir);
|
|
158
|
+
if (live !== null)
|
|
159
|
+
onUrl(officeUrl(live));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
onUrl(officeUrl(record));
|
|
163
|
+
if (openBrowser !== undefined)
|
|
164
|
+
void openBrowser(officeUrl(record)).catch(() => undefined);
|
|
165
|
+
const timer = setInterval(() => {
|
|
166
|
+
void officeEnabled(cwd).then(enabled => enabled ? office.tick() : finish());
|
|
167
|
+
}, 15_000);
|
|
168
|
+
await idle;
|
|
169
|
+
clearInterval(timer);
|
|
170
|
+
await releaseOffice(commonDir, record);
|
|
171
|
+
await office.close();
|
|
172
|
+
}
|
|
173
|
+
/** Reuse the live server or start one under launchd; null where launchd is unavailable. */
|
|
174
|
+
export async function ensureBackgroundOffice(cwd, launchd = new LaunchdController(), openBrowser) {
|
|
175
|
+
await requireOffice(cwd);
|
|
176
|
+
const { mainRoot, commonDir } = await checkouts(cwd);
|
|
177
|
+
const live = await readLiveOffice(commonDir);
|
|
178
|
+
if (live !== null)
|
|
179
|
+
return officeUrl(live);
|
|
180
|
+
let started = false;
|
|
181
|
+
const args = [entry, mainRoot];
|
|
182
|
+
if (!launchd.supported) {
|
|
183
|
+
const url = await ensureOffice(commonDir, async () => {
|
|
184
|
+
started = true;
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
const child = spawn(process.execPath, args, { cwd: mainRoot, detached: true, stdio: "ignore", windowsHide: true });
|
|
187
|
+
child.once("error", reject);
|
|
188
|
+
child.once("spawn", () => { child.unref(); resolve(); });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
if (started && openBrowser !== undefined)
|
|
192
|
+
void openBrowser(url).catch(() => undefined);
|
|
193
|
+
return url;
|
|
194
|
+
}
|
|
195
|
+
const privateDirectory = join(commonDir, "agent-ops", "office");
|
|
196
|
+
const descriptor = createLaunchdDescriptor({
|
|
197
|
+
runId: "office-" + createHash("sha256").update(commonDir).digest("hex").slice(0, 12), workerId: "server",
|
|
198
|
+
privateDirectory, command: process.execPath, args,
|
|
199
|
+
cwd: mainRoot, pathEnvironment: process.env.PATH
|
|
200
|
+
});
|
|
201
|
+
const url = await ensureOffice(commonDir, async () => {
|
|
202
|
+
started = true;
|
|
203
|
+
// The descriptor write anchors on this directory, so it must exist first.
|
|
204
|
+
await ensurePrivateDirectory(privateDirectory, commonDir);
|
|
205
|
+
await launchd.writeDescriptor(descriptor);
|
|
206
|
+
// A server that exited idle stays loaded in launchd; reload it with the current descriptor.
|
|
207
|
+
await launchd.bootout(descriptor).catch(() => { });
|
|
208
|
+
await launchd.bootstrap(descriptor);
|
|
209
|
+
});
|
|
210
|
+
if (started && openBrowser !== undefined)
|
|
211
|
+
void openBrowser(url).catch(() => undefined);
|
|
212
|
+
return url;
|
|
213
|
+
}
|
|
214
|
+
export function openOfficeBrowser(url) {
|
|
215
|
+
const command = process.platform === "darwin"
|
|
216
|
+
? "open"
|
|
217
|
+
: process.platform === "win32"
|
|
218
|
+
? "rundll32.exe"
|
|
219
|
+
: "xdg-open";
|
|
220
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
223
|
+
child.once("error", reject);
|
|
224
|
+
child.once("spawn", () => { child.unref(); resolve(); });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (process.argv[1] === entry) {
|
|
228
|
+
await serveOffice(process.argv[2] ?? process.cwd(), () => { }, process.argv[3] === "--open" ? openOfficeBrowser : undefined);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
@@ -26,6 +26,7 @@ import { discoverPendingPolicyTransition, renewBoundPolicyTransition, currentPol
|
|
|
26
26
|
import { nativeProcessIdentity } from "../../../runtime/src/run/transport.js";
|
|
27
27
|
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
28
28
|
import { RunControlService } from "../../../runtime/src/run/controls.js";
|
|
29
|
+
import { recordRunPhase } from "../../../runtime/src/run/phase.js";
|
|
29
30
|
/** Worktree construction happens before a native lease exists; it must never write global trust. */
|
|
30
31
|
export async function runWorktreeDependencies(deps, state) {
|
|
31
32
|
const policy = await readRunPolicy(state, await runRuntimeHash());
|
|
@@ -138,6 +139,12 @@ export async function renewPendingRunPolicy(repository, runId, journal) {
|
|
|
138
139
|
renewedPolicyArtifact: currentPolicyArtifact(current), expectedJournalDigest: journal.artifactDigest, now: new Date(now).toISOString()
|
|
139
140
|
});
|
|
140
141
|
}
|
|
142
|
+
/** The supervisor's final proof reports its stages into the run it belongs to. */
|
|
143
|
+
export async function runPhaseObserver(cwd, runId, workerId) {
|
|
144
|
+
const { commonDir } = await resolveCheckouts(worktreeDependencies(), cwd);
|
|
145
|
+
const repository = new FileRunRepository(join(commonDir, "agent-ops", "runs"), commonDir);
|
|
146
|
+
return async (phase, progress) => await recordRunPhase(repository, runId, workerId, phase, progress);
|
|
147
|
+
}
|
|
141
148
|
export const runEntry = fileURLToPath(new URL("./run-entry.js", import.meta.url));
|
|
142
149
|
export function runDescriptor(state) {
|
|
143
150
|
return createLaunchdDescriptor({ runId: state.runId, workerId: "supervisor", privateDirectory: join(state.commonDir, "agent-ops", "runs", state.runId),
|
|
@@ -172,14 +179,14 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
172
179
|
const workerId = `coordinator-${result.state.runId}`;
|
|
173
180
|
const workerRoot = join(checkouts.mainRoot, ".worktrees", sessionWorktreeName(ownerSessionId));
|
|
174
181
|
const now = new Date().toISOString();
|
|
175
|
-
const state = await repository.mutate(result.state.runId, current => ({ ...current,
|
|
182
|
+
const state = await repository.mutate(result.state.runId, current => ({ ...current, phase: "planning",
|
|
176
183
|
currentContractHash: current.goalHash,
|
|
177
184
|
tasks: [{ taskId: "planning-" + current.runId, dependencies: [], status: "ready", workerId,
|
|
178
185
|
deliveryDigest: null, sourceCommit: null, blockedReason: null }],
|
|
179
186
|
workers: [{ workerId, taskId: "planning-" + current.runId, host: current.host, ownerSessionId,
|
|
180
187
|
nativeSessionId: null, nativeJobId: null, worktree: workerRoot, processId: null, processIdentity: null,
|
|
181
188
|
generation: 1, status: "assigned", leaseExpiresAt: null, heartbeatAt: null, stopIntent: null,
|
|
182
|
-
nativeGoalState: "inactive", lastFailure: null }],
|
|
189
|
+
nativeGoalState: "inactive", lastFailure: null, phase: "planning" }],
|
|
183
190
|
budget: { ...current.budget, activeIntervals: [{ startMs: Date.parse(current.createdAt), endMs: null }], lastObservedAt: now } }));
|
|
184
191
|
await mkdir(runDescriptor(state).privateDirectory, { recursive: true });
|
|
185
192
|
await launchd.writeDescriptor(runDescriptor(state));
|
|
@@ -6,6 +6,7 @@ import { writeNoChangeDelivery, integrateSessionChildren } from "../../../runtim
|
|
|
6
6
|
import { recordNativeRunUsage, recordReviewRunUsage, recordSavedReviewRunUsage } from "../../../runtime/src/run/usage.js";
|
|
7
7
|
import { recordNativeRunAuthorization } from "../../../runtime/src/run/authorization.js";
|
|
8
8
|
import { observeRunFailure } from "../../../runtime/src/run/verification.js";
|
|
9
|
+
import { criteriaProgress, recordRunPhase } from "../../../runtime/src/run/phase.js";
|
|
9
10
|
import { compareFailureObservations } from "../../../runtime/src/run/convergence.js";
|
|
10
11
|
import { RunControlService } from "../../../runtime/src/run/controls.js";
|
|
11
12
|
import { beginPolicyTransition, markPolicyTransitionStage, completePolicyTransition, discoverPendingPolicyTransition } from "../../../runtime/src/run/policy-transition.js";
|
|
@@ -195,6 +196,7 @@ export async function superviseNativeRun(root, runId) {
|
|
|
195
196
|
await writeWorktreeRecord({ ...record, workerGeneration: worker.generation });
|
|
196
197
|
const goal = await instructions(worker, diagnostic);
|
|
197
198
|
const running = await supervisor.startWorker(runId, worker.workerId, worker.generation, goal);
|
|
199
|
+
await recordRunPhase(repository, runId, worker.workerId, (await repository.read(runId)).rootTaskId === null ? "planning" : "implementing");
|
|
198
200
|
try {
|
|
199
201
|
await transport.activateRegistered(runId, running, goal);
|
|
200
202
|
}
|
|
@@ -793,6 +795,7 @@ export async function superviseNativeRun(root, runId) {
|
|
|
793
795
|
tasks: saved.tasks.map(node => node.taskId === worker.taskId ? { ...node, taskId: task.task.id } : node),
|
|
794
796
|
workers: saved.workers.map(w => w.workerId === worker.workerId ? { ...w, taskId: task.task.id } : w) }));
|
|
795
797
|
response = { requestId: request.requestId, code: "RUN_PLAN_CREATED", task: task.task, contractHash: treeContractHash([task.task]) };
|
|
798
|
+
await recordRunPhase(repository, runId, worker.workerId, "implementing", { taskId: task.task.id, update: { passed: 0, total: task.task.criteria.length } });
|
|
796
799
|
await instructions((await repository.read(runId)).workers.find(w => w.workerId === worker.workerId));
|
|
797
800
|
}
|
|
798
801
|
else if (request.action === "worker") {
|
|
@@ -829,10 +832,14 @@ export async function superviseNativeRun(root, runId) {
|
|
|
829
832
|
await supervisor.confirmHandoff(runId, handoff, { processDead: !observation.processAlive });
|
|
830
833
|
await assertRunDeliverySnapshot(worker.worktree, worker.taskId, head, contract, tasks);
|
|
831
834
|
const artifactRefs = [];
|
|
835
|
+
await recordRunPhase(repository, runId, worker.workerId, "verifying");
|
|
832
836
|
for (const localTask of (await tasks.list()).filter(task => task.supersededBy === undefined && task.status !== "archived")) {
|
|
833
837
|
if (head === record.base)
|
|
834
838
|
await tasks.recordEvidence(localTask.task.id, {}, undefined, (await tasks.status({ taskId: worker.taskId })).noChangePaths);
|
|
835
839
|
const result = await step(worker.worktree, ["verify", "--task", localTask.task.id, "--base", record.base], current);
|
|
840
|
+
if (localTask.task.id === worker.taskId)
|
|
841
|
+
await recordRunPhase(repository, runId, worker.workerId, "verifying", { taskId: worker.taskId,
|
|
842
|
+
update: { verify: result.status === "ok" ? "PASS" : "FAIL", ...criteriaProgress(localTask.task.criteria, plain(result.data) && plain(result.data.report) ? result.data.report : {}) } });
|
|
836
843
|
if (result.status !== "ok")
|
|
837
844
|
throw new AgentOpsError(result.code, "Local verifier did not pass. Preserve the failed check diagnostics for repair.");
|
|
838
845
|
artifactRefs.push(...Object.values((await tasks.status({ taskId: localTask.task.id })).evidence).flat());
|
|
@@ -847,6 +854,7 @@ export async function superviseNativeRun(root, runId) {
|
|
|
847
854
|
}
|
|
848
855
|
await supervisor.publishDelivery(runId, handoff, { noChange: head === record.base, artifactRefs });
|
|
849
856
|
await scheduler.markDelivered(runId, worker.taskId, deliveryDigest, head);
|
|
857
|
+
await recordRunPhase(repository, runId, worker.workerId, "integrating");
|
|
850
858
|
response = { requestId: request.requestId, code: "RUN_WORKER_DELIVERED", taskId: worker.taskId, sourceCommit: head };
|
|
851
859
|
}
|
|
852
860
|
else if (request.action === "question") {
|
|
@@ -875,6 +883,7 @@ export async function superviseNativeRun(root, runId) {
|
|
|
875
883
|
.recordEvidence(current.rootTaskId, {}, undefined, scope.changedFiles);
|
|
876
884
|
}
|
|
877
885
|
await stopAll("stop", false);
|
|
886
|
+
await recordRunPhase(repository, runId, worker.workerId, "verifying");
|
|
878
887
|
const result = await advance(current);
|
|
879
888
|
if (result.status !== "ok")
|
|
880
889
|
throw new AgentOpsError(result.code, "Final proof failed; inspect saved verifier and fresh review reports before repair.");
|
|
@@ -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,
|
|
@@ -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
|
}
|