@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.
Files changed (48) hide show
  1. package/README.md +55 -1
  2. package/dist/packages/cli/src/args.js +19 -2
  3. package/dist/packages/cli/src/bin.js +14 -4
  4. package/dist/packages/cli/src/cli.js +20 -2
  5. package/dist/packages/cli/src/codex-loop-process.js +19 -2
  6. package/dist/packages/cli/src/commands/advance.js +19 -2
  7. package/dist/packages/cli/src/commands/init.js +2 -0
  8. package/dist/packages/cli/src/commands/office.js +20 -0
  9. package/dist/packages/cli/src/commands/run.js +6 -2
  10. package/dist/packages/cli/src/commands/update.js +8 -0
  11. package/dist/packages/cli/src/context.js +1 -0
  12. package/dist/packages/cli/src/hook-entry.js +2 -1
  13. package/dist/packages/cli/src/hook-process.js +23 -2
  14. package/dist/packages/cli/src/loop-entry.js +2 -1
  15. package/dist/packages/cli/src/office-entry.js +230 -0
  16. package/dist/packages/cli/src/run-deps.js +9 -2
  17. package/dist/packages/cli/src/run-entry.js +9 -0
  18. package/dist/packages/cli/src/wizard.js +14 -2
  19. package/dist/runtime/src/adapters/agy/config.js +4 -3
  20. package/dist/runtime/src/adapters/agy/events.js +9 -0
  21. package/dist/runtime/src/adapters/claude/config.js +3 -3
  22. package/dist/runtime/src/adapters/claude/events.js +8 -0
  23. package/dist/runtime/src/adapters/codex/config.js +3 -3
  24. package/dist/runtime/src/adapters/codex/events.js +8 -0
  25. package/dist/runtime/src/adapters/opencode/config.js +5 -4
  26. package/dist/runtime/src/adapters/opencode/events.js +8 -0
  27. package/dist/runtime/src/config/explain.js +5 -0
  28. package/dist/runtime/src/hooks/codex-loop.js +4 -0
  29. package/dist/runtime/src/install/harness.js +44 -1
  30. package/dist/runtime/src/install/plan.js +16 -11
  31. package/dist/runtime/src/install/preauth.js +44 -18
  32. package/dist/runtime/src/install/profiles.js +8 -2
  33. package/dist/runtime/src/install/update.js +1 -0
  34. package/dist/runtime/src/office/collect.js +233 -0
  35. package/dist/runtime/src/office/page.js +873 -0
  36. package/dist/runtime/src/office/scene.js +168 -0
  37. package/dist/runtime/src/office/server.js +139 -0
  38. package/dist/runtime/src/office/sessions.js +115 -0
  39. package/dist/runtime/src/office/snapshot.js +107 -0
  40. package/dist/runtime/src/parallel/guard.js +7 -1
  41. package/dist/runtime/src/review/slots.js +1 -1
  42. package/dist/runtime/src/run/phase.js +37 -0
  43. package/dist/runtime/src/run/service.js +8 -0
  44. package/dist/runtime/src/schema/validate.js +13 -2
  45. package/docs/en/spec/harness-adapters.md +113 -1
  46. package/docs/zh-TW/spec/harness-adapters.md +93 -1
  47. package/package.json +1 -1
  48. 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("optional-stop-verify") ||
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
  }