halfcycle 0.5.0 → 0.5.2

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.0",
3
+ "version": "0.5.2",
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) {
@@ -18240,7 +18261,7 @@ function approvalSentence(phase, offerTyping) {
18240
18261
  return TYPED_APPROVAL_OFF_SENTENCE;
18241
18262
  if (offerTyping === "update")
18242
18263
  return CLIENT_UPDATE_SENTENCE;
18243
- return `In their own Claude Code session, they ask to see the plan, check each part, and then type: approve ${phase}.`;
18264
+ return `In their own coding agent's session, they ask to see the plan, check each part, and then type: approve ${phase}.`;
18244
18265
  }
18245
18266
  function phaseWords(phase) {
18246
18267
  return `phase "${phase}"`;
@@ -29234,7 +29255,8 @@ function refusalFor(facts, description) {
29234
29255
  function heldStep(steps, description, why) {
29235
29256
  return {
29236
29257
  person: PERSON_HOLD_LINE,
29237
- agent: `${HELD_STEP_OPENER}${steps}. ${reasonSentence(description)} ${PRECAUTION_OPENER}${why}. ${HOLD_AGENT_INSTRUCTION}`
29258
+ agent: `${HELD_STEP_OPENER}${steps}. ${reasonSentence(description)} ${PRECAUTION_OPENER}${why}. ${HOLD_AGENT_INSTRUCTION}`,
29259
+ step: steps
29238
29260
  };
29239
29261
  }
29240
29262
  var STEP_VERBS = {
@@ -29651,6 +29673,7 @@ import { createHash as createHash5 } from "node:crypto";
29651
29673
  import { appendFileSync as appendFileSync4, readFileSync as readFileSync20, renameSync as renameSync10, unlinkSync as unlinkSync9, writeFileSync as writeFileSync12 } from "node:fs";
29652
29674
  var ALLOW_ONCE_MS = 5 * 60 * 1e3;
29653
29675
  var HELD_STEP_MAX_AGE_MS = 60 * 60 * 1e3;
29676
+ var STEP_MAX_LENGTH = 1e3;
29654
29677
  function isAllowReply(prompt) {
29655
29678
  return typeof prompt === "string" && prompt.trim().toLowerCase() === ALLOW_REPLY;
29656
29679
  }
@@ -29676,11 +29699,11 @@ function canonical(value) {
29676
29699
  }
29677
29700
  return JSON.stringify(value) ?? "null";
29678
29701
  }
29679
- function recordHeld(sessionId, key, deps = {}) {
29702
+ function recordHeld(sessionId, key, deps = {}, step = null) {
29680
29703
  const paths = allowOncePaths(sessionId, deps.dir);
29681
29704
  if (paths === null)
29682
29705
  return;
29683
- const record2 = { version: 1, session: sessionId, key, at: nowOf(deps) };
29706
+ const record2 = { version: 1, session: sessionId, key, at: nowOf(deps), ...isStep(step) ? { step } : {} };
29684
29707
  try {
29685
29708
  appendFileSync4(paths.held, `${JSON.stringify(record2)}
29686
29709
  `, { mode: 384 });
@@ -29688,23 +29711,26 @@ function recordHeld(sessionId, key, deps = {}) {
29688
29711
  return;
29689
29712
  }
29690
29713
  }
29691
- function armAllow(sessionId, deps = {}) {
29714
+ function armAllowStep(sessionId, deps = {}) {
29692
29715
  try {
29693
29716
  const paths = allowOncePaths(sessionId, deps.dir);
29694
29717
  if (paths === null)
29695
- return "nothing-held";
29718
+ return { outcome: "nothing-held", step: null };
29696
29719
  const now = nowOf(deps);
29697
- const keys = claimHeldKeys(paths.held, sessionId, now);
29720
+ const held = claimHeld(paths.held, sessionId, now);
29721
+ const keys = [...new Set(held.map((r) => r.key))];
29698
29722
  if (keys.length === 0)
29699
- return "nothing-held";
29723
+ return { outcome: "nothing-held", step: null };
29700
29724
  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";
29725
+ return { outcome: "several-held", step: null };
29726
+ const step = held[held.length - 1].step ?? null;
29727
+ const saved = writeRecord(paths.allow, { version: 1, session: sessionId, key: keys[0], at: now });
29728
+ return saved ? { outcome: "armed", step } : { outcome: "not-saved", step: null };
29703
29729
  } catch {
29704
- return "not-saved";
29730
+ return { outcome: "not-saved", step: null };
29705
29731
  }
29706
29732
  }
29707
- function claimHeldKeys(path, session, now) {
29733
+ function claimHeld(path, session, now) {
29708
29734
  const claimed = `${path}.${process.pid}.${now}.claim`;
29709
29735
  try {
29710
29736
  renameSync10(path, claimed);
@@ -29718,13 +29744,16 @@ function claimHeldKeys(path, session, now) {
29718
29744
  text = "";
29719
29745
  }
29720
29746
  removeQuietly(claimed);
29721
- const keys = /* @__PURE__ */ new Set();
29747
+ const records = [];
29722
29748
  for (const line2 of text.split("\n")) {
29723
29749
  const r = parseRecord(line2, session);
29724
29750
  if (r !== null && within(r.at, now, HELD_STEP_MAX_AGE_MS))
29725
- keys.add(r.key);
29751
+ records.push(r);
29726
29752
  }
29727
- return [...keys];
29753
+ return records;
29754
+ }
29755
+ function isStep(v) {
29756
+ return typeof v === "string" && v !== "" && v.length <= STEP_MAX_LENGTH;
29728
29757
  }
29729
29758
  function spendAllow(sessionId, key, deps = {}) {
29730
29759
  try {
@@ -29776,7 +29805,8 @@ function parseRecord(text, session) {
29776
29805
  return null;
29777
29806
  if (typeof r["at"] !== "number")
29778
29807
  return null;
29779
- return { version: 1, session, key: r["key"], at: r["at"] };
29808
+ const step = r["step"];
29809
+ return { version: 1, session, key: r["key"], at: r["at"], ...isStep(step) ? { step } : {} };
29780
29810
  }
29781
29811
  function writeRecord(path, record2) {
29782
29812
  const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
@@ -30696,7 +30726,7 @@ function runPreToolUse(deps) {
30696
30726
  const allowOnce = { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir }, now: () => at.getTime() };
30697
30727
  if (spendAllow(input["session_id"], key, allowOnce))
30698
30728
  return "";
30699
- recordHeld(input["session_id"], key, allowOnce);
30729
+ recordHeld(input["session_id"], key, allowOnce, answer.refusal.step);
30700
30730
  }
30701
30731
  }
30702
30732
  return `${preToolUseReplyText("refuse", answer.refusal.person, answer.refusal.agent)}
@@ -31684,14 +31714,15 @@ function autonomyLineForAnswer(answer) {
31684
31714
  }
31685
31715
  var NOTHING_ALLOWED = "Nothing was allowed";
31686
31716
  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.`);
31717
+ function allowArmedLine(step = null) {
31718
+ const named = step === null ? "The person allowed the paused step once." : `The person allowed this paused step once: ${step}.`;
31719
+ 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
31720
  }
31690
31721
  function allowNothingHeldLine() {
31691
31722
  return line(`${NOTHING_ALLOWED}: no step is paused in this session.`);
31692
31723
  }
31693
31724
  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}.`);
31725
+ 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
31726
  }
31696
31727
  function allowNotAttendedLine() {
31697
31728
  return line(`${NOTHING_ALLOWED}: a paused step can be allowed by typing only when a person types it into an open session.`);
@@ -31700,14 +31731,18 @@ function allowNotSavedLine() {
31700
31731
  return line(`${NOTHING_ALLOWED}: Halfcycle could not save the answer on this computer. Type ${ALLOW_REPLY} again.`);
31701
31732
  }
31702
31733
  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";
31734
+ var SHOW_AS_WRITTEN = "Show the person this line as written.";
31735
+ function codexLine(text) {
31736
+ return `${text} ${SHOW_AS_WRITTEN}`;
31737
+ }
31703
31738
  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.`);
31739
+ 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
31740
  }
31706
31741
  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.`);
31742
+ 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
31743
  }
31709
31744
  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.`);
31745
+ return codexLine(`${AUTONOMY_NOTHING_CHANGED}: ${CODEX_TYPING_NEVER_COUNTS}. Turn it on or off with "${AUTONOMY_A1_TITLE}" in the console's settings.`);
31711
31746
  }
31712
31747
 
31713
31748
  // dist/hooks/user-prompt-submit.js
@@ -31772,13 +31807,13 @@ function decideAllow(opts) {
31772
31807
  const env = opts.env ?? process.env;
31773
31808
  if ((opts.agent ?? "claude-code") === "claude-code" && !readSessionAttended(env))
31774
31809
  return allowNotAttendedLine();
31775
- const outcome = armAllow(sessionIdOf(opts.input), {
31810
+ const { outcome, step } = armAllowStep(sessionIdOf(opts.input), {
31776
31811
  ...opts.tempDir === void 0 ? {} : { dir: opts.tempDir },
31777
31812
  ...opts.now === void 0 ? {} : { now: opts.now }
31778
31813
  });
31779
31814
  switch (outcome) {
31780
31815
  case "armed":
31781
- return allowArmedLine();
31816
+ return allowArmedLine(step);
31782
31817
  case "nothing-held":
31783
31818
  return allowNothingHeldLine();
31784
31819
  case "several-held":
@@ -32415,6 +32450,10 @@ function isCheckedSessionStart(input, profile) {
32415
32450
  return CHECKED_SOURCES.has(source);
32416
32451
  }
32417
32452
  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.`;
32453
+ 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.";
32454
+ function connectedSentenceFor(profile) {
32455
+ return profile.id === "codex" ? CODEX_CONNECTED_SENTENCE : CONNECTED_SENTENCE;
32456
+ }
32418
32457
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
32419
32458
  var SESSION_START_PROBE_ATTEMPTS = 2;
32420
32459
  function registeredMcpUrl(projectRoot) {
@@ -32501,6 +32540,7 @@ async function probeDelivery(url2, bearer, fetchFn, deadlineMs, maxAttempts) {
32501
32540
  }
32502
32541
  }
32503
32542
  async function checkConnection(renewedToken, opts) {
32543
+ const profile = opts.profile ?? AGENT_PROFILES["claude-code"];
32504
32544
  const url2 = registeredMcpUrl(projectRootFromBinary(opts.binaryPath));
32505
32545
  if (url2 === void 0)
32506
32546
  return void 0;
@@ -32514,10 +32554,10 @@ async function checkConnection(renewedToken, opts) {
32514
32554
  environmentTokenFirst: false
32515
32555
  });
32516
32556
  if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
32517
- return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer };
32557
+ return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer, connected: false };
32518
32558
  }
32519
32559
  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 };
32560
+ return { sentence: outcome ?? connectedSentenceFor(profile), url: url2, bearer, connected: outcome === void 0 };
32521
32561
  }
32522
32562
  function positionRequestBody(engagementId) {
32523
32563
  return JSON.stringify({
@@ -32607,10 +32647,10 @@ async function runRefreshCredential(opts) {
32607
32647
  renewalOpen = false;
32608
32648
  const parts = [];
32609
32649
  if (isCheckedSessionStart(input, profile)) {
32610
- const connection = await checkConnection(renewedToken, { ...opts, writeErr });
32650
+ const connection = await checkConnection(renewedToken, { ...opts, profile, writeErr });
32611
32651
  if (connection !== void 0) {
32612
32652
  parts.push(connection.sentence);
32613
- if (connection.sentence === CONNECTED_SENTENCE) {
32653
+ if (connection.connected) {
32614
32654
  const engagementId = readConfig().config?.guardEngagementId;
32615
32655
  if (engagementId !== void 0) {
32616
32656
  const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
package/dist/cli.js CHANGED
@@ -148,6 +148,9 @@ var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
148
148
  var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
149
149
  var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
150
150
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
151
+ var CONTEXT_BUDGET_CHECK_REL = ".claude/hooks/context-budget-check.sh";
152
+ var CONTEXT_DRIFT_CHECK_REL = ".claude/hooks/context-drift-check.sh";
153
+ var CONTEXT_DRIFT_MATCHER = "Edit|Write";
151
154
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
152
155
  var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
153
156
  function hookCommand(script, verb) {
@@ -224,6 +227,20 @@ var HALFCYCLE_HOOK_WIRING = [
224
227
  timeout: 5,
225
228
  command: hookCommand(SESSION_END_MARKER_REL),
226
229
  script: SESSION_END_MARKER_REL
230
+ },
231
+ {
232
+ event: "SessionStart",
233
+ matcher: null,
234
+ timeout: 10,
235
+ command: hookCommand(CONTEXT_BUDGET_CHECK_REL),
236
+ script: CONTEXT_BUDGET_CHECK_REL
237
+ },
238
+ {
239
+ event: "PostToolUse",
240
+ matcher: CONTEXT_DRIFT_MATCHER,
241
+ timeout: 5,
242
+ command: hookCommand(CONTEXT_DRIFT_CHECK_REL),
243
+ script: CONTEXT_DRIFT_CHECK_REL
227
244
  }
228
245
  ];
229
246
  var RETIRED_HOOK_COMMANDS = [
@@ -233,7 +250,9 @@ var RETIRED_HOOK_COMMANDS = [
233
250
  "bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
234
251
  "bash ./.claude/hooks/guard-runner.sh hook stop",
235
252
  "bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
236
- "bash ./.claude/hooks/session-end-marker.sh"
253
+ "bash ./.claude/hooks/session-end-marker.sh",
254
+ "bash ./.claude/hooks/context-budget-check.sh",
255
+ "bash ./.claude/hooks/context-drift-check.sh"
237
256
  ];
238
257
  var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
239
258
  ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
@@ -1099,7 +1118,9 @@ var CODEX_HOOK_WIRING = [
1099
1118
  codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
1100
1119
  codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
1101
1120
  codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
1102
- codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
1121
+ codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL),
1122
+ codexHookRow("SessionStart", null, 10, CONTEXT_BUDGET_CHECK_REL),
1123
+ codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 5, CONTEXT_DRIFT_CHECK_REL)
1103
1124
  ];
1104
1125
  var RETIRED_CODEX_HOOK_COMMANDS = [];
1105
1126
  function codexHookCommandSet(retired) {
@@ -5470,7 +5491,7 @@ function autonomyA13(accepted, carried) {
5470
5491
  }
5471
5492
  function autonomyA14(count, parts, acceptLine) {
5472
5493
  const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `say "review what was built", then type ${acceptLine} to accept them with those parts, or type the reject line it gives for a task's tests.`;
5473
- return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests. In your own Claude Code session, ` + typed;
5494
+ return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests. In your own coding agent's session, ` + typed;
5474
5495
  }
5475
5496
  function autonomyA15(count) {
5476
5497
  return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
@@ -6431,6 +6452,8 @@ function kinds() {
6431
6452
  [".claude/hooks/user-prompt-findings.sh", "check-script"],
6432
6453
  [".claude/hooks/session-end-marker.sh", "check-script"],
6433
6454
  [".claude/hooks/session-start-marker.sh", "check-script"],
6455
+ [".claude/hooks/context-budget-check.sh", "check-script"],
6456
+ [".claude/hooks/context-drift-check.sh", "check-script"],
6434
6457
  [SETTINGS_REL, "settings"],
6435
6458
  [MCP_REGISTRATION_REL, "server-entry"],
6436
6459
  [GITIGNORE_REL, "ignore-list"],
@@ -8425,13 +8448,133 @@ var MCP_HEADERS_KNOWN_HEADER_HASHES = [
8425
8448
  function planMcpHeadersHelperRemoval(targetRepo) {
8426
8449
  return decideRetiredRemoval(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
8427
8450
  }
8451
+ var GENERATED_CONTEXT_BUDGET_HEADER = "# Context budget check \u2014 generated by";
8452
+ function generateContextBudgetCheck() {
8453
+ return `#!/usr/bin/env bash
8454
+ ${GENERATED_CONTEXT_BUDGET_HEADER} the Halfcycle installer.
8455
+ # Runs when a session starts. It warns, and never blocks, when the instructions
8456
+ # every session loads have grown past their budget, so they can be trimmed
8457
+ # before every later session pays for them:
8458
+ # - AGENTS.md at the project root, over 16384 bytes;
8459
+ # - this project's memory index in Claude Code, over 20480 bytes or 150 lines
8460
+ # (Claude Code cuts the index off at about 25 KB or 200 lines when it loads it).
8461
+
8462
+ cat >/dev/null 2>&1 || true
8463
+
8464
+ SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )" || exit 0
8465
+ PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )" || exit 0
8466
+
8467
+ NOTES=""
8468
+
8469
+ AGENTS_FILE="$PROJECT_ROOT/AGENTS.md"
8470
+ if [ -f "$AGENTS_FILE" ]; then
8471
+ BYTES=$(wc -c < "$AGENTS_FILE" | tr -d ' ')
8472
+ if [ "\${BYTES:-0}" -gt 16384 ]; then
8473
+ NOTES="\${NOTES}AGENTS.md is \${BYTES} bytes, over its budget of 16384. "
8474
+ fi
8475
+ fi
8476
+
8477
+ CONFIG_DIR="\${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
8478
+ SLUG=$(printf '%s' "\${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}" | sed 's/[^A-Za-z0-9]/-/g')
8479
+ INDEX="$CONFIG_DIR/projects/$SLUG/memory/MEMORY.md"
8480
+ if [ -f "$INDEX" ]; then
8481
+ BYTES=$(wc -c < "$INDEX" | tr -d ' ')
8482
+ LINES=$(wc -l < "$INDEX" | tr -d ' ')
8483
+ if [ "\${BYTES:-0}" -gt 20480 ] || [ "\${LINES:-0}" -gt 150 ]; then
8484
+ NOTES="\${NOTES}The memory index is \${BYTES} bytes and \${LINES} lines, over its budget of 20480 bytes and 150 lines. "
8485
+ fi
8486
+ fi
8487
+
8488
+ if [ -z "$NOTES" ]; then exit 0; fi
8489
+
8490
+ MESSAGE="Context budget: \${NOTES}Run the context-update command to fold and trim it."
8491
+ printf '{"systemMessage":"%s","hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MESSAGE" "$MESSAGE"
8492
+ exit 0
8493
+ `;
8494
+ }
8495
+ var GENERATED_CONTEXT_DRIFT_HEADER = "# Context drift check \u2014 generated by";
8496
+ function generateContextDriftCheck() {
8497
+ return `#!/usr/bin/env bash
8498
+ ${GENERATED_CONTEXT_DRIFT_HEADER} the Halfcycle installer.
8499
+ # Context drift detector \u2014 runs after each file edit. If a trigger file was modified,
8500
+ # it asks the session to run the context-update command before the session ends.
8501
+
8502
+ set -euo pipefail
8503
+
8504
+ INPUT=$(cat)
8505
+
8506
+ # First line: the event's name, for the reply. Then one line per edited file, made
8507
+ # absolute against the event's cwd: the edit's file_path, or every path a patch names.
8508
+ # With no python3 on the machine it says nothing, rather than fail after every edit.
8509
+ PARSED=$(HOOK_INPUT="$INPUT" python3 - <<'PYEOF'
8510
+ import json, os, re
8511
+ try:
8512
+ data = json.loads(os.environ["HOOK_INPUT"])
8513
+ except Exception:
8514
+ raise SystemExit(0)
8515
+ tool_input = data.get("tool_input") or {}
8516
+ cwd = data.get("cwd") or os.getcwd()
8517
+ if tool_input.get("file_path"):
8518
+ paths = [tool_input["file_path"]]
8519
+ else:
8520
+ headers = r"^\\*\\*\\* (?:Add File|Update File|Delete File|Move to): (.+)$"
8521
+ paths = re.findall(headers, str(tool_input.get("command", "")), re.M)
8522
+ print(data.get("hook_event_name", ""))
8523
+ for p in paths:
8524
+ print(os.path.realpath(os.path.join(cwd, p.strip())))
8525
+ PYEOF
8526
+ ) || exit 0
8527
+
8528
+ if [ -z "$PARSED" ]; then exit 0; fi
8529
+ HOOK_EVENT=$(printf '%s\\n' "$PARSED" | head -n 1)
8530
+
8531
+ # Portable PROJECT_ROOT derivation: the script lives two folders below the root
8532
+ SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )"
8533
+ PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )"
8534
+
8535
+ REASONS=""
8536
+ while IFS= read -r FILE_PATH; do
8537
+ [ -n "$FILE_PATH" ] || continue
8538
+ case "$FILE_PATH" in
8539
+ "$PROJECT_ROOT"/*) ;;
8540
+ *) continue ;;
8541
+ esac
8542
+
8543
+ REL_PATH="\${FILE_PATH#$PROJECT_ROOT/}"
8544
+
8545
+ # Skip the instructions files and each agent's own folders (editing them needs no reminder)
8546
+ case "$REL_PATH" in
8547
+ CLAUDE.md|AGENTS.md|README.md|.claude/*|.codex/*|.agents/*|dist/*|node_modules/*|.git/*)
8548
+ continue ;;
8549
+ esac
8550
+
8551
+ REASON=""
8552
+ case "$REL_PATH" in
8553
+ package.json|package-lock.json) REASON="package.json changed \u2014 check Tech Stack / Commands sections" ;;
8554
+ wrangler.jsonc|wrangler.toml) REASON="wrangler config changed \u2014 check Deployment section" ;;
8555
+ astro.config.*|next.config.*|vite.config.*) REASON="build config changed \u2014 check Tech Stack / Architecture" ;;
8556
+ src/pages/api/*|src/routes/api/*|pages/api/*) REASON="API route changed \u2014 check API Endpoints" ;;
8557
+ scripts/*) REASON="scripts/ changed \u2014 check Commands section" ;;
8558
+ tsconfig.json|.eslintrc*|.prettierrc*) REASON="tool config changed \u2014 check Code Conventions" ;;
8559
+ esac
8560
+ if [ -n "$REASON" ]; then REASONS="\${REASONS}\${REL_PATH} was modified: \${REASON}. "; fi
8561
+ done < <(printf '%s\\n' "$PARSED" | tail -n +2)
8562
+
8563
+ if [ -z "$REASONS" ]; then exit 0; fi
8564
+
8565
+ REMINDER="Context drift detected. \${REASONS}Before ending this session, run the context-update command to audit and update AGENTS.md and the memory folder."
8566
+ python3 -c 'import json, sys; print(json.dumps({"hookSpecificOutput": {"hookEventName": sys.argv[1], "additionalContext": sys.argv[2]}}))' "$HOOK_EVENT" "$REMINDER"
8567
+ `;
8568
+ }
8428
8569
  var OWNED_GENERATED_HEADERS = {
8429
8570
  ".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
8430
8571
  ".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
8431
8572
  ".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
8432
8573
  ".claude/hooks/user-prompt-findings.sh": GENERATED_USER_PROMPT_FINDINGS_HEADER,
8433
8574
  ".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
8434
- ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
8575
+ ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER,
8576
+ ".claude/hooks/context-budget-check.sh": GENERATED_CONTEXT_BUDGET_HEADER,
8577
+ ".claude/hooks/context-drift-check.sh": GENERATED_CONTEXT_DRIFT_HEADER
8435
8578
  };
8436
8579
  function hookScriptsThisVersionWrites() {
8437
8580
  return [
@@ -8440,7 +8583,9 @@ function hookScriptsThisVersionWrites() {
8440
8583
  [".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
8441
8584
  [".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
8442
8585
  [".claude/hooks/user-prompt-findings.sh", generateUserPromptFindingsHook()],
8443
- [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
8586
+ [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()],
8587
+ [".claude/hooks/context-budget-check.sh", generateContextBudgetCheck()],
8588
+ [".claude/hooks/context-drift-check.sh", generateContextDriftCheck()]
8444
8589
  ];
8445
8590
  }
8446
8591
  function generateMcpRegistration(existing, mcpOrigin) {
@@ -10626,7 +10771,7 @@ function refuseInsideProject(targetRepo) {
10626
10771
  let folder = realOrLiteral(targetRepo);
10627
10772
  for (let parent = dirname4(folder); parent !== folder; folder = parent, parent = dirname4(parent)) {
10628
10773
  if (existsSync5(join7(parent, BUNDLE_PIN_REL))) {
10629
- throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in Claude Code instead, or run "npx halfcycle" in a folder outside it.`);
10774
+ throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in your coding agent instead, or run "npx halfcycle" in a folder outside it.`);
10630
10775
  }
10631
10776
  }
10632
10777
  }
@@ -13904,7 +14049,7 @@ function approvalLine2(phase, number, typedApprovalOffered, pinnedVersion, shown
13904
14049
  if (!clientMeetsChatApprovalFloor(pinnedVersion)) {
13905
14050
  return then(CLIENT_UPDATE_SENTENCE, "Or the project's owner approves it in the System view now:");
13906
14051
  }
13907
- return then(`To approve it, the project's owner types \`approve ${phase}\` in their own Claude Code session. Only the owner can.`, "They can also approve it in the System view:");
14052
+ return then(`To approve it, the project's owner types \`approve ${phase}\` in their own coding agent's session. Only the owner can.`, "They can also approve it in the System view:");
13908
14053
  }
13909
14054
  function changeLines(plan, description, changed, carriedFrom = []) {
13910
14055
  const names = new Map(description.parts.map((part) => [part.id, part.name]));
@@ -14152,7 +14297,7 @@ function rv7Lines(view, claims) {
14152
14297
  if (decidable.length === 0)
14153
14298
  return ["No part waits for your decision now."];
14154
14299
  }
14155
- const lines = ["To decide, type one of these lines in your own Claude Code session:"];
14300
+ const lines = ["To decide, type one of these lines in your own coding agent's session:"];
14156
14301
  for (const part of decidable) {
14157
14302
  for (const act of reviewActsFor(part.built))
14158
14303
  lines.push(` ${typedLine(act, reference, part.part)}`);
@@ -14198,7 +14343,7 @@ var CHECKS_SUBHEADING = "An agent says these pass and Halfcycle never ran them.
14198
14343
  function uncheckedClaimLine(test) {
14199
14344
  return `Unchecked: ${test.claimedBy} says "${test.label}" passes. Halfcycle never ran it.`;
14200
14345
  }
14201
- var ANSWER_CLAIMS_BY_TYPING_LINE = "To answer them, type the reject or accept line listed above in your own Claude Code session.";
14346
+ var ANSWER_CLAIMS_BY_TYPING_LINE = "To answer them, type the reject or accept line listed above in your own coding agent's session.";
14202
14347
  function checksSectionLines(work) {
14203
14348
  const unchecked = work.tasks.flatMap((task) => task.tests.filter((t) => t.state === "unchecked"));
14204
14349
  if (unchecked.length === 0)
@@ -14619,7 +14764,7 @@ function typedReviewHintLine(offered, phase) {
14619
14764
  return `[halfcycle] ${TYPED_REVIEW_OFF_SENTENCE}`;
14620
14765
  const accept = typedAcceptLine(phase);
14621
14766
  const lines = accept === null ? "then type one decision per line, as the whole message." : `then type ${accept} as the whole message to accept every built part, or one line per part, such as ${accept}/<part>; the list gives each part's lines.`;
14622
- return `[halfcycle] The owner reviews each part in their own Claude Code session: they say "review what was built" to see the list, ${lines}`;
14767
+ return `[halfcycle] The owner reviews each part in their own coding agent's session: they say "review what was built" to see the list, ${lines}`;
14623
14768
  }
14624
14769
  function systemViewReviewLine(offered, engagementId, phase) {
14625
14770
  const address = systemViewUrl(engagementId, phase);
@@ -14628,7 +14773,7 @@ function systemViewReviewLine(offered, engagementId, phase) {
14628
14773
  if (!offered)
14629
14774
  return `[halfcycle] The owner reviews it in the System view: ${address}`;
14630
14775
  const accept = typedAcceptLine(phase);
14631
- const instead = accept === null ? "typing in their own Claude Code session" : `typing ${accept} in their own Claude Code session`;
14776
+ const instead = accept === null ? "typing in their own coding agent's session" : `typing ${accept} in their own coding agent's session`;
14632
14777
  return `[halfcycle] Or, instead of ${instead}, the owner reviews it in the System view: ${address}`;
14633
14778
  }
14634
14779
  function tasksOpenLine(phase, open, more) {
@@ -16483,7 +16628,7 @@ function renderUninstallReport(result, home) {
16483
16628
  case "nothing-to-say":
16484
16629
  break;
16485
16630
  }
16486
- out.push("[halfcycle] Your project on Halfcycle is unchanged. Restart any Claude Code session open in this project.");
16631
+ out.push("[halfcycle] Your project on Halfcycle is unchanged. Restart any coding agent session open in this project.");
16487
16632
  return { stdout: out.join("\n") + "\n", stderr: "" };
16488
16633
  }
16489
16634