@kylecheng3146/agent-ops 0.8.3 → 0.8.5

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.
@@ -9,7 +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
+ import { officePhaseHint, officeLoopPhaseHint } from "./office-entry.js";
13
13
  import { evaluateWorktreeEnter } from "../../../runtime/src/parallel/enter.js";
14
14
  import { evaluateWorktreeWrite, resolveMainRoot } from "../../../runtime/src/parallel/guard.js";
15
15
  import { ensureSessionWorktree, recordSessionOrigin } from "../../../runtime/src/parallel/service.js";
@@ -461,7 +461,7 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
461
461
  await startDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
462
462
  if (dependencies.office !== undefined && config.features.office?.enabled === true) {
463
463
  if (normalized !== null) {
464
- const phase = officePhaseHint(normalized);
464
+ const phase = officePhaseHint(normalized) ?? (harnessId === "codex" ? officeLoopPhaseHint(parsedInput, root) : undefined);
465
465
  void dependencies.office({
466
466
  root,
467
467
  harness: harnessId,
@@ -472,6 +472,9 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
472
472
  ...(normalized.agentId === undefined ? {} : { agentId: normalized.agentId }),
473
473
  ...(phase === undefined ? {} : { phase })
474
474
  }).catch(() => undefined);
475
+ // Native patch hooks added for Office must not emit unsupported-policy messages.
476
+ if (harnessId === "codex" && normalized.event === "unsupported" && phase === "implementing")
477
+ return 0;
475
478
  }
476
479
  }
477
480
  const gitRunner = dependencies.gitRunner ?? defaultGitRunner(root);
@@ -1,5 +1,5 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import { basename, join } from "node:path";
2
+ import { basename, join, resolve } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { rm } from "node:fs/promises";
5
5
  import { createHash } from "node:crypto";
@@ -64,17 +64,46 @@ export function officePhaseHint(event) {
64
64
  : [];
65
65
  let phase;
66
66
  for (const command of commands) {
67
- if (basename(command.command) !== "agent-ops")
67
+ let argv = [command.command, ...command.args];
68
+ // Unwrap display hints here; command-policy normalization stays unchanged.
69
+ while (basename(argv[0] ?? "") === "env") {
70
+ argv = argv.slice(1);
71
+ while (argv.length > 0) {
72
+ if (argv[0] === "-u" || argv[0] === "--unset") {
73
+ if (argv[1] === undefined || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(argv[1]))
74
+ break;
75
+ argv = argv.slice(2);
76
+ }
77
+ else if (/^--unset=[A-Za-z_][A-Za-z0-9_]*$/u.test(argv[0] ?? ""))
78
+ argv = argv.slice(1);
79
+ else
80
+ break;
81
+ }
82
+ if (argv[0] === "--")
83
+ argv = argv.slice(1);
84
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(argv[0] ?? ""))
85
+ argv = argv.slice(1);
86
+ }
87
+ if (basename(argv[0] ?? "") === "node" || basename(argv[0] ?? "") === "node.exe") {
88
+ const script = argv[1];
89
+ if (script === undefined || ![
90
+ fileURLToPath(new URL("./bin.js", import.meta.url)),
91
+ resolve(event.projectRoot, "dist/packages/cli/src/bin.js")
92
+ ].includes(resolve(event.projectRoot, script)))
93
+ continue;
94
+ argv = ["agent-ops", ...argv.slice(2)];
95
+ }
96
+ if (basename(argv[0] ?? "") !== "agent-ops")
68
97
  continue;
69
- const action = command.args[0];
98
+ const action = argv[1];
70
99
  // batch and advance report their later steps themselves; this is their first.
71
100
  const next = action === "verify" || action === "batch"
72
101
  ? "verifying"
73
102
  : action === "review"
74
103
  ? "reviewing"
75
- : action === "worktree" && command.args[1] === "finish"
104
+ : action === "worktree" && argv[2] === "finish"
76
105
  ? "integrating"
77
- : action === "task" && (command.args[1] === "complete" || command.args[1] === "advance")
106
+ : action === "task" && (argv[2] === "complete" || argv[2] === "advance")
78
107
  ? "integrating"
79
108
  : action === "task"
80
109
  ? "planning"
@@ -93,10 +122,12 @@ export function officeLoopPhaseHint(input, root) {
93
122
  const tool = input.tool_name;
94
123
  if (tool === "Edit" || tool === "Write" || tool === "NotebookEdit" || tool === "FileWrite")
95
124
  return "implementing";
125
+ const toolInput = recordValue(input, "tool_input");
126
+ const command = recordValue(toolInput, "command") ?? recordValue(toolInput, "cmd");
127
+ if (tool === "apply_patch")
128
+ return typeof command === "string" && command.length > 0 ? "implementing" : undefined;
96
129
  if (tool !== "Bash")
97
130
  return undefined;
98
- const toolInput = recordValue(input, "tool_input");
99
- const command = recordValue(toolInput, "command");
100
131
  return typeof command === "string" ? officePhaseHint(normalizeShellHookEvent(command, root)) : undefined;
101
132
  }
102
133
  /** Records hook presence and starts Office at the production process boundary. */
@@ -96,6 +96,9 @@ export function buildCodexHookConfig(capabilities, runtimePath) {
96
96
  if (capabilities.includes("task-debug")) {
97
97
  hooks.Interrupt = [{ hooks: [{ ...commandHook("Interrupt", runtimePath), timeout: 3 }] }];
98
98
  }
99
+ if (capabilities.includes("office-presence") && hooks.PreToolUse !== undefined) {
100
+ hooks.PreToolUse = hooks.PreToolUse.map(group => ({ ...group, matcher: "^(Bash|apply_patch)$" }));
101
+ }
99
102
  return {
100
103
  description: "agent-ops lifecycle hooks.",
101
104
  hooks
@@ -483,7 +483,7 @@ export async function createInstallPlan(options) {
483
483
  });
484
484
  const contribution = {
485
485
  artifacts: [
486
- ...(options.scope === "project" ? [{ id: "runtime-ignore", path: ".agent-ops/.gitignore", content: "/tasks/\n/reviews/\n" }] : []),
486
+ ...(options.scope === "project" ? [{ id: "runtime-ignore", path: ".agent-ops/.gitignore", content: "/tasks/\n/reviews/\n/state/\n" }] : []),
487
487
  ...baseContribution.artifacts,
488
488
  ...loopContribution.artifacts
489
489
  ],
@@ -87,9 +87,6 @@ export function debugRemainingMs(state, now = Date.now()) {
87
87
  export function debugStopCause(state, now = Date.now()) {
88
88
  if (state.control?.paused === true)
89
89
  return "user-pause";
90
- const latest = state.receipts.at(-1);
91
- if (latest?.cause !== null && latest?.cause !== undefined)
92
- return latest.cause;
93
90
  if (debugRemainingMs(state, now) === 0)
94
91
  return "budget";
95
92
  if (debugRounds(state) >= state.maxRounds)
@@ -102,7 +99,9 @@ export function debugStopCause(state, now = Date.now()) {
102
99
  break;
103
100
  stagnant++;
104
101
  }
105
- return stagnant >= 2 ? state.strategy === null ? "strategy-required" : "stagnation" : null;
102
+ if (stagnant >= 2)
103
+ return state.strategy === null ? "strategy-required" : "stagnation";
104
+ return state.receipts.at(-1)?.cause ?? null;
106
105
  }
107
106
  /** Only reviewed reasoning/proof belongs in the digest; operations and clocks do not. */
108
107
  export function debugProofDigest(state) {
@@ -349,9 +349,9 @@ export class DebugService {
349
349
  fail("TASK_DEBUG_HISTORY_LIMIT", "Execution history is full; retain it and report the blocker.");
350
350
  const productDigest = await this.#productDigest(root.debug);
351
351
  const previousCandidate = root.debug.receipts.filter(r => r.kind === "verify" && (r.outcome === "PASS" || r.outcome === "FAIL")).at(-1)?.productDigest ?? sha256("[]");
352
- const latestAttempt = root.debug.receipts.filter(r => r.kind === "verify").at(-1)?.productDigest ?? sha256("[]");
353
352
  const cause = debugStopCause(root.debug);
354
- if (cause !== null && !(["environment", "permission"].includes(cause) && (kind === "investigate" || kind === "verify" && productDigest === latestAttempt)) && !(cause === "iterations" && kind === "review" && productDigest === previousCandidate))
353
+ // Recovery reruns the original verifier; changed product content still needs fresh proof.
354
+ if (cause !== null && !(["environment", "permission"].includes(cause) && (kind === "investigate" || kind === "verify")) && !(cause === "iterations" && kind === "review" && productDigest === previousCandidate))
355
355
  fail("TASK_DEBUG_STOP_REQUIRED", `Runtime stop predicate: ${cause}. Record a checkpoint; resume alone cannot reset it.`);
356
356
  const processIdentity = await nativeProcessIdentity(process.pid);
357
357
  if (processIdentity === null)
@@ -579,7 +579,7 @@ export class DebugService {
579
579
  const exhaustedAtEvent = foregroundActive
580
580
  ? remainingAtEvent === 0
581
581
  : debugRemainingMs(root.debug) === 0 && event.timestamp > lastSealedAt;
582
- if (!exhaustedAtEvent || otherCause !== null || !/^(?:繼續|继续|continue)[。.!!]?$/iu.test(event.text.trim()))
582
+ if (!exhaustedAtEvent || (otherCause !== null && !["environment", "permission"].includes(otherCause)) || !/^(?:繼續|继续|continue)[。.!!]?$/iu.test(event.text.trim()))
583
583
  fail("TASK_DEBUG_GRANT_INVALID", "Only a contemporaneous human continue event extends exhausted time; other blockers remain authoritative.");
584
584
  await this.#mutate(root, { ...root.debug, grants: [...root.debug.grants, { eventId: event.id, sessionId, sourceDigest: event.sourceDigest, grantedMs: DEBUG_EXTENSION_MS }], checkpoint: null });
585
585
  }
@@ -1,6 +1,7 @@
1
1
  import { TextDecoder } from "node:util";
2
2
  import { AgentOpsError } from "../fs/paths.js";
3
3
  const WINDOWS_RESERVED_SEGMENT = /^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)(?:\..*)?$/i;
4
+ const HARNESS_PROCESS_HEARTBEAT = /^\.(?:claude|codex)\/\.agent-ops-process-[1-9]\d*-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
4
5
  function sortedUnique(values) {
5
6
  return [...new Set(values)].sort();
6
7
  }
@@ -91,7 +92,9 @@ export async function collectChangeSurface(runner) {
91
92
  ])).filter(
92
93
  // Indexed runtime was rejected above; only generated, untracked output is excluded.
93
94
  (path) => !path.startsWith(".agent-ops/tasks/") &&
94
- !path.startsWith(".agent-ops/reviews/"));
95
+ !path.startsWith(".agent-ops/reviews/") &&
96
+ // Private marker locks keep these runtime heartbeats until process exit.
97
+ !HARNESS_PROCESS_HEARTBEAT.test(path));
95
98
  return {
96
99
  staged,
97
100
  unstaged,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Evidence-driven development loops for agy, Codex, Claude Code, and opencode",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",