halfcycle 0.4.18 → 0.5.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.
@@ -15033,6 +15033,7 @@ var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
15033
15033
  // ../core/dist/task-plan.js
15034
15034
  var TASK_PLAN_TASKS_MAX = 500;
15035
15035
  var TASK_PLAN_ID_MAX = 64;
15036
+ var TASK_PLAN_BUILDS_NONE = "none";
15036
15037
  var TASK_PLAN_KINDS = ["build", "review", "spec", "walk", "check"];
15037
15038
  var CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/;
15038
15039
  var planText = external_exports.string().refine((text) => !CONTROL_CHARACTER.test(text), { message: "holds a control character" });
@@ -15048,7 +15049,8 @@ var taskPlanTaskSchema = external_exports.object({
15048
15049
  interfaces: external_exports.array(planText).optional()
15049
15050
  }).strict().optional(),
15050
15051
  wholeProject: external_exports.literal(true).optional(),
15051
- ships: external_exports.enum(["deploy", "release"]).optional()
15052
+ ships: external_exports.enum(["deploy", "release"]).optional(),
15053
+ builds: external_exports.union([external_exports.literal(TASK_PLAN_BUILDS_NONE), external_exports.array(planText)]).optional()
15052
15054
  }).strict().superRefine((task, ctx) => {
15053
15055
  if (task.reviews !== void 0 && task.kind !== "review") {
15054
15056
  ctx.addIssue({
@@ -15085,6 +15087,101 @@ var taskListFileSchema = external_exports.union([
15085
15087
  external_exports.array(external_exports.unknown())
15086
15088
  ]);
15087
15089
 
15090
+ // ../core/dist/agent-profile.js
15091
+ var AGENT_IDS = ["claude-code", "codex"];
15092
+ var CLAUDE_CODE_PROFILE = {
15093
+ id: "claude-code",
15094
+ settingsFiles: [SETTINGS_REL, LOCAL_SETTINGS_REL],
15095
+ wiring: HALFCYCLE_HOOK_WIRING,
15096
+ commands: HALFCYCLE_HOOK_COMMANDS,
15097
+ wiringPaths: HALFCYCLE_HOOK_WIRING_PATHS,
15098
+ writeCapableTools: WRITE_CAPABLE_TOOLS,
15099
+ projectRoot: "claude-project-dir",
15100
+ configFolder: "claude-config",
15101
+ mcpRegistration: { file: MCP_REGISTRATION_REL, serversKey: "mcpServers", serverKey: MCP_SERVER_KEY },
15102
+ tierModels: {
15103
+ deep: { model: "opus" },
15104
+ standard: { model: "sonnet" },
15105
+ fast: { model: "haiku" }
15106
+ }
15107
+ };
15108
+ var CODEX_HOOKS_REL = ".codex/hooks.json";
15109
+ var CODEX_CONFIG_REL = ".codex/config.toml";
15110
+ function codexHookCommand(script, args2) {
15111
+ const invocation = `cd "$(git rev-parse --show-toplevel)" && bash ./${script}`;
15112
+ return args2 === void 0 ? invocation : `${invocation} ${args2}`;
15113
+ }
15114
+ var CODEX_WINDOWS_NOT_SET_UP = "Halfcycle checks did not run because Halfcycle is not set up in this folder or any folder above it; run npx halfcycle in the project folder.";
15115
+ var CODEX_WINDOWS_NO_GIT_BASH = "Halfcycle checks did not run because Git for Windows was not found; install Git for Windows, then open a new session.";
15116
+ function codexHookCommandWindows(script, args2) {
15117
+ const tick = (text) => "`" + text + "`";
15118
+ const argv = [script, ...args2 === void 0 ? [] : args2.split(" ")].map(tick).join(",");
15119
+ const program = [
15120
+ "const p=require(`path`),f=require(`fs`),c=require(`child_process`);",
15121
+ "let d=process.cwd();",
15122
+ `for(;;){if(f.existsSync(p.join(d,${tick(VENDORED_BIN_REL)})))break;`,
15123
+ "const u=p.dirname(d);",
15124
+ "if(u===d){d=``;break}",
15125
+ "d=u}",
15126
+ `if(d===\`\`){console.error(${tick(CODEX_WINDOWS_NOT_SET_UP)})}else{`,
15127
+ "const g=c.spawnSync(`git`,[`--exec-path`],{encoding:`utf8`});",
15128
+ "const b=g.status===0?p.join(g.stdout.trim(),`..`,`..`,`..`,`bin`,`bash.exe`):``;",
15129
+ `if(b===\`\`||!f.existsSync(b)){console.error(${tick(CODEX_WINDOWS_NO_GIT_BASH)})}else{`,
15130
+ `const r=c.spawnSync(b,[${argv}],{cwd:d,stdio:\`inherit\`});`,
15131
+ "process.exitCode=typeof r.status===`number`?r.status:1}}"
15132
+ ].join("");
15133
+ return `node -e '${program}'`;
15134
+ }
15135
+ function codexHookRow(event, matcher, timeout, script, args2) {
15136
+ return {
15137
+ event,
15138
+ matcher,
15139
+ timeout,
15140
+ command: codexHookCommand(script, args2),
15141
+ script,
15142
+ commandWindows: codexHookCommandWindows(script, args2)
15143
+ };
15144
+ }
15145
+ var CODEX_WRITE_CAPABLE_TOOLS = ["apply_patch", "Edit", "Write"];
15146
+ var CODEX_PRE_TOOL_USE_MATCHER = "^(Bash|apply_patch)$";
15147
+ var CODEX_HOOK_WIRING = [
15148
+ codexHookRow("SessionStart", null, 5, SESSION_START_MARKER_REL),
15149
+ codexHookRow("SessionStart", null, 10, GUARD_RUNNER_REL, "refresh-credential --agent codex"),
15150
+ codexHookRow("UserPromptSubmit", null, 5, USER_PROMPT_REMINDER_REL),
15151
+ codexHookRow("UserPromptSubmit", null, 10, USER_PROMPT_SCOPE_REL, "--agent codex"),
15152
+ codexHookRow("UserPromptSubmit", null, 10, USER_PROMPT_FINDINGS_REL, "--agent codex"),
15153
+ codexHookRow("PreToolUse", CODEX_PRE_TOOL_USE_MATCHER, 10, GUARD_RUNNER_REL, "hook pre-tool-use --agent codex"),
15154
+ codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
15155
+ codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
15156
+ codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
15157
+ codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
15158
+ ];
15159
+ var RETIRED_CODEX_HOOK_COMMANDS = [];
15160
+ function codexHookCommandSet(retired) {
15161
+ return /* @__PURE__ */ new Set([...CODEX_HOOK_WIRING.map((entry) => entry.command), ...retired]);
15162
+ }
15163
+ var CODEX_PROFILE = {
15164
+ id: "codex",
15165
+ settingsFiles: [CODEX_HOOKS_REL],
15166
+ wiring: CODEX_HOOK_WIRING,
15167
+ commands: codexHookCommandSet(RETIRED_CODEX_HOOK_COMMANDS),
15168
+ wiringPaths: [.../* @__PURE__ */ new Set([CODEX_HOOKS_REL, ...CODEX_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])],
15169
+ writeCapableTools: CODEX_WRITE_CAPABLE_TOOLS,
15170
+ projectRoot: "git-toplevel",
15171
+ configFolder: "codex-home",
15172
+ mcpRegistration: { file: CODEX_CONFIG_REL, serversKey: "mcp_servers", serverKey: MCP_SERVER_KEY },
15173
+ tierModels: {
15174
+ deep: { model: "gpt-6-luna", effort: "high" },
15175
+ standard: { model: "gpt-6-luna", effort: "medium" },
15176
+ fast: { model: "gpt-6-luna", effort: "low" }
15177
+ }
15178
+ };
15179
+ var AGENT_PROFILES = {
15180
+ "claude-code": CLAUDE_CODE_PROFILE,
15181
+ codex: CODEX_PROFILE
15182
+ };
15183
+ var ALL_HOOK_COMMANDS = new Set(AGENT_IDS.flatMap((id) => [...AGENT_PROFILES[id].commands]));
15184
+
15088
15185
  // dist/control-origin.js
15089
15186
  var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
15090
15187
  function resolveControlOrigin(env) {
@@ -15269,7 +15366,8 @@ var seatPathSchema = external_exports.object({
15269
15366
  }).strict();
15270
15367
  var seatConfigurationSchema = external_exports.object({
15271
15368
  settingsFiles: external_exports.array(seatSettingsFileSchema),
15272
- paths: external_exports.array(seatPathSchema)
15369
+ paths: external_exports.array(seatPathSchema),
15370
+ agent: external_exports.string().optional()
15273
15371
  }).strict();
15274
15372
  var evaluationRequestSchema = external_exports.object({
15275
15373
  engagementId: external_exports.string(),
@@ -15960,7 +16058,7 @@ function crossFieldProblems(raw) {
15960
16058
  const parts = Array.isArray(raw.parts) ? raw.parts : [];
15961
16059
  const connections = Array.isArray(raw.connections) ? raw.connections : [];
15962
16060
  const ids = /* @__PURE__ */ new Set();
15963
- const placed = /* @__PURE__ */ new Map();
16061
+ const placed2 = /* @__PURE__ */ new Map();
15964
16062
  const owners = /* @__PURE__ */ new Map();
15965
16063
  parts.forEach((part, i) => {
15966
16064
  if (!isRecord(part))
@@ -15973,14 +16071,14 @@ function crossFieldProblems(raw) {
15973
16071
  }
15974
16072
  if (typeof part.lane === "string" && typeof part.slot === "number") {
15975
16073
  const place = `${part.lane}\0${part.slot}`;
15976
- const earlier = placed.get(place);
16074
+ const earlier = placed2.get(place);
15977
16075
  if (earlier !== void 0) {
15978
16076
  problems.push({
15979
16077
  path: `parts.${i}.slot`,
15980
16078
  message: `${quoted(parts[earlier].id)} already sits in lane ${shown(part.lane)} at slot ${part.slot}`
15981
16079
  });
15982
16080
  } else {
15983
- placed.set(place, i);
16081
+ placed2.set(place, i);
15984
16082
  }
15985
16083
  }
15986
16084
  if (typeof part.outside === "boolean" && Array.isArray(part.owns)) {
@@ -16348,7 +16446,10 @@ var buildSnapshotReportResultSchema = external_exports.object({
16348
16446
  // Which phase the graph was recorded against.
16349
16447
  phase: buildPhaseSchema,
16350
16448
  // When it was recorded.
16351
- reportedAt: utcIso8601
16449
+ reportedAt: utcIso8601,
16450
+ // One sentence to pass on to the developer about the graph just stored. Absent
16451
+ // when there is nothing to say.
16452
+ warning: external_exports.string().min(1).max(400).optional()
16352
16453
  }).strict();
16353
16454
 
16354
16455
  // ../events/dist/change-path-order.js
@@ -16795,8 +16896,13 @@ var METHOD_FILE_PATTERNS = [
16795
16896
  ".claude/commands",
16796
16897
  ".claude/hooks",
16797
16898
  ".claude/settings.json",
16899
+ ".claude/agents",
16798
16900
  ".mcp.json",
16799
16901
  ".halfcycle",
16902
+ ".agents/skills",
16903
+ ".codex/hooks.json",
16904
+ ".codex/config.toml",
16905
+ ".codex/agents",
16800
16906
  ".workbench",
16801
16907
  "docs/product-spec.md",
16802
16908
  "docs/architecture-overview.md",
@@ -18134,7 +18240,7 @@ function approvalSentence(phase, offerTyping) {
18134
18240
  return TYPED_APPROVAL_OFF_SENTENCE;
18135
18241
  if (offerTyping === "update")
18136
18242
  return CLIENT_UPDATE_SENTENCE;
18137
- return `In their own Claude Code session, they ask to see the plan, check each part, and then type: approve ${phase}.`;
18243
+ return `In their own coding agent's session, they ask to see the plan, check each part, and then type: approve ${phase}.`;
18138
18244
  }
18139
18245
  function phaseWords(phase) {
18140
18246
  return `phase "${phase}"`;
@@ -18743,6 +18849,7 @@ var savedSchema = external_exports.object({
18743
18849
  branches: external_exports.array(savedBranchSchema)
18744
18850
  // every saved line of work
18745
18851
  }).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
18852
+ var AUTONOMY_A1_TITLE = "Let builds run on their own";
18746
18853
  var AUTONOMY_A4 = 'Builds in this project now run on their own. Turn it off with "autonomy off", or in the console.';
18747
18854
  var AUTONOMY_A5 = "Builds in this project wait for you again.";
18748
18855
  function withoutFinalStop(text) {
@@ -18770,6 +18877,28 @@ var AUTONOMY_A18 = "Halfcycle cannot check PowerShell commands while builds run
18770
18877
 
18771
18878
  // dist/hook-protocol.js
18772
18879
  import { writeSync } from "node:fs";
18880
+
18881
+ // dist/agent-argument.js
18882
+ var AGENT_FLAG = "--agent";
18883
+ function agentFromArguments(rest) {
18884
+ const leading = rest[0] === AGENT_FLAG;
18885
+ const start = leading ? 2 : 0;
18886
+ for (let i = start; i < rest.length; i += 1) {
18887
+ if ((rest[i] ?? "").startsWith(AGENT_FLAG))
18888
+ return null;
18889
+ }
18890
+ if (!leading)
18891
+ return AGENT_PROFILES["claude-code"];
18892
+ const id = rest[1];
18893
+ if (id === void 0 || !isAgentId(id))
18894
+ return null;
18895
+ return AGENT_PROFILES[id];
18896
+ }
18897
+ function isAgentId(id) {
18898
+ return AGENT_IDS.includes(id);
18899
+ }
18900
+
18901
+ // dist/hook-protocol.js
18773
18902
  function emitBlock(reason, systemMessage) {
18774
18903
  const body = systemMessage === void 0 ? { decision: "block", reason } : { decision: "block", reason, systemMessage };
18775
18904
  process.stdout.write(JSON.stringify(body) + "\n");
@@ -18783,12 +18912,8 @@ function preToolUseReplyText(answer, text = "", agentLine) {
18783
18912
  hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: text }
18784
18913
  });
18785
18914
  }
18786
- const forAgent = agentLine === void 0 || agentLine === "" ? text : `${text}
18787
- ${agentLine}`;
18788
- return JSON.stringify({
18789
- systemMessage: text,
18790
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: forAgent }
18791
- });
18915
+ const hookSpecificOutput = agentLine === void 0 || agentLine === "" ? { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: text } : { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: text, additionalContext: agentLine };
18916
+ return JSON.stringify({ systemMessage: text, hookSpecificOutput });
18792
18917
  }
18793
18918
  function preToolUseNoticeText(text) {
18794
18919
  return JSON.stringify({ systemMessage: text });
@@ -18808,13 +18933,26 @@ function nonBlockingReply(event, text) {
18808
18933
  }
18809
18934
  return JSON.stringify({ systemMessage: text });
18810
18935
  }
18811
- function hookReply(event) {
18936
+ function hookReply(event, profile = AGENT_PROFILES["claude-code"]) {
18812
18937
  const queued = [];
18938
+ const forPerson = [];
18813
18939
  const forAgent = [];
18940
+ const jsonOnly = repliesInJsonOnly(profile);
18814
18941
  return {
18815
18942
  notice(text) {
18816
18943
  queued.push(text);
18817
18944
  },
18945
+ personNotice(text) {
18946
+ queued.push(text);
18947
+ forPerson.push(text);
18948
+ },
18949
+ loud(text, exitCode) {
18950
+ if (jsonOnly)
18951
+ queued.push(text);
18952
+ else
18953
+ writeSync(2, text);
18954
+ process.exitCode = exitCode;
18955
+ },
18818
18956
  agentContext(text) {
18819
18957
  forAgent.push(text);
18820
18958
  },
@@ -18824,11 +18962,23 @@ function hookReply(event) {
18824
18962
  },
18825
18963
  flush() {
18826
18964
  const text = queued.splice(0).join("\n");
18965
+ const person = forPerson.splice(0).join("\n");
18827
18966
  const context = event === "PostToolUse" ? forAgent.splice(0).join("\n") : "";
18828
18967
  const code = process.exitCode;
18829
18968
  if (code !== void 0 && code !== null && Number(code) !== 0) {
18830
- if (text !== "")
18969
+ if (text === "")
18970
+ return;
18971
+ if (jsonOnly)
18972
+ process.stdout.write(`${jsonOnlyLoudReply(event, text)}
18973
+ `);
18974
+ else
18831
18975
  writeSync(2, `${text}
18976
+ `);
18977
+ return;
18978
+ }
18979
+ if (jsonOnly && event === "PostToolUse" && person !== "") {
18980
+ process.stdout.write(`${postToolUsePersonReply(person, context === "" ? text : `${text}
18981
+ ${context}`)}
18832
18982
  `);
18833
18983
  return;
18834
18984
  }
@@ -18844,6 +18994,60 @@ function hookReply(event) {
18844
18994
  }
18845
18995
  };
18846
18996
  }
18997
+ function repliesInJsonOnly(profile) {
18998
+ return profile.id === "codex";
18999
+ }
19000
+ function hookExitCode(profile) {
19001
+ if (repliesInJsonOnly(profile))
19002
+ return 0;
19003
+ const code = process.exitCode;
19004
+ return code === void 0 || code === null ? 0 : Number(code);
19005
+ }
19006
+ function jsonOnlyLoudReply(event, text) {
19007
+ return event === "PostToolUse" ? postToolUsePersonReply(text, text) : nonBlockingReply(event, text);
19008
+ }
19009
+ function postToolUsePersonReply(person, agent) {
19010
+ return JSON.stringify({
19011
+ systemMessage: person,
19012
+ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: agent }
19013
+ });
19014
+ }
19015
+ function lastResortReply(argv, text) {
19016
+ const [cmd, sub] = argv;
19017
+ let event = null;
19018
+ let rest = [];
19019
+ if (cmd === "refresh-credential") {
19020
+ event = "SessionStart";
19021
+ rest = argv.slice(1);
19022
+ } else if (cmd === "hook" && sub !== void 0 && Object.hasOwn(LAST_RESORT_HOOK_EVENTS, sub)) {
19023
+ event = LAST_RESORT_HOOK_EVENTS[sub] ?? null;
19024
+ rest = argv.slice(2);
19025
+ }
19026
+ if (event === null)
19027
+ return null;
19028
+ const profile = agentFromArguments(rest);
19029
+ if (profile === null || !repliesInJsonOnly(profile))
19030
+ return null;
19031
+ return jsonOnlyLoudReply(event, text);
19032
+ }
19033
+ function answerUnexpectedError(argv, err, out) {
19034
+ const text = `halfcycle-runner: unexpected error: ${err instanceof Error ? err.message : String(err)}
19035
+ Contact hello@halfcycle.ai if this keeps happening.
19036
+ `;
19037
+ const reply = lastResortReply(argv, text);
19038
+ if (reply !== null) {
19039
+ out.stdout(`${reply}
19040
+ `);
19041
+ return 0;
19042
+ }
19043
+ out.stderr(text);
19044
+ return 1;
19045
+ }
19046
+ var LAST_RESORT_HOOK_EVENTS = {
19047
+ "post-tool-use": "PostToolUse",
19048
+ stop: "Stop",
19049
+ "subagent-stop": "SubagentStop"
19050
+ };
18847
19051
  function postToolUseReply(notice, context) {
18848
19052
  const additionalContext = notice === "" ? context : `${notice}
18849
19053
  ${context}`;
@@ -18855,6 +19059,9 @@ function userPromptContextReply(context) {
18855
19059
  function infraWarningText() {
18856
19060
  return "[Halfcycle] WARNING: Halfcycle's check service could not be reached, so nothing was checked this time. The session continues without checks.";
18857
19061
  }
19062
+ function unreadableEditText() {
19063
+ return "[Halfcycle] WARNING: The files this change touched could not be read, so this change was not checked. The session continues.";
19064
+ }
18858
19065
  function localFaultText(message) {
18859
19066
  return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
18860
19067
  }
@@ -18867,8 +19074,13 @@ This is not a service outage: the guard service answered. The suspect is this re
18867
19074
  }
18868
19075
  var UNCONFIGURED_EXIT_TOOL_USE = 2;
18869
19076
  var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
18870
- function emitNeverConfigured(exitCode, reason, engagementId) {
18871
- writeSync(2, neverConfiguredReport(reason, void 0, engagementId));
19077
+ function emitNeverConfigured(exitCode, reason, engagementId, reply) {
19078
+ const text = neverConfiguredReport(reason, void 0, engagementId);
19079
+ if (reply !== void 0) {
19080
+ reply.loud(text, exitCode);
19081
+ return;
19082
+ }
19083
+ writeSync(2, text);
18872
19084
  process.exitCode = exitCode;
18873
19085
  }
18874
19086
  function namesTheSignInCommand(text) {
@@ -18947,8 +19159,13 @@ This is not a service outage. The guard service is up and replying; ${cause}, so
18947
19159
  ${remedy}
18948
19160
  `;
18949
19161
  }
18950
- function emitCredentialRejected(statusCode, serviceMessage, exitCode, remedy = HOOK_REMEDY) {
18951
- writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, remedy));
19162
+ function emitCredentialRejected(statusCode, serviceMessage, exitCode, remedy = HOOK_REMEDY, reply) {
19163
+ const text = credentialRejectedMessage(statusCode, serviceMessage, remedy);
19164
+ if (reply !== void 0) {
19165
+ reply.loud(text, exitCode);
19166
+ return;
19167
+ }
19168
+ writeSync(2, text);
18952
19169
  process.exitCode = exitCode;
18953
19170
  }
18954
19171
  function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
@@ -21168,6 +21385,18 @@ function findingsPath(sessionId) {
21168
21385
  return null;
21169
21386
  return join6(markerDir(), `${FINDINGS_PREFIX}${sessionId}${FINDINGS_SUFFIX}`);
21170
21387
  }
21388
+ var HELD_STEP_PREFIX = "halfcycle-held-";
21389
+ var ALLOW_PREFIX = "halfcycle-allow-";
21390
+ var ALLOW_ONCE_SUFFIX = ".json";
21391
+ function allowOncePaths(sessionId, dir) {
21392
+ if (typeof sessionId !== "string" || !FINDINGS_ID.test(sessionId))
21393
+ return null;
21394
+ const base = dir ?? markerDir();
21395
+ return {
21396
+ held: join6(base, `${HELD_STEP_PREFIX}${sessionId}${ALLOW_ONCE_SUFFIX}`),
21397
+ allow: join6(base, `${ALLOW_PREFIX}${sessionId}${ALLOW_ONCE_SUFFIX}`)
21398
+ };
21399
+ }
21171
21400
  function markerDir() {
21172
21401
  const raw = process.env["TMPDIR"] || tmpdir();
21173
21402
  return raw.replace(/\/+$/, "");
@@ -21324,7 +21553,9 @@ function sweepSessionFiles(keepSessionId, now) {
21324
21553
  }
21325
21554
  const kinds = [
21326
21555
  [FINDINGS_PREFIX, FINDINGS_SUFFIX],
21327
- [OWN_WORK_PREFIX, OWN_WORK_SUFFIX]
21556
+ [OWN_WORK_PREFIX, OWN_WORK_SUFFIX],
21557
+ [HELD_STEP_PREFIX, ALLOW_ONCE_SUFFIX],
21558
+ [ALLOW_PREFIX, ALLOW_ONCE_SUFFIX]
21328
21559
  ];
21329
21560
  for (const name of names) {
21330
21561
  for (const [prefix, suffix] of kinds) {
@@ -21428,9 +21659,9 @@ function removeLivenessIndexEntry(dir, sessionId, name) {
21428
21659
  import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync2 } from "node:fs";
21429
21660
  import { homedir as homedir3 } from "node:os";
21430
21661
  import { join as join7, relative as relative3, resolve as resolve2 } from "node:path";
21431
- function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
21662
+ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration, profile = AGENT_PROFILES["claude-code"]) {
21432
21663
  try {
21433
- const facts = read(repoRoot);
21664
+ const facts = read(repoRoot, profile);
21434
21665
  if (facts === void 0)
21435
21666
  return void 0;
21436
21667
  const parsed = seatConfigurationSchema.safeParse(facts);
@@ -21439,8 +21670,8 @@ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
21439
21670
  return void 0;
21440
21671
  }
21441
21672
  }
21442
- function readSeatConfiguration(repoRoot) {
21443
- const given = resolve2(process.env["CLAUDE_PROJECT_DIR"] || process.cwd());
21673
+ function readSeatConfiguration(repoRoot, profile = AGENT_PROFILES["claude-code"]) {
21674
+ const given = profile.projectRoot === "git-toplevel" ? resolve2(repoRoot) : resolve2(process.env["CLAUDE_PROJECT_DIR"] || process.cwd());
21444
21675
  const projectDir = realpathSync2.native(given);
21445
21676
  const home = homedir3();
21446
21677
  let realHome = home;
@@ -21452,14 +21683,15 @@ function readSeatConfiguration(repoRoot) {
21452
21683
  if (projectDir === home || given === home || projectDir === realHome)
21453
21684
  return void 0;
21454
21685
  return {
21455
- settingsFiles: [readSettingsFile(projectDir, SETTINGS_REL), readSettingsFile(projectDir, LOCAL_SETTINGS_REL)],
21456
- paths: readPathStates(projectDir, repoRoot)
21686
+ settingsFiles: profile.settingsFiles.map((rel) => readSettingsFile(projectDir, rel, profile.commands)),
21687
+ paths: readPathStates(projectDir, repoRoot, profile.wiringPaths),
21688
+ agent: profile.id
21457
21689
  };
21458
21690
  }
21459
21691
  function isPlainObject3(value) {
21460
21692
  return typeof value === "object" && value !== null && !Array.isArray(value);
21461
21693
  }
21462
- function readSettingsFile(projectDir, rel) {
21694
+ function readSettingsFile(projectDir, rel, ours) {
21463
21695
  let raw;
21464
21696
  try {
21465
21697
  raw = readFileSync8(join7(projectDir, rel), "utf-8");
@@ -21476,9 +21708,9 @@ function readSettingsFile(projectDir, rel) {
21476
21708
  }
21477
21709
  if (!isPlainObject3(parsed))
21478
21710
  return { path: rel, state: "unreadable", hooks: [] };
21479
- return { path: rel, state: "read", hooks: ourHooks(parsed["hooks"]) };
21711
+ return { path: rel, state: "read", hooks: ourHooks(parsed["hooks"], ours) };
21480
21712
  }
21481
- function ourHooks(hooks) {
21713
+ function ourHooks(hooks, ours) {
21482
21714
  if (!isPlainObject3(hooks))
21483
21715
  return [];
21484
21716
  const rows = [];
@@ -21502,7 +21734,7 @@ function ourHooks(hooks) {
21502
21734
  if (!isPlainObject3(hook))
21503
21735
  continue;
21504
21736
  const command = hook["command"];
21505
- if (typeof command !== "string" || !HALFCYCLE_HOOK_COMMANDS.has(command))
21737
+ if (typeof command !== "string" || !ours.has(command))
21506
21738
  continue;
21507
21739
  let timeout = null;
21508
21740
  if (Object.hasOwn(hook, "timeout")) {
@@ -21521,8 +21753,8 @@ function ourHooks(hooks) {
21521
21753
  }
21522
21754
  return rows;
21523
21755
  }
21524
- function readPathStates(projectDir, repoRoot) {
21525
- const unknown2 = () => HALFCYCLE_HOOK_WIRING_PATHS.map((path) => ({ path, state: "unknown" }));
21756
+ function readPathStates(projectDir, repoRoot, wiringPaths) {
21757
+ const unknown2 = () => wiringPaths.map((path) => ({ path, state: "unknown" }));
21526
21758
  let projectTop = projectDir;
21527
21759
  if (repoRoot !== projectDir) {
21528
21760
  const top = getRepoRoot(projectDir);
@@ -21531,10 +21763,10 @@ function readPathStates(projectDir, repoRoot) {
21531
21763
  projectTop = top.value;
21532
21764
  }
21533
21765
  const prefix = relative3(projectTop, projectDir);
21534
- const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
21766
+ const status = getWiringStatus(wiringPaths, projectDir);
21535
21767
  if (!status.ok)
21536
21768
  return unknown2();
21537
- const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync5(join7(projectDir, rel)));
21769
+ const mapped = mapWiringStatus(status.value, prefix, wiringPaths, (rel) => existsSync5(join7(projectDir, rel)));
21538
21770
  return mapped ?? unknown2();
21539
21771
  }
21540
21772
  function mapWiringStatus(output, prefix, paths, onDisk) {
@@ -22253,8 +22485,8 @@ function separatorOf(path) {
22253
22485
  function subagentsFolderOf(transcript) {
22254
22486
  if (!transcript.endsWith(".jsonl") || transcript.length === ".jsonl".length)
22255
22487
  return null;
22256
- const sep5 = separatorOf(transcript);
22257
- return `${transcript.slice(0, -".jsonl".length)}${sep5}subagents`;
22488
+ const sep6 = separatorOf(transcript);
22489
+ return `${transcript.slice(0, -".jsonl".length)}${sep6}subagents`;
22258
22490
  }
22259
22491
  function subagentRecordName(agentId) {
22260
22492
  return /^[A-Za-z0-9_-]+$/.test(agentId) ? `agent-${agentId}.jsonl` : null;
@@ -22794,7 +23026,7 @@ async function pruneTimeLog(path, openedAt) {
22794
23026
  written = true;
22795
23027
  let seen = Buffer.byteLength(text, "utf-8");
22796
23028
  for (; ; ) {
22797
- await new Promise((resolve8) => setTimeout(resolve8, PRUNE_SETTLE_MS));
23029
+ await new Promise((resolve10) => setTimeout(resolve10, PRUNE_SETTLE_MS));
22798
23030
  const after = readFileSync11(aside);
22799
23031
  if (after.length <= seen)
22800
23032
  break;
@@ -23429,344 +23661,109 @@ function turnEndMessage(news) {
23429
23661
  return parts.length === 0 ? null : parts.join("\n");
23430
23662
  }
23431
23663
 
23432
- // dist/hooks/post-tool-use.js
23433
- var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
23434
- async function runPostToolUse(readSeat = readSeatConfiguration) {
23435
- const reply = hookReply("PostToolUse");
23436
- const at = { session: void 0, folder: null };
23437
- await evaluateEdit(reply, readSeat, at);
23438
- handOverAtEdit(reply, at);
23439
- reply.flush();
23440
- }
23441
- function handOverAtEdit(reply, at) {
23442
- if (at.folder === null)
23443
- return;
23444
- const code = process.exitCode;
23445
- if (code !== void 0 && code !== null && Number(code) !== 0)
23446
- return;
23447
- const given = deliverFindings(at.session, at.folder);
23448
- if (given.length === 0)
23449
- return;
23450
- reply.agentContext(detailsBlock(given, realPathOf(projectFolder())));
23451
- }
23452
- async function evaluateEdit(reply, readSeat, at) {
23453
- let input;
23454
- try {
23455
- const raw = await readStdin();
23456
- input = JSON.parse(raw);
23457
- } catch {
23458
- recordTimeEvent("edit", {});
23459
- return;
23460
- }
23461
- recordTimeEvent("edit", input);
23462
- const toolName = input.tool_name ?? "";
23463
- if (!HANDLED_EDIT_TOOLS.has(toolName))
23464
- return;
23465
- const isNotebook = toolName === "NotebookEdit";
23466
- const absFilePath = isNotebook ? input.tool_input?.notebook_path : input.tool_input?.file_path;
23467
- if (!absFilePath || typeof absFilePath !== "string")
23468
- return;
23469
- recordOwnEdit(input, absFilePath);
23470
- const repoRootResult = getRepoRoot(process.cwd());
23471
- if (!repoRootResult.ok) {
23472
- if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
23473
- reply.notice(noRepositoryText(repoRootResult));
23664
+ // dist/patch.js
23665
+ var PATCH_ADD_HEADER = "*** Add File: ";
23666
+ var PATCH_UPDATE_HEADER = "*** Update File: ";
23667
+ var PATCH_DELETE_HEADER = "*** Delete File: ";
23668
+ var PATCH_MOVE_HEADER = "*** Move to: ";
23669
+ function patchOperations(patch) {
23670
+ const ops = [];
23671
+ let addLines = null;
23672
+ let hunks = null;
23673
+ let lastWasUpdate = false;
23674
+ const hunk = () => {
23675
+ if (hunks.length === 0)
23676
+ hunks.push({ before: [], after: [] });
23677
+ return hunks[hunks.length - 1];
23678
+ };
23679
+ for (const raw of patch.split("\n")) {
23680
+ const line2 = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
23681
+ if (addLines !== null && line2.startsWith("+")) {
23682
+ lastWasUpdate = false;
23683
+ addLines.push(line2.slice(1));
23684
+ continue;
23474
23685
  }
23475
- return;
23476
- }
23477
- const repoRoot = repoRootResult.value;
23478
- at.session = input.session_id;
23479
- at.folder = realPathOf(repoRoot);
23480
- const diffContent = isNotebook ? "" : getSingleFileDiff(absFilePath, repoRoot);
23481
- const { relative: pathRelative } = await import("node:path");
23482
- const relPath = pathRelative(repoRoot, absFilePath);
23483
- if (isInstallerOutput(relPath))
23484
- return;
23485
- const context = loadChangeSetContext(repoRoot);
23486
- const changeSet = buildChangeSet([{ path: relPath, diffContent }], {
23487
- ...context,
23488
- excerpts: readExcerptConfig()
23489
- });
23490
- const configResult = readConfig();
23491
- if (!configResult.config) {
23492
- await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
23493
- if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
23494
- return;
23495
- emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
23496
- return;
23497
- }
23498
- const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
23499
- const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
23500
- const evaluation = await evaluateWithRefresh({
23501
- guardServiceUrl,
23502
- token: guardServiceToken,
23503
- engagementId: guardEngagementId,
23504
- request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
23505
- warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
23506
- });
23507
- const clientResult = evaluation.result;
23508
- if (!clientResult.ok) {
23509
- const failure = classifyFailure(clientResult);
23510
- await emitRunRecord({
23511
- engagementId: guardEngagementId,
23512
- runType: "hook",
23513
- outcome: outcomeForClientFailure(clientResult.kind),
23514
- failureReason: failure.reason,
23515
- changeSet,
23516
- credential: evaluation.token,
23517
- credentialRefused: failure.kind === "credential"
23518
- });
23519
- if (failure.kind === "credential") {
23520
- if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
23521
- return;
23522
- emitCredentialRejected(
23523
- failure.statusCode,
23524
- failure.message,
23525
- CREDENTIAL_REJECTED_EXIT_TOOL_USE,
23526
- // The remedy the renewal attempt earned: the sign-in when the account
23527
- // credential is the thing that was refused, and otherwise the plane's own
23528
- // sentence about why it would not mint.
23529
- evaluation.remedy
23530
- );
23531
- return;
23686
+ if (hunks !== null && isUpdateLine(line2)) {
23687
+ lastWasUpdate = false;
23688
+ if (line2.startsWith("@@")) {
23689
+ hunks.push({ before: [], after: [] });
23690
+ continue;
23691
+ }
23692
+ const mark = line2.charAt(0);
23693
+ const text = line2.slice(1);
23694
+ if (mark === "-")
23695
+ hunk().before.push(text);
23696
+ else if (mark === "+")
23697
+ hunk().after.push(text);
23698
+ else {
23699
+ hunk().before.push(text);
23700
+ hunk().after.push(text);
23701
+ }
23702
+ continue;
23532
23703
  }
23533
- if (failure.kind === "contract") {
23534
- reply.notice(contractFaultText(failure.message));
23535
- return;
23704
+ const trimmed = line2.trim();
23705
+ const move = headerPath(trimmed, PATCH_MOVE_HEADER);
23706
+ if (move !== void 0) {
23707
+ const last = ops[ops.length - 1];
23708
+ if (!lastWasUpdate || move === "" || last === void 0 || last.kind !== "update")
23709
+ return null;
23710
+ ops[ops.length - 1] = { ...last, movedTo: move };
23711
+ lastWasUpdate = false;
23712
+ continue;
23536
23713
  }
23537
- reply.notice(infraWarningText());
23538
- return;
23714
+ lastWasUpdate = false;
23715
+ const header = [PATCH_ADD_HEADER, PATCH_UPDATE_HEADER, PATCH_DELETE_HEADER].find((h) => headerPath(trimmed, h) !== void 0);
23716
+ if (header === void 0)
23717
+ continue;
23718
+ const path = headerPath(trimmed, header);
23719
+ if (path === "")
23720
+ return null;
23721
+ addLines = null;
23722
+ hunks = null;
23723
+ if (header === PATCH_ADD_HEADER) {
23724
+ addLines = [];
23725
+ ops.push({ kind: "add", path, lines: addLines });
23726
+ } else if (header === PATCH_UPDATE_HEADER) {
23727
+ hunks = [];
23728
+ ops.push({ kind: "update", path, movedTo: null, hunks });
23729
+ } else {
23730
+ ops.push({ kind: "delete", path });
23731
+ }
23732
+ lastWasUpdate = header === PATCH_UPDATE_HEADER;
23539
23733
  }
23540
- const telemetryConfig = readTelemetryConfig();
23541
- const run = buildGuardEvalRun({
23542
- engagementId: guardEngagementId,
23543
- runType: "hook",
23544
- // The phase this run belongs to, as the client named it — it names the
23545
- // guard-eval log file the run is appended to. Absent → the run is unphased.
23546
- phase: telemetryConfig.phase,
23547
- outcome: "evaluated",
23548
- changeSet,
23549
- result: clientResult.envelope
23550
- });
23551
- await emitTelemetry(run, {
23552
- controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
23553
- // THE CREDENTIAL IN FORCE, NOT THE ONE IN THE ENVIRONMENT (T-08,
23554
- // credential-lifecycle). `readTelemetryConfig` reads the bearer off
23555
- // `process.env` after the evaluation, and a renewal does not write there — so
23556
- // taking its value on a run that just recovered would present the credential
23557
- // the plane had already refused and drop the record with a 401, silently,
23558
- // because telemetry is best-effort.
23559
- controlTelemetryToken: evaluation.token,
23560
- // Per-engagement log dir from HALFCYCLE_GUARD_EVAL_LOG_DIR (E1-T-02, #61).
23561
- guardEvalLogDir: telemetryConfig.guardEvalLogDir
23562
- }, {
23563
- warn: (msg) => process.stderr.write(msg + "\n")
23564
- });
23565
- actOnEnvelope2(clientResult.envelope, reply);
23734
+ return ops;
23566
23735
  }
23567
- function actOnEnvelope2(envelope, reply) {
23568
- const notRun = notRunReport(envelope.notRun);
23569
- const notRunBlock = notRun ? `
23570
- ${notRun}` : "";
23571
- if (envelope.guardsFired.length === 0) {
23572
- if (notRun)
23573
- reply.notice(notRun);
23574
- return;
23575
- }
23576
- if (envelope.blocking) {
23577
- const personNotRun = notRunMessage((envelope.notRun ?? []).map((n) => n.reason));
23578
- const reason = `${blockReason(envelope.guardsFired.map((g) => g.explanation))}${personNotRun ? `
23579
- ${personNotRun}` : ""}`;
23580
- reply.block(reason, stoppedMessage(blockingExplanations(envelope.guardsFired)));
23581
- } else {
23582
- const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
23583
- reply.notice(`[Halfcycle] Guard warning: ${envelope.explanation}
23584
- ${guardLines}${notRunBlock}`);
23585
- }
23736
+ function isUpdateLine(line2) {
23737
+ return line2 === "" || line2.startsWith(" ") || line2.startsWith("+") || line2.startsWith("-") || line2.startsWith("@@");
23586
23738
  }
23587
- function readStdin() {
23588
- return new Promise((resolve8, reject) => {
23589
- const chunks = [];
23590
- process.stdin.on("data", (chunk) => chunks.push(chunk));
23591
- process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
23592
- process.stdin.on("error", reject);
23593
- });
23739
+ function headerPath(trimmed, header) {
23740
+ if (trimmed === header.trimEnd())
23741
+ return "";
23742
+ return trimmed.startsWith(header) ? trimmed.slice(header.length).trim() : void 0;
23594
23743
  }
23595
-
23596
- // dist/liveness.js
23597
- import { closeSync as closeSync2, mkdtempSync, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync5, rmSync, statSync as statSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
23598
- import { join as join11 } from "node:path";
23599
- var LIVE_WINDOW_MS = 10 * 60 * 1e3;
23600
- function field(input, key) {
23601
- if (typeof input !== "object" || input === null)
23744
+ function patchHeaderPaths(patch) {
23745
+ const ops = patchOperations(patch);
23746
+ if (ops === null)
23602
23747
  return null;
23603
- const v = input[key];
23604
- return typeof v === "string" && v !== "" ? v : null;
23748
+ return ops.map((op) => op.kind === "update" && op.movedTo !== null ? op.movedTo : op.path);
23605
23749
  }
23606
- function indexEntry(dir, sessionId, name) {
23607
- const folder = join11(dir, livenessIndexDirName(sessionId));
23608
- for (let attempt2 = 0; attempt2 < 3; attempt2++) {
23609
- try {
23610
- closeSync2(openSync2(join11(folder, name), "a", 384));
23611
- return true;
23612
- } catch (err) {
23613
- if (err.code !== "ENOENT")
23614
- return false;
23615
- }
23616
- let tmp = null;
23617
- try {
23618
- tmp = mkdtempSync(`${folder}.`);
23619
- for (const existing of readdirSync5(dir)) {
23620
- if (parseLivenessFileName(existing)?.sessionId === sessionId)
23621
- closeSync2(openSync2(join11(tmp, existing), "a", 384));
23622
- }
23623
- closeSync2(openSync2(join11(tmp, name), "a", 384));
23624
- renameSync5(tmp, folder);
23625
- return true;
23626
- } catch {
23627
- if (tmp !== null)
23628
- rmSync(tmp, { recursive: true, force: true });
23629
- }
23630
- }
23631
- return false;
23750
+
23751
+ // dist/pacing.js
23752
+ import { createHash as createHash4 } from "node:crypto";
23753
+ import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync14, readSync, realpathSync as realpathSync5, renameSync as renameSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "node:fs";
23754
+ import { homedir as homedir5 } from "node:os";
23755
+ import nodePath from "node:path";
23756
+ var DEFAULT_CONFIG_FOLDER_NAME = ".claude";
23757
+ var CODEX_DEFAULT_FOLDER_NAME = ".codex";
23758
+ var RECORDS_FOLDER_NAME = "projects";
23759
+ function hookInputForRule(input, rule) {
23760
+ if (rule === "claude-config" || typeof input !== "object" || input === null || Array.isArray(input))
23761
+ return input;
23762
+ const copy = { ...input };
23763
+ delete copy["transcript_path"];
23764
+ delete copy["agent_transcript_path"];
23765
+ return copy;
23632
23766
  }
23633
- function recordLiveness(input, deps = {}) {
23634
- try {
23635
- const sessionId = field(input, "session_id");
23636
- const agentId = field(input, "agent_id");
23637
- const cwd = field(input, "cwd");
23638
- if (sessionId === null || cwd === null || !livenessIdOk(sessionId))
23639
- return null;
23640
- if (agentId !== null && !livenessIdOk(agentId))
23641
- return null;
23642
- if (/[\r\n]/.test(cwd))
23643
- return null;
23644
- const copy = whichCopy(cwd, deps.walk);
23645
- const dir = deps.dir ?? markerDir();
23646
- const name = livenessFileName(livenessKey(copy?.common ?? null), sessionId, agentId);
23647
- if (!indexEntry(dir, sessionId, name))
23648
- return null;
23649
- const path = join11(dir, name);
23650
- const text = `${cwd}
23651
- ${copy?.top ?? ""}
23652
- `;
23653
- try {
23654
- writeFileSync7(path, text, { encoding: "utf-8", flag: "wx", mode: 384 });
23655
- return { firstSeen: true };
23656
- } catch (err) {
23657
- if (err.code !== "EEXIST")
23658
- return null;
23659
- }
23660
- writeFileSync7(path, text, { encoding: "utf-8", mode: 384 });
23661
- return { firstSeen: false };
23662
- } catch {
23663
- return null;
23664
- }
23665
- }
23666
- function clearLiveness(input, deps = {}) {
23667
- try {
23668
- const sessionId = field(input, "session_id");
23669
- const agentId = field(input, "agent_id");
23670
- if (sessionId === null || agentId === null)
23671
- return 0;
23672
- const dir = deps.dir ?? markerDir();
23673
- let removed = 0;
23674
- for (const name of readdirSync5(dir)) {
23675
- const p = parseLivenessFileName(name);
23676
- if (p === null || p.sessionId !== sessionId || p.agentId !== agentId)
23677
- continue;
23678
- try {
23679
- unlinkSync6(join11(dir, name));
23680
- removed += 1;
23681
- } catch {
23682
- }
23683
- removeLivenessIndexEntry(dir, sessionId, name);
23684
- }
23685
- return removed;
23686
- } catch {
23687
- return 0;
23688
- }
23689
- }
23690
- function topOf(path) {
23691
- try {
23692
- const top = readFileSync14(path, "utf-8").split("\n")[1] ?? "";
23693
- return top === "" ? null : top;
23694
- } catch {
23695
- return null;
23696
- }
23697
- }
23698
- function readLiveness(sessionId, cwd, deps = {}) {
23699
- try {
23700
- if (!sessionId || !cwd || !livenessIdOk(sessionId))
23701
- return null;
23702
- const copy = whichCopy(cwd, deps.walk);
23703
- if (copy === null)
23704
- return null;
23705
- const key = livenessKey(copy.common);
23706
- const dir = deps.dir ?? markerDir();
23707
- const now = (deps.now ?? Date.now)();
23708
- let main2 = null;
23709
- const agents = [];
23710
- for (const name of readdirSync5(dir)) {
23711
- const p = parseLivenessFileName(name);
23712
- if (p === null || p.key !== key || p.sessionId !== sessionId)
23713
- continue;
23714
- const full = join11(dir, name);
23715
- let seenAt;
23716
- try {
23717
- seenAt = statSync4(full).mtimeMs;
23718
- } catch {
23719
- continue;
23720
- }
23721
- const top = topOf(full);
23722
- if (top === null)
23723
- continue;
23724
- if (p.agentId === null)
23725
- main2 = { top, seenAt };
23726
- else if (now - seenAt <= LIVE_WINDOW_MS)
23727
- agents.push({ agentId: p.agentId, top, seenAt });
23728
- }
23729
- agents.sort((a, b) => a.agentId.localeCompare(b.agentId));
23730
- return { main: main2, agents };
23731
- } catch {
23732
- return null;
23733
- }
23734
- }
23735
- function liveCount(record2) {
23736
- return record2 === null ? 0 : record2.agents.length;
23737
- }
23738
- function withoutAgent(record2, agentId) {
23739
- if (record2 === null || agentId === null || agentId === void 0 || agentId === "")
23740
- return record2;
23741
- return { main: record2.main, agents: record2.agents.filter((a) => a.agentId !== agentId) };
23742
- }
23743
- function runsBesideOtherWork(toolInput, record2) {
23744
- const t = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
23745
- const bg = t["run_in_background"];
23746
- if (bg !== false && bg !== "false")
23747
- return true;
23748
- const name = t["name"];
23749
- if (name !== void 0 && name !== null && name !== "")
23750
- return true;
23751
- return liveCount(record2) > 0;
23752
- }
23753
-
23754
- // dist/pace-hook.js
23755
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync18, renameSync as renameSync9, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
23756
- import nodePath4 from "node:path";
23757
-
23758
- // dist/pace-verb.js
23759
- import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync8, statSync as statSync6, writeFileSync as writeFileSync10 } from "node:fs";
23760
- import { homedir as homedir6 } from "node:os";
23761
- import nodePath3 from "node:path";
23762
-
23763
- // dist/pacing.js
23764
- import { createHash as createHash4 } from "node:crypto";
23765
- import { closeSync as closeSync3, mkdirSync as mkdirSync4, openSync as openSync3, readdirSync as readdirSync6, readFileSync as readFileSync15, readSync, realpathSync as realpathSync5, renameSync as renameSync6, statSync as statSync5, writeFileSync as writeFileSync8 } from "node:fs";
23766
- import { homedir as homedir5 } from "node:os";
23767
- import nodePath from "node:path";
23768
- var DEFAULT_CONFIG_FOLDER_NAME = ".claude";
23769
- var RECORDS_FOLDER_NAME = "projects";
23770
23767
  function normaliseFolder(folder, input = {}) {
23771
23768
  const p = input.path ?? nodePath;
23772
23769
  const home = input.home ?? homedir5();
@@ -23780,6 +23777,12 @@ function normaliseFolder(folder, input = {}) {
23780
23777
  function configFolderKey(input = {}) {
23781
23778
  const p = input.path ?? nodePath;
23782
23779
  const env = input.env ?? process.env;
23780
+ if (input.rule === "codex-home") {
23781
+ const codexHome = env["CODEX_HOME"];
23782
+ if (typeof codexHome === "string" && codexHome !== "")
23783
+ return normaliseFolder(codexHome, input);
23784
+ return normaliseFolder(p.join(input.home ?? homedir5(), CODEX_DEFAULT_FOLDER_NAME), input);
23785
+ }
23783
23786
  const fromEnv = env["CLAUDE_CONFIG_DIR"];
23784
23787
  if (typeof fromEnv === "string" && fromEnv !== "")
23785
23788
  return normaliseFolder(fromEnv, input);
@@ -23811,7 +23814,7 @@ function pacingDir(home) {
23811
23814
  }
23812
23815
  function readJsonObject(path) {
23813
23816
  try {
23814
- const v = JSON.parse(readFileSync15(path, "utf-8"));
23817
+ const v = JSON.parse(readFileSync14(path, "utf-8"));
23815
23818
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
23816
23819
  } catch {
23817
23820
  return null;
@@ -23820,8 +23823,8 @@ function readJsonObject(path) {
23820
23823
  function writeJsonWhole(path, value) {
23821
23824
  mkdirSync4(nodePath.dirname(path), { recursive: true, mode: 448 });
23822
23825
  const tmp = `${path}.${String(process.pid)}.${createHash4("sha1").update(String(Math.random())).digest("hex").slice(0, 8)}.tmp`;
23823
- writeFileSync8(tmp, JSON.stringify(value), { mode: 384 });
23824
- renameSync6(tmp, path);
23826
+ writeFileSync7(tmp, JSON.stringify(value), { mode: 384 });
23827
+ renameSync5(tmp, path);
23825
23828
  }
23826
23829
  function writeAccountEntry(path, key, entry) {
23827
23830
  const current = readJsonObject(path);
@@ -23978,7 +23981,7 @@ function readLearnedAccount(key, deps = {}) {
23978
23981
  }
23979
23982
  function dirents(path) {
23980
23983
  try {
23981
- return readdirSync6(path, { withFileTypes: true });
23984
+ return readdirSync5(path, { withFileTypes: true });
23982
23985
  } catch {
23983
23986
  return [];
23984
23987
  }
@@ -24012,7 +24015,7 @@ var NEWLINE = 10;
24012
24015
  var ASSISTANT = Buffer.from('"assistant"');
24013
24016
  var READ_CHUNK = 8 * 1024 * 1024;
24014
24017
  function readWholeLines(path, from, size, onLine) {
24015
- const fd = openSync3(path, "r");
24018
+ const fd = openSync2(path, "r");
24016
24019
  try {
24017
24020
  let pos = from;
24018
24021
  let carry = Buffer.alloc(0);
@@ -24040,7 +24043,7 @@ function readWholeLines(path, from, size, onLine) {
24040
24043
  }
24041
24044
  return consumed;
24042
24045
  } finally {
24043
- closeSync3(fd);
24046
+ closeSync2(fd);
24044
24047
  }
24045
24048
  }
24046
24049
  function hashId(id) {
@@ -24069,7 +24072,7 @@ function scanRecords(key, deps = {}) {
24069
24072
  let size;
24070
24073
  let mtimeSec;
24071
24074
  try {
24072
- const st = statSync5(file2.path);
24075
+ const st = statSync4(file2.path);
24073
24076
  size = st.size;
24074
24077
  mtimeSec = Math.floor(st.mtimeMs / 1e3);
24075
24078
  } catch {
@@ -24205,7 +24208,7 @@ function timeLogTranscripts(home) {
24205
24208
  continue;
24206
24209
  let text;
24207
24210
  try {
24208
- text = readFileSync15(nodePath.join(engagements, e.name, TIME_LOG_DIR_NAME, TIME_LOG_FILENAME), "utf-8");
24211
+ text = readFileSync14(nodePath.join(engagements, e.name, TIME_LOG_DIR_NAME, TIME_LOG_FILENAME), "utf-8");
24209
24212
  } catch {
24210
24213
  continue;
24211
24214
  }
@@ -24350,19 +24353,405 @@ function paceOf(input) {
24350
24353
  } else {
24351
24354
  continue;
24352
24355
  }
24353
- if (PACE_RANK[d.pace] > PACE_RANK[best.pace] || d.pace === best.pace && d.allowance < best.allowance)
24354
- best = d;
24356
+ if (PACE_RANK[d.pace] > PACE_RANK[best.pace] || d.pace === best.pace && d.allowance < best.allowance)
24357
+ best = d;
24358
+ }
24359
+ return best;
24360
+ }
24361
+ function knownWindowOf(learned, reported, now) {
24362
+ for (const kind of PACE_WINDOW_KINDS) {
24363
+ if (learned?.ceilings[kind] !== void 0 || reportedFresh(reported[kind], now))
24364
+ return kind;
24365
+ }
24366
+ return null;
24367
+ }
24368
+
24369
+ // dist/hooks/post-tool-use.js
24370
+ import { isAbsolute as isAbsolute2, join as join11, relative as relative5, resolve as resolve4, sep as sep3 } from "node:path";
24371
+ var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
24372
+ async function runPostToolUse(readSeat = readSeatConfiguration, profile = AGENT_PROFILES["claude-code"]) {
24373
+ const reply = hookReply("PostToolUse", profile);
24374
+ const at = { session: void 0, folder: null };
24375
+ await evaluateEdit(reply, readSeat, at, profile);
24376
+ handOverAtEdit(reply, at);
24377
+ reply.flush();
24378
+ }
24379
+ function handOverAtEdit(reply, at) {
24380
+ if (at.folder === null)
24381
+ return;
24382
+ const code = process.exitCode;
24383
+ if (code !== void 0 && code !== null && Number(code) !== 0)
24384
+ return;
24385
+ const given = deliverFindings(at.session, at.folder);
24386
+ if (given.length === 0)
24387
+ return;
24388
+ reply.agentContext(detailsBlock(given, realPathOf(projectFolder())));
24389
+ }
24390
+ async function evaluateEdit(reply, readSeat, at, profile) {
24391
+ let input;
24392
+ try {
24393
+ const raw = await readStdin();
24394
+ input = hookInputForRule(JSON.parse(raw), profile.configFolder);
24395
+ } catch {
24396
+ recordTimeEvent("edit", {});
24397
+ return;
24398
+ }
24399
+ recordTimeEvent("edit", input);
24400
+ const toolName = input.tool_name ?? "";
24401
+ const edited = editedPaths(input, profile);
24402
+ if (edited === UNREADABLE_EDIT) {
24403
+ reply.personNotice(unreadableEditText());
24404
+ return;
24405
+ }
24406
+ if (edited.length === 0)
24407
+ return;
24408
+ for (const absFilePath of edited)
24409
+ recordOwnEdit(input, absFilePath);
24410
+ const repoRootResult = getRepoRoot(process.cwd());
24411
+ if (!repoRootResult.ok) {
24412
+ if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
24413
+ reply.notice(noRepositoryText(repoRootResult));
24414
+ }
24415
+ return;
24416
+ }
24417
+ const repoRoot = repoRootResult.value;
24418
+ at.session = input.session_id;
24419
+ at.folder = realPathOf(repoRoot);
24420
+ const files = [];
24421
+ const seen = /* @__PURE__ */ new Set();
24422
+ for (const named of edited) {
24423
+ const absFilePath = profile.id === "claude-code" ? named : insideRepository(named, repoRoot);
24424
+ if (absFilePath === null)
24425
+ continue;
24426
+ const relPath = relative5(repoRoot, absFilePath);
24427
+ if (isInstallerOutput(relPath) || seen.has(relPath))
24428
+ continue;
24429
+ seen.add(relPath);
24430
+ const diffContent = toolName === "NotebookEdit" ? "" : getSingleFileDiff(absFilePath, repoRoot);
24431
+ files.push({ path: relPath, diffContent });
24432
+ }
24433
+ if (files.length === 0)
24434
+ return;
24435
+ const context = loadChangeSetContext(repoRoot);
24436
+ const changeSet = buildChangeSet(files, {
24437
+ ...context,
24438
+ excerpts: readExcerptConfig()
24439
+ });
24440
+ const configResult = readConfig();
24441
+ if (!configResult.config) {
24442
+ await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
24443
+ if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
24444
+ return;
24445
+ emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()), reply);
24446
+ return;
24447
+ }
24448
+ const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
24449
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat, profile);
24450
+ const evaluation = await evaluateWithRefresh({
24451
+ guardServiceUrl,
24452
+ token: guardServiceToken,
24453
+ engagementId: guardEngagementId,
24454
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
24455
+ warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
24456
+ });
24457
+ const clientResult = evaluation.result;
24458
+ if (!clientResult.ok) {
24459
+ const failure = classifyFailure(clientResult);
24460
+ await emitRunRecord({
24461
+ engagementId: guardEngagementId,
24462
+ runType: "hook",
24463
+ outcome: outcomeForClientFailure(clientResult.kind),
24464
+ failureReason: failure.reason,
24465
+ changeSet,
24466
+ credential: evaluation.token,
24467
+ credentialRefused: failure.kind === "credential"
24468
+ });
24469
+ if (failure.kind === "credential") {
24470
+ if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
24471
+ return;
24472
+ emitCredentialRejected(
24473
+ failure.statusCode,
24474
+ failure.message,
24475
+ CREDENTIAL_REJECTED_EXIT_TOOL_USE,
24476
+ // The remedy the renewal attempt earned: the sign-in when the account
24477
+ // credential is the thing that was refused, and otherwise the plane's own
24478
+ // sentence about why it would not mint.
24479
+ evaluation.remedy,
24480
+ reply
24481
+ );
24482
+ return;
24483
+ }
24484
+ if (failure.kind === "contract") {
24485
+ reply.notice(contractFaultText(failure.message));
24486
+ return;
24487
+ }
24488
+ reply.notice(infraWarningText());
24489
+ return;
24490
+ }
24491
+ const telemetryConfig = readTelemetryConfig();
24492
+ const run = buildGuardEvalRun({
24493
+ engagementId: guardEngagementId,
24494
+ runType: "hook",
24495
+ // The phase this run belongs to, as the client named it — it names the
24496
+ // guard-eval log file the run is appended to. Absent → the run is unphased.
24497
+ phase: telemetryConfig.phase,
24498
+ outcome: "evaluated",
24499
+ changeSet,
24500
+ result: clientResult.envelope
24501
+ });
24502
+ await emitTelemetry(run, {
24503
+ controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
24504
+ // THE CREDENTIAL IN FORCE, NOT THE ONE IN THE ENVIRONMENT (T-08,
24505
+ // credential-lifecycle). `readTelemetryConfig` reads the bearer off
24506
+ // `process.env` after the evaluation, and a renewal does not write there — so
24507
+ // taking its value on a run that just recovered would present the credential
24508
+ // the plane had already refused and drop the record with a 401, silently,
24509
+ // because telemetry is best-effort.
24510
+ controlTelemetryToken: evaluation.token,
24511
+ // Per-engagement log dir from HALFCYCLE_GUARD_EVAL_LOG_DIR (E1-T-02, #61).
24512
+ guardEvalLogDir: telemetryConfig.guardEvalLogDir
24513
+ }, {
24514
+ warn: (msg) => process.stderr.write(msg + "\n")
24515
+ });
24516
+ actOnEnvelope2(clientResult.envelope, reply);
24517
+ }
24518
+ var UNREADABLE_EDIT = /* @__PURE__ */ Symbol("unreadable-edit");
24519
+ function editedPaths(input, profile) {
24520
+ const toolName = input.tool_name ?? "";
24521
+ if (profile.id === "claude-code") {
24522
+ if (!HANDLED_EDIT_TOOLS.has(toolName))
24523
+ return [];
24524
+ const absFilePath = toolName === "NotebookEdit" ? input.tool_input?.notebook_path : input.tool_input?.file_path;
24525
+ if (!absFilePath || typeof absFilePath !== "string")
24526
+ return [];
24527
+ return [absFilePath];
24528
+ }
24529
+ if (!profile.writeCapableTools.includes(toolName))
24530
+ return [];
24531
+ const filePath = input.tool_input?.file_path;
24532
+ const command = input.tool_input?.command;
24533
+ let named;
24534
+ if (typeof filePath === "string" && filePath !== "")
24535
+ named = [filePath];
24536
+ else if (typeof command === "string")
24537
+ named = patchHeaderPaths(command);
24538
+ else
24539
+ named = null;
24540
+ if (named === null || named.length === 0)
24541
+ return UNREADABLE_EDIT;
24542
+ const cwd = typeof input.cwd === "string" && isAbsolute2(input.cwd) ? input.cwd : null;
24543
+ const out = [];
24544
+ for (const path of named) {
24545
+ if (!isAbsolute2(path) && cwd === null)
24546
+ return UNREADABLE_EDIT;
24547
+ const abs = isAbsolute2(path) ? resolve4(path) : resolve4(cwd, path);
24548
+ if (!out.includes(abs))
24549
+ out.push(abs);
24550
+ }
24551
+ return out;
24552
+ }
24553
+ function insideRepository(path, repoRoot) {
24554
+ const rel = relative5(realPathOf(repoRoot), realPathOf(path));
24555
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute2(rel))
24556
+ return null;
24557
+ return join11(repoRoot, rel);
24558
+ }
24559
+ function actOnEnvelope2(envelope, reply) {
24560
+ const notRun = notRunReport(envelope.notRun);
24561
+ const notRunBlock = notRun ? `
24562
+ ${notRun}` : "";
24563
+ if (envelope.guardsFired.length === 0) {
24564
+ if (notRun)
24565
+ reply.notice(notRun);
24566
+ return;
24567
+ }
24568
+ if (envelope.blocking) {
24569
+ const personNotRun = notRunMessage((envelope.notRun ?? []).map((n) => n.reason));
24570
+ const reason = `${blockReason(envelope.guardsFired.map((g) => g.explanation))}${personNotRun ? `
24571
+ ${personNotRun}` : ""}`;
24572
+ reply.block(reason, stoppedMessage(blockingExplanations(envelope.guardsFired)));
24573
+ } else {
24574
+ const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
24575
+ reply.notice(`[Halfcycle] Guard warning: ${envelope.explanation}
24576
+ ${guardLines}${notRunBlock}`);
24577
+ }
24578
+ }
24579
+ function readStdin() {
24580
+ return new Promise((resolve10, reject) => {
24581
+ const chunks = [];
24582
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
24583
+ process.stdin.on("end", () => resolve10(Buffer.concat(chunks).toString("utf-8")));
24584
+ process.stdin.on("error", reject);
24585
+ });
24586
+ }
24587
+
24588
+ // dist/liveness.js
24589
+ import { closeSync as closeSync3, mkdtempSync, openSync as openSync3, readdirSync as readdirSync6, readFileSync as readFileSync15, renameSync as renameSync6, rmSync, statSync as statSync5, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
24590
+ import { join as join12 } from "node:path";
24591
+ var LIVE_WINDOW_MS = 10 * 60 * 1e3;
24592
+ function field(input, key) {
24593
+ if (typeof input !== "object" || input === null)
24594
+ return null;
24595
+ const v = input[key];
24596
+ return typeof v === "string" && v !== "" ? v : null;
24597
+ }
24598
+ function indexEntry(dir, sessionId, name) {
24599
+ const folder = join12(dir, livenessIndexDirName(sessionId));
24600
+ for (let attempt2 = 0; attempt2 < 3; attempt2++) {
24601
+ try {
24602
+ closeSync3(openSync3(join12(folder, name), "a", 384));
24603
+ return true;
24604
+ } catch (err) {
24605
+ if (err.code !== "ENOENT")
24606
+ return false;
24607
+ }
24608
+ let tmp = null;
24609
+ try {
24610
+ tmp = mkdtempSync(`${folder}.`);
24611
+ for (const existing of readdirSync6(dir)) {
24612
+ if (parseLivenessFileName(existing)?.sessionId === sessionId)
24613
+ closeSync3(openSync3(join12(tmp, existing), "a", 384));
24614
+ }
24615
+ closeSync3(openSync3(join12(tmp, name), "a", 384));
24616
+ renameSync6(tmp, folder);
24617
+ return true;
24618
+ } catch {
24619
+ if (tmp !== null)
24620
+ rmSync(tmp, { recursive: true, force: true });
24621
+ }
24622
+ }
24623
+ return false;
24624
+ }
24625
+ function recordLiveness(input, deps = {}) {
24626
+ try {
24627
+ const sessionId = field(input, "session_id");
24628
+ const agentId = field(input, "agent_id");
24629
+ const cwd = field(input, "cwd");
24630
+ if (sessionId === null || cwd === null || !livenessIdOk(sessionId))
24631
+ return null;
24632
+ if (agentId !== null && !livenessIdOk(agentId))
24633
+ return null;
24634
+ if (/[\r\n]/.test(cwd))
24635
+ return null;
24636
+ const copy = whichCopy(cwd, deps.walk);
24637
+ const dir = deps.dir ?? markerDir();
24638
+ const name = livenessFileName(livenessKey(copy?.common ?? null), sessionId, agentId);
24639
+ if (!indexEntry(dir, sessionId, name))
24640
+ return null;
24641
+ const path = join12(dir, name);
24642
+ const text = `${cwd}
24643
+ ${copy?.top ?? ""}
24644
+ `;
24645
+ try {
24646
+ writeFileSync8(path, text, { encoding: "utf-8", flag: "wx", mode: 384 });
24647
+ return { firstSeen: true };
24648
+ } catch (err) {
24649
+ if (err.code !== "EEXIST")
24650
+ return null;
24651
+ }
24652
+ writeFileSync8(path, text, { encoding: "utf-8", mode: 384 });
24653
+ return { firstSeen: false };
24654
+ } catch {
24655
+ return null;
24656
+ }
24657
+ }
24658
+ function clearLiveness(input, deps = {}) {
24659
+ try {
24660
+ const sessionId = field(input, "session_id");
24661
+ const agentId = field(input, "agent_id");
24662
+ if (sessionId === null || agentId === null)
24663
+ return 0;
24664
+ const dir = deps.dir ?? markerDir();
24665
+ let removed = 0;
24666
+ for (const name of readdirSync6(dir)) {
24667
+ const p = parseLivenessFileName(name);
24668
+ if (p === null || p.sessionId !== sessionId || p.agentId !== agentId)
24669
+ continue;
24670
+ try {
24671
+ unlinkSync6(join12(dir, name));
24672
+ removed += 1;
24673
+ } catch {
24674
+ }
24675
+ removeLivenessIndexEntry(dir, sessionId, name);
24676
+ }
24677
+ return removed;
24678
+ } catch {
24679
+ return 0;
24680
+ }
24681
+ }
24682
+ function topOf(path) {
24683
+ try {
24684
+ const top = readFileSync15(path, "utf-8").split("\n")[1] ?? "";
24685
+ return top === "" ? null : top;
24686
+ } catch {
24687
+ return null;
24688
+ }
24689
+ }
24690
+ function readLiveness(sessionId, cwd, deps = {}) {
24691
+ try {
24692
+ if (!sessionId || !cwd || !livenessIdOk(sessionId))
24693
+ return null;
24694
+ const copy = whichCopy(cwd, deps.walk);
24695
+ if (copy === null)
24696
+ return null;
24697
+ const key = livenessKey(copy.common);
24698
+ const dir = deps.dir ?? markerDir();
24699
+ const now = (deps.now ?? Date.now)();
24700
+ let main2 = null;
24701
+ const agents = [];
24702
+ for (const name of readdirSync6(dir)) {
24703
+ const p = parseLivenessFileName(name);
24704
+ if (p === null || p.key !== key || p.sessionId !== sessionId)
24705
+ continue;
24706
+ const full = join12(dir, name);
24707
+ let seenAt;
24708
+ try {
24709
+ seenAt = statSync5(full).mtimeMs;
24710
+ } catch {
24711
+ continue;
24712
+ }
24713
+ const top = topOf(full);
24714
+ if (top === null)
24715
+ continue;
24716
+ if (p.agentId === null)
24717
+ main2 = { top, seenAt };
24718
+ else if (now - seenAt <= LIVE_WINDOW_MS)
24719
+ agents.push({ agentId: p.agentId, top, seenAt });
24720
+ }
24721
+ agents.sort((a, b) => a.agentId.localeCompare(b.agentId));
24722
+ return { main: main2, agents };
24723
+ } catch {
24724
+ return null;
24355
24725
  }
24356
- return best;
24357
24726
  }
24358
- function knownWindowOf(learned, reported, now) {
24359
- for (const kind of PACE_WINDOW_KINDS) {
24360
- if (learned?.ceilings[kind] !== void 0 || reportedFresh(reported[kind], now))
24361
- return kind;
24362
- }
24363
- return null;
24727
+ function liveCount(record2) {
24728
+ return record2 === null ? 0 : record2.agents.length;
24729
+ }
24730
+ function withoutAgent(record2, agentId) {
24731
+ if (record2 === null || agentId === null || agentId === void 0 || agentId === "")
24732
+ return record2;
24733
+ return { main: record2.main, agents: record2.agents.filter((a) => a.agentId !== agentId) };
24734
+ }
24735
+ function runsBesideOtherWork(toolInput, record2) {
24736
+ const t = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
24737
+ const bg = t["run_in_background"];
24738
+ if (bg !== false && bg !== "false")
24739
+ return true;
24740
+ const name = t["name"];
24741
+ if (name !== void 0 && name !== null && name !== "")
24742
+ return true;
24743
+ return liveCount(record2) > 0;
24364
24744
  }
24365
24745
 
24746
+ // dist/pace-hook.js
24747
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync18, renameSync as renameSync9, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
24748
+ import nodePath4 from "node:path";
24749
+
24750
+ // dist/pace-verb.js
24751
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync8, statSync as statSync6, writeFileSync as writeFileSync10 } from "node:fs";
24752
+ import { homedir as homedir6 } from "node:os";
24753
+ import nodePath3 from "node:path";
24754
+
24366
24755
  // dist/scan-lock.js
24367
24756
  import { spawn } from "node:child_process";
24368
24757
  import { linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync16, renameSync as renameSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "node:fs";
@@ -24961,7 +25350,7 @@ async function paceAtTurnEnd(input, reply, deps = {}) {
24961
25350
  return;
24962
25351
  const nowMs = (deps.now ?? Date.now)();
24963
25352
  const now = new Date(nowMs);
24964
- const key = configFolderKey({ env, transcriptPath: field2(input, "transcript_path"), home: deps.home });
25353
+ const key = configFolderKey({ env, transcriptPath: field2(input, "transcript_path"), home: deps.home, rule: deps.configFolder });
24965
25354
  const stored = readPaceStateFile(dir);
24966
25355
  const before = stored.accounts[key] ?? null;
24967
25356
  if (before !== null) {
@@ -25065,7 +25454,7 @@ function whereOf(input, deps) {
25065
25454
  const dir = enginePacingDir(configured.config.guardEngagementId, deps.home);
25066
25455
  if (dir === null)
25067
25456
  return null;
25068
- return { dir, key: configFolderKey({ env, transcriptPath: field2(input, "transcript_path"), home: deps.home }) };
25457
+ return { dir, key: configFolderKey({ env, transcriptPath: field2(input, "transcript_path"), home: deps.home, rule: deps.configFolder }) };
25069
25458
  }
25070
25459
  function retirePendingStart(input, deps = {}) {
25071
25460
  try {
@@ -25139,7 +25528,7 @@ function paceHoldText(input, toolInput, deps = {}) {
25139
25528
  // dist/risky/facts.js
25140
25529
  import { existsSync as existsSync7, lstatSync as lstatSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, statSync as statSync7 } from "node:fs";
25141
25530
  import { homedir as homedir7, tmpdir as tmpdir2 } from "node:os";
25142
- import { basename as pathBasename, dirname as dirname6, isAbsolute as isAbsolute2, join as join12, relative as relative5, resolve as resolve4, sep as sep3 } from "node:path";
25531
+ import { basename as pathBasename, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative6, resolve as resolve5, sep as sep4 } from "node:path";
25143
25532
 
25144
25533
  // dist/risky/shell.js
25145
25534
  var MAX_DEPTH = 8;
@@ -27349,13 +27738,13 @@ var Env = class {
27349
27738
  description;
27350
27739
  projectCommonDir;
27351
27740
  constructor(ctx) {
27352
- this.projectDir = realOrSelf(resolve4(ctx.projectDir));
27353
- this.cwd = realOrSelf(resolve4(this.projectDir, ctx.cwd));
27354
- this.home = realOrSelf(resolve4(ctx.home ?? homedir7()));
27741
+ this.projectDir = realOrSelf(resolve5(ctx.projectDir));
27742
+ this.cwd = realOrSelf(resolve5(this.projectDir, ctx.cwd));
27743
+ this.home = realOrSelf(resolve5(ctx.home ?? homedir7()));
27355
27744
  this.timeoutMs = ctx.gitTimeoutMs ?? FACTS_GIT_TIMEOUT_MS;
27356
- this.tempDir = realOrSelf(resolve4(ctx.tempDir ?? tmpdir2()));
27745
+ this.tempDir = realOrSelf(resolve5(ctx.tempDir ?? tmpdir2()));
27357
27746
  const roots = ctx.tempRoots ?? [this.tempDir, tmpdir2(), ...TEMP_ROOTS];
27358
- this.tempRoots = [...new Set(roots.map((r) => realOrSelf(resolve4(r))))].filter((r) => !isInside(this.projectDir, r));
27747
+ this.tempRoots = [...new Set(roots.map((r) => realOrSelf(resolve5(r))))].filter((r) => !isInside(this.projectDir, r));
27359
27748
  }
27360
27749
  isTemp(abs) {
27361
27750
  return this.tempRoots.some((r) => isInside(abs, r));
@@ -27365,7 +27754,7 @@ var Env = class {
27365
27754
  }
27366
27755
  display(abs) {
27367
27756
  if (isInside(abs, this.projectDir)) {
27368
- const rel = relative5(this.projectDir, abs);
27757
+ const rel = relative6(this.projectDir, abs);
27369
27758
  return rel === "" ? "." : toSlash(rel);
27370
27759
  }
27371
27760
  return toSlash(abs);
@@ -27375,7 +27764,7 @@ var Env = class {
27375
27764
  return this.description;
27376
27765
  this.description = null;
27377
27766
  try {
27378
- const raw = JSON.parse(readFileSync19(join12(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8"));
27767
+ const raw = JSON.parse(readFileSync19(join13(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8"));
27379
27768
  const checked = checkSystemDescription(raw);
27380
27769
  if (checked.ok)
27381
27770
  this.description = checked.description;
@@ -27418,7 +27807,7 @@ var Env = class {
27418
27807
  if (!r.ok)
27419
27808
  return null;
27420
27809
  const d = r.value.trim();
27421
- return d === "" ? null : realOrSelf(resolve4(dir, d));
27810
+ return d === "" ? null : realOrSelf(resolve5(dir, d));
27422
27811
  }
27423
27812
  };
27424
27813
  function realOrSelf(p) {
@@ -27429,29 +27818,29 @@ function realOrSelf(p) {
27429
27818
  }
27430
27819
  }
27431
27820
  function toSlash(p) {
27432
- return sep3 === "/" ? p : p.split(sep3).join("/");
27821
+ return sep4 === "/" ? p : p.split(sep4).join("/");
27433
27822
  }
27434
27823
  function isInside(child, parent) {
27435
- const rel = relative5(parent, child);
27436
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
27824
+ const rel = relative6(parent, child);
27825
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
27437
27826
  }
27438
27827
  function resolveHome(text, home, base) {
27439
27828
  if (text === "~")
27440
27829
  return home;
27441
27830
  if (text.startsWith("~/"))
27442
- return join12(home, text.slice(2));
27443
- return resolve4(base, text);
27831
+ return join13(home, text.slice(2));
27832
+ return resolve5(base, text);
27444
27833
  }
27445
27834
  function realTarget(abs) {
27446
27835
  const parent = dirname6(abs);
27447
27836
  if (parent === abs)
27448
27837
  return abs;
27449
- return join12(realOrSelf(parent), pathBasename(abs));
27838
+ return join13(realOrSelf(parent), pathBasename(abs));
27450
27839
  }
27451
27840
  function wordPath(word, base, env) {
27452
27841
  if (word.temp !== void 0) {
27453
- const root = word.temp.base === "mktemp" ? join12(env.tempDir, ".halfcycle-mktemp-new") : env.tempDir;
27454
- return resolve4(`${root}${word.temp.suffix}`);
27842
+ const root = word.temp.base === "mktemp" ? join13(env.tempDir, ".halfcycle-mktemp-new") : env.tempDir;
27843
+ return resolve5(`${root}${word.temp.suffix}`);
27455
27844
  }
27456
27845
  return word.literal ? resolveHome(word.text, env.home, base) : null;
27457
27846
  }
@@ -27460,7 +27849,7 @@ function follow(start, prefix, dirs, m, env) {
27460
27849
  let fresh = start.fresh;
27461
27850
  let freshRoot = start.freshRoot;
27462
27851
  const seen = [...prefix];
27463
- const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve4(p), resolve4(freshRoot));
27852
+ const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve5(p), resolve5(freshRoot));
27464
27853
  const mark = (by) => {
27465
27854
  if (fresh !== void 0 && insideFresh(cwd))
27466
27855
  return;
@@ -27589,14 +27978,14 @@ function expandBraces(text) {
27589
27978
  return body.split(",").map((b) => `${head}${b}${tail}`);
27590
27979
  }
27591
27980
  function globPath(abs) {
27592
- const root = abs.startsWith(sep3) ? sep3 : "";
27593
- const segments = abs.split(sep3).filter((s) => s !== "");
27981
+ const root = abs.startsWith(sep4) ? sep4 : "";
27982
+ const segments = abs.split(sep4).filter((s) => s !== "");
27594
27983
  let current = [root === "" ? "." : root];
27595
27984
  for (const segment of segments) {
27596
27985
  const next = [];
27597
27986
  if (!/[*?[]/.test(segment)) {
27598
27987
  for (const c of current)
27599
- next.push(join12(c, segment));
27988
+ next.push(join13(c, segment));
27600
27989
  } else {
27601
27990
  const re = segmentPattern(segment);
27602
27991
  for (const c of current) {
@@ -27610,7 +27999,7 @@ function globPath(abs) {
27610
27999
  if (n.startsWith(".") && !segment.startsWith("."))
27611
28000
  continue;
27612
28001
  if (re.test(n))
27613
- next.push(join12(c, n));
28002
+ next.push(join13(c, n));
27614
28003
  }
27615
28004
  }
27616
28005
  }
@@ -27675,7 +28064,7 @@ function isDirectory(p) {
27675
28064
  }
27676
28065
  function filesUnder(targets, repoRoot, env, keep, seen) {
27677
28066
  const specs = targets.map((t) => {
27678
- const rel = toSlash(relative5(repoRoot, t));
28067
+ const rel = toSlash(relative6(repoRoot, t));
27679
28068
  return rel === "" ? "." : rel;
27680
28069
  });
27681
28070
  const tracked = env.git(["--literal-pathspecs", "ls-files", "--full-name", "-z", "--", ...specs], repoRoot);
@@ -27738,12 +28127,12 @@ function findKeeps(filter, roots, repoRoot) {
27738
28127
  const names = filter.names.map((n) => findPattern(n.pattern, n.fold, false));
27739
28128
  const excluded = filter.excluded.map((p) => findPattern(p, false, true));
27740
28129
  const steps = (rel) => {
27741
- const abs = join12(repoRoot, rel);
28130
+ const abs = join13(repoRoot, rel);
27742
28131
  const out = [];
27743
28132
  for (const r of roots) {
27744
28133
  if (!isInside(abs, r.abs))
27745
28134
  continue;
27746
- const below = toSlash(relative5(r.abs, abs)).split("/").filter((x) => x !== "");
28135
+ const below = toSlash(relative6(r.abs, abs)).split("/").filter((x) => x !== "");
27747
28136
  const rootName = r.text === null ? pathBasename(r.abs) : r.text.replace(/\/+$/, "").split("/").pop() || r.text;
27748
28137
  const prefixes = r.text === null ? [] : [.../* @__PURE__ */ new Set([r.text, r.text.replace(/\/+$/, "") || r.text])];
27749
28138
  const chain = [{ name: rootName, printed: r.text === null ? [] : [r.text] }];
@@ -27810,9 +28199,9 @@ function credentialFilesIn(folders, repoRoot, deadline, skip) {
27810
28199
  const out = [];
27811
28200
  const stack = [];
27812
28201
  for (const f of folders) {
27813
- if (isDirectory(join12(repoRoot, f)))
28202
+ if (isDirectory(join13(repoRoot, f)))
27814
28203
  stack.push(f);
27815
- else if (pathExists(join12(repoRoot, f)) && isCredentialPath(f))
28204
+ else if (pathExists(join13(repoRoot, f)) && isCredentialPath(f))
27816
28205
  out.push(f);
27817
28206
  }
27818
28207
  let seen = 0;
@@ -27820,7 +28209,7 @@ function credentialFilesIn(folders, repoRoot, deadline, skip) {
27820
28209
  const rel = stack.pop();
27821
28210
  let entries;
27822
28211
  try {
27823
- entries = readdirSync8(join12(repoRoot, rel), { withFileTypes: true });
28212
+ entries = readdirSync8(join13(repoRoot, rel), { withFileTypes: true });
27824
28213
  } catch {
27825
28214
  continue;
27826
28215
  }
@@ -27917,7 +28306,7 @@ function deleteFilesFacts(m, env) {
27917
28306
  for (const raw of expanded) {
27918
28307
  const abs = realTarget(raw);
27919
28308
  written.set(abs, word.literal ? word.text : null);
27920
- if (abs === sep3 || abs === env.home || abs !== env.projectDir && isInside(env.projectDir, abs)) {
28309
+ if (abs === sep4 || abs === env.home || abs !== env.projectDir && isInside(env.projectDir, abs)) {
27921
28310
  guarded.push(abs);
27922
28311
  } else if (isInside(abs, env.projectDir) || !env.isTemp(abs) && env.sameRepository(abs)) {
27923
28312
  if (pathExists(abs))
@@ -27961,7 +28350,7 @@ function deleteFilesFacts(m, env) {
27961
28350
  ...base.pre.length === 0 ? {
27962
28351
  save: {
27963
28352
  repoRoot,
27964
- paths: targets.map((t) => toSlash(relative5(repoRoot, t)) || "."),
28353
+ paths: targets.map((t) => toSlash(relative6(repoRoot, t)) || "."),
27965
28354
  ...keep === void 0 ? {} : { keep: (rel) => keep.deletes(rel) },
27966
28355
  lines: []
27967
28356
  }
@@ -27973,12 +28362,12 @@ function closeFoldsAll(targets, env) {
27973
28362
  const root = recordRoot(t, env);
27974
28363
  if (root === null || !isInside(t, root))
27975
28364
  return false;
27976
- const rel = toSlash(relative5(root, t));
28365
+ const rel = toSlash(relative6(root, t));
27977
28366
  if (rel === "" || recordPatternFor(rel)?.deleteAsked !== false)
27978
28367
  return false;
27979
28368
  let at = root;
27980
28369
  for (const seg of rel.split("/")) {
27981
- at = join12(at, seg);
28370
+ at = join13(at, seg);
27982
28371
  try {
27983
28372
  if (lstatSync2(at).isSymbolicLink())
27984
28373
  return false;
@@ -28167,11 +28556,11 @@ function cleanFiles(m, where, env) {
28167
28556
  const repoRoot = env.repoRoot(where.cwd);
28168
28557
  if (repoRoot === null)
28169
28558
  return null;
28170
- const abs = named.map((n) => resolve4(where.cwd, n));
28559
+ const abs = named.map((n) => resolve5(where.cwd, n));
28171
28560
  const files = filesUnder(abs, repoRoot, env);
28172
28561
  if (files === null)
28173
28562
  return null;
28174
- return { lost: files.filter((f) => f.neverSaved).map((f) => f.path), removed: abs.map((a) => toSlash(relative5(repoRoot, a)) || ".") };
28563
+ return { lost: files.filter((f) => f.neverSaved).map((f) => f.path), removed: abs.map((a) => toSlash(relative6(repoRoot, a)) || ".") };
28175
28564
  }
28176
28565
  function stashFiles(m, where, env) {
28177
28566
  let refs;
@@ -28561,7 +28950,7 @@ function publishFacts(m, env) {
28561
28950
  const start = startFolder(m, env);
28562
28951
  if (start !== null && PACKAGE_JSON_PROGRAMS.has(m.program)) {
28563
28952
  try {
28564
- const pkg = JSON.parse(readFileSync19(join12(start, "package.json"), "utf-8"));
28953
+ const pkg = JSON.parse(readFileSync19(join13(start, "package.json"), "utf-8"));
28565
28954
  if (typeof pkg["name"] === "string" && typeof pkg["version"] === "string" && pkg["name"] !== "" && pkg["version"] !== "") {
28566
28955
  name = pkg["name"];
28567
28956
  version2 = pkg["version"];
@@ -28674,7 +29063,7 @@ function recordFileAt(abs, env) {
28674
29063
  const root = recordRoot(abs, env);
28675
29064
  if (root === null || !isInside(abs, root))
28676
29065
  return null;
28677
- const rel = toSlash(relative5(root, abs));
29066
+ const rel = toSlash(relative6(root, abs));
28678
29067
  if (rel === "")
28679
29068
  return null;
28680
29069
  const row = recordPatternFor(rel);
@@ -28684,11 +29073,11 @@ function recordFilesUnder(folder, env) {
28684
29073
  const root = recordRoot(folder, env);
28685
29074
  if (root === null)
28686
29075
  return [];
28687
- const relFolder = toSlash(relative5(root, folder));
29076
+ const relFolder = toSlash(relative6(root, folder));
28688
29077
  const starts = /* @__PURE__ */ new Set();
28689
29078
  for (const { pattern } of PROJECT_RECORD_PATTERNS) {
28690
29079
  if (relFolder === "" || patternOwns(relFolder, pattern))
28691
- starts.add(join12(root, pattern));
29080
+ starts.add(join13(root, pattern));
28692
29081
  else if (patternOwns(pattern, relFolder))
28693
29082
  starts.add(folder);
28694
29083
  }
@@ -28714,7 +29103,7 @@ function recordFilesUnder(folder, env) {
28714
29103
  continue;
28715
29104
  }
28716
29105
  for (const n of names)
28717
- stack.push(join12(p, n));
29106
+ stack.push(join13(p, n));
28718
29107
  } else if (st.isFile()) {
28719
29108
  const r = recordFileAt(p, env);
28720
29109
  if (r !== null)
@@ -28735,7 +29124,7 @@ function recordHistory(files, env) {
28735
29124
  byTree.set(top, list);
28736
29125
  }
28737
29126
  for (const [top, list] of byTree) {
28738
- const specs = list.map((abs) => toSlash(relative5(top, abs)));
29127
+ const specs = list.map((abs) => toSlash(relative6(top, abs)));
28739
29128
  const tracked = trackedOf(specs, top, env);
28740
29129
  if (tracked === null)
28741
29130
  continue;
@@ -28792,8 +29181,14 @@ function parseNumstatZ(text) {
28792
29181
  // dist/risky/sentence.js
28793
29182
  var REASON_MAX_CHARS = 200;
28794
29183
  var MAX_LISTED = 3;
28795
- var TURN_QUESTIONS_ON_LINE = "To let it happen, turn the coding agent's permission questions back on and ask for it again.";
29184
+ var PAUSED_OPENER = "Halfcycle paused a risky step before it runs: ";
29185
+ var PERSON_HOLD_LINE = "Halfcycle paused a risky step before it ran. Nothing changed \u2014 your agent will check with you.";
29186
+ var HELD_STEP_OPENER = "The step was: ";
29187
+ var PRECAUTION_OPENER = "It was stopped as a precaution: ";
29188
+ var ALLOW_REPLY = "allow";
29189
+ var HOLD_AGENT_INSTRUCTION = `If the step is not needed, carry on without it; there is no need to mention it. If it is needed, ask the person in chat, in one plain sentence, whether to run it, and tell them to reply "${ALLOW_REPLY}" to run it once, or anything else to skip it. Do not try it again until they answer.`;
28796
29190
  var NOTHING_LOST_ON_NO = "Nothing is lost if you say no.";
29191
+ var IF_YES = "If you say yes, ";
28797
29192
  var DESCRIBED_TOOLS = /* @__PURE__ */ new Set(["Bash", "Agent"]);
28798
29193
  function reasonOf(call) {
28799
29194
  if (!DESCRIBED_TOOLS.has(call.toolName))
@@ -28822,10 +29217,43 @@ function questionFor(facts, description) {
28822
29217
  return couldNotTellQuestion(description);
28823
29218
  if (staked.length === 1) {
28824
29219
  const only = staked[0];
28825
- return `Halfcycle: ${whatOf(only)} ${reason} If you allow it, ${lostOf(only)} ${NOTHING_LOST_ON_NO}`;
29220
+ return `${PAUSED_OPENER}${stepOf(whatOf(only))}. ${reason} ${NOTHING_LOST_ON_NO} ${IF_YES}${lostOf(only)}`;
28826
29221
  }
28827
- const shown2 = staked.slice(0, MAX_LISTED).map((f) => `${whatOf(f)} If you allow it, ${lostOf(f)}`);
28828
- return `Halfcycle: ${shown2.join(" ")}${moreLine(staked.length - shown2.length)} ${reason} ${NOTHING_LOST_ON_NO}`;
29222
+ const shown2 = staked.slice(0, MAX_LISTED).map((f) => `${whatOf(f)} ${IF_YES}${lostOf(f)}`);
29223
+ return `${PAUSED_OPENER.trimEnd().slice(0, -1)}. ${shown2.join(" ")}${moreLine(staked.length - shown2.length)} ${reason} ${NOTHING_LOST_ON_NO}`;
29224
+ }
29225
+ function refusalFor(facts, description) {
29226
+ const staked = orderedStaked(facts);
29227
+ if (staked.length === 0)
29228
+ return couldNotTellRefusal(description);
29229
+ const shown2 = staked.slice(0, MAX_LISTED);
29230
+ const more = staked.length - shown2.length;
29231
+ const steps = `${shown2.map((f) => stepOf(whatOf(f))).join("; ")}${more > 0 ? `; and ${more} more risky ${more === 1 ? "step" : "steps"}` : ""}`;
29232
+ return heldStep(steps, description, shown2.map(whyOf).join("; "));
29233
+ }
29234
+ function heldStep(steps, description, why) {
29235
+ return {
29236
+ person: PERSON_HOLD_LINE,
29237
+ agent: `${HELD_STEP_OPENER}${steps}. ${reasonSentence(description)} ${PRECAUTION_OPENER}${why}. ${HOLD_AGENT_INSTRUCTION}`
29238
+ };
29239
+ }
29240
+ var STEP_VERBS = {
29241
+ Delete: "deleting",
29242
+ Throw: "throwing",
29243
+ Replace: "replacing",
29244
+ Move: "moving",
29245
+ Show: "showing",
29246
+ Publish: "publishing",
29247
+ Put: "putting"
29248
+ };
29249
+ function stepOf(what) {
29250
+ const bare = what.endsWith(".") ? what.slice(0, -1) : what;
29251
+ const space = bare.indexOf(" ");
29252
+ const verb = space < 0 ? bare : bare.slice(0, space);
29253
+ const ing = STEP_VERBS[verb];
29254
+ if (ing !== void 0)
29255
+ return `${ing}${bare.slice(verb.length)}`;
29256
+ return `one that would ${bare.charAt(0).toLowerCase()}${bare.slice(1)}`;
28829
29257
  }
28830
29258
  function orderedStaked(facts) {
28831
29259
  const said = /* @__PURE__ */ new Set();
@@ -28840,7 +29268,7 @@ function orderedStaked(facts) {
28840
29268
  function moreLine(more) {
28841
29269
  return more > 0 ? ` And ${more} more risky ${more === 1 ? "step" : "steps"}.` : "";
28842
29270
  }
28843
- var UNNAMED_PUSH_WHAT = "Replace a line of work on the shared copy.";
29271
+ var UNNAMED_PUSH_WHAT = "Replace a remote branch.";
28844
29272
  function whatsText(facts, extraPush = false) {
28845
29273
  const staked = orderedStaked(facts);
28846
29274
  const items = staked.map((f) => ({ order: riskyTypeOrder(f.type), what: whatOf(f) }));
@@ -28859,11 +29287,12 @@ function typeSentenceFor(facts, extraPush = false) {
28859
29287
  function savedWhatFor(facts) {
28860
29288
  return whatsText(facts);
28861
29289
  }
28862
- function refusalText(question) {
28863
- return `${question} ${TURN_QUESTIONS_ON_LINE}`;
28864
- }
29290
+ var COULD_NOT_TELL_STEP = "one that can delete, overwrite, publish or show something that cannot be taken back";
28865
29291
  function couldNotTellQuestion(description) {
28866
- return `${AUTONOMY_COULD_NOT_TELL_SENTENCE} ${reasonSentence(description)} If you allow it, whatever it changes may not be possible to bring back. ${NOTHING_LOST_ON_NO}`;
29292
+ return `${PAUSED_OPENER}${COULD_NOT_TELL_STEP}, and Halfcycle could not work out what. ${reasonSentence(description)} ${NOTHING_LOST_ON_NO} ${IF_YES}whatever it changes may not be possible to bring back.`;
29293
+ }
29294
+ function couldNotTellRefusal(description) {
29295
+ return heldStep(COULD_NOT_TELL_STEP, description, "Halfcycle could not work out what this step would change");
28867
29296
  }
28868
29297
  function whatOf(f) {
28869
29298
  switch (f.type) {
@@ -28892,23 +29321,45 @@ function lostOf(f) {
28892
29321
  case "delete-files":
28893
29322
  return deleteLost(f);
28894
29323
  case "discard-unsaved":
28895
- return f.files === null ? "Halfcycle cannot tell which changes this would throw away; any it does were never saved and can never be brought back." : `${f.files === 1 ? "that change was" : "those changes were"} never saved and can never be brought back.`;
29324
+ return f.files === null ? "any uncommitted changes it throws away could not be brought back; Halfcycle cannot tell which ones those are." : `${f.files === 1 ? "that change could not be brought back, because it was" : "those changes could not be brought back, because they were"} never committed.`;
28896
29325
  case "rewrite-shared-history":
28897
29326
  return pushLost(f);
28898
29327
  case "delete-unmerged-work":
28899
29328
  return unmergedLost(f);
28900
29329
  case "wipe-data":
28901
- return "Halfcycle cannot see the database, so it cannot tell how much that is or whether a copy exists.";
29330
+ return "that data is deleted; Halfcycle cannot see the database, so it cannot tell how much that is or whether a backup exists.";
28902
29331
  case "read-credentials":
28903
- return "nothing is deleted, but the secrets would be in the agent's conversation, which is kept on this computer and sent to the coding agent's provider, and cannot be taken back.";
29332
+ return "nothing is deleted, but the secrets become part of the agent's conversation, which is kept on this computer and sent to the coding agent's provider; that cannot be taken back.";
28904
29333
  case "publish":
28905
- return "once published it cannot be taken back.";
29334
+ return "anyone can install it straight away, and a published version cannot be taken back.";
28906
29335
  case "deploy":
28907
29336
  return "what is live now is replaced at once; Halfcycle cannot tell whether going back is possible.";
28908
29337
  case "shrink-record":
28909
29338
  return shrinkLost(f);
28910
29339
  }
28911
29340
  }
29341
+ function whyOf(f) {
29342
+ switch (f.type) {
29343
+ case "delete-files":
29344
+ return deleteWhy(f);
29345
+ case "discard-unsaved":
29346
+ return f.files === null ? "any uncommitted changes it throws away could not be brought back, and Halfcycle cannot tell which ones those are" : `${f.files === 1 ? "that change was never committed, so it" : "those changes were never committed, so they"} could not be brought back`;
29347
+ case "rewrite-shared-history":
29348
+ return pushWhy(f);
29349
+ case "delete-unmerged-work":
29350
+ return unmergedWhy(f);
29351
+ case "wipe-data":
29352
+ return "Halfcycle cannot see the database, so it cannot tell how much would be deleted or whether a backup exists";
29353
+ case "read-credentials":
29354
+ return "once shown, the secrets would be part of the agent's conversation, which is sent to the coding agent's provider";
29355
+ case "publish":
29356
+ return "a published version cannot be taken back";
29357
+ case "deploy":
29358
+ return "what is live now would be replaced at once, and Halfcycle cannot tell whether going back is possible";
29359
+ case "shrink-record":
29360
+ return shrinkWhy(f);
29361
+ }
29362
+ }
28912
29363
  function deleteWhat(f) {
28913
29364
  const thing = thingText(f.thing) ?? listNames(f.match.targets.map((t) => t.text));
28914
29365
  if (f.files === null)
@@ -28917,24 +29368,39 @@ function deleteWhat(f) {
28917
29368
  }
28918
29369
  function deleteLost(f) {
28919
29370
  if (f.files === null || f.neverSaved === null) {
28920
- return "Halfcycle cannot tell which files this would delete, so it cannot tell whether any of them could be brought back.";
29371
+ return "any file it deletes that was never committed could not be brought back; Halfcycle cannot tell which files those are.";
29372
+ }
29373
+ const n = f.files;
29374
+ const k = f.neverSaved;
29375
+ if (k === 0)
29376
+ return n === 1 ? "it can still be brought back from the project's history." : "all of them can still be brought back from the project's history.";
29377
+ if (k === n) {
29378
+ if (n === 1)
29379
+ return "that file could not be brought back, because it was never committed.";
29380
+ return `${n === 2 ? "both" : `all ${n}`} of those files could not be brought back, because they were never committed.`;
29381
+ }
29382
+ return `${k} of those files could not be brought back, because ${k === 1 ? "it was" : "they were"} never committed; the other ${n - k} can be brought back from the project's history.`;
29383
+ }
29384
+ function deleteWhy(f) {
29385
+ if (f.files === null || f.neverSaved === null) {
29386
+ return "Halfcycle cannot tell which files this would delete, or whether any of them were never committed";
28921
29387
  }
28922
29388
  const n = f.files;
28923
29389
  const k = f.neverSaved;
28924
29390
  if (k === 0)
28925
- return n === 1 ? "it can be brought back from the project's history." : "all of them can be brought back from the project's history.";
29391
+ return `a delete like this is checked first, even though ${n === 1 ? "the file" : "every file"} can be brought back from the project's history`;
28926
29392
  if (k === n) {
28927
29393
  if (n === 1)
28928
- return "that file was never saved to the project's history and can never be brought back.";
28929
- return `${n === 2 ? "both" : `all ${n}`} of those files were never saved to the project's history and can never be brought back.`;
29394
+ return "that file was never committed, so it could not be brought back";
29395
+ return `${n === 2 ? "both" : `all ${n}`} of those files were never committed, so they could not be brought back`;
28930
29396
  }
28931
- return `${k} of those files ${k === 1 ? "was" : "were"} never saved to the project's history and can never be brought back; the other ${n - k} can be.`;
29397
+ return `${k} of those files ${k === 1 ? "was" : "were"} never committed, so ${k === 1 ? "it" : "they"} could not be brought back`;
28932
29398
  }
28933
29399
  function discardWhat(f) {
28934
29400
  if (f.files === null)
28935
- return "Throw away unsaved changes in this project.";
29401
+ return "Throw away uncommitted changes in this project.";
28936
29402
  const thing = thingText(f.thing) ?? "this project";
28937
- return `Throw away the unsaved changes to ${thing} \u2014 ${f.files} ${f.files === 1 ? "file" : "files"}.`;
29403
+ return `Throw away the uncommitted changes to ${thing} \u2014 ${f.files} ${f.files === 1 ? "file" : "files"}.`;
28938
29404
  }
28939
29405
  function pushLines(f) {
28940
29406
  const lines2 = f.lines.filter((l) => l.dropped !== 0);
@@ -28945,19 +29411,30 @@ function pushWhat(f) {
28945
29411
  const verb = lines2.length > 0 && lines2.every((l) => l.deletes) ? "Delete" : "Replace";
28946
29412
  const named = lines2.filter((l) => l.branch !== null).map((l) => l.branch);
28947
29413
  if (named.length === 0)
28948
- return verb === "Replace" ? UNNAMED_PUSH_WHAT : `${verb} a line of work on the shared copy.`;
28949
- return `${verb} the shared copy's ${named.length === 1 ? "line" : "lines"} of work ${listNames(named)}.`;
29414
+ return verb === "Replace" ? UNNAMED_PUSH_WHAT : `${verb} a remote branch.`;
29415
+ return `${verb} the remote ${named.length === 1 ? "branch" : "branches"} ${listNames(named)}.`;
29416
+ }
29417
+ function unknownPushWhere(f) {
29418
+ const unknown2 = pushLines(f).find((l) => l.dropped === null);
29419
+ if (unknown2 === void 0)
29420
+ return null;
29421
+ return unknown2.branch === null ? "that branch" : `the remote branch ${quote(unknown2.branch)}`;
28950
29422
  }
28951
29423
  function pushLost(f) {
28952
29424
  const lines2 = pushLines(f);
28953
- const unknown2 = lines2.find((l) => l.dropped === null);
28954
- if (unknown2 !== void 0) {
28955
- const where = unknown2.branch === null ? "that line of work" : `the shared copy's line of work ${quote(unknown2.branch)}`;
28956
- return `whatever is on it now might no longer be there, because Halfcycle cannot tell what is on ${where}.`;
28957
- }
29425
+ const where = unknownPushWhere(f);
29426
+ if (where !== null)
29427
+ return `commits now on it might no longer be there; Halfcycle cannot tell what is on ${where}.`;
28958
29428
  const n = lines2.reduce((s, l) => s + (l.dropped ?? 0), 0);
28959
29429
  const them = n === 1 ? "it" : "them";
28960
- return `${n} saved ${n === 1 ? "change" : "changes"} now on ${lines2.length === 1 ? "it" : "them"} would no longer be there, as of the last time this copy checked; anyone who already has ${them} keeps ${them}, and Halfcycle cannot tell who does.`;
29430
+ return `${n} ${n === 1 ? "commit" : "commits"} now on ${lines2.length === 1 ? "it" : "them"} would no longer be there, as of the last fetch; anyone who already has ${them} keeps ${them}, and Halfcycle cannot tell who does.`;
29431
+ }
29432
+ function pushWhy(f) {
29433
+ const where = unknownPushWhere(f);
29434
+ if (where !== null)
29435
+ return `Halfcycle cannot tell what is on ${where}, so it cannot tell whether any commits would no longer be there`;
29436
+ const n = pushLines(f).reduce((s, l) => s + (l.dropped ?? 0), 0);
29437
+ return `${n} ${n === 1 ? "commit" : "commits"} now on the remote would no longer be there, and Halfcycle cannot tell whether anyone else has ${n === 1 ? "it" : "them"}`;
28961
29438
  }
28962
29439
  function unmergedLines(f) {
28963
29440
  const lines2 = f.lines.filter((l) => l.commits !== 0);
@@ -28967,24 +29444,34 @@ function unmergedWhat(f) {
28967
29444
  const lines2 = unmergedLines(f);
28968
29445
  if (f.match.form === "reset-to") {
28969
29446
  const l = lines2[0];
28970
- const which = l?.branch == null ? "this copy's current line of work" : `the line of work ${quote(l.branch)}`;
29447
+ const which = l?.branch == null ? "the current branch" : `the branch ${quote(l.branch)}`;
28971
29448
  if (l?.commits == null)
28972
29449
  return `Move ${which} back.`;
28973
- return `Move ${which} back ${l.commits} saved ${l.commits === 1 ? "change" : "changes"}.`;
29450
+ return `Move ${which} back ${l.commits} ${l.commits === 1 ? "commit" : "commits"}.`;
28974
29451
  }
28975
29452
  const named = lines2.filter((l) => l.branch !== null).map((l) => l.branch);
28976
29453
  if (named.length === 0)
28977
- return "Delete a line of work.";
28978
- return `Delete the ${named.length === 1 ? "line" : "lines"} of work ${listNames(named)}.`;
29454
+ return "Delete a branch.";
29455
+ return `Delete the ${named.length === 1 ? "branch" : "branches"} ${listNames(named)}.`;
28979
29456
  }
28980
- function unmergedLost(f) {
29457
+ function unmergedCount(f) {
28981
29458
  const lines2 = unmergedLines(f);
28982
- const tail = "this computer can bring them back for a while, the shared copy cannot.";
28983
- if (lines2.some((l) => l.commits === null)) {
28984
- return `Halfcycle cannot tell how many saved changes on it are on no other line of work; ${tail}`;
29459
+ if (lines2.some((l) => l.commits === null))
29460
+ return null;
29461
+ return lines2.reduce((s, l) => s + (l.commits ?? 0), 0);
29462
+ }
29463
+ function unmergedLost(f) {
29464
+ const n = unmergedCount(f);
29465
+ if (n === null) {
29466
+ return "some commits might be left on no branch, here or on the remote; Halfcycle cannot tell how many, and they can still be recovered on this computer for a while.";
28985
29467
  }
28986
- const n = lines2.reduce((s, l) => s + (l.commits ?? 0), 0);
28987
- return n === 1 ? "1 saved change on it is on no other line of work; this computer can bring it back for a while, the shared copy cannot." : `${n} saved changes on it are on no other line of work; ${tail}`;
29468
+ return n === 1 ? "1 commit would be left on no branch, here or on the remote; it can still be recovered on this computer for a while." : `${n} commits would be left on no branch, here or on the remote; they can still be recovered on this computer for a while.`;
29469
+ }
29470
+ function unmergedWhy(f) {
29471
+ const n = unmergedCount(f);
29472
+ if (n === null)
29473
+ return "some of its commits may not be on any other branch or on the remote";
29474
+ return `${n} of its commits ${n === 1 ? "is" : "are"} not on any other branch or on the remote`;
28988
29475
  }
28989
29476
  function wipeWhat(f) {
28990
29477
  const m = f.match;
@@ -29028,7 +29515,7 @@ function sqlTarget(statement) {
29028
29515
  }
29029
29516
  function credentialsWhat(f) {
29030
29517
  if (f.match.form === "gh-token")
29031
- return "Show the agent the sign-in token this computer uses to reach the shared copy.";
29518
+ return "Show the agent the GitHub sign-in token on this computer.";
29032
29519
  if (f.match.form === "keychain")
29033
29520
  return "Show the agent a password kept in this computer's keychain.";
29034
29521
  const file2 = f.file ?? f.match.file;
@@ -29067,6 +29554,20 @@ function shrinkLost(f) {
29067
29554
  return "Halfcycle cannot tell how much of it could be brought back from the project's history.";
29068
29555
  }
29069
29556
  }
29557
+ function shrinkWhy(f) {
29558
+ switch (f.history) {
29559
+ case "saved":
29560
+ return "a record cut this much is checked first, even though all of it can be brought back from the project's history";
29561
+ case "never":
29562
+ return "this record was never saved to the project's history, so what is removed could not be brought back";
29563
+ case "changed": {
29564
+ const u = Math.min(f.unsaved ?? f.removed, f.removed);
29565
+ return `${u === f.removed && u > 1 ? `all ${u}` : u} of those lines ${u === 1 ? "was" : "were"} changed since the record was last saved, so ${u === 1 ? "it" : "they"} could not be brought back`;
29566
+ }
29567
+ case null:
29568
+ return "Halfcycle cannot tell how much of it could be brought back from the project's history";
29569
+ }
29570
+ }
29070
29571
  var NOTHING_WAS_CHANGED = "Nothing was changed.";
29071
29572
  var OWN_COPY_START_FIX = 'To start it, add isolation: "worktree" so it works in a copy of its own. If you already made a copy for it, remove that copy (git worktree remove <path>) and keep its branch: never delete it. In the brief, tell the agent to work in the copy it starts in, its first action git switch <branch>, and drop any "your working folder is <path>" line, because that folder is gone.';
29072
29573
  function ownCopyRefusal(f, description) {
@@ -29127,24 +29628,188 @@ function listPlain(items) {
29127
29628
 
29128
29629
  // dist/risky/check.js
29129
29630
  function riskyAnswer(call, contextOf) {
29130
- const matches = recognise(call);
29631
+ const calls = isOneCall(call) ? [call] : call;
29632
+ const matches = calls.flatMap((c) => recognise(c)).sort((a, b) => riskyTypeOrder(a.type) - riskyTypeOrder(b.type));
29131
29633
  if (matches.length === 0)
29132
29634
  return { silent: true };
29133
- const reason = reasonOf(call);
29635
+ const reason = calls.length === 0 ? null : reasonOf(calls[0]);
29134
29636
  try {
29135
29637
  const staked = factsFor(matches, contextOf()).filter((f) => f.atStake);
29136
29638
  if (staked.length === 0)
29137
29639
  return { silent: true };
29138
- return { silent: false, question: questionFor(staked, reason), staked };
29640
+ return { silent: false, question: questionFor(staked, reason), refusal: refusalFor(staked, reason), staked };
29641
+ } catch {
29642
+ return { silent: false, question: couldNotTellQuestion(reason), refusal: couldNotTellRefusal(reason), staked: [] };
29643
+ }
29644
+ }
29645
+ function isOneCall(call) {
29646
+ return !Array.isArray(call);
29647
+ }
29648
+
29649
+ // dist/risky/allow-once.js
29650
+ import { createHash as createHash5 } from "node:crypto";
29651
+ import { appendFileSync as appendFileSync4, readFileSync as readFileSync20, renameSync as renameSync10, unlinkSync as unlinkSync9, writeFileSync as writeFileSync12 } from "node:fs";
29652
+ var ALLOW_ONCE_MS = 5 * 60 * 1e3;
29653
+ var HELD_STEP_MAX_AGE_MS = 60 * 60 * 1e3;
29654
+ function isAllowReply(prompt) {
29655
+ return typeof prompt === "string" && prompt.trim().toLowerCase() === ALLOW_REPLY;
29656
+ }
29657
+ function stepKey(input, staked) {
29658
+ const toolName = input["tool_name"];
29659
+ const toolInput = input["tool_input"];
29660
+ if (typeof toolName !== "string" || toolName === "")
29661
+ return null;
29662
+ if (typeof toolInput !== "object" || toolInput === null || Array.isArray(toolInput))
29663
+ return null;
29664
+ const rest = { ...toolInput };
29665
+ if (DESCRIBED_TOOLS.has(toolName))
29666
+ delete rest["description"];
29667
+ const cwd = typeof input["cwd"] === "string" ? input["cwd"] : "";
29668
+ return createHash5("sha256").update(canonical([toolName, cwd, rest, staked])).digest("hex");
29669
+ }
29670
+ function canonical(value) {
29671
+ if (Array.isArray(value))
29672
+ return `[${value.map(canonical).join(",")}]`;
29673
+ if (typeof value === "object" && value !== null) {
29674
+ const o = value;
29675
+ return `{${Object.keys(o).sort().filter((k) => o[k] !== void 0).map((k) => `${JSON.stringify(k)}:${canonical(o[k])}`).join(",")}}`;
29676
+ }
29677
+ return JSON.stringify(value) ?? "null";
29678
+ }
29679
+ function recordHeld(sessionId, key, deps = {}) {
29680
+ const paths = allowOncePaths(sessionId, deps.dir);
29681
+ if (paths === null)
29682
+ return;
29683
+ const record2 = { version: 1, session: sessionId, key, at: nowOf(deps) };
29684
+ try {
29685
+ appendFileSync4(paths.held, `${JSON.stringify(record2)}
29686
+ `, { mode: 384 });
29687
+ } catch {
29688
+ return;
29689
+ }
29690
+ }
29691
+ function armAllow(sessionId, deps = {}) {
29692
+ try {
29693
+ const paths = allowOncePaths(sessionId, deps.dir);
29694
+ if (paths === null)
29695
+ return "nothing-held";
29696
+ const now = nowOf(deps);
29697
+ const keys = claimHeldKeys(paths.held, sessionId, now);
29698
+ if (keys.length === 0)
29699
+ return "nothing-held";
29700
+ if (keys.length > 1)
29701
+ return "several-held";
29702
+ return writeRecord(paths.allow, { version: 1, session: sessionId, key: keys[0], at: now }) ? "armed" : "not-saved";
29703
+ } catch {
29704
+ return "not-saved";
29705
+ }
29706
+ }
29707
+ function claimHeldKeys(path, session, now) {
29708
+ const claimed = `${path}.${process.pid}.${now}.claim`;
29709
+ try {
29710
+ renameSync10(path, claimed);
29711
+ } catch {
29712
+ return [];
29713
+ }
29714
+ let text = "";
29715
+ try {
29716
+ text = readFileSync20(claimed, "utf8");
29717
+ } catch {
29718
+ text = "";
29719
+ }
29720
+ removeQuietly(claimed);
29721
+ const keys = /* @__PURE__ */ new Set();
29722
+ for (const line2 of text.split("\n")) {
29723
+ const r = parseRecord(line2, session);
29724
+ if (r !== null && within(r.at, now, HELD_STEP_MAX_AGE_MS))
29725
+ keys.add(r.key);
29726
+ }
29727
+ return [...keys];
29728
+ }
29729
+ function spendAllow(sessionId, key, deps = {}) {
29730
+ try {
29731
+ const paths = allowOncePaths(sessionId, deps.dir);
29732
+ if (paths === null)
29733
+ return false;
29734
+ const armed = readRecord2(paths.allow, sessionId);
29735
+ if (armed === null)
29736
+ return false;
29737
+ if (!within(armed.at, nowOf(deps), ALLOW_ONCE_MS)) {
29738
+ removeQuietly(paths.allow);
29739
+ return false;
29740
+ }
29741
+ if (armed.key !== key)
29742
+ return false;
29743
+ unlinkSync9(paths.allow);
29744
+ return true;
29745
+ } catch {
29746
+ return false;
29747
+ }
29748
+ }
29749
+ function within(at, now, limit) {
29750
+ const age = now - at;
29751
+ return Number.isFinite(age) && age >= 0 && age < limit;
29752
+ }
29753
+ function nowOf(deps) {
29754
+ return (deps.now ?? Date.now)();
29755
+ }
29756
+ function readRecord2(path, session) {
29757
+ try {
29758
+ return parseRecord(readFileSync20(path, "utf8"), session);
29759
+ } catch {
29760
+ return null;
29761
+ }
29762
+ }
29763
+ function parseRecord(text, session) {
29764
+ let parsed;
29765
+ try {
29766
+ parsed = JSON.parse(text);
29767
+ } catch {
29768
+ return null;
29769
+ }
29770
+ if (typeof parsed !== "object" || parsed === null)
29771
+ return null;
29772
+ const r = parsed;
29773
+ if (r["version"] !== 1 || r["session"] !== session)
29774
+ return null;
29775
+ if (typeof r["key"] !== "string" || !/^[0-9a-f]{64}$/.test(r["key"]))
29776
+ return null;
29777
+ if (typeof r["at"] !== "number")
29778
+ return null;
29779
+ return { version: 1, session, key: r["key"], at: r["at"] };
29780
+ }
29781
+ function writeRecord(path, record2) {
29782
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
29783
+ try {
29784
+ writeFileSync12(tmp, JSON.stringify(record2), { mode: 384 });
29785
+ renameSync10(tmp, path);
29786
+ return true;
29787
+ } catch {
29788
+ removeQuietly(tmp);
29789
+ return false;
29790
+ }
29791
+ }
29792
+ function removeQuietly(path) {
29793
+ try {
29794
+ unlinkSync9(path);
29139
29795
  } catch {
29140
- return { silent: false, question: couldNotTellQuestion(reason), staked: [] };
29141
29796
  }
29142
29797
  }
29143
29798
 
29144
29799
  // dist/risky/decide.js
29145
29800
  var QUESTIONS_ON_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "auto"]);
29146
- function answerModeFor(permissionMode) {
29147
- return typeof permissionMode === "string" && QUESTIONS_ON_MODES.has(permissionMode) ? "questions-on" : "questions-off";
29801
+ function answerModeFor(permissionMode, agent = "claude-code") {
29802
+ switch (agent) {
29803
+ case "codex":
29804
+ return "questions-off";
29805
+ case "claude-code":
29806
+ return typeof permissionMode === "string" && QUESTIONS_ON_MODES.has(permissionMode) ? "questions-on" : "questions-off";
29807
+ default: {
29808
+ const unhandled = agent;
29809
+ void unhandled;
29810
+ return "questions-off";
29811
+ }
29812
+ }
29148
29813
  }
29149
29814
  var REFUSED_IN_EVERY_MODE = /* @__PURE__ */ new Set(["outside-own-copy"]);
29150
29815
  function autonomousRowOf(type) {
@@ -29194,6 +29859,9 @@ function decide(action, facts, mode, mayPublishNow = () => false) {
29194
29859
  }
29195
29860
  }
29196
29861
  }
29862
+ function allowableByTyping(action, facts) {
29863
+ return decide(action, facts, "questions-on") === "ask";
29864
+ }
29197
29865
  function decideAutonomous(facts, mayPublishNow) {
29198
29866
  if (facts.length === 0)
29199
29867
  return { autonomous: "could-not-tell" };
@@ -29210,8 +29878,8 @@ function decideAutonomous(facts, mayPublishNow) {
29210
29878
  }
29211
29879
 
29212
29880
  // dist/risky/own-copy.js
29213
- import { readFileSync as readFileSync20 } from "node:fs";
29214
- import { join as join13, posix as posix2, win32 as win322 } from "node:path";
29881
+ import { readFileSync as readFileSync21 } from "node:fs";
29882
+ import { join as join14, posix as posix2, win32 as win322 } from "node:path";
29215
29883
  var OWN_COPY_START_TOOL = "Agent";
29216
29884
  var OWN_COPY_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
29217
29885
  var OWN_COPY_ISOLATION = "worktree";
@@ -29232,7 +29900,7 @@ function stringField2(o, key) {
29232
29900
  }
29233
29901
  function readOr(deps, path) {
29234
29902
  try {
29235
- return (deps.readFile ?? ((p) => readFileSync20(p, "utf-8")))(path);
29903
+ return (deps.readFile ?? ((p) => readFileSync21(p, "utf-8")))(path);
29236
29904
  } catch {
29237
29905
  return null;
29238
29906
  }
@@ -29272,7 +29940,7 @@ function typeCanChangeFiles(subagentType, deps) {
29272
29940
  return true;
29273
29941
  if (AGENT_FILE_NAME.test(type)) {
29274
29942
  for (const base of [deps.projectDir, deps.home]) {
29275
- const text = readOr(deps, join13(base, ".claude", "agents", `${type}.md`));
29943
+ const text = readOr(deps, join14(base, ".claude", "agents", `${type}.md`));
29276
29944
  if (text === null)
29277
29945
  continue;
29278
29946
  const tools = frontmatterTools(text);
@@ -29378,9 +30046,9 @@ function judgeChange(input, call, deps) {
29378
30046
 
29379
30047
  // dist/risky/save.js
29380
30048
  import { randomBytes } from "node:crypto";
29381
- import { chmodSync as chmodSync2, constants as fsConstants, copyFileSync, lstatSync as lstatSync3, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync21, readlinkSync, realpathSync as realpathSync7, renameSync as renameSync10, rmSync as rmSync2, statSync as statSync8, symlinkSync, writeFileSync as writeFileSync12 } from "node:fs";
30049
+ import { chmodSync as chmodSync2, constants as fsConstants, copyFileSync, lstatSync as lstatSync3, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync22, readlinkSync, realpathSync as realpathSync7, renameSync as renameSync11, rmSync as rmSync2, statSync as statSync8, symlinkSync, writeFileSync as writeFileSync13 } from "node:fs";
29382
30050
  import { homedir as homedir8 } from "node:os";
29383
- import { dirname as dirname7, isAbsolute as isAbsolute3, join as join14, resolve as resolve5 } from "node:path";
30051
+ import { dirname as dirname7, isAbsolute as isAbsolute4, join as join15, resolve as resolve6 } from "node:path";
29384
30052
  var SAVE_LIMIT_BYTES = 2e8;
29385
30053
  var SAVE_BUDGET_MS = 5e3;
29386
30054
  function notSaved(reason) {
@@ -29389,7 +30057,7 @@ function notSaved(reason) {
29389
30057
  function savedFolderOf(engagementId, home = homedir8()) {
29390
30058
  if (!isEngagementId(engagementId))
29391
30059
  return null;
29392
- return join14(home, HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
30060
+ return join15(home, HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
29393
30061
  }
29394
30062
  function saveFirst(staked, ctx) {
29395
30063
  const now = ctx.now ?? Date.now;
@@ -29420,20 +30088,20 @@ function saveFirst(staked, ctx) {
29420
30088
  const id = freshId(folder, ctx.newId);
29421
30089
  if (id === null)
29422
30090
  return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29423
- saveDir = join14(folder, id);
30091
+ saveDir = join15(folder, id);
29424
30092
  const repository = repositoryOf(g);
29425
30093
  if (repository === null)
29426
30094
  return failed(saveDir);
29427
- writeFileSync12(join14(saveDir, SAVED_REPOSITORY_FILE_NAME), `${repository}
30095
+ writeFileSync13(join15(saveDir, SAVED_REPOSITORY_FILE_NAME), `${repository}
29428
30096
  `, { mode: 384 });
29429
- const privateIndex = join14(saveDir, PRIVATE_INDEX_NAME);
30097
+ const privateIndex = join15(saveDir, PRIVATE_INDEX_NAME);
29430
30098
  const commit = saveCommit(id, headCommit2, found2.tracked, plan.lines, privateIndex, g, now());
29431
30099
  rmSync2(privateIndex, { force: true });
29432
30100
  if (commit === null)
29433
30101
  return failed(saveDir);
29434
- const copiesDir = join14(saveDir, SAVED_COPIES_DIR_NAME);
30102
+ const copiesDir = join15(saveDir, SAVED_COPIES_DIR_NAME);
29435
30103
  for (const rel of found2.copies) {
29436
- if (now() > deadline || !copyOne(join14(plan.root, rel), join14(copiesDir, rel)))
30104
+ if (now() > deadline || !copyOne(join15(plan.root, rel), join15(copiesDir, rel)))
29437
30105
  return failed(saveDir);
29438
30106
  }
29439
30107
  const description = savedSchema.safeParse({
@@ -29449,10 +30117,10 @@ function saveFirst(staked, ctx) {
29449
30117
  });
29450
30118
  if (!description.success)
29451
30119
  return failed(saveDir);
29452
- const file2 = join14(saveDir, SAVED_FILE_NAME);
29453
- writeFileSync12(`${file2}.part`, `${JSON.stringify(description.data, null, 2)}
30120
+ const file2 = join15(saveDir, SAVED_FILE_NAME);
30121
+ writeFileSync13(`${file2}.part`, `${JSON.stringify(description.data, null, 2)}
29454
30122
  `, { mode: 384 });
29455
- renameSync10(`${file2}.part`, file2);
30123
+ renameSync11(`${file2}.part`, file2);
29456
30124
  ctx.beforeRef?.();
29457
30125
  const ref = g.run(["update-ref", "--no-deref", savedRefOf(id), commit, ""]);
29458
30126
  if (!ref.ok)
@@ -29480,7 +30148,7 @@ function planOf(staked) {
29480
30148
  const t = f.save;
29481
30149
  if (t === void 0)
29482
30150
  return { ok: false, reason: f.cannotTell === void 0 ? AUTONOMY_SAVE_NOT_COVERED_REASON : AUTONOMY_COULD_NOT_TELL_REASON };
29483
- const r = realOrSelf2(resolve5(t.repoRoot));
30151
+ const r = realOrSelf2(resolve6(t.repoRoot));
29484
30152
  if (root === null)
29485
30153
  root = r;
29486
30154
  else if (root !== r)
@@ -29499,7 +30167,7 @@ function planOf(staked) {
29499
30167
  function isInsideRel(p) {
29500
30168
  if (p === ".")
29501
30169
  return true;
29502
- if (p === "" || isAbsolute3(p) || p.includes("\\"))
30170
+ if (p === "" || isAbsolute4(p) || p.includes("\\"))
29503
30171
  return false;
29504
30172
  return p.split("/").every((s) => s !== "" && s !== "." && s !== "..");
29505
30173
  }
@@ -29529,7 +30197,7 @@ function filesOf(plan, g, deadline) {
29529
30197
  trackedAll.add(path);
29530
30198
  if (!reached(path, plan.specs))
29531
30199
  continue;
29532
- const st = lstatOrNull(join14(plan.root, path));
30200
+ const st = lstatOrNull(join15(plan.root, path));
29533
30201
  if (st !== null && (st.isFile() || st.isSymbolicLink()))
29534
30202
  tracked.add(path);
29535
30203
  }
@@ -29541,7 +30209,7 @@ function filesOf(plan, g, deadline) {
29541
30209
  const rel = stack.pop();
29542
30210
  if (rel === ".git")
29543
30211
  continue;
29544
- const abs = rel === "." ? plan.root : join14(plan.root, rel);
30212
+ const abs = rel === "." ? plan.root : join15(plan.root, rel);
29545
30213
  const st = lstatOrNull(abs);
29546
30214
  if (st === null)
29547
30215
  continue;
@@ -29585,7 +30253,7 @@ function saveCommit(id, head, tracked, lines2, privateIndex, g, at) {
29585
30253
  const indexPath = g.run(["rev-parse", "--git-path", "index"]);
29586
30254
  if (!indexPath.ok)
29587
30255
  return null;
29588
- const projectIndex = resolve5(g.root, indexPath.value.trim());
30256
+ const projectIndex = resolve6(g.root, indexPath.value.trim());
29589
30257
  const env = commitEnv(privateIndex, at);
29590
30258
  if (lstatOrNull(projectIndex) !== null)
29591
30259
  copyFileSync(projectIndex, privateIndex);
@@ -29654,7 +30322,7 @@ function freshId(folder, newId) {
29654
30322
  if (!savedIdSchema.safeParse(id).success)
29655
30323
  return null;
29656
30324
  try {
29657
- mkdirSync8(join14(folder, id), { mode: 448 });
30325
+ mkdirSync8(join15(folder, id), { mode: 448 });
29658
30326
  return id;
29659
30327
  } catch (e) {
29660
30328
  if (e.code !== "EEXIST")
@@ -29675,11 +30343,11 @@ function repositoryOf(g) {
29675
30343
  if (!r.ok)
29676
30344
  return null;
29677
30345
  const dir = r.value.trim();
29678
- return dir === "" ? null : realOrSelf2(resolve5(g.root, dir));
30346
+ return dir === "" ? null : realOrSelf2(resolve6(g.root, dir));
29679
30347
  }
29680
30348
  function markedRepository(dir) {
29681
30349
  try {
29682
- const text = readFileSync21(join14(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
30350
+ const text = readFileSync22(join15(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
29683
30351
  return text === "" ? null : text;
29684
30352
  } catch {
29685
30353
  return null;
@@ -29713,7 +30381,7 @@ function pruneSaves(root, engagementId, ctx = {}) {
29713
30381
  if (!savedIdSchema.safeParse(id).success)
29714
30382
  continue;
29715
30383
  here.add(id);
29716
- const dir = join14(folder, id);
30384
+ const dir = join15(folder, id);
29717
30385
  const st = lstatOrNull(dir);
29718
30386
  const answer = savedPruneAnswer({
29719
30387
  refMadeAt: refs.get(id) ?? null,
@@ -29752,19 +30420,19 @@ function realOrSelf2(p) {
29752
30420
 
29753
30421
  // dist/autonomy-state.js
29754
30422
  import { spawnSync as spawnSync2 } from "node:child_process";
29755
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync22, renameSync as renameSync11, rmSync as rmSync3, writeFileSync as writeFileSync13 } from "node:fs";
30423
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync23, renameSync as renameSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "node:fs";
29756
30424
  import { homedir as homedir9, platform as platform2 } from "node:os";
29757
- import { dirname as dirname8, join as join15 } from "node:path";
30425
+ import { dirname as dirname8, join as join16 } from "node:path";
29758
30426
  var AUTONOMY_STATE_FILENAME = "autonomy.json";
29759
30427
  var AUTONOMY_STATE_DEADLINE_MS = 1500;
29760
30428
  function autonomyStateFilePath(engagementId, home) {
29761
- return join15(home ?? homedir9(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, AUTONOMY_STATE_FILENAME);
30429
+ return join16(home ?? homedir9(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, AUTONOMY_STATE_FILENAME);
29762
30430
  }
29763
30431
  function readCachedAutonomy(engagementId, nowMs, home) {
29764
30432
  if (!isEngagementId(engagementId))
29765
30433
  return null;
29766
30434
  try {
29767
- const parsed = autonomyStateFileSchema.safeParse(JSON.parse(readFileSync22(autonomyStateFilePath(engagementId, home), "utf-8")));
30435
+ const parsed = autonomyStateFileSchema.safeParse(JSON.parse(readFileSync23(autonomyStateFilePath(engagementId, home), "utf-8")));
29768
30436
  if (!parsed.success || !parsed.data.on)
29769
30437
  return null;
29770
30438
  const age = nowMs - Date.parse(parsed.data.readAt);
@@ -29810,14 +30478,14 @@ function deleteAutonomyStateFile(engagementId, home) {
29810
30478
  }
29811
30479
  function writeAutonomyStateFile(engagementId, file2, home) {
29812
30480
  const target = autonomyStateFilePath(engagementId, home);
29813
- const temp = join15(dirname8(target), `.${AUTONOMY_STATE_FILENAME}.${process.pid}.tmp`);
30481
+ const temp = join16(dirname8(target), `.${AUTONOMY_STATE_FILENAME}.${process.pid}.tmp`);
29814
30482
  try {
29815
30483
  mkdirSync9(dirname8(target), { recursive: true, mode: 448 });
29816
- writeFileSync13(temp, `${JSON.stringify(file2)}
30484
+ writeFileSync14(temp, `${JSON.stringify(file2)}
29817
30485
  `, { mode: 384 });
29818
30486
  if (platform2() !== "win32")
29819
30487
  chmodSync3(temp, 384);
29820
- renameSync11(temp, target);
30488
+ renameSync12(temp, target);
29821
30489
  return true;
29822
30490
  } catch {
29823
30491
  try {
@@ -29871,6 +30539,76 @@ async function cacheAutonomySetting(opts) {
29871
30539
  }
29872
30540
  }
29873
30541
 
30542
+ // dist/risky/patch-calls.js
30543
+ import { isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
30544
+ var SHELL_TOOL = "Bash";
30545
+ function toolCallsOf(input, profile) {
30546
+ const toolName = input["tool_name"];
30547
+ const toolInput = input["tool_input"];
30548
+ if (typeof toolName !== "string" || toolName === "")
30549
+ return null;
30550
+ if (typeof toolInput !== "object" || toolInput === null || Array.isArray(toolInput))
30551
+ return null;
30552
+ const call = { toolName, toolInput };
30553
+ if (profile.id === "claude-code")
30554
+ return [call];
30555
+ if (!profile.writeCapableTools.includes(toolName))
30556
+ return [call];
30557
+ const filePath = call.toolInput["file_path"];
30558
+ if (typeof filePath === "string" && filePath !== "")
30559
+ return [call];
30560
+ const command = call.toolInput["command"];
30561
+ if (typeof command !== "string")
30562
+ return null;
30563
+ const ops = patchOperations(command);
30564
+ if (ops === null || ops.length === 0)
30565
+ return null;
30566
+ const cwd = typeof input["cwd"] === "string" && isAbsolute5(input["cwd"]) ? input["cwd"] : null;
30567
+ const calls = [];
30568
+ for (const op of ops) {
30569
+ const made = callsFor(op, cwd);
30570
+ if (made === null)
30571
+ return null;
30572
+ calls.push(...made);
30573
+ }
30574
+ return calls;
30575
+ }
30576
+ function callsFor(op, cwd) {
30577
+ const path = placed(op.path, cwd);
30578
+ if (path === null)
30579
+ return null;
30580
+ switch (op.kind) {
30581
+ case "add":
30582
+ return [{ toolName: "Write", toolInput: { file_path: path, content: op.lines.length === 0 ? "" : `${op.lines.join("\n")}
30583
+ ` } }];
30584
+ case "delete":
30585
+ return [{ toolName: SHELL_TOOL, toolInput: { command: `rm -- ${shellWord(path)}` } }];
30586
+ case "update": {
30587
+ const edits = op.hunks.filter((h) => h.before.length > 0).map((h) => ({ old_string: h.before.join("\n"), new_string: h.after.join("\n") }));
30588
+ const out = [{ toolName: "MultiEdit", toolInput: { file_path: path, edits } }];
30589
+ if (op.movedTo !== null) {
30590
+ const dest = placed(op.movedTo, cwd);
30591
+ if (dest === null)
30592
+ return null;
30593
+ out.push({ toolName: SHELL_TOOL, toolInput: { command: `mv -- ${shellWord(path)} ${shellWord(dest)}` } });
30594
+ }
30595
+ return out;
30596
+ }
30597
+ default: {
30598
+ const unhandled = op;
30599
+ return unhandled;
30600
+ }
30601
+ }
30602
+ }
30603
+ function placed(path, cwd) {
30604
+ if (isAbsolute5(path))
30605
+ return resolve7(path);
30606
+ return cwd === null ? null : resolve7(cwd, path);
30607
+ }
30608
+ function shellWord(text) {
30609
+ return `'${text.split("'").join("'\\''")}'`;
30610
+ }
30611
+
29874
30612
  // dist/hooks/pre-tool-use.js
29875
30613
  import { performance } from "node:perf_hooks";
29876
30614
  import { homedir as homedir10 } from "node:os";
@@ -29882,13 +30620,15 @@ var OWN_COPY_UNKNOWN_NOTICE = "Halfcycle could not tell which copy of the projec
29882
30620
  var CHECKED = new Set(CHECKED_TOOLS);
29883
30621
  var RECORDED_ONLY = new Set(RECORDED_ONLY_TOOLS);
29884
30622
  function runPreToolUse(deps) {
29885
- const input = parseObject(deps.input);
30623
+ const parsed = parseObject(deps.input);
30624
+ const input = parsed === null ? null : hookInputForRule(parsed, profileOf(deps).configFolder);
29886
30625
  const at = clockOf(deps)();
29887
30626
  const stamped = { ...deps, now: () => at };
29888
30627
  recordTimeEvent("action", input ?? {}, stamped);
29889
30628
  recordOwnShell(input, at, { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir } });
29890
30629
  const seen = recordLiveness(input ?? {}, deps.liveness);
29891
30630
  const paceDeps = {
30631
+ configFolder: profileOf(deps).configFolder,
29892
30632
  ...deps.pace ?? {},
29893
30633
  ...deps.env === void 0 ? {} : { env: deps.env },
29894
30634
  ...deps.home === void 0 ? {} : { home: deps.home },
@@ -29900,14 +30640,15 @@ function runPreToolUse(deps) {
29900
30640
  const toolName = input === null ? void 0 : stringOf(input, "tool_name");
29901
30641
  if (toolName !== void 0 && RECORDED_ONLY.has(toolName))
29902
30642
  return recordedOnlyReply(mode);
29903
- const call = input === null ? null : toolCallOf(input);
29904
- if (call === null || !CHECKED.has(call.toolName))
30643
+ const calls = input === null ? null : toolCallsOf(input, profileOf(deps));
30644
+ if (calls === null || !calls.every((c) => CHECKED.has(c.toolName)))
29905
30645
  return unreadable(deps, input);
30646
+ const call = calls[0] ?? { toolName: stringOf(input, "tool_name") ?? "", toolInput: {} };
29906
30647
  let answer;
29907
30648
  try {
29908
- answer = riskyAnswer(call, () => {
30649
+ answer = riskyAnswer(calls, () => {
29909
30650
  const cwd = stringOf(input, "cwd") ?? (deps.cwd ?? (() => process.cwd()))();
29910
- const projectDir = deps.projectDir ?? projectFolder(deps.env ?? process.env);
30651
+ const projectDir = projectDirOf(deps);
29911
30652
  const tempDir = (deps.env ?? process.env)["TMPDIR"];
29912
30653
  return {
29913
30654
  cwd,
@@ -29920,7 +30661,7 @@ function runPreToolUse(deps) {
29920
30661
  } catch {
29921
30662
  answer = { silent: true };
29922
30663
  }
29923
- const own = ownCopyOf(input, call, deps);
30664
+ const own = ownCopyOfAll(input, calls, deps);
29924
30665
  try {
29925
30666
  if (own.kind === "refused") {
29926
30667
  const staked = [...answer.silent ? [] : answer.staked, own.facts];
@@ -29931,12 +30672,12 @@ function runPreToolUse(deps) {
29931
30672
  }
29932
30673
  }
29933
30674
  if (mode === "autonomous") {
29934
- const sendsSaves = call.toolName === "Bash" && typeof call.toolInput["command"] === "string" && pushesSavedRefs(call.toolInput["command"]);
30675
+ const sendsSaves = calls.some((c) => c.toolName === "Bash" && typeof c.toolInput["command"] === "string" && pushesSavedRefs(c.toolInput["command"]));
29935
30676
  if (!answer.silent || sendsSaves)
29936
30677
  return autonomousReply(call, answer.silent ? [] : answer.staked, sendsSaves, mode, autonomy, deps);
29937
30678
  }
29938
30679
  if (answer.silent) {
29939
- if (call.toolName === PACE_HELD_TOOL) {
30680
+ if (calls.length === 1 && call.toolName === PACE_HELD_TOOL) {
29940
30681
  const held = paceHoldText(input, call.toolInput, paceDeps);
29941
30682
  if (held !== null)
29942
30683
  return `${preToolUseReplyText("refuse", held)}
@@ -29949,7 +30690,16 @@ function runPreToolUse(deps) {
29949
30690
  return `${preToolUseReplyText("ask", answer.question)}
29950
30691
  `;
29951
30692
  }
29952
- return `${preToolUseReplyText("refuse", refusalText(answer.question))}
30693
+ if (allowableByTyping(call, answer.staked)) {
30694
+ const key = stepKey(input, answer.staked);
30695
+ if (key !== null) {
30696
+ const allowOnce = { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir }, now: () => at.getTime() };
30697
+ if (spendAllow(input["session_id"], key, allowOnce))
30698
+ return "";
30699
+ recordHeld(input["session_id"], key, allowOnce);
30700
+ }
30701
+ }
30702
+ return `${preToolUseReplyText("refuse", answer.refusal.person, answer.refusal.agent)}
29953
30703
  `;
29954
30704
  } catch {
29955
30705
  return "";
@@ -29963,7 +30713,7 @@ function answerModeOf(input, deps, at) {
29963
30713
  const cached2 = engagementId === void 0 ? null : readCachedAutonomy(engagementId, at.getTime(), deps.home);
29964
30714
  if (engagementId !== void 0 && cached2 !== null)
29965
30715
  return { mode: "autonomous", autonomy: { engagementId, mayPublish: cached2.mayPublish } };
29966
- return { mode: answerModeFor(input === null ? void 0 : input["permission_mode"]), autonomy: null };
30716
+ return { mode: answerModeFor(input === null ? void 0 : input["permission_mode"], profileOf(deps).id), autonomy: null };
29967
30717
  }
29968
30718
  function autonomousReply(call, staked, sendsSaves, mode, autonomy, deps) {
29969
30719
  try {
@@ -30043,11 +30793,40 @@ function recordedOnlyReply(mode) {
30043
30793
  }
30044
30794
  }
30045
30795
  }
30796
+ function profileOf(deps) {
30797
+ return deps.profile ?? AGENT_PROFILES["claude-code"];
30798
+ }
30799
+ function projectDirOf(deps) {
30800
+ if (deps.projectDir !== void 0)
30801
+ return deps.projectDir;
30802
+ const rule = profileOf(deps).projectRoot;
30803
+ switch (rule) {
30804
+ case "git-toplevel":
30805
+ return (deps.cwd ?? (() => process.cwd()))();
30806
+ case "claude-project-dir":
30807
+ return projectFolder(deps.env ?? process.env);
30808
+ default: {
30809
+ const unhandled = rule;
30810
+ return unhandled;
30811
+ }
30812
+ }
30813
+ }
30814
+ function ownCopyOfAll(input, calls, deps) {
30815
+ let unknown2 = false;
30816
+ for (const c of calls) {
30817
+ const v = ownCopyOf(input, c, deps);
30818
+ if (v.kind === "refused")
30819
+ return v;
30820
+ if (v.kind === "unknown")
30821
+ unknown2 = true;
30822
+ }
30823
+ return unknown2 ? { kind: "unknown" } : { kind: "pass" };
30824
+ }
30046
30825
  function ownCopyOf(input, call, deps) {
30047
30826
  try {
30048
30827
  const walk2 = deps.walk ?? deps.liveness?.walk;
30049
30828
  return judgeOwnCopy(input, call, {
30050
- projectDir: deps.projectDir ?? projectFolder(deps.env ?? process.env),
30829
+ projectDir: projectDirOf(deps),
30051
30830
  home: deps.home ?? homedir10(),
30052
30831
  ...walk2 === void 0 ? {} : { walk: walk2 },
30053
30832
  ...deps.liveness === void 0 ? {} : { liveness: deps.liveness }
@@ -30057,7 +30836,7 @@ function ownCopyOf(input, call, deps) {
30057
30836
  }
30058
30837
  }
30059
30838
  function ownCopyNotice(input, deps) {
30060
- const folder = stringOf(input, "cwd") ?? deps.projectDir ?? projectFolder(deps.env ?? process.env);
30839
+ const folder = stringOf(input, "cwd") ?? projectDirOf(deps);
30061
30840
  return firstNoticeThisSession("outside-own-copy-unknown", folder, stringOf(input, "session_id"), deps.liveness?.dir) ? `${preToolUseNoticeText(OWN_COPY_UNKNOWN_NOTICE)}
30062
30841
  ` : "";
30063
30842
  }
@@ -30073,23 +30852,16 @@ function stringOf(input, key) {
30073
30852
  const v = input[key];
30074
30853
  return typeof v === "string" && v !== "" ? v : void 0;
30075
30854
  }
30076
- function toolCallOf(input) {
30077
- const toolName = stringOf(input, "tool_name");
30078
- const toolInput = input["tool_input"];
30079
- if (toolName === void 0 || typeof toolInput !== "object" || toolInput === null || Array.isArray(toolInput))
30080
- return null;
30081
- return { toolName, toolInput };
30082
- }
30083
30855
  function unreadable(deps, input) {
30084
30856
  const session = (input === null ? void 0 : stringOf(input, "session_id")) ?? /"session_id"\s*:\s*"([^"\\]+)"/.exec(deps.input)?.[1];
30085
- const folder = (input === null ? void 0 : stringOf(input, "cwd")) ?? deps.projectDir ?? projectFolder(deps.env ?? process.env);
30857
+ const folder = (input === null ? void 0 : stringOf(input, "cwd")) ?? projectDirOf(deps);
30086
30858
  return firstNoticeThisSession("pre-tool-use-unreadable", folder, session) ? `${preToolUseNoticeText(UNREADABLE_INPUT_NOTICE)}
30087
30859
  ` : "";
30088
30860
  }
30089
30861
 
30090
30862
  // dist/hooks/session-diff.js
30091
- import { unlinkSync as unlinkSync9 } from "node:fs";
30092
- import { join as join16 } from "node:path";
30863
+ import { unlinkSync as unlinkSync10 } from "node:fs";
30864
+ import { join as join17 } from "node:path";
30093
30865
  var STOP_HOOK_EVENT = {
30094
30866
  stop: "Stop",
30095
30867
  "subagent-stop": "SubagentStop"
@@ -30124,14 +30896,14 @@ function collectDiff(repoRoot, base) {
30124
30896
  getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
30125
30897
  };
30126
30898
  }
30127
- async function runSessionDiff(input, kind, readSeat = readSeatConfiguration, afterPass) {
30128
- const reply = hookReply(STOP_HOOK_EVENT[kind]);
30129
- await evaluateSession(input, kind, reply, readSeat);
30899
+ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration, afterPass, profile = AGENT_PROFILES["claude-code"]) {
30900
+ const reply = hookReply(STOP_HOOK_EVENT[kind], profile);
30901
+ await evaluateSession(input, kind, reply, readSeat, profile);
30130
30902
  if (afterPass)
30131
30903
  await afterPass(reply);
30132
30904
  reply.flush();
30133
30905
  }
30134
- async function evaluateSession(input, kind, reply, readSeat) {
30906
+ async function evaluateSession(input, kind, reply, readSeat, profile) {
30135
30907
  const repoRootResult = getRepoRoot(process.cwd());
30136
30908
  if (!repoRootResult.ok) {
30137
30909
  if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
@@ -30154,7 +30926,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
30154
30926
  const candidates = ownEditedFiles(repoRoot, sessionId).filter((relPath) => !tracked.has(relPath));
30155
30927
  const untracked = getUntrackedFiles(repoRoot, candidates);
30156
30928
  const created = untracked.ok ? untracked.value.filter((relPath) => !tracked.has(relPath)) : [];
30157
- const getDiff = (relPath) => tracked.has(relPath) ? collected.getDiff(relPath) : getSingleFileDiff(join16(repoRoot, relPath), repoRoot);
30929
+ const getDiff = (relPath) => tracked.has(relPath) ? collected.getDiff(relPath) : getSingleFileDiff(join17(repoRoot, relPath), repoRoot);
30158
30930
  const { evaluated } = partitionInstallerOutput(collected.files);
30159
30931
  const { evaluated: createdEvaluated } = partitionInstallerOutput(created);
30160
30932
  const changedFiles2 = [...ownChangedFiles(repoRoot, evaluated, sessionId), ...createdEvaluated];
@@ -30180,11 +30952,11 @@ async function evaluateSession(input, kind, reply, readSeat) {
30180
30952
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
30181
30953
  if (!firstNoticeThisSession("credential-missing", repoRoot, sessionId))
30182
30954
  return;
30183
- emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
30955
+ emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()), reply);
30184
30956
  return;
30185
30957
  }
30186
30958
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
30187
- const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
30959
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat, profile);
30188
30960
  const evaluation = await evaluateWithRefresh({
30189
30961
  guardServiceUrl,
30190
30962
  token: guardServiceToken,
@@ -30215,7 +30987,8 @@ async function evaluateSession(input, kind, reply, readSeat) {
30215
30987
  // The remedy the renewal attempt earned: the sign-in when the account
30216
30988
  // credential is what was refused, and otherwise the plane's own sentence
30217
30989
  // about why it would not mint.
30218
- evaluation.remedy
30990
+ evaluation.remedy,
30991
+ reply
30219
30992
  );
30220
30993
  return;
30221
30994
  }
@@ -30271,7 +31044,7 @@ function removeFindingsFile(sessionId) {
30271
31044
  if (path === null)
30272
31045
  return;
30273
31046
  try {
30274
- unlinkSync9(path);
31047
+ unlinkSync10(path);
30275
31048
  } catch {
30276
31049
  }
30277
31050
  }
@@ -30317,9 +31090,9 @@ async function readHookStdin() {
30317
31090
  }
30318
31091
  }
30319
31092
  function readStdin2() {
30320
- return new Promise((resolve8) => {
31093
+ return new Promise((resolve10) => {
30321
31094
  if (process.stdin.isTTY) {
30322
- resolve8("");
31095
+ resolve10("");
30323
31096
  return;
30324
31097
  }
30325
31098
  const chunks = [];
@@ -30329,7 +31102,7 @@ function readStdin2() {
30329
31102
  return;
30330
31103
  settled = true;
30331
31104
  clearTimeout(timer);
30332
- resolve8(Buffer.concat(chunks).toString("utf-8"));
31105
+ resolve10(Buffer.concat(chunks).toString("utf-8"));
30333
31106
  };
30334
31107
  const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
30335
31108
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -30339,11 +31112,11 @@ function readStdin2() {
30339
31112
  }
30340
31113
 
30341
31114
  // dist/hooks/subagent-stop.js
30342
- async function runSubagentStop() {
30343
- const input = await readHookStdin();
31115
+ async function runSubagentStop(profile = AGENT_PROFILES["claude-code"]) {
31116
+ const input = hookInputForRule(await readHookStdin(), profile.configFolder);
30344
31117
  recordTimeEvent("turn-end", input);
30345
- subagentStopped(input);
30346
- await runSessionDiff(input, "subagent-stop");
31118
+ subagentStopped(input, { pace: { configFolder: profile.configFolder } });
31119
+ await runSessionDiff(input, "subagent-stop", void 0, void 0, profile);
30347
31120
  }
30348
31121
  function subagentStopped(input, deps = {}) {
30349
31122
  if (clearLiveness(input, deps.liveness) === 0)
@@ -30351,14 +31124,14 @@ function subagentStopped(input, deps = {}) {
30351
31124
  }
30352
31125
 
30353
31126
  // dist/hooks/stop.js
30354
- async function runStop(observeDeps = {}, timeDeps = {}, paceDeps = {}) {
30355
- const input = await readHookStdin();
31127
+ async function runStop(observeDeps = {}, timeDeps = {}, paceDeps = {}, profile = AGENT_PROFILES["claude-code"]) {
31128
+ const input = hookInputForRule(await readHookStdin(), profile.configFolder);
30356
31129
  recordTimeEvent("turn-end", input);
30357
31130
  await runSessionDiff(input, "stop", readSeatConfiguration, async (reply) => {
30358
31131
  await observeChanges(input.session_id, reply, observeDeps);
30359
31132
  await sendTimeRecord(timeDeps);
30360
- await paceAtTurnEnd(input, reply, paceDeps);
30361
- });
31133
+ await paceAtTurnEnd(input, reply, { configFolder: profile.configFolder, ...paceDeps });
31134
+ }, profile);
30362
31135
  }
30363
31136
 
30364
31137
  // dist/hooks/scope-phrase.js
@@ -30514,20 +31287,20 @@ function matchReviewPhraseShape(shape, reviews) {
30514
31287
  }
30515
31288
 
30516
31289
  // dist/hooks/shown-record.js
30517
- import { chmodSync as chmodSync4, readFileSync as readFileSync23, renameSync as renameSync12, unlinkSync as unlinkSync10, writeFileSync as writeFileSync14 } from "node:fs";
31290
+ import { chmodSync as chmodSync4, readFileSync as readFileSync24, renameSync as renameSync13, unlinkSync as unlinkSync11, writeFileSync as writeFileSync15 } from "node:fs";
30518
31291
  import { homedir as homedir11, platform as platform3 } from "node:os";
30519
- import { join as join17 } from "node:path";
31292
+ import { join as join18 } from "node:path";
30520
31293
  function recordDir(engagementId, home) {
30521
31294
  if (!isEngagementId(engagementId))
30522
31295
  return null;
30523
- return join17(home ?? homedir11(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
31296
+ return join18(home ?? homedir11(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
30524
31297
  }
30525
31298
  function readShownRecord(engagementId, home) {
30526
31299
  const dir = recordDir(engagementId, home);
30527
31300
  if (dir === null)
30528
31301
  return { entries: [], reviews: [] };
30529
31302
  try {
30530
- return parseShownRecord(readFileSync23(join17(dir, SHOWN_PLANS_FILENAME), "utf-8"));
31303
+ return parseShownRecord(readFileSync24(join18(dir, SHOWN_PLANS_FILENAME), "utf-8"));
30531
31304
  } catch {
30532
31305
  return { entries: [], reviews: [] };
30533
31306
  }
@@ -30548,24 +31321,24 @@ function moveListedReviewForward(engagementId, home, listed, reviewVersion, deci
30548
31321
  const dir = recordDir(engagementId, home);
30549
31322
  if (dir === null)
30550
31323
  return false;
30551
- const path = join17(dir, SHOWN_PLANS_FILENAME);
30552
- const current = parseShownRecord(readFileSync23(path, "utf-8"));
31324
+ const path = join18(dir, SHOWN_PLANS_FILENAME);
31325
+ const current = parseShownRecord(readFileSync24(path, "utf-8"));
30553
31326
  const at = current.reviews.findIndex((r) => r.phase === listed.phase && r.shownAt === listed.shownAt && r.reviewVersion === listed.reviewVersion);
30554
31327
  if (at === -1)
30555
31328
  return false;
30556
31329
  const acts = new Map(decided.map((d) => [d.part, d.act]));
30557
31330
  const reviews = current.reviews.map((review, i) => i === at ? { ...review, reviewVersion, parts: review.parts.map((p) => movedPart(p, acts.get(p.part))) } : review);
30558
31331
  const body = serialiseShownRecord({ entries: current.entries, reviews });
30559
- temp = join17(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
30560
- writeFileSync14(temp, body, { mode: 384 });
31332
+ temp = join18(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
31333
+ writeFileSync15(temp, body, { mode: 384 });
30561
31334
  if (platform3() !== "win32")
30562
31335
  chmodSync4(temp, 384);
30563
- renameSync12(temp, path);
31336
+ renameSync13(temp, path);
30564
31337
  return true;
30565
31338
  } catch {
30566
31339
  if (temp !== null) {
30567
31340
  try {
30568
- unlinkSync10(temp);
31341
+ unlinkSync11(temp);
30569
31342
  } catch {
30570
31343
  return false;
30571
31344
  }
@@ -30909,16 +31682,63 @@ function autonomyLineForAnswer(answer) {
30909
31682
  }
30910
31683
  }
30911
31684
  }
31685
+ var NOTHING_ALLOWED = "Nothing was allowed";
31686
+ var ALLOW_MINUTES = Math.round(ALLOW_ONCE_MS / 6e4);
31687
+ function allowArmedLine() {
31688
+ return line(`The person allowed the paused step once. Run it again now, exactly as before; it will go ahead once if it runs within ${ALLOW_MINUTES} minutes, and any other risky step is still paused.`);
31689
+ }
31690
+ function allowNothingHeldLine() {
31691
+ return line(`${NOTHING_ALLOWED}: no step is paused in this session.`);
31692
+ }
31693
+ function allowSeveralHeldLine() {
31694
+ return line(`${NOTHING_ALLOWED}: more than one step is paused in this session. Ask the agent to try the one you mean again, then type ${ALLOW_REPLY}.`);
31695
+ }
31696
+ function allowNotAttendedLine() {
31697
+ return line(`${NOTHING_ALLOWED}: a paused step can be allowed by typing only when a person types it into an open session.`);
31698
+ }
31699
+ function allowNotSavedLine() {
31700
+ return line(`${NOTHING_ALLOWED}: Halfcycle could not save the answer on this computer. Type ${ALLOW_REPLY} again.`);
31701
+ }
31702
+ var CODEX_TYPING_NEVER_COUNTS = "in Codex, Halfcycle cannot tell a line a person typed from one a program sent, so a typed decision never counts there";
31703
+ function codexScopeLine(act) {
31704
+ return act === "approve" ? line(`${nothingDone(act)}: ${CODEX_TYPING_NEVER_COUNTS}. To approve, choose Accept in the console.`) : line(`${nothingDone(act)}: ${CODEX_TYPING_NEVER_COUNTS}. The console offers only Accept, so work cannot be sent back from Codex.`);
31705
+ }
31706
+ function codexReviewLine(act) {
31707
+ return act === "accept" ? line(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. To accept, choose Accept in the console.`) : line(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. The console offers only Accept, so this cannot be done from Codex.`);
31708
+ }
31709
+ function codexAutonomyLine() {
31710
+ return line(`${AUTONOMY_NOTHING_CHANGED}: ${CODEX_TYPING_NEVER_COUNTS}. Turn it on or off with "${AUTONOMY_A1_TITLE}" in the console's settings.`);
31711
+ }
30912
31712
 
30913
31713
  // dist/hooks/user-prompt-submit.js
30914
31714
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
30915
31715
  var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
30916
31716
  async function runUserPromptSubmit(opts) {
31717
+ return decideLine(opts);
31718
+ }
31719
+ function promptReplyFor(line2, agent) {
31720
+ if (line2 === "" || agent === "claude-code")
31721
+ return line2;
31722
+ return `${userPromptContextReply(line2.trimEnd())}
31723
+ `;
31724
+ }
31725
+ function whoTyped(opts) {
31726
+ if (agentOf(opts) !== "claude-code")
31727
+ return "cannot-tell";
31728
+ return readSessionAttended(opts.env ?? process.env) ? "person" : "not-attended";
31729
+ }
31730
+ function agentOf(opts) {
31731
+ return opts.agent ?? "claude-code";
31732
+ }
31733
+ async function decideLine(opts) {
30917
31734
  let act = null;
30918
31735
  let autonomyAct = null;
30919
31736
  const review = { recognised: false };
30920
31737
  try {
30921
31738
  const prompt = promptOf(opts.input);
31739
+ if (isAllowReply(prompt))
31740
+ return `${decideAllow(opts)}
31741
+ `;
30922
31742
  const autonomyShape = autonomyPhraseShape(prompt);
30923
31743
  if (autonomyShape !== null) {
30924
31744
  autonomyAct = autonomyShape;
@@ -30948,9 +31768,35 @@ async function runUserPromptSubmit(opts) {
30948
31768
  `;
30949
31769
  }
30950
31770
  }
31771
+ function decideAllow(opts) {
31772
+ const env = opts.env ?? process.env;
31773
+ if ((opts.agent ?? "claude-code") === "claude-code" && !readSessionAttended(env))
31774
+ return allowNotAttendedLine();
31775
+ const outcome = armAllow(sessionIdOf(opts.input), {
31776
+ ...opts.tempDir === void 0 ? {} : { dir: opts.tempDir },
31777
+ ...opts.now === void 0 ? {} : { now: opts.now }
31778
+ });
31779
+ switch (outcome) {
31780
+ case "armed":
31781
+ return allowArmedLine();
31782
+ case "nothing-held":
31783
+ return allowNothingHeldLine();
31784
+ case "several-held":
31785
+ return allowSeveralHeldLine();
31786
+ case "not-saved":
31787
+ return allowNotSavedLine();
31788
+ default: {
31789
+ const unhandled = outcome;
31790
+ return unhandled;
31791
+ }
31792
+ }
31793
+ }
30951
31794
  async function decideAutonomy(act, opts) {
30952
31795
  const env = opts.env ?? process.env;
30953
- if (!readSessionAttended(env))
31796
+ const typed = whoTyped(opts);
31797
+ if (typed === "cannot-tell")
31798
+ return codexAutonomyLine();
31799
+ if (typed === "not-attended")
30954
31800
  return autonomyNotAttendedLine();
30955
31801
  const read = readConfig(env);
30956
31802
  if (read.config === null) {
@@ -30996,12 +31842,18 @@ async function decideReview(shape, opts, progress) {
30996
31842
  if (match2.kind === "nothing")
30997
31843
  return "";
30998
31844
  progress.recognised = true;
30999
- if (!readSessionAttended(env))
31845
+ const typed2 = whoTyped(opts);
31846
+ if (typed2 === "cannot-tell")
31847
+ return codexReviewLine("accept");
31848
+ if (typed2 === "not-attended")
31000
31849
  return reviewNotAttendedLine();
31001
31850
  return checkAndSendReview(match2, read2.config.guardEngagementId, read2.config.guardServiceToken, env, opts);
31002
31851
  }
31003
31852
  progress.recognised = true;
31004
- if (!readSessionAttended(env))
31853
+ const typed = whoTyped(opts);
31854
+ if (typed === "cannot-tell")
31855
+ return codexReviewLine(shape.kind === "part" && shape.act === "accept" ? "accept" : "other");
31856
+ if (typed === "not-attended")
31005
31857
  return reviewNotAttendedLine();
31006
31858
  const read = readConfig(env);
31007
31859
  if (read.config === null) {
@@ -31107,7 +31959,10 @@ async function postReview(bulk, entry, request, engagementId, credential, env, o
31107
31959
  async function decide2(shape, opts) {
31108
31960
  const { act } = shape;
31109
31961
  const env = opts.env ?? process.env;
31110
- if (!readSessionAttended(env))
31962
+ const typed = whoTyped(opts);
31963
+ if (typed === "cannot-tell")
31964
+ return codexScopeLine(act);
31965
+ if (typed === "not-attended")
31111
31966
  return notAttendedLine(act);
31112
31967
  const read = readConfig(env);
31113
31968
  if (read.config === null) {
@@ -31140,6 +31995,16 @@ async function decide2(shape, opts) {
31140
31995
  return showFirstLine(act);
31141
31996
  return post(act, entry, request.data, guardEngagementId, guardServiceToken, env, opts.fetchFn);
31142
31997
  }
31998
+ function sessionIdOf(input) {
31999
+ try {
32000
+ const parsed = JSON.parse(input);
32001
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
32002
+ return void 0;
32003
+ return parsed["session_id"];
32004
+ } catch {
32005
+ return void 0;
32006
+ }
32007
+ }
31143
32008
  function promptOf(input) {
31144
32009
  try {
31145
32010
  const parsed = JSON.parse(input);
@@ -31201,17 +32066,17 @@ function runUserPromptFindings(opts) {
31201
32066
  }
31202
32067
 
31203
32068
  // dist/resolve-engagement.js
31204
- import { existsSync as existsSync8, readFileSync as readFileSync24, realpathSync as realpathSync8 } from "node:fs";
31205
- import { join as join18, sep as sep4 } from "node:path";
32069
+ import { existsSync as existsSync8, readFileSync as readFileSync25, realpathSync as realpathSync8 } from "node:fs";
32070
+ import { join as join19, sep as sep5 } from "node:path";
31206
32071
  import { homedir as homedir12 } from "node:os";
31207
32072
  function defaultRegistryPath() {
31208
- return join18(homedir12(), ".halfcycle", "registry.json");
32073
+ return join19(homedir12(), ".halfcycle", "registry.json");
31209
32074
  }
31210
32075
  function readRegistry(registryPath = defaultRegistryPath()) {
31211
32076
  if (!existsSync8(registryPath))
31212
32077
  return {};
31213
32078
  try {
31214
- const parsed = JSON.parse(readFileSync24(registryPath, "utf-8"));
32079
+ const parsed = JSON.parse(readFileSync25(registryPath, "utf-8"));
31215
32080
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
31216
32081
  return {};
31217
32082
  const out = {};
@@ -31247,7 +32112,7 @@ function resolveEngagement(cwd, registry2) {
31247
32112
  function isSelfOrAncestor(ancestor, target) {
31248
32113
  if (ancestor === target)
31249
32114
  return true;
31250
- const withSep = ancestor.endsWith(sep4) ? ancestor : ancestor + sep4;
32115
+ const withSep = ancestor.endsWith(sep5) ? ancestor : ancestor + sep5;
31251
32116
  return target.startsWith(withSep);
31252
32117
  }
31253
32118
  function runResolveEngagement(cwd, registryPath) {
@@ -31259,13 +32124,27 @@ function runResolveEngagement(cwd, registryPath) {
31259
32124
  }
31260
32125
 
31261
32126
  // dist/refresh-credential.js
31262
- import { readFileSync as readFileSync26 } from "node:fs";
31263
- import { resolve as resolve7 } from "node:path";
32127
+ import { readFileSync as readFileSync27 } from "node:fs";
32128
+ import { resolve as resolve9 } from "node:path";
31264
32129
 
31265
32130
  // dist/mcp-headers.js
31266
- import { readFileSync as readFileSync25 } from "node:fs";
32131
+ import { readFileSync as readFileSync26 } from "node:fs";
31267
32132
  import { homedir as homedir13 } from "node:os";
31268
- import { dirname as dirname9, resolve as resolve6 } from "node:path";
32133
+ import { dirname as dirname9, resolve as resolve8 } from "node:path";
32134
+ var URL_FLAG = "--url";
32135
+ function urlFromArguments(rest) {
32136
+ const at = [];
32137
+ for (let i = 0; i < rest.length; i += 1) {
32138
+ if ((rest[i] ?? "").startsWith(URL_FLAG))
32139
+ at.push(i);
32140
+ }
32141
+ if (at.length === 0)
32142
+ return void 0;
32143
+ const only = at[0];
32144
+ if (at.length !== 1 || rest[only] !== URL_FLAG)
32145
+ return "";
32146
+ return rest[only + 1] ?? "";
32147
+ }
31269
32148
  function defaultWriteErr(text) {
31270
32149
  process.stderr.write(text);
31271
32150
  }
@@ -31278,12 +32157,12 @@ function headerLine(bearer, clientVersion) {
31278
32157
  }
31279
32158
  function projectRootFromBinary(binaryPath) {
31280
32159
  const depth = VENDORED_BIN_REL.split("/").length - 1;
31281
- return resolve6(dirname9(binaryPath), ...Array(depth).fill(".."));
32160
+ return resolve8(dirname9(binaryPath), ...Array(depth).fill(".."));
31282
32161
  }
31283
32162
  function readPinnedVersion(root) {
31284
32163
  let raw;
31285
32164
  try {
31286
- raw = readFileSync25(resolve6(root, BUNDLE_PIN_REL), "utf-8");
32165
+ raw = readFileSync26(resolve8(root, BUNDLE_PIN_REL), "utf-8");
31287
32166
  } catch {
31288
32167
  return void 0;
31289
32168
  }
@@ -31325,7 +32204,7 @@ function sentences(lines2) {
31325
32204
  }
31326
32205
  function badIdSentence(root) {
31327
32206
  return sentences([
31328
- `${resolve6(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
32207
+ `${resolve8(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
31329
32208
  'so no credential was read. Delete that file and run "npx halfcycle" again: this repository',
31330
32209
  "is then set up as a new Halfcycle project."
31331
32210
  ]);
@@ -31339,7 +32218,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
31339
32218
  ]);
31340
32219
  case "no-id":
31341
32220
  return sentences([
31342
- `${resolve6(root, BUNDLE_PIN_REL)} names no engagement id.`,
32221
+ `${resolve8(root, BUNDLE_PIN_REL)} names no engagement id.`,
31343
32222
  'run "npx halfcycle" here to rewrite it.'
31344
32223
  ]);
31345
32224
  case "bad-id":
@@ -31356,7 +32235,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
31356
32235
  case "no-credential":
31357
32236
  return sentences([
31358
32237
  `this machine holds no credential for engagement ${engagementId ?? ""}.`,
31359
- `run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve6(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
32238
+ `run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve8(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
31360
32239
  ]);
31361
32240
  }
31362
32241
  }
@@ -31418,13 +32297,14 @@ async function chooseBearer(opts) {
31418
32297
  }
31419
32298
  return NO_CREDENTIAL_PLACEHOLDER;
31420
32299
  }
32300
+ const registrationFile = opts.registrationFile ?? MCP_REGISTRATION_REL;
31421
32301
  const want = urlOrigin(recorded);
31422
32302
  const got = requested === void 0 ? void 0 : urlOrigin(requested);
31423
32303
  if (want === void 0 || got !== want) {
31424
32304
  writeErr(sentences([
31425
- `.mcp.json asks for this repository's Halfcycle credential to be sent to ${shownUrl(requested ?? "")},`,
32305
+ `${registrationFile} asks for this repository's Halfcycle credential to be sent to ${shownUrl(requested ?? "")},`,
31426
32306
  `which is not this machine's Halfcycle server (${want ?? "none recorded"}). The credential was NOT sent.`,
31427
- 'if nobody meant to change .mcp.json, treat that change as suspect; "npx halfcycle" restores the entry.'
32307
+ `if nobody meant to change ${registrationFile}, treat that change as suspect; "npx halfcycle" restores the entry.`
31428
32308
  ]));
31429
32309
  return NO_CREDENTIAL_PLACEHOLDER;
31430
32310
  }
@@ -31444,13 +32324,18 @@ async function chooseBearer(opts) {
31444
32324
  async function mcpHeaders(opts) {
31445
32325
  const writeErr = opts.writeErr ?? defaultWriteErr;
31446
32326
  const root = projectRootFromBinary(opts.binaryPath);
32327
+ const variableUrl = readMcpServerUrl();
31447
32328
  try {
31448
32329
  return headerLine(await chooseBearer({
31449
- ...opts,
32330
+ binaryPath: opts.binaryPath,
32331
+ home: opts.home,
32332
+ fetchFn: opts.fetchFn,
32333
+ deadlineMs: opts.deadlineMs,
31450
32334
  writeErr,
31451
- requestedUrl: readMcpServerUrl(),
32335
+ requestedUrl: variableUrl ?? opts.url,
31452
32336
  renewal: "renew-here",
31453
- environmentTokenFirst: true
32337
+ environmentTokenFirst: true,
32338
+ registrationFile: variableUrl === void 0 && opts.url !== void 0 ? AGENT_PROFILES.codex.mcpRegistration.file : void 0
31454
32339
  }), readPinnedVersion(root));
31455
32340
  } catch (err) {
31456
32341
  try {
@@ -31464,6 +32349,25 @@ async function mcpHeaders(opts) {
31464
32349
  return headerLine(NO_CREDENTIAL_PLACEHOLDER, readPinnedVersion(root));
31465
32350
  }
31466
32351
  }
32352
+ async function headersVerbLine(binaryPath, rest, run = mcpHeaders, writeErr = defaultWriteErr) {
32353
+ const url2 = urlFromArguments(rest);
32354
+ try {
32355
+ return await run({ binaryPath, url: url2 });
32356
+ } catch (err) {
32357
+ if (url2 === void 0)
32358
+ throw err;
32359
+ const fallback = headerLine(NO_CREDENTIAL_PLACEHOLDER, readPinnedVersion(projectRootFromBinary(binaryPath)));
32360
+ try {
32361
+ writeErr(sentences([
32362
+ `the credential could not be read: ${err instanceof Error ? err.message : String(err)}`,
32363
+ `run "npx halfcycle" in this repository to rewrite this machine's credential.`
32364
+ ]));
32365
+ } catch {
32366
+ return fallback;
32367
+ }
32368
+ return fallback;
32369
+ }
32370
+ }
31467
32371
 
31468
32372
  // dist/refresh-credential.js
31469
32373
  var UNAUTHORIZED2 = 401;
@@ -31504,13 +32408,19 @@ async function refreshCredentialToken(opts) {
31504
32408
  return { token: refreshed.token };
31505
32409
  }
31506
32410
  var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
32411
+ function isCheckedSessionStart(input, profile) {
32412
+ const source = input["source"];
32413
+ if (source === void 0 && profile.id === "codex")
32414
+ return true;
32415
+ return CHECKED_SOURCES.has(source);
32416
+ }
31507
32417
  var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If its tools are missing, turn on ${MCP_SERVER_KEY} in /mcp; if it is not listed, run "claude mcp reset-project-choices" here and restart Claude Code.`;
31508
32418
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
31509
32419
  var SESSION_START_PROBE_ATTEMPTS = 2;
31510
32420
  function registeredMcpUrl(projectRoot) {
31511
32421
  let parsed;
31512
32422
  try {
31513
- parsed = JSON.parse(readFileSync26(resolve7(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
32423
+ parsed = JSON.parse(readFileSync27(resolve9(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
31514
32424
  } catch {
31515
32425
  return void 0;
31516
32426
  }
@@ -31682,6 +32592,7 @@ function defaultWriteErr2(text) {
31682
32592
  }
31683
32593
  async function runRefreshCredential(opts) {
31684
32594
  const writeErr = opts.writeErr ?? defaultWriteErr2;
32595
+ const profile = opts.profile ?? AGENT_PROFILES["claude-code"];
31685
32596
  const startedAt = Date.now();
31686
32597
  try {
31687
32598
  const renewalSaid = [];
@@ -31695,7 +32606,7 @@ async function runRefreshCredential(opts) {
31695
32606
  ]);
31696
32607
  renewalOpen = false;
31697
32608
  const parts = [];
31698
- if (CHECKED_SOURCES.has(input["source"])) {
32609
+ if (isCheckedSessionStart(input, profile)) {
31699
32610
  const connection = await checkConnection(renewedToken, { ...opts, writeErr });
31700
32611
  if (connection !== void 0) {
31701
32612
  parts.push(connection.sentence);
@@ -31717,7 +32628,7 @@ async function runRefreshCredential(opts) {
31717
32628
  }
31718
32629
  }
31719
32630
  }
31720
- if (CHECKED_SOURCES.has(input["source"])) {
32631
+ if (isCheckedSessionStart(input, profile)) {
31721
32632
  const config2 = readConfig().config;
31722
32633
  if (config2 !== null) {
31723
32634
  const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
@@ -31757,7 +32668,7 @@ async function runRefreshCredential(opts) {
31757
32668
  import { fileURLToPath } from "node:url";
31758
32669
 
31759
32670
  // dist/capture.js
31760
- import { readFileSync as readFileSync27, writeFileSync as writeFileSync15, mkdirSync as mkdirSync10, existsSync as existsSync9 } from "node:fs";
32671
+ import { readFileSync as readFileSync28, writeFileSync as writeFileSync16, mkdirSync as mkdirSync10, existsSync as existsSync9 } from "node:fs";
31761
32672
  import { dirname as dirname10 } from "node:path";
31762
32673
  function runCapture(provider, endpoint, opts = {}) {
31763
32674
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -31794,7 +32705,7 @@ function runCapture(provider, endpoint, opts = {}) {
31794
32705
  const manifestFile = manifestPath(repoRoot);
31795
32706
  try {
31796
32707
  mkdirSync10(dirname10(fixture), { recursive: true });
31797
- writeFileSync15(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
32708
+ writeFileSync16(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
31798
32709
  } catch (err) {
31799
32710
  const msg = err instanceof Error ? err.message : String(err);
31800
32711
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -31825,7 +32736,7 @@ function readInputFile(fromFile) {
31825
32736
  if (!fromFile)
31826
32737
  return void 0;
31827
32738
  try {
31828
- return readFileSync27(fromFile, "utf-8");
32739
+ return readFileSync28(fromFile, "utf-8");
31829
32740
  } catch {
31830
32741
  return void 0;
31831
32742
  }
@@ -31837,10 +32748,10 @@ function relativeFixturePath(repoRoot, fixture) {
31837
32748
 
31838
32749
  // dist/prove.js
31839
32750
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
31840
- import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
31841
- import { createWriteStream, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync28, renameSync as renameSync13, unlinkSync as unlinkSync11, writeFileSync as writeFileSync16 } from "node:fs";
32751
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
32752
+ import { createWriteStream, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync29, renameSync as renameSync14, unlinkSync as unlinkSync12, writeFileSync as writeFileSync17 } from "node:fs";
31842
32753
  import { homedir as homedir14 } from "node:os";
31843
- import { dirname as dirname11, join as join19 } from "node:path";
32754
+ import { dirname as dirname11, join as join20 } from "node:path";
31844
32755
 
31845
32756
  // dist/test-counts.js
31846
32757
  function sum(values) {
@@ -31949,10 +32860,10 @@ function isProofTestPath(path) {
31949
32860
  function testRunsDir(engagementId, home) {
31950
32861
  if (!isEngagementId(engagementId))
31951
32862
  return null;
31952
- return join19(home ?? homedir14(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, TEST_RUNS_DIR_NAME);
32863
+ return join20(home ?? homedir14(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, TEST_RUNS_DIR_NAME);
31953
32864
  }
31954
32865
  function commandIdOf(command) {
31955
- return createHash5("sha256").update(command, "utf8").digest("hex").slice(0, 16);
32866
+ return createHash6("sha256").update(command, "utf8").digest("hex").slice(0, 16);
31956
32867
  }
31957
32868
  var PLAIN_WORD = /^[A-Za-z0-9_./:=@%+,-]+$/;
31958
32869
  function commandFromWords(words, platform4 = process.platform) {
@@ -32037,19 +32948,19 @@ function parseProveArgs(args2) {
32037
32948
  function writeJsonFile2(path, value) {
32038
32949
  mkdirSync11(dirname11(path), { recursive: true, mode: 448 });
32039
32950
  const tmp = `${path}.${process.pid}.${randomUUID3()}.tmp`;
32040
- writeFileSync16(tmp, JSON.stringify(value), { mode: 384 });
32041
- renameSync13(tmp, path);
32951
+ writeFileSync17(tmp, JSON.stringify(value), { mode: 384 });
32952
+ renameSync14(tmp, path);
32042
32953
  }
32043
32954
  function subjectKey(subject) {
32044
32955
  return subject.kind === "task" ? ["task", subject.phase, subject.task] : ["change", subject.change];
32045
32956
  }
32046
32957
  function keptCheckPath(dir, subject, label) {
32047
- const key = createHash5("sha256").update(JSON.stringify([...subjectKey(subject), label]), "utf8").digest("hex");
32048
- return join19(dir, COMMANDS_DIR_NAME, `${key.slice(0, 32)}.json`);
32958
+ const key = createHash6("sha256").update(JSON.stringify([...subjectKey(subject), label]), "utf8").digest("hex");
32959
+ return join20(dir, COMMANDS_DIR_NAME, `${key.slice(0, 32)}.json`);
32049
32960
  }
32050
32961
  function readKeptCheck(dir, subject, label) {
32051
32962
  try {
32052
- const raw = JSON.parse(readFileSync28(keptCheckPath(dir, subject, label), "utf-8"));
32963
+ const raw = JSON.parse(readFileSync29(keptCheckPath(dir, subject, label), "utf-8"));
32053
32964
  if (typeof raw.command !== "string" || raw.command === "" || typeof raw.commandId !== "string")
32054
32965
  return null;
32055
32966
  const b = raw.before;
@@ -32060,21 +32971,21 @@ function readKeptCheck(dir, subject, label) {
32060
32971
  }
32061
32972
  }
32062
32973
  function queuePath(dir, runId) {
32063
- return join19(dir, QUEUE_DIR_NAME, `${runId}.json`);
32974
+ return join20(dir, QUEUE_DIR_NAME, `${runId}.json`);
32064
32975
  }
32065
32976
  function readQueue(dir) {
32066
32977
  let names;
32067
32978
  try {
32068
- names = readdirSync10(join19(dir, QUEUE_DIR_NAME)).filter((n) => n.endsWith(".json"));
32979
+ names = readdirSync10(join20(dir, QUEUE_DIR_NAME)).filter((n) => n.endsWith(".json"));
32069
32980
  } catch {
32070
32981
  return [];
32071
32982
  }
32072
32983
  const reports = [];
32073
32984
  for (const name of names) {
32074
- const path = join19(dir, QUEUE_DIR_NAME, name);
32985
+ const path = join20(dir, QUEUE_DIR_NAME, name);
32075
32986
  let parsed = null;
32076
32987
  try {
32077
- parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync28(path, "utf-8")));
32988
+ parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync29(path, "utf-8")));
32078
32989
  } catch {
32079
32990
  parsed = null;
32080
32991
  }
@@ -32087,7 +32998,7 @@ function readQueue(dir) {
32087
32998
  }
32088
32999
  function removeFile(path) {
32089
33000
  try {
32090
- unlinkSync11(path);
33001
+ unlinkSync12(path);
32091
33002
  } catch {
32092
33003
  }
32093
33004
  }
@@ -32154,7 +33065,7 @@ function runCommand(command, repoRoot, logPath, env, deps) {
32154
33065
  mkdirSync11(dirname11(logPath), { recursive: true, mode: 448 });
32155
33066
  const log = createWriteStream(logPath, { mode: 384 });
32156
33067
  const began = process.hrtime.bigint();
32157
- return new Promise((resolve8) => {
33068
+ return new Promise((resolve10) => {
32158
33069
  let timedOut = false;
32159
33070
  let started = true;
32160
33071
  let settled = false;
@@ -32189,7 +33100,7 @@ function runCommand(command, repoRoot, logPath, env, deps) {
32189
33100
  process.off("SIGINT", onSignal);
32190
33101
  process.off("SIGTERM", onSignal);
32191
33102
  const durationMs = Number((process.hrtime.bigint() - began) / 1000000n);
32192
- log.end(() => resolve8({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
33103
+ log.end(() => resolve10({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
32193
33104
  };
32194
33105
  child.on("error", (e) => {
32195
33106
  started = false;
@@ -32243,7 +33154,7 @@ function checkCommand(command, reason, repoRoot, env, home) {
32243
33154
  ...home === void 0 ? {} : { home }
32244
33155
  }));
32245
33156
  } catch {
32246
- return { silent: false, question: couldNotTellQuestion(reason ?? null), staked: [] };
33157
+ return { silent: false, question: couldNotTellQuestion(reason ?? null), refusal: couldNotTellRefusal(reason ?? null), staked: [] };
32247
33158
  }
32248
33159
  }
32249
33160
  async function runProve(args2, deps = {}) {
@@ -32311,13 +33222,13 @@ ${PROVE_USAGE}`);
32311
33222
  if (request.when === "before" && changedOutsideTests > 0)
32312
33223
  say(lateBeforeLine(changedOutsideTests));
32313
33224
  const runId = randomUUID3();
32314
- const logPath = join19(dir, `${runId}.log`);
33225
+ const logPath = join20(dir, `${runId}.log`);
32315
33226
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
32316
33227
  const deadline = deps.deadlineMs ?? PROVE_RUN_DEADLINE_MS;
32317
33228
  const end = await runCommand(command, repoRoot, logPath, env, deps);
32318
33229
  let output = "";
32319
33230
  try {
32320
- output = readFileSync28(logPath, "utf-8");
33231
+ output = readFileSync29(logPath, "utf-8");
32321
33232
  } catch {
32322
33233
  output = "";
32323
33234
  }
@@ -32367,7 +33278,7 @@ ${PROVE_USAGE}`);
32367
33278
 
32368
33279
  // dist/statusline.js
32369
33280
  import { spawn as spawn3 } from "node:child_process";
32370
- import { readFileSync as readFileSync29 } from "node:fs";
33281
+ import { readFileSync as readFileSync30 } from "node:fs";
32371
33282
  import nodePath5 from "node:path";
32372
33283
  var OWN_STATUS_LINE_DEADLINE_MS = 5e3;
32373
33284
  var PERSON_SETTINGS_FILENAME = "settings.json";
@@ -32405,7 +33316,7 @@ function halfcycleStatusLineOf(figures) {
32405
33316
  }
32406
33317
  function personStatusLineCommand(configFolder) {
32407
33318
  try {
32408
- const v = JSON.parse(readFileSync29(nodePath5.join(configFolder, PERSON_SETTINGS_FILENAME), "utf-8"));
33319
+ const v = JSON.parse(readFileSync30(nodePath5.join(configFolder, PERSON_SETTINGS_FILENAME), "utf-8"));
32409
33320
  const line2 = v["statusLine"];
32410
33321
  if (typeof line2 !== "object" || line2 === null)
32411
33322
  return null;
@@ -32429,14 +33340,14 @@ function personCommandEnv(env) {
32429
33340
  }
32430
33341
  return out;
32431
33342
  }
32432
- var runOwnCommandReal = (command, input, env) => new Promise((resolve8) => {
33343
+ var runOwnCommandReal = (command, input, env) => new Promise((resolve10) => {
32433
33344
  let out = "";
32434
33345
  let settled = false;
32435
33346
  const done = (v) => {
32436
33347
  if (settled)
32437
33348
  return;
32438
33349
  settled = true;
32439
- resolve8(v);
33350
+ resolve10(v);
32440
33351
  };
32441
33352
  try {
32442
33353
  const child = spawn3(command, {
@@ -32546,7 +33457,12 @@ async function main() {
32546
33457
  return;
32547
33458
  }
32548
33459
  if (cmd === "refresh-credential") {
32549
- const reply = await runRefreshCredential({ binaryPath: fileURLToPath(import.meta.url) });
33460
+ const profile = agentFromArguments(args.slice(1));
33461
+ if (profile === null) {
33462
+ unknownCommand();
33463
+ return;
33464
+ }
33465
+ const reply = await runRefreshCredential({ binaryPath: fileURLToPath(import.meta.url), profile });
32550
33466
  process.stdout.write(reply, () => process.exit(0));
32551
33467
  return;
32552
33468
  }
@@ -32631,7 +33547,7 @@ async function main() {
32631
33547
  return;
32632
33548
  }
32633
33549
  if (cmd === "mcp-headers") {
32634
- const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
33550
+ const line2 = await headersVerbLine(fileURLToPath(import.meta.url), args.slice(1));
32635
33551
  process.stdout.write(line2, () => process.exit(0));
32636
33552
  return;
32637
33553
  }
@@ -32673,25 +33589,30 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32673
33589
  return;
32674
33590
  }
32675
33591
  if (cmd === "hook") {
33592
+ const profile = agentFromArguments(args.slice(2));
33593
+ if (profile === null) {
33594
+ unknownCommand();
33595
+ return;
33596
+ }
32676
33597
  if (sub === "post-tool-use") {
32677
- await runPostToolUse();
32678
- process.exit(process.exitCode ?? 0);
33598
+ await runPostToolUse(void 0, profile);
33599
+ process.exit(hookExitCode(profile));
32679
33600
  return;
32680
33601
  }
32681
33602
  if (sub === "stop") {
32682
- await runStop();
32683
- process.exit(process.exitCode ?? 0);
33603
+ await runStop(void 0, void 0, void 0, profile);
33604
+ process.exit(hookExitCode(profile));
32684
33605
  return;
32685
33606
  }
32686
33607
  if (sub === "subagent-stop") {
32687
- await runSubagentStop();
32688
- process.exit(process.exitCode ?? 0);
33608
+ await runSubagentStop(profile);
33609
+ process.exit(hookExitCode(profile));
32689
33610
  return;
32690
33611
  }
32691
33612
  if (sub === "pre-tool-use") {
32692
33613
  let reply = "";
32693
33614
  try {
32694
- reply = runPreToolUse({ input: await readStdin2() });
33615
+ reply = runPreToolUse({ input: await readStdin2(), profile });
32695
33616
  } catch {
32696
33617
  reply = "";
32697
33618
  }
@@ -32701,7 +33622,7 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32701
33622
  if (sub === "user-prompt-submit") {
32702
33623
  let reply = "";
32703
33624
  try {
32704
- reply = await runUserPromptSubmit({ input: await readStdin2() });
33625
+ reply = promptReplyFor(await runUserPromptSubmit({ input: await readStdin2(), agent: profile.id }), profile.id);
32705
33626
  } catch {
32706
33627
  reply = "";
32707
33628
  }
@@ -32724,17 +33645,20 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32724
33645
  process.exit(1);
32725
33646
  return;
32726
33647
  }
32727
- process.stderr.write(`halfcycle-runner: unknown command "${cmd ?? ""}"
33648
+ unknownCommand(cmd ?? "");
33649
+ }
33650
+ function unknownCommand(name = args.join(" ")) {
33651
+ process.stderr.write(`halfcycle-runner: unknown command "${name}"
32728
33652
  Usage: halfcycle-runner ci
32729
- halfcycle-runner hook post-tool-use
32730
- halfcycle-runner hook pre-tool-use
32731
- halfcycle-runner hook stop
32732
- halfcycle-runner hook subagent-stop
32733
- halfcycle-runner hook user-prompt-submit
32734
- halfcycle-runner hook user-prompt-findings
32735
- halfcycle-runner refresh-credential
33653
+ halfcycle-runner hook post-tool-use [--agent <id>]
33654
+ halfcycle-runner hook pre-tool-use [--agent <id>]
33655
+ halfcycle-runner hook stop [--agent <id>]
33656
+ halfcycle-runner hook subagent-stop [--agent <id>]
33657
+ halfcycle-runner hook user-prompt-submit [--agent <id>]
33658
+ halfcycle-runner hook user-prompt-findings [--agent <id>]
33659
+ halfcycle-runner refresh-credential [--agent <id>]
32736
33660
  halfcycle-runner check-connection
32737
- halfcycle-runner mcp-headers
33661
+ halfcycle-runner mcp-headers [--url <address>]
32738
33662
  halfcycle-runner observe
32739
33663
  halfcycle-runner observe --change <sortingId>
32740
33664
  halfcycle-runner time
@@ -32748,16 +33672,20 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32748
33672
  process.exit(1);
32749
33673
  }
32750
33674
  function readStdin3() {
32751
- return new Promise((resolve8, reject) => {
33675
+ return new Promise((resolve10, reject) => {
32752
33676
  const chunks = [];
32753
33677
  process.stdin.on("data", (chunk) => chunks.push(chunk));
32754
- process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
33678
+ process.stdin.on("end", () => resolve10(Buffer.concat(chunks).toString("utf-8")));
32755
33679
  process.stdin.on("error", reject);
32756
33680
  });
32757
33681
  }
32758
33682
  main().catch((err) => {
32759
- process.stderr.write(`halfcycle-runner: unexpected error: ${err instanceof Error ? err.message : String(err)}
32760
- Contact hello@halfcycle.ai if this keeps happening.
32761
- `);
32762
- process.exit(1);
33683
+ let reply = "";
33684
+ const code = answerUnexpectedError(args, err, {
33685
+ stdout: (text) => {
33686
+ reply += text;
33687
+ },
33688
+ stderr: (text) => process.stderr.write(text)
33689
+ });
33690
+ process.stdout.write(reply, () => process.exit(code));
32763
33691
  });