@kylecheng3146/agent-ops 0.5.3 → 0.7.0

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