halfcycle 0.4.14 → 0.4.15

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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/bin.bundle.mjs +2525 -541
  3. package/bin/pre-tool-use-filter.sh +41 -5
  4. package/commands/agent-teams-orchestration.md +1 -1
  5. package/commands/consistency-check.md +1 -1
  6. package/commands/context-update.md +1 -1
  7. package/commands/halfcycle-design.md +1 -1
  8. package/commands/halfcycle-setup.md +1 -1
  9. package/commands/stack-assembly-worker.md +1 -1
  10. package/commands/task-execution.md +1 -1
  11. package/commands/task-graph.md +1 -1
  12. package/commands/write-spec.md +1 -1
  13. package/dist/bin.js +0 -0
  14. package/dist/build-record/render-v2.d.ts.map +1 -1
  15. package/dist/claude-plan.d.ts +24 -5
  16. package/dist/claude-plan.d.ts.map +1 -1
  17. package/dist/cli-contract.d.ts +1 -1
  18. package/dist/cli-contract.d.ts.map +1 -1
  19. package/dist/cli.js +2242 -565
  20. package/dist/cli.js.map +3 -3
  21. package/dist/close-phase.d.ts.map +1 -1
  22. package/dist/doctor.d.ts +34 -0
  23. package/dist/doctor.d.ts.map +1 -0
  24. package/dist/folder-refusals.d.ts +27 -0
  25. package/dist/folder-refusals.d.ts.map +1 -0
  26. package/dist/identity.d.ts +31 -1
  27. package/dist/identity.d.ts.map +1 -1
  28. package/dist/index.js +1276 -386
  29. package/dist/index.js.map +3 -3
  30. package/dist/install-plan.d.ts +112 -0
  31. package/dist/install-plan.d.ts.map +1 -0
  32. package/dist/install.d.ts +535 -1
  33. package/dist/install.d.ts.map +1 -1
  34. package/dist/plan.d.ts +16 -3
  35. package/dist/plan.d.ts.map +1 -1
  36. package/dist/pre-create-checks.d.ts +28 -0
  37. package/dist/pre-create-checks.d.ts.map +1 -1
  38. package/dist/preview.d.ts +40 -0
  39. package/dist/preview.d.ts.map +1 -0
  40. package/dist/restore.d.ts +58 -0
  41. package/dist/restore.d.ts.map +1 -0
  42. package/dist/review.d.ts.map +1 -1
  43. package/dist/scan.d.ts +11 -0
  44. package/dist/scan.d.ts.map +1 -1
  45. package/dist/setup/discovery.d.ts.map +1 -1
  46. package/dist/setup/rows.d.ts +10 -6
  47. package/dist/setup/rows.d.ts.map +1 -1
  48. package/package.json +12 -12
@@ -14663,9 +14663,10 @@ var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
14663
14663
  var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
14664
14664
  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
+ var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
14666
14667
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
14667
14668
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
14668
- var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent)$";
14669
+ var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
14669
14670
  function hookCommand(script, verb) {
14670
14671
  const invocation = `bash "$CLAUDE_PROJECT_DIR"/${script}`;
14671
14672
  return verb === void 0 ? invocation : `${invocation} ${verb}`;
@@ -14699,6 +14700,13 @@ var HALFCYCLE_HOOK_WIRING = [
14699
14700
  command: hookCommand(USER_PROMPT_SCOPE_REL),
14700
14701
  script: USER_PROMPT_SCOPE_REL
14701
14702
  },
14703
+ {
14704
+ event: "UserPromptSubmit",
14705
+ matcher: null,
14706
+ timeout: 10,
14707
+ command: hookCommand(USER_PROMPT_FINDINGS_REL),
14708
+ script: USER_PROMPT_FINDINGS_REL
14709
+ },
14702
14710
  {
14703
14711
  event: "PreToolUse",
14704
14712
  matcher: PRE_TOOL_USE_MATCHER,
@@ -16393,7 +16401,7 @@ var SCOPE_PART_STATUSES = ["approved", "waiting"];
16393
16401
  var scopePartStatusSchema = external_exports.enum(SCOPE_PART_STATUSES);
16394
16402
  var SCOPE_ACTS = ["approve", "send-back"];
16395
16403
  var scopeActSchema = external_exports.enum(SCOPE_ACTS);
16396
- var SCOPE_GIVEN_IN = ["console", "chat"];
16404
+ var SCOPE_GIVEN_IN = ["console", "chat", "autonomy"];
16397
16405
  var scopeGivenInSchema = external_exports.enum(SCOPE_GIVEN_IN);
16398
16406
  var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
16399
16407
  var scopeNoteWireSchema = external_exports.string().max(SCOPE_NOTE_MAX);
@@ -16425,7 +16433,8 @@ var phasePlanRecordedSchema = external_exports.object({
16425
16433
  typedApprovalOffered: external_exports.boolean(),
16426
16434
  reportedAt: utcIso8601,
16427
16435
  carriedFrom: external_exports.array(carriedFromEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX).optional(),
16428
- movedUp: planMovedUpSchema.optional()
16436
+ movedUp: planMovedUpSchema.optional(),
16437
+ givenIn: scopeGivenInSchema.optional()
16429
16438
  }).strict();
16430
16439
  var phasePlanRefusedSchema = external_exports.object({
16431
16440
  outcome: external_exports.literal("refused"),
@@ -16823,6 +16832,12 @@ var PROJECT_RECORD_PATTERNS = [
16823
16832
  { pattern: "docs/build-records/build-tasks.json", name: () => "the build's progress record (build-tasks.json)" },
16824
16833
  { pattern: "docs/build-records/task-plan.json", name: () => "the build's plan (task-plan.json)", shrinkAsked: false },
16825
16834
  { pattern: "docs/build-records", name: (file2) => `a closed phase's Build Record (${file2})` },
16835
+ {
16836
+ pattern: ".claude/context/tasks/*",
16837
+ name: (file2) => `a task's notes for this phase (${file2})`,
16838
+ shrinkAsked: false,
16839
+ deleteAsked: false
16840
+ },
16826
16841
  { pattern: ".claude/context", name: (file2) => `the project's working notes (${file2})` }
16827
16842
  ];
16828
16843
  var observedCountSchema = external_exports.number({ error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).int({ error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).min(0, { error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).max(OBSERVED_COUNT_MAX, { error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` });
@@ -17645,6 +17660,14 @@ var REVIEW_PART_CHANGES = Object.freeze([...PHASE_PLAN_CHANGE_KINDS, "not-in-pla
17645
17660
  var reviewPartChangeSchema = external_exports.enum(REVIEW_PART_CHANGES);
17646
17661
  var REVIEW_DECISIONS = Object.freeze(["accepted", "carried", "dropped"]);
17647
17662
  var reviewDecisionSchema = external_exports.enum(REVIEW_DECISIONS);
17663
+ var reviewGivenInSchema = scopeGivenInSchema.extract(["autonomy"]);
17664
+ function checkReviewGivenIn(v, ctx) {
17665
+ if (v.givenIn === void 0)
17666
+ return;
17667
+ if (v.decision !== "accepted" && v.decision !== "carried") {
17668
+ ctx.addIssue({ code: "custom", path: ["givenIn"], message: "is present only on a part accepted or carried" });
17669
+ }
17670
+ }
17648
17671
  function actOf(decision) {
17649
17672
  if (decision === "accepted")
17650
17673
  return "accept";
@@ -17712,6 +17735,25 @@ var reviewRefusedSchema = external_exports.object({
17712
17735
  function bareReviewOutcome(outcome) {
17713
17736
  return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
17714
17737
  }
17738
+ var reviewClosedByAutonomySchema = external_exports.object({
17739
+ outcome: external_exports.literal("closed-by-autonomy"),
17740
+ accepted: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
17741
+ carried: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX)
17742
+ }).strict().refine((v) => v.accepted + v.carried <= REVIEW_PARTS_MAX, {
17743
+ message: "cannot exceed the parts a round holds",
17744
+ path: ["carried"]
17745
+ });
17746
+ var AUTONOMY_WAIT_REASONS = ["unchecked-claims", "built-without-approval"];
17747
+ var autonomyWaitReasonSchema = external_exports.enum(AUTONOMY_WAIT_REASONS);
17748
+ var reviewAutonomyWaitsSchema = external_exports.object({
17749
+ outcome: external_exports.literal("autonomy-waits"),
17750
+ reason: autonomyWaitReasonSchema,
17751
+ count: external_exports.number().int().min(1),
17752
+ parts: external_exports.array(external_exports.object({ part: systemPartIdSchema, name: systemPartSchema.shape.name }).strict()).min(1).max(REVIEW_PARTS_MAX)
17753
+ }).strict().refine((v) => v.reason !== "built-without-approval" || v.count === v.parts.length, {
17754
+ message: "counts the parts it names",
17755
+ path: ["count"]
17756
+ });
17715
17757
  var openReviewResultSchema = external_exports.discriminatedUnion("outcome", [
17716
17758
  reviewOpenedSchema,
17717
17759
  reviewTasksOpenSchema,
@@ -17720,7 +17762,9 @@ var openReviewResultSchema = external_exports.discriminatedUnion("outcome", [
17720
17762
  reviewOutstandingSchema,
17721
17763
  reviewCleanOverFindingsSchema,
17722
17764
  reviewRefusedSchema,
17723
- bareReviewOutcome("unavailable")
17765
+ bareReviewOutcome("unavailable"),
17766
+ reviewClosedByAutonomySchema,
17767
+ reviewAutonomyWaitsSchema
17724
17768
  ]);
17725
17769
  var reviewPhaseNotOpenSchema = external_exports.object({
17726
17770
  statusCode: external_exports.literal(409),
@@ -17834,6 +17878,7 @@ var reviewViewPartSchema = external_exports.object({
17834
17878
  decidable: external_exports.boolean(),
17835
17879
  waiting: external_exports.boolean(),
17836
17880
  decision: reviewDecisionSchema.optional(),
17881
+ givenIn: reviewGivenInSchema.optional(),
17837
17882
  reason: reviewNoteSchema.optional(),
17838
17883
  carriedTo: reviewCarriedToSchema.optional(),
17839
17884
  sentBack: external_exports.array(reviewSentBackSchema),
@@ -17861,6 +17906,7 @@ var reviewViewPartSchema = external_exports.object({
17861
17906
  if (v.change === "not-in-plan" && !v.built) {
17862
17907
  ctx.addIssue({ code: "custom", path: ["built"], message: "is always true for a part outside the plan" });
17863
17908
  }
17909
+ checkReviewGivenIn(v, ctx);
17864
17910
  });
17865
17911
  var reviewPhaseNumberSchema = external_exports.number().int().min(1).nullable();
17866
17912
  var phaseReviewNoneSchema = external_exports.object({
@@ -18286,6 +18332,87 @@ var reviewAnswerResultSchema = external_exports.discriminatedUnion("outcome", [
18286
18332
  bareOutcome2("unavailable")
18287
18333
  ]);
18288
18334
 
18335
+ // ../events/dist/owner-waits.js
18336
+ var OWNER_WAIT_ITEMS = Object.freeze([
18337
+ Object.freeze({ id: "delete-users-data", words: "Deleting or replacing data people already have." }),
18338
+ Object.freeze({
18339
+ id: "change-promises",
18340
+ words: "Changing what the product promises its users, such as pricing, terms or privacy."
18341
+ }),
18342
+ Object.freeze({ id: "spend-or-sign-up", words: "Anything that costs money or signs up for a service." }),
18343
+ Object.freeze({ id: "publish-or-release", words: "Publishing or releasing to users." }),
18344
+ Object.freeze({ id: "reshape-shared-data", words: "Dropping or reshaping stored data that other parts read." }),
18345
+ Object.freeze({ id: "change-standing-rule", words: "Changing a standing rule." })
18346
+ ]);
18347
+ var OWNER_WAIT_ITEM_IDS = Object.freeze(OWNER_WAIT_ITEMS.map((item) => item.id));
18348
+ var ownerWaitItemIdSchema = external_exports.enum(OWNER_WAIT_ITEM_IDS);
18349
+ var OWNER_WAIT_FIXED_COUNT = OWNER_WAIT_ITEMS.length;
18350
+ var OWNER_WAIT_ADDITIONS_MAX = 20;
18351
+ var OWNER_WAIT_TEXT_MAX = 240;
18352
+ var ownerWaitItemRefSchema = external_exports.union([
18353
+ external_exports.number().int().min(1),
18354
+ external_exports.string().uuid({ message: "must be a number from 1 to 6, or the id shown beside an added item" })
18355
+ ]);
18356
+ var ownerWaitText = descriptionText(OWNER_WAIT_TEXT_MAX);
18357
+ var ownerWaitAddRequestSchema = external_exports.object({
18358
+ engagementId: external_exports.string().min(1),
18359
+ text: ownerWaitText,
18360
+ reason: ownerWaitText
18361
+ }).strict();
18362
+ var ownerWaitAddResultSchema = external_exports.discriminatedUnion("outcome", [
18363
+ external_exports.object({ outcome: external_exports.literal("added"), id: external_exports.string().uuid(), addedAt: utcIso8601 }).strict(),
18364
+ external_exports.object({ outcome: external_exports.literal("refused"), reason: external_exports.literal("too-many") }).strict()
18365
+ ]);
18366
+ var ownerWaitUndoableSchema = external_exports.enum(["yes", "no"]);
18367
+ var ownerWaitDeclareRequestSchema = external_exports.object({
18368
+ engagementId: external_exports.string().min(1),
18369
+ item: ownerWaitItemRefSchema,
18370
+ question: ownerWaitText,
18371
+ recommendation: ownerWaitText,
18372
+ task: buildTaskIdSchema.optional(),
18373
+ by: agentNameSchema.optional(),
18374
+ undoable: ownerWaitUndoableSchema.optional(),
18375
+ how: statementText(STATEMENT_MAX).optional()
18376
+ }).strict().refine((r) => r.undoable === "yes" === (r.how !== void 0), {
18377
+ message: "says how to undo it exactly when it can be undone",
18378
+ path: ["how"]
18379
+ });
18380
+ var OWNER_WAIT_DECLARE_REFUSALS = ["no-open-phase", "unknown-item", "say-if-undoable"];
18381
+ var ownerWaitDeclareResultSchema = external_exports.discriminatedUnion("outcome", [
18382
+ external_exports.object({ outcome: external_exports.literal("recorded"), id: external_exports.string().uuid(), say: external_exports.string().min(1) }).strict(),
18383
+ external_exports.object({
18384
+ outcome: external_exports.literal("go-ahead"),
18385
+ id: external_exports.string().uuid(),
18386
+ callId: external_exports.number().int().positive(),
18387
+ say: external_exports.string().min(1)
18388
+ }).strict(),
18389
+ external_exports.object({ outcome: external_exports.literal("refused"), reason: external_exports.enum(OWNER_WAIT_DECLARE_REFUSALS) }).strict()
18390
+ ]);
18391
+ var ownerWaitAddedItemSchema = external_exports.object({
18392
+ id: external_exports.string().uuid(),
18393
+ words: external_exports.string().min(1),
18394
+ reason: external_exports.string().min(1),
18395
+ addedAt: utcIso8601
18396
+ }).strict();
18397
+ var ownerWaitListSchema = external_exports.object({
18398
+ fixed: external_exports.array(external_exports.object({ id: ownerWaitItemIdSchema, words: external_exports.string().min(1) }).strict()).length(OWNER_WAIT_FIXED_COUNT),
18399
+ added: external_exports.array(ownerWaitAddedItemSchema).max(OWNER_WAIT_ADDITIONS_MAX)
18400
+ }).strict();
18401
+ var ownerWaitDeclarationSchema = external_exports.object({
18402
+ id: external_exports.string().uuid(),
18403
+ fixedItem: ownerWaitItemIdSchema.nullable(),
18404
+ addition: external_exports.object({ id: external_exports.string().uuid(), words: external_exports.string().min(1) }).strict().nullable(),
18405
+ question: external_exports.string().min(1),
18406
+ recommendation: external_exports.string().min(1),
18407
+ task: buildTaskIdSchema.nullable(),
18408
+ declaredAt: utcIso8601
18409
+ }).strict().refine((d) => d.fixedItem === null !== (d.addition === null), {
18410
+ message: "names exactly one item: a fixed one or an added one",
18411
+ path: ["fixedItem"]
18412
+ });
18413
+ var ownerWaitDeclarationsSchema = external_exports.object({ declarations: external_exports.array(ownerWaitDeclarationSchema) }).strict();
18414
+ 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 in the console first.`;
18415
+
18289
18416
  // ../events/dist/pacing.js
18290
18417
  var CLAUDE_PLANS = Object.freeze([
18291
18418
  Object.freeze({ id: "pro", words: "Pro", startingAgents: 2 }),
@@ -18417,10 +18544,212 @@ function paceStatusLineOf(percent, window) {
18417
18544
  return `Halfcycle \xB7 ${String(Math.round(percent))}% of this ${PACE_WINDOW_WORDS[window]} used`;
18418
18545
  }
18419
18546
 
18547
+ // ../events/dist/autonomy.js
18548
+ var AUTONOMY_CHANGED_IN = ["console", "chat"];
18549
+ var autonomyChangedInSchema = external_exports.enum(AUTONOMY_CHANGED_IN);
18550
+ var autonomySettingsSchema = external_exports.object({
18551
+ on: external_exports.boolean(),
18552
+ // whether builds run on their own
18553
+ mayPublish: external_exports.boolean(),
18554
+ // whether builds may publish or release without waiting
18555
+ maySpend: external_exports.boolean(),
18556
+ // whether builds may spend money or sign up without waiting
18557
+ changedAt: utcIso8601.nullable(),
18558
+ // when the setting last changed; null if never
18559
+ changedIn: autonomyChangedInSchema.nullable()
18560
+ // where it last changed; null if never
18561
+ }).strict().refine((v) => v.changedAt === null === (v.changedIn === null), {
18562
+ message: "says when and where it changed, or neither",
18563
+ path: ["changedIn"]
18564
+ });
18565
+ var autonomyChangeRequestSchema = external_exports.object({
18566
+ on: external_exports.boolean(),
18567
+ // the switch
18568
+ mayPublish: external_exports.boolean(),
18569
+ // the publish box
18570
+ maySpend: external_exports.boolean()
18571
+ // the spend box
18572
+ }).strict();
18573
+ var AUTONOMY_CHAT_ACTS = ["on", "off"];
18574
+ var autonomyChatActSchema = external_exports.enum(AUTONOMY_CHAT_ACTS);
18575
+ var autonomyChatRequestSchema = external_exports.object({
18576
+ act: autonomyChatActSchema
18577
+ // what the owner typed
18578
+ }).strict();
18579
+ var autonomyChatResultSchema = external_exports.discriminatedUnion("outcome", [
18580
+ external_exports.object({
18581
+ outcome: external_exports.literal("recorded"),
18582
+ on: external_exports.boolean()
18583
+ // whether builds now run on their own
18584
+ }).strict(),
18585
+ external_exports.object({ outcome: external_exports.literal("not-owner") }).strict(),
18586
+ external_exports.object({ outcome: external_exports.literal("no-owner") }).strict(),
18587
+ external_exports.object({ outcome: external_exports.literal("not-offered") }).strict(),
18588
+ external_exports.object({ outcome: external_exports.literal("unavailable") }).strict()
18589
+ ]);
18590
+ var autonomyStateSchema = external_exports.object({
18591
+ on: external_exports.boolean(),
18592
+ // whether builds run on their own
18593
+ mayPublish: external_exports.boolean(),
18594
+ // whether builds may publish or release without waiting
18595
+ maySpend: external_exports.boolean()
18596
+ // whether builds may spend money or sign up without waiting
18597
+ }).strict();
18598
+ var autonomyStateFileSchema = autonomyStateSchema.extend({
18599
+ readAt: utcIso8601
18600
+ // when this copy was read from Halfcycle
18601
+ }).strict();
18602
+ var AUTONOMY_STATE_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
18603
+ var AUTONOMY_ACTIVITY_KINDS = ["went-ahead", "waiting", "approved", "closed", "waiting-on-owner"];
18604
+ var autonomyActivityKindSchema = external_exports.enum(AUTONOMY_ACTIVITY_KINDS);
18605
+ var autonomyActivityRowSchema = external_exports.object({
18606
+ kind: autonomyActivityKindSchema,
18607
+ // what happened
18608
+ phase: scopePhaseSchema,
18609
+ // the phase it happened in
18610
+ question: ownerWaitText.optional(),
18611
+ // the agent's question, for a decision
18612
+ chose: ownerWaitText.optional(),
18613
+ // what the agent chose or recommends, for a decision
18614
+ undoable: ownerWaitUndoableSchema.optional(),
18615
+ // whether the agent says it can be undone
18616
+ how: statementText(STATEMENT_MAX).optional(),
18617
+ // how to undo it, when it can be
18618
+ at: utcIso8601
18619
+ // when it happened
18620
+ }).strict().superRefine((row, ctx) => {
18621
+ const decision = row.kind === "went-ahead" || row.kind === "waiting";
18622
+ if (decision && row.question === void 0) {
18623
+ ctx.addIssue({ code: "custom", message: "a decision says its question", path: ["question"] });
18624
+ }
18625
+ if (!decision) {
18626
+ for (const key of ["question", "chose", "undoable", "how"]) {
18627
+ if (row[key] !== void 0) {
18628
+ ctx.addIssue({ code: "custom", message: "only a decision carries this", path: [key] });
18629
+ }
18630
+ }
18631
+ }
18632
+ if (row.kind === "went-ahead") {
18633
+ if (row.chose === void 0) {
18634
+ ctx.addIssue({ code: "custom", message: "a decision that went ahead says what was chosen", path: ["chose"] });
18635
+ }
18636
+ if (row.undoable === void 0) {
18637
+ ctx.addIssue({
18638
+ code: "custom",
18639
+ message: "a decision that went ahead says whether it can be undone",
18640
+ path: ["undoable"]
18641
+ });
18642
+ }
18643
+ }
18644
+ if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
18645
+ ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
18646
+ }
18647
+ if (row.kind === "went-ahead" && row.undoable === "yes" !== (row.how !== void 0)) {
18648
+ ctx.addIssue({ code: "custom", message: "says how to undo it exactly when it can be undone", path: ["how"] });
18649
+ }
18650
+ });
18651
+ var autonomyActivitySchema = external_exports.object({
18652
+ rows: external_exports.array(autonomyActivityRowSchema)
18653
+ // newest first
18654
+ }).strict();
18655
+ var savedIdSchema = external_exports.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
18656
+ var SAVED_REF_PREFIX = "refs/halfcycle/saved/";
18657
+ function savedRefOf(id) {
18658
+ return `${SAVED_REF_PREFIX}${id}`;
18659
+ }
18660
+ var SAVED_KEEP_MS = 7 * 24 * 60 * 60 * 1e3;
18661
+ var SAVED_DIR_NAME = "saved";
18662
+ var SAVED_FILE_NAME = "saved.json";
18663
+ var SAVED_COPIES_DIR_NAME = "files";
18664
+ var SAVED_REPOSITORY_FILE_NAME = "repository";
18665
+ var SAVED_HALF_WRITTEN_GRACE_MS = 6e4;
18666
+ function savedPruneAnswer(save, here) {
18667
+ const old = (t) => !Number.isFinite(t) || here.now - t > SAVED_KEEP_MS;
18668
+ if (save.refMadeAt !== null) {
18669
+ if (!old(save.refMadeAt))
18670
+ return "keep";
18671
+ return save.folderChangedAt === null ? "remove-ref" : "remove-ref-then-folder";
18672
+ }
18673
+ if (save.folderChangedAt === null || !Number.isFinite(save.folderChangedAt))
18674
+ return "keep";
18675
+ const ours = save.folderRepository === null || here.repository !== null && save.folderRepository === here.repository;
18676
+ const wait = ours ? SAVED_HALF_WRITTEN_GRACE_MS : SAVED_KEEP_MS;
18677
+ return here.now - save.folderChangedAt > wait ? "remove-folder" : "keep";
18678
+ }
18679
+ function isProjectRelativePath(p) {
18680
+ if (p.length === 0 || hasControlCharacter(p))
18681
+ return false;
18682
+ if (p.startsWith("/") || p.startsWith("\\") || /^[A-Za-z]:/.test(p))
18683
+ return false;
18684
+ return p.split(/[\\/]/).every((segment) => segment !== "" && windowsNameOf(segment) !== "" && !isGitFolderSegment(segment));
18685
+ }
18686
+ function windowsNameOf(segment) {
18687
+ return segment.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff]/g, "").split(":")[0].replace(/[. ]+$/, "").toLowerCase();
18688
+ }
18689
+ function isGitFolderSegment(segment) {
18690
+ const t = windowsNameOf(segment);
18691
+ return t === ".git" || /^git~\d+$/.test(t);
18692
+ }
18693
+ var SAVED_PATH_KEPT_IN = ["commit", "copy"];
18694
+ var savedPathSchema = external_exports.object({
18695
+ path: external_exports.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
18696
+ // the file, relative to the project
18697
+ keptIn: external_exports.enum(SAVED_PATH_KEPT_IN)
18698
+ // in the save's commit, or copied beside this file
18699
+ }).strict();
18700
+ var savedCommitSchema = external_exports.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
18701
+ var savedBranchSchema = external_exports.object({
18702
+ name: external_exports.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
18703
+ message: "must be a branch name"
18704
+ }),
18705
+ // the branch's name
18706
+ commit: savedCommitSchema
18707
+ // the commit it pointed at
18708
+ }).strict();
18709
+ var savedSchema = external_exports.object({
18710
+ id: savedIdSchema,
18711
+ // what restore is typed with
18712
+ savedAt: utcIso8601,
18713
+ // when the copy was made
18714
+ ref: external_exports.string().min(1),
18715
+ // where the saved commit is kept
18716
+ what: external_exports.string().min(1).max(2e3),
18717
+ // what the action would have changed, in words
18718
+ paths: external_exports.array(savedPathSchema),
18719
+ // every saved file
18720
+ branches: external_exports.array(savedBranchSchema)
18721
+ // every saved line of work
18722
+ }).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
18723
+ var AUTONOMY_A4 = 'Builds in this project now run on their own. Turn it off with "autonomy off", or in the console.';
18724
+ var AUTONOMY_A5 = "Builds in this project wait for you again.";
18725
+ function withoutFinalStop(text) {
18726
+ return text.endsWith(".") ? text.slice(0, -1) : text;
18727
+ }
18728
+ function autonomyA9(what, id) {
18729
+ return `Halfcycle saved a copy before this: ${withoutFinalStop(what)}. To bring it back, run: ${HALFCYCLE_COMMAND} restore ${id} (kept for 7 days).`;
18730
+ }
18731
+ function autonomyA10(typeSentence, item) {
18732
+ return `${typeSentence} Builds run on their own here, but this still waits for the owner. Declare it with halfcycle_declare_owner_wait, item ${String(item)}, and carry on with other work.`;
18733
+ }
18734
+ function autonomyA11(typeSentence) {
18735
+ return `${typeSentence} This is never done while builds run on their own. Find a way that does not need the secrets.`;
18736
+ }
18737
+ function autonomyA12(typeSentence, reason) {
18738
+ return `${typeSentence} Halfcycle could not save a copy first (${reason}), so it was stopped. Nothing was changed.`;
18739
+ }
18740
+ var AUTONOMY_COULD_NOT_TELL_SENTENCE = "Halfcycle: This step can delete, overwrite, publish or show something that cannot be taken back, and Halfcycle could not work out what.";
18741
+ var AUTONOMY_COULD_NOT_TELL_REASON = "it could not tell what this would change";
18742
+ var AUTONOMY_SAVE_TOO_LARGE_REASON = "more than 200 MB of files would have to be copied";
18743
+ var AUTONOMY_SAVE_NO_HISTORY_REASON = "the project has no saved history yet";
18744
+ var AUTONOMY_SAVE_FAILED_REASON = "the copy could not be made";
18745
+ var AUTONOMY_SAVE_NOT_COVERED_REASON = "this kind of change cannot be saved yet";
18746
+ var AUTONOMY_A18 = "Halfcycle cannot check PowerShell commands while builds run on their own. Use Bash for this; nothing was changed.";
18747
+
18420
18748
  // dist/hook-protocol.js
18421
18749
  import { writeSync } from "node:fs";
18422
- function emitBlock(reason) {
18423
- process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
18750
+ function emitBlock(reason, systemMessage) {
18751
+ const body = systemMessage === void 0 ? { decision: "block", reason } : { decision: "block", reason, systemMessage };
18752
+ process.stdout.write(JSON.stringify(body) + "\n");
18424
18753
  process.exit(0);
18425
18754
  }
18426
18755
  function preToolUseReplyText(answer, text = "", agentLine) {
@@ -18442,39 +18771,61 @@ function preToolUseNoticeText(text) {
18442
18771
  return JSON.stringify({ systemMessage: text });
18443
18772
  }
18444
18773
  function nonBlockingReply(event, text) {
18445
- if (event === "PostToolUse" || event === "SessionStart") {
18774
+ if (event === "SessionStart") {
18446
18775
  return JSON.stringify({
18447
18776
  systemMessage: text,
18448
18777
  hookSpecificOutput: { hookEventName: event, additionalContext: text }
18449
18778
  });
18450
18779
  }
18780
+ if (event === "PostToolUse") {
18781
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } });
18782
+ }
18451
18783
  return JSON.stringify({ systemMessage: text });
18452
18784
  }
18453
18785
  function hookReply(event) {
18454
18786
  const queued = [];
18787
+ const forAgent = [];
18455
18788
  return {
18456
18789
  notice(text) {
18457
18790
  queued.push(text);
18458
18791
  },
18459
- block(reason) {
18792
+ agentContext(text) {
18793
+ forAgent.push(text);
18794
+ },
18795
+ block(reason, systemMessage) {
18460
18796
  const carried = queued.splice(0);
18461
- return emitBlock(carried.length === 0 ? reason : [reason, ...carried].join("\n"));
18797
+ return emitBlock(carried.length === 0 ? reason : [reason, ...carried].join("\n"), systemMessage);
18462
18798
  },
18463
18799
  flush() {
18464
- if (queued.length === 0)
18465
- return;
18466
18800
  const text = queued.splice(0).join("\n");
18801
+ const context = event === "PostToolUse" ? forAgent.splice(0).join("\n") : "";
18467
18802
  const code = process.exitCode;
18468
18803
  if (code !== void 0 && code !== null && Number(code) !== 0) {
18469
- writeSync(2, `${text}
18804
+ if (text !== "")
18805
+ writeSync(2, `${text}
18806
+ `);
18807
+ return;
18808
+ }
18809
+ if (context !== "") {
18810
+ process.stdout.write(`${postToolUseReply(text, context)}
18470
18811
  `);
18471
18812
  return;
18472
18813
  }
18814
+ if (text === "")
18815
+ return;
18473
18816
  process.stdout.write(`${nonBlockingReply(event, text)}
18474
18817
  `);
18475
18818
  }
18476
18819
  };
18477
18820
  }
18821
+ function postToolUseReply(notice, context) {
18822
+ const additionalContext = notice === "" ? context : `${notice}
18823
+ ${context}`;
18824
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext } });
18825
+ }
18826
+ function userPromptContextReply(context) {
18827
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } });
18828
+ }
18478
18829
  function infraWarningText() {
18479
18830
  return "[Halfcycle] WARNING: Halfcycle's check service could not be reached, so nothing was checked this time. The session continues without checks.";
18480
18831
  }
@@ -18827,7 +19178,8 @@ function spawnGit(args2, cwd, timeoutMs, extra) {
18827
19178
  encoding: "utf-8",
18828
19179
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs },
18829
19180
  ...extra?.input === void 0 ? {} : { input: extra.input },
18830
- ...extra?.maxBuffer === void 0 ? {} : { maxBuffer: extra.maxBuffer }
19181
+ ...extra?.maxBuffer === void 0 ? {} : { maxBuffer: extra.maxBuffer },
19182
+ ...extra?.env === void 0 ? {} : { env: { ...process.env, ...extra.env } }
18831
19183
  });
18832
19184
  }
18833
19185
  function git(args2, cwd, timeoutMs, extra) {
@@ -18903,6 +19255,9 @@ function remoteHeadLine(remote, repoRoot, timeoutMs, globalArgs = []) {
18903
19255
  function readGitForFacts(args2, cwd, timeoutMs, input) {
18904
19256
  return git(args2, cwd, timeoutMs, { maxBuffer: 64 * 1024 * 1024, ...input === void 0 ? {} : { input } });
18905
19257
  }
19258
+ function gitForSave(args2, cwd, timeoutMs, extra = {}) {
19259
+ return git(args2, cwd, timeoutMs, { maxBuffer: 64 * 1024 * 1024, ...extra });
19260
+ }
18906
19261
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
18907
19262
  function resolveDiffBase(repoRoot, explicit) {
18908
19263
  const named = (explicit ?? "").trim();
@@ -18964,6 +19319,15 @@ function getRefToWorktreeChangedFiles(ref, repoRoot) {
18964
19319
  const files = result.value.split("\n").map((l) => l.trim()).filter(Boolean);
18965
19320
  return { ok: true, value: files };
18966
19321
  }
19322
+ function getUntrackedFiles(repoRoot, paths) {
19323
+ if (paths.length === 0)
19324
+ return { ok: true, value: [] };
19325
+ const pathspecs = paths.map((p) => `:(literal)${p}`);
19326
+ const result = git(["ls-files", "--others", "--exclude-standard", "-z", "--", ...pathspecs], repoRoot);
19327
+ if (!result.ok)
19328
+ return result;
19329
+ return { ok: true, value: result.value.split("\0").filter(Boolean) };
19330
+ }
18967
19331
  var PROVE_GIT_TIMEOUT_MS = 3e4;
18968
19332
  function pathsDifferingFromHead(repoRoot, timeoutMs = PROVE_GIT_TIMEOUT_MS) {
18969
19333
  const result = git(["--no-optional-locks", "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all"], repoRoot, timeoutMs);
@@ -20767,6 +21131,17 @@ var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
20767
21131
  var MARKER_PREFIX = "halfcycle-session-";
20768
21132
  var MARKER_SUFFIX = ".ref";
20769
21133
  var DEDUPE_SUFFIX = ".eval";
21134
+ var OWN_WORK_PREFIX = "halfcycle-own-";
21135
+ var OWN_WORK_SUFFIX = ".jsonl";
21136
+ var FINDINGS_PREFIX = "halfcycle-findings-";
21137
+ var FINDINGS_SUFFIX = ".json";
21138
+ var FINDINGS_ID_SOURCE = "^[A-Za-z0-9-]{1,128}$";
21139
+ var FINDINGS_ID = new RegExp(FINDINGS_ID_SOURCE);
21140
+ function findingsPath(sessionId) {
21141
+ if (typeof sessionId !== "string" || !FINDINGS_ID.test(sessionId))
21142
+ return null;
21143
+ return join6(markerDir(), `${FINDINGS_PREFIX}${sessionId}${FINDINGS_SUFFIX}`);
21144
+ }
20770
21145
  function markerDir() {
20771
21146
  const raw = process.env["TMPDIR"] || tmpdir();
20772
21147
  return raw.replace(/\/+$/, "");
@@ -20873,6 +21248,7 @@ function digestChangeState(base, entries) {
20873
21248
  function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now(), livenessCommon) {
20874
21249
  if (livenessCommon !== void 0)
20875
21250
  sweepLiveness(livenessCommon, now);
21251
+ sweepSessionFiles(keepSessionId, now);
20876
21252
  const dir = markerDir();
20877
21253
  const hash2 = repoHash(repoRoot);
20878
21254
  const keepName = keepSessionId ? markerFileName(repoRoot, keepSessionId) : markerFileName(repoRoot);
@@ -20912,6 +21288,33 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now(), livene
20912
21288
  }
20913
21289
  }
20914
21290
  }
21291
+ function sweepSessionFiles(keepSessionId, now) {
21292
+ const dir = markerDir();
21293
+ let names;
21294
+ try {
21295
+ names = readdirSync3(dir);
21296
+ } catch {
21297
+ return;
21298
+ }
21299
+ const kinds = [
21300
+ [FINDINGS_PREFIX, FINDINGS_SUFFIX],
21301
+ [OWN_WORK_PREFIX, OWN_WORK_SUFFIX]
21302
+ ];
21303
+ for (const name of names) {
21304
+ for (const [prefix, suffix] of kinds) {
21305
+ if (!name.startsWith(prefix) || !name.endsWith(suffix))
21306
+ continue;
21307
+ if (keepSessionId !== void 0 && name === `${prefix}${keepSessionId}${suffix}`)
21308
+ continue;
21309
+ const full = join6(dir, name);
21310
+ try {
21311
+ if (now - statSync(full).mtimeMs > MARKER_TTL_MS)
21312
+ unlinkSync3(full);
21313
+ } catch {
21314
+ }
21315
+ }
21316
+ }
21317
+ }
20915
21318
  function observeThrottlePath(folder) {
20916
21319
  return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}.observe-throttle`);
20917
21320
  }
@@ -21788,6 +22191,21 @@ function parseTimeLog(text) {
21788
22191
  }
21789
22192
  return out;
21790
22193
  }
22194
+ function readSessionTimeLines(sessionId, deps = {}) {
22195
+ try {
22196
+ const read = readConfig(deps.env ?? process.env);
22197
+ if (read.config === null)
22198
+ return [];
22199
+ const path = timeLogPath(read.config.guardEngagementId, deps.home);
22200
+ if (path === null)
22201
+ return [];
22202
+ const needle = JSON.stringify(sessionId);
22203
+ const kept = readFileSync11(path, "utf-8").split("\n").filter((raw) => raw.includes(needle));
22204
+ return parseTimeLog(kept.join("\n")).filter((line2) => line2.session === sessionId);
22205
+ } catch {
22206
+ return [];
22207
+ }
22208
+ }
21791
22209
  function shortHash(text) {
21792
22210
  return createHash2("sha256").update(text, "utf8").digest("hex").slice(0, 16);
21793
22211
  }
@@ -21809,8 +22227,8 @@ function separatorOf(path) {
21809
22227
  function subagentsFolderOf(transcript) {
21810
22228
  if (!transcript.endsWith(".jsonl") || transcript.length === ".jsonl".length)
21811
22229
  return null;
21812
- const sep4 = separatorOf(transcript);
21813
- return `${transcript.slice(0, -".jsonl".length)}${sep4}subagents`;
22230
+ const sep5 = separatorOf(transcript);
22231
+ return `${transcript.slice(0, -".jsonl".length)}${sep5}subagents`;
21814
22232
  }
21815
22233
  function subagentRecordName(agentId) {
21816
22234
  return /^[A-Za-z0-9_-]+$/.test(agentId) ? `agent-${agentId}.jsonl` : null;
@@ -22350,7 +22768,7 @@ async function pruneTimeLog(path, openedAt) {
22350
22768
  written = true;
22351
22769
  let seen = Buffer.byteLength(text, "utf-8");
22352
22770
  for (; ; ) {
22353
- await new Promise((resolve6) => setTimeout(resolve6, PRUNE_SETTLE_MS));
22771
+ await new Promise((resolve8) => setTimeout(resolve8, PRUNE_SETTLE_MS));
22354
22772
  const after = readFileSync11(aside);
22355
22773
  if (after.length <= seen)
22356
22774
  break;
@@ -22468,102 +22886,628 @@ async function sendTimeRecord(deps = {}) {
22468
22886
  }
22469
22887
  }
22470
22888
 
22471
- // dist/hooks/post-tool-use.js
22472
- var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
22473
- async function runPostToolUse(readSeat = readSeatConfiguration) {
22474
- const reply = hookReply("PostToolUse");
22475
- await evaluateEdit(reply, readSeat);
22476
- reply.flush();
22889
+ // dist/own-work.js
22890
+ import { appendFileSync as appendFileSync3, lstatSync, readFileSync as readFileSync12, realpathSync as realpathSync4 } from "node:fs";
22891
+ import { basename, dirname as dirname5, isAbsolute, join as join10, relative as relative4, resolve as resolve3, sep as sep2 } from "node:path";
22892
+
22893
+ // dist/risky/types.js
22894
+ var RISKY_ACTION_TYPES = [
22895
+ "delete-files",
22896
+ "discard-unsaved",
22897
+ "rewrite-shared-history",
22898
+ "delete-unmerged-work",
22899
+ "wipe-data",
22900
+ "read-credentials",
22901
+ "publish",
22902
+ "deploy",
22903
+ "shrink-record",
22904
+ "outside-own-copy"
22905
+ ];
22906
+ function riskyTypeOrder(type) {
22907
+ return RISKY_ACTION_TYPES.indexOf(type);
22477
22908
  }
22478
- async function evaluateEdit(reply, readSeat) {
22479
- let input;
22909
+ var CHECKED_TOOLS = ["Bash", "Read", "Grep", "Write", "Edit", "MultiEdit", "NotebookEdit", "Agent"];
22910
+ var RECORDED_ONLY_TOOLS = ["PowerShell"];
22911
+
22912
+ // dist/own-work.js
22913
+ var OWN_WORK_SHELL_TOOLS = ["Bash", "PowerShell"];
22914
+ var SHELL = new Set(OWN_WORK_SHELL_TOOLS);
22915
+ var MATCHED = /* @__PURE__ */ new Set([...CHECKED_TOOLS, ...RECORDED_ONLY_TOOLS]);
22916
+ function isOwnWorkShellTool(toolName) {
22917
+ return typeof toolName === "string" && SHELL.has(toolName) && MATCHED.has(toolName);
22918
+ }
22919
+ function ownWorkFileName(sessionId) {
22920
+ return `${OWN_WORK_PREFIX}${sessionId}${OWN_WORK_SUFFIX}`;
22921
+ }
22922
+ function ownWorkPath(sessionId, dir) {
22923
+ if (typeof sessionId !== "string" || !livenessIdOk(sessionId))
22924
+ return null;
22925
+ return join10(dir ?? markerDir(), ownWorkFileName(sessionId));
22926
+ }
22927
+ function inputString2(input, key) {
22928
+ if (typeof input !== "object" || input === null)
22929
+ return null;
22930
+ const value = input[key];
22931
+ return typeof value === "string" && value !== "" ? value : null;
22932
+ }
22933
+ function ownShellLine(input, at) {
22934
+ return { at: at.toISOString(), kind: "shell", agent: inputString2(input, "agent_id"), toolUse: inputString2(input, "tool_use_id") };
22935
+ }
22936
+ function ownEditLine(path, at) {
22937
+ return { at: at.toISOString(), kind: "edit", path };
22938
+ }
22939
+ function append(sessionId, line2, dir) {
22940
+ const path = ownWorkPath(sessionId, dir);
22941
+ if (path === null)
22942
+ return;
22943
+ appendFileSync3(path, `${JSON.stringify(line2)}
22944
+ `, { mode: 384 });
22945
+ }
22946
+ function recordOwnShell(input, at, deps = {}) {
22480
22947
  try {
22481
- const raw = await readStdin();
22482
- input = JSON.parse(raw);
22948
+ if (!isOwnWorkShellTool(inputString2(input, "tool_name")))
22949
+ return;
22950
+ append(inputString2(input, "session_id"), ownShellLine(input, at), deps.dir);
22483
22951
  } catch {
22484
- recordTimeEvent("edit", {});
22485
- return;
22486
22952
  }
22487
- recordTimeEvent("edit", input);
22488
- const toolName = input.tool_name ?? "";
22489
- if (!HANDLED_EDIT_TOOLS.has(toolName))
22490
- return;
22491
- const isNotebook = toolName === "NotebookEdit";
22492
- const absFilePath = isNotebook ? input.tool_input?.notebook_path : input.tool_input?.file_path;
22493
- if (!absFilePath || typeof absFilePath !== "string")
22494
- return;
22495
- const repoRootResult = getRepoRoot(process.cwd());
22496
- if (!repoRootResult.ok) {
22497
- if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
22498
- reply.notice(noRepositoryText(repoRootResult));
22953
+ }
22954
+ function recordOwnEdit(input, filePath, deps = {}) {
22955
+ try {
22956
+ if (filePath === "")
22957
+ return;
22958
+ const base = inputString2(input, "cwd") ?? process.cwd();
22959
+ const path = isAbsolute(filePath) ? filePath : resolve3(base, filePath);
22960
+ append(inputString2(input, "session_id"), ownEditLine(path, (deps.now ?? (() => /* @__PURE__ */ new Date()))()), deps.dir);
22961
+ } catch {
22962
+ }
22963
+ }
22964
+ function parseOwnWork(text) {
22965
+ const out = [];
22966
+ for (const raw of text.split("\n")) {
22967
+ if (raw === "")
22968
+ continue;
22969
+ try {
22970
+ const o = JSON.parse(raw);
22971
+ if (typeof o["at"] !== "string")
22972
+ continue;
22973
+ const nullable2 = (v) => v === null || typeof v === "string";
22974
+ if (o["kind"] === "edit" && typeof o["path"] === "string")
22975
+ out.push({ at: o["at"], kind: "edit", path: o["path"] });
22976
+ else if (o["kind"] === "shell" && nullable2(o["agent"]) && nullable2(o["toolUse"])) {
22977
+ out.push({ at: o["at"], kind: "shell", agent: o["agent"], toolUse: o["toolUse"] });
22978
+ }
22979
+ } catch {
22499
22980
  }
22500
- return;
22501
22981
  }
22502
- const repoRoot = repoRootResult.value;
22503
- const diffContent = isNotebook ? "" : getSingleFileDiff(absFilePath, repoRoot);
22504
- const { relative: pathRelative } = await import("node:path");
22505
- const relPath = pathRelative(repoRoot, absFilePath);
22506
- if (isInstallerOutput(relPath))
22507
- return;
22508
- const context = loadChangeSetContext(repoRoot);
22509
- const changeSet = buildChangeSet([{ path: relPath, diffContent }], {
22510
- ...context,
22511
- excerpts: readExcerptConfig()
22512
- });
22513
- const configResult = readConfig();
22514
- if (!configResult.config) {
22515
- await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
22516
- if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
22517
- return;
22518
- emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
22519
- return;
22982
+ return out;
22983
+ }
22984
+ function readOwnWork(sessionId, dir) {
22985
+ try {
22986
+ const path = ownWorkPath(sessionId, dir);
22987
+ if (path === null)
22988
+ return [];
22989
+ return parseOwnWork(readFileSync12(path, "utf-8"));
22990
+ } catch {
22991
+ return [];
22520
22992
  }
22521
- const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
22522
- const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
22523
- const evaluation = await evaluateWithRefresh({
22524
- guardServiceUrl,
22525
- token: guardServiceToken,
22526
- engagementId: guardEngagementId,
22527
- request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
22528
- warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
22529
- });
22530
- const clientResult = evaluation.result;
22531
- if (!clientResult.ok) {
22532
- const failure = classifyFailure(clientResult);
22533
- await emitRunRecord({
22534
- engagementId: guardEngagementId,
22535
- runType: "hook",
22536
- outcome: outcomeForClientFailure(clientResult.kind),
22537
- failureReason: failure.reason,
22538
- changeSet,
22539
- credential: evaluation.token,
22540
- credentialRefused: failure.kind === "credential"
22541
- });
22542
- if (failure.kind === "credential") {
22543
- if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
22544
- return;
22545
- emitCredentialRejected(
22546
- failure.statusCode,
22547
- failure.message,
22548
- CREDENTIAL_REJECTED_EXIT_TOOL_USE,
22549
- // The remedy the renewal attempt earned: the sign-in when the account
22550
- // credential is the thing that was refused, and otherwise the plane's own
22551
- // sentence about why it would not mint.
22552
- evaluation.remedy
22553
- );
22554
- return;
22993
+ }
22994
+ function shellWindows(own, timeLines, sessionId, passAt) {
22995
+ const out = [];
22996
+ for (const line2 of own) {
22997
+ if (line2.kind !== "shell")
22998
+ continue;
22999
+ const from = Date.parse(line2.at);
23000
+ if (Number.isNaN(from))
23001
+ continue;
23002
+ let to = passAt;
23003
+ for (const t of timeLines) {
23004
+ if (t.session !== sessionId || t.agent !== line2.agent)
23005
+ continue;
23006
+ const isOwnLine = line2.toolUse !== null ? t.toolUse === line2.toolUse : t.at === line2.at;
23007
+ if (isOwnLine)
23008
+ continue;
23009
+ const at = Date.parse(t.at);
23010
+ if (Number.isNaN(at) || at < from)
23011
+ continue;
23012
+ if (at < to)
23013
+ to = at;
22555
23014
  }
22556
- if (failure.kind === "contract") {
22557
- reply.notice(contractFaultText(failure.message));
22558
- return;
23015
+ out.push({ from, to: Math.max(from, to) });
23016
+ }
23017
+ return out;
23018
+ }
23019
+ function realPathOf(path) {
23020
+ const missing = [];
23021
+ let at = resolve3(path);
23022
+ for (; ; ) {
23023
+ try {
23024
+ return join10(realpathSync4.native(at), ...missing.reverse());
23025
+ } catch {
23026
+ const parent = dirname5(at);
23027
+ if (parent === at)
23028
+ return resolve3(path);
23029
+ missing.push(basename(at));
23030
+ at = parent;
22559
23031
  }
22560
- reply.notice(infraWarningText());
22561
- return;
22562
23032
  }
22563
- const telemetryConfig = readTelemetryConfig();
22564
- const run = buildGuardEvalRun({
22565
- engagementId: guardEngagementId,
22566
- runType: "hook",
23033
+ }
23034
+ function modifiedAt(path) {
23035
+ try {
23036
+ return lstatSync(path).mtimeMs;
23037
+ } catch {
23038
+ return null;
23039
+ }
23040
+ }
23041
+ function ownEditedFiles(repoRoot, sessionId, deps = {}) {
23042
+ const root = realPathOf(repoRoot);
23043
+ const out = [];
23044
+ const seen = /* @__PURE__ */ new Set();
23045
+ for (const line2 of readOwnWork(sessionId, deps.dir)) {
23046
+ if (line2.kind !== "edit")
23047
+ continue;
23048
+ const abs = realPathOf(line2.path);
23049
+ const rel = relative4(root, abs);
23050
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel))
23051
+ continue;
23052
+ if (modifiedAt(abs) === null)
23053
+ continue;
23054
+ const posix3 = rel.split(sep2).join("/");
23055
+ if (seen.has(posix3))
23056
+ continue;
23057
+ seen.add(posix3);
23058
+ out.push(posix3);
23059
+ }
23060
+ return out;
23061
+ }
23062
+ function ownChangedFiles(repoRoot, files, sessionId, deps = {}) {
23063
+ if (typeof sessionId !== "string" || ownWorkPath(sessionId, deps.dir) === null)
23064
+ return [...files];
23065
+ const own = readOwnWork(sessionId, deps.dir);
23066
+ if (own.length === 0)
23067
+ return [];
23068
+ const edited = /* @__PURE__ */ new Set();
23069
+ for (const line2 of own)
23070
+ if (line2.kind === "edit")
23071
+ edited.add(realPathOf(line2.path));
23072
+ const hasShell = own.some((line2) => line2.kind === "shell");
23073
+ const passAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
23074
+ const timeLines = hasShell ? readSessionTimeLines(sessionId, { env: deps.env, home: deps.home }) : [];
23075
+ const windows = hasShell ? shellWindows(own, timeLines, sessionId, passAt) : [];
23076
+ const inWindow = (mtimeMs) => {
23077
+ if (mtimeMs === null)
23078
+ return false;
23079
+ const ms = Math.floor(mtimeMs);
23080
+ return windows.some((w) => ms >= w.from && ms <= w.to);
23081
+ };
23082
+ const root = resolve3(repoRoot);
23083
+ return files.filter((rel) => {
23084
+ const abs = join10(root, rel);
23085
+ if (edited.has(realPathOf(abs)))
23086
+ return true;
23087
+ if (windows.length === 0)
23088
+ return false;
23089
+ const mtime = modifiedAt(abs);
23090
+ if (mtime !== null)
23091
+ return inWindow(mtime);
23092
+ return inWindow(modifiedAt(dirname5(abs)));
23093
+ });
23094
+ }
23095
+
23096
+ // dist/hooks/findings.js
23097
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
23098
+ import { closeSync, existsSync as existsSync6, openSync, readFileSync as readFileSync13, renameSync as renameSync4, statSync as statSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
23099
+ function findingKey(firing) {
23100
+ return createHash3("sha256").update(`${firing.guardId}
23101
+ ${firing.path ?? ""}
23102
+ ${firing.explanation}`).digest("hex");
23103
+ }
23104
+ function asFinding(raw) {
23105
+ if (typeof raw !== "object" || raw === null)
23106
+ return null;
23107
+ const o = raw;
23108
+ const str = (k) => typeof o[k] === "string";
23109
+ const bool = (k) => typeof o[k] === "boolean";
23110
+ if (!str("key") || !str("folder") || !str("guardId") || !str("patternRef") || !str("explanation") || !str("foundAt"))
23111
+ return null;
23112
+ if (o["severity"] !== "info" && o["severity"] !== "warn")
23113
+ return null;
23114
+ if (!bool("announced") || !bool("delivered") || !bool("escalated"))
23115
+ return null;
23116
+ if (o["path"] !== void 0 && typeof o["path"] !== "string")
23117
+ return null;
23118
+ return {
23119
+ key: o["key"],
23120
+ folder: o["folder"],
23121
+ guardId: o["guardId"],
23122
+ patternRef: o["patternRef"],
23123
+ severity: o["severity"],
23124
+ explanation: o["explanation"],
23125
+ ...typeof o["path"] === "string" ? { path: o["path"] } : {},
23126
+ foundAt: o["foundAt"],
23127
+ announced: o["announced"],
23128
+ delivered: o["delivered"],
23129
+ escalated: o["escalated"]
23130
+ };
23131
+ }
23132
+ function emptyFindings(session) {
23133
+ return { version: 1, session, findings: [] };
23134
+ }
23135
+ function readFindings(sessionId) {
23136
+ const path = findingsPath(sessionId);
23137
+ if (path === null)
23138
+ return null;
23139
+ const session = sessionId;
23140
+ try {
23141
+ const parsed = JSON.parse(readFileSync13(path, "utf-8"));
23142
+ if (parsed["version"] !== 1 || !Array.isArray(parsed["findings"]))
23143
+ return emptyFindings(session);
23144
+ const findings = [];
23145
+ for (const raw of parsed["findings"]) {
23146
+ const f = asFinding(raw);
23147
+ if (f !== null)
23148
+ findings.push(f);
23149
+ }
23150
+ return { version: 1, session, findings };
23151
+ } catch {
23152
+ return emptyFindings(session);
23153
+ }
23154
+ }
23155
+ function writeFindings(file2) {
23156
+ const path = findingsPath(file2.session);
23157
+ if (path === null)
23158
+ return;
23159
+ const temp = `${path.slice(0, -".json".length)}.${process.pid}.tmp.json`;
23160
+ try {
23161
+ writeFileSync6(temp, `${JSON.stringify(file2)}
23162
+ `, { mode: 384 });
23163
+ renameSync4(temp, path);
23164
+ } catch {
23165
+ try {
23166
+ unlinkSync5(temp);
23167
+ } catch {
23168
+ }
23169
+ }
23170
+ }
23171
+ var FINDINGS_LOCK_WAIT_MS = 1e3;
23172
+ var FINDINGS_LOCK_STALE_MS = 5e3;
23173
+ function lockPathFor(path) {
23174
+ return `${path.slice(0, -".json".length)}.lock.json`;
23175
+ }
23176
+ function pause(ms) {
23177
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23178
+ }
23179
+ function takeOverStaleLock(lock) {
23180
+ const aside = `${lock}.${process.pid}.${randomUUID2()}.stale`;
23181
+ try {
23182
+ renameSync4(lock, aside);
23183
+ } catch {
23184
+ return false;
23185
+ }
23186
+ let staleIndeed = false;
23187
+ try {
23188
+ staleIndeed = Date.now() - statSync3(aside).mtimeMs > FINDINGS_LOCK_STALE_MS;
23189
+ } catch {
23190
+ staleIndeed = false;
23191
+ }
23192
+ if (!staleIndeed) {
23193
+ try {
23194
+ renameSync4(aside, lock);
23195
+ } catch {
23196
+ try {
23197
+ unlinkSync5(aside);
23198
+ } catch {
23199
+ }
23200
+ }
23201
+ return false;
23202
+ }
23203
+ try {
23204
+ unlinkSync5(aside);
23205
+ } catch {
23206
+ }
23207
+ return true;
23208
+ }
23209
+ function withFindingsLock(sessionId, body, waitMs = FINDINGS_LOCK_WAIT_MS) {
23210
+ const path = findingsPath(sessionId);
23211
+ if (path === null)
23212
+ return void 0;
23213
+ const lock = lockPathFor(path);
23214
+ const deadline = Date.now() + waitMs;
23215
+ for (; ; ) {
23216
+ try {
23217
+ closeSync(openSync(lock, "wx", 384));
23218
+ break;
23219
+ } catch (err) {
23220
+ if (err.code !== "EEXIST")
23221
+ return void 0;
23222
+ let stale = false;
23223
+ try {
23224
+ stale = Date.now() - statSync3(lock).mtimeMs > FINDINGS_LOCK_STALE_MS;
23225
+ } catch {
23226
+ continue;
23227
+ }
23228
+ if (stale && takeOverStaleLock(lock))
23229
+ continue;
23230
+ if (Date.now() >= deadline)
23231
+ return void 0;
23232
+ pause(10);
23233
+ }
23234
+ }
23235
+ try {
23236
+ return body();
23237
+ } finally {
23238
+ try {
23239
+ unlinkSync5(lock);
23240
+ } catch {
23241
+ }
23242
+ }
23243
+ }
23244
+ function takeUndelivered(file2, folder) {
23245
+ const taken = [];
23246
+ const findings = file2.findings.map((f) => {
23247
+ if (f.delivered || folder !== null && f.folder !== folder)
23248
+ return f;
23249
+ taken.push(f);
23250
+ return { ...f, delivered: true };
23251
+ });
23252
+ return { file: { ...file2, findings }, taken };
23253
+ }
23254
+ function deliverFindings(sessionId, folder) {
23255
+ const path = findingsPath(sessionId);
23256
+ if (path === null || !existsSync6(path))
23257
+ return [];
23258
+ try {
23259
+ const taken = withFindingsLock(sessionId, () => {
23260
+ const before = readFindings(sessionId);
23261
+ if (before === null)
23262
+ return [];
23263
+ const { file: file2, taken: given } = takeUndelivered(before, folder);
23264
+ if (given.length > 0)
23265
+ writeFindings(file2);
23266
+ return given;
23267
+ });
23268
+ return taken ?? [];
23269
+ } catch {
23270
+ return [];
23271
+ }
23272
+ }
23273
+ function turnEndBookkeeping(file2, folder, outcome, topLevel, now) {
23274
+ const recorded = [...file2.findings];
23275
+ const firing = /* @__PURE__ */ new Map();
23276
+ const notRun = new Set(outcome.kind === "evaluated" ? outcome.notRun : []);
23277
+ const observed = (f) => f.folder === folder && !notRun.has(f.patternRef);
23278
+ if (outcome.kind === "evaluated") {
23279
+ for (const g of outcome.fired) {
23280
+ if (g.severity === "block")
23281
+ continue;
23282
+ const key = findingKey(g);
23283
+ if (!firing.has(key))
23284
+ firing.set(key, g);
23285
+ }
23286
+ }
23287
+ const removed = [];
23288
+ const kept = [];
23289
+ for (const f of recorded) {
23290
+ const closes = observed(f) && (outcome.kind === "no-own-changes" || outcome.kind === "evaluated" && !firing.has(f.key));
23291
+ (closes ? removed : kept).push(f);
23292
+ }
23293
+ const known = new Set(kept.filter((f) => f.folder === folder).map((f) => f.key));
23294
+ for (const [key, g] of firing) {
23295
+ if (known.has(key))
23296
+ continue;
23297
+ kept.push({
23298
+ key,
23299
+ folder,
23300
+ guardId: g.guardId,
23301
+ patternRef: g.patternRef,
23302
+ severity: g.severity,
23303
+ explanation: g.explanation,
23304
+ ...g.path !== void 0 ? { path: g.path } : {},
23305
+ foundAt: now.toISOString(),
23306
+ announced: false,
23307
+ delivered: false,
23308
+ escalated: false
23309
+ });
23310
+ }
23311
+ if (!topLevel) {
23312
+ return { file: { ...file2, findings: kept }, news: { fixed: 0, allFixed: false, found: 0, stillOpen: [] } };
23313
+ }
23314
+ const fixed = removed.filter((f) => f.announced).length;
23315
+ const allFixed = !kept.some((f) => f.announced);
23316
+ let found2 = 0;
23317
+ const stillOpen = [];
23318
+ const next = kept.map((f) => {
23319
+ if (!f.announced) {
23320
+ found2 += 1;
23321
+ return { ...f, announced: true };
23322
+ }
23323
+ if (f.delivered && !f.escalated && observed(f)) {
23324
+ stillOpen.push(f.explanation);
23325
+ return { ...f, escalated: true };
23326
+ }
23327
+ return f;
23328
+ });
23329
+ return { file: { ...file2, findings: next }, news: { fixed, allFixed, found: found2, stillOpen } };
23330
+ }
23331
+ function blockingExplanations(fired) {
23332
+ const blocks = fired.filter((g) => g.severity === "block");
23333
+ return (blocks.length > 0 ? blocks : fired).map((g) => g.explanation);
23334
+ }
23335
+
23336
+ // dist/hooks/finding-lines.js
23337
+ function bullets(explanations) {
23338
+ return explanations.map((e) => `- ${e}`).join("\n");
23339
+ }
23340
+ function distinctBullets(explanations) {
23341
+ return bullets([...new Set(explanations)]);
23342
+ }
23343
+ function foundLine(count) {
23344
+ return count === 1 ? "Halfcycle found 1 small thing to fix in this turn's work. The agent gets the details with your next message and will fix it; there is nothing for you to do." : `Halfcycle found ${count} small things to fix in this turn's work. The agent gets the details with your next message and will fix them; there is nothing for you to do.`;
23345
+ }
23346
+ function fixedLine(count, all) {
23347
+ if (all)
23348
+ return "Halfcycle: everything it found earlier is now fixed.";
23349
+ return count === 1 ? "Halfcycle: 1 of the things it found earlier is now fixed." : `Halfcycle: ${count} of the things it found earlier are now fixed.`;
23350
+ }
23351
+ function stillOpenMessage(explanations) {
23352
+ const n = explanations.length;
23353
+ const head = n === 1 ? "Halfcycle: 1 thing it found is still not fixed after the agent's last turn." : `Halfcycle: ${n} things it found are still not fixed after the agent's last turn.`;
23354
+ const tail = n === 1 ? "Ask the agent to fix it, or tell it to leave it as it is if that is what you want." : "Ask the agent to fix them, or tell it to leave them as they are if that is what you want.";
23355
+ return `${head}
23356
+ ${bullets(explanations)}
23357
+ ${tail}`;
23358
+ }
23359
+ function stoppedMessage(explanations) {
23360
+ return [
23361
+ "Halfcycle stopped this step because something in the agent's work has to be fixed first:",
23362
+ distinctBullets(explanations),
23363
+ "The agent has the details and is fixing it now. If it is still stopped after the agent's next try, ask the agent what is in the way."
23364
+ ].join("\n");
23365
+ }
23366
+ function couldNotFixMessage(explanations) {
23367
+ return [
23368
+ "Halfcycle: the agent could not fix what stopped its work:",
23369
+ distinctBullets(explanations),
23370
+ "Ask the agent what is in the way, or tell it how you want this handled."
23371
+ ].join("\n");
23372
+ }
23373
+ function blockReason(explanations) {
23374
+ return `Halfcycle stopped this step. Fix each of these before you carry on:
23375
+ ${distinctBullets(explanations)}`;
23376
+ }
23377
+ function detailsBlock(findings, projectFolder2) {
23378
+ const lines2 = findings.map((f) => {
23379
+ const where = f.folder === projectFolder2 ? f.path : f.path === void 0 ? f.folder : `${f.folder}/${f.path}`;
23380
+ const at = where === void 0 ? "" : ` in ${where}`;
23381
+ return ` [${f.severity.toUpperCase()}] ${f.patternRef}${at}: ${f.explanation}`;
23382
+ });
23383
+ return [
23384
+ "Halfcycle's checks found these in this session's own changes at the end of an earlier turn. Fix each one as part of this turn. If one is intended, tell the person in one plain sentence. When you mention them to the person, use plain words, not the labels in brackets.",
23385
+ ...lines2
23386
+ ].join("\n");
23387
+ }
23388
+ function turnEndMessage(news) {
23389
+ const parts = [];
23390
+ if (news.fixed > 0)
23391
+ parts.push(fixedLine(news.fixed, news.allFixed));
23392
+ if (news.found > 0)
23393
+ parts.push(foundLine(news.found));
23394
+ if (news.stillOpen.length > 0)
23395
+ parts.push(stillOpenMessage(news.stillOpen));
23396
+ return parts.length === 0 ? null : parts.join("\n");
23397
+ }
23398
+
23399
+ // dist/hooks/post-tool-use.js
23400
+ var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
23401
+ async function runPostToolUse(readSeat = readSeatConfiguration) {
23402
+ const reply = hookReply("PostToolUse");
23403
+ const at = { session: void 0, folder: null };
23404
+ await evaluateEdit(reply, readSeat, at);
23405
+ handOverAtEdit(reply, at);
23406
+ reply.flush();
23407
+ }
23408
+ function handOverAtEdit(reply, at) {
23409
+ if (at.folder === null)
23410
+ return;
23411
+ const code = process.exitCode;
23412
+ if (code !== void 0 && code !== null && Number(code) !== 0)
23413
+ return;
23414
+ const given = deliverFindings(at.session, at.folder);
23415
+ if (given.length === 0)
23416
+ return;
23417
+ reply.agentContext(detailsBlock(given, realPathOf(projectFolder())));
23418
+ }
23419
+ async function evaluateEdit(reply, readSeat, at) {
23420
+ let input;
23421
+ try {
23422
+ const raw = await readStdin();
23423
+ input = JSON.parse(raw);
23424
+ } catch {
23425
+ recordTimeEvent("edit", {});
23426
+ return;
23427
+ }
23428
+ recordTimeEvent("edit", input);
23429
+ const toolName = input.tool_name ?? "";
23430
+ if (!HANDLED_EDIT_TOOLS.has(toolName))
23431
+ return;
23432
+ const isNotebook = toolName === "NotebookEdit";
23433
+ const absFilePath = isNotebook ? input.tool_input?.notebook_path : input.tool_input?.file_path;
23434
+ if (!absFilePath || typeof absFilePath !== "string")
23435
+ return;
23436
+ recordOwnEdit(input, absFilePath);
23437
+ const repoRootResult = getRepoRoot(process.cwd());
23438
+ if (!repoRootResult.ok) {
23439
+ if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
23440
+ reply.notice(noRepositoryText(repoRootResult));
23441
+ }
23442
+ return;
23443
+ }
23444
+ const repoRoot = repoRootResult.value;
23445
+ at.session = input.session_id;
23446
+ at.folder = realPathOf(repoRoot);
23447
+ const diffContent = isNotebook ? "" : getSingleFileDiff(absFilePath, repoRoot);
23448
+ const { relative: pathRelative } = await import("node:path");
23449
+ const relPath = pathRelative(repoRoot, absFilePath);
23450
+ if (isInstallerOutput(relPath))
23451
+ return;
23452
+ const context = loadChangeSetContext(repoRoot);
23453
+ const changeSet = buildChangeSet([{ path: relPath, diffContent }], {
23454
+ ...context,
23455
+ excerpts: readExcerptConfig()
23456
+ });
23457
+ const configResult = readConfig();
23458
+ if (!configResult.config) {
23459
+ await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
23460
+ if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
23461
+ return;
23462
+ emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
23463
+ return;
23464
+ }
23465
+ const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
23466
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
23467
+ const evaluation = await evaluateWithRefresh({
23468
+ guardServiceUrl,
23469
+ token: guardServiceToken,
23470
+ engagementId: guardEngagementId,
23471
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
23472
+ warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
23473
+ });
23474
+ const clientResult = evaluation.result;
23475
+ if (!clientResult.ok) {
23476
+ const failure = classifyFailure(clientResult);
23477
+ await emitRunRecord({
23478
+ engagementId: guardEngagementId,
23479
+ runType: "hook",
23480
+ outcome: outcomeForClientFailure(clientResult.kind),
23481
+ failureReason: failure.reason,
23482
+ changeSet,
23483
+ credential: evaluation.token,
23484
+ credentialRefused: failure.kind === "credential"
23485
+ });
23486
+ if (failure.kind === "credential") {
23487
+ if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
23488
+ return;
23489
+ emitCredentialRejected(
23490
+ failure.statusCode,
23491
+ failure.message,
23492
+ CREDENTIAL_REJECTED_EXIT_TOOL_USE,
23493
+ // The remedy the renewal attempt earned: the sign-in when the account
23494
+ // credential is the thing that was refused, and otherwise the plane's own
23495
+ // sentence about why it would not mint.
23496
+ evaluation.remedy
23497
+ );
23498
+ return;
23499
+ }
23500
+ if (failure.kind === "contract") {
23501
+ reply.notice(contractFaultText(failure.message));
23502
+ return;
23503
+ }
23504
+ reply.notice(infraWarningText());
23505
+ return;
23506
+ }
23507
+ const telemetryConfig = readTelemetryConfig();
23508
+ const run = buildGuardEvalRun({
23509
+ engagementId: guardEngagementId,
23510
+ runType: "hook",
22567
23511
  // The phase this run belongs to, as the client named it — it names the
22568
23512
  // guard-eval log file the run is appended to. Absent → the run is unphased.
22569
23513
  phase: telemetryConfig.phase,
@@ -22597,10 +23541,8 @@ ${notRun}` : "";
22597
23541
  return;
22598
23542
  }
22599
23543
  if (envelope.blocking) {
22600
- const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
22601
- const reason = `Guard blocked: ${envelope.explanation}
22602
- ${guardLines}${notRunBlock}`;
22603
- reply.block(reason);
23544
+ const reason = `${blockReason(envelope.guardsFired.map((g) => g.explanation))}${notRunBlock}`;
23545
+ reply.block(reason, stoppedMessage(blockingExplanations(envelope.guardsFired)));
22604
23546
  } else {
22605
23547
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
22606
23548
  reply.notice(`[Halfcycle] Guard warning: ${envelope.explanation}
@@ -22608,17 +23550,17 @@ ${guardLines}${notRunBlock}`);
22608
23550
  }
22609
23551
  }
22610
23552
  function readStdin() {
22611
- return new Promise((resolve6, reject) => {
23553
+ return new Promise((resolve8, reject) => {
22612
23554
  const chunks = [];
22613
23555
  process.stdin.on("data", (chunk) => chunks.push(chunk));
22614
- process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8")));
23556
+ process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
22615
23557
  process.stdin.on("error", reject);
22616
23558
  });
22617
23559
  }
22618
23560
 
22619
23561
  // dist/liveness.js
22620
- import { closeSync, mkdtempSync, openSync, readdirSync as readdirSync5, readFileSync as readFileSync12, renameSync as renameSync4, rmSync, statSync as statSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
22621
- import { join as join10 } from "node:path";
23562
+ import { closeSync as closeSync2, mkdtempSync, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync5, rmSync, statSync as statSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
23563
+ import { join as join11 } from "node:path";
22622
23564
  var LIVE_WINDOW_MS = 10 * 60 * 1e3;
22623
23565
  function field(input, key) {
22624
23566
  if (typeof input !== "object" || input === null)
@@ -22627,10 +23569,10 @@ function field(input, key) {
22627
23569
  return typeof v === "string" && v !== "" ? v : null;
22628
23570
  }
22629
23571
  function indexEntry(dir, sessionId, name) {
22630
- const folder = join10(dir, livenessIndexDirName(sessionId));
23572
+ const folder = join11(dir, livenessIndexDirName(sessionId));
22631
23573
  for (let attempt2 = 0; attempt2 < 3; attempt2++) {
22632
23574
  try {
22633
- closeSync(openSync(join10(folder, name), "a", 384));
23575
+ closeSync2(openSync2(join11(folder, name), "a", 384));
22634
23576
  return true;
22635
23577
  } catch (err) {
22636
23578
  if (err.code !== "ENOENT")
@@ -22641,10 +23583,10 @@ function indexEntry(dir, sessionId, name) {
22641
23583
  tmp = mkdtempSync(`${folder}.`);
22642
23584
  for (const existing of readdirSync5(dir)) {
22643
23585
  if (parseLivenessFileName(existing)?.sessionId === sessionId)
22644
- closeSync(openSync(join10(tmp, existing), "a", 384));
23586
+ closeSync2(openSync2(join11(tmp, existing), "a", 384));
22645
23587
  }
22646
- closeSync(openSync(join10(tmp, name), "a", 384));
22647
- renameSync4(tmp, folder);
23588
+ closeSync2(openSync2(join11(tmp, name), "a", 384));
23589
+ renameSync5(tmp, folder);
22648
23590
  return true;
22649
23591
  } catch {
22650
23592
  if (tmp !== null)
@@ -22669,18 +23611,18 @@ function recordLiveness(input, deps = {}) {
22669
23611
  const name = livenessFileName(livenessKey(copy?.common ?? null), sessionId, agentId);
22670
23612
  if (!indexEntry(dir, sessionId, name))
22671
23613
  return null;
22672
- const path = join10(dir, name);
23614
+ const path = join11(dir, name);
22673
23615
  const text = `${cwd}
22674
23616
  ${copy?.top ?? ""}
22675
23617
  `;
22676
23618
  try {
22677
- writeFileSync6(path, text, { encoding: "utf-8", flag: "wx", mode: 384 });
23619
+ writeFileSync7(path, text, { encoding: "utf-8", flag: "wx", mode: 384 });
22678
23620
  return { firstSeen: true };
22679
23621
  } catch (err) {
22680
23622
  if (err.code !== "EEXIST")
22681
23623
  return null;
22682
23624
  }
22683
- writeFileSync6(path, text, { encoding: "utf-8", mode: 384 });
23625
+ writeFileSync7(path, text, { encoding: "utf-8", mode: 384 });
22684
23626
  return { firstSeen: false };
22685
23627
  } catch {
22686
23628
  return null;
@@ -22699,7 +23641,7 @@ function clearLiveness(input, deps = {}) {
22699
23641
  if (p === null || p.sessionId !== sessionId || p.agentId !== agentId)
22700
23642
  continue;
22701
23643
  try {
22702
- unlinkSync5(join10(dir, name));
23644
+ unlinkSync6(join11(dir, name));
22703
23645
  removed += 1;
22704
23646
  } catch {
22705
23647
  }
@@ -22712,7 +23654,7 @@ function clearLiveness(input, deps = {}) {
22712
23654
  }
22713
23655
  function topOf(path) {
22714
23656
  try {
22715
- const top = readFileSync12(path, "utf-8").split("\n")[1] ?? "";
23657
+ const top = readFileSync14(path, "utf-8").split("\n")[1] ?? "";
22716
23658
  return top === "" ? null : top;
22717
23659
  } catch {
22718
23660
  return null;
@@ -22734,10 +23676,10 @@ function readLiveness(sessionId, cwd, deps = {}) {
22734
23676
  const p = parseLivenessFileName(name);
22735
23677
  if (p === null || p.key !== key || p.sessionId !== sessionId)
22736
23678
  continue;
22737
- const full = join10(dir, name);
23679
+ const full = join11(dir, name);
22738
23680
  let seenAt;
22739
23681
  try {
22740
- seenAt = statSync3(full).mtimeMs;
23682
+ seenAt = statSync4(full).mtimeMs;
22741
23683
  } catch {
22742
23684
  continue;
22743
23685
  }
@@ -22775,17 +23717,17 @@ function runsBesideOtherWork(toolInput, record2) {
22775
23717
  }
22776
23718
 
22777
23719
  // dist/pace-hook.js
22778
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, renameSync as renameSync8, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "node:fs";
23720
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync18, renameSync as renameSync9, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
22779
23721
  import nodePath4 from "node:path";
22780
23722
 
22781
23723
  // dist/pace-verb.js
22782
- import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync15, renameSync as renameSync7, statSync as statSync5, writeFileSync as writeFileSync9 } from "node:fs";
23724
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync8, statSync as statSync6, writeFileSync as writeFileSync10 } from "node:fs";
22783
23725
  import { homedir as homedir6 } from "node:os";
22784
23726
  import nodePath3 from "node:path";
22785
23727
 
22786
23728
  // dist/pacing.js
22787
- import { createHash as createHash3 } from "node:crypto";
22788
- import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync6, readFileSync as readFileSync13, readSync, realpathSync as realpathSync4, renameSync as renameSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "node:fs";
23729
+ import { createHash as createHash4 } from "node:crypto";
23730
+ import { closeSync as closeSync3, mkdirSync as mkdirSync4, openSync as openSync3, readdirSync as readdirSync6, readFileSync as readFileSync15, readSync, realpathSync as realpathSync5, renameSync as renameSync6, statSync as statSync5, writeFileSync as writeFileSync8 } from "node:fs";
22789
23731
  import { homedir as homedir5 } from "node:os";
22790
23732
  import nodePath from "node:path";
22791
23733
  var DEFAULT_CONFIG_FOLDER_NAME = ".claude";
@@ -22834,7 +23776,7 @@ function pacingDir(home) {
22834
23776
  }
22835
23777
  function readJsonObject(path) {
22836
23778
  try {
22837
- const v = JSON.parse(readFileSync13(path, "utf-8"));
23779
+ const v = JSON.parse(readFileSync15(path, "utf-8"));
22838
23780
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
22839
23781
  } catch {
22840
23782
  return null;
@@ -22842,9 +23784,9 @@ function readJsonObject(path) {
22842
23784
  }
22843
23785
  function writeJsonWhole(path, value) {
22844
23786
  mkdirSync4(nodePath.dirname(path), { recursive: true, mode: 448 });
22845
- const tmp = `${path}.${String(process.pid)}.${createHash3("sha1").update(String(Math.random())).digest("hex").slice(0, 8)}.tmp`;
22846
- writeFileSync7(tmp, JSON.stringify(value), { mode: 384 });
22847
- renameSync5(tmp, path);
23787
+ const tmp = `${path}.${String(process.pid)}.${createHash4("sha1").update(String(Math.random())).digest("hex").slice(0, 8)}.tmp`;
23788
+ writeFileSync8(tmp, JSON.stringify(value), { mode: 384 });
23789
+ renameSync6(tmp, path);
22848
23790
  }
22849
23791
  function writeAccountEntry(path, key, entry) {
22850
23792
  const current = readJsonObject(path);
@@ -22864,6 +23806,62 @@ function accountKeysOf(path) {
22864
23806
  const accounts = current?.["accounts"];
22865
23807
  return typeof accounts === "object" && accounts !== null && !Array.isArray(accounts) ? Object.keys(accounts) : [];
22866
23808
  }
23809
+ var PACE_STATE_FILENAME = "state.json";
23810
+ function paceStateEntryOf(v) {
23811
+ if (typeof v !== "object" || v === null || Array.isArray(v))
23812
+ return null;
23813
+ const o = v;
23814
+ const pace = paceStateSchema.safeParse(o["pace"]);
23815
+ const kind = o["windowKind"] === null ? null : paceWindowKindSchema.safeParse(o["windowKind"]);
23816
+ const allowance = o["allowance"];
23817
+ const number4 = o["number"];
23818
+ const until = o["until"];
23819
+ const at = o["at"];
23820
+ if (!pace.success || kind !== null && !kind.success)
23821
+ return null;
23822
+ if (typeof allowance !== "number" || !Number.isInteger(allowance) || allowance < 0)
23823
+ return null;
23824
+ if (typeof number4 !== "number" || !Number.isInteger(number4) || number4 < 1 || number4 > PACE_AGENTS_MAX)
23825
+ return null;
23826
+ if (until !== null && (typeof until !== "string" || Number.isNaN(Date.parse(until))))
23827
+ return null;
23828
+ if (typeof at !== "string" || Number.isNaN(Date.parse(at)))
23829
+ return null;
23830
+ return { pace: pace.data, allowance, number: number4, windowKind: kind === null ? null : kind.data, until, at };
23831
+ }
23832
+ function rememberedBasisOf(v) {
23833
+ if (typeof v !== "object" || v === null || Array.isArray(v))
23834
+ return null;
23835
+ const o = v;
23836
+ const phase = o["phase"] === null ? null : phaseNameSchema.safeParse(o["phase"]);
23837
+ const plan = o["claudePlan"] === null ? null : claudePlanSchema.safeParse(o["claudePlan"]);
23838
+ const keep = keepFreePercentSchema.safeParse(o["keepFreePercent"]);
23839
+ if (phase !== null && !phase.success || plan !== null && !plan.success || !keep.success)
23840
+ return null;
23841
+ return { phase: phase === null ? null : phase.data, claudePlan: plan === null ? null : plan.data, keepFreePercent: keep.data };
23842
+ }
23843
+ function readPaceStateFile(dir) {
23844
+ const o = readJsonObject(nodePath.join(dir, PACE_STATE_FILENAME));
23845
+ const accounts = {};
23846
+ const rawAccounts = o?.["accounts"];
23847
+ if (typeof rawAccounts === "object" && rawAccounts !== null && !Array.isArray(rawAccounts)) {
23848
+ for (const [key, v] of Object.entries(rawAccounts)) {
23849
+ const e = paceStateEntryOf(v);
23850
+ if (e !== null)
23851
+ accounts[key] = e;
23852
+ }
23853
+ }
23854
+ return { accounts, basis: rememberedBasisOf(o?.["basis"]) };
23855
+ }
23856
+ function writePaceState(dir, key, entry, basis) {
23857
+ const current = readPaceStateFile(dir);
23858
+ const accounts = { ...current.accounts };
23859
+ if (entry === null)
23860
+ delete accounts[key];
23861
+ else
23862
+ accounts[key] = entry;
23863
+ writeJsonWhole(nodePath.join(dir, PACE_STATE_FILENAME), { accounts, basis: basis ?? current.basis });
23864
+ }
22867
23865
  function recordFactsOf(o) {
22868
23866
  if (o["type"] !== "assistant")
22869
23867
  return null;
@@ -22979,7 +23977,7 @@ var NEWLINE = 10;
22979
23977
  var ASSISTANT = Buffer.from('"assistant"');
22980
23978
  var READ_CHUNK = 8 * 1024 * 1024;
22981
23979
  function readWholeLines(path, from, size, onLine) {
22982
- const fd = openSync2(path, "r");
23980
+ const fd = openSync3(path, "r");
22983
23981
  try {
22984
23982
  let pos = from;
22985
23983
  let carry = Buffer.alloc(0);
@@ -23007,11 +24005,11 @@ function readWholeLines(path, from, size, onLine) {
23007
24005
  }
23008
24006
  return consumed;
23009
24007
  } finally {
23010
- closeSync2(fd);
24008
+ closeSync3(fd);
23011
24009
  }
23012
24010
  }
23013
24011
  function hashId(id) {
23014
- return createHash3("sha1").update(id).digest("base64url").slice(0, 16);
24012
+ return createHash4("sha1").update(id).digest("base64url").slice(0, 16);
23015
24013
  }
23016
24014
  function scanRecords(key, deps = {}) {
23017
24015
  const nowMs = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
@@ -23036,7 +24034,7 @@ function scanRecords(key, deps = {}) {
23036
24034
  let size;
23037
24035
  let mtimeSec;
23038
24036
  try {
23039
- const st = statSync4(file2.path);
24037
+ const st = statSync5(file2.path);
23040
24038
  size = st.size;
23041
24039
  mtimeSec = Math.floor(st.mtimeMs / 1e3);
23042
24040
  } catch {
@@ -23159,7 +24157,7 @@ function tokensBetween(minutes, fromSec, toSec) {
23159
24157
  }
23160
24158
  function resolvedRecords(key, path) {
23161
24159
  try {
23162
- return realpathSync4(recordsFolderOf(key, path));
24160
+ return realpathSync5(recordsFolderOf(key, path));
23163
24161
  } catch {
23164
24162
  return null;
23165
24163
  }
@@ -23172,7 +24170,7 @@ function timeLogTranscripts(home) {
23172
24170
  continue;
23173
24171
  let text;
23174
24172
  try {
23175
- text = readFileSync13(nodePath.join(engagements, e.name, TIME_LOG_DIR_NAME, TIME_LOG_FILENAME), "utf-8");
24173
+ text = readFileSync15(nodePath.join(engagements, e.name, TIME_LOG_DIR_NAME, TIME_LOG_FILENAME), "utf-8");
23176
24174
  } catch {
23177
24175
  continue;
23178
24176
  }
@@ -23332,7 +24330,7 @@ function knownWindowOf(learned, reported, now) {
23332
24330
 
23333
24331
  // dist/scan-lock.js
23334
24332
  import { spawn } from "node:child_process";
23335
- import { linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync14, renameSync as renameSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
24333
+ import { linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync16, renameSync as renameSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "node:fs";
23336
24334
  import nodePath2 from "node:path";
23337
24335
  var SCAN_LOCK_FILENAME = "scan.lock";
23338
24336
  var SCAN_LOCK_STALE_MS = 15 * 60 * 1e3;
@@ -23343,7 +24341,7 @@ function scanLockPath(home) {
23343
24341
  function readLockBody(path) {
23344
24342
  let text;
23345
24343
  try {
23346
- text = readFileSync14(path, "utf-8");
24344
+ text = readFileSync16(path, "utf-8");
23347
24345
  } catch (err) {
23348
24346
  return err.code === "ENOENT" ? "missing" : null;
23349
24347
  }
@@ -23373,7 +24371,7 @@ function isStale(body, staleMs, deps) {
23373
24371
  return !(deps.alive ?? processAlive)(body.pid);
23374
24372
  }
23375
24373
  function writeLock(path, pid, deps, flag) {
23376
- writeFileSync8(path, JSON.stringify({ pid, at: (deps.clock ?? Date.now)() }), { flag, mode: 384 });
24374
+ writeFileSync9(path, JSON.stringify({ pid, at: (deps.clock ?? Date.now)() }), { flag, mode: 384 });
23377
24375
  }
23378
24376
  function tryTakeScanLock(deps = {}) {
23379
24377
  return tryTakeLockFile(scanLockPath(deps.home), SCAN_LOCK_STALE_MS, deps);
@@ -23400,7 +24398,7 @@ function tryTakeLockFile(path, staleMs, deps = {}) {
23400
24398
  return false;
23401
24399
  const aside = `${path}.${String(pid)}.${String((deps.clock ?? Date.now)())}.stale`;
23402
24400
  try {
23403
- renameSync6(path, aside);
24401
+ renameSync7(path, aside);
23404
24402
  } catch {
23405
24403
  continue;
23406
24404
  }
@@ -23412,13 +24410,13 @@ function tryTakeLockFile(path, staleMs, deps = {}) {
23412
24410
  } catch {
23413
24411
  }
23414
24412
  try {
23415
- unlinkSync6(aside);
24413
+ unlinkSync7(aside);
23416
24414
  } catch {
23417
24415
  }
23418
24416
  return false;
23419
24417
  }
23420
24418
  try {
23421
- unlinkSync6(aside);
24419
+ unlinkSync7(aside);
23422
24420
  } catch {
23423
24421
  }
23424
24422
  }
@@ -23429,7 +24427,7 @@ function releaseLockFile(path, pid = process.pid) {
23429
24427
  if (body === "missing" || body === null || body.pid !== pid)
23430
24428
  return;
23431
24429
  try {
23432
- unlinkSync6(path);
24430
+ unlinkSync7(path);
23433
24431
  } catch {
23434
24432
  }
23435
24433
  }
@@ -23620,7 +24618,7 @@ function readBuildNumber(engagementId, home) {
23620
24618
  if (dir === null)
23621
24619
  return null;
23622
24620
  try {
23623
- const v = JSON.parse(readFileSync15(nodePath3.join(dir, BUILD_NUMBER_FILENAME), "utf-8"));
24621
+ const v = JSON.parse(readFileSync17(nodePath3.join(dir, BUILD_NUMBER_FILENAME), "utf-8"));
23624
24622
  const n = v["number"];
23625
24623
  if (typeof v["phase"] === "string" && typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= PACE_AGENTS_MAX && typeof v["namedByOwner"] === "boolean") {
23626
24624
  return { phase: v["phase"], number: n, namedByOwner: v["namedByOwner"] };
@@ -23640,12 +24638,12 @@ function writeBuildNumber(engagementId, value, home) {
23640
24638
  mkdirSync6(dir, { recursive: true, mode: 448 });
23641
24639
  const path = nodePath3.join(dir, BUILD_NUMBER_FILENAME);
23642
24640
  const tmp = `${path}.${String(process.pid)}.tmp`;
23643
- writeFileSync9(tmp, JSON.stringify(value), { mode: 384 });
23644
- renameSync7(tmp, path);
24641
+ writeFileSync10(tmp, JSON.stringify(value), { mode: 384 });
24642
+ renameSync8(tmp, path);
23645
24643
  }
23646
24644
  function readRememberedClaudePlan(home) {
23647
24645
  try {
23648
- const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync15(nodePath3.join(home ?? homedir6(), HALFCYCLE_DIR_NAME, REMEMBERED_CLAUDE_PLAN_FILENAME), "utf-8")));
24646
+ const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync17(nodePath3.join(home ?? homedir6(), HALFCYCLE_DIR_NAME, REMEMBERED_CLAUDE_PLAN_FILENAME), "utf-8")));
23649
24647
  return parsed.success ? parsed.data.claudePlan : null;
23650
24648
  } catch {
23651
24649
  return null;
@@ -23665,7 +24663,7 @@ function liveSubAgentsIn(cwd, deps = {}) {
23665
24663
  if (p === null || p.key !== key || p.agentId === null)
23666
24664
  continue;
23667
24665
  try {
23668
- if (now - statSync5(nodePath3.join(dir, name)).mtimeMs <= LIVE_WINDOW_MS)
24666
+ if (now - statSync6(nodePath3.join(dir, name)).mtimeMs <= LIVE_WINDOW_MS)
23669
24667
  n += 1;
23670
24668
  } catch {
23671
24669
  }
@@ -23824,6 +24822,23 @@ async function runPaceVerb(args2, deps = {}) {
23824
24822
  number4 = stored?.number ?? Math.min(claudePlanOf(claudePlan).startingAgents, PACE_AGENTS_MAX);
23825
24823
  }
23826
24824
  const decision = paceOf({ number: number4, keepFreePercent, learned, reported, now });
24825
+ if (basis !== null) {
24826
+ const dir = enginePacingDir(engagementId, deps.home);
24827
+ if (dir !== null) {
24828
+ const entry = {
24829
+ pace: decision.pace,
24830
+ allowance: decision.allowance,
24831
+ number: number4,
24832
+ windowKind: decision.windowKind,
24833
+ until: decision.until === null ? null : decision.until.toISOString(),
24834
+ at: now.toISOString()
24835
+ };
24836
+ try {
24837
+ writePaceState(dir, key, entry, { phase: basis.phase, claudePlan: basis.claudePlan, keepFreePercent });
24838
+ } catch {
24839
+ }
24840
+ }
24841
+ }
23827
24842
  const live = liveSubAgentsIn(deps.cwd ?? process.cwd(), deps.live);
23828
24843
  const result = {
23829
24844
  outcome: "paced",
@@ -23844,11 +24859,10 @@ async function runPaceVerb(args2, deps = {}) {
23844
24859
  var PACE_HELD_TOOL = "Agent";
23845
24860
  var PACE_STEP_MIN_INTERVAL_MS = OBSERVE_MIN_INTERVAL_MS;
23846
24861
  var PACE_STATE_FRESH_MS = 10 * 60 * 1e3;
23847
- var PACE_PENDING_START_MS = 60 * 1e3;
24862
+ var PACE_PENDING_START_MS = 15 * 1e3;
23848
24863
  var PACE_STARTS_LOCK_STALE_MS = 5 * 1e3;
23849
24864
  var PACE_STARTS_LOCK_GIVE_UP_MS = 50;
23850
24865
  var STARTS_LOCK_POLL_MS = 2;
23851
- var PACE_STATE_FILENAME = "state.json";
23852
24866
  var PACE_STARTS_FILENAME = "starts.json";
23853
24867
  var PACE_STARTS_LOCK_FILENAME = "starts.lock";
23854
24868
  function objectOf(v) {
@@ -23856,7 +24870,7 @@ function objectOf(v) {
23856
24870
  }
23857
24871
  function readJson(path) {
23858
24872
  try {
23859
- return objectOf(JSON.parse(readFileSync16(path, "utf-8")));
24873
+ return objectOf(JSON.parse(readFileSync18(path, "utf-8")));
23860
24874
  } catch {
23861
24875
  return null;
23862
24876
  }
@@ -23865,68 +24879,16 @@ function writeJsonWhole2(path, value) {
23865
24879
  mkdirSync7(nodePath4.dirname(path), { recursive: true, mode: 448 });
23866
24880
  const tmp = `${path}.${String(process.pid)}.${Math.random().toString(36).slice(2, 10)}.tmp`;
23867
24881
  try {
23868
- writeFileSync10(tmp, JSON.stringify(value), { mode: 384 });
23869
- renameSync8(tmp, path);
24882
+ writeFileSync11(tmp, JSON.stringify(value), { mode: 384 });
24883
+ renameSync9(tmp, path);
23870
24884
  } catch (err) {
23871
24885
  try {
23872
- unlinkSync7(tmp);
24886
+ unlinkSync8(tmp);
23873
24887
  } catch {
23874
24888
  }
23875
24889
  throw err;
23876
24890
  }
23877
24891
  }
23878
- function entryOf(v) {
23879
- const o = objectOf(v);
23880
- if (o === null)
23881
- return null;
23882
- const pace = paceStateSchema.safeParse(o["pace"]);
23883
- const kind = o["windowKind"] === null ? null : paceWindowKindSchema.safeParse(o["windowKind"]);
23884
- const allowance = o["allowance"];
23885
- const number4 = o["number"];
23886
- const until = o["until"];
23887
- const at = o["at"];
23888
- if (!pace.success || kind !== null && !kind.success)
23889
- return null;
23890
- if (typeof allowance !== "number" || !Number.isInteger(allowance) || allowance < 0)
23891
- return null;
23892
- if (typeof number4 !== "number" || !Number.isInteger(number4) || number4 < 1 || number4 > PACE_AGENTS_MAX)
23893
- return null;
23894
- if (until !== null && (typeof until !== "string" || Number.isNaN(Date.parse(until))))
23895
- return null;
23896
- if (typeof at !== "string" || Number.isNaN(Date.parse(at)))
23897
- return null;
23898
- return { pace: pace.data, allowance, number: number4, windowKind: kind === null ? null : kind.data, until, at };
23899
- }
23900
- function rememberedBasisOf(v) {
23901
- const o = objectOf(v);
23902
- if (o === null)
23903
- return null;
23904
- const phase = o["phase"] === null ? null : phaseNameSchema.safeParse(o["phase"]);
23905
- const plan = o["claudePlan"] === null ? null : claudePlanSchema.safeParse(o["claudePlan"]);
23906
- const keep = keepFreePercentSchema.safeParse(o["keepFreePercent"]);
23907
- if (phase !== null && !phase.success || plan !== null && !plan.success || !keep.success)
23908
- return null;
23909
- return { phase: phase === null ? null : phase.data, claudePlan: plan === null ? null : plan.data, keepFreePercent: keep.data };
23910
- }
23911
- function readPaceStateFile(dir) {
23912
- const o = readJson(nodePath4.join(dir, PACE_STATE_FILENAME));
23913
- const accounts = {};
23914
- for (const [key, v] of Object.entries(objectOf(o?.["accounts"]) ?? {})) {
23915
- const e = entryOf(v);
23916
- if (e !== null)
23917
- accounts[key] = e;
23918
- }
23919
- return { accounts, basis: rememberedBasisOf(o?.["basis"]) };
23920
- }
23921
- function writePaceState(dir, key, entry, basis) {
23922
- const current = readPaceStateFile(dir);
23923
- const accounts = { ...current.accounts };
23924
- if (entry === null)
23925
- delete accounts[key];
23926
- else
23927
- accounts[key] = entry;
23928
- writeJsonWhole2(nodePath4.join(dir, PACE_STATE_FILENAME), { accounts, basis: basis ?? current.basis });
23929
- }
23930
24892
  function field2(input, key) {
23931
24893
  const v = objectOf(input)?.[key];
23932
24894
  return typeof v === "string" && v !== "" ? v : null;
@@ -23934,12 +24896,14 @@ function field2(input, key) {
23934
24896
  function shownAs(pace, windowKind, allowance) {
23935
24897
  return pace === "full" ? "full" : `${pace}:${windowKind ?? ""}:${pace === "slowing" ? String(allowance) : ""}`;
23936
24898
  }
23937
- function paceChangeText(before, after, working, now) {
24899
+ function paceChangeText(before, after, working, now, keepFreePercent) {
23938
24900
  const was = before === null ? "full" : shownAs(before.pace, before.windowKind, before.allowance);
23939
24901
  const is = shownAs(after.pace, after.windowKind, after.allowance);
23940
24902
  if (was === is)
23941
24903
  return null;
23942
24904
  if (after.pace === "full") {
24905
+ if (keepFreePercent === 0)
24906
+ return null;
23943
24907
  return before !== null && before.windowKind !== null ? paceS3Of(before.windowKind, after.number) : null;
23944
24908
  }
23945
24909
  if (after.windowKind === null || after.until === null)
@@ -24009,7 +24973,7 @@ async function paceAtTurnEnd(input, reply, deps = {}) {
24009
24973
  };
24010
24974
  write(dir, key, after, read !== null ? basis : null);
24011
24975
  const working = liveCount(readLiveness(field2(input, "session_id"), field2(input, "cwd") ?? process.cwd(), deps.liveness));
24012
- const text = paceChangeText(before, after, working, now);
24976
+ const text = paceChangeText(before, after, working, now, basis.keepFreePercent);
24013
24977
  if (text !== null)
24014
24978
  reply.notice(text);
24015
24979
  } catch {
@@ -24138,9 +25102,9 @@ function paceHoldText(input, toolInput, deps = {}) {
24138
25102
  }
24139
25103
 
24140
25104
  // dist/risky/facts.js
24141
- import { existsSync as existsSync6, lstatSync, readdirSync as readdirSync8, readFileSync as readFileSync17, realpathSync as realpathSync5, statSync as statSync6 } from "node:fs";
25105
+ import { existsSync as existsSync7, lstatSync as lstatSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, statSync as statSync7 } from "node:fs";
24142
25106
  import { homedir as homedir7, tmpdir as tmpdir2 } from "node:os";
24143
- import { basename as pathBasename, dirname as dirname5, isAbsolute, join as join11, relative as relative4, resolve as resolve3, sep as sep2 } from "node:path";
25107
+ import { basename as pathBasename, dirname as dirname6, isAbsolute as isAbsolute2, join as join12, relative as relative5, resolve as resolve4, sep as sep3 } from "node:path";
24144
25108
 
24145
25109
  // dist/risky/shell.js
24146
25110
  var MAX_DEPTH = 8;
@@ -24515,7 +25479,7 @@ var Splitter = class _Splitter {
24515
25479
  const script = innerScript(words);
24516
25480
  if (script !== void 0) {
24517
25481
  if (this.depth < MAX_DEPTH) {
24518
- this.child(script, basename(words[0].text) === "eval").parseList(0, null);
25482
+ this.child(script, basename2(words[0].text) === "eval").parseList(0, null);
24519
25483
  }
24520
25484
  return;
24521
25485
  }
@@ -24584,7 +25548,7 @@ function unwrap(input) {
24584
25548
  words = words.slice(1);
24585
25549
  continue;
24586
25550
  }
24587
- const prog = basename(name);
25551
+ const prog = basename2(name);
24588
25552
  const valueOptions = WRAPPER_VALUE_OPTIONS[prog];
24589
25553
  if (valueOptions === void 0)
24590
25554
  return words;
@@ -24628,7 +25592,7 @@ function unwrap(input) {
24628
25592
  }
24629
25593
  var SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh"]);
24630
25594
  function innerScript(words) {
24631
- const prog = basename(words[0].text);
25595
+ const prog = basename2(words[0].text);
24632
25596
  if (prog === EVAL_PROGRAM)
24633
25597
  return words.slice(1).map((w) => w.text).join(" ");
24634
25598
  if (!SHELLS.has(prog))
@@ -24663,13 +25627,13 @@ function innerScript(words) {
24663
25627
  }
24664
25628
  function readsScriptFromInput(raw) {
24665
25629
  const words = unwrap(raw);
24666
- if (words.length === 0 || !SHELLS.has(basename(words[0].text)))
25630
+ if (words.length === 0 || !SHELLS.has(basename2(words[0].text)))
24667
25631
  return false;
24668
25632
  if (innerScript(words) !== void 0)
24669
25633
  return false;
24670
25634
  return words.slice(1).every((w) => w.text.startsWith("-") || w.text.startsWith("+"));
24671
25635
  }
24672
- function basename(word) {
25636
+ function basename2(word) {
24673
25637
  const k = Math.max(word.lastIndexOf("/"), word.lastIndexOf("\\"));
24674
25638
  return k < 0 ? word : word.slice(k + 1);
24675
25639
  }
@@ -24684,7 +25648,7 @@ function namesNeverFollowed(line2, commands) {
24684
25648
  const sourced = commands.some((c) => c.words[0] !== void 0 && (c.words[0].text === "source" || c.words[0].text === "."));
24685
25649
  const evaluated = commands.some((c) => c.inPlace === true);
24686
25650
  const indirect = commands.some((c) => {
24687
- const prog = c.words[0] === void 0 ? "" : basename(c.words[0].text);
25651
+ const prog = c.words[0] === void 0 ? "" : basename2(c.words[0].text);
24688
25652
  return prog === "trap" || prog === "builtin" || REFERENCE_BINDERS.has(prog) && c.words.slice(1).some((w) => /^-[A-Za-z]*n/.test(w.text));
24689
25653
  });
24690
25654
  const fns = functionsIn(line2);
@@ -24708,7 +25672,7 @@ function namesNeverFollowed(line2, commands) {
24708
25672
  out.add(hit[1]);
24709
25673
  }
24710
25674
  }
24711
- const prog = c.words[0] === void 0 ? "" : basename(c.words[0].text);
25675
+ const prog = c.words[0] === void 0 ? "" : basename2(c.words[0].text);
24712
25676
  const args2 = c.words.slice(1).map((w) => w.text);
24713
25677
  if (BINDERS.has(prog) || prog === "unset") {
24714
25678
  for (const a of args2) {
@@ -24933,7 +25897,7 @@ var LineVariables = class {
24933
25897
  }
24934
25898
  }
24935
25899
  update(cmd, before) {
24936
- const prog = cmd.words[0] === void 0 ? "" : basename(cmd.words[0].text);
25900
+ const prog = cmd.words[0] === void 0 ? "" : basename2(cmd.words[0].text);
24937
25901
  let settings = [];
24938
25902
  if (cmd.assigns !== void 0)
24939
25903
  settings = cmd.assigns;
@@ -25127,7 +26091,7 @@ function expandWord(word, vars) {
25127
26091
  return { text: v.text, literal: !pattern };
25128
26092
  }
25129
26093
  function expandCommand(cmd, vars) {
25130
- const first = cmd.words[0] === void 0 ? "" : basename(cmd.words[0].text);
26094
+ const first = cmd.words[0] === void 0 ? "" : basename2(cmd.words[0].text);
25131
26095
  const keepEmpty = FOLDER_CHANGERS.has(first);
25132
26096
  const words = [];
25133
26097
  cmd.words.forEach((w, i) => {
@@ -25171,7 +26135,7 @@ function pathsMade(cmd) {
25171
26135
  const words = cmd.words;
25172
26136
  if (words.length === 0)
25173
26137
  return [];
25174
- const prog = basename(words[0].text);
26138
+ const prog = basename2(words[0].text);
25175
26139
  const args2 = words.slice(1);
25176
26140
  if (prog === "mkdir") {
25177
26141
  const out = [];
@@ -25211,7 +26175,7 @@ function pathsMade(cmd) {
25211
26175
  if (ops.length >= 2)
25212
26176
  return [{ word: ops[ops.length - 1], by: "clone" }];
25213
26177
  if (ops.length === 1 && ops[0].literal) {
25214
- const name = basename(ops[0].text.replace(/\/+$/, "")).replace(/\.git$/, "");
26178
+ const name = basename2(ops[0].text.replace(/\/+$/, "")).replace(/\.git$/, "");
25215
26179
  if (name !== "" && !name.includes(":"))
25216
26180
  return [{ word: { text: name, literal: true }, by: "clone" }];
25217
26181
  }
@@ -25224,24 +26188,6 @@ function pathsMade(cmd) {
25224
26188
  return [];
25225
26189
  }
25226
26190
 
25227
- // dist/risky/types.js
25228
- var RISKY_ACTION_TYPES = [
25229
- "delete-files",
25230
- "discard-unsaved",
25231
- "rewrite-shared-history",
25232
- "delete-unmerged-work",
25233
- "wipe-data",
25234
- "read-credentials",
25235
- "publish",
25236
- "deploy",
25237
- "shrink-record",
25238
- "outside-own-copy"
25239
- ];
25240
- function riskyTypeOrder(type) {
25241
- return RISKY_ACTION_TYPES.indexOf(type);
25242
- }
25243
- var CHECKED_TOOLS = ["Bash", "Read", "Grep", "Write", "Edit", "MultiEdit", "NotebookEdit", "Agent"];
25244
-
25245
26191
  // dist/risky/recognise.js
25246
26192
  function madeKey(dirs, word) {
25247
26193
  return [...dirs.map((d) => d.text), word.text].join("\0");
@@ -25405,7 +26351,7 @@ function surelyRuns(simple, before) {
25405
26351
  }
25406
26352
  function folderAfter(prev, simple, before) {
25407
26353
  const first = simple.words[0];
25408
- if (first === void 0 || !FOLDER_CHANGERS.has(basename(first.text)))
26354
+ if (first === void 0 || !FOLDER_CHANGERS.has(basename2(first.text)))
25409
26355
  return prev;
25410
26356
  const dirs = prev?.dirs ?? [];
25411
26357
  const unknown2 = { dirs, known: false };
@@ -25414,7 +26360,7 @@ function folderAfter(prev, simple, before) {
25414
26360
  return prev;
25415
26361
  if (simple.next === "||" || before === "||")
25416
26362
  return unknown2;
25417
- const prog = basename(first.text);
26363
+ const prog = basename2(first.text);
25418
26364
  if (simple.inPlace === true || simple.conditional === true || prog === "popd")
25419
26365
  return unknown2;
25420
26366
  let args2 = simple.words.slice(1);
@@ -25458,7 +26404,7 @@ function recogniseSimple(cmd) {
25458
26404
  }
25459
26405
  function programForms(words) {
25460
26406
  const forms = [words];
25461
- const prog = basename(words[0].text);
26407
+ const prog = basename2(words[0].text);
25462
26408
  let rest;
25463
26409
  if (prog === "npx" || prog === "bunx" || prog === "pnpx") {
25464
26410
  rest = skipRunnerOptions(words.slice(1));
@@ -25504,7 +26450,7 @@ var PROGRAM_MATCHERS = /* @__PURE__ */ new Map([
25504
26450
  ["git", (cmd, args2) => gitMatch(cmd, args2)]
25505
26451
  ]);
25506
26452
  function recogniseWords(cmd, words) {
25507
- const prog = basename(words[0].text);
26453
+ const prog = basename2(words[0].text);
25508
26454
  const args2 = words.slice(1);
25509
26455
  const own = PROGRAM_MATCHERS.get(prog);
25510
26456
  if (own !== void 0)
@@ -25585,7 +26531,7 @@ function findMatch(cmd, args2) {
25585
26531
  if (t === FIND_DELETE_ACTION)
25586
26532
  deletes = true;
25587
26533
  if ((t === "-exec" || t === "-execdir" || t === "-ok" || t === "-okdir") && i + 1 < args2.length) {
25588
- const inner = basename(args2[i + 1].text);
26534
+ const inner = basename2(args2[i + 1].text);
25589
26535
  const viaGit = inner === "git" && args2[i + 2]?.text === "rm";
25590
26536
  if (FIND_DELETERS.has(inner) || viaGit) {
25591
26537
  deletes = true;
@@ -25633,8 +26579,9 @@ function findFilterOf(expr) {
25633
26579
  return names.length === 0 && excluded.length === 0 ? void 0 : { names, excluded };
25634
26580
  }
25635
26581
  var GIT_GLOBAL_VALUE_OPTIONS = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env", "--super-prefix", "--exec-path", "--attr-source", "--list-cmds"]);
25636
- function gitMatch(cmd, args2) {
26582
+ function gitArgsOf(args2) {
25637
26583
  const dirs = [];
26584
+ const config2 = [];
25638
26585
  let gitDir;
25639
26586
  let workTree;
25640
26587
  let i = 0;
@@ -25660,11 +26607,92 @@ function gitMatch(cmd, args2) {
25660
26607
  gitDir = value;
25661
26608
  if (name === "--work-tree" && value)
25662
26609
  workTree = value;
26610
+ if ((name === "-c" || name === "--config-env") && value)
26611
+ config2.push({ word: value, fromEnv: name === "--config-env" });
26612
+ }
26613
+ return { git: { dirs, ...gitDir ? { gitDir } : {}, ...workTree ? { workTree } : {} }, at: i, config: config2 };
26614
+ }
26615
+ function gitMatch(cmd, args2) {
26616
+ const { git: git2, at, config: config2 } = gitArgsOf(args2);
26617
+ const sub = args2[at]?.text;
26618
+ const own = sub === void 0 ? void 0 : GIT_SUBCOMMAND_MATCHERS.get(sub);
26619
+ return own === void 0 ? [] : own(cmd, args2.slice(at + 1), git2, config2);
26620
+ }
26621
+ var PUSH_ALL_REFS_OPTION = "--mirror";
26622
+ function hasLongAsGitReads(args2, name) {
26623
+ for (const a of args2) {
26624
+ if (a.text === "--")
26625
+ return false;
26626
+ const spelled = a.text.split("=")[0];
26627
+ if (spelled.length >= 3 && spelled.startsWith("--") && name.startsWith(spelled))
26628
+ return true;
26629
+ }
26630
+ return false;
26631
+ }
26632
+ function configNameOf(c) {
26633
+ const t = c.word.text;
26634
+ const eq = t.indexOf("=");
26635
+ return eq < 0 ? t : t.slice(0, eq);
26636
+ }
26637
+ function configValueOf(c) {
26638
+ if (c.fromEnv || !c.word.literal)
26639
+ return null;
26640
+ const t = c.word.text;
26641
+ const eq = t.indexOf("=");
26642
+ return eq < 0 ? true : t.slice(eq + 1);
26643
+ }
26644
+ function configNameUnknown(name) {
26645
+ return /[$`*?[]/.test(name);
26646
+ }
26647
+ var GIT_FALSE = /^(false|no|off|[-+]?0+)?$/i;
26648
+ function configMirrors(config2) {
26649
+ return config2.some((c) => {
26650
+ const name = configNameOf(c);
26651
+ if (configNameUnknown(name))
26652
+ return true;
26653
+ if (!/^remote\..+\.mirror$/i.test(name))
26654
+ return false;
26655
+ const value = configValueOf(c);
26656
+ return value === true || value === null || !GIT_FALSE.test(value);
26657
+ });
26658
+ }
26659
+ function pushMirrors(rest, config2) {
26660
+ return hasLongAsGitReads(rest, PUSH_ALL_REFS_OPTION) || configMirrors(config2);
26661
+ }
26662
+ var SAVED_NAMESPACE = SAVED_REF_PREFIX.split("/").slice(0, 2).join("/") + "/";
26663
+ function refspecSendsSaves(text) {
26664
+ if (text.includes(SAVED_NAMESPACE))
26665
+ return true;
26666
+ const src = text.replace(/^\+/, "").split(":")[0];
26667
+ const star = src.indexOf("*");
26668
+ return star >= 0 && SAVED_NAMESPACE.startsWith(src.slice(0, star));
26669
+ }
26670
+ function configPushSendsSaves(config2) {
26671
+ return config2.some((c) => {
26672
+ const name = configNameOf(c);
26673
+ if (configNameUnknown(name))
26674
+ return true;
26675
+ if (!/^remote\..+\.push$/i.test(name))
26676
+ return false;
26677
+ const value = configValueOf(c);
26678
+ return value === null || value !== true && refspecSendsSaves(value);
26679
+ });
26680
+ }
26681
+ function pushesSavedRefs(command) {
26682
+ for (const cmd of splitCommand(command)) {
26683
+ if (cmd.words.length === 0 || basename2(cmd.words[0].text) !== "git")
26684
+ continue;
26685
+ const args2 = cmd.words.slice(1);
26686
+ const { at, config: config2 } = gitArgsOf(args2);
26687
+ if (args2[at]?.text !== "push")
26688
+ continue;
26689
+ const rest = args2.slice(at + 1);
26690
+ if (pushMirrors(rest, config2) || configPushSendsSaves(config2))
26691
+ return true;
26692
+ if (rest.some((w) => refspecSendsSaves(w.text)))
26693
+ return true;
25663
26694
  }
25664
- const git2 = { dirs, ...gitDir ? { gitDir } : {}, ...workTree ? { workTree } : {} };
25665
- const sub = args2[i]?.text;
25666
- const own = sub === void 0 ? void 0 : GIT_SUBCOMMAND_MATCHERS.get(sub);
25667
- return own === void 0 ? [] : own(cmd, args2.slice(i + 1), git2);
26695
+ return false;
25668
26696
  }
25669
26697
  var GIT_SUBCOMMAND_MATCHERS = /* @__PURE__ */ new Map([
25670
26698
  ["rm", (cmd, rest, git2) => gitRm(cmd, rest, git2)],
@@ -25675,7 +26703,7 @@ var GIT_SUBCOMMAND_MATCHERS = /* @__PURE__ */ new Map([
25675
26703
  ["clean", (cmd, rest, git2) => gitClean(cmd, rest, git2)],
25676
26704
  ["stash", (cmd, rest, git2) => gitStash(cmd, rest, git2)],
25677
26705
  ["worktree", (cmd, rest, git2) => gitWorktree(cmd, rest, git2)],
25678
- ["push", (cmd, rest, git2) => gitPush(cmd, rest, git2)],
26706
+ ["push", (cmd, rest, git2, config2) => gitPush(cmd, rest, git2, config2)],
25679
26707
  ["branch", (cmd, rest, git2) => gitBranch(cmd, rest, git2)]
25680
26708
  ]);
25681
26709
  function gitSwitch(cmd, rest, git2) {
@@ -25753,7 +26781,7 @@ function gitWorktree(cmd, rest, git2) {
25753
26781
  return [{ type: "discard-unsaved", form: "worktree-remove", paths: operands(args2), git: git2, command: cmd }];
25754
26782
  }
25755
26783
  var PUSH_VALUE_OPTIONS = /* @__PURE__ */ new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec"]);
25756
- function gitPush(cmd, rest, git2) {
26784
+ function gitPush(cmd, rest, git2, config2) {
25757
26785
  if (hasShort(rest, "n") || hasLong(rest, "--dry-run"))
25758
26786
  return [];
25759
26787
  const ops = operands(rest, PUSH_VALUE_OPTIONS);
@@ -25762,7 +26790,7 @@ function gitPush(cmd, rest, git2) {
25762
26790
  const force = hasShort(rest, "f") || hasLong(rest, "--force");
25763
26791
  const forceWithLease = hasLong(rest, "--force-with-lease");
25764
26792
  const forceIfIncludes = hasLong(rest, "--force-if-includes");
25765
- const mirror = hasLong(rest, "--mirror");
26793
+ const mirror = pushMirrors(rest, config2);
25766
26794
  const deletes = hasShort(rest, "d") || hasLong(rest, "--delete") || refspecs.some((r) => r.text.startsWith(":") && r.text.length > 1);
25767
26795
  const plusRefspec = refspecs.some((r) => r.text.startsWith("+"));
25768
26796
  if (!(force || forceWithLease || forceIfIncludes || mirror || deletes || plusRefspec))
@@ -25802,7 +26830,7 @@ function wipeDataMatch(cmd, prog, args2, words) {
25802
26830
  const execAt = args2.findIndex((a) => a.text === "exec" || a.text === "run");
25803
26831
  if (execAt >= 0) {
25804
26832
  for (let i = execAt + 1; i < args2.length; i++) {
25805
- const inner = basename(args2[i].text);
26833
+ const inner = basename2(args2[i].text);
25806
26834
  if (SQL_CLIENTS.has(inner) || inner === "dropdb" || inner === "redis-cli") {
25807
26835
  return wipeDataMatch(cmd, inner, args2.slice(i + 1), args2.slice(i));
25808
26836
  }
@@ -26149,7 +27177,7 @@ function shellRecordMatches(cmd, found2) {
26149
27177
  });
26150
27178
  }
26151
27179
  const words = cmd.words;
26152
- const prog = words.length > 0 ? basename(words[0].text) : "";
27180
+ const prog = words.length > 0 ? basename2(words[0].text) : "";
26153
27181
  const args2 = words.slice(1);
26154
27182
  if (prog === TRUNCATE_PROGRAM) {
26155
27183
  const targets = operands(args2, /* @__PURE__ */ new Set(["-s", "--size", "-r", "--reference"])).filter((w) => mayBeRecordWord(w));
@@ -26182,7 +27210,7 @@ function movesStdout(r) {
26182
27210
  return [">", ">>", ">|", ">&"].includes(r.op) && (r.fd === void 0 || r.fd === "1");
26183
27211
  }
26184
27212
  function savedObjectShown(words) {
26185
- if (words.length !== 3 || basename(words[0].text) !== "git" || words[1].text !== "show" || !words[2].literal)
27213
+ if (words.length !== 3 || basename2(words[0].text) !== "git" || words[1].text !== "show" || !words[2].literal)
26186
27214
  return void 0;
26187
27215
  return /^[0-9a-f]{7,64}:[^:]+$/.test(words[2].text) ? words[2].text : void 0;
26188
27216
  }
@@ -26285,13 +27313,13 @@ var Env = class {
26285
27313
  description;
26286
27314
  projectCommonDir;
26287
27315
  constructor(ctx) {
26288
- this.projectDir = realOrSelf(resolve3(ctx.projectDir));
26289
- this.cwd = realOrSelf(resolve3(this.projectDir, ctx.cwd));
26290
- this.home = realOrSelf(resolve3(ctx.home ?? homedir7()));
27316
+ this.projectDir = realOrSelf(resolve4(ctx.projectDir));
27317
+ this.cwd = realOrSelf(resolve4(this.projectDir, ctx.cwd));
27318
+ this.home = realOrSelf(resolve4(ctx.home ?? homedir7()));
26291
27319
  this.timeoutMs = ctx.gitTimeoutMs ?? FACTS_GIT_TIMEOUT_MS;
26292
- this.tempDir = realOrSelf(resolve3(ctx.tempDir ?? tmpdir2()));
27320
+ this.tempDir = realOrSelf(resolve4(ctx.tempDir ?? tmpdir2()));
26293
27321
  const roots = ctx.tempRoots ?? [this.tempDir, tmpdir2(), ...TEMP_ROOTS];
26294
- this.tempRoots = [...new Set(roots.map((r) => realOrSelf(resolve3(r))))].filter((r) => !isInside(this.projectDir, r));
27322
+ this.tempRoots = [...new Set(roots.map((r) => realOrSelf(resolve4(r))))].filter((r) => !isInside(this.projectDir, r));
26295
27323
  }
26296
27324
  isTemp(abs) {
26297
27325
  return this.tempRoots.some((r) => isInside(abs, r));
@@ -26301,7 +27329,7 @@ var Env = class {
26301
27329
  }
26302
27330
  display(abs) {
26303
27331
  if (isInside(abs, this.projectDir)) {
26304
- const rel = relative4(this.projectDir, abs);
27332
+ const rel = relative5(this.projectDir, abs);
26305
27333
  return rel === "" ? "." : toSlash(rel);
26306
27334
  }
26307
27335
  return toSlash(abs);
@@ -26311,7 +27339,7 @@ var Env = class {
26311
27339
  return this.description;
26312
27340
  this.description = null;
26313
27341
  try {
26314
- const raw = JSON.parse(readFileSync17(join11(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8"));
27342
+ const raw = JSON.parse(readFileSync19(join12(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8"));
26315
27343
  const checked = checkSystemDescription(raw);
26316
27344
  if (checked.ok)
26317
27345
  this.description = checked.description;
@@ -26342,7 +27370,7 @@ var Env = class {
26342
27370
  return false;
26343
27371
  let dir = abs;
26344
27372
  while (!isDirectory(dir)) {
26345
- const up = dirname5(dir);
27373
+ const up = dirname6(dir);
26346
27374
  if (up === dir)
26347
27375
  return false;
26348
27376
  dir = up;
@@ -26354,40 +27382,40 @@ var Env = class {
26354
27382
  if (!r.ok)
26355
27383
  return null;
26356
27384
  const d = r.value.trim();
26357
- return d === "" ? null : realOrSelf(resolve3(dir, d));
27385
+ return d === "" ? null : realOrSelf(resolve4(dir, d));
26358
27386
  }
26359
27387
  };
26360
27388
  function realOrSelf(p) {
26361
27389
  try {
26362
- return realpathSync5(p);
27390
+ return realpathSync6(p);
26363
27391
  } catch {
26364
27392
  return p;
26365
27393
  }
26366
27394
  }
26367
27395
  function toSlash(p) {
26368
- return sep2 === "/" ? p : p.split(sep2).join("/");
27396
+ return sep3 === "/" ? p : p.split(sep3).join("/");
26369
27397
  }
26370
27398
  function isInside(child, parent) {
26371
- const rel = relative4(parent, child);
26372
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
27399
+ const rel = relative5(parent, child);
27400
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
26373
27401
  }
26374
27402
  function resolveHome(text, home, base) {
26375
27403
  if (text === "~")
26376
27404
  return home;
26377
27405
  if (text.startsWith("~/"))
26378
- return join11(home, text.slice(2));
26379
- return resolve3(base, text);
27406
+ return join12(home, text.slice(2));
27407
+ return resolve4(base, text);
26380
27408
  }
26381
27409
  function realTarget(abs) {
26382
- const parent = dirname5(abs);
27410
+ const parent = dirname6(abs);
26383
27411
  if (parent === abs)
26384
27412
  return abs;
26385
- return join11(realOrSelf(parent), pathBasename(abs));
27413
+ return join12(realOrSelf(parent), pathBasename(abs));
26386
27414
  }
26387
27415
  function wordPath(word, base, env) {
26388
27416
  if (word.temp !== void 0) {
26389
- const root = word.temp.base === "mktemp" ? join11(env.tempDir, ".halfcycle-mktemp-new") : env.tempDir;
26390
- return resolve3(`${root}${word.temp.suffix}`);
27417
+ const root = word.temp.base === "mktemp" ? join12(env.tempDir, ".halfcycle-mktemp-new") : env.tempDir;
27418
+ return resolve4(`${root}${word.temp.suffix}`);
26391
27419
  }
26392
27420
  return word.literal ? resolveHome(word.text, env.home, base) : null;
26393
27421
  }
@@ -26396,7 +27424,7 @@ function follow(start, prefix, dirs, m, env) {
26396
27424
  let fresh = start.fresh;
26397
27425
  let freshRoot = start.freshRoot;
26398
27426
  const seen = [...prefix];
26399
- const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve3(p), resolve3(freshRoot));
27427
+ const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve4(p), resolve4(freshRoot));
26400
27428
  const mark = (by) => {
26401
27429
  if (fresh !== void 0 && insideFresh(cwd))
26402
27430
  return;
@@ -26525,14 +27553,14 @@ function expandBraces(text) {
26525
27553
  return body.split(",").map((b) => `${head}${b}${tail}`);
26526
27554
  }
26527
27555
  function globPath(abs) {
26528
- const root = abs.startsWith(sep2) ? sep2 : "";
26529
- const segments = abs.split(sep2).filter((s) => s !== "");
27556
+ const root = abs.startsWith(sep3) ? sep3 : "";
27557
+ const segments = abs.split(sep3).filter((s) => s !== "");
26530
27558
  let current = [root === "" ? "." : root];
26531
27559
  for (const segment of segments) {
26532
27560
  const next = [];
26533
27561
  if (!/[*?[]/.test(segment)) {
26534
27562
  for (const c of current)
26535
- next.push(join11(c, segment));
27563
+ next.push(join12(c, segment));
26536
27564
  } else {
26537
27565
  const re = segmentPattern(segment);
26538
27566
  for (const c of current) {
@@ -26546,7 +27574,7 @@ function globPath(abs) {
26546
27574
  if (n.startsWith(".") && !segment.startsWith("."))
26547
27575
  continue;
26548
27576
  if (re.test(n))
26549
- next.push(join11(c, n));
27577
+ next.push(join12(c, n));
26550
27578
  }
26551
27579
  }
26552
27580
  }
@@ -26582,7 +27610,7 @@ function segmentPattern(segment) {
26582
27610
  }
26583
27611
  function pathExists(p) {
26584
27612
  try {
26585
- lstatSync(p);
27613
+ lstatSync2(p);
26586
27614
  return true;
26587
27615
  } catch {
26588
27616
  return false;
@@ -26590,28 +27618,28 @@ function pathExists(p) {
26590
27618
  }
26591
27619
  function isEmptyFolder(p) {
26592
27620
  try {
26593
- return lstatSync(p).isDirectory() && readdirSync8(p).length === 0;
27621
+ return lstatSync2(p).isDirectory() && readdirSync8(p).length === 0;
26594
27622
  } catch {
26595
27623
  return false;
26596
27624
  }
26597
27625
  }
26598
27626
  function isFolder(p) {
26599
27627
  try {
26600
- return statSync6(p).isDirectory();
27628
+ return statSync7(p).isDirectory();
26601
27629
  } catch {
26602
27630
  return false;
26603
27631
  }
26604
27632
  }
26605
27633
  function isDirectory(p) {
26606
27634
  try {
26607
- return lstatSync(p).isDirectory();
27635
+ return lstatSync2(p).isDirectory();
26608
27636
  } catch {
26609
27637
  return false;
26610
27638
  }
26611
27639
  }
26612
- function filesUnder(targets, repoRoot, env, keep) {
27640
+ function filesUnder(targets, repoRoot, env, keep, seen) {
26613
27641
  const specs = targets.map((t) => {
26614
- const rel = toSlash(relative4(repoRoot, t));
27642
+ const rel = toSlash(relative5(repoRoot, t));
26615
27643
  return rel === "" ? "." : rel;
26616
27644
  });
26617
27645
  const tracked = env.git(["--literal-pathspecs", "ls-files", "--full-name", "-z", "--", ...specs], repoRoot);
@@ -26645,6 +27673,8 @@ function filesUnder(targets, repoRoot, env, keep) {
26645
27673
  goneFromDisk.add(e.path);
26646
27674
  }
26647
27675
  }
27676
+ if (seen !== void 0)
27677
+ seen.ignored = ignored.length;
26648
27678
  const out = /* @__PURE__ */ new Map();
26649
27679
  for (const path of tracked.value.split("\0")) {
26650
27680
  if (path === "" || goneFromDisk.has(path))
@@ -26672,12 +27702,12 @@ function findKeeps(filter, roots, repoRoot) {
26672
27702
  const names = filter.names.map((n) => findPattern(n.pattern, n.fold, false));
26673
27703
  const excluded = filter.excluded.map((p) => findPattern(p, false, true));
26674
27704
  const steps = (rel) => {
26675
- const abs = join11(repoRoot, rel);
27705
+ const abs = join12(repoRoot, rel);
26676
27706
  const out = [];
26677
27707
  for (const r of roots) {
26678
27708
  if (!isInside(abs, r.abs))
26679
27709
  continue;
26680
- const below = toSlash(relative4(r.abs, abs)).split("/").filter((x) => x !== "");
27710
+ const below = toSlash(relative5(r.abs, abs)).split("/").filter((x) => x !== "");
26681
27711
  const rootName = r.text === null ? pathBasename(r.abs) : r.text.replace(/\/+$/, "").split("/").pop() || r.text;
26682
27712
  const prefixes = r.text === null ? [] : [.../* @__PURE__ */ new Set([r.text, r.text.replace(/\/+$/, "") || r.text])];
26683
27713
  const chain = [{ name: rootName, printed: r.text === null ? [] : [r.text] }];
@@ -26726,27 +27756,27 @@ function findPattern(pattern, fold, whole) {
26726
27756
  return new RegExp(`${re}$`, fold ? "i" : "");
26727
27757
  }
26728
27758
  function walkRoots(ignoredFolders, specs) {
26729
- const under = (child, parent) => parent === "." || child === parent || child.startsWith(`${parent}/`);
27759
+ const under2 = (child, parent) => parent === "." || child === parent || child.startsWith(`${parent}/`);
26730
27760
  const roots = /* @__PURE__ */ new Set();
26731
27761
  for (const raw of ignoredFolders) {
26732
27762
  const folder = raw.replace(/\/+$/, "");
26733
27763
  for (const spec of specs) {
26734
- if (under(folder, spec))
27764
+ if (under2(folder, spec))
26735
27765
  roots.add(folder);
26736
- else if (under(spec, folder))
27766
+ else if (under2(spec, folder))
26737
27767
  roots.add(spec);
26738
27768
  }
26739
27769
  }
26740
27770
  const all = [...roots];
26741
- return all.filter((r) => !all.some((o) => o !== r && under(r, o)));
27771
+ return all.filter((r) => !all.some((o) => o !== r && under2(r, o)));
26742
27772
  }
26743
27773
  function credentialFilesIn(folders, repoRoot, deadline, skip) {
26744
27774
  const out = [];
26745
27775
  const stack = [];
26746
27776
  for (const f of folders) {
26747
- if (isDirectory(join11(repoRoot, f)))
27777
+ if (isDirectory(join12(repoRoot, f)))
26748
27778
  stack.push(f);
26749
- else if (pathExists(join11(repoRoot, f)) && isCredentialPath(f))
27779
+ else if (pathExists(join12(repoRoot, f)) && isCredentialPath(f))
26750
27780
  out.push(f);
26751
27781
  }
26752
27782
  let seen = 0;
@@ -26754,7 +27784,7 @@ function credentialFilesIn(folders, repoRoot, deadline, skip) {
26754
27784
  const rel = stack.pop();
26755
27785
  let entries;
26756
27786
  try {
26757
- entries = readdirSync8(join11(repoRoot, rel), { withFileTypes: true });
27787
+ entries = readdirSync8(join12(repoRoot, rel), { withFileTypes: true });
26758
27788
  } catch {
26759
27789
  continue;
26760
27790
  }
@@ -26851,7 +27881,7 @@ function deleteFilesFacts(m, env) {
26851
27881
  for (const raw of expanded) {
26852
27882
  const abs = realTarget(raw);
26853
27883
  written.set(abs, word.literal ? word.text : null);
26854
- if (abs === sep2 || abs === env.home || abs !== env.projectDir && isInside(env.projectDir, abs)) {
27884
+ if (abs === sep3 || abs === env.home || abs !== env.projectDir && isInside(env.projectDir, abs)) {
26855
27885
  guarded.push(abs);
26856
27886
  } else if (isInside(abs, env.projectDir) || !env.isTemp(abs) && env.sameRepository(abs)) {
26857
27887
  if (pathExists(abs))
@@ -26867,16 +27897,18 @@ function deleteFilesFacts(m, env) {
26867
27897
  const targets = plainFolderRefused ? inside.filter((t) => !isDirectory(t)) : inside;
26868
27898
  if (targets.length === 0)
26869
27899
  return silentDelete(m);
26870
- const repoRoot = env.repoRoot(dirname5(targets[0])) ?? env.repoRoot(env.projectDir);
27900
+ const repoRoot = env.repoRoot(dirname6(targets[0])) ?? env.repoRoot(env.projectDir);
26871
27901
  if (repoRoot === null || !targets.every((t) => isInside(t, repoRoot)))
26872
27902
  return cannot(thingFor([], targets, null, env));
26873
27903
  const keep = m.findFilter === void 0 ? void 0 : findKeeps(m.findFilter, targets.map((t) => ({ abs: t, text: written.get(t) ?? null })), repoRoot);
26874
- const files = filesUnder(targets, repoRoot, env, keep);
27904
+ const seen = { ignored: 0 };
27905
+ const files = filesUnder(targets, repoRoot, env, keep, seen);
26875
27906
  if (files === null)
26876
27907
  return cannot(thingFor([], targets, repoRoot, env));
26877
27908
  const n = files.length;
26878
27909
  const k = files.filter((f) => f.neverSaved).length;
26879
- const atStake = m.form === "plain" ? k > 0 : n > 0;
27910
+ const closeFold = m.form !== "find" && seen.ignored === 0 && closeFoldsAll(targets, env);
27911
+ const atStake = m.form === "plain" || closeFold ? k > 0 : n > 0;
26880
27912
  if (!atStake)
26881
27913
  return silentDelete(m);
26882
27914
  const thing = thingFor(files.map((f) => f.path), targets, repoRoot, env);
@@ -26889,9 +27921,38 @@ function deleteFilesFacts(m, env) {
26889
27921
  ...m.form === "find" ? { cannotTell: "which-files" } : {},
26890
27922
  thing,
26891
27923
  files: m.form === "find" ? null : n,
26892
- neverSaved: m.form === "find" ? null : k
27924
+ neverSaved: m.form === "find" ? null : k,
27925
+ ...base.pre.length === 0 ? {
27926
+ save: {
27927
+ repoRoot,
27928
+ paths: targets.map((t) => toSlash(relative5(repoRoot, t)) || "."),
27929
+ ...keep === void 0 ? {} : { keep: (rel) => keep.deletes(rel) },
27930
+ lines: []
27931
+ }
27932
+ } : {}
26893
27933
  };
26894
27934
  }
27935
+ function closeFoldsAll(targets, env) {
27936
+ return targets.every((t) => {
27937
+ const root = recordRoot(t, env);
27938
+ if (root === null || !isInside(t, root))
27939
+ return false;
27940
+ const rel = toSlash(relative5(root, t));
27941
+ if (rel === "" || recordPatternFor(rel)?.deleteAsked !== false)
27942
+ return false;
27943
+ let at = root;
27944
+ for (const seg of rel.split("/")) {
27945
+ at = join12(at, seg);
27946
+ try {
27947
+ if (lstatSync2(at).isSymbolicLink())
27948
+ return false;
27949
+ } catch {
27950
+ return false;
27951
+ }
27952
+ }
27953
+ return true;
27954
+ });
27955
+ }
26895
27956
  function silentDelete(m) {
26896
27957
  return { type: m.type, match: m, atStake: false, lostForGood: [], recoverable: [], thing: null, files: 0, neverSaved: 0 };
26897
27958
  }
@@ -26949,6 +28010,7 @@ function discardUnsavedFacts(m, env) {
26949
28010
  const repoRoot = env.repoRoot(where.cwd);
26950
28011
  const part = repoRoot === null ? null : owningPart(lost, repoRoot, env);
26951
28012
  const thing = part !== null ? { kind: "part", name: part } : { kind: "paths", paths: lost };
28013
+ const saveable = repoRoot !== null && where.pre.length === 0 && m.form !== "stash-drop" && m.form !== "stash-clear" && m.form !== "worktree-remove";
26952
28014
  return {
26953
28015
  type: m.type,
26954
28016
  match: m,
@@ -26956,7 +28018,8 @@ function discardUnsavedFacts(m, env) {
26956
28018
  lostForGood: [{ kind: "unsaved-changes", count: lost.length }],
26957
28019
  recoverable: [],
26958
28020
  thing,
26959
- files: lost.length
28021
+ files: lost.length,
28022
+ ...saveable ? { save: { repoRoot, paths: lost, lines: [] } } : {}
26960
28023
  };
26961
28024
  }
26962
28025
  function changedFiles(where, paths, which, env, skipUnmerged = false) {
@@ -27009,8 +28072,9 @@ function restoreTouchesIndex(m) {
27009
28072
  }
27010
28073
  function cleanFacts(m, env) {
27011
28074
  const where = gitFolder(m, env);
27012
- const lost = where === null ? null : cleanFiles(m, where, env);
27013
- if (where === null || lost === null) {
28075
+ const cleaned = where === null ? null : cleanFiles(m, where, env);
28076
+ const lost = cleaned === null ? null : cleaned.lost;
28077
+ if (where === null || cleaned === null || lost === null) {
27014
28078
  return { type: m.type, match: m, atStake: true, lostForGood: [], recoverable: [], cannotTell: "which-files", thing: null, files: null, neverSaved: null };
27015
28079
  }
27016
28080
  if (lost.length === 0)
@@ -27025,7 +28089,8 @@ function cleanFacts(m, env) {
27025
28089
  recoverable: [],
27026
28090
  thing: part !== null ? { kind: "part", name: part } : { kind: "paths", paths: lost },
27027
28091
  files: lost.length,
27028
- neverSaved: lost.length
28092
+ neverSaved: lost.length,
28093
+ ...repoRoot !== null && where.pre.length === 0 ? { save: { repoRoot, paths: cleaned.removed, lines: [] } } : {}
27029
28094
  };
27030
28095
  }
27031
28096
  function cleanFiles(m, where, env) {
@@ -27062,12 +28127,15 @@ function cleanFiles(m, where, env) {
27062
28127
  return null;
27063
28128
  const named = dry.value.split("\n").map((l) => /^Would remove (.*)$/.exec(l)?.[1]).filter((x) => x !== void 0 && x !== "");
27064
28129
  if (named.length === 0)
27065
- return [];
28130
+ return { lost: [], removed: [] };
27066
28131
  const repoRoot = env.repoRoot(where.cwd);
27067
28132
  if (repoRoot === null)
27068
28133
  return null;
27069
- const files = filesUnder(named.map((n) => resolve3(where.cwd, n)), repoRoot, env);
27070
- return files === null ? null : files.filter((f) => f.neverSaved).map((f) => f.path);
28134
+ const abs = named.map((n) => resolve4(where.cwd, n));
28135
+ const files = filesUnder(abs, repoRoot, env);
28136
+ if (files === null)
28137
+ return null;
28138
+ return { lost: files.filter((f) => f.neverSaved).map((f) => f.path), removed: abs.map((a) => toSlash(relative5(repoRoot, a)) || ".") };
27071
28139
  }
27072
28140
  function stashFiles(m, where, env) {
27073
28141
  let refs;
@@ -27107,7 +28175,7 @@ function worktreeFiles(m, where, env) {
27107
28175
  if (path === null)
27108
28176
  return null;
27109
28177
  const abs = realOrSelf(path);
27110
- if (!existsSync6(abs))
28178
+ if (!existsSync7(abs))
27111
28179
  return [];
27112
28180
  const top = env.repoRoot(abs);
27113
28181
  if (top === null)
@@ -27119,19 +28187,23 @@ function worktreeFiles(m, where, env) {
27119
28187
  }
27120
28188
  function unmergedFacts(m, env) {
27121
28189
  const where = gitFolder(m, env, ["clone"]);
27122
- const lines2 = [];
28190
+ const leaves = [];
27123
28191
  if (where === null) {
27124
- lines2.push({ branch: null, commits: null });
28192
+ leaves.push({ line: { branch: null, commits: null }, tip: null });
27125
28193
  } else if (where.fresh === "clone") {
27126
- lines2.push({ branch: null, commits: 0 });
28194
+ leaves.push({ line: { branch: null, commits: 0 }, tip: null });
27127
28195
  } else if (m.form === "reset-to") {
27128
- lines2.push(resetLeaves(m.rev, where, env));
28196
+ leaves.push(resetLeaves(m.rev, where, env));
27129
28197
  } else {
27130
28198
  for (const b of m.branches)
27131
- lines2.push(branchLeaves(b, m.remoteTracking, where, env));
28199
+ leaves.push(branchLeaves(b, m.remoteTracking, where, env));
27132
28200
  }
28201
+ const lines2 = leaves.map((l) => l.line);
27133
28202
  const unknown2 = lines2.some((l) => l.commits === null);
27134
28203
  const total = lines2.reduce((s, l) => s + (l.commits ?? 0), 0);
28204
+ const kept = leaves.filter((l) => (l.line.commits ?? 0) > 0);
28205
+ const repoRoot = where === null || where.fresh !== void 0 || where.pre.length > 0 ? null : env.repoRoot(where.cwd);
28206
+ const saveable = !unknown2 && repoRoot !== null && kept.every((l) => l.tip !== null);
27135
28207
  return {
27136
28208
  type: m.type,
27137
28209
  match: m,
@@ -27139,44 +28211,46 @@ function unmergedFacts(m, env) {
27139
28211
  lostForGood: [],
27140
28212
  recoverable: total > 0 ? [{ kind: "local-changes", count: total }] : [],
27141
28213
  ...unknown2 ? { cannotTell: "which-line" } : {},
27142
- lines: lines2
28214
+ lines: lines2,
28215
+ ...saveable ? { save: { repoRoot, paths: [], lines: kept.map((l) => ({ name: m.remoteTracking ? null : l.line.branch, commit: l.tip })) } } : {}
27143
28216
  };
27144
28217
  }
27145
28218
  function resetLeaves(rev, where, env) {
27146
28219
  const current = currentBranch(where, env);
28220
+ const at = (commits, tip = null) => ({ line: { branch: current, commits }, tip });
27147
28221
  if (rev === void 0)
27148
- return { branch: current, commits: 0 };
28222
+ return at(0);
27149
28223
  let target;
27150
28224
  if (rev.gitRev !== void 0) {
27151
28225
  const r2 = env.git([...where.pre, ...rev.gitRev], where.cwd);
27152
28226
  const id = r2.ok ? r2.value.trim() : "";
27153
28227
  if (!/^[0-9a-f]{40,64}$/.test(id))
27154
- return { branch: current, commits: null };
28228
+ return at(null);
27155
28229
  target = id;
27156
28230
  } else {
27157
28231
  if (!rev.literal || rev.text.startsWith("-"))
27158
- return { branch: current, commits: null };
28232
+ return at(null);
27159
28233
  target = resolveCommit(rev.text, where, env);
27160
28234
  }
27161
28235
  if (target === null)
27162
- return { branch: current, commits: 0 };
28236
+ return at(0);
27163
28237
  const head = resolveCommit("HEAD", where, env);
27164
28238
  if (head === null)
27165
- return { branch: current, commits: 0 };
28239
+ return at(0);
27166
28240
  const excludeCurrent = current === null ? [] : [`--exclude=${current}`];
27167
28241
  const r = env.git([...where.pre, "rev-list", head, "--not", target, ...excludeCurrent, "--branches", "--remotes", "--tags"], where.cwd);
27168
- return { branch: current, commits: r.ok ? lines(r.value).length : null };
28242
+ return at(r.ok ? lines(r.value).length : null, head);
27169
28243
  }
27170
28244
  function branchLeaves(b, remoteTracking, where, env) {
27171
28245
  if (!b.literal || b.text.startsWith("-"))
27172
- return { branch: b.literal ? b.text : null, commits: null };
28246
+ return { line: { branch: b.literal ? b.text : null, commits: null }, tip: null };
27173
28247
  const ref = remoteTracking ? `refs/remotes/${b.text}` : `refs/heads/${b.text}`;
27174
28248
  const tip = resolveCommit(ref, where, env);
27175
28249
  if (tip === null)
27176
- return { branch: b.text, commits: 0 };
28250
+ return { line: { branch: b.text, commits: 0 }, tip: null };
27177
28251
  const exclude = remoteTracking ? ["--branches", `--exclude=${b.text}`, "--remotes", "--tags"] : [`--exclude=${b.text}`, "--branches", "--remotes", "--tags"];
27178
28252
  const r = env.git([...where.pre, "rev-list", tip, "--not", ...exclude], where.cwd);
27179
- return { branch: b.text, commits: r.ok ? lines(r.value).length : null };
28253
+ return { line: { branch: b.text, commits: r.ok ? lines(r.value).length : null }, tip };
27180
28254
  }
27181
28255
  function currentBranch(where, env) {
27182
28256
  const r = env.git([...where.pre, "symbolic-ref", "--quiet", "--short", "HEAD"], where.cwd);
@@ -27451,7 +28525,7 @@ function publishFacts(m, env) {
27451
28525
  const start = startFolder(m, env);
27452
28526
  if (start !== null && PACKAGE_JSON_PROGRAMS.has(m.program)) {
27453
28527
  try {
27454
- const pkg = JSON.parse(readFileSync17(join11(start, "package.json"), "utf-8"));
28528
+ const pkg = JSON.parse(readFileSync19(join12(start, "package.json"), "utf-8"));
27455
28529
  if (typeof pkg["name"] === "string" && typeof pkg["version"] === "string" && pkg["name"] !== "" && pkg["version"] !== "") {
27456
28530
  name = pkg["name"];
27457
28531
  version2 = pkg["version"];
@@ -27499,9 +28573,9 @@ function shrinkRecordFacts(m, env) {
27499
28573
  for (const file2 of [...found2.values()].sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0)) {
27500
28574
  let before;
27501
28575
  try {
27502
- if (!lstatSync(file2.abs).isFile())
28576
+ if (!lstatSync2(file2.abs).isFile())
27503
28577
  continue;
27504
- before = readFileSync17(file2.abs, "utf-8");
28578
+ before = readFileSync19(file2.abs, "utf-8");
27505
28579
  } catch {
27506
28580
  continue;
27507
28581
  }
@@ -27534,7 +28608,8 @@ function shrinkRecordFacts(m, env) {
27534
28608
  lines: lines2,
27535
28609
  removed,
27536
28610
  history: h === null ? null : h.history,
27537
- unsaved
28611
+ unsaved,
28612
+ ...base.pre.length === 0 ? { save: { repoRoot: file2.root, paths: [file2.rel], lines: [] } } : {}
27538
28613
  };
27539
28614
  });
27540
28615
  }
@@ -27552,7 +28627,7 @@ function recordRoot(abs, env) {
27552
28627
  return null;
27553
28628
  let dir = abs;
27554
28629
  while (!isDirectory(dir)) {
27555
- const up = dirname5(dir);
28630
+ const up = dirname6(dir);
27556
28631
  if (up === dir)
27557
28632
  return null;
27558
28633
  dir = up;
@@ -27563,7 +28638,7 @@ function recordFileAt(abs, env) {
27563
28638
  const root = recordRoot(abs, env);
27564
28639
  if (root === null || !isInside(abs, root))
27565
28640
  return null;
27566
- const rel = toSlash(relative4(root, abs));
28641
+ const rel = toSlash(relative5(root, abs));
27567
28642
  if (rel === "")
27568
28643
  return null;
27569
28644
  const row = recordPatternFor(rel);
@@ -27573,11 +28648,11 @@ function recordFilesUnder(folder, env) {
27573
28648
  const root = recordRoot(folder, env);
27574
28649
  if (root === null)
27575
28650
  return [];
27576
- const relFolder = toSlash(relative4(root, folder));
28651
+ const relFolder = toSlash(relative5(root, folder));
27577
28652
  const starts = /* @__PURE__ */ new Set();
27578
28653
  for (const { pattern } of PROJECT_RECORD_PATTERNS) {
27579
28654
  if (relFolder === "" || patternOwns(relFolder, pattern))
27580
- starts.add(join11(root, pattern));
28655
+ starts.add(join12(root, pattern));
27581
28656
  else if (patternOwns(pattern, relFolder))
27582
28657
  starts.add(folder);
27583
28658
  }
@@ -27591,7 +28666,7 @@ function recordFilesUnder(folder, env) {
27591
28666
  seen.add(p);
27592
28667
  let st;
27593
28668
  try {
27594
- st = lstatSync(p);
28669
+ st = lstatSync2(p);
27595
28670
  } catch {
27596
28671
  continue;
27597
28672
  }
@@ -27603,7 +28678,7 @@ function recordFilesUnder(folder, env) {
27603
28678
  continue;
27604
28679
  }
27605
28680
  for (const n of names)
27606
- stack.push(join11(p, n));
28681
+ stack.push(join12(p, n));
27607
28682
  } else if (st.isFile()) {
27608
28683
  const r = recordFileAt(p, env);
27609
28684
  if (r !== null)
@@ -27616,7 +28691,7 @@ function recordHistory(files, env) {
27616
28691
  const out = /* @__PURE__ */ new Map();
27617
28692
  const byTree = /* @__PURE__ */ new Map();
27618
28693
  for (const abs of files) {
27619
- const top = env.repoRoot(dirname5(abs));
28694
+ const top = env.repoRoot(dirname6(abs));
27620
28695
  if (top === null || !isInside(abs, top))
27621
28696
  continue;
27622
28697
  const list = byTree.get(top) ?? [];
@@ -27624,7 +28699,7 @@ function recordHistory(files, env) {
27624
28699
  byTree.set(top, list);
27625
28700
  }
27626
28701
  for (const [top, list] of byTree) {
27627
- const specs = list.map((abs) => toSlash(relative4(top, abs)));
28702
+ const specs = list.map((abs) => toSlash(relative5(top, abs)));
27628
28703
  const tracked = trackedOf(specs, top, env);
27629
28704
  if (tracked === null)
27630
28705
  continue;
@@ -27705,14 +28780,7 @@ function oneLine(text) {
27705
28780
  return text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").replace(/\s+/g, " ").trim();
27706
28781
  }
27707
28782
  function questionFor(facts, description) {
27708
- const said = /* @__PURE__ */ new Set();
27709
- const staked = facts.filter((f) => f.atStake).map((f, i) => ({ f, i })).sort((a, b) => riskyTypeOrder(a.f.type) - riskyTypeOrder(b.f.type) || a.i - b.i).map(({ f }) => f).filter((f) => {
27710
- const part = `${whatOf(f)} ${lostOf(f)}`;
27711
- if (said.has(part))
27712
- return false;
27713
- said.add(part);
27714
- return true;
27715
- });
28783
+ const staked = orderedStaked(facts);
27716
28784
  const reason = reasonSentence(description);
27717
28785
  if (staked.length === 0)
27718
28786
  return couldNotTellQuestion(description);
@@ -27721,15 +28789,45 @@ function questionFor(facts, description) {
27721
28789
  return `Halfcycle: ${whatOf(only)} ${reason} If you allow it, ${lostOf(only)} ${NOTHING_LOST_ON_NO}`;
27722
28790
  }
27723
28791
  const shown2 = staked.slice(0, MAX_LISTED).map((f) => `${whatOf(f)} If you allow it, ${lostOf(f)}`);
27724
- const more = staked.length - shown2.length;
27725
- const moreLine = more > 0 ? ` And ${more} more risky ${more === 1 ? "step" : "steps"}.` : "";
27726
- return `Halfcycle: ${shown2.join(" ")}${moreLine} ${reason} ${NOTHING_LOST_ON_NO}`;
28792
+ return `Halfcycle: ${shown2.join(" ")}${moreLine(staked.length - shown2.length)} ${reason} ${NOTHING_LOST_ON_NO}`;
28793
+ }
28794
+ function orderedStaked(facts) {
28795
+ const said = /* @__PURE__ */ new Set();
28796
+ return facts.filter((f) => f.atStake).map((f, i) => ({ f, i })).sort((a, b) => riskyTypeOrder(a.f.type) - riskyTypeOrder(b.f.type) || a.i - b.i).map(({ f }) => f).filter((f) => {
28797
+ const part = `${whatOf(f)} ${lostOf(f)}`;
28798
+ if (said.has(part))
28799
+ return false;
28800
+ said.add(part);
28801
+ return true;
28802
+ });
28803
+ }
28804
+ function moreLine(more) {
28805
+ return more > 0 ? ` And ${more} more risky ${more === 1 ? "step" : "steps"}.` : "";
28806
+ }
28807
+ var UNNAMED_PUSH_WHAT = "Replace a line of work on the shared copy.";
28808
+ function whatsText(facts, extraPush = false) {
28809
+ const staked = orderedStaked(facts);
28810
+ const items = staked.map((f) => ({ order: riskyTypeOrder(f.type), what: whatOf(f) }));
28811
+ const pushOrder = riskyTypeOrder("rewrite-shared-history");
28812
+ if (extraPush && !staked.some((f) => f.type === "rewrite-shared-history")) {
28813
+ const at = items.findIndex((x) => x.order > pushOrder);
28814
+ items.splice(at < 0 ? items.length : at, 0, { order: pushOrder, what: UNNAMED_PUSH_WHAT });
28815
+ }
28816
+ const shown2 = items.slice(0, MAX_LISTED).map((x) => x.what);
28817
+ return `${shown2.join(" ")}${moreLine(items.length - shown2.length)}`;
28818
+ }
28819
+ function typeSentenceFor(facts, extraPush = false) {
28820
+ const whats = whatsText(facts, extraPush);
28821
+ return whats === "" ? AUTONOMY_COULD_NOT_TELL_SENTENCE : `Halfcycle: ${whats}`;
28822
+ }
28823
+ function savedWhatFor(facts) {
28824
+ return whatsText(facts);
27727
28825
  }
27728
28826
  function refusalText(question) {
27729
28827
  return `${question} ${TURN_QUESTIONS_ON_LINE}`;
27730
28828
  }
27731
28829
  function couldNotTellQuestion(description) {
27732
- return `Halfcycle: This step can delete, overwrite, publish or show something that cannot be taken back, and Halfcycle could not work out what. ${reasonSentence(description)} If you allow it, whatever it changes may not be possible to bring back. ${NOTHING_LOST_ON_NO}`;
28830
+ return `${AUTONOMY_COULD_NOT_TELL_SENTENCE} ${reasonSentence(description)} If you allow it, whatever it changes may not be possible to bring back. ${NOTHING_LOST_ON_NO}`;
27733
28831
  }
27734
28832
  function whatOf(f) {
27735
28833
  switch (f.type) {
@@ -27811,7 +28909,7 @@ function pushWhat(f) {
27811
28909
  const verb = lines2.length > 0 && lines2.every((l) => l.deletes) ? "Delete" : "Replace";
27812
28910
  const named = lines2.filter((l) => l.branch !== null).map((l) => l.branch);
27813
28911
  if (named.length === 0)
27814
- return `${verb} a line of work on the shared copy.`;
28912
+ return verb === "Replace" ? UNNAMED_PUSH_WHAT : `${verb} a line of work on the shared copy.`;
27815
28913
  return `${verb} the shared copy's ${named.length === 1 ? "line" : "lines"} of work ${listNames(named)}.`;
27816
28914
  }
27817
28915
  function pushLost(f) {
@@ -28013,16 +29111,71 @@ function answerModeFor(permissionMode) {
28013
29111
  return typeof permissionMode === "string" && QUESTIONS_ON_MODES.has(permissionMode) ? "questions-on" : "questions-off";
28014
29112
  }
28015
29113
  var REFUSED_IN_EVERY_MODE = /* @__PURE__ */ new Set(["outside-own-copy"]);
28016
- function decide(action, facts, mode) {
29114
+ function autonomousRowOf(type) {
29115
+ switch (type) {
29116
+ case "delete-files":
29117
+ case "discard-unsaved":
29118
+ case "delete-unmerged-work":
29119
+ case "shrink-record":
29120
+ return "save";
29121
+ case "rewrite-shared-history":
29122
+ case "wipe-data":
29123
+ return "wait-item-1";
29124
+ case "publish":
29125
+ case "deploy":
29126
+ return "publish";
29127
+ case "read-credentials":
29128
+ return "never";
29129
+ case "outside-own-copy":
29130
+ return "every-mode";
29131
+ default: {
29132
+ const unhandled = type;
29133
+ return unhandled;
29134
+ }
29135
+ }
29136
+ }
29137
+ function savedByAutonomy(type) {
29138
+ return autonomousRowOf(type) === "save";
29139
+ }
29140
+ function ownerWaitItemNumber(id) {
29141
+ return OWNER_WAIT_ITEM_IDS.indexOf(id) + 1;
29142
+ }
29143
+ function decide(action, facts, mode, mayPublishNow = () => false) {
28017
29144
  void action;
28018
29145
  if (facts.some((f) => REFUSED_IN_EVERY_MODE.has(f.type)))
28019
29146
  return "refuse";
28020
- return mode === "questions-on" ? "ask" : "refuse";
29147
+ switch (mode) {
29148
+ case "questions-on":
29149
+ return "ask";
29150
+ case "questions-off":
29151
+ return "refuse";
29152
+ case "autonomous":
29153
+ return decideAutonomous(facts, mayPublishNow);
29154
+ default: {
29155
+ const unhandled = mode;
29156
+ void unhandled;
29157
+ return "refuse";
29158
+ }
29159
+ }
29160
+ }
29161
+ function decideAutonomous(facts, mayPublishNow) {
29162
+ if (facts.length === 0)
29163
+ return { autonomous: "could-not-tell" };
29164
+ const rows = [...facts].sort((a, b) => riskyTypeOrder(a.type) - riskyTypeOrder(b.type)).map((f) => autonomousRowOf(f.type));
29165
+ if (rows.includes("never"))
29166
+ return { autonomous: "never-under-autonomy" };
29167
+ const firstWait = rows.find((r) => r === "wait-item-1" || r === "publish");
29168
+ if (firstWait === "wait-item-1")
29169
+ return { autonomous: "waits-for-owner", item: ownerWaitItemNumber("delete-users-data") };
29170
+ if (firstWait === "publish" && !mayPublishNow()) {
29171
+ return { autonomous: "waits-for-owner", item: ownerWaitItemNumber("publish-or-release") };
29172
+ }
29173
+ return rows.includes("save") ? { autonomous: "save-then-let-through" } : { autonomous: "let-through" };
28021
29174
  }
28022
29175
 
28023
29176
  // dist/risky/own-copy.js
28024
- import { readFileSync as readFileSync18 } from "node:fs";
28025
- import { join as join12, posix as posix2, win32 as win322 } from "node:path";
29177
+ import { readFileSync as readFileSync20 } from "node:fs";
29178
+ import { join as join13, posix as posix2, win32 as win322 } from "node:path";
28026
29179
  var OWN_COPY_START_TOOL = "Agent";
28027
29180
  var OWN_COPY_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
28028
29181
  var OWN_COPY_ISOLATION = "worktree";
@@ -28043,7 +29196,7 @@ function stringField2(o, key) {
28043
29196
  }
28044
29197
  function readOr(deps, path) {
28045
29198
  try {
28046
- return (deps.readFile ?? ((p) => readFileSync18(p, "utf-8")))(path);
29199
+ return (deps.readFile ?? ((p) => readFileSync20(p, "utf-8")))(path);
28047
29200
  } catch {
28048
29201
  return null;
28049
29202
  }
@@ -28083,7 +29236,7 @@ function typeCanChangeFiles(subagentType, deps) {
28083
29236
  return true;
28084
29237
  if (AGENT_FILE_NAME.test(type)) {
28085
29238
  for (const base of [deps.projectDir, deps.home]) {
28086
- const text = readOr(deps, join12(base, ".claude", "agents", `${type}.md`));
29239
+ const text = readOr(deps, join13(base, ".claude", "agents", `${type}.md`));
28087
29240
  if (text === null)
28088
29241
  continue;
28089
29242
  const tools = frontmatterTools(text);
@@ -28107,94 +29260,597 @@ function partOf(top, rel, p, deps) {
28107
29260
  const owner = ownerOf(rel, checked.description);
28108
29261
  return checked.description.parts.find((p2) => p2.id === owner)?.name ?? null;
28109
29262
  } catch {
28110
- return null;
29263
+ return null;
29264
+ }
29265
+ }
29266
+ function isMainCopy(c) {
29267
+ return c.top === c.main;
29268
+ }
29269
+ function judgeOwnCopy(input, call, deps) {
29270
+ try {
29271
+ if (call.toolName === OWN_COPY_START_TOOL)
29272
+ return judgeStart(input, call, deps);
29273
+ if (OWN_COPY_WRITE_TOOLS.has(call.toolName))
29274
+ return judgeChange(input, call, deps);
29275
+ return PASS;
29276
+ } catch {
29277
+ return PASS;
29278
+ }
29279
+ }
29280
+ function judgeStart(input, call, deps) {
29281
+ if (call.toolInput["isolation"] === OWN_COPY_ISOLATION)
29282
+ return PASS;
29283
+ if (!typeCanChangeFiles(call.toolInput["subagent_type"], deps))
29284
+ return PASS;
29285
+ const cwd = stringField2(input, "cwd");
29286
+ if (cwd === null || whichCopy(cwd, deps.walk) === null)
29287
+ return UNKNOWN;
29288
+ const record2 = readLiveness(stringField2(input, "session_id"), cwd, deps.liveness);
29289
+ if (!runsBesideOtherWork(call.toolInput, withoutAgent(record2, stringField2(input, "agent_id"))))
29290
+ return PASS;
29291
+ return { kind: "refused", facts: { type: "outside-own-copy", atStake: true, arm: "start" } };
29292
+ }
29293
+ function judgeChange(input, call, deps) {
29294
+ const cwd = stringField2(input, "cwd");
29295
+ const own = cwd === null ? null : whichCopy(cwd, deps.walk);
29296
+ if (cwd === null || own === null)
29297
+ return UNKNOWN;
29298
+ const given = targetOf(call);
29299
+ if (given === null)
29300
+ return PASS;
29301
+ const rules = deps.walk?.rules ?? machineRules();
29302
+ const p = rules === "windows" ? win322 : posix2;
29303
+ const place = copyHolding(given, deps.walk);
29304
+ if (place === null || place.copy.common !== own.common)
29305
+ return PASS;
29306
+ const target = place.copy;
29307
+ const agentId = stringField2(input, "agent_id");
29308
+ let arm = null;
29309
+ let record2;
29310
+ const live = () => record2 === void 0 ? record2 = readLiveness(stringField2(input, "session_id"), cwd, deps.liveness) : record2;
29311
+ if (agentId !== null) {
29312
+ if (target.top !== own.top)
29313
+ arm = "other-copy";
29314
+ else {
29315
+ const rec = live();
29316
+ const mainTop = rec?.main?.top ?? (isMainCopy(own) ? own.top : null);
29317
+ const others = withoutAgent(rec, agentId);
29318
+ if (mainTop === own.top && others !== null && others.agents.length > 0)
29319
+ arm = "main-copy-while-others";
29320
+ }
29321
+ } else if (target.top !== own.top && (live()?.agents ?? []).some((a) => a.top === target.top)) {
29322
+ arm = "agents-copy";
29323
+ }
29324
+ if (arm === null)
29325
+ return PASS;
29326
+ const inFolder = place.folder.length > target.top.length ? place.folder.slice(target.top.length + 1) : "";
29327
+ const rel = [inFolder, place.rest].filter((s) => s !== "").join(p.sep).split(p.sep).join("/");
29328
+ return {
29329
+ kind: "refused",
29330
+ facts: {
29331
+ type: "outside-own-copy",
29332
+ atStake: true,
29333
+ arm,
29334
+ part: partOf(target.top, rel, p, deps),
29335
+ file: rel,
29336
+ ownPath: p.join(own.top, ...rel.split("/")),
29337
+ ownIsMain: isMainCopy(own),
29338
+ targetIsMain: isMainCopy(target)
29339
+ }
29340
+ };
29341
+ }
29342
+
29343
+ // dist/risky/save.js
29344
+ import { randomBytes } from "node:crypto";
29345
+ import { chmodSync as chmodSync2, constants as fsConstants, copyFileSync, lstatSync as lstatSync3, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync21, readlinkSync, realpathSync as realpathSync7, renameSync as renameSync10, rmSync as rmSync2, statSync as statSync8, symlinkSync, writeFileSync as writeFileSync12 } from "node:fs";
29346
+ import { homedir as homedir8 } from "node:os";
29347
+ import { dirname as dirname7, isAbsolute as isAbsolute3, join as join14, resolve as resolve5 } from "node:path";
29348
+ var SAVE_LIMIT_BYTES = 2e8;
29349
+ var SAVE_BUDGET_MS = 5e3;
29350
+ function notSaved(reason) {
29351
+ return { saved: false, reason };
29352
+ }
29353
+ function savedFolderOf(engagementId, home = homedir8()) {
29354
+ if (!isEngagementId(engagementId))
29355
+ return null;
29356
+ return join14(home, HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
29357
+ }
29358
+ function saveFirst(staked, ctx) {
29359
+ const now = ctx.now ?? Date.now;
29360
+ const deadline = now() + (ctx.budgetMs ?? SAVE_BUDGET_MS);
29361
+ const folder = savedFolderOf(ctx.engagementId, ctx.home);
29362
+ if (folder === null)
29363
+ return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29364
+ const plan = planOf(staked);
29365
+ if (!plan.ok)
29366
+ return notSaved(plan.reason);
29367
+ const g = new SaveGit(plan.root, deadline, now);
29368
+ const top = g.run(["rev-parse", "--show-toplevel"]);
29369
+ if (!top.ok || realOrSelf2(top.value.trim()) !== plan.root)
29370
+ return notSaved(AUTONOMY_SAVE_NOT_COVERED_REASON);
29371
+ const head = g.run(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
29372
+ const headCommit2 = head.ok ? head.value.trim() : "";
29373
+ if (!/^[0-9a-f]{40,64}$/.test(headCommit2))
29374
+ return notSaved(AUTONOMY_SAVE_NO_HISTORY_REASON);
29375
+ try {
29376
+ pruneSaves(plan.root, ctx.engagementId, { ...ctx.home === void 0 ? {} : { home: ctx.home }, now, deadline });
29377
+ } catch {
29378
+ }
29379
+ const found2 = filesOf(plan, g, deadline);
29380
+ if (!found2.ok)
29381
+ return notSaved(found2.reason);
29382
+ let saveDir = null;
29383
+ try {
29384
+ const id = freshId(folder, ctx.newId);
29385
+ if (id === null)
29386
+ return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29387
+ saveDir = join14(folder, id);
29388
+ const repository = repositoryOf(g);
29389
+ if (repository === null)
29390
+ return failed(saveDir);
29391
+ writeFileSync12(join14(saveDir, SAVED_REPOSITORY_FILE_NAME), `${repository}
29392
+ `, { mode: 384 });
29393
+ const privateIndex = join14(saveDir, PRIVATE_INDEX_NAME);
29394
+ const commit = saveCommit(id, headCommit2, found2.tracked, plan.lines, privateIndex, g, now());
29395
+ rmSync2(privateIndex, { force: true });
29396
+ if (commit === null)
29397
+ return failed(saveDir);
29398
+ const copiesDir = join14(saveDir, SAVED_COPIES_DIR_NAME);
29399
+ for (const rel of found2.copies) {
29400
+ if (now() > deadline || !copyOne(join14(plan.root, rel), join14(copiesDir, rel)))
29401
+ return failed(saveDir);
29402
+ }
29403
+ const description = savedSchema.safeParse({
29404
+ id,
29405
+ savedAt: new Date(now()).toISOString(),
29406
+ ref: savedRefOf(id),
29407
+ what: ctx.what.slice(0, 2e3),
29408
+ paths: [
29409
+ ...found2.tracked.map((path) => ({ path, keptIn: "commit" })),
29410
+ ...found2.copies.map((path) => ({ path, keptIn: "copy" }))
29411
+ ],
29412
+ branches: plan.lines.filter((l) => l.name !== null).map((l) => ({ name: l.name, commit: l.commit }))
29413
+ });
29414
+ if (!description.success)
29415
+ return failed(saveDir);
29416
+ const file2 = join14(saveDir, SAVED_FILE_NAME);
29417
+ writeFileSync12(`${file2}.part`, `${JSON.stringify(description.data, null, 2)}
29418
+ `, { mode: 384 });
29419
+ renameSync10(`${file2}.part`, file2);
29420
+ ctx.beforeRef?.();
29421
+ const ref = g.run(["update-ref", "--no-deref", savedRefOf(id), commit, ""]);
29422
+ if (!ref.ok)
29423
+ return failed(saveDir);
29424
+ return { saved: true, id, description: description.data };
29425
+ } catch {
29426
+ return saveDir === null ? notSaved(AUTONOMY_SAVE_FAILED_REASON) : failed(saveDir);
29427
+ }
29428
+ }
29429
+ var PRIVATE_INDEX_NAME = "index.part";
29430
+ function failed(saveDir) {
29431
+ try {
29432
+ rmSync2(saveDir, { recursive: true, force: true });
29433
+ } catch {
29434
+ }
29435
+ return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29436
+ }
29437
+ function planOf(staked) {
29438
+ if (staked.length === 0)
29439
+ return { ok: false, reason: AUTONOMY_COULD_NOT_TELL_REASON };
29440
+ let root = null;
29441
+ const specs = [];
29442
+ const lines2 = /* @__PURE__ */ new Map();
29443
+ for (const f of staked) {
29444
+ const t = f.save;
29445
+ if (t === void 0)
29446
+ return { ok: false, reason: f.cannotTell === void 0 ? AUTONOMY_SAVE_NOT_COVERED_REASON : AUTONOMY_COULD_NOT_TELL_REASON };
29447
+ const r = realOrSelf2(resolve5(t.repoRoot));
29448
+ if (root === null)
29449
+ root = r;
29450
+ else if (root !== r)
29451
+ return { ok: false, reason: AUTONOMY_SAVE_NOT_COVERED_REASON };
29452
+ for (const p of t.paths) {
29453
+ if (!isInsideRel(p))
29454
+ return { ok: false, reason: AUTONOMY_SAVE_NOT_COVERED_REASON };
29455
+ specs.push(t.keep === void 0 ? { path: p } : { path: p, keep: t.keep });
29456
+ }
29457
+ for (const l of t.lines)
29458
+ if (!lines2.has(l.commit) || l.name !== null)
29459
+ lines2.set(l.commit, l);
29460
+ }
29461
+ return { ok: true, root, specs, lines: [...lines2.values()] };
29462
+ }
29463
+ function isInsideRel(p) {
29464
+ if (p === ".")
29465
+ return true;
29466
+ if (p === "" || isAbsolute3(p) || p.includes("\\"))
29467
+ return false;
29468
+ return p.split("/").every((s) => s !== "" && s !== "." && s !== "..");
29469
+ }
29470
+ function under(rel, spec) {
29471
+ return spec === "." || rel === spec || rel.startsWith(`${spec}/`);
29472
+ }
29473
+ function reached(rel, specs) {
29474
+ return specs.some((s) => under(rel, s.path) && (s.keep === void 0 || s.keep(rel)));
29475
+ }
29476
+ function filesOf(plan, g, deadline) {
29477
+ if (plan.specs.length === 0)
29478
+ return { ok: true, tracked: [], copies: [] };
29479
+ const specs = [...new Set(plan.specs.map((s) => s.path))];
29480
+ const listed = g.run(["--literal-pathspecs", "ls-files", "-z", "--full-name", "--stage", "--", ...specs]);
29481
+ if (!listed.ok)
29482
+ return { ok: false, reason: AUTONOMY_SAVE_FAILED_REASON };
29483
+ const trackedAll = /* @__PURE__ */ new Set();
29484
+ const tracked = /* @__PURE__ */ new Set();
29485
+ for (const rec of listed.value.split("\0")) {
29486
+ const tab = rec.indexOf(" ");
29487
+ if (tab < 0)
29488
+ continue;
29489
+ const mode = rec.slice(0, rec.indexOf(" "));
29490
+ const path = rec.slice(tab + 1);
29491
+ if (mode === "160000")
29492
+ continue;
29493
+ trackedAll.add(path);
29494
+ if (!reached(path, plan.specs))
29495
+ continue;
29496
+ const st = lstatOrNull(join14(plan.root, path));
29497
+ if (st !== null && (st.isFile() || st.isSymbolicLink()))
29498
+ tracked.add(path);
29499
+ }
29500
+ const copies = /* @__PURE__ */ new Set();
29501
+ let bytes = 0;
29502
+ let seen = 0;
29503
+ const stack = [...specs];
29504
+ while (stack.length > 0) {
29505
+ const rel = stack.pop();
29506
+ if (rel === ".git")
29507
+ continue;
29508
+ const abs = rel === "." ? plan.root : join14(plan.root, rel);
29509
+ const st = lstatOrNull(abs);
29510
+ if (st === null)
29511
+ continue;
29512
+ if (st.isDirectory()) {
29513
+ let names;
29514
+ try {
29515
+ names = readdirSync9(abs);
29516
+ } catch {
29517
+ return { ok: false, reason: AUTONOMY_SAVE_FAILED_REASON };
29518
+ }
29519
+ for (const n of names) {
29520
+ const child = rel === "." ? n : `${rel}/${n}`;
29521
+ if (child !== ".git")
29522
+ stack.push(child);
29523
+ }
29524
+ } else if ((st.isFile() || st.isSymbolicLink()) && !trackedAll.has(rel) && !copies.has(rel) && reached(rel, plan.specs)) {
29525
+ copies.add(rel);
29526
+ bytes += st.size;
29527
+ if (bytes > SAVE_LIMIT_BYTES)
29528
+ return { ok: false, reason: AUTONOMY_SAVE_TOO_LARGE_REASON };
29529
+ }
29530
+ if (++seen % 1024 === 0 && Date.now() > deadline)
29531
+ return { ok: false, reason: AUTONOMY_SAVE_FAILED_REASON };
29532
+ }
29533
+ return { ok: true, tracked: [...tracked].sort(), copies: [...copies].sort() };
29534
+ }
29535
+ function copyOne(src, dst) {
29536
+ const st = lstatOrNull(src);
29537
+ if (st === null)
29538
+ return false;
29539
+ mkdirSync8(dirname7(dst), { recursive: true, mode: 448 });
29540
+ if (st.isSymbolicLink()) {
29541
+ symlinkSync(readlinkSync(src), dst);
29542
+ return true;
29543
+ }
29544
+ copyFileSync(src, dst, fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE);
29545
+ chmodSync2(dst, st.mode & 4095);
29546
+ return statSync8(dst).size === st.size;
29547
+ }
29548
+ function saveCommit(id, head, tracked, lines2, privateIndex, g, at) {
29549
+ const indexPath = g.run(["rev-parse", "--git-path", "index"]);
29550
+ if (!indexPath.ok)
29551
+ return null;
29552
+ const projectIndex = resolve5(g.root, indexPath.value.trim());
29553
+ const env = commitEnv(privateIndex, at);
29554
+ if (lstatOrNull(projectIndex) !== null)
29555
+ copyFileSync(projectIndex, privateIndex);
29556
+ else if (!g.run(["read-tree", head], env).ok)
29557
+ return null;
29558
+ const stagedTree = g.run(["write-tree"], env);
29559
+ if (!stagedTree.ok)
29560
+ return null;
29561
+ const staged = g.run(["commit-tree", "--no-gpg-sign", stagedTree.value.trim(), "-p", head, "-m", `Halfcycle save ${id}: staged changes`], env);
29562
+ if (!staged.ok)
29563
+ return null;
29564
+ if (tracked.length > 0 && !g.run(["update-index", "-z", "--stdin"], env, `${tracked.join("\0")}\0`).ok)
29565
+ return null;
29566
+ const tree = g.run(["write-tree"], env);
29567
+ if (!tree.ok)
29568
+ return null;
29569
+ const parents = [.../* @__PURE__ */ new Set([head, staged.value.trim(), ...lines2.map((l) => l.commit)])];
29570
+ const commit = g.run(["commit-tree", "--no-gpg-sign", tree.value.trim(), ...parents.flatMap((p) => ["-p", p]), "-m", `Halfcycle save ${id}`], env);
29571
+ if (!commit.ok)
29572
+ return null;
29573
+ const sha = commit.value.trim();
29574
+ return /^[0-9a-f]{40,64}$/.test(sha) ? sha : null;
29575
+ }
29576
+ function commitEnv(privateIndex, at) {
29577
+ const date5 = `${String(Math.floor(at / 1e3))} +0000`;
29578
+ return {
29579
+ GIT_INDEX_FILE: privateIndex,
29580
+ GIT_OPTIONAL_LOCKS: "0",
29581
+ GIT_AUTHOR_NAME: "Halfcycle",
29582
+ GIT_AUTHOR_EMAIL: "save@halfcycle.invalid",
29583
+ GIT_AUTHOR_DATE: date5,
29584
+ GIT_COMMITTER_NAME: "Halfcycle",
29585
+ GIT_COMMITTER_EMAIL: "save@halfcycle.invalid",
29586
+ GIT_COMMITTER_DATE: date5
29587
+ };
29588
+ }
29589
+ var LONGEST_CAP_MS = 2147483647;
29590
+ function wholeMsLeft(ms) {
29591
+ if (!(ms >= 1))
29592
+ return null;
29593
+ return Math.min(Math.floor(ms), LONGEST_CAP_MS);
29594
+ }
29595
+ var SaveGit = class {
29596
+ root;
29597
+ deadline;
29598
+ now;
29599
+ constructor(root, deadline, now) {
29600
+ this.root = root;
29601
+ this.deadline = deadline;
29602
+ this.now = now;
29603
+ }
29604
+ run(args2, env, input) {
29605
+ const left = wholeMsLeft(this.deadline - this.now());
29606
+ if (left === null)
29607
+ return { ok: false, message: "out of time" };
29608
+ return gitForSave(args2, this.root, left, {
29609
+ env: env ?? { GIT_OPTIONAL_LOCKS: "0" },
29610
+ ...input === void 0 ? {} : { input }
29611
+ });
29612
+ }
29613
+ };
29614
+ function freshId(folder, newId) {
29615
+ mkdirSync8(folder, { recursive: true, mode: 448 });
29616
+ for (let i = 0; i < 5; i++) {
29617
+ const id = newId?.() ?? defaultId();
29618
+ if (!savedIdSchema.safeParse(id).success)
29619
+ return null;
29620
+ try {
29621
+ mkdirSync8(join14(folder, id), { mode: 448 });
29622
+ return id;
29623
+ } catch (e) {
29624
+ if (e.code !== "EEXIST")
29625
+ return null;
29626
+ }
29627
+ }
29628
+ return null;
29629
+ }
29630
+ function defaultId() {
29631
+ const d = /* @__PURE__ */ new Date();
29632
+ const two = (n) => String(n).padStart(2, "0");
29633
+ const stamp = `${String(d.getUTCFullYear())}${two(d.getUTCMonth() + 1)}${two(d.getUTCDate())}-${two(d.getUTCHours())}${two(d.getUTCMinutes())}${two(d.getUTCSeconds())}`;
29634
+ const rand = [...randomBytes(4)].map((b) => "abcdefghijklmnopqrstuvwxyz0123456789"[b % 36]).join("");
29635
+ return `${stamp}-${rand}`;
29636
+ }
29637
+ function repositoryOf(g) {
29638
+ const r = g.run(["rev-parse", "--git-common-dir"]);
29639
+ if (!r.ok)
29640
+ return null;
29641
+ const dir = r.value.trim();
29642
+ return dir === "" ? null : realOrSelf2(resolve5(g.root, dir));
29643
+ }
29644
+ function markedRepository(dir) {
29645
+ try {
29646
+ const text = readFileSync21(join14(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
29647
+ return text === "" ? null : text;
29648
+ } catch {
29649
+ return null;
29650
+ }
29651
+ }
29652
+ function pruneSaves(root, engagementId, ctx = {}) {
29653
+ const folder = savedFolderOf(engagementId, ctx.home);
29654
+ if (folder === null)
29655
+ return;
29656
+ const now = ctx.now ?? Date.now;
29657
+ const g = new SaveGit(root, ctx.deadline ?? now() + SAVE_BUDGET_MS, now);
29658
+ const listed = g.run(["for-each-ref", "--format=%(refname)%00%(committerdate:unix)", SAVED_REF_PREFIX]);
29659
+ if (!listed.ok)
29660
+ return;
29661
+ const refs = /* @__PURE__ */ new Map();
29662
+ for (const line2 of listed.value.split("\n")) {
29663
+ const [name, when] = line2.split("\0");
29664
+ if (name === void 0 || when === void 0 || !name.startsWith(SAVED_REF_PREFIX))
29665
+ continue;
29666
+ refs.set(name.slice(SAVED_REF_PREFIX.length), Number(when) * 1e3);
29667
+ }
29668
+ const repository = repositoryOf(g);
29669
+ let names = [];
29670
+ try {
29671
+ names = readdirSync9(folder);
29672
+ } catch {
29673
+ names = [];
29674
+ }
29675
+ const here = /* @__PURE__ */ new Set();
29676
+ for (const id of names) {
29677
+ if (!savedIdSchema.safeParse(id).success)
29678
+ continue;
29679
+ here.add(id);
29680
+ const dir = join14(folder, id);
29681
+ const st = lstatOrNull(dir);
29682
+ const answer = savedPruneAnswer({
29683
+ refMadeAt: refs.get(id) ?? null,
29684
+ folderChangedAt: st !== null && st.isDirectory() ? st.mtimeMs : null,
29685
+ folderRepository: markedRepository(dir)
29686
+ }, { repository, now: now() });
29687
+ if (answer === "remove-folder")
29688
+ rmSync2(dir, { recursive: true, force: true });
29689
+ else if (answer === "remove-ref-then-folder" && g.run(["update-ref", "-d", savedRefOf(id)]).ok) {
29690
+ rmSync2(dir, { recursive: true, force: true });
29691
+ } else if (answer === "remove-ref")
29692
+ g.run(["update-ref", "-d", savedRefOf(id)]);
29693
+ }
29694
+ for (const [id, madeAt] of refs) {
29695
+ if (here.has(id) || !savedIdSchema.safeParse(id).success)
29696
+ continue;
29697
+ const answer = savedPruneAnswer({ refMadeAt: madeAt, folderChangedAt: null, folderRepository: null }, { repository, now: now() });
29698
+ if (answer === "remove-ref")
29699
+ g.run(["update-ref", "-d", savedRefOf(id)]);
29700
+ }
29701
+ }
29702
+ function lstatOrNull(p) {
29703
+ try {
29704
+ return lstatSync3(p);
29705
+ } catch {
29706
+ return null;
29707
+ }
29708
+ }
29709
+ function realOrSelf2(p) {
29710
+ try {
29711
+ return realpathSync7(p);
29712
+ } catch {
29713
+ return p;
29714
+ }
29715
+ }
29716
+
29717
+ // dist/autonomy-state.js
29718
+ import { spawnSync as spawnSync2 } from "node:child_process";
29719
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync22, renameSync as renameSync11, rmSync as rmSync3, writeFileSync as writeFileSync13 } from "node:fs";
29720
+ import { homedir as homedir9, platform as platform2 } from "node:os";
29721
+ import { dirname as dirname8, join as join15 } from "node:path";
29722
+ var AUTONOMY_STATE_FILENAME = "autonomy.json";
29723
+ var AUTONOMY_STATE_DEADLINE_MS = 1500;
29724
+ function autonomyStateFilePath(engagementId, home) {
29725
+ return join15(home ?? homedir9(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, AUTONOMY_STATE_FILENAME);
29726
+ }
29727
+ function readCachedAutonomy(engagementId, nowMs, home) {
29728
+ if (!isEngagementId(engagementId))
29729
+ return null;
29730
+ try {
29731
+ const parsed = autonomyStateFileSchema.safeParse(JSON.parse(readFileSync22(autonomyStateFilePath(engagementId, home), "utf-8")));
29732
+ if (!parsed.success || !parsed.data.on)
29733
+ return null;
29734
+ const age = nowMs - Date.parse(parsed.data.readAt);
29735
+ return Number.isFinite(age) && age >= 0 && age < AUTONOMY_STATE_FILE_MAX_AGE_MS ? parsed.data : null;
29736
+ } catch {
29737
+ return null;
29738
+ }
29739
+ }
29740
+ function confirmMayPublishNow(opts) {
29741
+ if (!isEngagementId(opts.engagementId) || CREDENTIAL_PLACEHOLDERS.includes(opts.bearer))
29742
+ return false;
29743
+ if (opts.timeoutMs <= 0)
29744
+ return false;
29745
+ try {
29746
+ const url2 = `${opts.controlOrigin}/engagements/${encodeURIComponent(opts.engagementId)}/autonomy/state`;
29747
+ const r = spawnSync2(process.execPath, ["--input-type=module", "-e", LIVE_READ_PROGRAM], {
29748
+ input: JSON.stringify({ url: url2, bearer: opts.bearer, ms: Math.max(1, opts.timeoutMs - 250) }),
29749
+ encoding: "utf-8",
29750
+ timeout: opts.timeoutMs,
29751
+ windowsHide: true
29752
+ });
29753
+ if (r.status !== 0 || typeof r.stdout !== "string")
29754
+ return false;
29755
+ const parsed = autonomyStateSchema.safeParse(JSON.parse(r.stdout));
29756
+ return parsed.success && parsed.data.on && parsed.data.mayPublish;
29757
+ } catch {
29758
+ return false;
28111
29759
  }
28112
29760
  }
28113
- function isMainCopy(c) {
28114
- return c.top === c.main;
29761
+ var LIVE_READ_PROGRAM = [
29762
+ "let t='';process.stdin.setEncoding('utf8');",
29763
+ "process.stdin.on('data',(c)=>{t+=c;});",
29764
+ "process.stdin.on('end',async()=>{try{const o=JSON.parse(t);",
29765
+ "const r=await fetch(o.url,{method:'GET',headers:{Authorization:'Bearer '+o.bearer},redirect:'manual',signal:AbortSignal.timeout(o.ms)});",
29766
+ "if(r.status<200||r.status>299)process.exit(1);process.stdout.write(JSON.stringify(await r.json()));process.exit(0);",
29767
+ "}catch{process.exit(1);}});"
29768
+ ].join("");
29769
+ function deleteAutonomyStateFile(engagementId, home) {
29770
+ try {
29771
+ rmSync3(autonomyStateFilePath(engagementId, home), { force: true });
29772
+ } catch {
29773
+ }
28115
29774
  }
28116
- function judgeOwnCopy(input, call, deps) {
29775
+ function writeAutonomyStateFile(engagementId, file2, home) {
29776
+ const target = autonomyStateFilePath(engagementId, home);
29777
+ const temp = join15(dirname8(target), `.${AUTONOMY_STATE_FILENAME}.${process.pid}.tmp`);
28117
29778
  try {
28118
- if (call.toolName === OWN_COPY_START_TOOL)
28119
- return judgeStart(input, call, deps);
28120
- if (OWN_COPY_WRITE_TOOLS.has(call.toolName))
28121
- return judgeChange(input, call, deps);
28122
- return PASS;
29779
+ mkdirSync9(dirname8(target), { recursive: true, mode: 448 });
29780
+ writeFileSync13(temp, `${JSON.stringify(file2)}
29781
+ `, { mode: 384 });
29782
+ if (platform2() !== "win32")
29783
+ chmodSync3(temp, 384);
29784
+ renameSync11(temp, target);
29785
+ return true;
28123
29786
  } catch {
28124
- return PASS;
29787
+ try {
29788
+ rmSync3(temp, { force: true });
29789
+ } catch {
29790
+ }
29791
+ return false;
28125
29792
  }
28126
29793
  }
28127
- function judgeStart(input, call, deps) {
28128
- if (call.toolInput["isolation"] === OWN_COPY_ISOLATION)
28129
- return PASS;
28130
- if (!typeCanChangeFiles(call.toolInput["subagent_type"], deps))
28131
- return PASS;
28132
- const cwd = stringField2(input, "cwd");
28133
- if (cwd === null || whichCopy(cwd, deps.walk) === null)
28134
- return UNKNOWN;
28135
- const record2 = readLiveness(stringField2(input, "session_id"), cwd, deps.liveness);
28136
- if (!runsBesideOtherWork(call.toolInput, withoutAgent(record2, stringField2(input, "agent_id"))))
28137
- return PASS;
28138
- return { kind: "refused", facts: { type: "outside-own-copy", atStake: true, arm: "start" } };
29794
+ async function readAutonomyStateFromControl(opts) {
29795
+ const controller = new AbortController();
29796
+ const timer = setTimeout(() => controller.abort(), opts.deadlineMs);
29797
+ try {
29798
+ const url2 = `${opts.controlOrigin}/engagements/${encodeURIComponent(opts.engagementId)}/autonomy/state`;
29799
+ const res = await opts.fetchFn(url2, {
29800
+ method: "GET",
29801
+ headers: { Authorization: `Bearer ${opts.bearer}` },
29802
+ redirect: "manual",
29803
+ signal: controller.signal
29804
+ });
29805
+ if (res.status < 200 || res.status > 299)
29806
+ return void 0;
29807
+ const parsed = autonomyStateSchema.safeParse(await res.json());
29808
+ return parsed.success ? parsed.data : void 0;
29809
+ } catch {
29810
+ return void 0;
29811
+ } finally {
29812
+ clearTimeout(timer);
29813
+ }
28139
29814
  }
28140
- function judgeChange(input, call, deps) {
28141
- const cwd = stringField2(input, "cwd");
28142
- const own = cwd === null ? null : whichCopy(cwd, deps.walk);
28143
- if (cwd === null || own === null)
28144
- return UNKNOWN;
28145
- const given = targetOf(call);
28146
- if (given === null)
28147
- return PASS;
28148
- const rules = deps.walk?.rules ?? machineRules();
28149
- const p = rules === "windows" ? win322 : posix2;
28150
- const place = copyHolding(given, deps.walk);
28151
- if (place === null || place.copy.common !== own.common)
28152
- return PASS;
28153
- const target = place.copy;
28154
- const agentId = stringField2(input, "agent_id");
28155
- let arm = null;
28156
- let record2;
28157
- const live = () => record2 === void 0 ? record2 = readLiveness(stringField2(input, "session_id"), cwd, deps.liveness) : record2;
28158
- if (agentId !== null) {
28159
- if (target.top !== own.top)
28160
- arm = "other-copy";
28161
- else {
28162
- const rec = live();
28163
- const mainTop = rec?.main?.top ?? (isMainCopy(own) ? own.top : null);
28164
- const others = withoutAgent(rec, agentId);
28165
- if (mainTop === own.top && others !== null && others.agents.length > 0)
28166
- arm = "main-copy-while-others";
28167
- }
28168
- } else if (target.top !== own.top && (live()?.agents ?? []).some((a) => a.top === target.top)) {
28169
- arm = "agents-copy";
29815
+ async function cacheAutonomySetting(opts) {
29816
+ const { engagementId, home } = opts;
29817
+ if (!isEngagementId(engagementId))
29818
+ return;
29819
+ const bearer = opts.bearer;
29820
+ const usable = bearer !== void 0 && !CREDENTIAL_PLACEHOLDERS.includes(bearer) && opts.deadlineMs > 0;
29821
+ const state = usable ? await readAutonomyStateFromControl({
29822
+ controlOrigin: opts.controlOrigin,
29823
+ engagementId,
29824
+ bearer,
29825
+ fetchFn: opts.fetchFn,
29826
+ deadlineMs: opts.deadlineMs
29827
+ }) : void 0;
29828
+ if (state === void 0) {
29829
+ deleteAutonomyStateFile(engagementId, home);
29830
+ return;
29831
+ }
29832
+ const file2 = autonomyStateFileSchema.safeParse({ ...state, readAt: (/* @__PURE__ */ new Date()).toISOString() });
29833
+ if (!file2.success || !writeAutonomyStateFile(engagementId, file2.data, home)) {
29834
+ deleteAutonomyStateFile(engagementId, home);
28170
29835
  }
28171
- if (arm === null)
28172
- return PASS;
28173
- const inFolder = place.folder.length > target.top.length ? place.folder.slice(target.top.length + 1) : "";
28174
- const rel = [inFolder, place.rest].filter((s) => s !== "").join(p.sep).split(p.sep).join("/");
28175
- return {
28176
- kind: "refused",
28177
- facts: {
28178
- type: "outside-own-copy",
28179
- atStake: true,
28180
- arm,
28181
- part: partOf(target.top, rel, p, deps),
28182
- file: rel,
28183
- ownPath: p.join(own.top, ...rel.split("/")),
28184
- ownIsMain: isMainCopy(own),
28185
- targetIsMain: isMainCopy(target)
28186
- }
28187
- };
28188
29836
  }
28189
29837
 
28190
29838
  // dist/hooks/pre-tool-use.js
28191
- import { homedir as homedir8 } from "node:os";
29839
+ import { performance } from "node:perf_hooks";
29840
+ import { homedir as homedir10 } from "node:os";
28192
29841
  var UNREADABLE_INPUT_NOTICE = "Halfcycle could not read what the coding agent was about to do, so that step went ahead without being checked for anything that cannot be undone. If this keeps happening, update Halfcycle.";
29842
+ var PRE_TOOL_USE_LIMIT_MS = (HALFCYCLE_HOOK_WIRING.find((e) => e.event === "PreToolUse")?.timeout ?? 10) * 1e3;
29843
+ var AUTONOMY_REPLY_MARGIN_MS = 1e3;
29844
+ var MAY_PUBLISH_READ_MS = 3e3;
28193
29845
  var OWN_COPY_UNKNOWN_NOTICE = "Halfcycle could not tell which copy of the project this agent works in, so it let the step go ahead without checking that it stays in its own copy.";
28194
29846
  var CHECKED = new Set(CHECKED_TOOLS);
29847
+ var RECORDED_ONLY = new Set(RECORDED_ONLY_TOOLS);
28195
29848
  function runPreToolUse(deps) {
28196
29849
  const input = parseObject(deps.input);
28197
- recordTimeEvent("action", input ?? {}, deps);
29850
+ const at = clockOf(deps)();
29851
+ const stamped = { ...deps, now: () => at };
29852
+ recordTimeEvent("action", input ?? {}, stamped);
29853
+ recordOwnShell(input, at, { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir } });
28198
29854
  const seen = recordLiveness(input ?? {}, deps.liveness);
28199
29855
  const paceDeps = {
28200
29856
  ...deps.pace ?? {},
@@ -28204,6 +29860,10 @@ function runPreToolUse(deps) {
28204
29860
  };
28205
29861
  if (seen?.firstSeen === true && input !== null && typeof input["agent_id"] === "string")
28206
29862
  retirePendingStart(input, paceDeps);
29863
+ const { mode, autonomy } = answerModeOf(input, deps, at);
29864
+ const toolName = input === null ? void 0 : stringOf(input, "tool_name");
29865
+ if (toolName !== void 0 && RECORDED_ONLY.has(toolName))
29866
+ return recordedOnlyReply(mode);
28207
29867
  const call = input === null ? null : toolCallOf(input);
28208
29868
  if (call === null || !CHECKED.has(call.toolName))
28209
29869
  return unreadable(deps, input);
@@ -28226,7 +29886,6 @@ function runPreToolUse(deps) {
28226
29886
  }
28227
29887
  const own = ownCopyOf(input, call, deps);
28228
29888
  try {
28229
- const mode = answerModeFor(input["permission_mode"]);
28230
29889
  if (own.kind === "refused") {
28231
29890
  const staked = [...answer.silent ? [] : answer.staked, own.facts];
28232
29891
  if (decide(call, staked, mode) === "refuse") {
@@ -28235,6 +29894,11 @@ function runPreToolUse(deps) {
28235
29894
  `;
28236
29895
  }
28237
29896
  }
29897
+ if (mode === "autonomous") {
29898
+ const sendsSaves = call.toolName === "Bash" && typeof call.toolInput["command"] === "string" && pushesSavedRefs(call.toolInput["command"]);
29899
+ if (!answer.silent || sendsSaves)
29900
+ return autonomousReply(call, answer.silent ? [] : answer.staked, sendsSaves, mode, autonomy, deps);
29901
+ }
28238
29902
  if (answer.silent) {
28239
29903
  if (call.toolName === PACE_HELD_TOOL) {
28240
29904
  const held = paceHoldText(input, call.toolInput, paceDeps);
@@ -28255,12 +29919,100 @@ function runPreToolUse(deps) {
28255
29919
  return "";
28256
29920
  }
28257
29921
  }
29922
+ function clockOf(deps) {
29923
+ return deps.now ?? (() => /* @__PURE__ */ new Date());
29924
+ }
29925
+ function answerModeOf(input, deps, at) {
29926
+ const engagementId = readConfig(deps.env ?? process.env).config?.guardEngagementId;
29927
+ const cached2 = engagementId === void 0 ? null : readCachedAutonomy(engagementId, at.getTime(), deps.home);
29928
+ if (engagementId !== void 0 && cached2 !== null)
29929
+ return { mode: "autonomous", autonomy: { engagementId, mayPublish: cached2.mayPublish } };
29930
+ return { mode: answerModeFor(input === null ? void 0 : input["permission_mode"]), autonomy: null };
29931
+ }
29932
+ function autonomousReply(call, staked, sendsSaves, mode, autonomy, deps) {
29933
+ try {
29934
+ return autonomousAnswer(call, staked, sendsSaves, mode, autonomy, deps);
29935
+ } catch {
29936
+ return `${preToolUseReplyText("refuse", autonomyA12(typeSentenceFor(staked, sendsSaves), AUTONOMY_SAVE_FAILED_REASON))}
29937
+ `;
29938
+ }
29939
+ }
29940
+ function autonomousAnswer(call, staked, sendsSaves, mode, autonomy, deps) {
29941
+ const startedAt = deps.autonomy?.startedAtMs ?? performance.timeOrigin;
29942
+ const left = () => Math.floor(startedAt + PRE_TOOL_USE_LIMIT_MS - AUTONOMY_REPLY_MARGIN_MS - Date.now());
29943
+ const mayPublishNow = () => {
29944
+ if (!autonomy.mayPublish)
29945
+ return false;
29946
+ if (deps.autonomy?.mayPublishNow !== void 0)
29947
+ return deps.autonomy.mayPublishNow();
29948
+ const env = deps.env ?? process.env;
29949
+ const config2 = readConfig(env).config;
29950
+ if (config2 === null)
29951
+ return false;
29952
+ return confirmMayPublishNow({
29953
+ controlOrigin: readControlOrigin(env).origin,
29954
+ engagementId: autonomy.engagementId,
29955
+ bearer: config2.guardServiceToken,
29956
+ timeoutMs: Math.min(MAY_PUBLISH_READ_MS, left())
29957
+ });
29958
+ };
29959
+ const decided = decide(call, sendsSaves ? [...staked, { type: "rewrite-shared-history" }] : staked, mode, mayPublishNow);
29960
+ const refuse = (text) => `${preToolUseReplyText("refuse", text)}
29961
+ `;
29962
+ if (decided === "ask" || decided === "refuse")
29963
+ return refuse(autonomyA12(typeSentenceFor([]), AUTONOMY_COULD_NOT_TELL_REASON));
29964
+ switch (decided.autonomous) {
29965
+ case "waits-for-owner":
29966
+ return refuse(autonomyA10(typeSentenceFor(staked, sendsSaves), decided.item));
29967
+ case "never-under-autonomy":
29968
+ return refuse(autonomyA11(typeSentenceFor(staked, sendsSaves)));
29969
+ case "could-not-tell":
29970
+ return refuse(autonomyA12(typeSentenceFor([]), AUTONOMY_COULD_NOT_TELL_REASON));
29971
+ case "let-through":
29972
+ return "";
29973
+ case "save-then-let-through": {
29974
+ const kept = staked.filter((f) => savedByAutonomy(f.type));
29975
+ const what = savedWhatFor(kept);
29976
+ const budgetMs = Math.min(SAVE_BUDGET_MS, left());
29977
+ const saved = budgetMs <= 0 ? { saved: false, reason: AUTONOMY_SAVE_FAILED_REASON } : saveFirst(kept, {
29978
+ engagementId: autonomy.engagementId,
29979
+ what,
29980
+ budgetMs,
29981
+ ...deps.home === void 0 ? {} : { home: deps.home },
29982
+ ...deps.autonomy?.newId === void 0 ? {} : { newId: deps.autonomy.newId },
29983
+ ...deps.autonomy?.beforeRef === void 0 ? {} : { beforeRef: deps.autonomy.beforeRef }
29984
+ });
29985
+ if (!saved.saved)
29986
+ return refuse(autonomyA12(typeSentenceFor(saved.reason === AUTONOMY_COULD_NOT_TELL_REASON ? [] : staked), saved.reason));
29987
+ return `${preToolUseNoticeText(autonomyA9(what, saved.id))}
29988
+ `;
29989
+ }
29990
+ default: {
29991
+ const unhandled = decided;
29992
+ return unhandled;
29993
+ }
29994
+ }
29995
+ }
29996
+ function recordedOnlyReply(mode) {
29997
+ switch (mode) {
29998
+ case "questions-on":
29999
+ case "questions-off":
30000
+ return "";
30001
+ case "autonomous":
30002
+ return `${preToolUseReplyText("refuse", AUTONOMY_A18)}
30003
+ `;
30004
+ default: {
30005
+ const unhandled = mode;
30006
+ return unhandled;
30007
+ }
30008
+ }
30009
+ }
28258
30010
  function ownCopyOf(input, call, deps) {
28259
30011
  try {
28260
30012
  const walk2 = deps.walk ?? deps.liveness?.walk;
28261
30013
  return judgeOwnCopy(input, call, {
28262
30014
  projectDir: deps.projectDir ?? projectFolder(deps.env ?? process.env),
28263
- home: deps.home ?? homedir8(),
30015
+ home: deps.home ?? homedir10(),
28264
30016
  ...walk2 === void 0 ? {} : { walk: walk2 },
28265
30017
  ...deps.liveness === void 0 ? {} : { liveness: deps.liveness }
28266
30018
  });
@@ -28300,6 +30052,8 @@ function unreadable(deps, input) {
28300
30052
  }
28301
30053
 
28302
30054
  // dist/hooks/session-diff.js
30055
+ import { unlinkSync as unlinkSync9 } from "node:fs";
30056
+ import { join as join16 } from "node:path";
28303
30057
  var STOP_HOOK_EVENT = {
28304
30058
  stop: "Stop",
28305
30059
  "subagent-stop": "SubagentStop"
@@ -28351,6 +30105,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
28351
30105
  }
28352
30106
  const repoRoot = repoRootResult.value;
28353
30107
  const sessionId = input.session_id;
30108
+ const folder = realPathOf(repoRoot);
28354
30109
  orphanSweep(repoRoot, (ref) => refExists(ref, repoRoot), sessionId, Date.now(), whichCopy(process.cwd())?.common ?? null);
28355
30110
  const base = resolveDiffBase2(repoRoot, sessionId);
28356
30111
  const diffBase = base.kind;
@@ -28359,9 +30114,16 @@ async function evaluateSession(input, kind, reply, readSeat) {
28359
30114
  reply.notice(localFaultText(`Could not get session diff: ${collected.message}`));
28360
30115
  return;
28361
30116
  }
28362
- const { getDiff } = collected;
28363
- const { evaluated: changedFiles2 } = partitionInstallerOutput(collected.files);
30117
+ const tracked = new Set(collected.files);
30118
+ const candidates = ownEditedFiles(repoRoot, sessionId).filter((relPath) => !tracked.has(relPath));
30119
+ const untracked = getUntrackedFiles(repoRoot, candidates);
30120
+ const created = untracked.ok ? untracked.value.filter((relPath) => !tracked.has(relPath)) : [];
30121
+ const getDiff = (relPath) => tracked.has(relPath) ? collected.getDiff(relPath) : getSingleFileDiff(join16(repoRoot, relPath), repoRoot);
30122
+ const { evaluated } = partitionInstallerOutput(collected.files);
30123
+ const { evaluated: createdEvaluated } = partitionInstallerOutput(created);
30124
+ const changedFiles2 = [...ownChangedFiles(repoRoot, evaluated, sessionId), ...createdEvaluated];
28364
30125
  if (changedFiles2.length === 0) {
30126
+ queueTurnEnd(keepBooks(sessionId, folder, { kind: "no-own-changes" }, kind === "stop"), reply);
28365
30127
  return;
28366
30128
  }
28367
30129
  const fileDiffEntries = changedFiles2.map((relPath) => ({
@@ -28372,6 +30134,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
28372
30134
  const digest = digestChangeState(baseKey, fileDiffEntries);
28373
30135
  const prior = readDedupe(repoRoot, sessionId);
28374
30136
  if (prior && prior.base === baseKey && prior.digest === digest) {
30137
+ queueTurnEnd(keepBooks(sessionId, folder, { kind: "unchanged" }, kind === "stop"), reply);
28375
30138
  return;
28376
30139
  }
28377
30140
  const context = loadChangeSetContext(repoRoot);
@@ -28450,30 +30213,56 @@ async function evaluateSession(input, kind, reply, readSeat) {
28450
30213
  warn: (msg) => process.stderr.write(msg + "\n")
28451
30214
  });
28452
30215
  writeDedupe(repoRoot, sessionId, { base: baseKey, digest });
28453
- actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true, reply);
30216
+ actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true, reply, sessionId, folder);
30217
+ }
30218
+ function keepBooks(sessionId, folder, outcome, topLevel) {
30219
+ const books = () => {
30220
+ const before = readFindings(sessionId);
30221
+ const { file: file2, news } = turnEndBookkeeping(before ?? emptyFindings(""), folder, outcome, topLevel, /* @__PURE__ */ new Date());
30222
+ if (before !== null && JSON.stringify(before) !== JSON.stringify(file2)) {
30223
+ if (file2.findings.length > 0)
30224
+ writeFindings(file2);
30225
+ else
30226
+ removeFindingsFile(sessionId);
30227
+ }
30228
+ return turnEndMessage(news);
30229
+ };
30230
+ const locked = withFindingsLock(sessionId, books);
30231
+ return locked === void 0 ? books() : locked;
28454
30232
  }
28455
- function actOnEnvelope3(envelope, kind, neverBlock, reply) {
28456
- const notRun = notRunReport(envelope.notRun);
28457
- const notRunBlock = notRun ? `
28458
- ${notRun}` : "";
28459
- if (envelope.guardsFired.length === 0) {
28460
- if (notRun)
28461
- reply.notice(notRun);
30233
+ function removeFindingsFile(sessionId) {
30234
+ const path = findingsPath(sessionId);
30235
+ if (path === null)
28462
30236
  return;
30237
+ try {
30238
+ unlinkSync9(path);
30239
+ } catch {
28463
30240
  }
28464
- const where = kind === "stop" ? "session stop" : "session stop";
28465
- const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
30241
+ }
30242
+ function queueTurnEnd(words, reply) {
30243
+ if (words !== null)
30244
+ reply.notice(words);
30245
+ }
30246
+ function actOnEnvelope3(envelope, kind, neverBlock, reply, sessionId, folder) {
30247
+ const notRun = notRunReport(envelope.notRun);
30248
+ const outcome = {
30249
+ kind: "evaluated",
30250
+ fired: envelope.guardsFired,
30251
+ notRun: (envelope.notRun ?? []).map((n) => n.patternRef)
30252
+ };
30253
+ const topLevel = kind === "stop";
28466
30254
  if (envelope.blocking && !neverBlock) {
28467
- reply.block(`Guard blocked at ${where}: ${envelope.explanation}
28468
- ${guardLines}${notRunBlock}`);
28469
- } else if (envelope.blocking && neverBlock) {
28470
- reply.notice(`[Halfcycle] Guard would block at ${where} but the stop was already continued (stop_hook_active) \u2014 surfacing as a warning:
28471
- ${envelope.explanation}
28472
- ${guardLines}${notRunBlock}`);
28473
- } else {
28474
- reply.notice(`[Halfcycle] Guard warning at ${where}: ${envelope.explanation}
28475
- ${guardLines}${notRunBlock}`);
30255
+ keepBooks(sessionId, folder, outcome, false);
30256
+ const reason = blockReason(envelope.guardsFired.map((g) => g.explanation));
30257
+ reply.block(notRun ? `${reason}
30258
+ ${notRun}` : reason, stoppedMessage(blockingExplanations(envelope.guardsFired)));
30259
+ }
30260
+ if (envelope.blocking && neverBlock) {
30261
+ reply.notice(couldNotFixMessage(blockingExplanations(envelope.guardsFired)));
28476
30262
  }
30263
+ queueTurnEnd(keepBooks(sessionId, folder, outcome, topLevel), reply);
30264
+ if (notRun)
30265
+ reply.notice(notRun);
28477
30266
  }
28478
30267
 
28479
30268
  // dist/hooks/stdin.js
@@ -28492,9 +30281,9 @@ async function readHookStdin() {
28492
30281
  }
28493
30282
  }
28494
30283
  function readStdin2() {
28495
- return new Promise((resolve6) => {
30284
+ return new Promise((resolve8) => {
28496
30285
  if (process.stdin.isTTY) {
28497
- resolve6("");
30286
+ resolve8("");
28498
30287
  return;
28499
30288
  }
28500
30289
  const chunks = [];
@@ -28504,7 +30293,7 @@ function readStdin2() {
28504
30293
  return;
28505
30294
  settled = true;
28506
30295
  clearTimeout(timer);
28507
- resolve6(Buffer.concat(chunks).toString("utf-8"));
30296
+ resolve8(Buffer.concat(chunks).toString("utf-8"));
28508
30297
  };
28509
30298
  const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
28510
30299
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -28584,6 +30373,22 @@ function matchScopePhraseShape(shape, shown2) {
28584
30373
  return { kind: "show-first", act: "send-back" };
28585
30374
  }
28586
30375
 
30376
+ // dist/hooks/autonomy-phrase.js
30377
+ var AUTONOMY_ON = /^autonomy on$/i;
30378
+ var AUTONOMY_OFF = /^autonomy off$/i;
30379
+ function autonomyPhraseShape(prompt) {
30380
+ if (typeof prompt !== "string")
30381
+ return null;
30382
+ const line2 = prompt.trim();
30383
+ if (LINE_BREAK.test(line2))
30384
+ return null;
30385
+ if (AUTONOMY_ON.test(line2))
30386
+ return "on";
30387
+ if (AUTONOMY_OFF.test(line2))
30388
+ return "off";
30389
+ return null;
30390
+ }
30391
+
28587
30392
  // dist/hooks/review-phrase.js
28588
30393
  var VERBS = [
28589
30394
  [/^accept /i, "accept"],
@@ -28650,20 +30455,20 @@ function matchReviewPhraseShape(shape, reviews) {
28650
30455
  }
28651
30456
 
28652
30457
  // dist/hooks/shown-record.js
28653
- import { chmodSync as chmodSync2, readFileSync as readFileSync19, renameSync as renameSync9, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
28654
- import { homedir as homedir9, platform as platform2 } from "node:os";
28655
- import { join as join13 } from "node:path";
30458
+ import { chmodSync as chmodSync4, readFileSync as readFileSync23, renameSync as renameSync12, unlinkSync as unlinkSync10, writeFileSync as writeFileSync14 } from "node:fs";
30459
+ import { homedir as homedir11, platform as platform3 } from "node:os";
30460
+ import { join as join17 } from "node:path";
28656
30461
  function recordDir(engagementId, home) {
28657
30462
  if (!isEngagementId(engagementId))
28658
30463
  return null;
28659
- return join13(home ?? homedir9(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
30464
+ return join17(home ?? homedir11(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
28660
30465
  }
28661
30466
  function readShownRecord(engagementId, home) {
28662
30467
  const dir = recordDir(engagementId, home);
28663
30468
  if (dir === null)
28664
30469
  return { entries: [], reviews: [] };
28665
30470
  try {
28666
- return parseShownRecord(readFileSync19(join13(dir, SHOWN_PLANS_FILENAME), "utf-8"));
30471
+ return parseShownRecord(readFileSync23(join17(dir, SHOWN_PLANS_FILENAME), "utf-8"));
28667
30472
  } catch {
28668
30473
  return { entries: [], reviews: [] };
28669
30474
  }
@@ -28684,24 +30489,24 @@ function moveListedReviewForward(engagementId, home, listed, reviewVersion, deci
28684
30489
  const dir = recordDir(engagementId, home);
28685
30490
  if (dir === null)
28686
30491
  return false;
28687
- const path = join13(dir, SHOWN_PLANS_FILENAME);
28688
- const current = parseShownRecord(readFileSync19(path, "utf-8"));
30492
+ const path = join17(dir, SHOWN_PLANS_FILENAME);
30493
+ const current = parseShownRecord(readFileSync23(path, "utf-8"));
28689
30494
  const at = current.reviews.findIndex((r) => r.phase === listed.phase && r.shownAt === listed.shownAt && r.reviewVersion === listed.reviewVersion);
28690
30495
  if (at === -1)
28691
30496
  return false;
28692
30497
  const acts = new Map(decided.map((d) => [d.part, d.act]));
28693
30498
  const reviews = current.reviews.map((review, i) => i === at ? { ...review, reviewVersion, parts: review.parts.map((p) => movedPart(p, acts.get(p.part))) } : review);
28694
30499
  const body = serialiseShownRecord({ entries: current.entries, reviews });
28695
- temp = join13(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
28696
- writeFileSync11(temp, body, { mode: 384 });
28697
- if (platform2() !== "win32")
28698
- chmodSync2(temp, 384);
28699
- renameSync9(temp, path);
30500
+ temp = join17(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
30501
+ writeFileSync14(temp, body, { mode: 384 });
30502
+ if (platform3() !== "win32")
30503
+ chmodSync4(temp, 384);
30504
+ renameSync12(temp, path);
28700
30505
  return true;
28701
30506
  } catch {
28702
30507
  if (temp !== null) {
28703
30508
  try {
28704
- unlinkSync8(temp);
30509
+ unlinkSync10(temp);
28705
30510
  } catch {
28706
30511
  return false;
28707
30512
  }
@@ -28996,15 +30801,59 @@ function reviewLineForAnswer(context, answer) {
28996
30801
  }
28997
30802
  }
28998
30803
  }
30804
+ var AUTONOMY_NOTHING_CHANGED = "Nothing changed";
30805
+ function autonomyNotAttendedLine() {
30806
+ return line(`${AUTONOMY_NOTHING_CHANGED}: turning this on or off by typing counts only when a person types it into an open session.`);
30807
+ }
30808
+ function autonomyNotSignedInLine() {
30809
+ return line(`${AUTONOMY_NOTHING_CHANGED}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
30810
+ }
30811
+ function autonomyNotOwnerLine() {
30812
+ return line(`${AUTONOMY_NOTHING_CHANGED}: only the project's owner can turn this on or off.`);
30813
+ }
30814
+ function autonomyNoOwnerLine() {
30815
+ return line(`${AUTONOMY_NOTHING_CHANGED}: this project has no owner, so nobody can turn this on or off.`);
30816
+ }
30817
+ function autonomyNotOfferedLine() {
30818
+ return line(`${AUTONOMY_NOTHING_CHANGED}. ${TYPED_APPROVAL_OFF_SENTENCE}`);
30819
+ }
30820
+ function autonomyCannotReachLine() {
30821
+ return line(`${AUTONOMY_NOTHING_CHANGED}: Halfcycle could not be reached just now. Type the line again in a minute.`);
30822
+ }
30823
+ function autonomyLineForAnswer(answer) {
30824
+ switch (answer.outcome) {
30825
+ case "recorded":
30826
+ return line(answer.on ? AUTONOMY_A4 : AUTONOMY_A5);
30827
+ case "not-owner":
30828
+ return autonomyNotOwnerLine();
30829
+ case "no-owner":
30830
+ return autonomyNoOwnerLine();
30831
+ case "not-offered":
30832
+ return autonomyNotOfferedLine();
30833
+ case "unavailable":
30834
+ return autonomyCannotReachLine();
30835
+ default: {
30836
+ const unreachable = answer;
30837
+ return unreachable;
30838
+ }
30839
+ }
30840
+ }
28999
30841
 
29000
30842
  // dist/hooks/user-prompt-submit.js
29001
30843
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
29002
30844
  var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
29003
30845
  async function runUserPromptSubmit(opts) {
29004
30846
  let act = null;
30847
+ let autonomyAct = null;
29005
30848
  const review = { recognised: false };
29006
30849
  try {
29007
30850
  const prompt = promptOf(opts.input);
30851
+ const autonomyShape = autonomyPhraseShape(prompt);
30852
+ if (autonomyShape !== null) {
30853
+ autonomyAct = autonomyShape;
30854
+ return `${await decideAutonomy(autonomyShape, opts)}
30855
+ `;
30856
+ }
29008
30857
  const reviewShape = reviewPhraseShape(prompt);
29009
30858
  if (reviewShape !== null) {
29010
30859
  const out = await decideReview(reviewShape, opts, review);
@@ -29018,6 +30867,9 @@ async function runUserPromptSubmit(opts) {
29018
30867
  return `${await decide2(shape, opts)}
29019
30868
  `;
29020
30869
  } catch {
30870
+ if (autonomyAct !== null)
30871
+ return `${autonomyCannotReachLine()}
30872
+ `;
29021
30873
  if (review.recognised)
29022
30874
  return `${reviewCannotReachLine()}
29023
30875
  `;
@@ -29025,6 +30877,44 @@ async function runUserPromptSubmit(opts) {
29025
30877
  `;
29026
30878
  }
29027
30879
  }
30880
+ async function decideAutonomy(act, opts) {
30881
+ const env = opts.env ?? process.env;
30882
+ if (!readSessionAttended(env))
30883
+ return autonomyNotAttendedLine();
30884
+ const read = readConfig(env);
30885
+ if (read.config === null) {
30886
+ return readNeverConfiguredReason(env) === "not-this-account" ? autonomyNotOwnerLine() : autonomyNotSignedInLine();
30887
+ }
30888
+ const { guardServiceToken, guardEngagementId } = read.config;
30889
+ return postAutonomy(act, guardEngagementId, guardServiceToken, env, opts.fetchFn);
30890
+ }
30891
+ async function postAutonomy(act, engagementId, credential, env, fetchFn = globalThis.fetch) {
30892
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/autonomy/chat`;
30893
+ const controller = new AbortController();
30894
+ const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
30895
+ try {
30896
+ const response = await fetchFn(url2, {
30897
+ method: "POST",
30898
+ headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json" },
30899
+ body: JSON.stringify(autonomyChatRequestSchema.parse({ act })),
30900
+ signal: controller.signal
30901
+ });
30902
+ if (response.status === 401)
30903
+ return autonomyNotSignedInLine();
30904
+ if (response.status === 429 || response.status >= 500)
30905
+ return autonomyCannotReachLine();
30906
+ const parsed = autonomyChatResultSchema.safeParse(await response.json());
30907
+ if (!parsed.success)
30908
+ return autonomyCannotReachLine();
30909
+ if (parsed.data.outcome === "recorded" && response.status !== 200)
30910
+ return autonomyCannotReachLine();
30911
+ return autonomyLineForAnswer(parsed.data);
30912
+ } catch {
30913
+ return autonomyCannotReachLine();
30914
+ } finally {
30915
+ clearTimeout(timer);
30916
+ }
30917
+ }
29028
30918
  async function decideReview(shape, opts, progress) {
29029
30919
  const env = opts.env ?? process.env;
29030
30920
  if (shape.kind === "phase") {
@@ -29206,18 +31096,36 @@ async function post(act, entry, request, engagementId, credential, env, fetchFn
29206
31096
  }
29207
31097
  }
29208
31098
 
31099
+ // dist/hooks/user-prompt-findings.js
31100
+ function runUserPromptFindings(opts) {
31101
+ try {
31102
+ const parsed = JSON.parse(opts.input);
31103
+ if (typeof parsed !== "object" || parsed === null)
31104
+ return "";
31105
+ const sessionId = parsed["session_id"];
31106
+ const given = deliverFindings(sessionId, null);
31107
+ if (given.length === 0)
31108
+ return "";
31109
+ const project = realPathOf(projectFolder(opts.env ?? process.env));
31110
+ return `${userPromptContextReply(detailsBlock(given, project))}
31111
+ `;
31112
+ } catch {
31113
+ return "";
31114
+ }
31115
+ }
31116
+
29209
31117
  // dist/resolve-engagement.js
29210
- import { existsSync as existsSync7, readFileSync as readFileSync20, realpathSync as realpathSync6 } from "node:fs";
29211
- import { join as join14, sep as sep3 } from "node:path";
29212
- import { homedir as homedir10 } from "node:os";
31118
+ import { existsSync as existsSync8, readFileSync as readFileSync24, realpathSync as realpathSync8 } from "node:fs";
31119
+ import { join as join18, sep as sep4 } from "node:path";
31120
+ import { homedir as homedir12 } from "node:os";
29213
31121
  function defaultRegistryPath() {
29214
- return join14(homedir10(), ".halfcycle", "registry.json");
31122
+ return join18(homedir12(), ".halfcycle", "registry.json");
29215
31123
  }
29216
31124
  function readRegistry(registryPath = defaultRegistryPath()) {
29217
- if (!existsSync7(registryPath))
31125
+ if (!existsSync8(registryPath))
29218
31126
  return {};
29219
31127
  try {
29220
- const parsed = JSON.parse(readFileSync20(registryPath, "utf-8"));
31128
+ const parsed = JSON.parse(readFileSync24(registryPath, "utf-8"));
29221
31129
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
29222
31130
  return {};
29223
31131
  const out = {};
@@ -29232,7 +31140,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
29232
31140
  }
29233
31141
  function canonicalise(p) {
29234
31142
  try {
29235
- return realpathSync6(p);
31143
+ return realpathSync8(p);
29236
31144
  } catch {
29237
31145
  return p;
29238
31146
  }
@@ -29253,7 +31161,7 @@ function resolveEngagement(cwd, registry2) {
29253
31161
  function isSelfOrAncestor(ancestor, target) {
29254
31162
  if (ancestor === target)
29255
31163
  return true;
29256
- const withSep = ancestor.endsWith(sep3) ? ancestor : ancestor + sep3;
31164
+ const withSep = ancestor.endsWith(sep4) ? ancestor : ancestor + sep4;
29257
31165
  return target.startsWith(withSep);
29258
31166
  }
29259
31167
  function runResolveEngagement(cwd, registryPath) {
@@ -29265,13 +31173,13 @@ function runResolveEngagement(cwd, registryPath) {
29265
31173
  }
29266
31174
 
29267
31175
  // dist/refresh-credential.js
29268
- import { readFileSync as readFileSync22 } from "node:fs";
29269
- import { resolve as resolve5 } from "node:path";
31176
+ import { readFileSync as readFileSync26 } from "node:fs";
31177
+ import { resolve as resolve7 } from "node:path";
29270
31178
 
29271
31179
  // dist/mcp-headers.js
29272
- import { readFileSync as readFileSync21 } from "node:fs";
29273
- import { homedir as homedir11 } from "node:os";
29274
- import { dirname as dirname6, resolve as resolve4 } from "node:path";
31180
+ import { readFileSync as readFileSync25 } from "node:fs";
31181
+ import { homedir as homedir13 } from "node:os";
31182
+ import { dirname as dirname9, resolve as resolve6 } from "node:path";
29275
31183
  function defaultWriteErr(text) {
29276
31184
  process.stderr.write(text);
29277
31185
  }
@@ -29284,12 +31192,12 @@ function headerLine(bearer, clientVersion) {
29284
31192
  }
29285
31193
  function projectRootFromBinary(binaryPath) {
29286
31194
  const depth = VENDORED_BIN_REL.split("/").length - 1;
29287
- return resolve4(dirname6(binaryPath), ...Array(depth).fill(".."));
31195
+ return resolve6(dirname9(binaryPath), ...Array(depth).fill(".."));
29288
31196
  }
29289
31197
  function readPinnedVersion(root) {
29290
31198
  let raw;
29291
31199
  try {
29292
- raw = readFileSync21(resolve4(root, BUNDLE_PIN_REL), "utf-8");
31200
+ raw = readFileSync25(resolve6(root, BUNDLE_PIN_REL), "utf-8");
29293
31201
  } catch {
29294
31202
  return void 0;
29295
31203
  }
@@ -29331,7 +31239,7 @@ function sentences(lines2) {
29331
31239
  }
29332
31240
  function badIdSentence(root) {
29333
31241
  return sentences([
29334
- `${resolve4(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
31242
+ `${resolve6(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
29335
31243
  'so no credential was read. Delete that file and run "npx halfcycle" again: this repository',
29336
31244
  "is then set up as a new Halfcycle project."
29337
31245
  ]);
@@ -29345,7 +31253,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
29345
31253
  ]);
29346
31254
  case "no-id":
29347
31255
  return sentences([
29348
- `${resolve4(root, BUNDLE_PIN_REL)} names no engagement id.`,
31256
+ `${resolve6(root, BUNDLE_PIN_REL)} names no engagement id.`,
29349
31257
  'run "npx halfcycle" here to rewrite it.'
29350
31258
  ]);
29351
31259
  case "bad-id":
@@ -29362,7 +31270,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
29362
31270
  case "no-credential":
29363
31271
  return sentences([
29364
31272
  `this machine holds no credential for engagement ${engagementId ?? ""}.`,
29365
- `run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve4(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
31273
+ `run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve6(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
29366
31274
  ]);
29367
31275
  }
29368
31276
  }
@@ -29385,7 +31293,7 @@ async function refreshWithinDeadline(source, opts, writeErr) {
29385
31293
  }
29386
31294
  async function chooseBearer(opts) {
29387
31295
  const writeErr = opts.writeErr ?? defaultWriteErr;
29388
- const home = opts.home ?? homedir11();
31296
+ const home = opts.home ?? homedir13();
29389
31297
  const root = projectRootFromBinary(opts.binaryPath);
29390
31298
  const environmentToken = opts.environmentTokenFirst ? readEnvironmentToken() : void 0;
29391
31299
  const store = resolveStore(root, home);
@@ -29512,10 +31420,11 @@ async function refreshCredentialToken(opts) {
29512
31420
  var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
29513
31421
  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.`;
29514
31422
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
31423
+ var SESSION_START_PROBE_ATTEMPTS = 2;
29515
31424
  function registeredMcpUrl(projectRoot) {
29516
31425
  let parsed;
29517
31426
  try {
29518
- parsed = JSON.parse(readFileSync22(resolve5(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
31427
+ parsed = JSON.parse(readFileSync26(resolve7(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
29519
31428
  } catch {
29520
31429
  return void 0;
29521
31430
  }
@@ -29581,11 +31490,11 @@ async function postInitialize(url2, bearer, fetchFn, signal) {
29581
31490
  }
29582
31491
  return namesItsServer(body) ? void 0 : SERVICE_PROBLEM_SENTENCE;
29583
31492
  }
29584
- async function probeDelivery(url2, bearer, fetchFn, deadlineMs) {
31493
+ async function probeDelivery(url2, bearer, fetchFn, deadlineMs, maxAttempts) {
29585
31494
  const controller = new AbortController();
29586
31495
  const timer = setTimeout(() => controller.abort(), deadlineMs);
29587
31496
  try {
29588
- for (let attempt2 = 0; attempt2 < 2; attempt2++) {
31497
+ for (let attempt2 = 0; attempt2 < maxAttempts; attempt2++) {
29589
31498
  const outcome = await postInitialize(url2, bearer, fetchFn, controller.signal);
29590
31499
  if (outcome !== CUT_OFF)
29591
31500
  return outcome;
@@ -29611,7 +31520,7 @@ async function checkConnection(renewedToken, opts) {
29611
31520
  if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
29612
31521
  return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer };
29613
31522
  }
29614
- const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
31523
+ const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS, opts.maxAttempts ?? SESSION_START_PROBE_ATTEMPTS);
29615
31524
  return { sentence: outcome ?? CONNECTED_SENTENCE, url: url2, bearer };
29616
31525
  }
29617
31526
  function positionRequestBody(engagementId) {
@@ -29722,6 +31631,20 @@ async function runRefreshCredential(opts) {
29722
31631
  }
29723
31632
  }
29724
31633
  }
31634
+ if (CHECKED_SOURCES.has(input["source"])) {
31635
+ const config2 = readConfig().config;
31636
+ if (config2 !== null) {
31637
+ const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
31638
+ await cacheAutonomySetting({
31639
+ controlOrigin: readControlOrigin().origin,
31640
+ engagementId: config2.guardEngagementId,
31641
+ bearer: renewedToken ?? config2.guardServiceToken,
31642
+ fetchFn: opts.fetchFn ?? globalThis.fetch,
31643
+ home: opts.home,
31644
+ deadlineMs: Math.min(opts.autonomyStateDeadlineMs ?? AUTONOMY_STATE_DEADLINE_MS, remainingForMargin)
31645
+ });
31646
+ }
31647
+ }
29725
31648
  if (readConfig().config === null) {
29726
31649
  const remedySaid = parts.some(namesTheSignInCommand);
29727
31650
  parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid, pinnedEngagementId(projectRootFromBinary(opts.binaryPath))));
@@ -29748,8 +31671,8 @@ async function runRefreshCredential(opts) {
29748
31671
  import { fileURLToPath } from "node:url";
29749
31672
 
29750
31673
  // dist/capture.js
29751
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync12, mkdirSync as mkdirSync8, existsSync as existsSync8 } from "node:fs";
29752
- import { dirname as dirname7 } from "node:path";
31674
+ import { readFileSync as readFileSync27, writeFileSync as writeFileSync15, mkdirSync as mkdirSync10, existsSync as existsSync9 } from "node:fs";
31675
+ import { dirname as dirname10 } from "node:path";
29753
31676
  function runCapture(provider, endpoint, opts = {}) {
29754
31677
  const repoRoot = opts.repoRoot ?? process.cwd();
29755
31678
  const raw = opts.input ?? readInputFile(opts.fromFile);
@@ -29784,8 +31707,8 @@ function runCapture(provider, endpoint, opts = {}) {
29784
31707
  const fixture = fixturePath(repoRoot, provider, slug);
29785
31708
  const manifestFile = manifestPath(repoRoot);
29786
31709
  try {
29787
- mkdirSync8(dirname7(fixture), { recursive: true });
29788
- writeFileSync12(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
31710
+ mkdirSync10(dirname10(fixture), { recursive: true });
31711
+ writeFileSync15(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
29789
31712
  } catch (err) {
29790
31713
  const msg = err instanceof Error ? err.message : String(err);
29791
31714
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -29816,7 +31739,7 @@ function readInputFile(fromFile) {
29816
31739
  if (!fromFile)
29817
31740
  return void 0;
29818
31741
  try {
29819
- return readFileSync23(fromFile, "utf-8");
31742
+ return readFileSync27(fromFile, "utf-8");
29820
31743
  } catch {
29821
31744
  return void 0;
29822
31745
  }
@@ -29827,11 +31750,11 @@ function relativeFixturePath(repoRoot, fixture) {
29827
31750
  }
29828
31751
 
29829
31752
  // dist/prove.js
29830
- import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
29831
- import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
29832
- import { createWriteStream, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync24, renameSync as renameSync10, unlinkSync as unlinkSync9, writeFileSync as writeFileSync13 } from "node:fs";
29833
- import { homedir as homedir12 } from "node:os";
29834
- import { dirname as dirname8, join as join15 } from "node:path";
31753
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
31754
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
31755
+ import { createWriteStream, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync28, renameSync as renameSync13, unlinkSync as unlinkSync11, writeFileSync as writeFileSync16 } from "node:fs";
31756
+ import { homedir as homedir14 } from "node:os";
31757
+ import { dirname as dirname11, join as join19 } from "node:path";
29835
31758
 
29836
31759
  // dist/test-counts.js
29837
31760
  function sum(values) {
@@ -29866,36 +31789,36 @@ function jestCounts(output) {
29866
31789
  if (lines2.length === 0)
29867
31790
  return null;
29868
31791
  let passed = 0;
29869
- let failed = 0;
31792
+ let failed2 = 0;
29870
31793
  for (const line2 of lines2) {
29871
31794
  passed += firstNumber(line2[1], /(\d+) passed/) ?? 0;
29872
- failed += firstNumber(line2[1], /(\d+) failed/) ?? 0;
31795
+ failed2 += firstNumber(line2[1], /(\d+) failed/) ?? 0;
29873
31796
  }
29874
- return { passed, failed };
31797
+ return { passed, failed: failed2 };
29875
31798
  }
29876
31799
  function vitestCounts(output) {
29877
31800
  const lines2 = [...output.matchAll(/^\s*Tests\s+(.+)$/gm)];
29878
31801
  if (lines2.length === 0)
29879
31802
  return null;
29880
31803
  let passed = 0;
29881
- let failed = 0;
31804
+ let failed2 = 0;
29882
31805
  for (const line2 of lines2) {
29883
31806
  passed += firstNumber(line2[1], /(\d+) passed/) ?? 0;
29884
- failed += firstNumber(line2[1], /(\d+) failed/) ?? 0;
31807
+ failed2 += firstNumber(line2[1], /(\d+) failed/) ?? 0;
29885
31808
  }
29886
- return { passed, failed };
31809
+ return { passed, failed: failed2 };
29887
31810
  }
29888
31811
  function pytestCounts(output) {
29889
31812
  const lines2 = [...output.matchAll(/^\W*((?:\d+ \w+(?:, )?)+)\s+in [\d.]+s\W*$/gm)];
29890
31813
  if (lines2.length === 0)
29891
31814
  return null;
29892
31815
  let passed = 0;
29893
- let failed = 0;
31816
+ let failed2 = 0;
29894
31817
  for (const line2 of lines2) {
29895
31818
  passed += firstNumber(line2[1], /(\d+) passed/) ?? 0;
29896
- failed += firstNumber(line2[1], /(\d+) failed/) ?? 0;
31819
+ failed2 += firstNumber(line2[1], /(\d+) failed/) ?? 0;
29897
31820
  }
29898
- return { passed, failed };
31821
+ return { passed, failed: failed2 };
29899
31822
  }
29900
31823
  function cargoTestCounts(output) {
29901
31824
  const lines2 = [...output.matchAll(/^test result: (?:ok|FAILED)\.\s*(\d+) passed;\s*(\d+) failed;/gm)];
@@ -29940,19 +31863,19 @@ function isProofTestPath(path) {
29940
31863
  function testRunsDir(engagementId, home) {
29941
31864
  if (!isEngagementId(engagementId))
29942
31865
  return null;
29943
- return join15(home ?? homedir12(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, TEST_RUNS_DIR_NAME);
31866
+ return join19(home ?? homedir14(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, TEST_RUNS_DIR_NAME);
29944
31867
  }
29945
31868
  function commandIdOf(command) {
29946
- return createHash4("sha256").update(command, "utf8").digest("hex").slice(0, 16);
31869
+ return createHash5("sha256").update(command, "utf8").digest("hex").slice(0, 16);
29947
31870
  }
29948
31871
  var PLAIN_WORD = /^[A-Za-z0-9_./:=@%+,-]+$/;
29949
- function commandFromWords(words, platform3 = process.platform) {
31872
+ function commandFromWords(words, platform4 = process.platform) {
29950
31873
  if (words.length === 1)
29951
31874
  return words[0];
29952
31875
  return words.map((word) => {
29953
31876
  if (PLAIN_WORD.test(word))
29954
31877
  return word;
29955
- return platform3 === "win32" ? `"${word.replace(/"/g, '""')}"` : quoteForShell(word);
31878
+ return platform4 === "win32" ? `"${word.replace(/"/g, '""')}"` : quoteForShell(word);
29956
31879
  }).join(" ");
29957
31880
  }
29958
31881
  var PROVE_USAGE = 'Usage: prove <phase> <task> --label "<label>" --before -- <test command>\n prove <phase> <task> --label "<label>" --after [-- <test command>]\n (or --change <id> in place of <phase> <task>; add --reason "<why>" to say why)';
@@ -30026,21 +31949,21 @@ function parseProveArgs(args2) {
30026
31949
  };
30027
31950
  }
30028
31951
  function writeJsonFile2(path, value) {
30029
- mkdirSync9(dirname8(path), { recursive: true, mode: 448 });
30030
- const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
30031
- writeFileSync13(tmp, JSON.stringify(value), { mode: 384 });
30032
- renameSync10(tmp, path);
31952
+ mkdirSync11(dirname11(path), { recursive: true, mode: 448 });
31953
+ const tmp = `${path}.${process.pid}.${randomUUID3()}.tmp`;
31954
+ writeFileSync16(tmp, JSON.stringify(value), { mode: 384 });
31955
+ renameSync13(tmp, path);
30033
31956
  }
30034
31957
  function subjectKey(subject) {
30035
31958
  return subject.kind === "task" ? ["task", subject.phase, subject.task] : ["change", subject.change];
30036
31959
  }
30037
31960
  function keptCheckPath(dir, subject, label) {
30038
- const key = createHash4("sha256").update(JSON.stringify([...subjectKey(subject), label]), "utf8").digest("hex");
30039
- return join15(dir, COMMANDS_DIR_NAME, `${key.slice(0, 32)}.json`);
31961
+ const key = createHash5("sha256").update(JSON.stringify([...subjectKey(subject), label]), "utf8").digest("hex");
31962
+ return join19(dir, COMMANDS_DIR_NAME, `${key.slice(0, 32)}.json`);
30040
31963
  }
30041
31964
  function readKeptCheck(dir, subject, label) {
30042
31965
  try {
30043
- const raw = JSON.parse(readFileSync24(keptCheckPath(dir, subject, label), "utf-8"));
31966
+ const raw = JSON.parse(readFileSync28(keptCheckPath(dir, subject, label), "utf-8"));
30044
31967
  if (typeof raw.command !== "string" || raw.command === "" || typeof raw.commandId !== "string")
30045
31968
  return null;
30046
31969
  const b = raw.before;
@@ -30051,21 +31974,21 @@ function readKeptCheck(dir, subject, label) {
30051
31974
  }
30052
31975
  }
30053
31976
  function queuePath(dir, runId) {
30054
- return join15(dir, QUEUE_DIR_NAME, `${runId}.json`);
31977
+ return join19(dir, QUEUE_DIR_NAME, `${runId}.json`);
30055
31978
  }
30056
31979
  function readQueue(dir) {
30057
31980
  let names;
30058
31981
  try {
30059
- names = readdirSync9(join15(dir, QUEUE_DIR_NAME)).filter((n) => n.endsWith(".json"));
31982
+ names = readdirSync10(join19(dir, QUEUE_DIR_NAME)).filter((n) => n.endsWith(".json"));
30060
31983
  } catch {
30061
31984
  return [];
30062
31985
  }
30063
31986
  const reports = [];
30064
31987
  for (const name of names) {
30065
- const path = join15(dir, QUEUE_DIR_NAME, name);
31988
+ const path = join19(dir, QUEUE_DIR_NAME, name);
30066
31989
  let parsed = null;
30067
31990
  try {
30068
- parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync24(path, "utf-8")));
31991
+ parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync28(path, "utf-8")));
30069
31992
  } catch {
30070
31993
  parsed = null;
30071
31994
  }
@@ -30078,7 +32001,7 @@ function readQueue(dir) {
30078
32001
  }
30079
32002
  function removeFile(path) {
30080
32003
  try {
30081
- unlinkSync9(path);
32004
+ unlinkSync11(path);
30082
32005
  } catch {
30083
32006
  }
30084
32007
  }
@@ -30118,13 +32041,13 @@ async function sendQueuedRuns(dir, config2, controlOrigin, fetchFn) {
30118
32041
  }
30119
32042
  return outcomes;
30120
32043
  }
30121
- function killTree(child, platform3, signal) {
32044
+ function killTree(child, platform4, signal) {
30122
32045
  const pid = child.pid;
30123
32046
  if (pid === void 0)
30124
32047
  return;
30125
32048
  try {
30126
- if (platform3 === "win32") {
30127
- spawnSync2("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
32049
+ if (platform4 === "win32") {
32050
+ spawnSync3("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
30128
32051
  } else {
30129
32052
  process.kill(-pid, signal);
30130
32053
  }
@@ -30136,16 +32059,16 @@ function killTree(child, platform3, signal) {
30136
32059
  }
30137
32060
  }
30138
32061
  function runCommand(command, repoRoot, logPath, env, deps) {
30139
- const platform3 = deps.platform ?? process.platform;
32062
+ const platform4 = deps.platform ?? process.platform;
30140
32063
  const out = deps.stdout ?? process.stdout;
30141
32064
  const err = deps.stderr ?? process.stderr;
30142
32065
  const deadline = deps.deadlineMs ?? PROVE_RUN_DEADLINE_MS;
30143
32066
  const childEnv = { ...env };
30144
32067
  delete childEnv[SERVICE_TOKEN_VAR];
30145
- mkdirSync9(dirname8(logPath), { recursive: true, mode: 448 });
32068
+ mkdirSync11(dirname11(logPath), { recursive: true, mode: 448 });
30146
32069
  const log = createWriteStream(logPath, { mode: 384 });
30147
32070
  const began = process.hrtime.bigint();
30148
- return new Promise((resolve6) => {
32071
+ return new Promise((resolve8) => {
30149
32072
  let timedOut = false;
30150
32073
  let started = true;
30151
32074
  let settled = false;
@@ -30154,15 +32077,15 @@ function runCommand(command, repoRoot, logPath, env, deps) {
30154
32077
  env: childEnv,
30155
32078
  shell: true,
30156
32079
  stdio: ["ignore", "pipe", "pipe"],
30157
- detached: platform3 !== "win32",
32080
+ detached: platform4 !== "win32",
30158
32081
  windowsHide: true
30159
32082
  });
30160
- const onSignal = (signal) => killTree(child, platform3, signal);
32083
+ const onSignal = (signal) => killTree(child, platform4, signal);
30161
32084
  process.on("SIGINT", onSignal);
30162
32085
  process.on("SIGTERM", onSignal);
30163
32086
  const timer = setTimeout(() => {
30164
32087
  timedOut = true;
30165
- killTree(child, platform3, "SIGKILL");
32088
+ killTree(child, platform4, "SIGKILL");
30166
32089
  }, deadline);
30167
32090
  child.stdout?.on("data", (chunk) => {
30168
32091
  out.write(chunk);
@@ -30180,7 +32103,7 @@ function runCommand(command, repoRoot, logPath, env, deps) {
30180
32103
  process.off("SIGINT", onSignal);
30181
32104
  process.off("SIGTERM", onSignal);
30182
32105
  const durationMs = Number((process.hrtime.bigint() - began) / 1000000n);
30183
- log.end(() => resolve6({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
32106
+ log.end(() => resolve8({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
30184
32107
  };
30185
32108
  child.on("error", (e) => {
30186
32109
  started = false;
@@ -30301,14 +32224,14 @@ ${PROVE_USAGE}`);
30301
32224
  }
30302
32225
  if (request.when === "before" && changedOutsideTests > 0)
30303
32226
  say(lateBeforeLine(changedOutsideTests));
30304
- const runId = randomUUID2();
30305
- const logPath = join15(dir, `${runId}.log`);
32227
+ const runId = randomUUID3();
32228
+ const logPath = join19(dir, `${runId}.log`);
30306
32229
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
30307
32230
  const deadline = deps.deadlineMs ?? PROVE_RUN_DEADLINE_MS;
30308
32231
  const end = await runCommand(command, repoRoot, logPath, env, deps);
30309
32232
  let output = "";
30310
32233
  try {
30311
- output = readFileSync24(logPath, "utf-8");
32234
+ output = readFileSync28(logPath, "utf-8");
30312
32235
  } catch {
30313
32236
  output = "";
30314
32237
  }
@@ -30358,7 +32281,7 @@ ${PROVE_USAGE}`);
30358
32281
 
30359
32282
  // dist/statusline.js
30360
32283
  import { spawn as spawn3 } from "node:child_process";
30361
- import { readFileSync as readFileSync25 } from "node:fs";
32284
+ import { readFileSync as readFileSync29 } from "node:fs";
30362
32285
  import nodePath5 from "node:path";
30363
32286
  var OWN_STATUS_LINE_DEADLINE_MS = 5e3;
30364
32287
  var PERSON_SETTINGS_FILENAME = "settings.json";
@@ -30396,7 +32319,7 @@ function halfcycleStatusLineOf(figures) {
30396
32319
  }
30397
32320
  function personStatusLineCommand(configFolder) {
30398
32321
  try {
30399
- const v = JSON.parse(readFileSync25(nodePath5.join(configFolder, PERSON_SETTINGS_FILENAME), "utf-8"));
32322
+ const v = JSON.parse(readFileSync29(nodePath5.join(configFolder, PERSON_SETTINGS_FILENAME), "utf-8"));
30400
32323
  const line2 = v["statusLine"];
30401
32324
  if (typeof line2 !== "object" || line2 === null)
30402
32325
  return null;
@@ -30420,14 +32343,14 @@ function personCommandEnv(env) {
30420
32343
  }
30421
32344
  return out;
30422
32345
  }
30423
- var runOwnCommandReal = (command, input, env) => new Promise((resolve6) => {
32346
+ var runOwnCommandReal = (command, input, env) => new Promise((resolve8) => {
30424
32347
  let out = "";
30425
32348
  let settled = false;
30426
32349
  const done = (v) => {
30427
32350
  if (settled)
30428
32351
  return;
30429
32352
  settled = true;
30430
- resolve6(v);
32353
+ resolve8(v);
30431
32354
  };
30432
32355
  try {
30433
32356
  const child = spawn3(command, {
@@ -30490,6 +32413,43 @@ async function runStatusline(raw, deps = {}) {
30490
32413
  `;
30491
32414
  }
30492
32415
 
32416
+ // dist/check-connection.js
32417
+ function connectionKind(bearer, sentence) {
32418
+ if (bearer === NOT_THIS_ACCOUNT_PLACEHOLDER)
32419
+ return "other-account";
32420
+ if (bearer === UPDATE_CLAUDE_CODE_PLACEHOLDER)
32421
+ return "update-claude-code";
32422
+ if (bearer === NO_CREDENTIAL_PLACEHOLDER)
32423
+ return "not-signed-in";
32424
+ if (sentence === CONNECTED_SENTENCE)
32425
+ return "connected";
32426
+ if (sentence === CANNOT_REACH_SENTENCE)
32427
+ return "cannot-reach";
32428
+ if (sentence === SERVICE_PROBLEM_SENTENCE)
32429
+ return "service-problem";
32430
+ if (sentence === NO_CREDENTIAL_SENTENCE)
32431
+ return "signed-out";
32432
+ throw new Error("the connection check gave an outcome with no kind");
32433
+ }
32434
+ async function checkConnectionLine(opts) {
32435
+ const result = await checkConnection(void 0, {
32436
+ ...opts,
32437
+ maxAttempts: 1,
32438
+ writeErr: opts.writeErr ?? discardReason
32439
+ });
32440
+ if (result === void 0)
32441
+ return { kind: "not-registered", sentence: null };
32442
+ return { kind: connectionKind(result.bearer, result.sentence), sentence: result.sentence };
32443
+ }
32444
+ async function runCheckConnection(opts) {
32445
+ const line2 = await checkConnectionLine(opts);
32446
+ return `${JSON.stringify(line2)}
32447
+ `;
32448
+ }
32449
+ function discardReason() {
32450
+ return void 0;
32451
+ }
32452
+
30493
32453
  // dist/bin.js
30494
32454
  var args = process.argv.slice(2);
30495
32455
  async function main() {
@@ -30572,6 +32532,18 @@ async function main() {
30572
32532
  process.stdout.write(line2, () => process.exit(0));
30573
32533
  return;
30574
32534
  }
32535
+ if (cmd === "check-connection") {
32536
+ let line2;
32537
+ try {
32538
+ line2 = await runCheckConnection({ binaryPath: fileURLToPath(import.meta.url) });
32539
+ } catch {
32540
+ process.stderr.write("halfcycle-runner: the connection could not be checked.\n");
32541
+ process.exit(1);
32542
+ return;
32543
+ }
32544
+ process.stdout.write(line2, () => process.exit(0));
32545
+ return;
32546
+ }
30575
32547
  if (cmd === "mcp-headers") {
30576
32548
  const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
30577
32549
  process.stdout.write(line2, () => process.exit(0));
@@ -30650,8 +32622,18 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
30650
32622
  process.stdout.write(reply, () => process.exit(0));
30651
32623
  return;
30652
32624
  }
32625
+ if (sub === "user-prompt-findings") {
32626
+ let reply = "";
32627
+ try {
32628
+ reply = runUserPromptFindings({ input: await readStdin2() });
32629
+ } catch {
32630
+ reply = "";
32631
+ }
32632
+ process.stdout.write(reply, () => process.exit(0));
32633
+ return;
32634
+ }
30653
32635
  process.stderr.write(`halfcycle-runner: unknown hook "${sub ?? ""}"
30654
- Valid hooks: post-tool-use, pre-tool-use, stop, subagent-stop, user-prompt-submit
32636
+ Valid hooks: post-tool-use, pre-tool-use, stop, subagent-stop, user-prompt-submit, user-prompt-findings
30655
32637
  `);
30656
32638
  process.exit(1);
30657
32639
  return;
@@ -30663,7 +32645,9 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
30663
32645
  halfcycle-runner hook stop
30664
32646
  halfcycle-runner hook subagent-stop
30665
32647
  halfcycle-runner hook user-prompt-submit
32648
+ halfcycle-runner hook user-prompt-findings
30666
32649
  halfcycle-runner refresh-credential
32650
+ halfcycle-runner check-connection
30667
32651
  halfcycle-runner mcp-headers
30668
32652
  halfcycle-runner observe
30669
32653
  halfcycle-runner observe --change <sortingId>
@@ -30678,10 +32662,10 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
30678
32662
  process.exit(1);
30679
32663
  }
30680
32664
  function readStdin3() {
30681
- return new Promise((resolve6, reject) => {
32665
+ return new Promise((resolve8, reject) => {
30682
32666
  const chunks = [];
30683
32667
  process.stdin.on("data", (chunk) => chunks.push(chunk));
30684
- process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8")));
32668
+ process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
30685
32669
  process.stdin.on("error", reject);
30686
32670
  });
30687
32671
  }