@kylecheng3146/agent-ops 0.6.0 → 0.7.1

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 (57) hide show
  1. package/README.md +60 -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/review.js +10 -4
  10. package/dist/packages/cli/src/commands/run.js +6 -2
  11. package/dist/packages/cli/src/commands/update.js +8 -0
  12. package/dist/packages/cli/src/context.js +7 -5
  13. package/dist/packages/cli/src/hook-entry.js +2 -1
  14. package/dist/packages/cli/src/hook-process.js +23 -2
  15. package/dist/packages/cli/src/loop-entry.js +2 -1
  16. package/dist/packages/cli/src/office-entry.js +261 -0
  17. package/dist/packages/cli/src/parallel-deps.js +4 -2
  18. package/dist/packages/cli/src/run-deps.js +9 -2
  19. package/dist/packages/cli/src/run-entry.js +9 -0
  20. package/dist/packages/cli/src/wizard.js +14 -2
  21. package/dist/runtime/src/adapters/agy/config.js +4 -3
  22. package/dist/runtime/src/adapters/agy/events.js +9 -0
  23. package/dist/runtime/src/adapters/claude/config.js +3 -3
  24. package/dist/runtime/src/adapters/claude/events.js +8 -0
  25. package/dist/runtime/src/adapters/codex/config.js +3 -3
  26. package/dist/runtime/src/adapters/codex/events.js +8 -0
  27. package/dist/runtime/src/adapters/opencode/config.js +5 -4
  28. package/dist/runtime/src/adapters/opencode/events.js +8 -0
  29. package/dist/runtime/src/config/explain.js +5 -0
  30. package/dist/runtime/src/fs/transaction.js +2 -1
  31. package/dist/runtime/src/hooks/codex-loop.js +4 -0
  32. package/dist/runtime/src/hooks/completion-gate.js +38 -3
  33. package/dist/runtime/src/install/harness.js +44 -1
  34. package/dist/runtime/src/install/plan.js +16 -11
  35. package/dist/runtime/src/install/preauth.js +44 -18
  36. package/dist/runtime/src/install/profiles.js +8 -2
  37. package/dist/runtime/src/install/update.js +1 -0
  38. package/dist/runtime/src/office/collect.js +268 -0
  39. package/dist/runtime/src/office/page.js +887 -0
  40. package/dist/runtime/src/office/repos.js +43 -0
  41. package/dist/runtime/src/office/scene.js +181 -0
  42. package/dist/runtime/src/office/server.js +143 -0
  43. package/dist/runtime/src/office/sessions.js +130 -0
  44. package/dist/runtime/src/office/snapshot.js +117 -0
  45. package/dist/runtime/src/parallel/finish.js +3 -0
  46. package/dist/runtime/src/parallel/guard.js +7 -1
  47. package/dist/runtime/src/review/slots.js +1 -1
  48. package/dist/runtime/src/run/hosts/codex.js +1 -1
  49. package/dist/runtime/src/run/phase.js +37 -0
  50. package/dist/runtime/src/run/service.js +8 -0
  51. package/dist/runtime/src/schema/validate.js +13 -2
  52. package/dist/runtime/src/security/permissions.js +6 -0
  53. package/dist/runtime/src/verify/source-fingerprint.js +4 -0
  54. package/docs/en/spec/harness-adapters.md +122 -1
  55. package/docs/zh-TW/spec/harness-adapters.md +100 -1
  56. package/package.json +1 -1
  57. package/schemas/config.schema.json +10 -1
package/README.md CHANGED
@@ -56,6 +56,43 @@ Native authorization decisions retain redacted command scope and rationale;
56
56
  missing provider decisions remain unknown. Repair compares actual check outcomes
57
57
  and outstanding finding pins rather than source fingerprints.
58
58
 
59
+ Office is a Preview feature, disabled by default. Enable it in the init/update
60
+ selector (update keeps your current choice), or use explicit flags:
61
+
62
+ ```sh
63
+ agent-ops update --office on
64
+ agent-ops office
65
+ # Disable later: agent-ops update --office off
66
+ ```
67
+
68
+ It prints a tokened `http://127.0.0.1:<port>/` URL (also shown by `run start` and
69
+ `run status`) to a read-only pixel office with cream walls, oak floors
70
+ and sage furniture. The overview fits the viewport without scrolling: each run
71
+ team shares a room, and each ordinary session has its own room, including sessions
72
+ without a task or worktree. The furnished floorplan fills the page below the
73
+ header, with a shared hallway between rooms. You control the Supervisor using
74
+ arrow keys or WASD and can walk into each room; clicking a room also opens its
75
+ larger detail view, with a return control. Characters move between planning, development,
76
+ verification, review and integration areas. Short nameplates show their current
77
+ work; room whiteboards show criteria progress, verification and review results,
78
+ and questions waiting for an answer. Clicking a character shows task details,
79
+ changed files and copyable commands. Chinese and English can be switched in the
80
+ header; the viewer badge reads “You” in English and “你” in Chinese. The page remembers the choice and respects reduced-motion preferences.
81
+ Supervisor movement and character animation run locally without model requests.
82
+ Completed rooms move to Recently completed and remain for at most two hours.
83
+ The page reads agent-ops state and `git diff` paths only, never transcripts or file
84
+ contents. One Office serves every repository you work in: each Office-enabled
85
+ repository registers itself when its sessions report, rooms wear their
86
+ repository's nameplate, and a header menu filters the building to one
87
+ repository. Repositories that are removed, opt out, or stay quiet for two hours
88
+ leave the building. When enabled, managed ordinary sessions start your one
89
+ server automatically; a newly started Office opens the browser once, and
90
+ subsequent sessions in any repository reuse it. A finished worktree's room
91
+ moves to Recently completed as soon as `worktree finish` merges it. The server exits after ten minutes without active runs or
92
+ ordinary sessions. Ordinary-session presence follows hook activity; a crashed
93
+ or silent session expires after 30 minutes without an event. Office failures
94
+ remain advisory and never block session work.
95
+
59
96
  Native lifecycle reachability and framework probe conditions are recorded in
60
97
  [the capability probe](docs/harness/native-goal-probe.md); these observations do not
61
98
  establish external adoption or a reduction in review cost.
@@ -83,7 +120,7 @@ installation plan.
83
120
 
84
121
  The interactive multi-select screens start with no harness or profile selected.
85
122
  Choose at least one of `agy`, `codex`, `claude`, and `opencode`, and at least one of
86
- the `core`, `advisory`, `guardrails`, and `loop` profiles before confirming. For
123
+ the `core`, `advisory`, `guardrails`, `loop`, and `run` profiles before confirming. For
87
124
  scripted use, `--harness all` selects all four harnesses; comma-separated
88
125
  selections such as `codex,opencode` are also supported. The legacy `both` value
89
126
  remains an alias for `codex,claude`.
@@ -200,6 +237,28 @@ agent-ops update \
200
237
  --dry-run --json
201
238
  ```
202
239
 
240
+ ### Automatic runs (`run` profile)
241
+
242
+ `run` is an opt-in profile that also selects `core` and `loop`. With it, the
243
+ Claude Code and Codex rules hand a change that needs more than five acceptance
244
+ criteria to `agent-ops run` instead of splitting it into subtasks; five or fewer
245
+ stay in the session. The agent writes the prompt verbatim plus its proposed
246
+ criteria to the gitignored `.agent-ops/state/run-goal.md` from the main checkout
247
+ (the worktree guard lets exactly that write through), then starts
248
+ `agent-ops run --goal-file .agent-ops/state/run-goal.md --host claude --wait`
249
+ (or `--host codex`, escalated like review) in the background and relays any
250
+ question through `agent-ops run respond`. A start refused with
251
+ `RUN_TARGET_REQUIRED`, `RUN_BACKGROUND_UNSUPPORTED`, `RUN_TARGET_DIRTY`, or
252
+ `WORKTREE_NESTED` falls back to the subtask flow; `RUN_REPO_UNTRUSTED` stops and
253
+ asks you. A run that ends without completing is reported with its status, last
254
+ log events, and the `agent-ops run resume <id>` / `agent-ops run stop <id>`
255
+ commands; it is never resumed automatically. agy keeps the subtask flow.
256
+ Init and update pre-authorize `agent-ops run` for Claude Code and Codex.
257
+
258
+ ```bash
259
+ agent-ops init --scope project --harness codex,claude --profile run --yes
260
+ ```
261
+
203
262
  ## Quick start from a source checkout
204
263
 
205
264
  For development or to run the repository version directly, use a source
@@ -12,11 +12,12 @@ export const COMMAND_NAMES = [
12
12
  "batch",
13
13
  "allow-stop",
14
14
  "worktree",
15
- "run"
15
+ "run",
16
+ "office"
16
17
  ];
17
18
  const COMMAND_SET = new Set(COMMAND_NAMES);
18
19
  const SCOPES = new Set(["project", "user"]);
19
- const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
20
+ const PROFILES = new Set(["advisory", "core", "guardrails", "loop", "run"]);
20
21
  // opencode is absent: it has no read-only flag, so it cannot review.
21
22
  const REVIEW_TARGETS = new Set(["agy", "claude", "codex"]);
22
23
  export const WORKTREE_ACTIONS = ["add", "commit", "finish", "list", "resume", "remove"];
@@ -93,6 +94,7 @@ export function parseArgs(argv) {
93
94
  let checkAuth = false;
94
95
  const checkAuthTargets = [];
95
96
  let completionGate;
97
+ let office;
96
98
  let dryRun = false;
97
99
  let json = false;
98
100
  let yes = false;
@@ -316,6 +318,16 @@ export function parseArgs(argv) {
316
318
  }
317
319
  completionGate = true;
318
320
  break;
321
+ case "--office": {
322
+ if (office !== undefined)
323
+ duplicate(token);
324
+ const value = readOptionValue(argv, index, token);
325
+ if (value !== "on" && value !== "off")
326
+ invalidValue(token, value);
327
+ office = value;
328
+ index += 1;
329
+ break;
330
+ }
319
331
  case "--worktree": {
320
332
  if (worktree !== undefined) {
321
333
  duplicate(token);
@@ -510,6 +522,7 @@ export function parseArgs(argv) {
510
522
  evidence.length > 0 ||
511
523
  reviewTargets.length > 0 ||
512
524
  completionGate !== undefined ||
525
+ office !== undefined ||
513
526
  sessionId !== undefined ||
514
527
  base !== undefined ||
515
528
  parentBase !== undefined ||
@@ -600,6 +613,9 @@ export function parseArgs(argv) {
600
613
  if (completionGate !== undefined && command !== "init") {
601
614
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--completion-gate may be used only with init.");
602
615
  }
616
+ if (office !== undefined && command !== "init" && command !== "update") {
617
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--office may be used only with init or update.");
618
+ }
603
619
  if (worktree !== undefined && command !== "init" && command !== "update") {
604
620
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--worktree may be used only with init or update.");
605
621
  }
@@ -776,6 +792,7 @@ export function parseArgs(argv) {
776
792
  ...(intent === undefined ? {} : { intent }),
777
793
  ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
778
794
  ...(completionGate === undefined ? {} : { completionGate }),
795
+ ...(office === undefined ? {} : { office }),
779
796
  ...(criteria.length === 0 ? {} : { criteria }),
780
797
  ...(criterionFiles.length === 0 ? {} : { criterionFiles }),
781
798
  ...(criteriaFile === undefined ? {} : { criteriaFile }),
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { productionRunContext } from "./run-deps.js";
2
+ import { productionRunContext, runPhaseObserver } from "./run-deps.js";
3
3
  import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
4
4
  import { recordRunVerification } from "../../../runtime/src/run/verification.js";
5
5
  import { runOwnedLocalProof } from "./owned-run-step.js";
6
6
  import { runRunCommand } from "./commands/run.js";
7
+ import { runOfficeCommand } from "./commands/office.js";
8
+ import { ensureBackgroundOffice, observeOfficeSession, openOfficeBrowser, serveOffice } from "./office-entry.js";
7
9
  import { readFile, readdir, stat } from "node:fs/promises";
8
10
  import { randomUUID } from "node:crypto";
9
11
  import { homedir } from "node:os";
@@ -223,7 +225,7 @@ else if (argv[0] === "hook") {
223
225
  stdin: process.stdin,
224
226
  writeStdout: (value) => process.stdout.write(value),
225
227
  writeStderr: (value) => process.stderr.write(value)
226
- }, CLI_VERSION, { worktree: worktreeDependencies() });
228
+ }, CLI_VERSION, { worktree: worktreeDependencies(), office: observeOfficeSession });
227
229
  }
228
230
  else {
229
231
  process.exitCode = await runCli(argv, {
@@ -249,7 +251,11 @@ else {
249
251
  throw new AgentOpsError("RUN_POLICY_COORDINATOR_REQUIRED", "Run workers must request scoped policy review from the coordinator; permanent trust and host rules cannot be changed by the run.");
250
252
  if (args.command === "run") {
251
253
  const context = await productionRunContext(root);
252
- return await runRunCommand({ args, ...context, root: context.mainRoot });
254
+ return await runRunCommand({ args, ...context, root: context.mainRoot, office: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser) });
255
+ }
256
+ if (args.command === "office") {
257
+ return await runOfficeCommand({ ensure: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser),
258
+ foreground: async (onUrl) => await serveOffice(root, onUrl), writeStdout: (text) => { process.stdout.write(text); } });
253
259
  }
254
260
  if (args.command === "init") {
255
261
  const store = trustStore();
@@ -406,6 +412,7 @@ else {
406
412
  trustStore: store,
407
413
  calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
408
414
  confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
415
+ promptOffice: async (message, enabled) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, enabled),
409
416
  promptWorktree: async (message) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, true),
410
417
  ...(updateArgs.targetVersion === undefined
411
418
  ? {}
@@ -447,7 +454,9 @@ else {
447
454
  cwd: root,
448
455
  sessionId: sessionId ?? await resolveCommandSessionId(root),
449
456
  parentTaskId: args.taskId,
450
- deps: worktreeDependencies()
457
+ deps: worktreeDependencies(),
458
+ ...(process.env.AGENT_OPS_RUN_ID === undefined || process.env.AGENT_OPS_WORKER_ID === undefined ? {} :
459
+ { onPhase: await runPhaseObserver(root, process.env.AGENT_OPS_RUN_ID, process.env.AGENT_OPS_WORKER_ID) })
451
460
  });
452
461
  }
453
462
  const createConfig = args.action === "create"
@@ -517,6 +526,7 @@ else {
517
526
  : async (request) => await withReviewSlot({
518
527
  dir: slotDir,
519
528
  signal,
529
+ holder: { root, ...(reviewArgs.taskId === undefined ? {} : { taskId: reviewArgs.taskId }) },
520
530
  onWait: (line) => {
521
531
  process.stderr.write(`${progressPrefix}${line}\n`);
522
532
  }
@@ -30,6 +30,7 @@ Commands:
30
30
  verify Run configured verification
31
31
  review Run an independent review or show recorded reports
32
32
  run Start or control a supervised Claude/Codex native goal
33
+ office Open the local pixel office (Preview; opt-in)
33
34
  batch Verify and review a parent task's subtasks together
34
35
  allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
35
36
  agy-run Run headless agy with a process-exit completion recheck
@@ -40,9 +41,10 @@ Options:
40
41
  --scope <project|user>
41
42
  --harness <all|both|agy|claude|codex|opencode|comma-separated> Init/update/uninstall; review uses configured targets
42
43
  --hook-target <harness=surface-id> Repeatable advanced init/update option
43
- --profile <core|advisory|guardrails|loop> Repeatable
44
+ --profile <core|advisory|guardrails|loop|run> Repeatable
44
45
  --review-target <codex|agy|claude> Repeatable init option; review pair order
45
46
  --completion-gate Init only: enable the project-loop completion gate
47
+ --office <on|off> Init/update: enable or disable Office (Preview)
46
48
  --worktree <auto|off> Init/update: configure session worktree isolation
47
49
  --check-auth Doctor only: probe selected review targets'
48
50
  authentication with one real call
@@ -72,6 +74,20 @@ Options:
72
74
  * rejected.
73
75
  */
74
76
  export const COMMAND_HELP_TEXT = {
77
+ office: `Usage: agent-ops office [--json]
78
+
79
+ Office (Preview) is opt-in: select it during init/update or run
80
+ agent-ops update --office on. Disable with --office off. A missing choice is off.
81
+
82
+ Print the URL of this repository's office: a read-only pixel page on
83
+ 127.0.0.1. All run teams and ordinary sessions appear in a single-screen room
84
+ overview; walk the Supervisor with arrows/WASD or click a room for details.
85
+ Switch Chinese/English in the header. Movement and animation run locally.
86
+ When enabled, managed sessions start the shared server automatically. A new Office opens the
87
+ browser once; later sessions reuse it. It exits after 10 minutes without active
88
+ runs or sessions. Recently completed rooms remain for at most 2 hours.
89
+ The URL carries an access token; do not share it.
90
+ `,
75
91
  run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--time-budget <60m>] [--wait] [--json]
76
92
  agent-ops run [start] --goal-file <path> --host <claude|codex>
77
93
  agent-ops run <status|logs|resume|stop> <run-id> [--json]
@@ -88,9 +104,10 @@ Options:
88
104
  --scope <project|user>
89
105
  --harness <all|both|agy|claude|codex|opencode|comma-separated>
90
106
  --hook-target <harness=surface-id> Repeatable
91
- --profile <core|advisory|guardrails|loop> Repeatable
107
+ --profile <core|advisory|guardrails|loop|run> Repeatable
92
108
  --review-target <codex|agy|claude> Repeatable, in fallback-chain order
93
109
  --completion-gate Enable the project-loop completion gate
110
+ --office <on|off> Office (Preview); default off, update preserves choice
94
111
  --worktree <auto|off> Configure session worktree isolation
95
112
  --dry-run Print the plan without writing
96
113
  --json
@@ -134,6 +151,7 @@ Options:
134
151
  --scope <project|user>
135
152
  --harness <all|both|agy|claude|codex|opencode|comma-separated>
136
153
  --target-version <version> Offline-capable update target
154
+ --office <on|off> Office (Preview); preserve choice unless supplied
137
155
  --worktree <auto|off> Configure session worktree isolation
138
156
  --dry-run
139
157
  --json
@@ -1,4 +1,5 @@
1
- import { PROJECT_LOOP_EVENTS, runProjectLoop } from "../../../runtime/src/hooks/codex-loop.js";
1
+ import { PROJECT_LOOP_EVENTS, resolveProjectLoopRoot, runProjectLoop } from "../../../runtime/src/hooks/codex-loop.js";
2
+ import { officeEnabled, officeLoopPhaseHint } from "./office-entry.js";
2
3
  const MAX_LOOP_INPUT_BYTES = 64 * 1024;
3
4
  function isLoopHarness(value) {
4
5
  return value === "claude" || value === "codex";
@@ -43,10 +44,26 @@ export async function runLoopProcess(argv, io, dependencies = {}) {
43
44
  return 0;
44
45
  }
45
46
  try {
47
+ const input = parseInput(await readStdin(io.stdin));
48
+ const inputIsObject = typeof input === "object" && input !== null && !Array.isArray(input);
49
+ const managedRoot = inputIsObject
50
+ ? await resolveProjectLoopRoot(input, dependencies.root ?? process.cwd(), harness).catch(() => null)
51
+ : null;
52
+ if (dependencies.office !== undefined && managedRoot !== null && await officeEnabled(managedRoot)) {
53
+ const phase = officeLoopPhaseHint(input, managedRoot);
54
+ void dependencies.office({
55
+ root: managedRoot,
56
+ harness,
57
+ event,
58
+ input,
59
+ validated: true,
60
+ ...(phase === undefined ? {} : { phase })
61
+ }).catch(() => undefined);
62
+ }
46
63
  const result = await runProjectLoop({
47
64
  harness,
48
65
  event,
49
- input: parseInput(await readStdin(io.stdin)),
66
+ input,
50
67
  root: dependencies.root ?? process.cwd(),
51
68
  ...(dependencies.now === undefined ? {} : { now: dependencies.now }),
52
69
  ...(dependencies.gitStatus === undefined
@@ -9,6 +9,7 @@ import { renderReviewDetails, renderReviewHighlights } from "../../../../runtime
9
9
  import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
10
10
  import { okEnvelope } from "../output.js";
11
11
  import { readFinishedReview } from "./review-show.js";
12
+ import { criteriaProgress } from "../../../../runtime/src/run/phase.js";
12
13
  import { prepareRunIntegration, readRunIntegrationProof, abandonPreparedRunIntegration } from "../../../../runtime/src/run/integration.js";
13
14
  /** The child process runs the existing CLI in the candidate checkout. */
14
15
  export async function runAdvanceStep(cwd, args) {
@@ -132,10 +133,26 @@ export async function runAdvanceCommand(options) {
132
133
  if (noChangePaths !== undefined)
133
134
  for (const task of ordered)
134
135
  await deps.tasks(record.path).recordEvidence(task.task.id, {}, undefined, noChangePaths);
136
+ const onPhase = options.onPhase ?? (async () => { });
137
+ await onPhase("verifying");
135
138
  for (const task of ordered) {
136
- requirePass(await step(record.path, ["verify", "--task", task.task.id, "--base", target]), `Verify ${task.task.id}`);
139
+ const verified = await step(record.path, ["verify", "--task", task.task.id, "--base", target]);
140
+ await onPhase("verifying", { taskId: task.task.id, update: { verify: verified.status === "ok" ? "PASS" : "FAIL",
141
+ ...criteriaProgress(task.task.criteria, verified.data?.report ?? {}) } });
142
+ requirePass(verified, `Verify ${task.task.id}`);
137
143
  }
138
- const reviewMode = await reviewFinalTree(step, record.path, parent.task.id, ordered.map(({ task }) => task.id), target);
144
+ await onPhase("reviewing");
145
+ let reviewMode;
146
+ try {
147
+ reviewMode = await reviewFinalTree(step, record.path, parent.task.id, ordered.map(({ task }) => task.id), target);
148
+ }
149
+ catch (failure) {
150
+ for (const { task } of ordered)
151
+ await onPhase("reviewing", { taskId: task.id, update: { review: "FAIL" } });
152
+ throw failure;
153
+ }
154
+ for (const { task } of ordered)
155
+ await onPhase("integrating", { taskId: task.id, update: { review: "PASS", passed: task.criteria.length, total: task.criteria.length } });
139
156
  const scope = await resolveReviewScope({ root: record.path, runner: runner(deps, record.path), base: target,
140
157
  ...(noChangePaths === undefined ? {} : { noChangePaths }) });
141
158
  const sourceFingerprint = await calculateSourceFingerprint(record.path, scope, runner(deps, record.path));
@@ -13,6 +13,7 @@ export function formatInstallPlan(plan, trust, warnings = []) {
13
13
  title: "Installation plan",
14
14
  metadata: [
15
15
  `Scope: ${plan.scope}`,
16
+ `Office (Preview): ${plan.config.features.office?.enabled === true ? "enabled" : "disabled"}`,
16
17
  `Harness: ${plan.harness}`,
17
18
  `Profiles: ${plan.profiles.join(", ")}`,
18
19
  ...(plan.config.worktree === undefined
@@ -103,6 +104,7 @@ export async function runInitCommand(options) {
103
104
  scope: args.scope,
104
105
  harness: args.harness,
105
106
  profiles: args.profiles,
107
+ ...(args.office === undefined ? {} : { officeEnabled: args.office === "on" }),
106
108
  adapters: options.adapters,
107
109
  ...(options.toolkitVersion === undefined
108
110
  ? {}
@@ -0,0 +1,20 @@
1
+ import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
2
+ import { errorEnvelope, okEnvelope } from "../output.js";
3
+ export async function runOfficeCommand(options) {
4
+ try {
5
+ const url = await options.ensure();
6
+ if (url !== null)
7
+ return okEnvelope("OFFICE_READY", { url, text: `Office (Preview): ${url}` });
8
+ let shown = "";
9
+ await options.foreground((served) => {
10
+ shown = served;
11
+ options.writeStdout(`Office (Preview): ${served}\nServing in the foreground; it exits 10 minutes after no run or session is active.\n`);
12
+ });
13
+ return okEnvelope("OFFICE_CLOSED", { url: shown, text: "Office closed: no run was active for 10 minutes." });
14
+ }
15
+ catch (cause) {
16
+ if (cause instanceof AgentOpsError)
17
+ return errorEnvelope(cause.code, cause.message);
18
+ throw cause;
19
+ }
20
+ }
@@ -203,17 +203,23 @@ async function preflightReview(options, context, sourceFingerprint) {
203
203
  options.gitRunner === undefined) {
204
204
  return { ok: false, reason: "stale-verification" };
205
205
  }
206
- if (context.criteria.some((criterion) => !options.config.verification.commands.some(({ id, required }) => required && (criterion.verifierIds ?? []).includes(id)))) {
206
+ const typed = (criterion) => {
207
+ const owner = context.criterionOwners?.get(criterion.id);
208
+ return context.records.find(r => r.task.id === (owner?.taskId ?? context.taskId))?.task.criteria
209
+ .find(c => c.id === (owner?.criterionId ?? criterion.id))?.acceptance !== undefined;
210
+ };
211
+ // A typed criterion (review-only included) is held to every required
212
+ // repository command below, so only a legacy one must name its own.
213
+ if (context.criteria.some((criterion) => !typed(criterion) && !options.config.verification.commands.some(({ id, required }) => required && (criterion.verifierIds ?? []).includes(id)))) {
207
214
  return { ok: false, reason: "missing-verification-evidence" };
208
215
  }
209
216
  const configHash = calculateConfigHash(options.config);
210
217
  const commands = [];
211
218
  for (const criterion of context.criteria) {
212
219
  const owner = context.criterionOwners?.get(criterion.id);
213
- const typed = context.records.find(r => r.task.id === (owner?.taskId ?? context.taskId))?.task.criteria
214
- .find(c => c.id === (owner?.criterionId ?? criterion.id))?.acceptance !== undefined;
220
+ const isTyped = typed(criterion);
215
221
  for (const commandId of new Set([...(criterion.verifierIds ?? []),
216
- ...(typed ? options.config.verification.commands.filter(c => c.required).map(c => c.id) : [])])) {
222
+ ...(isTyped ? options.config.verification.commands.filter(c => c.required).map(c => c.id) : [])])) {
217
223
  const command = options.config.verification.commands.find((candidate) => candidate.id === commandId);
218
224
  if (command === undefined) {
219
225
  return { ok: false, reason: "missing-verification-evidence" };
@@ -66,6 +66,10 @@ async function answerFromArgs(args, root) {
66
66
  * External lifecycle surface. The command only starts/controls a supervisor;
67
67
  * it never marks the task complete or treats a native process exit as proof.
68
68
  */
69
+ async function withOffice(options, response) {
70
+ const url = await options.office?.().catch(() => null) ?? null;
71
+ return url === null ? response : { ...response, officeUrl: url, text: `${response.text}\nOffice (Preview): ${url}` };
72
+ }
69
73
  export async function runRunCommand(options) {
70
74
  const action = actionOf(options.args);
71
75
  try {
@@ -83,12 +87,12 @@ export async function runRunCommand(options) {
83
87
  const state = options.args.wait === true && options.waitForCompletion !== undefined
84
88
  ? await options.waitForCompletion(result.state.runId)
85
89
  : result.state;
86
- const response = data(action, { ...result, state });
90
+ const response = await withOffice(options, data(action, { ...result, state }));
87
91
  return okEnvelope(options.args.wait === true ? "RUN_FINISHED" : "RUN_STARTED", response);
88
92
  }
89
93
  const runId = runIdOf(options.args);
90
94
  if (action === "status")
91
- return okEnvelope("RUN_STATUS", stateData(action, await options.service.status(runId), `Read run ${runId}.`));
95
+ return okEnvelope("RUN_STATUS", await withOffice(options, stateData(action, await options.service.status(runId), `Read run ${runId}.`)));
92
96
  if (action === "logs") {
93
97
  const state = await options.service.status(runId);
94
98
  const events = await options.service.logs(runId);
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { loadConfigFile } from "../../../../runtime/src/config/load.js";
3
4
  import { detectWorktreeSetup } from "../../../../runtime/src/install/parallel-setup.js";
4
5
  import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
5
6
  import { okEnvelope } from "../output.js";
@@ -15,6 +16,7 @@ export function formatUpdatePlan(plan, trust) {
15
16
  title: "Update plan",
16
17
  metadata: [
17
18
  `Target version: ${plan.targetVersion}`,
19
+ `Office (Preview): ${plan.installation.config.features.office?.enabled === true ? "enabled" : "disabled"}`,
18
20
  `Harness: ${plan.installation.harness.join(", ")}`,
19
21
  ...(plan.installation.config.worktree === undefined
20
22
  ? []
@@ -95,9 +97,15 @@ export async function runUpdateCommand(options) {
95
97
  };
96
98
  }
97
99
  }
100
+ let officeEnabled = options.args.office === undefined ? undefined : options.args.office === "on";
101
+ if (officeEnabled === undefined && options.promptOffice !== undefined && options.isTTY && !options.args.yes) {
102
+ const { config } = await loadConfigFile(join(options.root, ".agent-ops", "config.json"));
103
+ officeEnabled = await options.promptOffice("Enable Office (Preview)?", config.features.office?.enabled === true);
104
+ }
98
105
  const plan = await createUpdatePlan({
99
106
  root: options.root,
100
107
  adapters: options.adapters,
108
+ ...(officeEnabled === undefined ? {} : { officeEnabled }),
101
109
  ...(options.args.harness === undefined
102
110
  ? {}
103
111
  : { harness: options.args.harness }),
@@ -16,6 +16,7 @@ export const DEFAULT_CONFIG = {
16
16
  profiles: [],
17
17
  verification: { commands: [] },
18
18
  features: {
19
+ office: { enabled: false },
19
20
  stopVerification: {
20
21
  enabled: false
21
22
  },
@@ -35,7 +36,7 @@ export const DEFAULT_CONFIG = {
35
36
  */
36
37
  export function projectConfigRoot(root) {
37
38
  try {
38
- const common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
39
+ const common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
39
40
  if (basename(common) !== ".git") {
40
41
  return root;
41
42
  }
@@ -183,7 +184,8 @@ export function repositoryRemoteUrl(root) {
183
184
  try {
184
185
  return execFileSync("git", ["config", "--get", "remote.origin.url"], {
185
186
  cwd: root,
186
- encoding: "utf8"
187
+ encoding: "utf8",
188
+ windowsHide: true
187
189
  }).trim();
188
190
  }
189
191
  catch {
@@ -213,7 +215,7 @@ export async function repositoryTrust(root, config, cliVersion) {
213
215
  export async function runPolicyContext(root) {
214
216
  let common;
215
217
  try {
216
- common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
218
+ common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
217
219
  }
218
220
  catch {
219
221
  return null;
@@ -233,8 +235,8 @@ export async function runPolicyContext(root) {
233
235
  let proof = false;
234
236
  if (proofProcess != null && String(proofProcess.processId) === process.env.AGENT_OPS_RUN_PROOF_PID) {
235
237
  try {
236
- proof = execFileSync("ps", ["-o", "pgid=", "-p", String(process.pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() === String(proofProcess.processId) &&
237
- execFileSync("ps", ["-o", "lstart=", "-p", String(proofProcess.processId)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() === proofProcess.processIdentity;
238
+ proof = execFileSync("ps", ["-o", "pgid=", "-p", String(process.pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === String(proofProcess.processId) &&
239
+ execFileSync("ps", ["-o", "lstart=", "-p", String(proofProcess.processId)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === proofProcess.processIdentity;
238
240
  }
239
241
  catch {
240
242
  proof = false;
@@ -4,8 +4,9 @@
4
4
  import { CLI_VERSION } from "./version.js";
5
5
  import { runHookProcess } from "./hook-process.js";
6
6
  import { worktreeDependencies } from "./parallel-deps.js";
7
+ import { observeOfficeSession } from "./office-entry.js";
7
8
  process.exitCode = await runHookProcess(process.argv.slice(2), {
8
9
  stdin: process.stdin,
9
10
  writeStdout: (value) => process.stdout.write(value),
10
11
  writeStderr: (value) => process.stderr.write(value)
11
- }, CLI_VERSION, { worktree: worktreeDependencies() });
12
+ }, CLI_VERSION, { worktree: worktreeDependencies(), office: observeOfficeSession });
@@ -9,6 +9,7 @@ import { resolveContainedPath } from "../../../runtime/src/fs/paths.js";
9
9
  import { redactSecrets } from "../../../runtime/src/security/redact.js";
10
10
  import { claudeStopRecursionMarker } from "../../../runtime/src/adapters/claude/input.js";
11
11
  import { HARNESS_IDS, harnessDescriptor } from "../../../runtime/src/install/harness.js";
12
+ import { officePhaseHint } from "./office-entry.js";
12
13
  import { evaluateWorktreeEnter } from "../../../runtime/src/parallel/enter.js";
13
14
  import { evaluateWorktreeWrite, resolveMainRoot } from "../../../runtime/src/parallel/guard.js";
14
15
  import { ensureSessionWorktree, recordSessionOrigin } from "../../../runtime/src/parallel/service.js";
@@ -284,7 +285,11 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
284
285
  parsedInput !== null &&
285
286
  typeof parsedInput.projectRoot === "string"
286
287
  ? parsedInput.projectRoot
287
- : undefined;
288
+ : typeof parsedInput === "object" &&
289
+ parsedInput !== null &&
290
+ typeof parsedInput.cwd === "string"
291
+ ? parsedInput.cwd
292
+ : undefined;
288
293
  if (inputRoot !== undefined) {
289
294
  root = inputRoot;
290
295
  }
@@ -326,6 +331,22 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
326
331
  ? await repositoryTrust(root, config, cliVersion)
327
332
  : await dependencies.trust(root, config, cliVersion);
328
333
  const trusted = trustStatus === "TRUSTED";
334
+ if (dependencies.office !== undefined && config.features.office?.enabled === true) {
335
+ const normalized = normalizeHookInput(harnessId, parsedInput);
336
+ if (normalized !== null) {
337
+ const phase = officePhaseHint(normalized);
338
+ void dependencies.office({
339
+ root,
340
+ harness: harnessId,
341
+ event: hookEvent,
342
+ input: parsedInput,
343
+ validated: true,
344
+ ...(normalized.sessionId === undefined ? {} : { sessionId: normalized.sessionId }),
345
+ ...(normalized.agentId === undefined ? {} : { agentId: normalized.agentId }),
346
+ ...(phase === undefined ? {} : { phase })
347
+ }).catch(() => undefined);
348
+ }
349
+ }
329
350
  const gitRunner = dependencies.gitRunner ?? defaultGitRunner(root);
330
351
  const processRunner = dependencies.processRunner ?? new NodeVerificationProcessRunner();
331
352
  const stopVerification = shouldBuildStopVerification(harnessId, event, config, parsedInput)
@@ -383,7 +404,7 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
383
404
  ? { action: "continue", status: "UNKNOWN", code: "WORKTREE_GUARD_UNAVAILABLE" }
384
405
  : await evaluateWorktreeWrite(mainRoot, write.paths, write.sessionId, dependencies.worktree === undefined
385
406
  ? undefined
386
- : async (sessionId, agentId) => (await ensureSessionWorktree(dependencies.worktree, { cwd: mainRoot, sessionId, ...(agentId === undefined ? {} : { agentId }) })).path, write.agentId);
407
+ : async (sessionId, agentId) => (await ensureSessionWorktree(dependencies.worktree, { cwd: mainRoot, sessionId, ...(agentId === undefined ? {} : { agentId }) })).path, write.agentId, config.profiles.includes("run"));
387
408
  }
388
409
  : undefined;
389
410
  const worktreeEnter = config.worktree?.mode === "auto"
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // Entry point used only by the generated project-local loop launchers.
3
3
  import { runLoopProcess } from "./codex-loop-process.js";
4
+ import { observeOfficeSession } from "./office-entry.js";
4
5
  process.exitCode = await runLoopProcess(process.argv.slice(2), {
5
6
  stdin: process.stdin,
6
7
  writeStdout: (value) => process.stdout.write(value),
7
8
  writeStderr: (value) => process.stderr.write(value)
8
- });
9
+ }, { office: observeOfficeSession });