halfcycle 0.4.17 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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",
@@ -18295,7 +18401,8 @@ var chatReviewAcceptRequestSchema = external_exports.object({
18295
18401
  act: external_exports.literal("accept"),
18296
18402
  reference: phaseIdentitySchema,
18297
18403
  reviewVersion: external_exports.number().int().min(0),
18298
- parts: external_exports.array(systemPartIdSchema).max(REVIEW_PARTS_MAX)
18404
+ parts: external_exports.array(systemPartIdSchema).max(REVIEW_PARTS_MAX),
18405
+ waitingClaims: external_exports.literal(true).optional()
18299
18406
  }).strict();
18300
18407
  function notedRequest(act) {
18301
18408
  return external_exports.object({
@@ -18310,12 +18417,20 @@ var chatReviewQuestionRequestSchema = notedRequest("question");
18310
18417
  var chatReviewSendBackRequestSchema = notedRequest("send-back");
18311
18418
  var chatReviewCarryRequestSchema = notedRequest("carry");
18312
18419
  var chatReviewDropRequestSchema = notedRequest("drop");
18420
+ var chatReviewRejectRequestSchema = external_exports.object({
18421
+ act: external_exports.literal("reject"),
18422
+ reference: phaseIdentitySchema,
18423
+ reviewVersion: external_exports.number().int().min(0),
18424
+ task: buildTaskIdSchema,
18425
+ note: reviewNoteWireSchema
18426
+ }).strict();
18313
18427
  var chatReviewRequestSchema = external_exports.discriminatedUnion("act", [
18314
18428
  chatReviewAcceptRequestSchema,
18315
18429
  chatReviewQuestionRequestSchema,
18316
18430
  chatReviewSendBackRequestSchema,
18317
18431
  chatReviewCarryRequestSchema,
18318
- chatReviewDropRequestSchema
18432
+ chatReviewDropRequestSchema,
18433
+ chatReviewRejectRequestSchema
18319
18434
  ]);
18320
18435
  function bareOutcome2(outcome) {
18321
18436
  return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
@@ -18328,7 +18443,8 @@ var chatReviewResultSchema = external_exports.discriminatedUnion("outcome", [
18328
18443
  ...reviewDecisionResultSchema.options.filter(notTheConsoleNotDecidable),
18329
18444
  bareOutcome2("show-first"),
18330
18445
  bareOutcome2("not-offered"),
18331
- chatAmbiguousSchema
18446
+ chatAmbiguousSchema,
18447
+ bareOutcome2("nothing-waiting")
18332
18448
  ]);
18333
18449
  var reviewAnswerRequestSchema = external_exports.object({
18334
18450
  part: systemPartIdSchema,
@@ -18733,6 +18849,7 @@ var savedSchema = external_exports.object({
18733
18849
  branches: external_exports.array(savedBranchSchema)
18734
18850
  // every saved line of work
18735
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";
18736
18853
  var AUTONOMY_A4 = 'Builds in this project now run on their own. Turn it off with "autonomy off", or in the console.';
18737
18854
  var AUTONOMY_A5 = "Builds in this project wait for you again.";
18738
18855
  function withoutFinalStop(text) {
@@ -18760,6 +18877,28 @@ var AUTONOMY_A18 = "Halfcycle cannot check PowerShell commands while builds run
18760
18877
 
18761
18878
  // dist/hook-protocol.js
18762
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
18763
18902
  function emitBlock(reason, systemMessage) {
18764
18903
  const body = systemMessage === void 0 ? { decision: "block", reason } : { decision: "block", reason, systemMessage };
18765
18904
  process.stdout.write(JSON.stringify(body) + "\n");
@@ -18773,12 +18912,8 @@ function preToolUseReplyText(answer, text = "", agentLine) {
18773
18912
  hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: text }
18774
18913
  });
18775
18914
  }
18776
- const forAgent = agentLine === void 0 || agentLine === "" ? text : `${text}
18777
- ${agentLine}`;
18778
- return JSON.stringify({
18779
- systemMessage: text,
18780
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: forAgent }
18781
- });
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 });
18782
18917
  }
18783
18918
  function preToolUseNoticeText(text) {
18784
18919
  return JSON.stringify({ systemMessage: text });
@@ -18798,13 +18933,26 @@ function nonBlockingReply(event, text) {
18798
18933
  }
18799
18934
  return JSON.stringify({ systemMessage: text });
18800
18935
  }
18801
- function hookReply(event) {
18936
+ function hookReply(event, profile = AGENT_PROFILES["claude-code"]) {
18802
18937
  const queued = [];
18938
+ const forPerson = [];
18803
18939
  const forAgent = [];
18940
+ const jsonOnly = repliesInJsonOnly(profile);
18804
18941
  return {
18805
18942
  notice(text) {
18806
18943
  queued.push(text);
18807
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
+ },
18808
18956
  agentContext(text) {
18809
18957
  forAgent.push(text);
18810
18958
  },
@@ -18814,11 +18962,23 @@ function hookReply(event) {
18814
18962
  },
18815
18963
  flush() {
18816
18964
  const text = queued.splice(0).join("\n");
18965
+ const person = forPerson.splice(0).join("\n");
18817
18966
  const context = event === "PostToolUse" ? forAgent.splice(0).join("\n") : "";
18818
18967
  const code = process.exitCode;
18819
18968
  if (code !== void 0 && code !== null && Number(code) !== 0) {
18820
- if (text !== "")
18969
+ if (text === "")
18970
+ return;
18971
+ if (jsonOnly)
18972
+ process.stdout.write(`${jsonOnlyLoudReply(event, text)}
18973
+ `);
18974
+ else
18821
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}`)}
18822
18982
  `);
18823
18983
  return;
18824
18984
  }
@@ -18834,6 +18994,60 @@ function hookReply(event) {
18834
18994
  }
18835
18995
  };
18836
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
+ };
18837
19051
  function postToolUseReply(notice, context) {
18838
19052
  const additionalContext = notice === "" ? context : `${notice}
18839
19053
  ${context}`;
@@ -18845,6 +19059,9 @@ function userPromptContextReply(context) {
18845
19059
  function infraWarningText() {
18846
19060
  return "[Halfcycle] WARNING: Halfcycle's check service could not be reached, so nothing was checked this time. The session continues without checks.";
18847
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
+ }
18848
19065
  function localFaultText(message) {
18849
19066
  return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
18850
19067
  }
@@ -18857,8 +19074,13 @@ This is not a service outage: the guard service answered. The suspect is this re
18857
19074
  }
18858
19075
  var UNCONFIGURED_EXIT_TOOL_USE = 2;
18859
19076
  var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
18860
- function emitNeverConfigured(exitCode, reason, engagementId) {
18861
- 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);
18862
19084
  process.exitCode = exitCode;
18863
19085
  }
18864
19086
  function namesTheSignInCommand(text) {
@@ -18937,8 +19159,13 @@ This is not a service outage. The guard service is up and replying; ${cause}, so
18937
19159
  ${remedy}
18938
19160
  `;
18939
19161
  }
18940
- function emitCredentialRejected(statusCode, serviceMessage, exitCode, remedy = HOOK_REMEDY) {
18941
- 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);
18942
19169
  process.exitCode = exitCode;
18943
19170
  }
18944
19171
  function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
@@ -21158,6 +21385,18 @@ function findingsPath(sessionId) {
21158
21385
  return null;
21159
21386
  return join6(markerDir(), `${FINDINGS_PREFIX}${sessionId}${FINDINGS_SUFFIX}`);
21160
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
+ }
21161
21400
  function markerDir() {
21162
21401
  const raw = process.env["TMPDIR"] || tmpdir();
21163
21402
  return raw.replace(/\/+$/, "");
@@ -21314,7 +21553,9 @@ function sweepSessionFiles(keepSessionId, now) {
21314
21553
  }
21315
21554
  const kinds = [
21316
21555
  [FINDINGS_PREFIX, FINDINGS_SUFFIX],
21317
- [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]
21318
21559
  ];
21319
21560
  for (const name of names) {
21320
21561
  for (const [prefix, suffix] of kinds) {
@@ -21418,9 +21659,9 @@ function removeLivenessIndexEntry(dir, sessionId, name) {
21418
21659
  import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync2 } from "node:fs";
21419
21660
  import { homedir as homedir3 } from "node:os";
21420
21661
  import { join as join7, relative as relative3, resolve as resolve2 } from "node:path";
21421
- function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
21662
+ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration, profile = AGENT_PROFILES["claude-code"]) {
21422
21663
  try {
21423
- const facts = read(repoRoot);
21664
+ const facts = read(repoRoot, profile);
21424
21665
  if (facts === void 0)
21425
21666
  return void 0;
21426
21667
  const parsed = seatConfigurationSchema.safeParse(facts);
@@ -21429,8 +21670,8 @@ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
21429
21670
  return void 0;
21430
21671
  }
21431
21672
  }
21432
- function readSeatConfiguration(repoRoot) {
21433
- 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());
21434
21675
  const projectDir = realpathSync2.native(given);
21435
21676
  const home = homedir3();
21436
21677
  let realHome = home;
@@ -21442,14 +21683,15 @@ function readSeatConfiguration(repoRoot) {
21442
21683
  if (projectDir === home || given === home || projectDir === realHome)
21443
21684
  return void 0;
21444
21685
  return {
21445
- settingsFiles: [readSettingsFile(projectDir, SETTINGS_REL), readSettingsFile(projectDir, LOCAL_SETTINGS_REL)],
21446
- 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
21447
21689
  };
21448
21690
  }
21449
21691
  function isPlainObject3(value) {
21450
21692
  return typeof value === "object" && value !== null && !Array.isArray(value);
21451
21693
  }
21452
- function readSettingsFile(projectDir, rel) {
21694
+ function readSettingsFile(projectDir, rel, ours) {
21453
21695
  let raw;
21454
21696
  try {
21455
21697
  raw = readFileSync8(join7(projectDir, rel), "utf-8");
@@ -21466,9 +21708,9 @@ function readSettingsFile(projectDir, rel) {
21466
21708
  }
21467
21709
  if (!isPlainObject3(parsed))
21468
21710
  return { path: rel, state: "unreadable", hooks: [] };
21469
- return { path: rel, state: "read", hooks: ourHooks(parsed["hooks"]) };
21711
+ return { path: rel, state: "read", hooks: ourHooks(parsed["hooks"], ours) };
21470
21712
  }
21471
- function ourHooks(hooks) {
21713
+ function ourHooks(hooks, ours) {
21472
21714
  if (!isPlainObject3(hooks))
21473
21715
  return [];
21474
21716
  const rows = [];
@@ -21492,7 +21734,7 @@ function ourHooks(hooks) {
21492
21734
  if (!isPlainObject3(hook))
21493
21735
  continue;
21494
21736
  const command = hook["command"];
21495
- if (typeof command !== "string" || !HALFCYCLE_HOOK_COMMANDS.has(command))
21737
+ if (typeof command !== "string" || !ours.has(command))
21496
21738
  continue;
21497
21739
  let timeout = null;
21498
21740
  if (Object.hasOwn(hook, "timeout")) {
@@ -21511,8 +21753,8 @@ function ourHooks(hooks) {
21511
21753
  }
21512
21754
  return rows;
21513
21755
  }
21514
- function readPathStates(projectDir, repoRoot) {
21515
- 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" }));
21516
21758
  let projectTop = projectDir;
21517
21759
  if (repoRoot !== projectDir) {
21518
21760
  const top = getRepoRoot(projectDir);
@@ -21521,10 +21763,10 @@ function readPathStates(projectDir, repoRoot) {
21521
21763
  projectTop = top.value;
21522
21764
  }
21523
21765
  const prefix = relative3(projectTop, projectDir);
21524
- const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
21766
+ const status = getWiringStatus(wiringPaths, projectDir);
21525
21767
  if (!status.ok)
21526
21768
  return unknown2();
21527
- 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)));
21528
21770
  return mapped ?? unknown2();
21529
21771
  }
21530
21772
  function mapWiringStatus(output, prefix, paths, onDisk) {
@@ -22243,8 +22485,8 @@ function separatorOf(path) {
22243
22485
  function subagentsFolderOf(transcript) {
22244
22486
  if (!transcript.endsWith(".jsonl") || transcript.length === ".jsonl".length)
22245
22487
  return null;
22246
- const sep5 = separatorOf(transcript);
22247
- return `${transcript.slice(0, -".jsonl".length)}${sep5}subagents`;
22488
+ const sep6 = separatorOf(transcript);
22489
+ return `${transcript.slice(0, -".jsonl".length)}${sep6}subagents`;
22248
22490
  }
22249
22491
  function subagentRecordName(agentId) {
22250
22492
  return /^[A-Za-z0-9_-]+$/.test(agentId) ? `agent-${agentId}.jsonl` : null;
@@ -22784,7 +23026,7 @@ async function pruneTimeLog(path, openedAt) {
22784
23026
  written = true;
22785
23027
  let seen = Buffer.byteLength(text, "utf-8");
22786
23028
  for (; ; ) {
22787
- await new Promise((resolve8) => setTimeout(resolve8, PRUNE_SETTLE_MS));
23029
+ await new Promise((resolve10) => setTimeout(resolve10, PRUNE_SETTLE_MS));
22788
23030
  const after = readFileSync11(aside);
22789
23031
  if (after.length <= seen)
22790
23032
  break;
@@ -23419,344 +23661,109 @@ function turnEndMessage(news) {
23419
23661
  return parts.length === 0 ? null : parts.join("\n");
23420
23662
  }
23421
23663
 
23422
- // dist/hooks/post-tool-use.js
23423
- var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
23424
- async function runPostToolUse(readSeat = readSeatConfiguration) {
23425
- const reply = hookReply("PostToolUse");
23426
- const at = { session: void 0, folder: null };
23427
- await evaluateEdit(reply, readSeat, at);
23428
- handOverAtEdit(reply, at);
23429
- reply.flush();
23430
- }
23431
- function handOverAtEdit(reply, at) {
23432
- if (at.folder === null)
23433
- return;
23434
- const code = process.exitCode;
23435
- if (code !== void 0 && code !== null && Number(code) !== 0)
23436
- return;
23437
- const given = deliverFindings(at.session, at.folder);
23438
- if (given.length === 0)
23439
- return;
23440
- reply.agentContext(detailsBlock(given, realPathOf(projectFolder())));
23441
- }
23442
- async function evaluateEdit(reply, readSeat, at) {
23443
- let input;
23444
- try {
23445
- const raw = await readStdin();
23446
- input = JSON.parse(raw);
23447
- } catch {
23448
- recordTimeEvent("edit", {});
23449
- return;
23450
- }
23451
- recordTimeEvent("edit", input);
23452
- const toolName = input.tool_name ?? "";
23453
- if (!HANDLED_EDIT_TOOLS.has(toolName))
23454
- return;
23455
- const isNotebook = toolName === "NotebookEdit";
23456
- const absFilePath = isNotebook ? input.tool_input?.notebook_path : input.tool_input?.file_path;
23457
- if (!absFilePath || typeof absFilePath !== "string")
23458
- return;
23459
- recordOwnEdit(input, absFilePath);
23460
- const repoRootResult = getRepoRoot(process.cwd());
23461
- if (!repoRootResult.ok) {
23462
- if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
23463
- 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;
23464
23685
  }
23465
- return;
23466
- }
23467
- const repoRoot = repoRootResult.value;
23468
- at.session = input.session_id;
23469
- at.folder = realPathOf(repoRoot);
23470
- const diffContent = isNotebook ? "" : getSingleFileDiff(absFilePath, repoRoot);
23471
- const { relative: pathRelative } = await import("node:path");
23472
- const relPath = pathRelative(repoRoot, absFilePath);
23473
- if (isInstallerOutput(relPath))
23474
- return;
23475
- const context = loadChangeSetContext(repoRoot);
23476
- const changeSet = buildChangeSet([{ path: relPath, diffContent }], {
23477
- ...context,
23478
- excerpts: readExcerptConfig()
23479
- });
23480
- const configResult = readConfig();
23481
- if (!configResult.config) {
23482
- await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
23483
- if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
23484
- return;
23485
- emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
23486
- return;
23487
- }
23488
- const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
23489
- const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
23490
- const evaluation = await evaluateWithRefresh({
23491
- guardServiceUrl,
23492
- token: guardServiceToken,
23493
- engagementId: guardEngagementId,
23494
- request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
23495
- warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
23496
- });
23497
- const clientResult = evaluation.result;
23498
- if (!clientResult.ok) {
23499
- const failure = classifyFailure(clientResult);
23500
- await emitRunRecord({
23501
- engagementId: guardEngagementId,
23502
- runType: "hook",
23503
- outcome: outcomeForClientFailure(clientResult.kind),
23504
- failureReason: failure.reason,
23505
- changeSet,
23506
- credential: evaluation.token,
23507
- credentialRefused: failure.kind === "credential"
23508
- });
23509
- if (failure.kind === "credential") {
23510
- if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
23511
- return;
23512
- emitCredentialRejected(
23513
- failure.statusCode,
23514
- failure.message,
23515
- CREDENTIAL_REJECTED_EXIT_TOOL_USE,
23516
- // The remedy the renewal attempt earned: the sign-in when the account
23517
- // credential is the thing that was refused, and otherwise the plane's own
23518
- // sentence about why it would not mint.
23519
- evaluation.remedy
23520
- );
23521
- 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;
23522
23703
  }
23523
- if (failure.kind === "contract") {
23524
- reply.notice(contractFaultText(failure.message));
23525
- 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;
23526
23713
  }
23527
- reply.notice(infraWarningText());
23528
- 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;
23529
23733
  }
23530
- const telemetryConfig = readTelemetryConfig();
23531
- const run = buildGuardEvalRun({
23532
- engagementId: guardEngagementId,
23533
- runType: "hook",
23534
- // The phase this run belongs to, as the client named it — it names the
23535
- // guard-eval log file the run is appended to. Absent → the run is unphased.
23536
- phase: telemetryConfig.phase,
23537
- outcome: "evaluated",
23538
- changeSet,
23539
- result: clientResult.envelope
23540
- });
23541
- await emitTelemetry(run, {
23542
- controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
23543
- // THE CREDENTIAL IN FORCE, NOT THE ONE IN THE ENVIRONMENT (T-08,
23544
- // credential-lifecycle). `readTelemetryConfig` reads the bearer off
23545
- // `process.env` after the evaluation, and a renewal does not write there — so
23546
- // taking its value on a run that just recovered would present the credential
23547
- // the plane had already refused and drop the record with a 401, silently,
23548
- // because telemetry is best-effort.
23549
- controlTelemetryToken: evaluation.token,
23550
- // Per-engagement log dir from HALFCYCLE_GUARD_EVAL_LOG_DIR (E1-T-02, #61).
23551
- guardEvalLogDir: telemetryConfig.guardEvalLogDir
23552
- }, {
23553
- warn: (msg) => process.stderr.write(msg + "\n")
23554
- });
23555
- actOnEnvelope2(clientResult.envelope, reply);
23734
+ return ops;
23556
23735
  }
23557
- function actOnEnvelope2(envelope, reply) {
23558
- const notRun = notRunReport(envelope.notRun);
23559
- const notRunBlock = notRun ? `
23560
- ${notRun}` : "";
23561
- if (envelope.guardsFired.length === 0) {
23562
- if (notRun)
23563
- reply.notice(notRun);
23564
- return;
23565
- }
23566
- if (envelope.blocking) {
23567
- const personNotRun = notRunMessage((envelope.notRun ?? []).map((n) => n.reason));
23568
- const reason = `${blockReason(envelope.guardsFired.map((g) => g.explanation))}${personNotRun ? `
23569
- ${personNotRun}` : ""}`;
23570
- reply.block(reason, stoppedMessage(blockingExplanations(envelope.guardsFired)));
23571
- } else {
23572
- const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
23573
- reply.notice(`[Halfcycle] Guard warning: ${envelope.explanation}
23574
- ${guardLines}${notRunBlock}`);
23575
- }
23736
+ function isUpdateLine(line2) {
23737
+ return line2 === "" || line2.startsWith(" ") || line2.startsWith("+") || line2.startsWith("-") || line2.startsWith("@@");
23576
23738
  }
23577
- function readStdin() {
23578
- return new Promise((resolve8, reject) => {
23579
- const chunks = [];
23580
- process.stdin.on("data", (chunk) => chunks.push(chunk));
23581
- process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
23582
- process.stdin.on("error", reject);
23583
- });
23739
+ function headerPath(trimmed, header) {
23740
+ if (trimmed === header.trimEnd())
23741
+ return "";
23742
+ return trimmed.startsWith(header) ? trimmed.slice(header.length).trim() : void 0;
23584
23743
  }
23585
-
23586
- // dist/liveness.js
23587
- 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";
23588
- import { join as join11 } from "node:path";
23589
- var LIVE_WINDOW_MS = 10 * 60 * 1e3;
23590
- function field(input, key) {
23591
- if (typeof input !== "object" || input === null)
23744
+ function patchHeaderPaths(patch) {
23745
+ const ops = patchOperations(patch);
23746
+ if (ops === null)
23592
23747
  return null;
23593
- const v = input[key];
23594
- return typeof v === "string" && v !== "" ? v : null;
23748
+ return ops.map((op) => op.kind === "update" && op.movedTo !== null ? op.movedTo : op.path);
23595
23749
  }
23596
- function indexEntry(dir, sessionId, name) {
23597
- const folder = join11(dir, livenessIndexDirName(sessionId));
23598
- for (let attempt2 = 0; attempt2 < 3; attempt2++) {
23599
- try {
23600
- closeSync2(openSync2(join11(folder, name), "a", 384));
23601
- return true;
23602
- } catch (err) {
23603
- if (err.code !== "ENOENT")
23604
- return false;
23605
- }
23606
- let tmp = null;
23607
- try {
23608
- tmp = mkdtempSync(`${folder}.`);
23609
- for (const existing of readdirSync5(dir)) {
23610
- if (parseLivenessFileName(existing)?.sessionId === sessionId)
23611
- closeSync2(openSync2(join11(tmp, existing), "a", 384));
23612
- }
23613
- closeSync2(openSync2(join11(tmp, name), "a", 384));
23614
- renameSync5(tmp, folder);
23615
- return true;
23616
- } catch {
23617
- if (tmp !== null)
23618
- rmSync(tmp, { recursive: true, force: true });
23619
- }
23620
- }
23621
- 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;
23622
23766
  }
23623
- function recordLiveness(input, deps = {}) {
23624
- try {
23625
- const sessionId = field(input, "session_id");
23626
- const agentId = field(input, "agent_id");
23627
- const cwd = field(input, "cwd");
23628
- if (sessionId === null || cwd === null || !livenessIdOk(sessionId))
23629
- return null;
23630
- if (agentId !== null && !livenessIdOk(agentId))
23631
- return null;
23632
- if (/[\r\n]/.test(cwd))
23633
- return null;
23634
- const copy = whichCopy(cwd, deps.walk);
23635
- const dir = deps.dir ?? markerDir();
23636
- const name = livenessFileName(livenessKey(copy?.common ?? null), sessionId, agentId);
23637
- if (!indexEntry(dir, sessionId, name))
23638
- return null;
23639
- const path = join11(dir, name);
23640
- const text = `${cwd}
23641
- ${copy?.top ?? ""}
23642
- `;
23643
- try {
23644
- writeFileSync7(path, text, { encoding: "utf-8", flag: "wx", mode: 384 });
23645
- return { firstSeen: true };
23646
- } catch (err) {
23647
- if (err.code !== "EEXIST")
23648
- return null;
23649
- }
23650
- writeFileSync7(path, text, { encoding: "utf-8", mode: 384 });
23651
- return { firstSeen: false };
23652
- } catch {
23653
- return null;
23654
- }
23655
- }
23656
- function clearLiveness(input, deps = {}) {
23657
- try {
23658
- const sessionId = field(input, "session_id");
23659
- const agentId = field(input, "agent_id");
23660
- if (sessionId === null || agentId === null)
23661
- return 0;
23662
- const dir = deps.dir ?? markerDir();
23663
- let removed = 0;
23664
- for (const name of readdirSync5(dir)) {
23665
- const p = parseLivenessFileName(name);
23666
- if (p === null || p.sessionId !== sessionId || p.agentId !== agentId)
23667
- continue;
23668
- try {
23669
- unlinkSync6(join11(dir, name));
23670
- removed += 1;
23671
- } catch {
23672
- }
23673
- removeLivenessIndexEntry(dir, sessionId, name);
23674
- }
23675
- return removed;
23676
- } catch {
23677
- return 0;
23678
- }
23679
- }
23680
- function topOf(path) {
23681
- try {
23682
- const top = readFileSync14(path, "utf-8").split("\n")[1] ?? "";
23683
- return top === "" ? null : top;
23684
- } catch {
23685
- return null;
23686
- }
23687
- }
23688
- function readLiveness(sessionId, cwd, deps = {}) {
23689
- try {
23690
- if (!sessionId || !cwd || !livenessIdOk(sessionId))
23691
- return null;
23692
- const copy = whichCopy(cwd, deps.walk);
23693
- if (copy === null)
23694
- return null;
23695
- const key = livenessKey(copy.common);
23696
- const dir = deps.dir ?? markerDir();
23697
- const now = (deps.now ?? Date.now)();
23698
- let main2 = null;
23699
- const agents = [];
23700
- for (const name of readdirSync5(dir)) {
23701
- const p = parseLivenessFileName(name);
23702
- if (p === null || p.key !== key || p.sessionId !== sessionId)
23703
- continue;
23704
- const full = join11(dir, name);
23705
- let seenAt;
23706
- try {
23707
- seenAt = statSync4(full).mtimeMs;
23708
- } catch {
23709
- continue;
23710
- }
23711
- const top = topOf(full);
23712
- if (top === null)
23713
- continue;
23714
- if (p.agentId === null)
23715
- main2 = { top, seenAt };
23716
- else if (now - seenAt <= LIVE_WINDOW_MS)
23717
- agents.push({ agentId: p.agentId, top, seenAt });
23718
- }
23719
- agents.sort((a, b) => a.agentId.localeCompare(b.agentId));
23720
- return { main: main2, agents };
23721
- } catch {
23722
- return null;
23723
- }
23724
- }
23725
- function liveCount(record2) {
23726
- return record2 === null ? 0 : record2.agents.length;
23727
- }
23728
- function withoutAgent(record2, agentId) {
23729
- if (record2 === null || agentId === null || agentId === void 0 || agentId === "")
23730
- return record2;
23731
- return { main: record2.main, agents: record2.agents.filter((a) => a.agentId !== agentId) };
23732
- }
23733
- function runsBesideOtherWork(toolInput, record2) {
23734
- const t = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
23735
- const bg = t["run_in_background"];
23736
- if (bg !== false && bg !== "false")
23737
- return true;
23738
- const name = t["name"];
23739
- if (name !== void 0 && name !== null && name !== "")
23740
- return true;
23741
- return liveCount(record2) > 0;
23742
- }
23743
-
23744
- // dist/pace-hook.js
23745
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync18, renameSync as renameSync9, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
23746
- import nodePath4 from "node:path";
23747
-
23748
- // dist/pace-verb.js
23749
- import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync8, statSync as statSync6, writeFileSync as writeFileSync10 } from "node:fs";
23750
- import { homedir as homedir6 } from "node:os";
23751
- import nodePath3 from "node:path";
23752
-
23753
- // dist/pacing.js
23754
- import { createHash as createHash4 } from "node:crypto";
23755
- 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";
23756
- import { homedir as homedir5 } from "node:os";
23757
- import nodePath from "node:path";
23758
- var DEFAULT_CONFIG_FOLDER_NAME = ".claude";
23759
- var RECORDS_FOLDER_NAME = "projects";
23760
23767
  function normaliseFolder(folder, input = {}) {
23761
23768
  const p = input.path ?? nodePath;
23762
23769
  const home = input.home ?? homedir5();
@@ -23770,6 +23777,12 @@ function normaliseFolder(folder, input = {}) {
23770
23777
  function configFolderKey(input = {}) {
23771
23778
  const p = input.path ?? nodePath;
23772
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
+ }
23773
23786
  const fromEnv = env["CLAUDE_CONFIG_DIR"];
23774
23787
  if (typeof fromEnv === "string" && fromEnv !== "")
23775
23788
  return normaliseFolder(fromEnv, input);
@@ -23801,7 +23814,7 @@ function pacingDir(home) {
23801
23814
  }
23802
23815
  function readJsonObject(path) {
23803
23816
  try {
23804
- const v = JSON.parse(readFileSync15(path, "utf-8"));
23817
+ const v = JSON.parse(readFileSync14(path, "utf-8"));
23805
23818
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
23806
23819
  } catch {
23807
23820
  return null;
@@ -23810,8 +23823,8 @@ function readJsonObject(path) {
23810
23823
  function writeJsonWhole(path, value) {
23811
23824
  mkdirSync4(nodePath.dirname(path), { recursive: true, mode: 448 });
23812
23825
  const tmp = `${path}.${String(process.pid)}.${createHash4("sha1").update(String(Math.random())).digest("hex").slice(0, 8)}.tmp`;
23813
- writeFileSync8(tmp, JSON.stringify(value), { mode: 384 });
23814
- renameSync6(tmp, path);
23826
+ writeFileSync7(tmp, JSON.stringify(value), { mode: 384 });
23827
+ renameSync5(tmp, path);
23815
23828
  }
23816
23829
  function writeAccountEntry(path, key, entry) {
23817
23830
  const current = readJsonObject(path);
@@ -23968,7 +23981,7 @@ function readLearnedAccount(key, deps = {}) {
23968
23981
  }
23969
23982
  function dirents(path) {
23970
23983
  try {
23971
- return readdirSync6(path, { withFileTypes: true });
23984
+ return readdirSync5(path, { withFileTypes: true });
23972
23985
  } catch {
23973
23986
  return [];
23974
23987
  }
@@ -24002,7 +24015,7 @@ var NEWLINE = 10;
24002
24015
  var ASSISTANT = Buffer.from('"assistant"');
24003
24016
  var READ_CHUNK = 8 * 1024 * 1024;
24004
24017
  function readWholeLines(path, from, size, onLine) {
24005
- const fd = openSync3(path, "r");
24018
+ const fd = openSync2(path, "r");
24006
24019
  try {
24007
24020
  let pos = from;
24008
24021
  let carry = Buffer.alloc(0);
@@ -24030,7 +24043,7 @@ function readWholeLines(path, from, size, onLine) {
24030
24043
  }
24031
24044
  return consumed;
24032
24045
  } finally {
24033
- closeSync3(fd);
24046
+ closeSync2(fd);
24034
24047
  }
24035
24048
  }
24036
24049
  function hashId(id) {
@@ -24059,7 +24072,7 @@ function scanRecords(key, deps = {}) {
24059
24072
  let size;
24060
24073
  let mtimeSec;
24061
24074
  try {
24062
- const st = statSync5(file2.path);
24075
+ const st = statSync4(file2.path);
24063
24076
  size = st.size;
24064
24077
  mtimeSec = Math.floor(st.mtimeMs / 1e3);
24065
24078
  } catch {
@@ -24195,7 +24208,7 @@ function timeLogTranscripts(home) {
24195
24208
  continue;
24196
24209
  let text;
24197
24210
  try {
24198
- 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");
24199
24212
  } catch {
24200
24213
  continue;
24201
24214
  }
@@ -24350,9 +24363,395 @@ function knownWindowOf(learned, reported, now) {
24350
24363
  if (learned?.ceilings[kind] !== void 0 || reportedFresh(reported[kind], now))
24351
24364
  return kind;
24352
24365
  }
24353
- return null;
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;
24725
+ }
24726
+ }
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;
24354
24744
  }
24355
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
+
24356
24755
  // dist/scan-lock.js
24357
24756
  import { spawn } from "node:child_process";
24358
24757
  import { linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync16, renameSync as renameSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "node:fs";
@@ -24951,7 +25350,7 @@ async function paceAtTurnEnd(input, reply, deps = {}) {
24951
25350
  return;
24952
25351
  const nowMs = (deps.now ?? Date.now)();
24953
25352
  const now = new Date(nowMs);
24954
- 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 });
24955
25354
  const stored = readPaceStateFile(dir);
24956
25355
  const before = stored.accounts[key] ?? null;
24957
25356
  if (before !== null) {
@@ -25055,7 +25454,7 @@ function whereOf(input, deps) {
25055
25454
  const dir = enginePacingDir(configured.config.guardEngagementId, deps.home);
25056
25455
  if (dir === null)
25057
25456
  return null;
25058
- 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 }) };
25059
25458
  }
25060
25459
  function retirePendingStart(input, deps = {}) {
25061
25460
  try {
@@ -25129,7 +25528,7 @@ function paceHoldText(input, toolInput, deps = {}) {
25129
25528
  // dist/risky/facts.js
25130
25529
  import { existsSync as existsSync7, lstatSync as lstatSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, statSync as statSync7 } from "node:fs";
25131
25530
  import { homedir as homedir7, tmpdir as tmpdir2 } from "node:os";
25132
- 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";
25133
25532
 
25134
25533
  // dist/risky/shell.js
25135
25534
  var MAX_DEPTH = 8;
@@ -27339,13 +27738,13 @@ var Env = class {
27339
27738
  description;
27340
27739
  projectCommonDir;
27341
27740
  constructor(ctx) {
27342
- this.projectDir = realOrSelf(resolve4(ctx.projectDir));
27343
- this.cwd = realOrSelf(resolve4(this.projectDir, ctx.cwd));
27344
- 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()));
27345
27744
  this.timeoutMs = ctx.gitTimeoutMs ?? FACTS_GIT_TIMEOUT_MS;
27346
- this.tempDir = realOrSelf(resolve4(ctx.tempDir ?? tmpdir2()));
27745
+ this.tempDir = realOrSelf(resolve5(ctx.tempDir ?? tmpdir2()));
27347
27746
  const roots = ctx.tempRoots ?? [this.tempDir, tmpdir2(), ...TEMP_ROOTS];
27348
- 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));
27349
27748
  }
27350
27749
  isTemp(abs) {
27351
27750
  return this.tempRoots.some((r) => isInside(abs, r));
@@ -27355,7 +27754,7 @@ var Env = class {
27355
27754
  }
27356
27755
  display(abs) {
27357
27756
  if (isInside(abs, this.projectDir)) {
27358
- const rel = relative5(this.projectDir, abs);
27757
+ const rel = relative6(this.projectDir, abs);
27359
27758
  return rel === "" ? "." : toSlash(rel);
27360
27759
  }
27361
27760
  return toSlash(abs);
@@ -27365,7 +27764,7 @@ var Env = class {
27365
27764
  return this.description;
27366
27765
  this.description = null;
27367
27766
  try {
27368
- 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"));
27369
27768
  const checked = checkSystemDescription(raw);
27370
27769
  if (checked.ok)
27371
27770
  this.description = checked.description;
@@ -27408,7 +27807,7 @@ var Env = class {
27408
27807
  if (!r.ok)
27409
27808
  return null;
27410
27809
  const d = r.value.trim();
27411
- return d === "" ? null : realOrSelf(resolve4(dir, d));
27810
+ return d === "" ? null : realOrSelf(resolve5(dir, d));
27412
27811
  }
27413
27812
  };
27414
27813
  function realOrSelf(p) {
@@ -27419,29 +27818,29 @@ function realOrSelf(p) {
27419
27818
  }
27420
27819
  }
27421
27820
  function toSlash(p) {
27422
- return sep3 === "/" ? p : p.split(sep3).join("/");
27821
+ return sep4 === "/" ? p : p.split(sep4).join("/");
27423
27822
  }
27424
27823
  function isInside(child, parent) {
27425
- const rel = relative5(parent, child);
27426
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
27824
+ const rel = relative6(parent, child);
27825
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
27427
27826
  }
27428
27827
  function resolveHome(text, home, base) {
27429
27828
  if (text === "~")
27430
27829
  return home;
27431
27830
  if (text.startsWith("~/"))
27432
- return join12(home, text.slice(2));
27433
- return resolve4(base, text);
27831
+ return join13(home, text.slice(2));
27832
+ return resolve5(base, text);
27434
27833
  }
27435
27834
  function realTarget(abs) {
27436
27835
  const parent = dirname6(abs);
27437
27836
  if (parent === abs)
27438
27837
  return abs;
27439
- return join12(realOrSelf(parent), pathBasename(abs));
27838
+ return join13(realOrSelf(parent), pathBasename(abs));
27440
27839
  }
27441
27840
  function wordPath(word, base, env) {
27442
27841
  if (word.temp !== void 0) {
27443
- const root = word.temp.base === "mktemp" ? join12(env.tempDir, ".halfcycle-mktemp-new") : env.tempDir;
27444
- 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}`);
27445
27844
  }
27446
27845
  return word.literal ? resolveHome(word.text, env.home, base) : null;
27447
27846
  }
@@ -27450,7 +27849,7 @@ function follow(start, prefix, dirs, m, env) {
27450
27849
  let fresh = start.fresh;
27451
27850
  let freshRoot = start.freshRoot;
27452
27851
  const seen = [...prefix];
27453
- const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve4(p), resolve4(freshRoot));
27852
+ const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve5(p), resolve5(freshRoot));
27454
27853
  const mark = (by) => {
27455
27854
  if (fresh !== void 0 && insideFresh(cwd))
27456
27855
  return;
@@ -27579,14 +27978,14 @@ function expandBraces(text) {
27579
27978
  return body.split(",").map((b) => `${head}${b}${tail}`);
27580
27979
  }
27581
27980
  function globPath(abs) {
27582
- const root = abs.startsWith(sep3) ? sep3 : "";
27583
- const segments = abs.split(sep3).filter((s) => s !== "");
27981
+ const root = abs.startsWith(sep4) ? sep4 : "";
27982
+ const segments = abs.split(sep4).filter((s) => s !== "");
27584
27983
  let current = [root === "" ? "." : root];
27585
27984
  for (const segment of segments) {
27586
27985
  const next = [];
27587
27986
  if (!/[*?[]/.test(segment)) {
27588
27987
  for (const c of current)
27589
- next.push(join12(c, segment));
27988
+ next.push(join13(c, segment));
27590
27989
  } else {
27591
27990
  const re = segmentPattern(segment);
27592
27991
  for (const c of current) {
@@ -27600,7 +27999,7 @@ function globPath(abs) {
27600
27999
  if (n.startsWith(".") && !segment.startsWith("."))
27601
28000
  continue;
27602
28001
  if (re.test(n))
27603
- next.push(join12(c, n));
28002
+ next.push(join13(c, n));
27604
28003
  }
27605
28004
  }
27606
28005
  }
@@ -27665,7 +28064,7 @@ function isDirectory(p) {
27665
28064
  }
27666
28065
  function filesUnder(targets, repoRoot, env, keep, seen) {
27667
28066
  const specs = targets.map((t) => {
27668
- const rel = toSlash(relative5(repoRoot, t));
28067
+ const rel = toSlash(relative6(repoRoot, t));
27669
28068
  return rel === "" ? "." : rel;
27670
28069
  });
27671
28070
  const tracked = env.git(["--literal-pathspecs", "ls-files", "--full-name", "-z", "--", ...specs], repoRoot);
@@ -27728,12 +28127,12 @@ function findKeeps(filter, roots, repoRoot) {
27728
28127
  const names = filter.names.map((n) => findPattern(n.pattern, n.fold, false));
27729
28128
  const excluded = filter.excluded.map((p) => findPattern(p, false, true));
27730
28129
  const steps = (rel) => {
27731
- const abs = join12(repoRoot, rel);
28130
+ const abs = join13(repoRoot, rel);
27732
28131
  const out = [];
27733
28132
  for (const r of roots) {
27734
28133
  if (!isInside(abs, r.abs))
27735
28134
  continue;
27736
- const below = toSlash(relative5(r.abs, abs)).split("/").filter((x) => x !== "");
28135
+ const below = toSlash(relative6(r.abs, abs)).split("/").filter((x) => x !== "");
27737
28136
  const rootName = r.text === null ? pathBasename(r.abs) : r.text.replace(/\/+$/, "").split("/").pop() || r.text;
27738
28137
  const prefixes = r.text === null ? [] : [.../* @__PURE__ */ new Set([r.text, r.text.replace(/\/+$/, "") || r.text])];
27739
28138
  const chain = [{ name: rootName, printed: r.text === null ? [] : [r.text] }];
@@ -27800,9 +28199,9 @@ function credentialFilesIn(folders, repoRoot, deadline, skip) {
27800
28199
  const out = [];
27801
28200
  const stack = [];
27802
28201
  for (const f of folders) {
27803
- if (isDirectory(join12(repoRoot, f)))
28202
+ if (isDirectory(join13(repoRoot, f)))
27804
28203
  stack.push(f);
27805
- else if (pathExists(join12(repoRoot, f)) && isCredentialPath(f))
28204
+ else if (pathExists(join13(repoRoot, f)) && isCredentialPath(f))
27806
28205
  out.push(f);
27807
28206
  }
27808
28207
  let seen = 0;
@@ -27810,7 +28209,7 @@ function credentialFilesIn(folders, repoRoot, deadline, skip) {
27810
28209
  const rel = stack.pop();
27811
28210
  let entries;
27812
28211
  try {
27813
- entries = readdirSync8(join12(repoRoot, rel), { withFileTypes: true });
28212
+ entries = readdirSync8(join13(repoRoot, rel), { withFileTypes: true });
27814
28213
  } catch {
27815
28214
  continue;
27816
28215
  }
@@ -27907,7 +28306,7 @@ function deleteFilesFacts(m, env) {
27907
28306
  for (const raw of expanded) {
27908
28307
  const abs = realTarget(raw);
27909
28308
  written.set(abs, word.literal ? word.text : null);
27910
- 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)) {
27911
28310
  guarded.push(abs);
27912
28311
  } else if (isInside(abs, env.projectDir) || !env.isTemp(abs) && env.sameRepository(abs)) {
27913
28312
  if (pathExists(abs))
@@ -27951,7 +28350,7 @@ function deleteFilesFacts(m, env) {
27951
28350
  ...base.pre.length === 0 ? {
27952
28351
  save: {
27953
28352
  repoRoot,
27954
- paths: targets.map((t) => toSlash(relative5(repoRoot, t)) || "."),
28353
+ paths: targets.map((t) => toSlash(relative6(repoRoot, t)) || "."),
27955
28354
  ...keep === void 0 ? {} : { keep: (rel) => keep.deletes(rel) },
27956
28355
  lines: []
27957
28356
  }
@@ -27963,12 +28362,12 @@ function closeFoldsAll(targets, env) {
27963
28362
  const root = recordRoot(t, env);
27964
28363
  if (root === null || !isInside(t, root))
27965
28364
  return false;
27966
- const rel = toSlash(relative5(root, t));
28365
+ const rel = toSlash(relative6(root, t));
27967
28366
  if (rel === "" || recordPatternFor(rel)?.deleteAsked !== false)
27968
28367
  return false;
27969
28368
  let at = root;
27970
28369
  for (const seg of rel.split("/")) {
27971
- at = join12(at, seg);
28370
+ at = join13(at, seg);
27972
28371
  try {
27973
28372
  if (lstatSync2(at).isSymbolicLink())
27974
28373
  return false;
@@ -28157,11 +28556,11 @@ function cleanFiles(m, where, env) {
28157
28556
  const repoRoot = env.repoRoot(where.cwd);
28158
28557
  if (repoRoot === null)
28159
28558
  return null;
28160
- const abs = named.map((n) => resolve4(where.cwd, n));
28559
+ const abs = named.map((n) => resolve5(where.cwd, n));
28161
28560
  const files = filesUnder(abs, repoRoot, env);
28162
28561
  if (files === null)
28163
28562
  return null;
28164
- 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)) || ".") };
28165
28564
  }
28166
28565
  function stashFiles(m, where, env) {
28167
28566
  let refs;
@@ -28551,7 +28950,7 @@ function publishFacts(m, env) {
28551
28950
  const start = startFolder(m, env);
28552
28951
  if (start !== null && PACKAGE_JSON_PROGRAMS.has(m.program)) {
28553
28952
  try {
28554
- const pkg = JSON.parse(readFileSync19(join12(start, "package.json"), "utf-8"));
28953
+ const pkg = JSON.parse(readFileSync19(join13(start, "package.json"), "utf-8"));
28555
28954
  if (typeof pkg["name"] === "string" && typeof pkg["version"] === "string" && pkg["name"] !== "" && pkg["version"] !== "") {
28556
28955
  name = pkg["name"];
28557
28956
  version2 = pkg["version"];
@@ -28664,7 +29063,7 @@ function recordFileAt(abs, env) {
28664
29063
  const root = recordRoot(abs, env);
28665
29064
  if (root === null || !isInside(abs, root))
28666
29065
  return null;
28667
- const rel = toSlash(relative5(root, abs));
29066
+ const rel = toSlash(relative6(root, abs));
28668
29067
  if (rel === "")
28669
29068
  return null;
28670
29069
  const row = recordPatternFor(rel);
@@ -28674,11 +29073,11 @@ function recordFilesUnder(folder, env) {
28674
29073
  const root = recordRoot(folder, env);
28675
29074
  if (root === null)
28676
29075
  return [];
28677
- const relFolder = toSlash(relative5(root, folder));
29076
+ const relFolder = toSlash(relative6(root, folder));
28678
29077
  const starts = /* @__PURE__ */ new Set();
28679
29078
  for (const { pattern } of PROJECT_RECORD_PATTERNS) {
28680
29079
  if (relFolder === "" || patternOwns(relFolder, pattern))
28681
- starts.add(join12(root, pattern));
29080
+ starts.add(join13(root, pattern));
28682
29081
  else if (patternOwns(pattern, relFolder))
28683
29082
  starts.add(folder);
28684
29083
  }
@@ -28704,7 +29103,7 @@ function recordFilesUnder(folder, env) {
28704
29103
  continue;
28705
29104
  }
28706
29105
  for (const n of names)
28707
- stack.push(join12(p, n));
29106
+ stack.push(join13(p, n));
28708
29107
  } else if (st.isFile()) {
28709
29108
  const r = recordFileAt(p, env);
28710
29109
  if (r !== null)
@@ -28725,7 +29124,7 @@ function recordHistory(files, env) {
28725
29124
  byTree.set(top, list);
28726
29125
  }
28727
29126
  for (const [top, list] of byTree) {
28728
- const specs = list.map((abs) => toSlash(relative5(top, abs)));
29127
+ const specs = list.map((abs) => toSlash(relative6(top, abs)));
28729
29128
  const tracked = trackedOf(specs, top, env);
28730
29129
  if (tracked === null)
28731
29130
  continue;
@@ -28782,8 +29181,14 @@ function parseNumstatZ(text) {
28782
29181
  // dist/risky/sentence.js
28783
29182
  var REASON_MAX_CHARS = 200;
28784
29183
  var MAX_LISTED = 3;
28785
- 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.`;
28786
29190
  var NOTHING_LOST_ON_NO = "Nothing is lost if you say no.";
29191
+ var IF_YES = "If you say yes, ";
28787
29192
  var DESCRIBED_TOOLS = /* @__PURE__ */ new Set(["Bash", "Agent"]);
28788
29193
  function reasonOf(call) {
28789
29194
  if (!DESCRIBED_TOOLS.has(call.toolName))
@@ -28812,10 +29217,43 @@ function questionFor(facts, description) {
28812
29217
  return couldNotTellQuestion(description);
28813
29218
  if (staked.length === 1) {
28814
29219
  const only = staked[0];
28815
- 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)}`;
28816
29221
  }
28817
- const shown2 = staked.slice(0, MAX_LISTED).map((f) => `${whatOf(f)} If you allow it, ${lostOf(f)}`);
28818
- 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)}`;
28819
29257
  }
28820
29258
  function orderedStaked(facts) {
28821
29259
  const said = /* @__PURE__ */ new Set();
@@ -28830,7 +29268,7 @@ function orderedStaked(facts) {
28830
29268
  function moreLine(more) {
28831
29269
  return more > 0 ? ` And ${more} more risky ${more === 1 ? "step" : "steps"}.` : "";
28832
29270
  }
28833
- var UNNAMED_PUSH_WHAT = "Replace a line of work on the shared copy.";
29271
+ var UNNAMED_PUSH_WHAT = "Replace a remote branch.";
28834
29272
  function whatsText(facts, extraPush = false) {
28835
29273
  const staked = orderedStaked(facts);
28836
29274
  const items = staked.map((f) => ({ order: riskyTypeOrder(f.type), what: whatOf(f) }));
@@ -28849,11 +29287,12 @@ function typeSentenceFor(facts, extraPush = false) {
28849
29287
  function savedWhatFor(facts) {
28850
29288
  return whatsText(facts);
28851
29289
  }
28852
- function refusalText(question) {
28853
- return `${question} ${TURN_QUESTIONS_ON_LINE}`;
28854
- }
29290
+ var COULD_NOT_TELL_STEP = "one that can delete, overwrite, publish or show something that cannot be taken back";
28855
29291
  function couldNotTellQuestion(description) {
28856
- 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");
28857
29296
  }
28858
29297
  function whatOf(f) {
28859
29298
  switch (f.type) {
@@ -28882,23 +29321,45 @@ function lostOf(f) {
28882
29321
  case "delete-files":
28883
29322
  return deleteLost(f);
28884
29323
  case "discard-unsaved":
28885
- 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.`;
28886
29325
  case "rewrite-shared-history":
28887
29326
  return pushLost(f);
28888
29327
  case "delete-unmerged-work":
28889
29328
  return unmergedLost(f);
28890
29329
  case "wipe-data":
28891
- 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.";
28892
29331
  case "read-credentials":
28893
- 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.";
28894
29333
  case "publish":
28895
- return "once published it cannot be taken back.";
29334
+ return "anyone can install it straight away, and a published version cannot be taken back.";
28896
29335
  case "deploy":
28897
29336
  return "what is live now is replaced at once; Halfcycle cannot tell whether going back is possible.";
28898
29337
  case "shrink-record":
28899
29338
  return shrinkLost(f);
28900
29339
  }
28901
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
+ }
28902
29363
  function deleteWhat(f) {
28903
29364
  const thing = thingText(f.thing) ?? listNames(f.match.targets.map((t) => t.text));
28904
29365
  if (f.files === null)
@@ -28907,24 +29368,39 @@ function deleteWhat(f) {
28907
29368
  }
28908
29369
  function deleteLost(f) {
28909
29370
  if (f.files === null || f.neverSaved === null) {
28910
- 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.";
28911
29372
  }
28912
29373
  const n = f.files;
28913
29374
  const k = f.neverSaved;
28914
29375
  if (k === 0)
28915
- 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.";
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.";
28916
29377
  if (k === n) {
28917
29378
  if (n === 1)
28918
- return "that file was never saved to the project's history and can never be brought back.";
28919
- return `${n === 2 ? "both" : `all ${n}`} of those files were never saved to the project's history and can never be brought back.`;
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.`;
28920
29381
  }
28921
- 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.`;
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";
29387
+ }
29388
+ const n = f.files;
29389
+ const k = f.neverSaved;
29390
+ if (k === 0)
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`;
29392
+ if (k === n) {
29393
+ if (n === 1)
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`;
29396
+ }
29397
+ return `${k} of those files ${k === 1 ? "was" : "were"} never committed, so ${k === 1 ? "it" : "they"} could not be brought back`;
28922
29398
  }
28923
29399
  function discardWhat(f) {
28924
29400
  if (f.files === null)
28925
- return "Throw away unsaved changes in this project.";
29401
+ return "Throw away uncommitted changes in this project.";
28926
29402
  const thing = thingText(f.thing) ?? "this project";
28927
- 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"}.`;
28928
29404
  }
28929
29405
  function pushLines(f) {
28930
29406
  const lines2 = f.lines.filter((l) => l.dropped !== 0);
@@ -28935,19 +29411,30 @@ function pushWhat(f) {
28935
29411
  const verb = lines2.length > 0 && lines2.every((l) => l.deletes) ? "Delete" : "Replace";
28936
29412
  const named = lines2.filter((l) => l.branch !== null).map((l) => l.branch);
28937
29413
  if (named.length === 0)
28938
- return verb === "Replace" ? UNNAMED_PUSH_WHAT : `${verb} a line of work on the shared copy.`;
28939
- 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)}`;
28940
29422
  }
28941
29423
  function pushLost(f) {
28942
29424
  const lines2 = pushLines(f);
28943
- const unknown2 = lines2.find((l) => l.dropped === null);
28944
- if (unknown2 !== void 0) {
28945
- const where = unknown2.branch === null ? "that line of work" : `the shared copy's line of work ${quote(unknown2.branch)}`;
28946
- return `whatever is on it now might no longer be there, because Halfcycle cannot tell what is on ${where}.`;
28947
- }
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}.`;
28948
29428
  const n = lines2.reduce((s, l) => s + (l.dropped ?? 0), 0);
28949
29429
  const them = n === 1 ? "it" : "them";
28950
- 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"}`;
28951
29438
  }
28952
29439
  function unmergedLines(f) {
28953
29440
  const lines2 = f.lines.filter((l) => l.commits !== 0);
@@ -28957,24 +29444,34 @@ function unmergedWhat(f) {
28957
29444
  const lines2 = unmergedLines(f);
28958
29445
  if (f.match.form === "reset-to") {
28959
29446
  const l = lines2[0];
28960
- 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)}`;
28961
29448
  if (l?.commits == null)
28962
29449
  return `Move ${which} back.`;
28963
- return `Move ${which} back ${l.commits} saved ${l.commits === 1 ? "change" : "changes"}.`;
29450
+ return `Move ${which} back ${l.commits} ${l.commits === 1 ? "commit" : "commits"}.`;
28964
29451
  }
28965
29452
  const named = lines2.filter((l) => l.branch !== null).map((l) => l.branch);
28966
29453
  if (named.length === 0)
28967
- return "Delete a line of work.";
28968
- 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)}.`;
28969
29456
  }
28970
- function unmergedLost(f) {
29457
+ function unmergedCount(f) {
28971
29458
  const lines2 = unmergedLines(f);
28972
- const tail = "this computer can bring them back for a while, the shared copy cannot.";
28973
- if (lines2.some((l) => l.commits === null)) {
28974
- 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.";
28975
29467
  }
28976
- const n = lines2.reduce((s, l) => s + (l.commits ?? 0), 0);
28977
- 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`;
28978
29475
  }
28979
29476
  function wipeWhat(f) {
28980
29477
  const m = f.match;
@@ -29018,7 +29515,7 @@ function sqlTarget(statement) {
29018
29515
  }
29019
29516
  function credentialsWhat(f) {
29020
29517
  if (f.match.form === "gh-token")
29021
- 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.";
29022
29519
  if (f.match.form === "keychain")
29023
29520
  return "Show the agent a password kept in this computer's keychain.";
29024
29521
  const file2 = f.file ?? f.match.file;
@@ -29057,6 +29554,20 @@ function shrinkLost(f) {
29057
29554
  return "Halfcycle cannot tell how much of it could be brought back from the project's history.";
29058
29555
  }
29059
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
+ }
29060
29571
  var NOTHING_WAS_CHANGED = "Nothing was changed.";
29061
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.';
29062
29573
  function ownCopyRefusal(f, description) {
@@ -29117,24 +29628,188 @@ function listPlain(items) {
29117
29628
 
29118
29629
  // dist/risky/check.js
29119
29630
  function riskyAnswer(call, contextOf) {
29120
- 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));
29121
29633
  if (matches.length === 0)
29122
29634
  return { silent: true };
29123
- const reason = reasonOf(call);
29635
+ const reason = calls.length === 0 ? null : reasonOf(calls[0]);
29124
29636
  try {
29125
29637
  const staked = factsFor(matches, contextOf()).filter((f) => f.atStake);
29126
29638
  if (staked.length === 0)
29127
29639
  return { silent: true };
29128
- 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);
29129
29795
  } catch {
29130
- return { silent: false, question: couldNotTellQuestion(reason), staked: [] };
29131
29796
  }
29132
29797
  }
29133
29798
 
29134
29799
  // dist/risky/decide.js
29135
29800
  var QUESTIONS_ON_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "auto"]);
29136
- function answerModeFor(permissionMode) {
29137
- 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
+ }
29138
29813
  }
29139
29814
  var REFUSED_IN_EVERY_MODE = /* @__PURE__ */ new Set(["outside-own-copy"]);
29140
29815
  function autonomousRowOf(type) {
@@ -29184,6 +29859,9 @@ function decide(action, facts, mode, mayPublishNow = () => false) {
29184
29859
  }
29185
29860
  }
29186
29861
  }
29862
+ function allowableByTyping(action, facts) {
29863
+ return decide(action, facts, "questions-on") === "ask";
29864
+ }
29187
29865
  function decideAutonomous(facts, mayPublishNow) {
29188
29866
  if (facts.length === 0)
29189
29867
  return { autonomous: "could-not-tell" };
@@ -29200,8 +29878,8 @@ function decideAutonomous(facts, mayPublishNow) {
29200
29878
  }
29201
29879
 
29202
29880
  // dist/risky/own-copy.js
29203
- import { readFileSync as readFileSync20 } from "node:fs";
29204
- 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";
29205
29883
  var OWN_COPY_START_TOOL = "Agent";
29206
29884
  var OWN_COPY_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
29207
29885
  var OWN_COPY_ISOLATION = "worktree";
@@ -29222,7 +29900,7 @@ function stringField2(o, key) {
29222
29900
  }
29223
29901
  function readOr(deps, path) {
29224
29902
  try {
29225
- return (deps.readFile ?? ((p) => readFileSync20(p, "utf-8")))(path);
29903
+ return (deps.readFile ?? ((p) => readFileSync21(p, "utf-8")))(path);
29226
29904
  } catch {
29227
29905
  return null;
29228
29906
  }
@@ -29262,7 +29940,7 @@ function typeCanChangeFiles(subagentType, deps) {
29262
29940
  return true;
29263
29941
  if (AGENT_FILE_NAME.test(type)) {
29264
29942
  for (const base of [deps.projectDir, deps.home]) {
29265
- const text = readOr(deps, join13(base, ".claude", "agents", `${type}.md`));
29943
+ const text = readOr(deps, join14(base, ".claude", "agents", `${type}.md`));
29266
29944
  if (text === null)
29267
29945
  continue;
29268
29946
  const tools = frontmatterTools(text);
@@ -29368,9 +30046,9 @@ function judgeChange(input, call, deps) {
29368
30046
 
29369
30047
  // dist/risky/save.js
29370
30048
  import { randomBytes } from "node:crypto";
29371
- 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";
29372
30050
  import { homedir as homedir8 } from "node:os";
29373
- 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";
29374
30052
  var SAVE_LIMIT_BYTES = 2e8;
29375
30053
  var SAVE_BUDGET_MS = 5e3;
29376
30054
  function notSaved(reason) {
@@ -29379,7 +30057,7 @@ function notSaved(reason) {
29379
30057
  function savedFolderOf(engagementId, home = homedir8()) {
29380
30058
  if (!isEngagementId(engagementId))
29381
30059
  return null;
29382
- 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);
29383
30061
  }
29384
30062
  function saveFirst(staked, ctx) {
29385
30063
  const now = ctx.now ?? Date.now;
@@ -29410,20 +30088,20 @@ function saveFirst(staked, ctx) {
29410
30088
  const id = freshId(folder, ctx.newId);
29411
30089
  if (id === null)
29412
30090
  return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29413
- saveDir = join14(folder, id);
30091
+ saveDir = join15(folder, id);
29414
30092
  const repository = repositoryOf(g);
29415
30093
  if (repository === null)
29416
30094
  return failed(saveDir);
29417
- writeFileSync12(join14(saveDir, SAVED_REPOSITORY_FILE_NAME), `${repository}
30095
+ writeFileSync13(join15(saveDir, SAVED_REPOSITORY_FILE_NAME), `${repository}
29418
30096
  `, { mode: 384 });
29419
- const privateIndex = join14(saveDir, PRIVATE_INDEX_NAME);
30097
+ const privateIndex = join15(saveDir, PRIVATE_INDEX_NAME);
29420
30098
  const commit = saveCommit(id, headCommit2, found2.tracked, plan.lines, privateIndex, g, now());
29421
30099
  rmSync2(privateIndex, { force: true });
29422
30100
  if (commit === null)
29423
30101
  return failed(saveDir);
29424
- const copiesDir = join14(saveDir, SAVED_COPIES_DIR_NAME);
30102
+ const copiesDir = join15(saveDir, SAVED_COPIES_DIR_NAME);
29425
30103
  for (const rel of found2.copies) {
29426
- if (now() > deadline || !copyOne(join14(plan.root, rel), join14(copiesDir, rel)))
30104
+ if (now() > deadline || !copyOne(join15(plan.root, rel), join15(copiesDir, rel)))
29427
30105
  return failed(saveDir);
29428
30106
  }
29429
30107
  const description = savedSchema.safeParse({
@@ -29439,10 +30117,10 @@ function saveFirst(staked, ctx) {
29439
30117
  });
29440
30118
  if (!description.success)
29441
30119
  return failed(saveDir);
29442
- const file2 = join14(saveDir, SAVED_FILE_NAME);
29443
- 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)}
29444
30122
  `, { mode: 384 });
29445
- renameSync10(`${file2}.part`, file2);
30123
+ renameSync11(`${file2}.part`, file2);
29446
30124
  ctx.beforeRef?.();
29447
30125
  const ref = g.run(["update-ref", "--no-deref", savedRefOf(id), commit, ""]);
29448
30126
  if (!ref.ok)
@@ -29470,7 +30148,7 @@ function planOf(staked) {
29470
30148
  const t = f.save;
29471
30149
  if (t === void 0)
29472
30150
  return { ok: false, reason: f.cannotTell === void 0 ? AUTONOMY_SAVE_NOT_COVERED_REASON : AUTONOMY_COULD_NOT_TELL_REASON };
29473
- const r = realOrSelf2(resolve5(t.repoRoot));
30151
+ const r = realOrSelf2(resolve6(t.repoRoot));
29474
30152
  if (root === null)
29475
30153
  root = r;
29476
30154
  else if (root !== r)
@@ -29489,7 +30167,7 @@ function planOf(staked) {
29489
30167
  function isInsideRel(p) {
29490
30168
  if (p === ".")
29491
30169
  return true;
29492
- if (p === "" || isAbsolute3(p) || p.includes("\\"))
30170
+ if (p === "" || isAbsolute4(p) || p.includes("\\"))
29493
30171
  return false;
29494
30172
  return p.split("/").every((s) => s !== "" && s !== "." && s !== "..");
29495
30173
  }
@@ -29519,7 +30197,7 @@ function filesOf(plan, g, deadline) {
29519
30197
  trackedAll.add(path);
29520
30198
  if (!reached(path, plan.specs))
29521
30199
  continue;
29522
- const st = lstatOrNull(join14(plan.root, path));
30200
+ const st = lstatOrNull(join15(plan.root, path));
29523
30201
  if (st !== null && (st.isFile() || st.isSymbolicLink()))
29524
30202
  tracked.add(path);
29525
30203
  }
@@ -29531,7 +30209,7 @@ function filesOf(plan, g, deadline) {
29531
30209
  const rel = stack.pop();
29532
30210
  if (rel === ".git")
29533
30211
  continue;
29534
- const abs = rel === "." ? plan.root : join14(plan.root, rel);
30212
+ const abs = rel === "." ? plan.root : join15(plan.root, rel);
29535
30213
  const st = lstatOrNull(abs);
29536
30214
  if (st === null)
29537
30215
  continue;
@@ -29575,7 +30253,7 @@ function saveCommit(id, head, tracked, lines2, privateIndex, g, at) {
29575
30253
  const indexPath = g.run(["rev-parse", "--git-path", "index"]);
29576
30254
  if (!indexPath.ok)
29577
30255
  return null;
29578
- const projectIndex = resolve5(g.root, indexPath.value.trim());
30256
+ const projectIndex = resolve6(g.root, indexPath.value.trim());
29579
30257
  const env = commitEnv(privateIndex, at);
29580
30258
  if (lstatOrNull(projectIndex) !== null)
29581
30259
  copyFileSync(projectIndex, privateIndex);
@@ -29644,7 +30322,7 @@ function freshId(folder, newId) {
29644
30322
  if (!savedIdSchema.safeParse(id).success)
29645
30323
  return null;
29646
30324
  try {
29647
- mkdirSync8(join14(folder, id), { mode: 448 });
30325
+ mkdirSync8(join15(folder, id), { mode: 448 });
29648
30326
  return id;
29649
30327
  } catch (e) {
29650
30328
  if (e.code !== "EEXIST")
@@ -29665,11 +30343,11 @@ function repositoryOf(g) {
29665
30343
  if (!r.ok)
29666
30344
  return null;
29667
30345
  const dir = r.value.trim();
29668
- return dir === "" ? null : realOrSelf2(resolve5(g.root, dir));
30346
+ return dir === "" ? null : realOrSelf2(resolve6(g.root, dir));
29669
30347
  }
29670
30348
  function markedRepository(dir) {
29671
30349
  try {
29672
- 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();
29673
30351
  return text === "" ? null : text;
29674
30352
  } catch {
29675
30353
  return null;
@@ -29703,7 +30381,7 @@ function pruneSaves(root, engagementId, ctx = {}) {
29703
30381
  if (!savedIdSchema.safeParse(id).success)
29704
30382
  continue;
29705
30383
  here.add(id);
29706
- const dir = join14(folder, id);
30384
+ const dir = join15(folder, id);
29707
30385
  const st = lstatOrNull(dir);
29708
30386
  const answer = savedPruneAnswer({
29709
30387
  refMadeAt: refs.get(id) ?? null,
@@ -29742,19 +30420,19 @@ function realOrSelf2(p) {
29742
30420
 
29743
30421
  // dist/autonomy-state.js
29744
30422
  import { spawnSync as spawnSync2 } from "node:child_process";
29745
- 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";
29746
30424
  import { homedir as homedir9, platform as platform2 } from "node:os";
29747
- import { dirname as dirname8, join as join15 } from "node:path";
30425
+ import { dirname as dirname8, join as join16 } from "node:path";
29748
30426
  var AUTONOMY_STATE_FILENAME = "autonomy.json";
29749
30427
  var AUTONOMY_STATE_DEADLINE_MS = 1500;
29750
30428
  function autonomyStateFilePath(engagementId, home) {
29751
- 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);
29752
30430
  }
29753
30431
  function readCachedAutonomy(engagementId, nowMs, home) {
29754
30432
  if (!isEngagementId(engagementId))
29755
30433
  return null;
29756
30434
  try {
29757
- 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")));
29758
30436
  if (!parsed.success || !parsed.data.on)
29759
30437
  return null;
29760
30438
  const age = nowMs - Date.parse(parsed.data.readAt);
@@ -29800,14 +30478,14 @@ function deleteAutonomyStateFile(engagementId, home) {
29800
30478
  }
29801
30479
  function writeAutonomyStateFile(engagementId, file2, home) {
29802
30480
  const target = autonomyStateFilePath(engagementId, home);
29803
- const temp = join15(dirname8(target), `.${AUTONOMY_STATE_FILENAME}.${process.pid}.tmp`);
30481
+ const temp = join16(dirname8(target), `.${AUTONOMY_STATE_FILENAME}.${process.pid}.tmp`);
29804
30482
  try {
29805
30483
  mkdirSync9(dirname8(target), { recursive: true, mode: 448 });
29806
- writeFileSync13(temp, `${JSON.stringify(file2)}
30484
+ writeFileSync14(temp, `${JSON.stringify(file2)}
29807
30485
  `, { mode: 384 });
29808
30486
  if (platform2() !== "win32")
29809
30487
  chmodSync3(temp, 384);
29810
- renameSync11(temp, target);
30488
+ renameSync12(temp, target);
29811
30489
  return true;
29812
30490
  } catch {
29813
30491
  try {
@@ -29861,6 +30539,76 @@ async function cacheAutonomySetting(opts) {
29861
30539
  }
29862
30540
  }
29863
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
+
29864
30612
  // dist/hooks/pre-tool-use.js
29865
30613
  import { performance } from "node:perf_hooks";
29866
30614
  import { homedir as homedir10 } from "node:os";
@@ -29872,13 +30620,15 @@ var OWN_COPY_UNKNOWN_NOTICE = "Halfcycle could not tell which copy of the projec
29872
30620
  var CHECKED = new Set(CHECKED_TOOLS);
29873
30621
  var RECORDED_ONLY = new Set(RECORDED_ONLY_TOOLS);
29874
30622
  function runPreToolUse(deps) {
29875
- const input = parseObject(deps.input);
30623
+ const parsed = parseObject(deps.input);
30624
+ const input = parsed === null ? null : hookInputForRule(parsed, profileOf(deps).configFolder);
29876
30625
  const at = clockOf(deps)();
29877
30626
  const stamped = { ...deps, now: () => at };
29878
30627
  recordTimeEvent("action", input ?? {}, stamped);
29879
30628
  recordOwnShell(input, at, { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir } });
29880
30629
  const seen = recordLiveness(input ?? {}, deps.liveness);
29881
30630
  const paceDeps = {
30631
+ configFolder: profileOf(deps).configFolder,
29882
30632
  ...deps.pace ?? {},
29883
30633
  ...deps.env === void 0 ? {} : { env: deps.env },
29884
30634
  ...deps.home === void 0 ? {} : { home: deps.home },
@@ -29890,14 +30640,15 @@ function runPreToolUse(deps) {
29890
30640
  const toolName = input === null ? void 0 : stringOf(input, "tool_name");
29891
30641
  if (toolName !== void 0 && RECORDED_ONLY.has(toolName))
29892
30642
  return recordedOnlyReply(mode);
29893
- const call = input === null ? null : toolCallOf(input);
29894
- 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)))
29895
30645
  return unreadable(deps, input);
30646
+ const call = calls[0] ?? { toolName: stringOf(input, "tool_name") ?? "", toolInput: {} };
29896
30647
  let answer;
29897
30648
  try {
29898
- answer = riskyAnswer(call, () => {
30649
+ answer = riskyAnswer(calls, () => {
29899
30650
  const cwd = stringOf(input, "cwd") ?? (deps.cwd ?? (() => process.cwd()))();
29900
- const projectDir = deps.projectDir ?? projectFolder(deps.env ?? process.env);
30651
+ const projectDir = projectDirOf(deps);
29901
30652
  const tempDir = (deps.env ?? process.env)["TMPDIR"];
29902
30653
  return {
29903
30654
  cwd,
@@ -29910,7 +30661,7 @@ function runPreToolUse(deps) {
29910
30661
  } catch {
29911
30662
  answer = { silent: true };
29912
30663
  }
29913
- const own = ownCopyOf(input, call, deps);
30664
+ const own = ownCopyOfAll(input, calls, deps);
29914
30665
  try {
29915
30666
  if (own.kind === "refused") {
29916
30667
  const staked = [...answer.silent ? [] : answer.staked, own.facts];
@@ -29921,12 +30672,12 @@ function runPreToolUse(deps) {
29921
30672
  }
29922
30673
  }
29923
30674
  if (mode === "autonomous") {
29924
- 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"]));
29925
30676
  if (!answer.silent || sendsSaves)
29926
30677
  return autonomousReply(call, answer.silent ? [] : answer.staked, sendsSaves, mode, autonomy, deps);
29927
30678
  }
29928
30679
  if (answer.silent) {
29929
- if (call.toolName === PACE_HELD_TOOL) {
30680
+ if (calls.length === 1 && call.toolName === PACE_HELD_TOOL) {
29930
30681
  const held = paceHoldText(input, call.toolInput, paceDeps);
29931
30682
  if (held !== null)
29932
30683
  return `${preToolUseReplyText("refuse", held)}
@@ -29939,7 +30690,16 @@ function runPreToolUse(deps) {
29939
30690
  return `${preToolUseReplyText("ask", answer.question)}
29940
30691
  `;
29941
30692
  }
29942
- 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)}
29943
30703
  `;
29944
30704
  } catch {
29945
30705
  return "";
@@ -29953,7 +30713,7 @@ function answerModeOf(input, deps, at) {
29953
30713
  const cached2 = engagementId === void 0 ? null : readCachedAutonomy(engagementId, at.getTime(), deps.home);
29954
30714
  if (engagementId !== void 0 && cached2 !== null)
29955
30715
  return { mode: "autonomous", autonomy: { engagementId, mayPublish: cached2.mayPublish } };
29956
- 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 };
29957
30717
  }
29958
30718
  function autonomousReply(call, staked, sendsSaves, mode, autonomy, deps) {
29959
30719
  try {
@@ -30033,11 +30793,40 @@ function recordedOnlyReply(mode) {
30033
30793
  }
30034
30794
  }
30035
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
+ }
30036
30825
  function ownCopyOf(input, call, deps) {
30037
30826
  try {
30038
30827
  const walk2 = deps.walk ?? deps.liveness?.walk;
30039
30828
  return judgeOwnCopy(input, call, {
30040
- projectDir: deps.projectDir ?? projectFolder(deps.env ?? process.env),
30829
+ projectDir: projectDirOf(deps),
30041
30830
  home: deps.home ?? homedir10(),
30042
30831
  ...walk2 === void 0 ? {} : { walk: walk2 },
30043
30832
  ...deps.liveness === void 0 ? {} : { liveness: deps.liveness }
@@ -30047,7 +30836,7 @@ function ownCopyOf(input, call, deps) {
30047
30836
  }
30048
30837
  }
30049
30838
  function ownCopyNotice(input, deps) {
30050
- const folder = stringOf(input, "cwd") ?? deps.projectDir ?? projectFolder(deps.env ?? process.env);
30839
+ const folder = stringOf(input, "cwd") ?? projectDirOf(deps);
30051
30840
  return firstNoticeThisSession("outside-own-copy-unknown", folder, stringOf(input, "session_id"), deps.liveness?.dir) ? `${preToolUseNoticeText(OWN_COPY_UNKNOWN_NOTICE)}
30052
30841
  ` : "";
30053
30842
  }
@@ -30063,23 +30852,16 @@ function stringOf(input, key) {
30063
30852
  const v = input[key];
30064
30853
  return typeof v === "string" && v !== "" ? v : void 0;
30065
30854
  }
30066
- function toolCallOf(input) {
30067
- const toolName = stringOf(input, "tool_name");
30068
- const toolInput = input["tool_input"];
30069
- if (toolName === void 0 || typeof toolInput !== "object" || toolInput === null || Array.isArray(toolInput))
30070
- return null;
30071
- return { toolName, toolInput };
30072
- }
30073
30855
  function unreadable(deps, input) {
30074
30856
  const session = (input === null ? void 0 : stringOf(input, "session_id")) ?? /"session_id"\s*:\s*"([^"\\]+)"/.exec(deps.input)?.[1];
30075
- 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);
30076
30858
  return firstNoticeThisSession("pre-tool-use-unreadable", folder, session) ? `${preToolUseNoticeText(UNREADABLE_INPUT_NOTICE)}
30077
30859
  ` : "";
30078
30860
  }
30079
30861
 
30080
30862
  // dist/hooks/session-diff.js
30081
- import { unlinkSync as unlinkSync9 } from "node:fs";
30082
- import { join as join16 } from "node:path";
30863
+ import { unlinkSync as unlinkSync10 } from "node:fs";
30864
+ import { join as join17 } from "node:path";
30083
30865
  var STOP_HOOK_EVENT = {
30084
30866
  stop: "Stop",
30085
30867
  "subagent-stop": "SubagentStop"
@@ -30114,14 +30896,14 @@ function collectDiff(repoRoot, base) {
30114
30896
  getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
30115
30897
  };
30116
30898
  }
30117
- async function runSessionDiff(input, kind, readSeat = readSeatConfiguration, afterPass) {
30118
- const reply = hookReply(STOP_HOOK_EVENT[kind]);
30119
- 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);
30120
30902
  if (afterPass)
30121
30903
  await afterPass(reply);
30122
30904
  reply.flush();
30123
30905
  }
30124
- async function evaluateSession(input, kind, reply, readSeat) {
30906
+ async function evaluateSession(input, kind, reply, readSeat, profile) {
30125
30907
  const repoRootResult = getRepoRoot(process.cwd());
30126
30908
  if (!repoRootResult.ok) {
30127
30909
  if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
@@ -30144,7 +30926,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
30144
30926
  const candidates = ownEditedFiles(repoRoot, sessionId).filter((relPath) => !tracked.has(relPath));
30145
30927
  const untracked = getUntrackedFiles(repoRoot, candidates);
30146
30928
  const created = untracked.ok ? untracked.value.filter((relPath) => !tracked.has(relPath)) : [];
30147
- 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);
30148
30930
  const { evaluated } = partitionInstallerOutput(collected.files);
30149
30931
  const { evaluated: createdEvaluated } = partitionInstallerOutput(created);
30150
30932
  const changedFiles2 = [...ownChangedFiles(repoRoot, evaluated, sessionId), ...createdEvaluated];
@@ -30170,11 +30952,11 @@ async function evaluateSession(input, kind, reply, readSeat) {
30170
30952
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
30171
30953
  if (!firstNoticeThisSession("credential-missing", repoRoot, sessionId))
30172
30954
  return;
30173
- emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
30955
+ emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()), reply);
30174
30956
  return;
30175
30957
  }
30176
30958
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
30177
- const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
30959
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat, profile);
30178
30960
  const evaluation = await evaluateWithRefresh({
30179
30961
  guardServiceUrl,
30180
30962
  token: guardServiceToken,
@@ -30205,7 +30987,8 @@ async function evaluateSession(input, kind, reply, readSeat) {
30205
30987
  // The remedy the renewal attempt earned: the sign-in when the account
30206
30988
  // credential is what was refused, and otherwise the plane's own sentence
30207
30989
  // about why it would not mint.
30208
- evaluation.remedy
30990
+ evaluation.remedy,
30991
+ reply
30209
30992
  );
30210
30993
  return;
30211
30994
  }
@@ -30261,7 +31044,7 @@ function removeFindingsFile(sessionId) {
30261
31044
  if (path === null)
30262
31045
  return;
30263
31046
  try {
30264
- unlinkSync9(path);
31047
+ unlinkSync10(path);
30265
31048
  } catch {
30266
31049
  }
30267
31050
  }
@@ -30307,9 +31090,9 @@ async function readHookStdin() {
30307
31090
  }
30308
31091
  }
30309
31092
  function readStdin2() {
30310
- return new Promise((resolve8) => {
31093
+ return new Promise((resolve10) => {
30311
31094
  if (process.stdin.isTTY) {
30312
- resolve8("");
31095
+ resolve10("");
30313
31096
  return;
30314
31097
  }
30315
31098
  const chunks = [];
@@ -30319,7 +31102,7 @@ function readStdin2() {
30319
31102
  return;
30320
31103
  settled = true;
30321
31104
  clearTimeout(timer);
30322
- resolve8(Buffer.concat(chunks).toString("utf-8"));
31105
+ resolve10(Buffer.concat(chunks).toString("utf-8"));
30323
31106
  };
30324
31107
  const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
30325
31108
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -30329,11 +31112,11 @@ function readStdin2() {
30329
31112
  }
30330
31113
 
30331
31114
  // dist/hooks/subagent-stop.js
30332
- async function runSubagentStop() {
30333
- const input = await readHookStdin();
31115
+ async function runSubagentStop(profile = AGENT_PROFILES["claude-code"]) {
31116
+ const input = hookInputForRule(await readHookStdin(), profile.configFolder);
30334
31117
  recordTimeEvent("turn-end", input);
30335
- subagentStopped(input);
30336
- await runSessionDiff(input, "subagent-stop");
31118
+ subagentStopped(input, { pace: { configFolder: profile.configFolder } });
31119
+ await runSessionDiff(input, "subagent-stop", void 0, void 0, profile);
30337
31120
  }
30338
31121
  function subagentStopped(input, deps = {}) {
30339
31122
  if (clearLiveness(input, deps.liveness) === 0)
@@ -30341,14 +31124,14 @@ function subagentStopped(input, deps = {}) {
30341
31124
  }
30342
31125
 
30343
31126
  // dist/hooks/stop.js
30344
- async function runStop(observeDeps = {}, timeDeps = {}, paceDeps = {}) {
30345
- const input = await readHookStdin();
31127
+ async function runStop(observeDeps = {}, timeDeps = {}, paceDeps = {}, profile = AGENT_PROFILES["claude-code"]) {
31128
+ const input = hookInputForRule(await readHookStdin(), profile.configFolder);
30346
31129
  recordTimeEvent("turn-end", input);
30347
31130
  await runSessionDiff(input, "stop", readSeatConfiguration, async (reply) => {
30348
31131
  await observeChanges(input.session_id, reply, observeDeps);
30349
31132
  await sendTimeRecord(timeDeps);
30350
- await paceAtTurnEnd(input, reply, paceDeps);
30351
- });
31133
+ await paceAtTurnEnd(input, reply, { configFolder: profile.configFolder, ...paceDeps });
31134
+ }, profile);
30352
31135
  }
30353
31136
 
30354
31137
  // dist/hooks/scope-phrase.js
@@ -30429,6 +31212,9 @@ function reviewPhraseShape(prompt) {
30429
31212
  const line2 = prompt.trim();
30430
31213
  if (LINE_BREAK.test(line2))
30431
31214
  return null;
31215
+ const reject = REJECT_VERB.exec(line2);
31216
+ if (reject !== null)
31217
+ return rejectShape(line2.slice(reject[0].length));
30432
31218
  for (const [prefix, act] of VERBS) {
30433
31219
  const verb = prefix.exec(line2);
30434
31220
  if (verb === null)
@@ -30446,6 +31232,21 @@ function reviewPhraseShape(prompt) {
30446
31232
  }
30447
31233
  return null;
30448
31234
  }
31235
+ var REJECT_VERB = /^reject /i;
31236
+ function rejectShape(rest) {
31237
+ const colon = rest.indexOf(":");
31238
+ if (colon === -1)
31239
+ return null;
31240
+ const head = rest.slice(0, colon);
31241
+ const slash = head.indexOf("/");
31242
+ if (slash === -1)
31243
+ return null;
31244
+ const reference = head.slice(0, slash);
31245
+ const task = head.slice(slash + 1);
31246
+ if (!isValidPhaseIdentity(reference) || /\s/.test(task) || !buildTaskIdSchema.safeParse(task).success)
31247
+ return null;
31248
+ return { kind: "reject", reference, task, words: rest.slice(colon + 1).trim() };
31249
+ }
30449
31250
  function partShape(act, head, words) {
30450
31251
  const slash = head.indexOf("/");
30451
31252
  if (slash === -1)
@@ -30458,10 +31259,15 @@ function partShape(act, head, words) {
30458
31259
  }
30459
31260
  function matchReviewPhraseShape(shape, reviews) {
30460
31261
  const resolved = resolvePhaseReference(shape.reference, reviews);
30461
- const act = shape.kind === "phase" ? "accept" : shape.act;
31262
+ const act = shape.kind === "phase" ? "accept" : shape.kind === "reject" ? "reject" : shape.act;
30462
31263
  if (resolved.kind === "ambiguous")
30463
31264
  return { kind: "ambiguous", act, entries: resolved.entries };
30464
31265
  const entry = resolved.kind === "found" ? resolved.entry : null;
31266
+ if (shape.kind === "reject") {
31267
+ if (entry === null)
31268
+ return { kind: "show-first", act: "reject" };
31269
+ return { kind: "reject", entry, reference: shape.reference, task: shape.task, words: shape.words };
31270
+ }
30465
31271
  if (shape.kind === "phase") {
30466
31272
  if (entry === null)
30467
31273
  return { kind: "nothing" };
@@ -30481,20 +31287,20 @@ function matchReviewPhraseShape(shape, reviews) {
30481
31287
  }
30482
31288
 
30483
31289
  // dist/hooks/shown-record.js
30484
- 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";
30485
31291
  import { homedir as homedir11, platform as platform3 } from "node:os";
30486
- import { join as join17 } from "node:path";
31292
+ import { join as join18 } from "node:path";
30487
31293
  function recordDir(engagementId, home) {
30488
31294
  if (!isEngagementId(engagementId))
30489
31295
  return null;
30490
- return join17(home ?? homedir11(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
31296
+ return join18(home ?? homedir11(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
30491
31297
  }
30492
31298
  function readShownRecord(engagementId, home) {
30493
31299
  const dir = recordDir(engagementId, home);
30494
31300
  if (dir === null)
30495
31301
  return { entries: [], reviews: [] };
30496
31302
  try {
30497
- return parseShownRecord(readFileSync23(join17(dir, SHOWN_PLANS_FILENAME), "utf-8"));
31303
+ return parseShownRecord(readFileSync24(join18(dir, SHOWN_PLANS_FILENAME), "utf-8"));
30498
31304
  } catch {
30499
31305
  return { entries: [], reviews: [] };
30500
31306
  }
@@ -30515,24 +31321,24 @@ function moveListedReviewForward(engagementId, home, listed, reviewVersion, deci
30515
31321
  const dir = recordDir(engagementId, home);
30516
31322
  if (dir === null)
30517
31323
  return false;
30518
- const path = join17(dir, SHOWN_PLANS_FILENAME);
30519
- const current = parseShownRecord(readFileSync23(path, "utf-8"));
31324
+ const path = join18(dir, SHOWN_PLANS_FILENAME);
31325
+ const current = parseShownRecord(readFileSync24(path, "utf-8"));
30520
31326
  const at = current.reviews.findIndex((r) => r.phase === listed.phase && r.shownAt === listed.shownAt && r.reviewVersion === listed.reviewVersion);
30521
31327
  if (at === -1)
30522
31328
  return false;
30523
31329
  const acts = new Map(decided.map((d) => [d.part, d.act]));
30524
31330
  const reviews = current.reviews.map((review, i) => i === at ? { ...review, reviewVersion, parts: review.parts.map((p) => movedPart(p, acts.get(p.part))) } : review);
30525
31331
  const body = serialiseShownRecord({ entries: current.entries, reviews });
30526
- temp = join17(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
30527
- writeFileSync14(temp, body, { mode: 384 });
31332
+ temp = join18(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
31333
+ writeFileSync15(temp, body, { mode: 384 });
30528
31334
  if (platform3() !== "win32")
30529
31335
  chmodSync4(temp, 384);
30530
- renameSync12(temp, path);
31336
+ renameSync13(temp, path);
30531
31337
  return true;
30532
31338
  } catch {
30533
31339
  if (temp !== null) {
30534
31340
  try {
30535
- unlinkSync10(temp);
31341
+ unlinkSync11(temp);
30536
31342
  } catch {
30537
31343
  return false;
30538
31344
  }
@@ -30650,6 +31456,8 @@ function reviewWrongActLine(name, built) {
30650
31456
  }
30651
31457
  function wordsActNoun(act) {
30652
31458
  switch (act) {
31459
+ case "reject":
31460
+ return "a rejection";
30653
31461
  case "question":
30654
31462
  return "a question";
30655
31463
  case "send-back":
@@ -30686,7 +31494,10 @@ function reviewPartNotListedLine(part, phase) {
30686
31494
  return line(`${NOTHING_RECORDED}: "${part}" is not a part of the review of ${namedPhase(phase)} as it was listed here.`);
30687
31495
  }
30688
31496
  function reviewNothingToAcceptLine(phase) {
30689
- return line(`${NOTHING_RECORDED}: no built part of ${namedPhase(phase)} waits for your decision.`);
31497
+ return line(`${NOTHING_RECORDED}: no built part and no check nobody ran of ${namedPhase(phase)} waits for your decision.`);
31498
+ }
31499
+ function reviewNoClaimWaitingLine(task, phase) {
31500
+ return line(`${NOTHING_RECORDED}: no check nobody ran on task "${task}" of ${namedPhase(phase)} waits for your answer. ${LIST_AGAIN} and copy the task from its reject line.`);
30690
31501
  }
30691
31502
  function reviewListedTooLongAgoLine(phase) {
30692
31503
  return line(`${NOTHING_RECORDED}: ${namedPhase(phase)} was listed an hour or more ago. ${LIST_AGAIN}, then type the line again within the hour.`);
@@ -30762,9 +31573,12 @@ function reviewRecordedLine(decided, phase, waiting) {
30762
31573
  }
30763
31574
  function reviewBulkRecordedLine(accepted, phase, waiting) {
30764
31575
  const named = namedPhase(phase);
30765
- const first = accepted === 1 ? `Recorded in your session: 1 part of ${named} accepted.` : `Recorded in your session: ${accepted} parts of ${named} accepted.`;
31576
+ const first = accepted === 0 ? `Recorded in your session: any check nobody ran of ${named} that waited is accepted.` : accepted === 1 ? `Recorded in your session: 1 part of ${named} accepted, with any check nobody ran that waited.` : `Recorded in your session: ${accepted} parts of ${named} accepted, with any check nobody ran that waited.`;
30766
31577
  return line(`${first} ${stillWaiting(waiting)}`);
30767
31578
  }
31579
+ function reviewClaimsRejectedLine(task, phase, waiting) {
31580
+ return line(`Recorded in your session: the checks nobody ran on task "${task}" are rejected, with your words. ${stillWaitingOf(phase, waiting)}`);
31581
+ }
30768
31582
  function reviewClosedLine(phase) {
30769
31583
  return `Recorded in your session. That was the last decision: phase ${namedPhase(phase)} is closed. Run its close step now, then tell the person what it says.`;
30770
31584
  }
@@ -30791,12 +31605,16 @@ function reviewLineForAnswer(context, answer) {
30791
31605
  switch (answer.outcome) {
30792
31606
  case "recorded": {
30793
31607
  if (answer.closed) {
30794
- return answer.decided.length === 0 ? reviewNoPartsClosedLine(phase) : reviewClosedLine(phase);
31608
+ return context.listedParts === 0 ? reviewNoPartsClosedLine(phase) : reviewClosedLine(phase);
30795
31609
  }
31610
+ if (context.task !== null)
31611
+ return reviewClaimsRejectedLine(context.task, phase, answer.waiting);
30796
31612
  if (context.bulk)
30797
31613
  return reviewBulkRecordedLine(answer.decided.length, phase, answer.waiting);
30798
31614
  return reviewRecordedLine(answer.decided[0], phase, answer.waiting);
30799
31615
  }
31616
+ case "nothing-waiting":
31617
+ return context.task !== null ? reviewNoClaimWaitingLine(context.task, phase) : reviewNothingToAcceptLine(phase);
30800
31618
  case "review-changed":
30801
31619
  return reviewChangedLine(phase);
30802
31620
  case "no-review":
@@ -30864,16 +31682,63 @@ function autonomyLineForAnswer(answer) {
30864
31682
  }
30865
31683
  }
30866
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
+ }
30867
31712
 
30868
31713
  // dist/hooks/user-prompt-submit.js
30869
31714
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
30870
31715
  var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
30871
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) {
30872
31734
  let act = null;
30873
31735
  let autonomyAct = null;
30874
31736
  const review = { recognised: false };
30875
31737
  try {
30876
31738
  const prompt = promptOf(opts.input);
31739
+ if (isAllowReply(prompt))
31740
+ return `${decideAllow(opts)}
31741
+ `;
30877
31742
  const autonomyShape = autonomyPhraseShape(prompt);
30878
31743
  if (autonomyShape !== null) {
30879
31744
  autonomyAct = autonomyShape;
@@ -30903,9 +31768,35 @@ async function runUserPromptSubmit(opts) {
30903
31768
  `;
30904
31769
  }
30905
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
+ }
30906
31794
  async function decideAutonomy(act, opts) {
30907
31795
  const env = opts.env ?? process.env;
30908
- if (!readSessionAttended(env))
31796
+ const typed = whoTyped(opts);
31797
+ if (typed === "cannot-tell")
31798
+ return codexAutonomyLine();
31799
+ if (typed === "not-attended")
30909
31800
  return autonomyNotAttendedLine();
30910
31801
  const read = readConfig(env);
30911
31802
  if (read.config === null) {
@@ -30951,12 +31842,18 @@ async function decideReview(shape, opts, progress) {
30951
31842
  if (match2.kind === "nothing")
30952
31843
  return "";
30953
31844
  progress.recognised = true;
30954
- if (!readSessionAttended(env))
31845
+ const typed2 = whoTyped(opts);
31846
+ if (typed2 === "cannot-tell")
31847
+ return codexReviewLine("accept");
31848
+ if (typed2 === "not-attended")
30955
31849
  return reviewNotAttendedLine();
30956
31850
  return checkAndSendReview(match2, read2.config.guardEngagementId, read2.config.guardServiceToken, env, opts);
30957
31851
  }
30958
31852
  progress.recognised = true;
30959
- 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")
30960
31857
  return reviewNotAttendedLine();
30961
31858
  const read = readConfig(env);
30962
31859
  if (read.config === null) {
@@ -30977,14 +31874,22 @@ async function checkAndSendReview(match, engagementId, credential, env, opts) {
30977
31874
  return reviewListedTooLongAgoLine(entry.phase);
30978
31875
  let candidate;
30979
31876
  if (match.kind === "accept") {
30980
- if (entry.parts.length > 0 && match.parts.length === 0)
30981
- return reviewNothingToAcceptLine(entry.phase);
30982
31877
  candidate = {
30983
31878
  act: "accept",
30984
31879
  reference: match.reference,
30985
31880
  reviewVersion: entry.reviewVersion,
30986
- parts: match.parts.map((p) => p.part)
31881
+ parts: match.parts.map((p) => p.part),
31882
+ waitingClaims: true
30987
31883
  };
31884
+ } else if (match.kind === "reject") {
31885
+ const words = match.words;
31886
+ if (words === "")
31887
+ return reviewNoWordsLine("reject");
31888
+ if (!reviewNoteWireSchema.safeParse(words).success)
31889
+ return reviewWordsTooLongLine();
31890
+ if (looksLikeSecret(words))
31891
+ return reviewWordsLookSecretLine();
31892
+ candidate = { act: "reject", reference: match.reference, reviewVersion: entry.reviewVersion, task: match.task, note: words };
30988
31893
  } else {
30989
31894
  const { act, part } = match;
30990
31895
  if (!reviewActsFor(part.built).includes(act))
@@ -31037,7 +31942,14 @@ async function postReview(bulk, entry, request, engagementId, credential, env, o
31037
31942
  return reviewCannotReachLine();
31038
31943
  moveListedReviewForward(engagementId, opts.home, entry, answer.reviewVersion, answer.decided);
31039
31944
  }
31040
- return reviewLineForAnswer({ act: request.act, bulk, phase: entry.phase, number: entry.number }, answer);
31945
+ return reviewLineForAnswer({
31946
+ act: request.act,
31947
+ bulk,
31948
+ phase: entry.phase,
31949
+ number: entry.number,
31950
+ listedParts: entry.parts.length,
31951
+ task: request.act === "reject" ? request.task : null
31952
+ }, answer);
31041
31953
  } catch {
31042
31954
  return reviewCannotReachLine();
31043
31955
  } finally {
@@ -31047,7 +31959,10 @@ async function postReview(bulk, entry, request, engagementId, credential, env, o
31047
31959
  async function decide2(shape, opts) {
31048
31960
  const { act } = shape;
31049
31961
  const env = opts.env ?? process.env;
31050
- if (!readSessionAttended(env))
31962
+ const typed = whoTyped(opts);
31963
+ if (typed === "cannot-tell")
31964
+ return codexScopeLine(act);
31965
+ if (typed === "not-attended")
31051
31966
  return notAttendedLine(act);
31052
31967
  const read = readConfig(env);
31053
31968
  if (read.config === null) {
@@ -31080,6 +31995,16 @@ async function decide2(shape, opts) {
31080
31995
  return showFirstLine(act);
31081
31996
  return post(act, entry, request.data, guardEngagementId, guardServiceToken, env, opts.fetchFn);
31082
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
+ }
31083
32008
  function promptOf(input) {
31084
32009
  try {
31085
32010
  const parsed = JSON.parse(input);
@@ -31141,17 +32066,17 @@ function runUserPromptFindings(opts) {
31141
32066
  }
31142
32067
 
31143
32068
  // dist/resolve-engagement.js
31144
- import { existsSync as existsSync8, readFileSync as readFileSync24, realpathSync as realpathSync8 } from "node:fs";
31145
- 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";
31146
32071
  import { homedir as homedir12 } from "node:os";
31147
32072
  function defaultRegistryPath() {
31148
- return join18(homedir12(), ".halfcycle", "registry.json");
32073
+ return join19(homedir12(), ".halfcycle", "registry.json");
31149
32074
  }
31150
32075
  function readRegistry(registryPath = defaultRegistryPath()) {
31151
32076
  if (!existsSync8(registryPath))
31152
32077
  return {};
31153
32078
  try {
31154
- const parsed = JSON.parse(readFileSync24(registryPath, "utf-8"));
32079
+ const parsed = JSON.parse(readFileSync25(registryPath, "utf-8"));
31155
32080
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
31156
32081
  return {};
31157
32082
  const out = {};
@@ -31187,7 +32112,7 @@ function resolveEngagement(cwd, registry2) {
31187
32112
  function isSelfOrAncestor(ancestor, target) {
31188
32113
  if (ancestor === target)
31189
32114
  return true;
31190
- const withSep = ancestor.endsWith(sep4) ? ancestor : ancestor + sep4;
32115
+ const withSep = ancestor.endsWith(sep5) ? ancestor : ancestor + sep5;
31191
32116
  return target.startsWith(withSep);
31192
32117
  }
31193
32118
  function runResolveEngagement(cwd, registryPath) {
@@ -31199,13 +32124,27 @@ function runResolveEngagement(cwd, registryPath) {
31199
32124
  }
31200
32125
 
31201
32126
  // dist/refresh-credential.js
31202
- import { readFileSync as readFileSync26 } from "node:fs";
31203
- import { resolve as resolve7 } from "node:path";
32127
+ import { readFileSync as readFileSync27 } from "node:fs";
32128
+ import { resolve as resolve9 } from "node:path";
31204
32129
 
31205
32130
  // dist/mcp-headers.js
31206
- import { readFileSync as readFileSync25 } from "node:fs";
32131
+ import { readFileSync as readFileSync26 } from "node:fs";
31207
32132
  import { homedir as homedir13 } from "node:os";
31208
- 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
+ }
31209
32148
  function defaultWriteErr(text) {
31210
32149
  process.stderr.write(text);
31211
32150
  }
@@ -31218,12 +32157,12 @@ function headerLine(bearer, clientVersion) {
31218
32157
  }
31219
32158
  function projectRootFromBinary(binaryPath) {
31220
32159
  const depth = VENDORED_BIN_REL.split("/").length - 1;
31221
- return resolve6(dirname9(binaryPath), ...Array(depth).fill(".."));
32160
+ return resolve8(dirname9(binaryPath), ...Array(depth).fill(".."));
31222
32161
  }
31223
32162
  function readPinnedVersion(root) {
31224
32163
  let raw;
31225
32164
  try {
31226
- raw = readFileSync25(resolve6(root, BUNDLE_PIN_REL), "utf-8");
32165
+ raw = readFileSync26(resolve8(root, BUNDLE_PIN_REL), "utf-8");
31227
32166
  } catch {
31228
32167
  return void 0;
31229
32168
  }
@@ -31265,7 +32204,7 @@ function sentences(lines2) {
31265
32204
  }
31266
32205
  function badIdSentence(root) {
31267
32206
  return sentences([
31268
- `${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,`,
31269
32208
  'so no credential was read. Delete that file and run "npx halfcycle" again: this repository',
31270
32209
  "is then set up as a new Halfcycle project."
31271
32210
  ]);
@@ -31279,7 +32218,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
31279
32218
  ]);
31280
32219
  case "no-id":
31281
32220
  return sentences([
31282
- `${resolve6(root, BUNDLE_PIN_REL)} names no engagement id.`,
32221
+ `${resolve8(root, BUNDLE_PIN_REL)} names no engagement id.`,
31283
32222
  'run "npx halfcycle" here to rewrite it.'
31284
32223
  ]);
31285
32224
  case "bad-id":
@@ -31296,7 +32235,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
31296
32235
  case "no-credential":
31297
32236
  return sentences([
31298
32237
  `this machine holds no credential for engagement ${engagementId ?? ""}.`,
31299
- `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.`
31300
32239
  ]);
31301
32240
  }
31302
32241
  }
@@ -31358,13 +32297,14 @@ async function chooseBearer(opts) {
31358
32297
  }
31359
32298
  return NO_CREDENTIAL_PLACEHOLDER;
31360
32299
  }
32300
+ const registrationFile = opts.registrationFile ?? MCP_REGISTRATION_REL;
31361
32301
  const want = urlOrigin(recorded);
31362
32302
  const got = requested === void 0 ? void 0 : urlOrigin(requested);
31363
32303
  if (want === void 0 || got !== want) {
31364
32304
  writeErr(sentences([
31365
- `.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 ?? "")},`,
31366
32306
  `which is not this machine's Halfcycle server (${want ?? "none recorded"}). The credential was NOT sent.`,
31367
- '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.`
31368
32308
  ]));
31369
32309
  return NO_CREDENTIAL_PLACEHOLDER;
31370
32310
  }
@@ -31384,13 +32324,18 @@ async function chooseBearer(opts) {
31384
32324
  async function mcpHeaders(opts) {
31385
32325
  const writeErr = opts.writeErr ?? defaultWriteErr;
31386
32326
  const root = projectRootFromBinary(opts.binaryPath);
32327
+ const variableUrl = readMcpServerUrl();
31387
32328
  try {
31388
32329
  return headerLine(await chooseBearer({
31389
- ...opts,
32330
+ binaryPath: opts.binaryPath,
32331
+ home: opts.home,
32332
+ fetchFn: opts.fetchFn,
32333
+ deadlineMs: opts.deadlineMs,
31390
32334
  writeErr,
31391
- requestedUrl: readMcpServerUrl(),
32335
+ requestedUrl: variableUrl ?? opts.url,
31392
32336
  renewal: "renew-here",
31393
- environmentTokenFirst: true
32337
+ environmentTokenFirst: true,
32338
+ registrationFile: variableUrl === void 0 && opts.url !== void 0 ? AGENT_PROFILES.codex.mcpRegistration.file : void 0
31394
32339
  }), readPinnedVersion(root));
31395
32340
  } catch (err) {
31396
32341
  try {
@@ -31404,6 +32349,25 @@ async function mcpHeaders(opts) {
31404
32349
  return headerLine(NO_CREDENTIAL_PLACEHOLDER, readPinnedVersion(root));
31405
32350
  }
31406
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
+ }
31407
32371
 
31408
32372
  // dist/refresh-credential.js
31409
32373
  var UNAUTHORIZED2 = 401;
@@ -31444,13 +32408,19 @@ async function refreshCredentialToken(opts) {
31444
32408
  return { token: refreshed.token };
31445
32409
  }
31446
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
+ }
31447
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.`;
31448
32418
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
31449
32419
  var SESSION_START_PROBE_ATTEMPTS = 2;
31450
32420
  function registeredMcpUrl(projectRoot) {
31451
32421
  let parsed;
31452
32422
  try {
31453
- parsed = JSON.parse(readFileSync26(resolve7(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
32423
+ parsed = JSON.parse(readFileSync27(resolve9(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
31454
32424
  } catch {
31455
32425
  return void 0;
31456
32426
  }
@@ -31622,6 +32592,7 @@ function defaultWriteErr2(text) {
31622
32592
  }
31623
32593
  async function runRefreshCredential(opts) {
31624
32594
  const writeErr = opts.writeErr ?? defaultWriteErr2;
32595
+ const profile = opts.profile ?? AGENT_PROFILES["claude-code"];
31625
32596
  const startedAt = Date.now();
31626
32597
  try {
31627
32598
  const renewalSaid = [];
@@ -31635,7 +32606,7 @@ async function runRefreshCredential(opts) {
31635
32606
  ]);
31636
32607
  renewalOpen = false;
31637
32608
  const parts = [];
31638
- if (CHECKED_SOURCES.has(input["source"])) {
32609
+ if (isCheckedSessionStart(input, profile)) {
31639
32610
  const connection = await checkConnection(renewedToken, { ...opts, writeErr });
31640
32611
  if (connection !== void 0) {
31641
32612
  parts.push(connection.sentence);
@@ -31657,7 +32628,7 @@ async function runRefreshCredential(opts) {
31657
32628
  }
31658
32629
  }
31659
32630
  }
31660
- if (CHECKED_SOURCES.has(input["source"])) {
32631
+ if (isCheckedSessionStart(input, profile)) {
31661
32632
  const config2 = readConfig().config;
31662
32633
  if (config2 !== null) {
31663
32634
  const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
@@ -31697,7 +32668,7 @@ async function runRefreshCredential(opts) {
31697
32668
  import { fileURLToPath } from "node:url";
31698
32669
 
31699
32670
  // dist/capture.js
31700
- 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";
31701
32672
  import { dirname as dirname10 } from "node:path";
31702
32673
  function runCapture(provider, endpoint, opts = {}) {
31703
32674
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -31734,7 +32705,7 @@ function runCapture(provider, endpoint, opts = {}) {
31734
32705
  const manifestFile = manifestPath(repoRoot);
31735
32706
  try {
31736
32707
  mkdirSync10(dirname10(fixture), { recursive: true });
31737
- writeFileSync15(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
32708
+ writeFileSync16(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
31738
32709
  } catch (err) {
31739
32710
  const msg = err instanceof Error ? err.message : String(err);
31740
32711
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -31765,7 +32736,7 @@ function readInputFile(fromFile) {
31765
32736
  if (!fromFile)
31766
32737
  return void 0;
31767
32738
  try {
31768
- return readFileSync27(fromFile, "utf-8");
32739
+ return readFileSync28(fromFile, "utf-8");
31769
32740
  } catch {
31770
32741
  return void 0;
31771
32742
  }
@@ -31777,10 +32748,10 @@ function relativeFixturePath(repoRoot, fixture) {
31777
32748
 
31778
32749
  // dist/prove.js
31779
32750
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
31780
- import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
31781
- 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";
31782
32753
  import { homedir as homedir14 } from "node:os";
31783
- import { dirname as dirname11, join as join19 } from "node:path";
32754
+ import { dirname as dirname11, join as join20 } from "node:path";
31784
32755
 
31785
32756
  // dist/test-counts.js
31786
32757
  function sum(values) {
@@ -31889,10 +32860,10 @@ function isProofTestPath(path) {
31889
32860
  function testRunsDir(engagementId, home) {
31890
32861
  if (!isEngagementId(engagementId))
31891
32862
  return null;
31892
- 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);
31893
32864
  }
31894
32865
  function commandIdOf(command) {
31895
- return createHash5("sha256").update(command, "utf8").digest("hex").slice(0, 16);
32866
+ return createHash6("sha256").update(command, "utf8").digest("hex").slice(0, 16);
31896
32867
  }
31897
32868
  var PLAIN_WORD = /^[A-Za-z0-9_./:=@%+,-]+$/;
31898
32869
  function commandFromWords(words, platform4 = process.platform) {
@@ -31977,19 +32948,19 @@ function parseProveArgs(args2) {
31977
32948
  function writeJsonFile2(path, value) {
31978
32949
  mkdirSync11(dirname11(path), { recursive: true, mode: 448 });
31979
32950
  const tmp = `${path}.${process.pid}.${randomUUID3()}.tmp`;
31980
- writeFileSync16(tmp, JSON.stringify(value), { mode: 384 });
31981
- renameSync13(tmp, path);
32951
+ writeFileSync17(tmp, JSON.stringify(value), { mode: 384 });
32952
+ renameSync14(tmp, path);
31982
32953
  }
31983
32954
  function subjectKey(subject) {
31984
32955
  return subject.kind === "task" ? ["task", subject.phase, subject.task] : ["change", subject.change];
31985
32956
  }
31986
32957
  function keptCheckPath(dir, subject, label) {
31987
- const key = createHash5("sha256").update(JSON.stringify([...subjectKey(subject), label]), "utf8").digest("hex");
31988
- 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`);
31989
32960
  }
31990
32961
  function readKeptCheck(dir, subject, label) {
31991
32962
  try {
31992
- const raw = JSON.parse(readFileSync28(keptCheckPath(dir, subject, label), "utf-8"));
32963
+ const raw = JSON.parse(readFileSync29(keptCheckPath(dir, subject, label), "utf-8"));
31993
32964
  if (typeof raw.command !== "string" || raw.command === "" || typeof raw.commandId !== "string")
31994
32965
  return null;
31995
32966
  const b = raw.before;
@@ -32000,21 +32971,21 @@ function readKeptCheck(dir, subject, label) {
32000
32971
  }
32001
32972
  }
32002
32973
  function queuePath(dir, runId) {
32003
- return join19(dir, QUEUE_DIR_NAME, `${runId}.json`);
32974
+ return join20(dir, QUEUE_DIR_NAME, `${runId}.json`);
32004
32975
  }
32005
32976
  function readQueue(dir) {
32006
32977
  let names;
32007
32978
  try {
32008
- 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"));
32009
32980
  } catch {
32010
32981
  return [];
32011
32982
  }
32012
32983
  const reports = [];
32013
32984
  for (const name of names) {
32014
- const path = join19(dir, QUEUE_DIR_NAME, name);
32985
+ const path = join20(dir, QUEUE_DIR_NAME, name);
32015
32986
  let parsed = null;
32016
32987
  try {
32017
- parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync28(path, "utf-8")));
32988
+ parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync29(path, "utf-8")));
32018
32989
  } catch {
32019
32990
  parsed = null;
32020
32991
  }
@@ -32027,7 +32998,7 @@ function readQueue(dir) {
32027
32998
  }
32028
32999
  function removeFile(path) {
32029
33000
  try {
32030
- unlinkSync11(path);
33001
+ unlinkSync12(path);
32031
33002
  } catch {
32032
33003
  }
32033
33004
  }
@@ -32094,7 +33065,7 @@ function runCommand(command, repoRoot, logPath, env, deps) {
32094
33065
  mkdirSync11(dirname11(logPath), { recursive: true, mode: 448 });
32095
33066
  const log = createWriteStream(logPath, { mode: 384 });
32096
33067
  const began = process.hrtime.bigint();
32097
- return new Promise((resolve8) => {
33068
+ return new Promise((resolve10) => {
32098
33069
  let timedOut = false;
32099
33070
  let started = true;
32100
33071
  let settled = false;
@@ -32129,7 +33100,7 @@ function runCommand(command, repoRoot, logPath, env, deps) {
32129
33100
  process.off("SIGINT", onSignal);
32130
33101
  process.off("SIGTERM", onSignal);
32131
33102
  const durationMs = Number((process.hrtime.bigint() - began) / 1000000n);
32132
- log.end(() => resolve8({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
33103
+ log.end(() => resolve10({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
32133
33104
  };
32134
33105
  child.on("error", (e) => {
32135
33106
  started = false;
@@ -32183,7 +33154,7 @@ function checkCommand(command, reason, repoRoot, env, home) {
32183
33154
  ...home === void 0 ? {} : { home }
32184
33155
  }));
32185
33156
  } catch {
32186
- return { silent: false, question: couldNotTellQuestion(reason ?? null), staked: [] };
33157
+ return { silent: false, question: couldNotTellQuestion(reason ?? null), refusal: couldNotTellRefusal(reason ?? null), staked: [] };
32187
33158
  }
32188
33159
  }
32189
33160
  async function runProve(args2, deps = {}) {
@@ -32251,13 +33222,13 @@ ${PROVE_USAGE}`);
32251
33222
  if (request.when === "before" && changedOutsideTests > 0)
32252
33223
  say(lateBeforeLine(changedOutsideTests));
32253
33224
  const runId = randomUUID3();
32254
- const logPath = join19(dir, `${runId}.log`);
33225
+ const logPath = join20(dir, `${runId}.log`);
32255
33226
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
32256
33227
  const deadline = deps.deadlineMs ?? PROVE_RUN_DEADLINE_MS;
32257
33228
  const end = await runCommand(command, repoRoot, logPath, env, deps);
32258
33229
  let output = "";
32259
33230
  try {
32260
- output = readFileSync28(logPath, "utf-8");
33231
+ output = readFileSync29(logPath, "utf-8");
32261
33232
  } catch {
32262
33233
  output = "";
32263
33234
  }
@@ -32307,7 +33278,7 @@ ${PROVE_USAGE}`);
32307
33278
 
32308
33279
  // dist/statusline.js
32309
33280
  import { spawn as spawn3 } from "node:child_process";
32310
- import { readFileSync as readFileSync29 } from "node:fs";
33281
+ import { readFileSync as readFileSync30 } from "node:fs";
32311
33282
  import nodePath5 from "node:path";
32312
33283
  var OWN_STATUS_LINE_DEADLINE_MS = 5e3;
32313
33284
  var PERSON_SETTINGS_FILENAME = "settings.json";
@@ -32345,7 +33316,7 @@ function halfcycleStatusLineOf(figures) {
32345
33316
  }
32346
33317
  function personStatusLineCommand(configFolder) {
32347
33318
  try {
32348
- 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"));
32349
33320
  const line2 = v["statusLine"];
32350
33321
  if (typeof line2 !== "object" || line2 === null)
32351
33322
  return null;
@@ -32369,14 +33340,14 @@ function personCommandEnv(env) {
32369
33340
  }
32370
33341
  return out;
32371
33342
  }
32372
- var runOwnCommandReal = (command, input, env) => new Promise((resolve8) => {
33343
+ var runOwnCommandReal = (command, input, env) => new Promise((resolve10) => {
32373
33344
  let out = "";
32374
33345
  let settled = false;
32375
33346
  const done = (v) => {
32376
33347
  if (settled)
32377
33348
  return;
32378
33349
  settled = true;
32379
- resolve8(v);
33350
+ resolve10(v);
32380
33351
  };
32381
33352
  try {
32382
33353
  const child = spawn3(command, {
@@ -32486,7 +33457,12 @@ async function main() {
32486
33457
  return;
32487
33458
  }
32488
33459
  if (cmd === "refresh-credential") {
32489
- 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 });
32490
33466
  process.stdout.write(reply, () => process.exit(0));
32491
33467
  return;
32492
33468
  }
@@ -32571,7 +33547,7 @@ async function main() {
32571
33547
  return;
32572
33548
  }
32573
33549
  if (cmd === "mcp-headers") {
32574
- const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
33550
+ const line2 = await headersVerbLine(fileURLToPath(import.meta.url), args.slice(1));
32575
33551
  process.stdout.write(line2, () => process.exit(0));
32576
33552
  return;
32577
33553
  }
@@ -32613,25 +33589,30 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32613
33589
  return;
32614
33590
  }
32615
33591
  if (cmd === "hook") {
33592
+ const profile = agentFromArguments(args.slice(2));
33593
+ if (profile === null) {
33594
+ unknownCommand();
33595
+ return;
33596
+ }
32616
33597
  if (sub === "post-tool-use") {
32617
- await runPostToolUse();
32618
- process.exit(process.exitCode ?? 0);
33598
+ await runPostToolUse(void 0, profile);
33599
+ process.exit(hookExitCode(profile));
32619
33600
  return;
32620
33601
  }
32621
33602
  if (sub === "stop") {
32622
- await runStop();
32623
- process.exit(process.exitCode ?? 0);
33603
+ await runStop(void 0, void 0, void 0, profile);
33604
+ process.exit(hookExitCode(profile));
32624
33605
  return;
32625
33606
  }
32626
33607
  if (sub === "subagent-stop") {
32627
- await runSubagentStop();
32628
- process.exit(process.exitCode ?? 0);
33608
+ await runSubagentStop(profile);
33609
+ process.exit(hookExitCode(profile));
32629
33610
  return;
32630
33611
  }
32631
33612
  if (sub === "pre-tool-use") {
32632
33613
  let reply = "";
32633
33614
  try {
32634
- reply = runPreToolUse({ input: await readStdin2() });
33615
+ reply = runPreToolUse({ input: await readStdin2(), profile });
32635
33616
  } catch {
32636
33617
  reply = "";
32637
33618
  }
@@ -32641,7 +33622,7 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32641
33622
  if (sub === "user-prompt-submit") {
32642
33623
  let reply = "";
32643
33624
  try {
32644
- reply = await runUserPromptSubmit({ input: await readStdin2() });
33625
+ reply = promptReplyFor(await runUserPromptSubmit({ input: await readStdin2(), agent: profile.id }), profile.id);
32645
33626
  } catch {
32646
33627
  reply = "";
32647
33628
  }
@@ -32664,17 +33645,20 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32664
33645
  process.exit(1);
32665
33646
  return;
32666
33647
  }
32667
- 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}"
32668
33652
  Usage: halfcycle-runner ci
32669
- halfcycle-runner hook post-tool-use
32670
- halfcycle-runner hook pre-tool-use
32671
- halfcycle-runner hook stop
32672
- halfcycle-runner hook subagent-stop
32673
- halfcycle-runner hook user-prompt-submit
32674
- halfcycle-runner hook user-prompt-findings
32675
- 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>]
32676
33660
  halfcycle-runner check-connection
32677
- halfcycle-runner mcp-headers
33661
+ halfcycle-runner mcp-headers [--url <address>]
32678
33662
  halfcycle-runner observe
32679
33663
  halfcycle-runner observe --change <sortingId>
32680
33664
  halfcycle-runner time
@@ -32688,16 +33672,20 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
32688
33672
  process.exit(1);
32689
33673
  }
32690
33674
  function readStdin3() {
32691
- return new Promise((resolve8, reject) => {
33675
+ return new Promise((resolve10, reject) => {
32692
33676
  const chunks = [];
32693
33677
  process.stdin.on("data", (chunk) => chunks.push(chunk));
32694
- process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
33678
+ process.stdin.on("end", () => resolve10(Buffer.concat(chunks).toString("utf-8")));
32695
33679
  process.stdin.on("error", reject);
32696
33680
  });
32697
33681
  }
32698
33682
  main().catch((err) => {
32699
- process.stderr.write(`halfcycle-runner: unexpected error: ${err instanceof Error ? err.message : String(err)}
32700
- Contact hello@halfcycle.ai if this keeps happening.
32701
- `);
32702
- 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));
32703
33691
  });