halfcycle 0.5.1 → 0.5.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcycle",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
5
5
  "commands": [
6
6
  {
@@ -14665,6 +14665,9 @@ var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
14665
14665
  var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
14666
14666
  var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
14667
14667
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
14668
+ var CONTEXT_BUDGET_CHECK_REL = ".claude/hooks/context-budget-check.sh";
14669
+ var CONTEXT_DRIFT_CHECK_REL = ".claude/hooks/context-drift-check.sh";
14670
+ var CONTEXT_DRIFT_MATCHER = "Edit|Write";
14668
14671
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
14669
14672
  var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
14670
14673
  function hookCommand(script, verb) {
@@ -14741,6 +14744,20 @@ var HALFCYCLE_HOOK_WIRING = [
14741
14744
  timeout: 5,
14742
14745
  command: hookCommand(SESSION_END_MARKER_REL),
14743
14746
  script: SESSION_END_MARKER_REL
14747
+ },
14748
+ {
14749
+ event: "SessionStart",
14750
+ matcher: null,
14751
+ timeout: 10,
14752
+ command: hookCommand(CONTEXT_BUDGET_CHECK_REL),
14753
+ script: CONTEXT_BUDGET_CHECK_REL
14754
+ },
14755
+ {
14756
+ event: "PostToolUse",
14757
+ matcher: CONTEXT_DRIFT_MATCHER,
14758
+ timeout: 5,
14759
+ command: hookCommand(CONTEXT_DRIFT_CHECK_REL),
14760
+ script: CONTEXT_DRIFT_CHECK_REL
14744
14761
  }
14745
14762
  ];
14746
14763
  var RETIRED_HOOK_COMMANDS = [
@@ -14750,7 +14767,9 @@ var RETIRED_HOOK_COMMANDS = [
14750
14767
  "bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
14751
14768
  "bash ./.claude/hooks/guard-runner.sh hook stop",
14752
14769
  "bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
14753
- "bash ./.claude/hooks/session-end-marker.sh"
14770
+ "bash ./.claude/hooks/session-end-marker.sh",
14771
+ "bash ./.claude/hooks/context-budget-check.sh",
14772
+ "bash ./.claude/hooks/context-drift-check.sh"
14754
14773
  ];
14755
14774
  var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
14756
14775
  ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
@@ -15154,7 +15173,9 @@ var CODEX_HOOK_WIRING = [
15154
15173
  codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
15155
15174
  codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
15156
15175
  codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
15157
- codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
15176
+ codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL),
15177
+ codexHookRow("SessionStart", null, 10, CONTEXT_BUDGET_CHECK_REL),
15178
+ codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 5, CONTEXT_DRIFT_CHECK_REL)
15158
15179
  ];
15159
15180
  var RETIRED_CODEX_HOOK_COMMANDS = [];
15160
15181
  function codexHookCommandSet(retired) {
@@ -18082,6 +18103,30 @@ var phaseReviewViewSchema = external_exports.discriminatedUnion("state", [
18082
18103
  phaseReviewOpenSchema,
18083
18104
  phaseReviewClosedSchema
18084
18105
  ]);
18106
+ function reviewPartAwaitingAnswer(part) {
18107
+ return part.questions.some((q) => q.agentAnswer === void 0);
18108
+ }
18109
+ function reviewPartSentBackIn(part, round) {
18110
+ return part.sentBack.some((s) => s.round === round);
18111
+ }
18112
+ function shownReviewEntryOf(view, shownAt) {
18113
+ const parts = view.parts.map((p) => ({
18114
+ part: p.part,
18115
+ name: p.name,
18116
+ built: p.built,
18117
+ decidable: p.decidable,
18118
+ awaitingAnswer: reviewPartAwaitingAnswer(p),
18119
+ sentBack: reviewPartSentBackIn(p, view.round)
18120
+ }));
18121
+ return {
18122
+ phase: view.phase,
18123
+ number: view.number,
18124
+ round: view.round,
18125
+ reviewVersion: view.reviewVersion,
18126
+ shownAt,
18127
+ parts
18128
+ };
18129
+ }
18085
18130
 
18086
18131
  // ../events/dist/client-version.js
18087
18132
  var CLIENT_VERSION_HEADER = "x-halfcycle-client";
@@ -18542,6 +18587,42 @@ var ownerWaitDeclarationSchema = external_exports.object({
18542
18587
  var ownerWaitDeclarationsSchema = external_exports.object({ declarations: external_exports.array(ownerWaitDeclarationSchema) }).strict();
18543
18588
  var OWNER_WAIT_TOO_MANY = `This project already has ${String(OWNER_WAIT_ADDITIONS_MAX)} things added to what waits for the owner. Ask the owner to take one off on their settings page in the console first; it cannot be done by typing in chat.`;
18544
18589
 
18590
+ // ../events/dist/owner-wait-answer.js
18591
+ var OWNER_WAIT_SHORT_ID_LENGTH = 8;
18592
+ var ownerWaitShortIdSchema = external_exports.string().regex(/^[0-9a-f]{8}$/);
18593
+ function ownerWaitShortIdOf(declarationId) {
18594
+ return declarationId.slice(0, OWNER_WAIT_SHORT_ID_LENGTH).toLowerCase();
18595
+ }
18596
+ var OWNER_WAIT_ANSWER_ACTS = ["approve", "decline"];
18597
+ var ownerWaitAnswerActSchema = external_exports.enum(OWNER_WAIT_ANSWER_ACTS);
18598
+ function ownerWaitApproveLineOf(declarationId) {
18599
+ return `approve wait ${ownerWaitShortIdOf(declarationId)}`;
18600
+ }
18601
+ function ownerWaitDeclineLineOf(declarationId) {
18602
+ return `decline wait ${ownerWaitShortIdOf(declarationId)}: <why>`;
18603
+ }
18604
+ var ownerWaitChatRequestSchema = external_exports.discriminatedUnion("act", [
18605
+ external_exports.object({ act: external_exports.literal("approve"), wait: ownerWaitShortIdSchema }).strict(),
18606
+ external_exports.object({ act: external_exports.literal("decline"), wait: ownerWaitShortIdSchema, note: scopeNoteWireSchema }).strict()
18607
+ ]);
18608
+ function bareOutcome3(outcome) {
18609
+ return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
18610
+ }
18611
+ var ownerWaitChatResultSchema = external_exports.discriminatedUnion("outcome", [
18612
+ external_exports.object({
18613
+ outcome: external_exports.literal("recorded"),
18614
+ act: ownerWaitAnswerActSchema,
18615
+ question: external_exports.string().min(1)
18616
+ }).strict(),
18617
+ external_exports.object({ outcome: external_exports.literal("refused"), problems: external_exports.array(systemDescriptionProblemSchema).min(1) }).strict(),
18618
+ bareOutcome3("not-owner"),
18619
+ bareOutcome3("no-owner"),
18620
+ bareOutcome3("not-found"),
18621
+ bareOutcome3("ambiguous"),
18622
+ bareOutcome3("not-offered"),
18623
+ bareOutcome3("unavailable")
18624
+ ]);
18625
+
18545
18626
  // ../events/dist/pacing.js
18546
18627
  var CLAUDE_PLANS = Object.freeze([
18547
18628
  Object.freeze({ id: "pro", words: "Pro", startingAgents: 2 }),
@@ -18744,6 +18825,8 @@ var autonomyActivityRowSchema = external_exports.object({
18744
18825
  // whether the agent says it can be undone
18745
18826
  how: statementText(STATEMENT_MAX).optional(),
18746
18827
  // how to undo it, when it can be
18828
+ wait: ownerWaitShortIdSchema.optional(),
18829
+ // the short id the owner types to answer a waiting decision
18747
18830
  at: utcIso8601
18748
18831
  // when it happened
18749
18832
  }).strict().superRefine((row, ctx) => {
@@ -18770,6 +18853,9 @@ var autonomyActivityRowSchema = external_exports.object({
18770
18853
  });
18771
18854
  }
18772
18855
  }
18856
+ if (row.kind !== "waiting" && row.wait !== void 0) {
18857
+ ctx.addIssue({ code: "custom", message: "only a waiting decision is answered by typing", path: ["wait"] });
18858
+ }
18773
18859
  if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
18774
18860
  ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
18775
18861
  }
@@ -29234,7 +29320,8 @@ function refusalFor(facts, description) {
29234
29320
  function heldStep(steps, description, why) {
29235
29321
  return {
29236
29322
  person: PERSON_HOLD_LINE,
29237
- agent: `${HELD_STEP_OPENER}${steps}. ${reasonSentence(description)} ${PRECAUTION_OPENER}${why}. ${HOLD_AGENT_INSTRUCTION}`
29323
+ agent: `${HELD_STEP_OPENER}${steps}. ${reasonSentence(description)} ${PRECAUTION_OPENER}${why}. ${HOLD_AGENT_INSTRUCTION}`,
29324
+ step: steps
29238
29325
  };
29239
29326
  }
29240
29327
  var STEP_VERBS = {
@@ -29651,6 +29738,7 @@ import { createHash as createHash5 } from "node:crypto";
29651
29738
  import { appendFileSync as appendFileSync4, readFileSync as readFileSync20, renameSync as renameSync10, unlinkSync as unlinkSync9, writeFileSync as writeFileSync12 } from "node:fs";
29652
29739
  var ALLOW_ONCE_MS = 5 * 60 * 1e3;
29653
29740
  var HELD_STEP_MAX_AGE_MS = 60 * 60 * 1e3;
29741
+ var STEP_MAX_LENGTH = 1e3;
29654
29742
  function isAllowReply(prompt) {
29655
29743
  return typeof prompt === "string" && prompt.trim().toLowerCase() === ALLOW_REPLY;
29656
29744
  }
@@ -29676,11 +29764,11 @@ function canonical(value) {
29676
29764
  }
29677
29765
  return JSON.stringify(value) ?? "null";
29678
29766
  }
29679
- function recordHeld(sessionId, key, deps = {}) {
29767
+ function recordHeld(sessionId, key, deps = {}, step = null) {
29680
29768
  const paths = allowOncePaths(sessionId, deps.dir);
29681
29769
  if (paths === null)
29682
29770
  return;
29683
- const record2 = { version: 1, session: sessionId, key, at: nowOf(deps) };
29771
+ const record2 = { version: 1, session: sessionId, key, at: nowOf(deps), ...isStep(step) ? { step } : {} };
29684
29772
  try {
29685
29773
  appendFileSync4(paths.held, `${JSON.stringify(record2)}
29686
29774
  `, { mode: 384 });
@@ -29688,23 +29776,26 @@ function recordHeld(sessionId, key, deps = {}) {
29688
29776
  return;
29689
29777
  }
29690
29778
  }
29691
- function armAllow(sessionId, deps = {}) {
29779
+ function armAllowStep(sessionId, deps = {}) {
29692
29780
  try {
29693
29781
  const paths = allowOncePaths(sessionId, deps.dir);
29694
29782
  if (paths === null)
29695
- return "nothing-held";
29783
+ return { outcome: "nothing-held", step: null };
29696
29784
  const now = nowOf(deps);
29697
- const keys = claimHeldKeys(paths.held, sessionId, now);
29785
+ const held = claimHeld(paths.held, sessionId, now);
29786
+ const keys = [...new Set(held.map((r) => r.key))];
29698
29787
  if (keys.length === 0)
29699
- return "nothing-held";
29788
+ return { outcome: "nothing-held", step: null };
29700
29789
  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";
29790
+ return { outcome: "several-held", step: null };
29791
+ const step = held[held.length - 1].step ?? null;
29792
+ const saved = writeRecord(paths.allow, { version: 1, session: sessionId, key: keys[0], at: now });
29793
+ return saved ? { outcome: "armed", step } : { outcome: "not-saved", step: null };
29703
29794
  } catch {
29704
- return "not-saved";
29795
+ return { outcome: "not-saved", step: null };
29705
29796
  }
29706
29797
  }
29707
- function claimHeldKeys(path, session, now) {
29798
+ function claimHeld(path, session, now) {
29708
29799
  const claimed = `${path}.${process.pid}.${now}.claim`;
29709
29800
  try {
29710
29801
  renameSync10(path, claimed);
@@ -29718,13 +29809,16 @@ function claimHeldKeys(path, session, now) {
29718
29809
  text = "";
29719
29810
  }
29720
29811
  removeQuietly(claimed);
29721
- const keys = /* @__PURE__ */ new Set();
29812
+ const records = [];
29722
29813
  for (const line2 of text.split("\n")) {
29723
29814
  const r = parseRecord(line2, session);
29724
29815
  if (r !== null && within(r.at, now, HELD_STEP_MAX_AGE_MS))
29725
- keys.add(r.key);
29816
+ records.push(r);
29726
29817
  }
29727
- return [...keys];
29818
+ return records;
29819
+ }
29820
+ function isStep(v) {
29821
+ return typeof v === "string" && v !== "" && v.length <= STEP_MAX_LENGTH;
29728
29822
  }
29729
29823
  function spendAllow(sessionId, key, deps = {}) {
29730
29824
  try {
@@ -29776,7 +29870,8 @@ function parseRecord(text, session) {
29776
29870
  return null;
29777
29871
  if (typeof r["at"] !== "number")
29778
29872
  return null;
29779
- return { version: 1, session, key: r["key"], at: r["at"] };
29873
+ const step = r["step"];
29874
+ return { version: 1, session, key: r["key"], at: r["at"], ...isStep(step) ? { step } : {} };
29780
29875
  }
29781
29876
  function writeRecord(path, record2) {
29782
29877
  const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
@@ -30696,7 +30791,7 @@ function runPreToolUse(deps) {
30696
30791
  const allowOnce = { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir }, now: () => at.getTime() };
30697
30792
  if (spendAllow(input["session_id"], key, allowOnce))
30698
30793
  return "";
30699
- recordHeld(input["session_id"], key, allowOnce);
30794
+ recordHeld(input["session_id"], key, allowOnce, answer.refusal.step);
30700
30795
  }
30701
30796
  }
30702
30797
  return `${preToolUseReplyText("refuse", answer.refusal.person, answer.refusal.agent)}
@@ -31257,6 +31352,14 @@ function partShape(act, head, words) {
31257
31352
  return null;
31258
31353
  return { kind: "part", act, reference, part, words };
31259
31354
  }
31355
+ function bulkAcceptTargetOf(reference, record2) {
31356
+ const phases = /* @__PURE__ */ new Map();
31357
+ for (const shown2 of [...record2.reviews, ...record2.entries]) {
31358
+ if (!phases.has(shown2.phase))
31359
+ phases.set(shown2.phase, { phase: shown2.phase, number: shown2.number });
31360
+ }
31361
+ return resolvePhaseReference(reference, [...phases.values()]);
31362
+ }
31260
31363
  function matchReviewPhraseShape(shape, reviews) {
31261
31364
  const resolved = resolvePhaseReference(shape.reference, reviews);
31262
31365
  const act = shape.kind === "phase" ? "accept" : shape.kind === "reject" ? "reject" : shape.act;
@@ -31347,6 +31450,28 @@ function moveListedReviewForward(engagementId, home, listed, reviewVersion, deci
31347
31450
  }
31348
31451
  }
31349
31452
 
31453
+ // dist/hooks/wait-phrase.js
31454
+ var APPROVE = /^approve wait ([0-9a-f]{8})$/i;
31455
+ var DECLINE = /^decline wait ([0-9a-f]{8})(?:\s*:(.*))?$/is;
31456
+ function waitPhraseShape(prompt) {
31457
+ if (typeof prompt !== "string")
31458
+ return null;
31459
+ const line2 = prompt.trim();
31460
+ if (LINE_BREAK.test(line2))
31461
+ return null;
31462
+ const approve = APPROVE.exec(line2);
31463
+ if (approve !== null)
31464
+ return shapeOf("approve", approve[1], null);
31465
+ const decline = DECLINE.exec(line2);
31466
+ if (decline !== null)
31467
+ return shapeOf("decline", decline[1], decline[2] === void 0 ? null : decline[2].trim());
31468
+ return null;
31469
+ }
31470
+ function shapeOf(act, typed, words) {
31471
+ const wait = typed.toLowerCase();
31472
+ return ownerWaitShortIdSchema.safeParse(wait).success ? { act, wait, words } : null;
31473
+ }
31474
+
31350
31475
  // dist/hooks/user-prompt-submit-lines.js
31351
31476
  var TELL_THE_PERSON = "Tell the person.";
31352
31477
  function line(text) {
@@ -31684,14 +31809,15 @@ function autonomyLineForAnswer(answer) {
31684
31809
  }
31685
31810
  var NOTHING_ALLOWED = "Nothing was allowed";
31686
31811
  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.`);
31812
+ function allowArmedLine(step = null) {
31813
+ const named = step === null ? "The person allowed the paused step once." : `The person allowed this paused step once: ${step}.`;
31814
+ return line(`${named} 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
31815
  }
31690
31816
  function allowNothingHeldLine() {
31691
31817
  return line(`${NOTHING_ALLOWED}: no step is paused in this session.`);
31692
31818
  }
31693
31819
  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}.`);
31820
+ return `${NOTHING_ALLOWED}: more than one step was paused, so Halfcycle could not tell which one you meant. Nothing ran. When your agent tries the step again, type ${ALLOW_REPLY} once more. ${SHOW_AS_WRITTEN}`;
31695
31821
  }
31696
31822
  function allowNotAttendedLine() {
31697
31823
  return line(`${NOTHING_ALLOWED}: a paused step can be allowed by typing only when a person types it into an open session.`);
@@ -31700,14 +31826,81 @@ function allowNotSavedLine() {
31700
31826
  return line(`${NOTHING_ALLOWED}: Halfcycle could not save the answer on this computer. Type ${ALLOW_REPLY} again.`);
31701
31827
  }
31702
31828
  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";
31829
+ var SHOW_AS_WRITTEN = "Show the person this line as written.";
31830
+ function codexLine(text) {
31831
+ return `${text} ${SHOW_AS_WRITTEN}`;
31832
+ }
31703
31833
  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.`);
31834
+ return act === "approve" ? codexLine(`${nothingDone(act)}: ${CODEX_TYPING_NEVER_COUNTS}. To approve, choose Accept in the console.`) : codexLine(`${nothingDone(act)}: ${CODEX_TYPING_NEVER_COUNTS}. The console offers only Accept, so work cannot be sent back from Codex.`);
31705
31835
  }
31706
31836
  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.`);
31837
+ return act === "accept" ? codexLine(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. To accept, choose Accept in the console.`) : codexLine(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. The console offers only Accept, so this cannot be done from Codex.`);
31708
31838
  }
31709
31839
  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.`);
31840
+ return codexLine(`${AUTONOMY_NOTHING_CHANGED}: ${CODEX_TYPING_NEVER_COUNTS}. Turn it on or off with "${AUTONOMY_A1_TITLE}" in the console's settings.`);
31841
+ }
31842
+ function waitTypedLine(context) {
31843
+ return context.act === "approve" ? ownerWaitApproveLineOf(context.wait) : ownerWaitDeclineLineOf(context.wait);
31844
+ }
31845
+ function waitRecordedLine(act, question) {
31846
+ return act === "approve" ? line(`Recorded in your session: you approved "${question}". The agent can go ahead with it.`) : line(`Recorded in your session: you declined "${question}", with your words. The agent must not go ahead with it.`);
31847
+ }
31848
+ function waitNotAttendedLine() {
31849
+ return line(`${NOTHING_RECORDED}: an answer counts only when a person types it into an open session.`);
31850
+ }
31851
+ function waitNotSignedInLine() {
31852
+ return line(`${NOTHING_RECORDED}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
31853
+ }
31854
+ function waitNotOwnerLine() {
31855
+ return line(`${NOTHING_RECORDED}: only the project's owner can answer a decision that waits for them.`);
31856
+ }
31857
+ function waitNoOwnerLine() {
31858
+ return line(`${NOTHING_RECORDED}: this project has no owner, so nobody can answer its decisions.`);
31859
+ }
31860
+ function waitNotFoundLine(context) {
31861
+ return line(`${NOTHING_RECORDED}: no decision waiting for you has the id ${context.wait}. Type the line the agent gave you with its question, such as ${waitTypedLine(context)}.`);
31862
+ }
31863
+ function waitAmbiguousLine(context) {
31864
+ return line(`${NOTHING_RECORDED}: more than one decision waiting for you starts with ${context.wait}. Ask the agent to put the question to you again.`);
31865
+ }
31866
+ function waitNoWordsLine(context) {
31867
+ return line(`${NOTHING_RECORDED}: a decline needs your reason after the colon, as in ${ownerWaitDeclineLineOf(context.wait)}.`);
31868
+ }
31869
+ function waitRefusedLine() {
31870
+ return line(`${NOTHING_RECORDED}: Halfcycle did not accept the words. Reword them in plain words and type the line again.`);
31871
+ }
31872
+ function waitSwitchedOffLine() {
31873
+ return line(`${NOTHING_RECORDED}. ${TYPED_APPROVAL_OFF_SENTENCE}`);
31874
+ }
31875
+ function waitCannotReachLine() {
31876
+ return line(`${NOTHING_RECORDED}: Halfcycle could not be reached just now. Type the line again in a minute.`);
31877
+ }
31878
+ function codexWaitLine(context) {
31879
+ return codexLine(`${NOTHING_RECORDED}: ${CODEX_TYPING_NEVER_COUNTS}. To answer, type ${waitTypedLine(context)} in a Claude Code session on this project.`);
31880
+ }
31881
+ function waitLineForAnswer(context, answer) {
31882
+ switch (answer.outcome) {
31883
+ case "recorded":
31884
+ return waitRecordedLine(answer.act, answer.question);
31885
+ case "refused":
31886
+ return waitRefusedLine();
31887
+ case "not-owner":
31888
+ return waitNotOwnerLine();
31889
+ case "no-owner":
31890
+ return waitNoOwnerLine();
31891
+ case "not-found":
31892
+ return waitNotFoundLine(context);
31893
+ case "ambiguous":
31894
+ return waitAmbiguousLine(context);
31895
+ case "not-offered":
31896
+ return waitSwitchedOffLine();
31897
+ case "unavailable":
31898
+ return waitCannotReachLine();
31899
+ default: {
31900
+ const unreachable = answer;
31901
+ return unreachable;
31902
+ }
31903
+ }
31711
31904
  }
31712
31905
 
31713
31906
  // dist/hooks/user-prompt-submit.js
@@ -31733,6 +31926,7 @@ function agentOf(opts) {
31733
31926
  async function decideLine(opts) {
31734
31927
  let act = null;
31735
31928
  let autonomyAct = null;
31929
+ let waitRecognised = false;
31736
31930
  const review = { recognised: false };
31737
31931
  try {
31738
31932
  const prompt = promptOf(opts.input);
@@ -31743,6 +31937,12 @@ async function decideLine(opts) {
31743
31937
  if (autonomyShape !== null) {
31744
31938
  autonomyAct = autonomyShape;
31745
31939
  return `${await decideAutonomy(autonomyShape, opts)}
31940
+ `;
31941
+ }
31942
+ const waitShape = waitPhraseShape(prompt);
31943
+ if (waitShape !== null) {
31944
+ waitRecognised = true;
31945
+ return `${await decideWait(waitShape, opts)}
31746
31946
  `;
31747
31947
  }
31748
31948
  const reviewShape = reviewPhraseShape(prompt);
@@ -31760,6 +31960,9 @@ async function decideLine(opts) {
31760
31960
  } catch {
31761
31961
  if (autonomyAct !== null)
31762
31962
  return `${autonomyCannotReachLine()}
31963
+ `;
31964
+ if (waitRecognised)
31965
+ return `${waitCannotReachLine()}
31763
31966
  `;
31764
31967
  if (review.recognised)
31765
31968
  return `${reviewCannotReachLine()}
@@ -31772,13 +31975,13 @@ function decideAllow(opts) {
31772
31975
  const env = opts.env ?? process.env;
31773
31976
  if ((opts.agent ?? "claude-code") === "claude-code" && !readSessionAttended(env))
31774
31977
  return allowNotAttendedLine();
31775
- const outcome = armAllow(sessionIdOf(opts.input), {
31978
+ const { outcome, step } = armAllowStep(sessionIdOf(opts.input), {
31776
31979
  ...opts.tempDir === void 0 ? {} : { dir: opts.tempDir },
31777
31980
  ...opts.now === void 0 ? {} : { now: opts.now }
31778
31981
  });
31779
31982
  switch (outcome) {
31780
31983
  case "armed":
31781
- return allowArmedLine();
31984
+ return allowArmedLine(step);
31782
31985
  case "nothing-held":
31783
31986
  return allowNothingHeldLine();
31784
31987
  case "several-held":
@@ -31805,6 +32008,57 @@ async function decideAutonomy(act, opts) {
31805
32008
  const { guardServiceToken, guardEngagementId } = read.config;
31806
32009
  return postAutonomy(act, guardEngagementId, guardServiceToken, env, opts.fetchFn);
31807
32010
  }
32011
+ async function decideWait(shape, opts) {
32012
+ const env = opts.env ?? process.env;
32013
+ const context = { act: shape.act, wait: shape.wait };
32014
+ const typed = whoTyped(opts);
32015
+ if (typed === "cannot-tell")
32016
+ return codexWaitLine(context);
32017
+ if (typed === "not-attended")
32018
+ return waitNotAttendedLine();
32019
+ const read = readConfig(env);
32020
+ if (read.config === null) {
32021
+ return readNeverConfiguredReason(env) === "not-this-account" ? waitNotOwnerLine() : waitNotSignedInLine();
32022
+ }
32023
+ let candidate = { act: "approve", wait: shape.wait };
32024
+ if (shape.act === "decline") {
32025
+ const words = shape.words ?? "";
32026
+ if (words === "")
32027
+ return waitNoWordsLine(context);
32028
+ if (!scopeNoteWireSchema.safeParse(words).success)
32029
+ return reviewWordsTooLongLine();
32030
+ if (looksLikeSecret(words))
32031
+ return reviewWordsLookSecretLine();
32032
+ candidate = { act: "decline", wait: shape.wait, note: words };
32033
+ }
32034
+ const request = ownerWaitChatRequestSchema.parse(candidate);
32035
+ const { guardServiceToken, guardEngagementId } = read.config;
32036
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(guardEngagementId)}/owner-waits/chat`;
32037
+ const controller = new AbortController();
32038
+ const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
32039
+ try {
32040
+ const response = await (opts.fetchFn ?? globalThis.fetch)(url2, {
32041
+ method: "POST",
32042
+ headers: { Authorization: `Bearer ${guardServiceToken}`, "Content-Type": "application/json" },
32043
+ body: JSON.stringify(request),
32044
+ signal: controller.signal
32045
+ });
32046
+ if (response.status === 401)
32047
+ return waitNotSignedInLine();
32048
+ if (response.status === 429 || response.status >= 500)
32049
+ return waitCannotReachLine();
32050
+ const parsed = ownerWaitChatResultSchema.safeParse(await response.json());
32051
+ if (!parsed.success)
32052
+ return waitCannotReachLine();
32053
+ if (parsed.data.outcome === "recorded" && response.status !== 200)
32054
+ return waitCannotReachLine();
32055
+ return waitLineForAnswer(context, parsed.data);
32056
+ } catch {
32057
+ return waitCannotReachLine();
32058
+ } finally {
32059
+ clearTimeout(timer);
32060
+ }
32061
+ }
31808
32062
  async function postAutonomy(act, engagementId, credential, env, fetchFn = globalThis.fetch) {
31809
32063
  const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/autonomy/chat`;
31810
32064
  const controller = new AbortController();
@@ -31838,8 +32092,12 @@ async function decideReview(shape, opts, progress) {
31838
32092
  const read2 = readConfig(env);
31839
32093
  if (read2.config === null)
31840
32094
  return "";
31841
- const match2 = matchReviewPhraseShape(shape, readShownRecord(read2.config.guardEngagementId, opts.home).reviews);
31842
- if (match2.kind === "nothing")
32095
+ const { guardEngagementId: engagementId, guardServiceToken: credential } = read2.config;
32096
+ const record2 = readShownRecord(engagementId, opts.home);
32097
+ const listed = matchReviewPhraseShape(shape, record2.reviews);
32098
+ const fresh = listed.kind === "accept" && listedRecently(listed.entry.shownAt, (opts.now ?? Date.now)());
32099
+ const target = listed.kind === "ambiguous" || fresh ? null : bulkAcceptTargetOf(shape.reference, record2);
32100
+ if (target?.kind === "none")
31843
32101
  return "";
31844
32102
  progress.recognised = true;
31845
32103
  const typed2 = whoTyped(opts);
@@ -31847,7 +32105,11 @@ async function decideReview(shape, opts, progress) {
31847
32105
  return codexReviewLine("accept");
31848
32106
  if (typed2 === "not-attended")
31849
32107
  return reviewNotAttendedLine();
31850
- return checkAndSendReview(match2, read2.config.guardEngagementId, read2.config.guardServiceToken, env, opts);
32108
+ if (target === null)
32109
+ return checkAndSendReview(listed, engagementId, credential, env, opts);
32110
+ if (target.kind === "ambiguous")
32111
+ return reviewAmbiguousLine(target.entries.map((e) => e.phase));
32112
+ return readThenAccept(shape, target.entry.phase, engagementId, credential, env, opts);
31851
32113
  }
31852
32114
  progress.recognised = true;
31853
32115
  const typed = whoTyped(opts);
@@ -31862,6 +32124,38 @@ async function decideReview(shape, opts, progress) {
31862
32124
  const match = matchReviewPhraseShape(shape, readShownRecord(read.config.guardEngagementId, opts.home).reviews);
31863
32125
  return checkAndSendReview(match, read.config.guardEngagementId, read.config.guardServiceToken, env, opts);
31864
32126
  }
32127
+ var REVIEW_READ_TIMEOUT_MS = 3e3;
32128
+ async function readThenAccept(shape, phase, engagementId, credential, env, opts) {
32129
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
32130
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/phases/${encodeURIComponent(phase)}/review`;
32131
+ const controller = new AbortController();
32132
+ const timer = setTimeout(() => controller.abort(), REVIEW_READ_TIMEOUT_MS);
32133
+ let raw;
32134
+ try {
32135
+ const response = await fetchFn(url2, {
32136
+ method: "GET",
32137
+ headers: { Authorization: `Bearer ${credential}` },
32138
+ signal: controller.signal
32139
+ });
32140
+ if (response.status === 401)
32141
+ return reviewNotSignedInLine();
32142
+ if (response.status !== 200)
32143
+ return reviewCannotReachLine();
32144
+ raw = await response.json();
32145
+ } catch {
32146
+ return reviewCannotReachLine();
32147
+ } finally {
32148
+ clearTimeout(timer);
32149
+ }
32150
+ const parsed = phaseReviewViewSchema.safeParse(raw);
32151
+ if (!parsed.success)
32152
+ return reviewCannotReachLine();
32153
+ const view = parsed.data;
32154
+ if (view.state !== "open")
32155
+ return reviewNoReviewLine(phase);
32156
+ const entry = shownReviewEntryOf(view, new Date((opts.now ?? Date.now)()).toISOString());
32157
+ return checkAndSendReview(matchReviewPhraseShape(shape, [entry]), engagementId, credential, env, opts);
32158
+ }
31865
32159
  async function checkAndSendReview(match, engagementId, credential, env, opts) {
31866
32160
  if (match.kind === "nothing" || match.kind === "show-first")
31867
32161
  return reviewShowFirstLine();
@@ -32415,6 +32709,10 @@ function isCheckedSessionStart(input, profile) {
32415
32709
  return CHECKED_SOURCES.has(source);
32416
32710
  }
32417
32711
  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.`;
32712
+ var CODEX_CONNECTED_SENTENCE = "Halfcycle's sign-in works and its service answered. If its tools are missing, start a new Codex session to reconnect them.";
32713
+ function connectedSentenceFor(profile) {
32714
+ return profile.id === "codex" ? CODEX_CONNECTED_SENTENCE : CONNECTED_SENTENCE;
32715
+ }
32418
32716
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
32419
32717
  var SESSION_START_PROBE_ATTEMPTS = 2;
32420
32718
  function registeredMcpUrl(projectRoot) {
@@ -32501,6 +32799,7 @@ async function probeDelivery(url2, bearer, fetchFn, deadlineMs, maxAttempts) {
32501
32799
  }
32502
32800
  }
32503
32801
  async function checkConnection(renewedToken, opts) {
32802
+ const profile = opts.profile ?? AGENT_PROFILES["claude-code"];
32504
32803
  const url2 = registeredMcpUrl(projectRootFromBinary(opts.binaryPath));
32505
32804
  if (url2 === void 0)
32506
32805
  return void 0;
@@ -32514,10 +32813,10 @@ async function checkConnection(renewedToken, opts) {
32514
32813
  environmentTokenFirst: false
32515
32814
  });
32516
32815
  if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
32517
- return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer };
32816
+ return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer, connected: false };
32518
32817
  }
32519
32818
  const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS, opts.maxAttempts ?? SESSION_START_PROBE_ATTEMPTS);
32520
- return { sentence: outcome ?? CONNECTED_SENTENCE, url: url2, bearer };
32819
+ return { sentence: outcome ?? connectedSentenceFor(profile), url: url2, bearer, connected: outcome === void 0 };
32521
32820
  }
32522
32821
  function positionRequestBody(engagementId) {
32523
32822
  return JSON.stringify({
@@ -32607,10 +32906,10 @@ async function runRefreshCredential(opts) {
32607
32906
  renewalOpen = false;
32608
32907
  const parts = [];
32609
32908
  if (isCheckedSessionStart(input, profile)) {
32610
- const connection = await checkConnection(renewedToken, { ...opts, writeErr });
32909
+ const connection = await checkConnection(renewedToken, { ...opts, profile, writeErr });
32611
32910
  if (connection !== void 0) {
32612
32911
  parts.push(connection.sentence);
32613
- if (connection.sentence === CONNECTED_SENTENCE) {
32912
+ if (connection.connected) {
32614
32913
  const engagementId = readConfig().config?.guardEngagementId;
32615
32914
  if (engagementId !== void 0) {
32616
32915
  const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);