@kylecheng3146/agent-ops 0.8.4 → 0.8.6

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.
@@ -637,8 +637,8 @@ export function parseArgs(argv) {
637
637
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
638
638
  }
639
639
  if (base !== undefined && command !== "verify" && command !== "review" &&
640
- command !== "batch" && !(command === "task" && action === "complete")) {
641
- throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify, review, batch or task complete.");
640
+ command !== "batch" && !(command === "task" && (action === "complete" || action === "debug"))) {
641
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify, review, batch, task complete or task debug.");
642
642
  }
643
643
  if ((parentBase !== undefined || width !== undefined) && command !== "batch") {
644
644
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `${parentBase !== undefined ? "--parent-base" : "--width"} may be used only with batch.`);
@@ -196,7 +196,7 @@ Options:
196
196
  --task <id> status, attach, revise, replan, pin-finding, complete, archive, export, advance
197
197
  --session <id> create, attach, status, advance, debug
198
198
  --evidence <criterion-id=reference> complete, repeatable
199
- --base <git-ref> complete: a clean committed range
199
+ --base <git-ref> complete, debug: a clean committed range
200
200
  --yes advance: authorize the two review sessions
201
201
  --json
202
202
 
@@ -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
@@ -3,6 +3,10 @@ import { sha256 } from "../fs/hash.js";
3
3
  import { AgentOpsError } from "../fs/paths.js";
4
4
  import { RunControlService } from "./controls.js";
5
5
  const plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6
+ const MAX_SCOPE = 32 * 1024;
7
+ /** A large Write or patch is still one observation: keep its identity by digest, never block the run on its size. */
8
+ const bounded = (scope) => scope.length <= MAX_SCOPE ? scope :
9
+ canonicalJson({ truncated: true, length: scope.length, sha256: sha256(scope), head: scope.slice(0, 4096) });
6
10
  /** Provider decisions are observations, never an alternate permission executor. */
7
11
  export async function recordNativeRunAuthorization(repository, event) {
8
12
  if (!plain(event.payload))
@@ -51,10 +55,10 @@ export async function recordNativeRunAuthorization(repository, event) {
51
55
  if (event.host === "codex" && payload.threadId !== worker.nativeJobId)
52
56
  throw new AgentOpsError("RUN_AUTHORIZATION_STALE", "Native approval belongs to another thread.");
53
57
  const service = new RunControlService(repository);
54
- for (const decision of decisions)
58
+ for (const { command, resource, ...decision } of decisions)
55
59
  await service.recordAuthorization(event.runId, { ...decision,
56
- authorizationId: decision.id, workerId: worker.workerId, generation: worker.generation, nativeSessionId: worker.nativeSessionId,
57
- source: event.host === "codex" ? "codex-auto-review" : "claude-auto-mode", commandDigest: sha256(decision.command),
58
- resourceDigest: sha256(decision.resource), policyConfigHash: state.policyBinding.configHash,
60
+ command: bounded(command), resource: bounded(resource), authorizationId: decision.id, workerId: worker.workerId, generation: worker.generation, nativeSessionId: worker.nativeSessionId,
61
+ source: event.host === "codex" ? "codex-auto-review" : "claude-auto-mode", commandDigest: sha256(bounded(command)),
62
+ resourceDigest: sha256(bounded(resource)), policyConfigHash: state.policyBinding.configHash,
59
63
  runtimeHash: state.policyBinding.runtimeHash, policyArtifactDigest: state.policyBinding.artifactDigest, expiresAt: state.policyBinding.expiresAt });
60
64
  }
@@ -2,6 +2,9 @@
2
2
  export function isDebugIssue(prompt) {
3
3
  if (prompt.length === 0 || prompt.length > 64 * 1024 || prompt.includes("\0"))
4
4
  return false;
5
+ // Harness-injected turns (subagent hand-backs, task notifications) are not a human report.
6
+ if (/^\s*<(?:agent-message|task-notification|teammate-message)\b/u.test(prompt))
7
+ return false;
5
8
  const value = prompt.normalize("NFKC").toLowerCase();
6
9
  if (/\b(?:explain|describe|what is|how does|refactor|feature|implement|design)\b|說明|解释|解釋|重構|新增功能|設計|實作功能/u.test(value))
7
10
  return false;
@@ -38,7 +38,8 @@ function codexUserText(row) {
38
38
  return undefined;
39
39
  }
40
40
  /** Host transcripts are the user-event authority; CLI actor/text fields are never accepted. */
41
- export async function readDebugUserEvent(root, sessionId, home = homedir(), kind = "latest") {
41
+ /** `worktreeRoot` is the session's owned worktree: Claude stamps each row with the shell's cwd, which moves there after `cd`. */
42
+ export async function readDebugUserEvent(root, sessionId, home = homedir(), kind = "latest", worktreeRoot) {
42
43
  if (!/^[a-f0-9-]{36}$/u.test(sessionId))
43
44
  throw new AgentOpsError("TASK_DEBUG_GRANT_UNSUPPORTED", "This host has no identifiable native user-event source.");
44
45
  const canonicalHome = await realpath(home).catch(() => home);
@@ -77,7 +78,8 @@ export async function readDebugUserEvent(root, sessionId, home = homedir(), kind
77
78
  for (let index = rows.length - 1; index >= 0; index--) {
78
79
  const row = rows[index];
79
80
  const text = codexSource ? codexUserText(row) :
80
- row?.type === "user" && row.sessionId === sessionId && row.cwd === root && row.message?.role === "user" ? row.message.content : undefined;
81
+ row?.type === "user" && row.sessionId === sessionId && (row.cwd === root || (worktreeRoot !== undefined && row.cwd === worktreeRoot)) && row.message?.role === "user" &&
82
+ row.isMeta !== true && (row.origin === undefined || row.origin?.kind === "human") ? row.message.content : undefined;
81
83
  if (typeof text !== "string" || text.length > 64 * 1024 || (kind === "control" && debugUserControl(text) === null))
82
84
  continue;
83
85
  const timestamp = Date.parse(row.timestamp);
@@ -248,7 +248,7 @@ export class DebugService {
248
248
  const record = await readWorktreeRecord(this.options.root);
249
249
  const origin = record?.mainRoot ?? this.options.root;
250
250
  try {
251
- return await readDebugUserEvent(origin, sessionId, home, kind);
251
+ return await readDebugUserEvent(origin, sessionId, home, kind, origin === this.options.root ? undefined : this.options.root);
252
252
  }
253
253
  catch (error) {
254
254
  if (origin === this.options.root || !(error instanceof AgentOpsError) || error.code !== "TASK_DEBUG_GRANT_UNSUPPORTED")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
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",