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.
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),
@@ -395,15 +414,15 @@ function readReviewPart(raw) {
395
414
  const name = raw["name"];
396
415
  const built = raw["built"];
397
416
  const decidable = raw["decidable"];
398
- const awaitingAnswer2 = raw["awaitingAnswer"];
417
+ const awaitingAnswer = raw["awaitingAnswer"];
399
418
  const sentBack = raw["sentBack"];
400
419
  if (!isNonEmptyString(part) || !isNonEmptyString(name))
401
420
  return null;
402
421
  if (typeof built !== "boolean" || typeof decidable !== "boolean")
403
422
  return null;
404
- if (typeof awaitingAnswer2 !== "boolean" || typeof sentBack !== "boolean")
423
+ if (typeof awaitingAnswer !== "boolean" || typeof sentBack !== "boolean")
405
424
  return null;
406
- return { part, name, built, decidable, awaitingAnswer: awaitingAnswer2, sentBack };
425
+ return { part, name, built, decidable, awaitingAnswer, sentBack };
407
426
  }
408
427
  function isPlainObject(value) {
409
428
  if (typeof value !== "object" || value === null || Array.isArray(value))
@@ -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) {
@@ -4482,6 +4503,30 @@ var phaseReviewViewSchema = z20.discriminatedUnion("state", [
4482
4503
  phaseReviewOpenSchema,
4483
4504
  phaseReviewClosedSchema
4484
4505
  ]);
4506
+ function reviewPartAwaitingAnswer(part) {
4507
+ return part.questions.some((q) => q.agentAnswer === void 0);
4508
+ }
4509
+ function reviewPartSentBackIn(part, round) {
4510
+ return part.sentBack.some((s) => s.round === round);
4511
+ }
4512
+ function shownReviewEntryOf(view, shownAt) {
4513
+ const parts = view.parts.map((p) => ({
4514
+ part: p.part,
4515
+ name: p.name,
4516
+ built: p.built,
4517
+ decidable: p.decidable,
4518
+ awaitingAnswer: reviewPartAwaitingAnswer(p),
4519
+ sentBack: reviewPartSentBackIn(p, view.round)
4520
+ }));
4521
+ return {
4522
+ phase: view.phase,
4523
+ number: view.number,
4524
+ round: view.round,
4525
+ reviewVersion: view.reviewVersion,
4526
+ shownAt,
4527
+ parts
4528
+ };
4529
+ }
4485
4530
 
4486
4531
  // ../events/dist/build-record-v3.js
4487
4532
  var BUILD_RECORD_V3_FORMAT = "halfcycle-build-record/v3";
@@ -5125,8 +5170,35 @@ function ownerWaitClosedLineOf(item, question2) {
5125
5170
  }
5126
5171
  var OWNER_WAIT_DECLARATIONS_READ_FAILED = "Halfcycle could not read the decisions that waited for you.";
5127
5172
 
5128
- // ../events/dist/pacing.js
5173
+ // ../events/dist/owner-wait-answer.js
5129
5174
  import { z as z32 } from "zod";
5175
+ var ownerWaitShortIdSchema = z32.string().regex(/^[0-9a-f]{8}$/);
5176
+ var OWNER_WAIT_ANSWER_ACTS = ["approve", "decline"];
5177
+ var ownerWaitAnswerActSchema = z32.enum(OWNER_WAIT_ANSWER_ACTS);
5178
+ var ownerWaitChatRequestSchema = z32.discriminatedUnion("act", [
5179
+ z32.object({ act: z32.literal("approve"), wait: ownerWaitShortIdSchema }).strict(),
5180
+ z32.object({ act: z32.literal("decline"), wait: ownerWaitShortIdSchema, note: scopeNoteWireSchema }).strict()
5181
+ ]);
5182
+ function bareOutcome3(outcome) {
5183
+ return z32.object({ outcome: z32.literal(outcome) }).strict();
5184
+ }
5185
+ var ownerWaitChatResultSchema = z32.discriminatedUnion("outcome", [
5186
+ z32.object({
5187
+ outcome: z32.literal("recorded"),
5188
+ act: ownerWaitAnswerActSchema,
5189
+ question: z32.string().min(1)
5190
+ }).strict(),
5191
+ z32.object({ outcome: z32.literal("refused"), problems: z32.array(systemDescriptionProblemSchema).min(1) }).strict(),
5192
+ bareOutcome3("not-owner"),
5193
+ bareOutcome3("no-owner"),
5194
+ bareOutcome3("not-found"),
5195
+ bareOutcome3("ambiguous"),
5196
+ bareOutcome3("not-offered"),
5197
+ bareOutcome3("unavailable")
5198
+ ]);
5199
+
5200
+ // ../events/dist/pacing.js
5201
+ import { z as z33 } from "zod";
5130
5202
  var CLAUDE_PLANS = Object.freeze([
5131
5203
  Object.freeze({ id: "pro", words: "Pro", startingAgents: 2 }),
5132
5204
  Object.freeze({ id: "max-5x", words: "Max (5\xD7)", startingAgents: 4 }),
@@ -5137,7 +5209,7 @@ var CLAUDE_PLANS = Object.freeze([
5137
5209
  Object.freeze({ id: "not-sure", words: "Not sure", startingAgents: 2 })
5138
5210
  ]);
5139
5211
  var CLAUDE_PLAN_IDS = Object.freeze(CLAUDE_PLANS.map((plan) => plan.id));
5140
- var claudePlanSchema = z32.enum(CLAUDE_PLAN_IDS);
5212
+ var claudePlanSchema = z33.enum(CLAUDE_PLAN_IDS);
5141
5213
  var CLAUDE_PLAN_UNKNOWN = "not-sure";
5142
5214
  function claudePlanOf(id) {
5143
5215
  return CLAUDE_PLANS.find((plan) => plan.id === id) ?? CLAUDE_PLANS.find((plan) => plan.id === CLAUDE_PLAN_UNKNOWN) ?? CLAUDE_PLANS[0];
@@ -5146,21 +5218,21 @@ function claudePlanKnown(id) {
5146
5218
  return id !== null && id !== CLAUDE_PLAN_UNKNOWN;
5147
5219
  }
5148
5220
  var CLAUDE_PLAN_SET_IN = ["setup", "console"];
5149
- var claudePlanSetInSchema = z32.enum(CLAUDE_PLAN_SET_IN);
5221
+ var claudePlanSetInSchema = z33.enum(CLAUDE_PLAN_SET_IN);
5150
5222
  var KEEP_FREE_PERCENTS = [0, 20];
5151
- var keepFreePercentSchema = z32.union([z32.literal(KEEP_FREE_PERCENTS[0]), z32.literal(KEEP_FREE_PERCENTS[1])]);
5223
+ var keepFreePercentSchema = z33.union([z33.literal(KEEP_FREE_PERCENTS[0]), z33.literal(KEEP_FREE_PERCENTS[1])]);
5152
5224
  var PACE_AGENTS_MAX = 20;
5153
5225
  var PACE_WINDOW_KINDS = ["five-hour", "seven-day"];
5154
- var paceWindowKindSchema = z32.enum(PACE_WINDOW_KINDS);
5226
+ var paceWindowKindSchema = z33.enum(PACE_WINDOW_KINDS);
5155
5227
  var PACE_WINDOW_WORDS = Object.freeze({
5156
5228
  "five-hour": "5-hour window",
5157
5229
  "seven-day": "weekly window"
5158
5230
  });
5159
5231
  var PACE_STATES = ["full", "slowing", "holding"];
5160
- var paceStateSchema = z32.enum(PACE_STATES);
5161
- var claudePlanRequestSchema = z32.object({ claudePlan: claudePlanSchema }).strict();
5162
- var keepFreeRequestSchema = z32.object({ keepFreePercent: keepFreePercentSchema }).strict();
5163
- var pacingSettingsSchema = z32.object({
5232
+ var paceStateSchema = z33.enum(PACE_STATES);
5233
+ var claudePlanRequestSchema = z33.object({ claudePlan: claudePlanSchema }).strict();
5234
+ var keepFreeRequestSchema = z33.object({ keepFreePercent: keepFreePercentSchema }).strict();
5235
+ var pacingSettingsSchema = z33.object({
5164
5236
  claudePlan: claudePlanSchema.nullable(),
5165
5237
  claudePlanSetIn: claudePlanSetInSchema.nullable(),
5166
5238
  keepFreePercent: keepFreePercentSchema
@@ -5168,43 +5240,43 @@ var pacingSettingsSchema = z32.object({
5168
5240
  message: "a plan says where it was given, and only a plan does",
5169
5241
  path: ["claudePlanSetIn"]
5170
5242
  });
5171
- var pacingBasisTaskSchema = z32.object({
5243
+ var pacingBasisTaskSchema = z33.object({
5172
5244
  id: buildTaskIdSchema,
5173
- dependsOn: z32.array(buildTaskIdSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5174
- budgetMinutes: z32.number().int().min(1).max(BUILD_TASK_BUDGET_MAX_MINUTES).optional(),
5175
- human: z32.boolean()
5245
+ dependsOn: z33.array(buildTaskIdSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5246
+ budgetMinutes: z33.number().int().min(1).max(BUILD_TASK_BUDGET_MAX_MINUTES).optional(),
5247
+ human: z33.boolean()
5176
5248
  }).strict();
5177
- var pacingBasisSchema = z32.object({
5178
- outcome: z32.literal("basis"),
5249
+ var pacingBasisSchema = z33.object({
5250
+ outcome: z33.literal("basis"),
5179
5251
  claudePlan: claudePlanSchema.nullable(),
5180
5252
  keepFreePercent: keepFreePercentSchema,
5181
- settingsUrl: z32.string().min(1),
5253
+ settingsUrl: z33.string().min(1),
5182
5254
  phase: phaseNameSchema.nullable(),
5183
- tasks: z32.array(pacingBasisTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5184
- medianTaskMinutes: z32.number().nonnegative().nullable(),
5185
- medianTaskTokens: z32.number().nonnegative().nullable()
5255
+ tasks: z33.array(pacingBasisTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5256
+ medianTaskMinutes: z33.number().nonnegative().nullable(),
5257
+ medianTaskTokens: z33.number().nonnegative().nullable()
5186
5258
  }).strict();
5187
- var paceSuggestionSchema = z32.object({
5188
- number: z32.number().int().min(1).max(PACE_AGENTS_MAX),
5189
- minutes: z32.number().int().nonnegative().nullable(),
5190
- pauses: z32.number().int().nonnegative(),
5259
+ var paceSuggestionSchema = z33.object({
5260
+ number: z33.number().int().min(1).max(PACE_AGENTS_MAX),
5261
+ minutes: z33.number().int().nonnegative().nullable(),
5262
+ pauses: z33.number().int().nonnegative(),
5191
5263
  claudePlan: claudePlanSchema.nullable(),
5192
5264
  keepFreePercent: keepFreePercentSchema,
5193
- settingsUrl: z32.string().min(1).nullable(),
5265
+ settingsUrl: z33.string().min(1).nullable(),
5194
5266
  knownWindow: paceWindowKindSchema.nullable()
5195
5267
  }).strict();
5196
5268
  var PACE_NOT_PACED_REASONS = ["not-configured", "unreadable"];
5197
- var paceRunResultSchema = z32.discriminatedUnion("outcome", [
5198
- z32.object({
5199
- outcome: z32.literal("paced"),
5269
+ var paceRunResultSchema = z33.discriminatedUnion("outcome", [
5270
+ z33.object({
5271
+ outcome: z33.literal("paced"),
5200
5272
  pace: paceStateSchema,
5201
- number: z32.number().int().min(1).max(PACE_AGENTS_MAX),
5202
- allowedNow: z32.number().int().nonnegative(),
5273
+ number: z33.number().int().min(1).max(PACE_AGENTS_MAX),
5274
+ allowedNow: z33.number().int().nonnegative(),
5203
5275
  windowKind: paceWindowKindSchema.optional(),
5204
5276
  until: utcIso8601.optional(),
5205
5277
  suggestion: paceSuggestionSchema.optional()
5206
5278
  }).strict(),
5207
- z32.object({ outcome: z32.literal("not-paced"), reason: z32.enum(PACE_NOT_PACED_REASONS) }).strict()
5279
+ z33.object({ outcome: z33.literal("not-paced"), reason: z33.enum(PACE_NOT_PACED_REASONS) }).strict()
5208
5280
  ]);
5209
5281
  function paceAgentsOf(n) {
5210
5282
  return n === 1 ? "1 agent" : `${String(n)} agents`;
@@ -5289,15 +5361,15 @@ function paceAllowedNowOf(k) {
5289
5361
  var PACE_AGENTS_TOO_MANY = `Run at most ${String(PACE_AGENTS_MAX)} agents at once.`;
5290
5362
 
5291
5363
  // ../events/dist/autonomy.js
5292
- import { z as z33 } from "zod";
5364
+ import { z as z34 } from "zod";
5293
5365
  var AUTONOMY_CHANGED_IN = ["console", "chat"];
5294
- var autonomyChangedInSchema = z33.enum(AUTONOMY_CHANGED_IN);
5295
- var autonomySettingsSchema = z33.object({
5296
- on: z33.boolean(),
5366
+ var autonomyChangedInSchema = z34.enum(AUTONOMY_CHANGED_IN);
5367
+ var autonomySettingsSchema = z34.object({
5368
+ on: z34.boolean(),
5297
5369
  // whether builds run on their own
5298
- mayPublish: z33.boolean(),
5370
+ mayPublish: z34.boolean(),
5299
5371
  // whether builds may publish or release without waiting
5300
- maySpend: z33.boolean(),
5372
+ maySpend: z34.boolean(),
5301
5373
  // whether builds may spend money or sign up without waiting
5302
5374
  changedAt: utcIso8601.nullable(),
5303
5375
  // when the setting last changed; null if never
@@ -5307,37 +5379,37 @@ var autonomySettingsSchema = z33.object({
5307
5379
  message: "says when and where it changed, or neither",
5308
5380
  path: ["changedIn"]
5309
5381
  });
5310
- var autonomyChangeRequestSchema = z33.object({
5311
- on: z33.boolean(),
5382
+ var autonomyChangeRequestSchema = z34.object({
5383
+ on: z34.boolean(),
5312
5384
  // the switch
5313
- mayPublish: z33.boolean(),
5385
+ mayPublish: z34.boolean(),
5314
5386
  // the publish box
5315
- maySpend: z33.boolean()
5387
+ maySpend: z34.boolean()
5316
5388
  // the spend box
5317
5389
  }).strict();
5318
5390
  var AUTONOMY_CHAT_ACTS = ["on", "off"];
5319
- var autonomyChatActSchema = z33.enum(AUTONOMY_CHAT_ACTS);
5320
- var autonomyChatRequestSchema = z33.object({
5391
+ var autonomyChatActSchema = z34.enum(AUTONOMY_CHAT_ACTS);
5392
+ var autonomyChatRequestSchema = z34.object({
5321
5393
  act: autonomyChatActSchema
5322
5394
  // what the owner typed
5323
5395
  }).strict();
5324
- var autonomyChatResultSchema = z33.discriminatedUnion("outcome", [
5325
- z33.object({
5326
- outcome: z33.literal("recorded"),
5327
- on: z33.boolean()
5396
+ var autonomyChatResultSchema = z34.discriminatedUnion("outcome", [
5397
+ z34.object({
5398
+ outcome: z34.literal("recorded"),
5399
+ on: z34.boolean()
5328
5400
  // whether builds now run on their own
5329
5401
  }).strict(),
5330
- z33.object({ outcome: z33.literal("not-owner") }).strict(),
5331
- z33.object({ outcome: z33.literal("no-owner") }).strict(),
5332
- z33.object({ outcome: z33.literal("not-offered") }).strict(),
5333
- z33.object({ outcome: z33.literal("unavailable") }).strict()
5402
+ z34.object({ outcome: z34.literal("not-owner") }).strict(),
5403
+ z34.object({ outcome: z34.literal("no-owner") }).strict(),
5404
+ z34.object({ outcome: z34.literal("not-offered") }).strict(),
5405
+ z34.object({ outcome: z34.literal("unavailable") }).strict()
5334
5406
  ]);
5335
- var autonomyStateSchema = z33.object({
5336
- on: z33.boolean(),
5407
+ var autonomyStateSchema = z34.object({
5408
+ on: z34.boolean(),
5337
5409
  // whether builds run on their own
5338
- mayPublish: z33.boolean(),
5410
+ mayPublish: z34.boolean(),
5339
5411
  // whether builds may publish or release without waiting
5340
- maySpend: z33.boolean()
5412
+ maySpend: z34.boolean()
5341
5413
  // whether builds may spend money or sign up without waiting
5342
5414
  }).strict();
5343
5415
  var autonomyStateFileSchema = autonomyStateSchema.extend({
@@ -5346,8 +5418,8 @@ var autonomyStateFileSchema = autonomyStateSchema.extend({
5346
5418
  }).strict();
5347
5419
  var AUTONOMY_STATE_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5348
5420
  var AUTONOMY_ACTIVITY_KINDS = ["went-ahead", "waiting", "approved", "closed", "waiting-on-owner"];
5349
- var autonomyActivityKindSchema = z33.enum(AUTONOMY_ACTIVITY_KINDS);
5350
- var autonomyActivityRowSchema = z33.object({
5421
+ var autonomyActivityKindSchema = z34.enum(AUTONOMY_ACTIVITY_KINDS);
5422
+ var autonomyActivityRowSchema = z34.object({
5351
5423
  kind: autonomyActivityKindSchema,
5352
5424
  // what happened
5353
5425
  phase: scopePhaseSchema,
@@ -5360,6 +5432,8 @@ var autonomyActivityRowSchema = z33.object({
5360
5432
  // whether the agent says it can be undone
5361
5433
  how: statementText(STATEMENT_MAX).optional(),
5362
5434
  // how to undo it, when it can be
5435
+ wait: ownerWaitShortIdSchema.optional(),
5436
+ // the short id the owner types to answer a waiting decision
5363
5437
  at: utcIso8601
5364
5438
  // when it happened
5365
5439
  }).strict().superRefine((row, ctx) => {
@@ -5386,6 +5460,9 @@ var autonomyActivityRowSchema = z33.object({
5386
5460
  });
5387
5461
  }
5388
5462
  }
5463
+ if (row.kind !== "waiting" && row.wait !== void 0) {
5464
+ ctx.addIssue({ code: "custom", message: "only a waiting decision is answered by typing", path: ["wait"] });
5465
+ }
5389
5466
  if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
5390
5467
  ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
5391
5468
  }
@@ -5393,11 +5470,11 @@ var autonomyActivityRowSchema = z33.object({
5393
5470
  ctx.addIssue({ code: "custom", message: "says how to undo it exactly when it can be undone", path: ["how"] });
5394
5471
  }
5395
5472
  });
5396
- var autonomyActivitySchema = z33.object({
5397
- rows: z33.array(autonomyActivityRowSchema)
5473
+ var autonomyActivitySchema = z34.object({
5474
+ rows: z34.array(autonomyActivityRowSchema)
5398
5475
  // newest first
5399
5476
  }).strict();
5400
- var savedIdSchema = z33.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
5477
+ var savedIdSchema = z34.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
5401
5478
  var SAVED_REF_PREFIX = "refs/halfcycle/saved/";
5402
5479
  function savedRefOf(id) {
5403
5480
  return `${SAVED_REF_PREFIX}${id}`;
@@ -5436,33 +5513,33 @@ function isGitFolderSegment(segment) {
5436
5513
  return t === ".git" || /^git~\d+$/.test(t);
5437
5514
  }
5438
5515
  var SAVED_PATH_KEPT_IN = ["commit", "copy"];
5439
- var savedPathSchema = z33.object({
5440
- path: z33.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
5516
+ var savedPathSchema = z34.object({
5517
+ path: z34.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
5441
5518
  // the file, relative to the project
5442
- keptIn: z33.enum(SAVED_PATH_KEPT_IN)
5519
+ keptIn: z34.enum(SAVED_PATH_KEPT_IN)
5443
5520
  // in the save's commit, or copied beside this file
5444
5521
  }).strict();
5445
- var savedCommitSchema = z33.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
5446
- var savedBranchSchema = z33.object({
5447
- name: z33.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
5522
+ var savedCommitSchema = z34.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
5523
+ var savedBranchSchema = z34.object({
5524
+ name: z34.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
5448
5525
  message: "must be a branch name"
5449
5526
  }),
5450
5527
  // the branch's name
5451
5528
  commit: savedCommitSchema
5452
5529
  // the commit it pointed at
5453
5530
  }).strict();
5454
- var savedSchema = z33.object({
5531
+ var savedSchema = z34.object({
5455
5532
  id: savedIdSchema,
5456
5533
  // what restore is typed with
5457
5534
  savedAt: utcIso8601,
5458
5535
  // when the copy was made
5459
- ref: z33.string().min(1),
5536
+ ref: z34.string().min(1),
5460
5537
  // where the saved commit is kept
5461
- what: z33.string().min(1).max(2e3),
5538
+ what: z34.string().min(1).max(2e3),
5462
5539
  // what the action would have changed, in words
5463
- paths: z33.array(savedPathSchema),
5540
+ paths: z34.array(savedPathSchema),
5464
5541
  // every saved file
5465
- branches: z33.array(savedBranchSchema)
5542
+ branches: z34.array(savedBranchSchema)
5466
5543
  // every saved line of work
5467
5544
  }).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
5468
5545
  function autonomyA13(accepted, carried) {
@@ -5472,8 +5549,9 @@ function autonomyA14(count, parts, acceptLine) {
5472
5549
  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
5550
  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
5551
  }
5475
- function autonomyA15(count) {
5476
- return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
5552
+ function autonomyA15(count, acceptLine) {
5553
+ const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `type ${acceptLine} to accept them and close the phase, or say "review what was built" to decide one differently.`;
5554
+ return `The phase waits for you: ${String(count)} parts were built without an approved scope or outside the plan, and only you can decide them. In your own coding agent's session, ${typed}`;
5477
5555
  }
5478
5556
  function namesOf(names) {
5479
5557
  if (names.length <= 1)
@@ -6431,6 +6509,8 @@ function kinds() {
6431
6509
  [".claude/hooks/user-prompt-findings.sh", "check-script"],
6432
6510
  [".claude/hooks/session-end-marker.sh", "check-script"],
6433
6511
  [".claude/hooks/session-start-marker.sh", "check-script"],
6512
+ [".claude/hooks/context-budget-check.sh", "check-script"],
6513
+ [".claude/hooks/context-drift-check.sh", "check-script"],
6434
6514
  [SETTINGS_REL, "settings"],
6435
6515
  [MCP_REGISTRATION_REL, "server-entry"],
6436
6516
  [GITIGNORE_REL, "ignore-list"],
@@ -8425,13 +8505,133 @@ var MCP_HEADERS_KNOWN_HEADER_HASHES = [
8425
8505
  function planMcpHeadersHelperRemoval(targetRepo) {
8426
8506
  return decideRetiredRemoval(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
8427
8507
  }
8508
+ var GENERATED_CONTEXT_BUDGET_HEADER = "# Context budget check \u2014 generated by";
8509
+ function generateContextBudgetCheck() {
8510
+ return `#!/usr/bin/env bash
8511
+ ${GENERATED_CONTEXT_BUDGET_HEADER} the Halfcycle installer.
8512
+ # Runs when a session starts. It warns, and never blocks, when the instructions
8513
+ # every session loads have grown past their budget, so they can be trimmed
8514
+ # before every later session pays for them:
8515
+ # - AGENTS.md at the project root, over 16384 bytes;
8516
+ # - this project's memory index in Claude Code, over 20480 bytes or 150 lines
8517
+ # (Claude Code cuts the index off at about 25 KB or 200 lines when it loads it).
8518
+
8519
+ cat >/dev/null 2>&1 || true
8520
+
8521
+ SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )" || exit 0
8522
+ PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )" || exit 0
8523
+
8524
+ NOTES=""
8525
+
8526
+ AGENTS_FILE="$PROJECT_ROOT/AGENTS.md"
8527
+ if [ -f "$AGENTS_FILE" ]; then
8528
+ BYTES=$(wc -c < "$AGENTS_FILE" | tr -d ' ')
8529
+ if [ "\${BYTES:-0}" -gt 16384 ]; then
8530
+ NOTES="\${NOTES}AGENTS.md is \${BYTES} bytes, over its budget of 16384. "
8531
+ fi
8532
+ fi
8533
+
8534
+ CONFIG_DIR="\${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
8535
+ SLUG=$(printf '%s' "\${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}" | sed 's/[^A-Za-z0-9]/-/g')
8536
+ INDEX="$CONFIG_DIR/projects/$SLUG/memory/MEMORY.md"
8537
+ if [ -f "$INDEX" ]; then
8538
+ BYTES=$(wc -c < "$INDEX" | tr -d ' ')
8539
+ LINES=$(wc -l < "$INDEX" | tr -d ' ')
8540
+ if [ "\${BYTES:-0}" -gt 20480 ] || [ "\${LINES:-0}" -gt 150 ]; then
8541
+ NOTES="\${NOTES}The memory index is \${BYTES} bytes and \${LINES} lines, over its budget of 20480 bytes and 150 lines. "
8542
+ fi
8543
+ fi
8544
+
8545
+ if [ -z "$NOTES" ]; then exit 0; fi
8546
+
8547
+ MESSAGE="Context budget: \${NOTES}Run the context-update command to fold and trim it."
8548
+ printf '{"systemMessage":"%s","hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MESSAGE" "$MESSAGE"
8549
+ exit 0
8550
+ `;
8551
+ }
8552
+ var GENERATED_CONTEXT_DRIFT_HEADER = "# Context drift check \u2014 generated by";
8553
+ function generateContextDriftCheck() {
8554
+ return `#!/usr/bin/env bash
8555
+ ${GENERATED_CONTEXT_DRIFT_HEADER} the Halfcycle installer.
8556
+ # Context drift detector \u2014 runs after each file edit. If a trigger file was modified,
8557
+ # it asks the session to run the context-update command before the session ends.
8558
+
8559
+ set -euo pipefail
8560
+
8561
+ INPUT=$(cat)
8562
+
8563
+ # First line: the event's name, for the reply. Then one line per edited file, made
8564
+ # absolute against the event's cwd: the edit's file_path, or every path a patch names.
8565
+ # With no python3 on the machine it says nothing, rather than fail after every edit.
8566
+ PARSED=$(HOOK_INPUT="$INPUT" python3 - <<'PYEOF'
8567
+ import json, os, re
8568
+ try:
8569
+ data = json.loads(os.environ["HOOK_INPUT"])
8570
+ except Exception:
8571
+ raise SystemExit(0)
8572
+ tool_input = data.get("tool_input") or {}
8573
+ cwd = data.get("cwd") or os.getcwd()
8574
+ if tool_input.get("file_path"):
8575
+ paths = [tool_input["file_path"]]
8576
+ else:
8577
+ headers = r"^\\*\\*\\* (?:Add File|Update File|Delete File|Move to): (.+)$"
8578
+ paths = re.findall(headers, str(tool_input.get("command", "")), re.M)
8579
+ print(data.get("hook_event_name", ""))
8580
+ for p in paths:
8581
+ print(os.path.realpath(os.path.join(cwd, p.strip())))
8582
+ PYEOF
8583
+ ) || exit 0
8584
+
8585
+ if [ -z "$PARSED" ]; then exit 0; fi
8586
+ HOOK_EVENT=$(printf '%s\\n' "$PARSED" | head -n 1)
8587
+
8588
+ # Portable PROJECT_ROOT derivation: the script lives two folders below the root
8589
+ SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )"
8590
+ PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )"
8591
+
8592
+ REASONS=""
8593
+ while IFS= read -r FILE_PATH; do
8594
+ [ -n "$FILE_PATH" ] || continue
8595
+ case "$FILE_PATH" in
8596
+ "$PROJECT_ROOT"/*) ;;
8597
+ *) continue ;;
8598
+ esac
8599
+
8600
+ REL_PATH="\${FILE_PATH#$PROJECT_ROOT/}"
8601
+
8602
+ # Skip the instructions files and each agent's own folders (editing them needs no reminder)
8603
+ case "$REL_PATH" in
8604
+ CLAUDE.md|AGENTS.md|README.md|.claude/*|.codex/*|.agents/*|dist/*|node_modules/*|.git/*)
8605
+ continue ;;
8606
+ esac
8607
+
8608
+ REASON=""
8609
+ case "$REL_PATH" in
8610
+ package.json|package-lock.json) REASON="package.json changed \u2014 check Tech Stack / Commands sections" ;;
8611
+ wrangler.jsonc|wrangler.toml) REASON="wrangler config changed \u2014 check Deployment section" ;;
8612
+ astro.config.*|next.config.*|vite.config.*) REASON="build config changed \u2014 check Tech Stack / Architecture" ;;
8613
+ src/pages/api/*|src/routes/api/*|pages/api/*) REASON="API route changed \u2014 check API Endpoints" ;;
8614
+ scripts/*) REASON="scripts/ changed \u2014 check Commands section" ;;
8615
+ tsconfig.json|.eslintrc*|.prettierrc*) REASON="tool config changed \u2014 check Code Conventions" ;;
8616
+ esac
8617
+ if [ -n "$REASON" ]; then REASONS="\${REASONS}\${REL_PATH} was modified: \${REASON}. "; fi
8618
+ done < <(printf '%s\\n' "$PARSED" | tail -n +2)
8619
+
8620
+ if [ -z "$REASONS" ]; then exit 0; fi
8621
+
8622
+ REMINDER="Context drift detected. \${REASONS}Before ending this session, run the context-update command to audit and update AGENTS.md and the memory folder."
8623
+ python3 -c 'import json, sys; print(json.dumps({"hookSpecificOutput": {"hookEventName": sys.argv[1], "additionalContext": sys.argv[2]}}))' "$HOOK_EVENT" "$REMINDER"
8624
+ `;
8625
+ }
8428
8626
  var OWNED_GENERATED_HEADERS = {
8429
8627
  ".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
8430
8628
  ".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
8431
8629
  ".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
8432
8630
  ".claude/hooks/user-prompt-findings.sh": GENERATED_USER_PROMPT_FINDINGS_HEADER,
8433
8631
  ".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
8434
- ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
8632
+ ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER,
8633
+ ".claude/hooks/context-budget-check.sh": GENERATED_CONTEXT_BUDGET_HEADER,
8634
+ ".claude/hooks/context-drift-check.sh": GENERATED_CONTEXT_DRIFT_HEADER
8435
8635
  };
8436
8636
  function hookScriptsThisVersionWrites() {
8437
8637
  return [
@@ -8440,7 +8640,9 @@ function hookScriptsThisVersionWrites() {
8440
8640
  [".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
8441
8641
  [".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
8442
8642
  [".claude/hooks/user-prompt-findings.sh", generateUserPromptFindingsHook()],
8443
- [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
8643
+ [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()],
8644
+ [".claude/hooks/context-budget-check.sh", generateContextBudgetCheck()],
8645
+ [".claude/hooks/context-drift-check.sh", generateContextDriftCheck()]
8444
8646
  ];
8445
8647
  }
8446
8648
  function generateMcpRegistration(existing, mcpOrigin) {
@@ -14044,20 +14246,14 @@ var CHANGE_WORDS = {
14044
14246
  remove: "planned to be removed",
14045
14247
  "not-in-plan": "not in the plan"
14046
14248
  };
14047
- function awaitingAnswer(part) {
14048
- return part.questions.some((q) => q.agentAnswer === void 0);
14049
- }
14050
- function sentBackInRound(part, round) {
14051
- return part.sentBack.some((s) => s.round === round);
14052
- }
14053
14249
  function partStatus(part, round) {
14054
14250
  const settled = settledStatus(part);
14055
14251
  if (settled !== void 0) {
14056
14252
  return part.givenIn === "autonomy" ? `${settled}, ${DECIDED_BY_AUTONOMY_SENTENCE}` : settled;
14057
14253
  }
14058
- if (awaitingAnswer(part))
14254
+ if (reviewPartAwaitingAnswer(part))
14059
14255
  return "Waiting for the agent's answer";
14060
- if (sentBackInRound(part, round))
14256
+ if (reviewPartSentBackIn(part, round))
14061
14257
  return "Sent back, waiting for the next round";
14062
14258
  return "Waits for your decision";
14063
14259
  }
@@ -14232,22 +14428,7 @@ var AN3_LINE = "halfcycle review: the answer must be one paragraph of at most 1,
14232
14428
  var AN4_LINE = "halfcycle review: the answer looks like it holds a password or key, so it was not sent. Write it again without it.";
14233
14429
  var AN5_LINE = "halfcycle review: the answer was not accepted. Reword it in plain words and run this again.";
14234
14430
  function shownReviewEntry(view, shownAt) {
14235
- const parts = view.parts.map((p) => ({
14236
- part: p.part,
14237
- name: p.name,
14238
- built: p.built,
14239
- decidable: p.decidable,
14240
- awaitingAnswer: awaitingAnswer(p),
14241
- sentBack: sentBackInRound(p, view.round)
14242
- }));
14243
- return {
14244
- phase: view.phase,
14245
- number: view.number,
14246
- round: view.round,
14247
- reviewVersion: view.reviewVersion,
14248
- shownAt,
14249
- parts
14250
- };
14431
+ return shownReviewEntryOf(view, shownAt);
14251
14432
  }
14252
14433
  function writeShownReview(engagementId, entry, home) {
14253
14434
  const written = updateShownRecord(engagementId, (current) => ({
@@ -14719,7 +14900,7 @@ function closeStepReport(phase, attempt, engagementId) {
14719
14900
  exitCode = 0;
14720
14901
  break;
14721
14902
  case "autonomy-waits":
14722
- stdout.push(answer.reason === "unchecked-claims" ? autonomyA14(answer.count, answer.parts.map((p) => p.name), typedAcceptLine(phase)) : autonomyA15(answer.count));
14903
+ stdout.push(answer.reason === "unchecked-claims" ? autonomyA14(answer.count, answer.parts.map((p) => p.name), typedAcceptLine(phase)) : autonomyA15(answer.count, typedAcceptLine(phase)));
14723
14904
  exitCode = 0;
14724
14905
  break;
14725
14906
  default: