halfcycle 0.4.14 → 0.4.16

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 (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/bin.bundle.mjs +2549 -552
  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 +5236 -3540
  20. package/dist/cli.js.map +3 -3
  21. package/dist/close-phase.d.ts +10 -1
  22. package/dist/close-phase.d.ts.map +1 -1
  23. package/dist/doctor.d.ts +34 -0
  24. package/dist/doctor.d.ts.map +1 -0
  25. package/dist/folder-refusals.d.ts +27 -0
  26. package/dist/folder-refusals.d.ts.map +1 -0
  27. package/dist/identity.d.ts +31 -1
  28. package/dist/identity.d.ts.map +1 -1
  29. package/dist/index.js +1332 -404
  30. package/dist/index.js.map +3 -3
  31. package/dist/install-plan.d.ts +112 -0
  32. package/dist/install-plan.d.ts.map +1 -0
  33. package/dist/install.d.ts +535 -1
  34. package/dist/install.d.ts.map +1 -1
  35. package/dist/plan.d.ts +16 -3
  36. package/dist/plan.d.ts.map +1 -1
  37. package/dist/pre-create-checks.d.ts +28 -0
  38. package/dist/pre-create-checks.d.ts.map +1 -1
  39. package/dist/preview.d.ts +40 -0
  40. package/dist/preview.d.ts.map +1 -0
  41. package/dist/restore.d.ts +58 -0
  42. package/dist/restore.d.ts.map +1 -0
  43. package/dist/review.d.ts +9 -1
  44. package/dist/review.d.ts.map +1 -1
  45. package/dist/scan.d.ts +11 -0
  46. package/dist/scan.d.ts.map +1 -1
  47. package/dist/setup/discovery.d.ts.map +1 -1
  48. package/dist/setup/rows.d.ts +10 -6
  49. package/dist/setup/rows.d.ts.map +1 -1
  50. 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) {
@@ -18441,40 +18770,65 @@ ${agentLine}`;
18441
18770
  function preToolUseNoticeText(text) {
18442
18771
  return JSON.stringify({ systemMessage: text });
18443
18772
  }
18773
+ function preToolUseSavedText(text) {
18774
+ return JSON.stringify({ systemMessage: text, hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: text } });
18775
+ }
18444
18776
  function nonBlockingReply(event, text) {
18445
- if (event === "PostToolUse" || event === "SessionStart") {
18777
+ if (event === "SessionStart") {
18446
18778
  return JSON.stringify({
18447
18779
  systemMessage: text,
18448
18780
  hookSpecificOutput: { hookEventName: event, additionalContext: text }
18449
18781
  });
18450
18782
  }
18783
+ if (event === "PostToolUse") {
18784
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } });
18785
+ }
18451
18786
  return JSON.stringify({ systemMessage: text });
18452
18787
  }
18453
18788
  function hookReply(event) {
18454
18789
  const queued = [];
18790
+ const forAgent = [];
18455
18791
  return {
18456
18792
  notice(text) {
18457
18793
  queued.push(text);
18458
18794
  },
18459
- block(reason) {
18795
+ agentContext(text) {
18796
+ forAgent.push(text);
18797
+ },
18798
+ block(reason, systemMessage) {
18460
18799
  const carried = queued.splice(0);
18461
- return emitBlock(carried.length === 0 ? reason : [reason, ...carried].join("\n"));
18800
+ return emitBlock(carried.length === 0 ? reason : [reason, ...carried].join("\n"), systemMessage);
18462
18801
  },
18463
18802
  flush() {
18464
- if (queued.length === 0)
18465
- return;
18466
18803
  const text = queued.splice(0).join("\n");
18804
+ const context = event === "PostToolUse" ? forAgent.splice(0).join("\n") : "";
18467
18805
  const code = process.exitCode;
18468
18806
  if (code !== void 0 && code !== null && Number(code) !== 0) {
18469
- writeSync(2, `${text}
18807
+ if (text !== "")
18808
+ writeSync(2, `${text}
18809
+ `);
18810
+ return;
18811
+ }
18812
+ if (context !== "") {
18813
+ process.stdout.write(`${postToolUseReply(text, context)}
18470
18814
  `);
18471
18815
  return;
18472
18816
  }
18817
+ if (text === "")
18818
+ return;
18473
18819
  process.stdout.write(`${nonBlockingReply(event, text)}
18474
18820
  `);
18475
18821
  }
18476
18822
  };
18477
18823
  }
18824
+ function postToolUseReply(notice, context) {
18825
+ const additionalContext = notice === "" ? context : `${notice}
18826
+ ${context}`;
18827
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext } });
18828
+ }
18829
+ function userPromptContextReply(context) {
18830
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } });
18831
+ }
18478
18832
  function infraWarningText() {
18479
18833
  return "[Halfcycle] WARNING: Halfcycle's check service could not be reached, so nothing was checked this time. The session continues without checks.";
18480
18834
  }
@@ -18827,7 +19181,8 @@ function spawnGit(args2, cwd, timeoutMs, extra) {
18827
19181
  encoding: "utf-8",
18828
19182
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs },
18829
19183
  ...extra?.input === void 0 ? {} : { input: extra.input },
18830
- ...extra?.maxBuffer === void 0 ? {} : { maxBuffer: extra.maxBuffer }
19184
+ ...extra?.maxBuffer === void 0 ? {} : { maxBuffer: extra.maxBuffer },
19185
+ ...extra?.env === void 0 ? {} : { env: { ...process.env, ...extra.env } }
18831
19186
  });
18832
19187
  }
18833
19188
  function git(args2, cwd, timeoutMs, extra) {
@@ -18903,6 +19258,9 @@ function remoteHeadLine(remote, repoRoot, timeoutMs, globalArgs = []) {
18903
19258
  function readGitForFacts(args2, cwd, timeoutMs, input) {
18904
19259
  return git(args2, cwd, timeoutMs, { maxBuffer: 64 * 1024 * 1024, ...input === void 0 ? {} : { input } });
18905
19260
  }
19261
+ function gitForSave(args2, cwd, timeoutMs, extra = {}) {
19262
+ return git(args2, cwd, timeoutMs, { maxBuffer: 64 * 1024 * 1024, ...extra });
19263
+ }
18906
19264
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
18907
19265
  function resolveDiffBase(repoRoot, explicit) {
18908
19266
  const named = (explicit ?? "").trim();
@@ -18964,6 +19322,15 @@ function getRefToWorktreeChangedFiles(ref, repoRoot) {
18964
19322
  const files = result.value.split("\n").map((l) => l.trim()).filter(Boolean);
18965
19323
  return { ok: true, value: files };
18966
19324
  }
19325
+ function getUntrackedFiles(repoRoot, paths) {
19326
+ if (paths.length === 0)
19327
+ return { ok: true, value: [] };
19328
+ const pathspecs = paths.map((p) => `:(literal)${p}`);
19329
+ const result = git(["ls-files", "--others", "--exclude-standard", "-z", "--", ...pathspecs], repoRoot);
19330
+ if (!result.ok)
19331
+ return result;
19332
+ return { ok: true, value: result.value.split("\0").filter(Boolean) };
19333
+ }
18967
19334
  var PROVE_GIT_TIMEOUT_MS = 3e4;
18968
19335
  function pathsDifferingFromHead(repoRoot, timeoutMs = PROVE_GIT_TIMEOUT_MS) {
18969
19336
  const result = git(["--no-optional-locks", "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all"], repoRoot, timeoutMs);
@@ -20767,6 +21134,17 @@ var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
20767
21134
  var MARKER_PREFIX = "halfcycle-session-";
20768
21135
  var MARKER_SUFFIX = ".ref";
20769
21136
  var DEDUPE_SUFFIX = ".eval";
21137
+ var OWN_WORK_PREFIX = "halfcycle-own-";
21138
+ var OWN_WORK_SUFFIX = ".jsonl";
21139
+ var FINDINGS_PREFIX = "halfcycle-findings-";
21140
+ var FINDINGS_SUFFIX = ".json";
21141
+ var FINDINGS_ID_SOURCE = "^[A-Za-z0-9-]{1,128}$";
21142
+ var FINDINGS_ID = new RegExp(FINDINGS_ID_SOURCE);
21143
+ function findingsPath(sessionId) {
21144
+ if (typeof sessionId !== "string" || !FINDINGS_ID.test(sessionId))
21145
+ return null;
21146
+ return join6(markerDir(), `${FINDINGS_PREFIX}${sessionId}${FINDINGS_SUFFIX}`);
21147
+ }
20770
21148
  function markerDir() {
20771
21149
  const raw = process.env["TMPDIR"] || tmpdir();
20772
21150
  return raw.replace(/\/+$/, "");
@@ -20873,6 +21251,7 @@ function digestChangeState(base, entries) {
20873
21251
  function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now(), livenessCommon) {
20874
21252
  if (livenessCommon !== void 0)
20875
21253
  sweepLiveness(livenessCommon, now);
21254
+ sweepSessionFiles(keepSessionId, now);
20876
21255
  const dir = markerDir();
20877
21256
  const hash2 = repoHash(repoRoot);
20878
21257
  const keepName = keepSessionId ? markerFileName(repoRoot, keepSessionId) : markerFileName(repoRoot);
@@ -20912,6 +21291,33 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now(), livene
20912
21291
  }
20913
21292
  }
20914
21293
  }
21294
+ function sweepSessionFiles(keepSessionId, now) {
21295
+ const dir = markerDir();
21296
+ let names;
21297
+ try {
21298
+ names = readdirSync3(dir);
21299
+ } catch {
21300
+ return;
21301
+ }
21302
+ const kinds = [
21303
+ [FINDINGS_PREFIX, FINDINGS_SUFFIX],
21304
+ [OWN_WORK_PREFIX, OWN_WORK_SUFFIX]
21305
+ ];
21306
+ for (const name of names) {
21307
+ for (const [prefix, suffix] of kinds) {
21308
+ if (!name.startsWith(prefix) || !name.endsWith(suffix))
21309
+ continue;
21310
+ if (keepSessionId !== void 0 && name === `${prefix}${keepSessionId}${suffix}`)
21311
+ continue;
21312
+ const full = join6(dir, name);
21313
+ try {
21314
+ if (now - statSync(full).mtimeMs > MARKER_TTL_MS)
21315
+ unlinkSync3(full);
21316
+ } catch {
21317
+ }
21318
+ }
21319
+ }
21320
+ }
20915
21321
  function observeThrottlePath(folder) {
20916
21322
  return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}.observe-throttle`);
20917
21323
  }
@@ -21788,6 +22194,21 @@ function parseTimeLog(text) {
21788
22194
  }
21789
22195
  return out;
21790
22196
  }
22197
+ function readSessionTimeLines(sessionId, deps = {}) {
22198
+ try {
22199
+ const read = readConfig(deps.env ?? process.env);
22200
+ if (read.config === null)
22201
+ return [];
22202
+ const path = timeLogPath(read.config.guardEngagementId, deps.home);
22203
+ if (path === null)
22204
+ return [];
22205
+ const needle = JSON.stringify(sessionId);
22206
+ const kept = readFileSync11(path, "utf-8").split("\n").filter((raw) => raw.includes(needle));
22207
+ return parseTimeLog(kept.join("\n")).filter((line2) => line2.session === sessionId);
22208
+ } catch {
22209
+ return [];
22210
+ }
22211
+ }
21791
22212
  function shortHash(text) {
21792
22213
  return createHash2("sha256").update(text, "utf8").digest("hex").slice(0, 16);
21793
22214
  }
@@ -21809,8 +22230,8 @@ function separatorOf(path) {
21809
22230
  function subagentsFolderOf(transcript) {
21810
22231
  if (!transcript.endsWith(".jsonl") || transcript.length === ".jsonl".length)
21811
22232
  return null;
21812
- const sep4 = separatorOf(transcript);
21813
- return `${transcript.slice(0, -".jsonl".length)}${sep4}subagents`;
22233
+ const sep5 = separatorOf(transcript);
22234
+ return `${transcript.slice(0, -".jsonl".length)}${sep5}subagents`;
21814
22235
  }
21815
22236
  function subagentRecordName(agentId) {
21816
22237
  return /^[A-Za-z0-9_-]+$/.test(agentId) ? `agent-${agentId}.jsonl` : null;
@@ -22350,7 +22771,7 @@ async function pruneTimeLog(path, openedAt) {
22350
22771
  written = true;
22351
22772
  let seen = Buffer.byteLength(text, "utf-8");
22352
22773
  for (; ; ) {
22353
- await new Promise((resolve6) => setTimeout(resolve6, PRUNE_SETTLE_MS));
22774
+ await new Promise((resolve8) => setTimeout(resolve8, PRUNE_SETTLE_MS));
22354
22775
  const after = readFileSync11(aside);
22355
22776
  if (after.length <= seen)
22356
22777
  break;
@@ -22468,101 +22889,634 @@ async function sendTimeRecord(deps = {}) {
22468
22889
  }
22469
22890
  }
22470
22891
 
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();
22892
+ // dist/own-work.js
22893
+ import { appendFileSync as appendFileSync3, lstatSync, readFileSync as readFileSync12, realpathSync as realpathSync4 } from "node:fs";
22894
+ import { basename, dirname as dirname5, isAbsolute, join as join10, relative as relative4, resolve as resolve3, sep as sep2 } from "node:path";
22895
+
22896
+ // dist/risky/types.js
22897
+ var RISKY_ACTION_TYPES = [
22898
+ "delete-files",
22899
+ "discard-unsaved",
22900
+ "rewrite-shared-history",
22901
+ "delete-unmerged-work",
22902
+ "wipe-data",
22903
+ "read-credentials",
22904
+ "publish",
22905
+ "deploy",
22906
+ "shrink-record",
22907
+ "outside-own-copy"
22908
+ ];
22909
+ function riskyTypeOrder(type) {
22910
+ return RISKY_ACTION_TYPES.indexOf(type);
22477
22911
  }
22478
- async function evaluateEdit(reply, readSeat) {
22479
- let input;
22912
+ var CHECKED_TOOLS = ["Bash", "Read", "Grep", "Write", "Edit", "MultiEdit", "NotebookEdit", "Agent"];
22913
+ var RECORDED_ONLY_TOOLS = ["PowerShell"];
22914
+
22915
+ // dist/own-work.js
22916
+ var OWN_WORK_SHELL_TOOLS = ["Bash", "PowerShell"];
22917
+ var SHELL = new Set(OWN_WORK_SHELL_TOOLS);
22918
+ var MATCHED = /* @__PURE__ */ new Set([...CHECKED_TOOLS, ...RECORDED_ONLY_TOOLS]);
22919
+ function isOwnWorkShellTool(toolName) {
22920
+ return typeof toolName === "string" && SHELL.has(toolName) && MATCHED.has(toolName);
22921
+ }
22922
+ function ownWorkFileName(sessionId) {
22923
+ return `${OWN_WORK_PREFIX}${sessionId}${OWN_WORK_SUFFIX}`;
22924
+ }
22925
+ function ownWorkPath(sessionId, dir) {
22926
+ if (typeof sessionId !== "string" || !livenessIdOk(sessionId))
22927
+ return null;
22928
+ return join10(dir ?? markerDir(), ownWorkFileName(sessionId));
22929
+ }
22930
+ function inputString2(input, key) {
22931
+ if (typeof input !== "object" || input === null)
22932
+ return null;
22933
+ const value = input[key];
22934
+ return typeof value === "string" && value !== "" ? value : null;
22935
+ }
22936
+ function ownShellLine(input, at) {
22937
+ return { at: at.toISOString(), kind: "shell", agent: inputString2(input, "agent_id"), toolUse: inputString2(input, "tool_use_id") };
22938
+ }
22939
+ function ownEditLine(path, at) {
22940
+ return { at: at.toISOString(), kind: "edit", path };
22941
+ }
22942
+ function append(sessionId, line2, dir) {
22943
+ const path = ownWorkPath(sessionId, dir);
22944
+ if (path === null)
22945
+ return;
22946
+ appendFileSync3(path, `${JSON.stringify(line2)}
22947
+ `, { mode: 384 });
22948
+ }
22949
+ function recordOwnShell(input, at, deps = {}) {
22480
22950
  try {
22481
- const raw = await readStdin();
22482
- input = JSON.parse(raw);
22951
+ if (!isOwnWorkShellTool(inputString2(input, "tool_name")))
22952
+ return;
22953
+ append(inputString2(input, "session_id"), ownShellLine(input, at), deps.dir);
22483
22954
  } catch {
22484
- recordTimeEvent("edit", {});
22485
- return;
22486
22955
  }
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));
22956
+ }
22957
+ function recordOwnEdit(input, filePath, deps = {}) {
22958
+ try {
22959
+ if (filePath === "")
22960
+ return;
22961
+ const base = inputString2(input, "cwd") ?? process.cwd();
22962
+ const path = isAbsolute(filePath) ? filePath : resolve3(base, filePath);
22963
+ append(inputString2(input, "session_id"), ownEditLine(path, (deps.now ?? (() => /* @__PURE__ */ new Date()))()), deps.dir);
22964
+ } catch {
22965
+ }
22966
+ }
22967
+ function parseOwnWork(text) {
22968
+ const out = [];
22969
+ for (const raw of text.split("\n")) {
22970
+ if (raw === "")
22971
+ continue;
22972
+ try {
22973
+ const o = JSON.parse(raw);
22974
+ if (typeof o["at"] !== "string")
22975
+ continue;
22976
+ const nullable2 = (v) => v === null || typeof v === "string";
22977
+ if (o["kind"] === "edit" && typeof o["path"] === "string")
22978
+ out.push({ at: o["at"], kind: "edit", path: o["path"] });
22979
+ else if (o["kind"] === "shell" && nullable2(o["agent"]) && nullable2(o["toolUse"])) {
22980
+ out.push({ at: o["at"], kind: "shell", agent: o["agent"], toolUse: o["toolUse"] });
22981
+ }
22982
+ } catch {
22499
22983
  }
22500
- return;
22501
22984
  }
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;
22985
+ return out;
22986
+ }
22987
+ function readOwnWork(sessionId, dir) {
22988
+ try {
22989
+ const path = ownWorkPath(sessionId, dir);
22990
+ if (path === null)
22991
+ return [];
22992
+ return parseOwnWork(readFileSync12(path, "utf-8"));
22993
+ } catch {
22994
+ return [];
22520
22995
  }
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;
22996
+ }
22997
+ function shellWindows(own, timeLines, sessionId, passAt) {
22998
+ const out = [];
22999
+ for (const line2 of own) {
23000
+ if (line2.kind !== "shell")
23001
+ continue;
23002
+ const from = Date.parse(line2.at);
23003
+ if (Number.isNaN(from))
23004
+ continue;
23005
+ let to = passAt;
23006
+ for (const t of timeLines) {
23007
+ if (t.session !== sessionId || t.agent !== line2.agent)
23008
+ continue;
23009
+ const isOwnLine = line2.toolUse !== null ? t.toolUse === line2.toolUse : t.at === line2.at;
23010
+ if (isOwnLine)
23011
+ continue;
23012
+ const at = Date.parse(t.at);
23013
+ if (Number.isNaN(at) || at < from)
23014
+ continue;
23015
+ if (at < to)
23016
+ to = at;
22555
23017
  }
22556
- if (failure.kind === "contract") {
22557
- reply.notice(contractFaultText(failure.message));
22558
- return;
23018
+ out.push({ from, to: Math.max(from, to) });
23019
+ }
23020
+ return out;
23021
+ }
23022
+ function realPathOf(path) {
23023
+ const missing = [];
23024
+ let at = resolve3(path);
23025
+ for (; ; ) {
23026
+ try {
23027
+ return join10(realpathSync4.native(at), ...missing.reverse());
23028
+ } catch {
23029
+ const parent = dirname5(at);
23030
+ if (parent === at)
23031
+ return resolve3(path);
23032
+ missing.push(basename(at));
23033
+ at = parent;
22559
23034
  }
22560
- reply.notice(infraWarningText());
22561
- return;
22562
23035
  }
22563
- const telemetryConfig = readTelemetryConfig();
22564
- const run = buildGuardEvalRun({
22565
- engagementId: guardEngagementId,
23036
+ }
23037
+ function modifiedAt(path) {
23038
+ try {
23039
+ return lstatSync(path).mtimeMs;
23040
+ } catch {
23041
+ return null;
23042
+ }
23043
+ }
23044
+ function ownEditedFiles(repoRoot, sessionId, deps = {}) {
23045
+ const root = realPathOf(repoRoot);
23046
+ const out = [];
23047
+ const seen = /* @__PURE__ */ new Set();
23048
+ for (const line2 of readOwnWork(sessionId, deps.dir)) {
23049
+ if (line2.kind !== "edit")
23050
+ continue;
23051
+ const abs = realPathOf(line2.path);
23052
+ const rel = relative4(root, abs);
23053
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel))
23054
+ continue;
23055
+ if (modifiedAt(abs) === null)
23056
+ continue;
23057
+ const posix3 = rel.split(sep2).join("/");
23058
+ if (seen.has(posix3))
23059
+ continue;
23060
+ seen.add(posix3);
23061
+ out.push(posix3);
23062
+ }
23063
+ return out;
23064
+ }
23065
+ function ownChangedFiles(repoRoot, files, sessionId, deps = {}) {
23066
+ if (typeof sessionId !== "string" || ownWorkPath(sessionId, deps.dir) === null)
23067
+ return [...files];
23068
+ const own = readOwnWork(sessionId, deps.dir);
23069
+ if (own.length === 0)
23070
+ return [];
23071
+ const edited = /* @__PURE__ */ new Set();
23072
+ for (const line2 of own)
23073
+ if (line2.kind === "edit")
23074
+ edited.add(realPathOf(line2.path));
23075
+ const hasShell = own.some((line2) => line2.kind === "shell");
23076
+ const passAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
23077
+ const timeLines = hasShell ? readSessionTimeLines(sessionId, { env: deps.env, home: deps.home }) : [];
23078
+ const windows = hasShell ? shellWindows(own, timeLines, sessionId, passAt) : [];
23079
+ const inWindow = (mtimeMs) => {
23080
+ if (mtimeMs === null)
23081
+ return false;
23082
+ const ms = Math.floor(mtimeMs);
23083
+ return windows.some((w) => ms >= w.from && ms <= w.to);
23084
+ };
23085
+ const root = resolve3(repoRoot);
23086
+ return files.filter((rel) => {
23087
+ const abs = join10(root, rel);
23088
+ if (edited.has(realPathOf(abs)))
23089
+ return true;
23090
+ if (windows.length === 0)
23091
+ return false;
23092
+ const mtime = modifiedAt(abs);
23093
+ if (mtime !== null)
23094
+ return inWindow(mtime);
23095
+ return inWindow(modifiedAt(dirname5(abs)));
23096
+ });
23097
+ }
23098
+
23099
+ // dist/hooks/findings.js
23100
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
23101
+ import { closeSync, existsSync as existsSync6, openSync, readFileSync as readFileSync13, renameSync as renameSync4, statSync as statSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
23102
+ function findingKey(firing) {
23103
+ return createHash3("sha256").update(`${firing.guardId}
23104
+ ${firing.path ?? ""}
23105
+ ${firing.explanation}`).digest("hex");
23106
+ }
23107
+ function asFinding(raw) {
23108
+ if (typeof raw !== "object" || raw === null)
23109
+ return null;
23110
+ const o = raw;
23111
+ const str = (k) => typeof o[k] === "string";
23112
+ const bool = (k) => typeof o[k] === "boolean";
23113
+ if (!str("key") || !str("folder") || !str("guardId") || !str("patternRef") || !str("explanation") || !str("foundAt"))
23114
+ return null;
23115
+ if (o["severity"] !== "info" && o["severity"] !== "warn")
23116
+ return null;
23117
+ if (!bool("announced") || !bool("delivered") || !bool("escalated"))
23118
+ return null;
23119
+ if (o["path"] !== void 0 && typeof o["path"] !== "string")
23120
+ return null;
23121
+ return {
23122
+ key: o["key"],
23123
+ folder: o["folder"],
23124
+ guardId: o["guardId"],
23125
+ patternRef: o["patternRef"],
23126
+ severity: o["severity"],
23127
+ explanation: o["explanation"],
23128
+ ...typeof o["path"] === "string" ? { path: o["path"] } : {},
23129
+ foundAt: o["foundAt"],
23130
+ announced: o["announced"],
23131
+ delivered: o["delivered"],
23132
+ escalated: o["escalated"]
23133
+ };
23134
+ }
23135
+ function emptyFindings(session) {
23136
+ return { version: 1, session, findings: [] };
23137
+ }
23138
+ function readFindings(sessionId) {
23139
+ const path = findingsPath(sessionId);
23140
+ if (path === null)
23141
+ return null;
23142
+ const session = sessionId;
23143
+ try {
23144
+ const parsed = JSON.parse(readFileSync13(path, "utf-8"));
23145
+ if (parsed["version"] !== 1 || !Array.isArray(parsed["findings"]))
23146
+ return emptyFindings(session);
23147
+ const findings = [];
23148
+ for (const raw of parsed["findings"]) {
23149
+ const f = asFinding(raw);
23150
+ if (f !== null)
23151
+ findings.push(f);
23152
+ }
23153
+ return { version: 1, session, findings };
23154
+ } catch {
23155
+ return emptyFindings(session);
23156
+ }
23157
+ }
23158
+ function writeFindings(file2) {
23159
+ const path = findingsPath(file2.session);
23160
+ if (path === null)
23161
+ return;
23162
+ const temp = `${path.slice(0, -".json".length)}.${process.pid}.tmp.json`;
23163
+ try {
23164
+ writeFileSync6(temp, `${JSON.stringify(file2)}
23165
+ `, { mode: 384 });
23166
+ renameSync4(temp, path);
23167
+ } catch {
23168
+ try {
23169
+ unlinkSync5(temp);
23170
+ } catch {
23171
+ }
23172
+ }
23173
+ }
23174
+ var FINDINGS_LOCK_WAIT_MS = 1e3;
23175
+ var FINDINGS_LOCK_STALE_MS = 5e3;
23176
+ function lockPathFor(path) {
23177
+ return `${path.slice(0, -".json".length)}.lock.json`;
23178
+ }
23179
+ function pause(ms) {
23180
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23181
+ }
23182
+ function takeOverStaleLock(lock) {
23183
+ const aside = `${lock}.${process.pid}.${randomUUID2()}.stale`;
23184
+ try {
23185
+ renameSync4(lock, aside);
23186
+ } catch {
23187
+ return false;
23188
+ }
23189
+ let staleIndeed = false;
23190
+ try {
23191
+ staleIndeed = Date.now() - statSync3(aside).mtimeMs > FINDINGS_LOCK_STALE_MS;
23192
+ } catch {
23193
+ staleIndeed = false;
23194
+ }
23195
+ if (!staleIndeed) {
23196
+ try {
23197
+ renameSync4(aside, lock);
23198
+ } catch {
23199
+ try {
23200
+ unlinkSync5(aside);
23201
+ } catch {
23202
+ }
23203
+ }
23204
+ return false;
23205
+ }
23206
+ try {
23207
+ unlinkSync5(aside);
23208
+ } catch {
23209
+ }
23210
+ return true;
23211
+ }
23212
+ function withFindingsLock(sessionId, body, waitMs = FINDINGS_LOCK_WAIT_MS) {
23213
+ const path = findingsPath(sessionId);
23214
+ if (path === null)
23215
+ return void 0;
23216
+ const lock = lockPathFor(path);
23217
+ const deadline = Date.now() + waitMs;
23218
+ for (; ; ) {
23219
+ try {
23220
+ closeSync(openSync(lock, "wx", 384));
23221
+ break;
23222
+ } catch (err) {
23223
+ if (err.code !== "EEXIST")
23224
+ return void 0;
23225
+ let stale = false;
23226
+ try {
23227
+ stale = Date.now() - statSync3(lock).mtimeMs > FINDINGS_LOCK_STALE_MS;
23228
+ } catch {
23229
+ continue;
23230
+ }
23231
+ if (stale && takeOverStaleLock(lock))
23232
+ continue;
23233
+ if (Date.now() >= deadline)
23234
+ return void 0;
23235
+ pause(10);
23236
+ }
23237
+ }
23238
+ try {
23239
+ return body();
23240
+ } finally {
23241
+ try {
23242
+ unlinkSync5(lock);
23243
+ } catch {
23244
+ }
23245
+ }
23246
+ }
23247
+ function takeUndelivered(file2, folder) {
23248
+ const taken = [];
23249
+ const findings = file2.findings.map((f) => {
23250
+ if (f.delivered || folder !== null && f.folder !== folder)
23251
+ return f;
23252
+ taken.push(f);
23253
+ return { ...f, delivered: true };
23254
+ });
23255
+ return { file: { ...file2, findings }, taken };
23256
+ }
23257
+ function deliverFindings(sessionId, folder) {
23258
+ const path = findingsPath(sessionId);
23259
+ if (path === null || !existsSync6(path))
23260
+ return [];
23261
+ try {
23262
+ const taken = withFindingsLock(sessionId, () => {
23263
+ const before = readFindings(sessionId);
23264
+ if (before === null)
23265
+ return [];
23266
+ const { file: file2, taken: given } = takeUndelivered(before, folder);
23267
+ if (given.length > 0)
23268
+ writeFindings(file2);
23269
+ return given;
23270
+ });
23271
+ return taken ?? [];
23272
+ } catch {
23273
+ return [];
23274
+ }
23275
+ }
23276
+ function turnEndBookkeeping(file2, folder, outcome, topLevel, now) {
23277
+ const recorded = [...file2.findings];
23278
+ const firing = /* @__PURE__ */ new Map();
23279
+ const notRun = new Set(outcome.kind === "evaluated" ? outcome.notRun : []);
23280
+ const observed = (f) => f.folder === folder && !notRun.has(f.patternRef);
23281
+ if (outcome.kind === "evaluated") {
23282
+ for (const g of outcome.fired) {
23283
+ if (g.severity === "block")
23284
+ continue;
23285
+ const key = findingKey(g);
23286
+ if (!firing.has(key))
23287
+ firing.set(key, g);
23288
+ }
23289
+ }
23290
+ const removed = [];
23291
+ const kept = [];
23292
+ for (const f of recorded) {
23293
+ const closes = observed(f) && (outcome.kind === "no-own-changes" || outcome.kind === "evaluated" && !firing.has(f.key));
23294
+ (closes ? removed : kept).push(f);
23295
+ }
23296
+ const known = new Set(kept.filter((f) => f.folder === folder).map((f) => f.key));
23297
+ for (const [key, g] of firing) {
23298
+ if (known.has(key))
23299
+ continue;
23300
+ kept.push({
23301
+ key,
23302
+ folder,
23303
+ guardId: g.guardId,
23304
+ patternRef: g.patternRef,
23305
+ severity: g.severity,
23306
+ explanation: g.explanation,
23307
+ ...g.path !== void 0 ? { path: g.path } : {},
23308
+ foundAt: now.toISOString(),
23309
+ announced: false,
23310
+ delivered: false,
23311
+ escalated: false
23312
+ });
23313
+ }
23314
+ if (!topLevel) {
23315
+ return { file: { ...file2, findings: kept }, news: { fixed: 0, allFixed: false, found: 0, stillOpen: [] } };
23316
+ }
23317
+ const fixed = removed.filter((f) => f.announced).length;
23318
+ const allFixed = !kept.some((f) => f.announced);
23319
+ let found2 = 0;
23320
+ const stillOpen = [];
23321
+ const next = kept.map((f) => {
23322
+ if (!f.announced) {
23323
+ found2 += 1;
23324
+ return { ...f, announced: true };
23325
+ }
23326
+ if (f.delivered && !f.escalated && observed(f)) {
23327
+ stillOpen.push(f.explanation);
23328
+ return { ...f, escalated: true };
23329
+ }
23330
+ return f;
23331
+ });
23332
+ return { file: { ...file2, findings: next }, news: { fixed, allFixed, found: found2, stillOpen } };
23333
+ }
23334
+ function blockingExplanations(fired) {
23335
+ const blocks = fired.filter((g) => g.severity === "block");
23336
+ return (blocks.length > 0 ? blocks : fired).map((g) => g.explanation);
23337
+ }
23338
+
23339
+ // dist/hooks/finding-lines.js
23340
+ function bullets(explanations) {
23341
+ return explanations.map((e) => `- ${e}`).join("\n");
23342
+ }
23343
+ function distinctBullets(explanations) {
23344
+ return bullets([...new Set(explanations)]);
23345
+ }
23346
+ function foundLine(count) {
23347
+ 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.`;
23348
+ }
23349
+ function fixedLine(count, all) {
23350
+ if (all)
23351
+ return "Halfcycle: everything it found earlier is now fixed.";
23352
+ 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.`;
23353
+ }
23354
+ function stillOpenMessage(explanations) {
23355
+ const n = explanations.length;
23356
+ 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.`;
23357
+ 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.";
23358
+ return `${head}
23359
+ ${bullets(explanations)}
23360
+ ${tail}`;
23361
+ }
23362
+ function stoppedMessage(explanations) {
23363
+ return [
23364
+ "Halfcycle stopped this step because something in the agent's work has to be fixed first:",
23365
+ distinctBullets(explanations),
23366
+ "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."
23367
+ ].join("\n");
23368
+ }
23369
+ function couldNotFixMessage(explanations) {
23370
+ return [
23371
+ "Halfcycle: the agent could not fix what stopped its work:",
23372
+ distinctBullets(explanations),
23373
+ "Ask the agent what is in the way, or tell it how you want this handled."
23374
+ ].join("\n");
23375
+ }
23376
+ function notRunMessage(reasons) {
23377
+ if (reasons.length === 0)
23378
+ return "";
23379
+ const head = reasons.length === 1 ? "Halfcycle: 1 check could not run on this change." : `Halfcycle: ${reasons.length} checks could not run on this change.`;
23380
+ return `${head}
23381
+ ${distinctBullets(reasons)}`;
23382
+ }
23383
+ function blockReason(explanations) {
23384
+ return `Halfcycle stopped this step. Fix each of these before you carry on:
23385
+ ${distinctBullets(explanations)}`;
23386
+ }
23387
+ function detailsBlock(findings, projectFolder2) {
23388
+ const lines2 = findings.map((f) => {
23389
+ const where = f.folder === projectFolder2 ? f.path : f.path === void 0 ? f.folder : `${f.folder}/${f.path}`;
23390
+ const at = where === void 0 ? "" : ` in ${where}`;
23391
+ return ` [${f.severity.toUpperCase()}] ${f.patternRef}${at}: ${f.explanation}`;
23392
+ });
23393
+ return [
23394
+ "Halfcycle's checks found these in this session's own changes at the end of an earlier turn. Fix each one now, as part of this turn, before anything else; then carry on with what the person asks. Do not ask the person whether to fix them, and do not end your turn with a question before they are fixed. If the person later tells you to keep one, that is their call. When you mention them to the person, use plain words, not the labels in brackets.",
23395
+ ...lines2
23396
+ ].join("\n");
23397
+ }
23398
+ function turnEndMessage(news) {
23399
+ const parts = [];
23400
+ if (news.fixed > 0)
23401
+ parts.push(fixedLine(news.fixed, news.allFixed));
23402
+ if (news.found > 0)
23403
+ parts.push(foundLine(news.found));
23404
+ if (news.stillOpen.length > 0)
23405
+ parts.push(stillOpenMessage(news.stillOpen));
23406
+ return parts.length === 0 ? null : parts.join("\n");
23407
+ }
23408
+
23409
+ // dist/hooks/post-tool-use.js
23410
+ var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
23411
+ async function runPostToolUse(readSeat = readSeatConfiguration) {
23412
+ const reply = hookReply("PostToolUse");
23413
+ const at = { session: void 0, folder: null };
23414
+ await evaluateEdit(reply, readSeat, at);
23415
+ handOverAtEdit(reply, at);
23416
+ reply.flush();
23417
+ }
23418
+ function handOverAtEdit(reply, at) {
23419
+ if (at.folder === null)
23420
+ return;
23421
+ const code = process.exitCode;
23422
+ if (code !== void 0 && code !== null && Number(code) !== 0)
23423
+ return;
23424
+ const given = deliverFindings(at.session, at.folder);
23425
+ if (given.length === 0)
23426
+ return;
23427
+ reply.agentContext(detailsBlock(given, realPathOf(projectFolder())));
23428
+ }
23429
+ async function evaluateEdit(reply, readSeat, at) {
23430
+ let input;
23431
+ try {
23432
+ const raw = await readStdin();
23433
+ input = JSON.parse(raw);
23434
+ } catch {
23435
+ recordTimeEvent("edit", {});
23436
+ return;
23437
+ }
23438
+ recordTimeEvent("edit", input);
23439
+ const toolName = input.tool_name ?? "";
23440
+ if (!HANDLED_EDIT_TOOLS.has(toolName))
23441
+ return;
23442
+ const isNotebook = toolName === "NotebookEdit";
23443
+ const absFilePath = isNotebook ? input.tool_input?.notebook_path : input.tool_input?.file_path;
23444
+ if (!absFilePath || typeof absFilePath !== "string")
23445
+ return;
23446
+ recordOwnEdit(input, absFilePath);
23447
+ const repoRootResult = getRepoRoot(process.cwd());
23448
+ if (!repoRootResult.ok) {
23449
+ if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
23450
+ reply.notice(noRepositoryText(repoRootResult));
23451
+ }
23452
+ return;
23453
+ }
23454
+ const repoRoot = repoRootResult.value;
23455
+ at.session = input.session_id;
23456
+ at.folder = realPathOf(repoRoot);
23457
+ const diffContent = isNotebook ? "" : getSingleFileDiff(absFilePath, repoRoot);
23458
+ const { relative: pathRelative } = await import("node:path");
23459
+ const relPath = pathRelative(repoRoot, absFilePath);
23460
+ if (isInstallerOutput(relPath))
23461
+ return;
23462
+ const context = loadChangeSetContext(repoRoot);
23463
+ const changeSet = buildChangeSet([{ path: relPath, diffContent }], {
23464
+ ...context,
23465
+ excerpts: readExcerptConfig()
23466
+ });
23467
+ const configResult = readConfig();
23468
+ if (!configResult.config) {
23469
+ await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
23470
+ if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
23471
+ return;
23472
+ emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
23473
+ return;
23474
+ }
23475
+ const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
23476
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
23477
+ const evaluation = await evaluateWithRefresh({
23478
+ guardServiceUrl,
23479
+ token: guardServiceToken,
23480
+ engagementId: guardEngagementId,
23481
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration),
23482
+ warn: warnOnceThisSession("credential-renewed-for-this-run", repoRoot, input.session_id)
23483
+ });
23484
+ const clientResult = evaluation.result;
23485
+ if (!clientResult.ok) {
23486
+ const failure = classifyFailure(clientResult);
23487
+ await emitRunRecord({
23488
+ engagementId: guardEngagementId,
23489
+ runType: "hook",
23490
+ outcome: outcomeForClientFailure(clientResult.kind),
23491
+ failureReason: failure.reason,
23492
+ changeSet,
23493
+ credential: evaluation.token,
23494
+ credentialRefused: failure.kind === "credential"
23495
+ });
23496
+ if (failure.kind === "credential") {
23497
+ if (!firstNoticeThisSession("credential-rejected", repoRoot, input.session_id))
23498
+ return;
23499
+ emitCredentialRejected(
23500
+ failure.statusCode,
23501
+ failure.message,
23502
+ CREDENTIAL_REJECTED_EXIT_TOOL_USE,
23503
+ // The remedy the renewal attempt earned: the sign-in when the account
23504
+ // credential is the thing that was refused, and otherwise the plane's own
23505
+ // sentence about why it would not mint.
23506
+ evaluation.remedy
23507
+ );
23508
+ return;
23509
+ }
23510
+ if (failure.kind === "contract") {
23511
+ reply.notice(contractFaultText(failure.message));
23512
+ return;
23513
+ }
23514
+ reply.notice(infraWarningText());
23515
+ return;
23516
+ }
23517
+ const telemetryConfig = readTelemetryConfig();
23518
+ const run = buildGuardEvalRun({
23519
+ engagementId: guardEngagementId,
22566
23520
  runType: "hook",
22567
23521
  // The phase this run belongs to, as the client named it — it names the
22568
23522
  // guard-eval log file the run is appended to. Absent → the run is unphased.
@@ -22597,10 +23551,10 @@ ${notRun}` : "";
22597
23551
  return;
22598
23552
  }
22599
23553
  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);
23554
+ const personNotRun = notRunMessage((envelope.notRun ?? []).map((n) => n.reason));
23555
+ const reason = `${blockReason(envelope.guardsFired.map((g) => g.explanation))}${personNotRun ? `
23556
+ ${personNotRun}` : ""}`;
23557
+ reply.block(reason, stoppedMessage(blockingExplanations(envelope.guardsFired)));
22604
23558
  } else {
22605
23559
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
22606
23560
  reply.notice(`[Halfcycle] Guard warning: ${envelope.explanation}
@@ -22608,17 +23562,17 @@ ${guardLines}${notRunBlock}`);
22608
23562
  }
22609
23563
  }
22610
23564
  function readStdin() {
22611
- return new Promise((resolve6, reject) => {
23565
+ return new Promise((resolve8, reject) => {
22612
23566
  const chunks = [];
22613
23567
  process.stdin.on("data", (chunk) => chunks.push(chunk));
22614
- process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8")));
23568
+ process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
22615
23569
  process.stdin.on("error", reject);
22616
23570
  });
22617
23571
  }
22618
23572
 
22619
23573
  // 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";
23574
+ 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";
23575
+ import { join as join11 } from "node:path";
22622
23576
  var LIVE_WINDOW_MS = 10 * 60 * 1e3;
22623
23577
  function field(input, key) {
22624
23578
  if (typeof input !== "object" || input === null)
@@ -22627,10 +23581,10 @@ function field(input, key) {
22627
23581
  return typeof v === "string" && v !== "" ? v : null;
22628
23582
  }
22629
23583
  function indexEntry(dir, sessionId, name) {
22630
- const folder = join10(dir, livenessIndexDirName(sessionId));
23584
+ const folder = join11(dir, livenessIndexDirName(sessionId));
22631
23585
  for (let attempt2 = 0; attempt2 < 3; attempt2++) {
22632
23586
  try {
22633
- closeSync(openSync(join10(folder, name), "a", 384));
23587
+ closeSync2(openSync2(join11(folder, name), "a", 384));
22634
23588
  return true;
22635
23589
  } catch (err) {
22636
23590
  if (err.code !== "ENOENT")
@@ -22641,10 +23595,10 @@ function indexEntry(dir, sessionId, name) {
22641
23595
  tmp = mkdtempSync(`${folder}.`);
22642
23596
  for (const existing of readdirSync5(dir)) {
22643
23597
  if (parseLivenessFileName(existing)?.sessionId === sessionId)
22644
- closeSync(openSync(join10(tmp, existing), "a", 384));
23598
+ closeSync2(openSync2(join11(tmp, existing), "a", 384));
22645
23599
  }
22646
- closeSync(openSync(join10(tmp, name), "a", 384));
22647
- renameSync4(tmp, folder);
23600
+ closeSync2(openSync2(join11(tmp, name), "a", 384));
23601
+ renameSync5(tmp, folder);
22648
23602
  return true;
22649
23603
  } catch {
22650
23604
  if (tmp !== null)
@@ -22669,18 +23623,18 @@ function recordLiveness(input, deps = {}) {
22669
23623
  const name = livenessFileName(livenessKey(copy?.common ?? null), sessionId, agentId);
22670
23624
  if (!indexEntry(dir, sessionId, name))
22671
23625
  return null;
22672
- const path = join10(dir, name);
23626
+ const path = join11(dir, name);
22673
23627
  const text = `${cwd}
22674
23628
  ${copy?.top ?? ""}
22675
23629
  `;
22676
23630
  try {
22677
- writeFileSync6(path, text, { encoding: "utf-8", flag: "wx", mode: 384 });
23631
+ writeFileSync7(path, text, { encoding: "utf-8", flag: "wx", mode: 384 });
22678
23632
  return { firstSeen: true };
22679
23633
  } catch (err) {
22680
23634
  if (err.code !== "EEXIST")
22681
23635
  return null;
22682
23636
  }
22683
- writeFileSync6(path, text, { encoding: "utf-8", mode: 384 });
23637
+ writeFileSync7(path, text, { encoding: "utf-8", mode: 384 });
22684
23638
  return { firstSeen: false };
22685
23639
  } catch {
22686
23640
  return null;
@@ -22699,7 +23653,7 @@ function clearLiveness(input, deps = {}) {
22699
23653
  if (p === null || p.sessionId !== sessionId || p.agentId !== agentId)
22700
23654
  continue;
22701
23655
  try {
22702
- unlinkSync5(join10(dir, name));
23656
+ unlinkSync6(join11(dir, name));
22703
23657
  removed += 1;
22704
23658
  } catch {
22705
23659
  }
@@ -22712,7 +23666,7 @@ function clearLiveness(input, deps = {}) {
22712
23666
  }
22713
23667
  function topOf(path) {
22714
23668
  try {
22715
- const top = readFileSync12(path, "utf-8").split("\n")[1] ?? "";
23669
+ const top = readFileSync14(path, "utf-8").split("\n")[1] ?? "";
22716
23670
  return top === "" ? null : top;
22717
23671
  } catch {
22718
23672
  return null;
@@ -22734,10 +23688,10 @@ function readLiveness(sessionId, cwd, deps = {}) {
22734
23688
  const p = parseLivenessFileName(name);
22735
23689
  if (p === null || p.key !== key || p.sessionId !== sessionId)
22736
23690
  continue;
22737
- const full = join10(dir, name);
23691
+ const full = join11(dir, name);
22738
23692
  let seenAt;
22739
23693
  try {
22740
- seenAt = statSync3(full).mtimeMs;
23694
+ seenAt = statSync4(full).mtimeMs;
22741
23695
  } catch {
22742
23696
  continue;
22743
23697
  }
@@ -22775,17 +23729,17 @@ function runsBesideOtherWork(toolInput, record2) {
22775
23729
  }
22776
23730
 
22777
23731
  // dist/pace-hook.js
22778
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, renameSync as renameSync8, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "node:fs";
23732
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync18, renameSync as renameSync9, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
22779
23733
  import nodePath4 from "node:path";
22780
23734
 
22781
23735
  // 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";
23736
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync8, statSync as statSync6, writeFileSync as writeFileSync10 } from "node:fs";
22783
23737
  import { homedir as homedir6 } from "node:os";
22784
23738
  import nodePath3 from "node:path";
22785
23739
 
22786
23740
  // 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";
23741
+ import { createHash as createHash4 } from "node:crypto";
23742
+ 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
23743
  import { homedir as homedir5 } from "node:os";
22790
23744
  import nodePath from "node:path";
22791
23745
  var DEFAULT_CONFIG_FOLDER_NAME = ".claude";
@@ -22834,7 +23788,7 @@ function pacingDir(home) {
22834
23788
  }
22835
23789
  function readJsonObject(path) {
22836
23790
  try {
22837
- const v = JSON.parse(readFileSync13(path, "utf-8"));
23791
+ const v = JSON.parse(readFileSync15(path, "utf-8"));
22838
23792
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
22839
23793
  } catch {
22840
23794
  return null;
@@ -22842,9 +23796,9 @@ function readJsonObject(path) {
22842
23796
  }
22843
23797
  function writeJsonWhole(path, value) {
22844
23798
  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);
23799
+ const tmp = `${path}.${String(process.pid)}.${createHash4("sha1").update(String(Math.random())).digest("hex").slice(0, 8)}.tmp`;
23800
+ writeFileSync8(tmp, JSON.stringify(value), { mode: 384 });
23801
+ renameSync6(tmp, path);
22848
23802
  }
22849
23803
  function writeAccountEntry(path, key, entry) {
22850
23804
  const current = readJsonObject(path);
@@ -22864,6 +23818,62 @@ function accountKeysOf(path) {
22864
23818
  const accounts = current?.["accounts"];
22865
23819
  return typeof accounts === "object" && accounts !== null && !Array.isArray(accounts) ? Object.keys(accounts) : [];
22866
23820
  }
23821
+ var PACE_STATE_FILENAME = "state.json";
23822
+ function paceStateEntryOf(v) {
23823
+ if (typeof v !== "object" || v === null || Array.isArray(v))
23824
+ return null;
23825
+ const o = v;
23826
+ const pace = paceStateSchema.safeParse(o["pace"]);
23827
+ const kind = o["windowKind"] === null ? null : paceWindowKindSchema.safeParse(o["windowKind"]);
23828
+ const allowance = o["allowance"];
23829
+ const number4 = o["number"];
23830
+ const until = o["until"];
23831
+ const at = o["at"];
23832
+ if (!pace.success || kind !== null && !kind.success)
23833
+ return null;
23834
+ if (typeof allowance !== "number" || !Number.isInteger(allowance) || allowance < 0)
23835
+ return null;
23836
+ if (typeof number4 !== "number" || !Number.isInteger(number4) || number4 < 1 || number4 > PACE_AGENTS_MAX)
23837
+ return null;
23838
+ if (until !== null && (typeof until !== "string" || Number.isNaN(Date.parse(until))))
23839
+ return null;
23840
+ if (typeof at !== "string" || Number.isNaN(Date.parse(at)))
23841
+ return null;
23842
+ return { pace: pace.data, allowance, number: number4, windowKind: kind === null ? null : kind.data, until, at };
23843
+ }
23844
+ function rememberedBasisOf(v) {
23845
+ if (typeof v !== "object" || v === null || Array.isArray(v))
23846
+ return null;
23847
+ const o = v;
23848
+ const phase = o["phase"] === null ? null : phaseNameSchema.safeParse(o["phase"]);
23849
+ const plan = o["claudePlan"] === null ? null : claudePlanSchema.safeParse(o["claudePlan"]);
23850
+ const keep = keepFreePercentSchema.safeParse(o["keepFreePercent"]);
23851
+ if (phase !== null && !phase.success || plan !== null && !plan.success || !keep.success)
23852
+ return null;
23853
+ return { phase: phase === null ? null : phase.data, claudePlan: plan === null ? null : plan.data, keepFreePercent: keep.data };
23854
+ }
23855
+ function readPaceStateFile(dir) {
23856
+ const o = readJsonObject(nodePath.join(dir, PACE_STATE_FILENAME));
23857
+ const accounts = {};
23858
+ const rawAccounts = o?.["accounts"];
23859
+ if (typeof rawAccounts === "object" && rawAccounts !== null && !Array.isArray(rawAccounts)) {
23860
+ for (const [key, v] of Object.entries(rawAccounts)) {
23861
+ const e = paceStateEntryOf(v);
23862
+ if (e !== null)
23863
+ accounts[key] = e;
23864
+ }
23865
+ }
23866
+ return { accounts, basis: rememberedBasisOf(o?.["basis"]) };
23867
+ }
23868
+ function writePaceState(dir, key, entry, basis) {
23869
+ const current = readPaceStateFile(dir);
23870
+ const accounts = { ...current.accounts };
23871
+ if (entry === null)
23872
+ delete accounts[key];
23873
+ else
23874
+ accounts[key] = entry;
23875
+ writeJsonWhole(nodePath.join(dir, PACE_STATE_FILENAME), { accounts, basis: basis ?? current.basis });
23876
+ }
22867
23877
  function recordFactsOf(o) {
22868
23878
  if (o["type"] !== "assistant")
22869
23879
  return null;
@@ -22979,7 +23989,7 @@ var NEWLINE = 10;
22979
23989
  var ASSISTANT = Buffer.from('"assistant"');
22980
23990
  var READ_CHUNK = 8 * 1024 * 1024;
22981
23991
  function readWholeLines(path, from, size, onLine) {
22982
- const fd = openSync2(path, "r");
23992
+ const fd = openSync3(path, "r");
22983
23993
  try {
22984
23994
  let pos = from;
22985
23995
  let carry = Buffer.alloc(0);
@@ -23007,11 +24017,11 @@ function readWholeLines(path, from, size, onLine) {
23007
24017
  }
23008
24018
  return consumed;
23009
24019
  } finally {
23010
- closeSync2(fd);
24020
+ closeSync3(fd);
23011
24021
  }
23012
24022
  }
23013
24023
  function hashId(id) {
23014
- return createHash3("sha1").update(id).digest("base64url").slice(0, 16);
24024
+ return createHash4("sha1").update(id).digest("base64url").slice(0, 16);
23015
24025
  }
23016
24026
  function scanRecords(key, deps = {}) {
23017
24027
  const nowMs = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
@@ -23036,7 +24046,7 @@ function scanRecords(key, deps = {}) {
23036
24046
  let size;
23037
24047
  let mtimeSec;
23038
24048
  try {
23039
- const st = statSync4(file2.path);
24049
+ const st = statSync5(file2.path);
23040
24050
  size = st.size;
23041
24051
  mtimeSec = Math.floor(st.mtimeMs / 1e3);
23042
24052
  } catch {
@@ -23159,7 +24169,7 @@ function tokensBetween(minutes, fromSec, toSec) {
23159
24169
  }
23160
24170
  function resolvedRecords(key, path) {
23161
24171
  try {
23162
- return realpathSync4(recordsFolderOf(key, path));
24172
+ return realpathSync5(recordsFolderOf(key, path));
23163
24173
  } catch {
23164
24174
  return null;
23165
24175
  }
@@ -23172,7 +24182,7 @@ function timeLogTranscripts(home) {
23172
24182
  continue;
23173
24183
  let text;
23174
24184
  try {
23175
- text = readFileSync13(nodePath.join(engagements, e.name, TIME_LOG_DIR_NAME, TIME_LOG_FILENAME), "utf-8");
24185
+ text = readFileSync15(nodePath.join(engagements, e.name, TIME_LOG_DIR_NAME, TIME_LOG_FILENAME), "utf-8");
23176
24186
  } catch {
23177
24187
  continue;
23178
24188
  }
@@ -23332,7 +24342,7 @@ function knownWindowOf(learned, reported, now) {
23332
24342
 
23333
24343
  // dist/scan-lock.js
23334
24344
  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";
24345
+ import { linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync16, renameSync as renameSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "node:fs";
23336
24346
  import nodePath2 from "node:path";
23337
24347
  var SCAN_LOCK_FILENAME = "scan.lock";
23338
24348
  var SCAN_LOCK_STALE_MS = 15 * 60 * 1e3;
@@ -23343,7 +24353,7 @@ function scanLockPath(home) {
23343
24353
  function readLockBody(path) {
23344
24354
  let text;
23345
24355
  try {
23346
- text = readFileSync14(path, "utf-8");
24356
+ text = readFileSync16(path, "utf-8");
23347
24357
  } catch (err) {
23348
24358
  return err.code === "ENOENT" ? "missing" : null;
23349
24359
  }
@@ -23373,7 +24383,7 @@ function isStale(body, staleMs, deps) {
23373
24383
  return !(deps.alive ?? processAlive)(body.pid);
23374
24384
  }
23375
24385
  function writeLock(path, pid, deps, flag) {
23376
- writeFileSync8(path, JSON.stringify({ pid, at: (deps.clock ?? Date.now)() }), { flag, mode: 384 });
24386
+ writeFileSync9(path, JSON.stringify({ pid, at: (deps.clock ?? Date.now)() }), { flag, mode: 384 });
23377
24387
  }
23378
24388
  function tryTakeScanLock(deps = {}) {
23379
24389
  return tryTakeLockFile(scanLockPath(deps.home), SCAN_LOCK_STALE_MS, deps);
@@ -23400,7 +24410,7 @@ function tryTakeLockFile(path, staleMs, deps = {}) {
23400
24410
  return false;
23401
24411
  const aside = `${path}.${String(pid)}.${String((deps.clock ?? Date.now)())}.stale`;
23402
24412
  try {
23403
- renameSync6(path, aside);
24413
+ renameSync7(path, aside);
23404
24414
  } catch {
23405
24415
  continue;
23406
24416
  }
@@ -23412,13 +24422,13 @@ function tryTakeLockFile(path, staleMs, deps = {}) {
23412
24422
  } catch {
23413
24423
  }
23414
24424
  try {
23415
- unlinkSync6(aside);
24425
+ unlinkSync7(aside);
23416
24426
  } catch {
23417
24427
  }
23418
24428
  return false;
23419
24429
  }
23420
24430
  try {
23421
- unlinkSync6(aside);
24431
+ unlinkSync7(aside);
23422
24432
  } catch {
23423
24433
  }
23424
24434
  }
@@ -23429,7 +24439,7 @@ function releaseLockFile(path, pid = process.pid) {
23429
24439
  if (body === "missing" || body === null || body.pid !== pid)
23430
24440
  return;
23431
24441
  try {
23432
- unlinkSync6(path);
24442
+ unlinkSync7(path);
23433
24443
  } catch {
23434
24444
  }
23435
24445
  }
@@ -23620,7 +24630,7 @@ function readBuildNumber(engagementId, home) {
23620
24630
  if (dir === null)
23621
24631
  return null;
23622
24632
  try {
23623
- const v = JSON.parse(readFileSync15(nodePath3.join(dir, BUILD_NUMBER_FILENAME), "utf-8"));
24633
+ const v = JSON.parse(readFileSync17(nodePath3.join(dir, BUILD_NUMBER_FILENAME), "utf-8"));
23624
24634
  const n = v["number"];
23625
24635
  if (typeof v["phase"] === "string" && typeof n === "number" && Number.isInteger(n) && n >= 1 && n <= PACE_AGENTS_MAX && typeof v["namedByOwner"] === "boolean") {
23626
24636
  return { phase: v["phase"], number: n, namedByOwner: v["namedByOwner"] };
@@ -23640,12 +24650,12 @@ function writeBuildNumber(engagementId, value, home) {
23640
24650
  mkdirSync6(dir, { recursive: true, mode: 448 });
23641
24651
  const path = nodePath3.join(dir, BUILD_NUMBER_FILENAME);
23642
24652
  const tmp = `${path}.${String(process.pid)}.tmp`;
23643
- writeFileSync9(tmp, JSON.stringify(value), { mode: 384 });
23644
- renameSync7(tmp, path);
24653
+ writeFileSync10(tmp, JSON.stringify(value), { mode: 384 });
24654
+ renameSync8(tmp, path);
23645
24655
  }
23646
24656
  function readRememberedClaudePlan(home) {
23647
24657
  try {
23648
- const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync15(nodePath3.join(home ?? homedir6(), HALFCYCLE_DIR_NAME, REMEMBERED_CLAUDE_PLAN_FILENAME), "utf-8")));
24658
+ const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync17(nodePath3.join(home ?? homedir6(), HALFCYCLE_DIR_NAME, REMEMBERED_CLAUDE_PLAN_FILENAME), "utf-8")));
23649
24659
  return parsed.success ? parsed.data.claudePlan : null;
23650
24660
  } catch {
23651
24661
  return null;
@@ -23665,7 +24675,7 @@ function liveSubAgentsIn(cwd, deps = {}) {
23665
24675
  if (p === null || p.key !== key || p.agentId === null)
23666
24676
  continue;
23667
24677
  try {
23668
- if (now - statSync5(nodePath3.join(dir, name)).mtimeMs <= LIVE_WINDOW_MS)
24678
+ if (now - statSync6(nodePath3.join(dir, name)).mtimeMs <= LIVE_WINDOW_MS)
23669
24679
  n += 1;
23670
24680
  } catch {
23671
24681
  }
@@ -23824,6 +24834,23 @@ async function runPaceVerb(args2, deps = {}) {
23824
24834
  number4 = stored?.number ?? Math.min(claudePlanOf(claudePlan).startingAgents, PACE_AGENTS_MAX);
23825
24835
  }
23826
24836
  const decision = paceOf({ number: number4, keepFreePercent, learned, reported, now });
24837
+ if (basis !== null) {
24838
+ const dir = enginePacingDir(engagementId, deps.home);
24839
+ if (dir !== null) {
24840
+ const entry = {
24841
+ pace: decision.pace,
24842
+ allowance: decision.allowance,
24843
+ number: number4,
24844
+ windowKind: decision.windowKind,
24845
+ until: decision.until === null ? null : decision.until.toISOString(),
24846
+ at: now.toISOString()
24847
+ };
24848
+ try {
24849
+ writePaceState(dir, key, entry, { phase: basis.phase, claudePlan: basis.claudePlan, keepFreePercent });
24850
+ } catch {
24851
+ }
24852
+ }
24853
+ }
23827
24854
  const live = liveSubAgentsIn(deps.cwd ?? process.cwd(), deps.live);
23828
24855
  const result = {
23829
24856
  outcome: "paced",
@@ -23844,11 +24871,10 @@ async function runPaceVerb(args2, deps = {}) {
23844
24871
  var PACE_HELD_TOOL = "Agent";
23845
24872
  var PACE_STEP_MIN_INTERVAL_MS = OBSERVE_MIN_INTERVAL_MS;
23846
24873
  var PACE_STATE_FRESH_MS = 10 * 60 * 1e3;
23847
- var PACE_PENDING_START_MS = 60 * 1e3;
24874
+ var PACE_PENDING_START_MS = 15 * 1e3;
23848
24875
  var PACE_STARTS_LOCK_STALE_MS = 5 * 1e3;
23849
24876
  var PACE_STARTS_LOCK_GIVE_UP_MS = 50;
23850
24877
  var STARTS_LOCK_POLL_MS = 2;
23851
- var PACE_STATE_FILENAME = "state.json";
23852
24878
  var PACE_STARTS_FILENAME = "starts.json";
23853
24879
  var PACE_STARTS_LOCK_FILENAME = "starts.lock";
23854
24880
  function objectOf(v) {
@@ -23856,7 +24882,7 @@ function objectOf(v) {
23856
24882
  }
23857
24883
  function readJson(path) {
23858
24884
  try {
23859
- return objectOf(JSON.parse(readFileSync16(path, "utf-8")));
24885
+ return objectOf(JSON.parse(readFileSync18(path, "utf-8")));
23860
24886
  } catch {
23861
24887
  return null;
23862
24888
  }
@@ -23865,68 +24891,16 @@ function writeJsonWhole2(path, value) {
23865
24891
  mkdirSync7(nodePath4.dirname(path), { recursive: true, mode: 448 });
23866
24892
  const tmp = `${path}.${String(process.pid)}.${Math.random().toString(36).slice(2, 10)}.tmp`;
23867
24893
  try {
23868
- writeFileSync10(tmp, JSON.stringify(value), { mode: 384 });
23869
- renameSync8(tmp, path);
24894
+ writeFileSync11(tmp, JSON.stringify(value), { mode: 384 });
24895
+ renameSync9(tmp, path);
23870
24896
  } catch (err) {
23871
24897
  try {
23872
- unlinkSync7(tmp);
24898
+ unlinkSync8(tmp);
23873
24899
  } catch {
23874
24900
  }
23875
24901
  throw err;
23876
24902
  }
23877
24903
  }
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
24904
  function field2(input, key) {
23931
24905
  const v = objectOf(input)?.[key];
23932
24906
  return typeof v === "string" && v !== "" ? v : null;
@@ -23934,12 +24908,14 @@ function field2(input, key) {
23934
24908
  function shownAs(pace, windowKind, allowance) {
23935
24909
  return pace === "full" ? "full" : `${pace}:${windowKind ?? ""}:${pace === "slowing" ? String(allowance) : ""}`;
23936
24910
  }
23937
- function paceChangeText(before, after, working, now) {
24911
+ function paceChangeText(before, after, working, now, keepFreePercent) {
23938
24912
  const was = before === null ? "full" : shownAs(before.pace, before.windowKind, before.allowance);
23939
24913
  const is = shownAs(after.pace, after.windowKind, after.allowance);
23940
24914
  if (was === is)
23941
24915
  return null;
23942
24916
  if (after.pace === "full") {
24917
+ if (keepFreePercent === 0)
24918
+ return null;
23943
24919
  return before !== null && before.windowKind !== null ? paceS3Of(before.windowKind, after.number) : null;
23944
24920
  }
23945
24921
  if (after.windowKind === null || after.until === null)
@@ -24009,7 +24985,7 @@ async function paceAtTurnEnd(input, reply, deps = {}) {
24009
24985
  };
24010
24986
  write(dir, key, after, read !== null ? basis : null);
24011
24987
  const working = liveCount(readLiveness(field2(input, "session_id"), field2(input, "cwd") ?? process.cwd(), deps.liveness));
24012
- const text = paceChangeText(before, after, working, now);
24988
+ const text = paceChangeText(before, after, working, now, basis.keepFreePercent);
24013
24989
  if (text !== null)
24014
24990
  reply.notice(text);
24015
24991
  } catch {
@@ -24138,9 +25114,9 @@ function paceHoldText(input, toolInput, deps = {}) {
24138
25114
  }
24139
25115
 
24140
25116
  // 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";
25117
+ import { existsSync as existsSync7, lstatSync as lstatSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, statSync as statSync7 } from "node:fs";
24142
25118
  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";
25119
+ 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
25120
 
24145
25121
  // dist/risky/shell.js
24146
25122
  var MAX_DEPTH = 8;
@@ -24515,7 +25491,7 @@ var Splitter = class _Splitter {
24515
25491
  const script = innerScript(words);
24516
25492
  if (script !== void 0) {
24517
25493
  if (this.depth < MAX_DEPTH) {
24518
- this.child(script, basename(words[0].text) === "eval").parseList(0, null);
25494
+ this.child(script, basename2(words[0].text) === "eval").parseList(0, null);
24519
25495
  }
24520
25496
  return;
24521
25497
  }
@@ -24584,7 +25560,7 @@ function unwrap(input) {
24584
25560
  words = words.slice(1);
24585
25561
  continue;
24586
25562
  }
24587
- const prog = basename(name);
25563
+ const prog = basename2(name);
24588
25564
  const valueOptions = WRAPPER_VALUE_OPTIONS[prog];
24589
25565
  if (valueOptions === void 0)
24590
25566
  return words;
@@ -24628,7 +25604,7 @@ function unwrap(input) {
24628
25604
  }
24629
25605
  var SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh"]);
24630
25606
  function innerScript(words) {
24631
- const prog = basename(words[0].text);
25607
+ const prog = basename2(words[0].text);
24632
25608
  if (prog === EVAL_PROGRAM)
24633
25609
  return words.slice(1).map((w) => w.text).join(" ");
24634
25610
  if (!SHELLS.has(prog))
@@ -24663,13 +25639,13 @@ function innerScript(words) {
24663
25639
  }
24664
25640
  function readsScriptFromInput(raw) {
24665
25641
  const words = unwrap(raw);
24666
- if (words.length === 0 || !SHELLS.has(basename(words[0].text)))
25642
+ if (words.length === 0 || !SHELLS.has(basename2(words[0].text)))
24667
25643
  return false;
24668
25644
  if (innerScript(words) !== void 0)
24669
25645
  return false;
24670
25646
  return words.slice(1).every((w) => w.text.startsWith("-") || w.text.startsWith("+"));
24671
25647
  }
24672
- function basename(word) {
25648
+ function basename2(word) {
24673
25649
  const k = Math.max(word.lastIndexOf("/"), word.lastIndexOf("\\"));
24674
25650
  return k < 0 ? word : word.slice(k + 1);
24675
25651
  }
@@ -24684,7 +25660,7 @@ function namesNeverFollowed(line2, commands) {
24684
25660
  const sourced = commands.some((c) => c.words[0] !== void 0 && (c.words[0].text === "source" || c.words[0].text === "."));
24685
25661
  const evaluated = commands.some((c) => c.inPlace === true);
24686
25662
  const indirect = commands.some((c) => {
24687
- const prog = c.words[0] === void 0 ? "" : basename(c.words[0].text);
25663
+ const prog = c.words[0] === void 0 ? "" : basename2(c.words[0].text);
24688
25664
  return prog === "trap" || prog === "builtin" || REFERENCE_BINDERS.has(prog) && c.words.slice(1).some((w) => /^-[A-Za-z]*n/.test(w.text));
24689
25665
  });
24690
25666
  const fns = functionsIn(line2);
@@ -24708,7 +25684,7 @@ function namesNeverFollowed(line2, commands) {
24708
25684
  out.add(hit[1]);
24709
25685
  }
24710
25686
  }
24711
- const prog = c.words[0] === void 0 ? "" : basename(c.words[0].text);
25687
+ const prog = c.words[0] === void 0 ? "" : basename2(c.words[0].text);
24712
25688
  const args2 = c.words.slice(1).map((w) => w.text);
24713
25689
  if (BINDERS.has(prog) || prog === "unset") {
24714
25690
  for (const a of args2) {
@@ -24933,7 +25909,7 @@ var LineVariables = class {
24933
25909
  }
24934
25910
  }
24935
25911
  update(cmd, before) {
24936
- const prog = cmd.words[0] === void 0 ? "" : basename(cmd.words[0].text);
25912
+ const prog = cmd.words[0] === void 0 ? "" : basename2(cmd.words[0].text);
24937
25913
  let settings = [];
24938
25914
  if (cmd.assigns !== void 0)
24939
25915
  settings = cmd.assigns;
@@ -25127,7 +26103,7 @@ function expandWord(word, vars) {
25127
26103
  return { text: v.text, literal: !pattern };
25128
26104
  }
25129
26105
  function expandCommand(cmd, vars) {
25130
- const first = cmd.words[0] === void 0 ? "" : basename(cmd.words[0].text);
26106
+ const first = cmd.words[0] === void 0 ? "" : basename2(cmd.words[0].text);
25131
26107
  const keepEmpty = FOLDER_CHANGERS.has(first);
25132
26108
  const words = [];
25133
26109
  cmd.words.forEach((w, i) => {
@@ -25171,7 +26147,7 @@ function pathsMade(cmd) {
25171
26147
  const words = cmd.words;
25172
26148
  if (words.length === 0)
25173
26149
  return [];
25174
- const prog = basename(words[0].text);
26150
+ const prog = basename2(words[0].text);
25175
26151
  const args2 = words.slice(1);
25176
26152
  if (prog === "mkdir") {
25177
26153
  const out = [];
@@ -25211,7 +26187,7 @@ function pathsMade(cmd) {
25211
26187
  if (ops.length >= 2)
25212
26188
  return [{ word: ops[ops.length - 1], by: "clone" }];
25213
26189
  if (ops.length === 1 && ops[0].literal) {
25214
- const name = basename(ops[0].text.replace(/\/+$/, "")).replace(/\.git$/, "");
26190
+ const name = basename2(ops[0].text.replace(/\/+$/, "")).replace(/\.git$/, "");
25215
26191
  if (name !== "" && !name.includes(":"))
25216
26192
  return [{ word: { text: name, literal: true }, by: "clone" }];
25217
26193
  }
@@ -25224,24 +26200,6 @@ function pathsMade(cmd) {
25224
26200
  return [];
25225
26201
  }
25226
26202
 
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
26203
  // dist/risky/recognise.js
25246
26204
  function madeKey(dirs, word) {
25247
26205
  return [...dirs.map((d) => d.text), word.text].join("\0");
@@ -25405,7 +26363,7 @@ function surelyRuns(simple, before) {
25405
26363
  }
25406
26364
  function folderAfter(prev, simple, before) {
25407
26365
  const first = simple.words[0];
25408
- if (first === void 0 || !FOLDER_CHANGERS.has(basename(first.text)))
26366
+ if (first === void 0 || !FOLDER_CHANGERS.has(basename2(first.text)))
25409
26367
  return prev;
25410
26368
  const dirs = prev?.dirs ?? [];
25411
26369
  const unknown2 = { dirs, known: false };
@@ -25414,7 +26372,7 @@ function folderAfter(prev, simple, before) {
25414
26372
  return prev;
25415
26373
  if (simple.next === "||" || before === "||")
25416
26374
  return unknown2;
25417
- const prog = basename(first.text);
26375
+ const prog = basename2(first.text);
25418
26376
  if (simple.inPlace === true || simple.conditional === true || prog === "popd")
25419
26377
  return unknown2;
25420
26378
  let args2 = simple.words.slice(1);
@@ -25458,7 +26416,7 @@ function recogniseSimple(cmd) {
25458
26416
  }
25459
26417
  function programForms(words) {
25460
26418
  const forms = [words];
25461
- const prog = basename(words[0].text);
26419
+ const prog = basename2(words[0].text);
25462
26420
  let rest;
25463
26421
  if (prog === "npx" || prog === "bunx" || prog === "pnpx") {
25464
26422
  rest = skipRunnerOptions(words.slice(1));
@@ -25504,7 +26462,7 @@ var PROGRAM_MATCHERS = /* @__PURE__ */ new Map([
25504
26462
  ["git", (cmd, args2) => gitMatch(cmd, args2)]
25505
26463
  ]);
25506
26464
  function recogniseWords(cmd, words) {
25507
- const prog = basename(words[0].text);
26465
+ const prog = basename2(words[0].text);
25508
26466
  const args2 = words.slice(1);
25509
26467
  const own = PROGRAM_MATCHERS.get(prog);
25510
26468
  if (own !== void 0)
@@ -25585,7 +26543,7 @@ function findMatch(cmd, args2) {
25585
26543
  if (t === FIND_DELETE_ACTION)
25586
26544
  deletes = true;
25587
26545
  if ((t === "-exec" || t === "-execdir" || t === "-ok" || t === "-okdir") && i + 1 < args2.length) {
25588
- const inner = basename(args2[i + 1].text);
26546
+ const inner = basename2(args2[i + 1].text);
25589
26547
  const viaGit = inner === "git" && args2[i + 2]?.text === "rm";
25590
26548
  if (FIND_DELETERS.has(inner) || viaGit) {
25591
26549
  deletes = true;
@@ -25633,8 +26591,9 @@ function findFilterOf(expr) {
25633
26591
  return names.length === 0 && excluded.length === 0 ? void 0 : { names, excluded };
25634
26592
  }
25635
26593
  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) {
26594
+ function gitArgsOf(args2) {
25637
26595
  const dirs = [];
26596
+ const config2 = [];
25638
26597
  let gitDir;
25639
26598
  let workTree;
25640
26599
  let i = 0;
@@ -25660,11 +26619,92 @@ function gitMatch(cmd, args2) {
25660
26619
  gitDir = value;
25661
26620
  if (name === "--work-tree" && value)
25662
26621
  workTree = value;
26622
+ if ((name === "-c" || name === "--config-env") && value)
26623
+ config2.push({ word: value, fromEnv: name === "--config-env" });
25663
26624
  }
25664
- const git2 = { dirs, ...gitDir ? { gitDir } : {}, ...workTree ? { workTree } : {} };
25665
- const sub = args2[i]?.text;
26625
+ return { git: { dirs, ...gitDir ? { gitDir } : {}, ...workTree ? { workTree } : {} }, at: i, config: config2 };
26626
+ }
26627
+ function gitMatch(cmd, args2) {
26628
+ const { git: git2, at, config: config2 } = gitArgsOf(args2);
26629
+ const sub = args2[at]?.text;
25666
26630
  const own = sub === void 0 ? void 0 : GIT_SUBCOMMAND_MATCHERS.get(sub);
25667
- return own === void 0 ? [] : own(cmd, args2.slice(i + 1), git2);
26631
+ return own === void 0 ? [] : own(cmd, args2.slice(at + 1), git2, config2);
26632
+ }
26633
+ var PUSH_ALL_REFS_OPTION = "--mirror";
26634
+ function hasLongAsGitReads(args2, name) {
26635
+ for (const a of args2) {
26636
+ if (a.text === "--")
26637
+ return false;
26638
+ const spelled = a.text.split("=")[0];
26639
+ if (spelled.length >= 3 && spelled.startsWith("--") && name.startsWith(spelled))
26640
+ return true;
26641
+ }
26642
+ return false;
26643
+ }
26644
+ function configNameOf(c) {
26645
+ const t = c.word.text;
26646
+ const eq = t.indexOf("=");
26647
+ return eq < 0 ? t : t.slice(0, eq);
26648
+ }
26649
+ function configValueOf(c) {
26650
+ if (c.fromEnv || !c.word.literal)
26651
+ return null;
26652
+ const t = c.word.text;
26653
+ const eq = t.indexOf("=");
26654
+ return eq < 0 ? true : t.slice(eq + 1);
26655
+ }
26656
+ function configNameUnknown(name) {
26657
+ return /[$`*?[]/.test(name);
26658
+ }
26659
+ var GIT_FALSE = /^(false|no|off|[-+]?0+)?$/i;
26660
+ function configMirrors(config2) {
26661
+ return config2.some((c) => {
26662
+ const name = configNameOf(c);
26663
+ if (configNameUnknown(name))
26664
+ return true;
26665
+ if (!/^remote\..+\.mirror$/i.test(name))
26666
+ return false;
26667
+ const value = configValueOf(c);
26668
+ return value === true || value === null || !GIT_FALSE.test(value);
26669
+ });
26670
+ }
26671
+ function pushMirrors(rest, config2) {
26672
+ return hasLongAsGitReads(rest, PUSH_ALL_REFS_OPTION) || configMirrors(config2);
26673
+ }
26674
+ var SAVED_NAMESPACE = SAVED_REF_PREFIX.split("/").slice(0, 2).join("/") + "/";
26675
+ function refspecSendsSaves(text) {
26676
+ if (text.includes(SAVED_NAMESPACE))
26677
+ return true;
26678
+ const src = text.replace(/^\+/, "").split(":")[0];
26679
+ const star = src.indexOf("*");
26680
+ return star >= 0 && SAVED_NAMESPACE.startsWith(src.slice(0, star));
26681
+ }
26682
+ function configPushSendsSaves(config2) {
26683
+ return config2.some((c) => {
26684
+ const name = configNameOf(c);
26685
+ if (configNameUnknown(name))
26686
+ return true;
26687
+ if (!/^remote\..+\.push$/i.test(name))
26688
+ return false;
26689
+ const value = configValueOf(c);
26690
+ return value === null || value !== true && refspecSendsSaves(value);
26691
+ });
26692
+ }
26693
+ function pushesSavedRefs(command) {
26694
+ for (const cmd of splitCommand(command)) {
26695
+ if (cmd.words.length === 0 || basename2(cmd.words[0].text) !== "git")
26696
+ continue;
26697
+ const args2 = cmd.words.slice(1);
26698
+ const { at, config: config2 } = gitArgsOf(args2);
26699
+ if (args2[at]?.text !== "push")
26700
+ continue;
26701
+ const rest = args2.slice(at + 1);
26702
+ if (pushMirrors(rest, config2) || configPushSendsSaves(config2))
26703
+ return true;
26704
+ if (rest.some((w) => refspecSendsSaves(w.text)))
26705
+ return true;
26706
+ }
26707
+ return false;
25668
26708
  }
25669
26709
  var GIT_SUBCOMMAND_MATCHERS = /* @__PURE__ */ new Map([
25670
26710
  ["rm", (cmd, rest, git2) => gitRm(cmd, rest, git2)],
@@ -25675,7 +26715,7 @@ var GIT_SUBCOMMAND_MATCHERS = /* @__PURE__ */ new Map([
25675
26715
  ["clean", (cmd, rest, git2) => gitClean(cmd, rest, git2)],
25676
26716
  ["stash", (cmd, rest, git2) => gitStash(cmd, rest, git2)],
25677
26717
  ["worktree", (cmd, rest, git2) => gitWorktree(cmd, rest, git2)],
25678
- ["push", (cmd, rest, git2) => gitPush(cmd, rest, git2)],
26718
+ ["push", (cmd, rest, git2, config2) => gitPush(cmd, rest, git2, config2)],
25679
26719
  ["branch", (cmd, rest, git2) => gitBranch(cmd, rest, git2)]
25680
26720
  ]);
25681
26721
  function gitSwitch(cmd, rest, git2) {
@@ -25700,7 +26740,8 @@ function gitRm(cmd, rest, git2) {
25700
26740
  const targets = operands(rest, /* @__PURE__ */ new Set(["--pathspec-from-file"]));
25701
26741
  if (targets.length === 0)
25702
26742
  return [];
25703
- return [{ type: "delete-files", form: "plain", program: "git rm", targets, git: git2, command: cmd }];
26743
+ const form = gitRmRecursive(cmd.words) ? "recursive" : "plain";
26744
+ return [{ type: "delete-files", form, program: "git rm", targets, git: git2, command: cmd }];
25704
26745
  }
25705
26746
  function gitReset(cmd, rest, git2) {
25706
26747
  const hard = hasLong(rest, "--hard");
@@ -25753,7 +26794,7 @@ function gitWorktree(cmd, rest, git2) {
25753
26794
  return [{ type: "discard-unsaved", form: "worktree-remove", paths: operands(args2), git: git2, command: cmd }];
25754
26795
  }
25755
26796
  var PUSH_VALUE_OPTIONS = /* @__PURE__ */ new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec"]);
25756
- function gitPush(cmd, rest, git2) {
26797
+ function gitPush(cmd, rest, git2, config2) {
25757
26798
  if (hasShort(rest, "n") || hasLong(rest, "--dry-run"))
25758
26799
  return [];
25759
26800
  const ops = operands(rest, PUSH_VALUE_OPTIONS);
@@ -25762,7 +26803,7 @@ function gitPush(cmd, rest, git2) {
25762
26803
  const force = hasShort(rest, "f") || hasLong(rest, "--force");
25763
26804
  const forceWithLease = hasLong(rest, "--force-with-lease");
25764
26805
  const forceIfIncludes = hasLong(rest, "--force-if-includes");
25765
- const mirror = hasLong(rest, "--mirror");
26806
+ const mirror = pushMirrors(rest, config2);
25766
26807
  const deletes = hasShort(rest, "d") || hasLong(rest, "--delete") || refspecs.some((r) => r.text.startsWith(":") && r.text.length > 1);
25767
26808
  const plusRefspec = refspecs.some((r) => r.text.startsWith("+"));
25768
26809
  if (!(force || forceWithLease || forceIfIncludes || mirror || deletes || plusRefspec))
@@ -25802,7 +26843,7 @@ function wipeDataMatch(cmd, prog, args2, words) {
25802
26843
  const execAt = args2.findIndex((a) => a.text === "exec" || a.text === "run");
25803
26844
  if (execAt >= 0) {
25804
26845
  for (let i = execAt + 1; i < args2.length; i++) {
25805
- const inner = basename(args2[i].text);
26846
+ const inner = basename2(args2[i].text);
25806
26847
  if (SQL_CLIENTS.has(inner) || inner === "dropdb" || inner === "redis-cli") {
25807
26848
  return wipeDataMatch(cmd, inner, args2.slice(i + 1), args2.slice(i));
25808
26849
  }
@@ -26135,7 +27176,7 @@ function shellRecordMatches(cmd, found2) {
26135
27176
  for (const m of found2) {
26136
27177
  if (m.type !== "delete-files" || m.program === "find" || m.program === "git clean" || m.command !== cmd)
26137
27178
  continue;
26138
- const recursive = m.form === "recursive" || m.program === "git rm" && gitRmRecursive(cmd.words);
27179
+ const recursive = m.form === "recursive";
26139
27180
  const targets = recursive ? m.targets : m.targets.filter((w) => mayBeRecordWord(w));
26140
27181
  if (targets.length === 0)
26141
27182
  continue;
@@ -26149,7 +27190,7 @@ function shellRecordMatches(cmd, found2) {
26149
27190
  });
26150
27191
  }
26151
27192
  const words = cmd.words;
26152
- const prog = words.length > 0 ? basename(words[0].text) : "";
27193
+ const prog = words.length > 0 ? basename2(words[0].text) : "";
26153
27194
  const args2 = words.slice(1);
26154
27195
  if (prog === TRUNCATE_PROGRAM) {
26155
27196
  const targets = operands(args2, /* @__PURE__ */ new Set(["-s", "--size", "-r", "--reference"])).filter((w) => mayBeRecordWord(w));
@@ -26182,7 +27223,7 @@ function movesStdout(r) {
26182
27223
  return [">", ">>", ">|", ">&"].includes(r.op) && (r.fd === void 0 || r.fd === "1");
26183
27224
  }
26184
27225
  function savedObjectShown(words) {
26185
- if (words.length !== 3 || basename(words[0].text) !== "git" || words[1].text !== "show" || !words[2].literal)
27226
+ if (words.length !== 3 || basename2(words[0].text) !== "git" || words[1].text !== "show" || !words[2].literal)
26186
27227
  return void 0;
26187
27228
  return /^[0-9a-f]{7,64}:[^:]+$/.test(words[2].text) ? words[2].text : void 0;
26188
27229
  }
@@ -26285,13 +27326,13 @@ var Env = class {
26285
27326
  description;
26286
27327
  projectCommonDir;
26287
27328
  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()));
27329
+ this.projectDir = realOrSelf(resolve4(ctx.projectDir));
27330
+ this.cwd = realOrSelf(resolve4(this.projectDir, ctx.cwd));
27331
+ this.home = realOrSelf(resolve4(ctx.home ?? homedir7()));
26291
27332
  this.timeoutMs = ctx.gitTimeoutMs ?? FACTS_GIT_TIMEOUT_MS;
26292
- this.tempDir = realOrSelf(resolve3(ctx.tempDir ?? tmpdir2()));
27333
+ this.tempDir = realOrSelf(resolve4(ctx.tempDir ?? tmpdir2()));
26293
27334
  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));
27335
+ this.tempRoots = [...new Set(roots.map((r) => realOrSelf(resolve4(r))))].filter((r) => !isInside(this.projectDir, r));
26295
27336
  }
26296
27337
  isTemp(abs) {
26297
27338
  return this.tempRoots.some((r) => isInside(abs, r));
@@ -26301,7 +27342,7 @@ var Env = class {
26301
27342
  }
26302
27343
  display(abs) {
26303
27344
  if (isInside(abs, this.projectDir)) {
26304
- const rel = relative4(this.projectDir, abs);
27345
+ const rel = relative5(this.projectDir, abs);
26305
27346
  return rel === "" ? "." : toSlash(rel);
26306
27347
  }
26307
27348
  return toSlash(abs);
@@ -26311,7 +27352,7 @@ var Env = class {
26311
27352
  return this.description;
26312
27353
  this.description = null;
26313
27354
  try {
26314
- const raw = JSON.parse(readFileSync17(join11(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8"));
27355
+ const raw = JSON.parse(readFileSync19(join12(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8"));
26315
27356
  const checked = checkSystemDescription(raw);
26316
27357
  if (checked.ok)
26317
27358
  this.description = checked.description;
@@ -26342,7 +27383,7 @@ var Env = class {
26342
27383
  return false;
26343
27384
  let dir = abs;
26344
27385
  while (!isDirectory(dir)) {
26345
- const up = dirname5(dir);
27386
+ const up = dirname6(dir);
26346
27387
  if (up === dir)
26347
27388
  return false;
26348
27389
  dir = up;
@@ -26354,40 +27395,40 @@ var Env = class {
26354
27395
  if (!r.ok)
26355
27396
  return null;
26356
27397
  const d = r.value.trim();
26357
- return d === "" ? null : realOrSelf(resolve3(dir, d));
27398
+ return d === "" ? null : realOrSelf(resolve4(dir, d));
26358
27399
  }
26359
27400
  };
26360
27401
  function realOrSelf(p) {
26361
27402
  try {
26362
- return realpathSync5(p);
27403
+ return realpathSync6(p);
26363
27404
  } catch {
26364
27405
  return p;
26365
27406
  }
26366
27407
  }
26367
27408
  function toSlash(p) {
26368
- return sep2 === "/" ? p : p.split(sep2).join("/");
27409
+ return sep3 === "/" ? p : p.split(sep3).join("/");
26369
27410
  }
26370
27411
  function isInside(child, parent) {
26371
- const rel = relative4(parent, child);
26372
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
27412
+ const rel = relative5(parent, child);
27413
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
26373
27414
  }
26374
27415
  function resolveHome(text, home, base) {
26375
27416
  if (text === "~")
26376
27417
  return home;
26377
27418
  if (text.startsWith("~/"))
26378
- return join11(home, text.slice(2));
26379
- return resolve3(base, text);
27419
+ return join12(home, text.slice(2));
27420
+ return resolve4(base, text);
26380
27421
  }
26381
27422
  function realTarget(abs) {
26382
- const parent = dirname5(abs);
27423
+ const parent = dirname6(abs);
26383
27424
  if (parent === abs)
26384
27425
  return abs;
26385
- return join11(realOrSelf(parent), pathBasename(abs));
27426
+ return join12(realOrSelf(parent), pathBasename(abs));
26386
27427
  }
26387
27428
  function wordPath(word, base, env) {
26388
27429
  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}`);
27430
+ const root = word.temp.base === "mktemp" ? join12(env.tempDir, ".halfcycle-mktemp-new") : env.tempDir;
27431
+ return resolve4(`${root}${word.temp.suffix}`);
26391
27432
  }
26392
27433
  return word.literal ? resolveHome(word.text, env.home, base) : null;
26393
27434
  }
@@ -26396,7 +27437,7 @@ function follow(start, prefix, dirs, m, env) {
26396
27437
  let fresh = start.fresh;
26397
27438
  let freshRoot = start.freshRoot;
26398
27439
  const seen = [...prefix];
26399
- const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve3(p), resolve3(freshRoot));
27440
+ const insideFresh = (p) => freshRoot !== void 0 && isInside(resolve4(p), resolve4(freshRoot));
26400
27441
  const mark = (by) => {
26401
27442
  if (fresh !== void 0 && insideFresh(cwd))
26402
27443
  return;
@@ -26525,14 +27566,14 @@ function expandBraces(text) {
26525
27566
  return body.split(",").map((b) => `${head}${b}${tail}`);
26526
27567
  }
26527
27568
  function globPath(abs) {
26528
- const root = abs.startsWith(sep2) ? sep2 : "";
26529
- const segments = abs.split(sep2).filter((s) => s !== "");
27569
+ const root = abs.startsWith(sep3) ? sep3 : "";
27570
+ const segments = abs.split(sep3).filter((s) => s !== "");
26530
27571
  let current = [root === "" ? "." : root];
26531
27572
  for (const segment of segments) {
26532
27573
  const next = [];
26533
27574
  if (!/[*?[]/.test(segment)) {
26534
27575
  for (const c of current)
26535
- next.push(join11(c, segment));
27576
+ next.push(join12(c, segment));
26536
27577
  } else {
26537
27578
  const re = segmentPattern(segment);
26538
27579
  for (const c of current) {
@@ -26546,7 +27587,7 @@ function globPath(abs) {
26546
27587
  if (n.startsWith(".") && !segment.startsWith("."))
26547
27588
  continue;
26548
27589
  if (re.test(n))
26549
- next.push(join11(c, n));
27590
+ next.push(join12(c, n));
26550
27591
  }
26551
27592
  }
26552
27593
  }
@@ -26582,7 +27623,7 @@ function segmentPattern(segment) {
26582
27623
  }
26583
27624
  function pathExists(p) {
26584
27625
  try {
26585
- lstatSync(p);
27626
+ lstatSync2(p);
26586
27627
  return true;
26587
27628
  } catch {
26588
27629
  return false;
@@ -26590,28 +27631,28 @@ function pathExists(p) {
26590
27631
  }
26591
27632
  function isEmptyFolder(p) {
26592
27633
  try {
26593
- return lstatSync(p).isDirectory() && readdirSync8(p).length === 0;
27634
+ return lstatSync2(p).isDirectory() && readdirSync8(p).length === 0;
26594
27635
  } catch {
26595
27636
  return false;
26596
27637
  }
26597
27638
  }
26598
27639
  function isFolder(p) {
26599
27640
  try {
26600
- return statSync6(p).isDirectory();
27641
+ return statSync7(p).isDirectory();
26601
27642
  } catch {
26602
27643
  return false;
26603
27644
  }
26604
27645
  }
26605
27646
  function isDirectory(p) {
26606
27647
  try {
26607
- return lstatSync(p).isDirectory();
27648
+ return lstatSync2(p).isDirectory();
26608
27649
  } catch {
26609
27650
  return false;
26610
27651
  }
26611
27652
  }
26612
- function filesUnder(targets, repoRoot, env, keep) {
27653
+ function filesUnder(targets, repoRoot, env, keep, seen) {
26613
27654
  const specs = targets.map((t) => {
26614
- const rel = toSlash(relative4(repoRoot, t));
27655
+ const rel = toSlash(relative5(repoRoot, t));
26615
27656
  return rel === "" ? "." : rel;
26616
27657
  });
26617
27658
  const tracked = env.git(["--literal-pathspecs", "ls-files", "--full-name", "-z", "--", ...specs], repoRoot);
@@ -26645,6 +27686,8 @@ function filesUnder(targets, repoRoot, env, keep) {
26645
27686
  goneFromDisk.add(e.path);
26646
27687
  }
26647
27688
  }
27689
+ if (seen !== void 0)
27690
+ seen.ignored = ignored.length;
26648
27691
  const out = /* @__PURE__ */ new Map();
26649
27692
  for (const path of tracked.value.split("\0")) {
26650
27693
  if (path === "" || goneFromDisk.has(path))
@@ -26672,12 +27715,12 @@ function findKeeps(filter, roots, repoRoot) {
26672
27715
  const names = filter.names.map((n) => findPattern(n.pattern, n.fold, false));
26673
27716
  const excluded = filter.excluded.map((p) => findPattern(p, false, true));
26674
27717
  const steps = (rel) => {
26675
- const abs = join11(repoRoot, rel);
27718
+ const abs = join12(repoRoot, rel);
26676
27719
  const out = [];
26677
27720
  for (const r of roots) {
26678
27721
  if (!isInside(abs, r.abs))
26679
27722
  continue;
26680
- const below = toSlash(relative4(r.abs, abs)).split("/").filter((x) => x !== "");
27723
+ const below = toSlash(relative5(r.abs, abs)).split("/").filter((x) => x !== "");
26681
27724
  const rootName = r.text === null ? pathBasename(r.abs) : r.text.replace(/\/+$/, "").split("/").pop() || r.text;
26682
27725
  const prefixes = r.text === null ? [] : [.../* @__PURE__ */ new Set([r.text, r.text.replace(/\/+$/, "") || r.text])];
26683
27726
  const chain = [{ name: rootName, printed: r.text === null ? [] : [r.text] }];
@@ -26726,27 +27769,27 @@ function findPattern(pattern, fold, whole) {
26726
27769
  return new RegExp(`${re}$`, fold ? "i" : "");
26727
27770
  }
26728
27771
  function walkRoots(ignoredFolders, specs) {
26729
- const under = (child, parent) => parent === "." || child === parent || child.startsWith(`${parent}/`);
27772
+ const under2 = (child, parent) => parent === "." || child === parent || child.startsWith(`${parent}/`);
26730
27773
  const roots = /* @__PURE__ */ new Set();
26731
27774
  for (const raw of ignoredFolders) {
26732
27775
  const folder = raw.replace(/\/+$/, "");
26733
27776
  for (const spec of specs) {
26734
- if (under(folder, spec))
27777
+ if (under2(folder, spec))
26735
27778
  roots.add(folder);
26736
- else if (under(spec, folder))
27779
+ else if (under2(spec, folder))
26737
27780
  roots.add(spec);
26738
27781
  }
26739
27782
  }
26740
27783
  const all = [...roots];
26741
- return all.filter((r) => !all.some((o) => o !== r && under(r, o)));
27784
+ return all.filter((r) => !all.some((o) => o !== r && under2(r, o)));
26742
27785
  }
26743
27786
  function credentialFilesIn(folders, repoRoot, deadline, skip) {
26744
27787
  const out = [];
26745
27788
  const stack = [];
26746
27789
  for (const f of folders) {
26747
- if (isDirectory(join11(repoRoot, f)))
27790
+ if (isDirectory(join12(repoRoot, f)))
26748
27791
  stack.push(f);
26749
- else if (pathExists(join11(repoRoot, f)) && isCredentialPath(f))
27792
+ else if (pathExists(join12(repoRoot, f)) && isCredentialPath(f))
26750
27793
  out.push(f);
26751
27794
  }
26752
27795
  let seen = 0;
@@ -26754,7 +27797,7 @@ function credentialFilesIn(folders, repoRoot, deadline, skip) {
26754
27797
  const rel = stack.pop();
26755
27798
  let entries;
26756
27799
  try {
26757
- entries = readdirSync8(join11(repoRoot, rel), { withFileTypes: true });
27800
+ entries = readdirSync8(join12(repoRoot, rel), { withFileTypes: true });
26758
27801
  } catch {
26759
27802
  continue;
26760
27803
  }
@@ -26851,7 +27894,7 @@ function deleteFilesFacts(m, env) {
26851
27894
  for (const raw of expanded) {
26852
27895
  const abs = realTarget(raw);
26853
27896
  written.set(abs, word.literal ? word.text : null);
26854
- if (abs === sep2 || abs === env.home || abs !== env.projectDir && isInside(env.projectDir, abs)) {
27897
+ if (abs === sep3 || abs === env.home || abs !== env.projectDir && isInside(env.projectDir, abs)) {
26855
27898
  guarded.push(abs);
26856
27899
  } else if (isInside(abs, env.projectDir) || !env.isTemp(abs) && env.sameRepository(abs)) {
26857
27900
  if (pathExists(abs))
@@ -26863,20 +27906,22 @@ function deleteFilesFacts(m, env) {
26863
27906
  return cannot({ kind: "paths", paths: guarded.map((g) => toSlash(g)) });
26864
27907
  if (unseen)
26865
27908
  return cannot(null);
26866
- const plainFolderRefused = m.form === "plain" && !(m.program === "git rm" && gitRmRecursive(m.command?.words ?? []));
27909
+ const plainFolderRefused = m.form === "plain";
26867
27910
  const targets = plainFolderRefused ? inside.filter((t) => !isDirectory(t)) : inside;
26868
27911
  if (targets.length === 0)
26869
27912
  return silentDelete(m);
26870
- const repoRoot = env.repoRoot(dirname5(targets[0])) ?? env.repoRoot(env.projectDir);
27913
+ const repoRoot = env.repoRoot(dirname6(targets[0])) ?? env.repoRoot(env.projectDir);
26871
27914
  if (repoRoot === null || !targets.every((t) => isInside(t, repoRoot)))
26872
27915
  return cannot(thingFor([], targets, null, env));
26873
27916
  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);
27917
+ const seen = { ignored: 0 };
27918
+ const files = filesUnder(targets, repoRoot, env, keep, seen);
26875
27919
  if (files === null)
26876
27920
  return cannot(thingFor([], targets, repoRoot, env));
26877
27921
  const n = files.length;
26878
27922
  const k = files.filter((f) => f.neverSaved).length;
26879
- const atStake = m.form === "plain" ? k > 0 : n > 0;
27923
+ const closeFold = m.form !== "find" && seen.ignored === 0 && closeFoldsAll(targets, env);
27924
+ const atStake = m.form === "plain" || closeFold ? k > 0 : n > 0;
26880
27925
  if (!atStake)
26881
27926
  return silentDelete(m);
26882
27927
  const thing = thingFor(files.map((f) => f.path), targets, repoRoot, env);
@@ -26889,9 +27934,38 @@ function deleteFilesFacts(m, env) {
26889
27934
  ...m.form === "find" ? { cannotTell: "which-files" } : {},
26890
27935
  thing,
26891
27936
  files: m.form === "find" ? null : n,
26892
- neverSaved: m.form === "find" ? null : k
27937
+ neverSaved: m.form === "find" ? null : k,
27938
+ ...base.pre.length === 0 ? {
27939
+ save: {
27940
+ repoRoot,
27941
+ paths: targets.map((t) => toSlash(relative5(repoRoot, t)) || "."),
27942
+ ...keep === void 0 ? {} : { keep: (rel) => keep.deletes(rel) },
27943
+ lines: []
27944
+ }
27945
+ } : {}
26893
27946
  };
26894
27947
  }
27948
+ function closeFoldsAll(targets, env) {
27949
+ return targets.every((t) => {
27950
+ const root = recordRoot(t, env);
27951
+ if (root === null || !isInside(t, root))
27952
+ return false;
27953
+ const rel = toSlash(relative5(root, t));
27954
+ if (rel === "" || recordPatternFor(rel)?.deleteAsked !== false)
27955
+ return false;
27956
+ let at = root;
27957
+ for (const seg of rel.split("/")) {
27958
+ at = join12(at, seg);
27959
+ try {
27960
+ if (lstatSync2(at).isSymbolicLink())
27961
+ return false;
27962
+ } catch {
27963
+ return false;
27964
+ }
27965
+ }
27966
+ return true;
27967
+ });
27968
+ }
26895
27969
  function silentDelete(m) {
26896
27970
  return { type: m.type, match: m, atStake: false, lostForGood: [], recoverable: [], thing: null, files: 0, neverSaved: 0 };
26897
27971
  }
@@ -26949,6 +28023,7 @@ function discardUnsavedFacts(m, env) {
26949
28023
  const repoRoot = env.repoRoot(where.cwd);
26950
28024
  const part = repoRoot === null ? null : owningPart(lost, repoRoot, env);
26951
28025
  const thing = part !== null ? { kind: "part", name: part } : { kind: "paths", paths: lost };
28026
+ const saveable = repoRoot !== null && where.pre.length === 0 && m.form !== "stash-drop" && m.form !== "stash-clear" && m.form !== "worktree-remove";
26952
28027
  return {
26953
28028
  type: m.type,
26954
28029
  match: m,
@@ -26956,7 +28031,8 @@ function discardUnsavedFacts(m, env) {
26956
28031
  lostForGood: [{ kind: "unsaved-changes", count: lost.length }],
26957
28032
  recoverable: [],
26958
28033
  thing,
26959
- files: lost.length
28034
+ files: lost.length,
28035
+ ...saveable ? { save: { repoRoot, paths: lost, lines: [] } } : {}
26960
28036
  };
26961
28037
  }
26962
28038
  function changedFiles(where, paths, which, env, skipUnmerged = false) {
@@ -27009,8 +28085,9 @@ function restoreTouchesIndex(m) {
27009
28085
  }
27010
28086
  function cleanFacts(m, env) {
27011
28087
  const where = gitFolder(m, env);
27012
- const lost = where === null ? null : cleanFiles(m, where, env);
27013
- if (where === null || lost === null) {
28088
+ const cleaned = where === null ? null : cleanFiles(m, where, env);
28089
+ const lost = cleaned === null ? null : cleaned.lost;
28090
+ if (where === null || cleaned === null || lost === null) {
27014
28091
  return { type: m.type, match: m, atStake: true, lostForGood: [], recoverable: [], cannotTell: "which-files", thing: null, files: null, neverSaved: null };
27015
28092
  }
27016
28093
  if (lost.length === 0)
@@ -27025,7 +28102,8 @@ function cleanFacts(m, env) {
27025
28102
  recoverable: [],
27026
28103
  thing: part !== null ? { kind: "part", name: part } : { kind: "paths", paths: lost },
27027
28104
  files: lost.length,
27028
- neverSaved: lost.length
28105
+ neverSaved: lost.length,
28106
+ ...repoRoot !== null && where.pre.length === 0 ? { save: { repoRoot, paths: cleaned.removed, lines: [] } } : {}
27029
28107
  };
27030
28108
  }
27031
28109
  function cleanFiles(m, where, env) {
@@ -27062,12 +28140,15 @@ function cleanFiles(m, where, env) {
27062
28140
  return null;
27063
28141
  const named = dry.value.split("\n").map((l) => /^Would remove (.*)$/.exec(l)?.[1]).filter((x) => x !== void 0 && x !== "");
27064
28142
  if (named.length === 0)
27065
- return [];
28143
+ return { lost: [], removed: [] };
27066
28144
  const repoRoot = env.repoRoot(where.cwd);
27067
28145
  if (repoRoot === null)
27068
28146
  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);
28147
+ const abs = named.map((n) => resolve4(where.cwd, n));
28148
+ const files = filesUnder(abs, repoRoot, env);
28149
+ if (files === null)
28150
+ return null;
28151
+ return { lost: files.filter((f) => f.neverSaved).map((f) => f.path), removed: abs.map((a) => toSlash(relative5(repoRoot, a)) || ".") };
27071
28152
  }
27072
28153
  function stashFiles(m, where, env) {
27073
28154
  let refs;
@@ -27107,7 +28188,7 @@ function worktreeFiles(m, where, env) {
27107
28188
  if (path === null)
27108
28189
  return null;
27109
28190
  const abs = realOrSelf(path);
27110
- if (!existsSync6(abs))
28191
+ if (!existsSync7(abs))
27111
28192
  return [];
27112
28193
  const top = env.repoRoot(abs);
27113
28194
  if (top === null)
@@ -27119,19 +28200,23 @@ function worktreeFiles(m, where, env) {
27119
28200
  }
27120
28201
  function unmergedFacts(m, env) {
27121
28202
  const where = gitFolder(m, env, ["clone"]);
27122
- const lines2 = [];
28203
+ const leaves = [];
27123
28204
  if (where === null) {
27124
- lines2.push({ branch: null, commits: null });
28205
+ leaves.push({ line: { branch: null, commits: null }, tip: null });
27125
28206
  } else if (where.fresh === "clone") {
27126
- lines2.push({ branch: null, commits: 0 });
28207
+ leaves.push({ line: { branch: null, commits: 0 }, tip: null });
27127
28208
  } else if (m.form === "reset-to") {
27128
- lines2.push(resetLeaves(m.rev, where, env));
28209
+ leaves.push(resetLeaves(m.rev, where, env));
27129
28210
  } else {
27130
28211
  for (const b of m.branches)
27131
- lines2.push(branchLeaves(b, m.remoteTracking, where, env));
28212
+ leaves.push(branchLeaves(b, m.remoteTracking, where, env));
27132
28213
  }
28214
+ const lines2 = leaves.map((l) => l.line);
27133
28215
  const unknown2 = lines2.some((l) => l.commits === null);
27134
28216
  const total = lines2.reduce((s, l) => s + (l.commits ?? 0), 0);
28217
+ const kept = leaves.filter((l) => (l.line.commits ?? 0) > 0);
28218
+ const repoRoot = where === null || where.fresh !== void 0 || where.pre.length > 0 ? null : env.repoRoot(where.cwd);
28219
+ const saveable = !unknown2 && repoRoot !== null && kept.every((l) => l.tip !== null);
27135
28220
  return {
27136
28221
  type: m.type,
27137
28222
  match: m,
@@ -27139,44 +28224,46 @@ function unmergedFacts(m, env) {
27139
28224
  lostForGood: [],
27140
28225
  recoverable: total > 0 ? [{ kind: "local-changes", count: total }] : [],
27141
28226
  ...unknown2 ? { cannotTell: "which-line" } : {},
27142
- lines: lines2
28227
+ lines: lines2,
28228
+ ...saveable ? { save: { repoRoot, paths: [], lines: kept.map((l) => ({ name: m.remoteTracking ? null : l.line.branch, commit: l.tip })) } } : {}
27143
28229
  };
27144
28230
  }
27145
28231
  function resetLeaves(rev, where, env) {
27146
28232
  const current = currentBranch(where, env);
28233
+ const at = (commits, tip = null) => ({ line: { branch: current, commits }, tip });
27147
28234
  if (rev === void 0)
27148
- return { branch: current, commits: 0 };
28235
+ return at(0);
27149
28236
  let target;
27150
28237
  if (rev.gitRev !== void 0) {
27151
28238
  const r2 = env.git([...where.pre, ...rev.gitRev], where.cwd);
27152
28239
  const id = r2.ok ? r2.value.trim() : "";
27153
28240
  if (!/^[0-9a-f]{40,64}$/.test(id))
27154
- return { branch: current, commits: null };
28241
+ return at(null);
27155
28242
  target = id;
27156
28243
  } else {
27157
28244
  if (!rev.literal || rev.text.startsWith("-"))
27158
- return { branch: current, commits: null };
28245
+ return at(null);
27159
28246
  target = resolveCommit(rev.text, where, env);
27160
28247
  }
27161
28248
  if (target === null)
27162
- return { branch: current, commits: 0 };
28249
+ return at(0);
27163
28250
  const head = resolveCommit("HEAD", where, env);
27164
28251
  if (head === null)
27165
- return { branch: current, commits: 0 };
28252
+ return at(0);
27166
28253
  const excludeCurrent = current === null ? [] : [`--exclude=${current}`];
27167
28254
  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 };
28255
+ return at(r.ok ? lines(r.value).length : null, head);
27169
28256
  }
27170
28257
  function branchLeaves(b, remoteTracking, where, env) {
27171
28258
  if (!b.literal || b.text.startsWith("-"))
27172
- return { branch: b.literal ? b.text : null, commits: null };
28259
+ return { line: { branch: b.literal ? b.text : null, commits: null }, tip: null };
27173
28260
  const ref = remoteTracking ? `refs/remotes/${b.text}` : `refs/heads/${b.text}`;
27174
28261
  const tip = resolveCommit(ref, where, env);
27175
28262
  if (tip === null)
27176
- return { branch: b.text, commits: 0 };
28263
+ return { line: { branch: b.text, commits: 0 }, tip: null };
27177
28264
  const exclude = remoteTracking ? ["--branches", `--exclude=${b.text}`, "--remotes", "--tags"] : [`--exclude=${b.text}`, "--branches", "--remotes", "--tags"];
27178
28265
  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 };
28266
+ return { line: { branch: b.text, commits: r.ok ? lines(r.value).length : null }, tip };
27180
28267
  }
27181
28268
  function currentBranch(where, env) {
27182
28269
  const r = env.git([...where.pre, "symbolic-ref", "--quiet", "--short", "HEAD"], where.cwd);
@@ -27451,7 +28538,7 @@ function publishFacts(m, env) {
27451
28538
  const start = startFolder(m, env);
27452
28539
  if (start !== null && PACKAGE_JSON_PROGRAMS.has(m.program)) {
27453
28540
  try {
27454
- const pkg = JSON.parse(readFileSync17(join11(start, "package.json"), "utf-8"));
28541
+ const pkg = JSON.parse(readFileSync19(join12(start, "package.json"), "utf-8"));
27455
28542
  if (typeof pkg["name"] === "string" && typeof pkg["version"] === "string" && pkg["name"] !== "" && pkg["version"] !== "") {
27456
28543
  name = pkg["name"];
27457
28544
  version2 = pkg["version"];
@@ -27499,9 +28586,9 @@ function shrinkRecordFacts(m, env) {
27499
28586
  for (const file2 of [...found2.values()].sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0)) {
27500
28587
  let before;
27501
28588
  try {
27502
- if (!lstatSync(file2.abs).isFile())
28589
+ if (!lstatSync2(file2.abs).isFile())
27503
28590
  continue;
27504
- before = readFileSync17(file2.abs, "utf-8");
28591
+ before = readFileSync19(file2.abs, "utf-8");
27505
28592
  } catch {
27506
28593
  continue;
27507
28594
  }
@@ -27534,7 +28621,8 @@ function shrinkRecordFacts(m, env) {
27534
28621
  lines: lines2,
27535
28622
  removed,
27536
28623
  history: h === null ? null : h.history,
27537
- unsaved
28624
+ unsaved,
28625
+ ...base.pre.length === 0 ? { save: { repoRoot: file2.root, paths: [file2.rel], lines: [] } } : {}
27538
28626
  };
27539
28627
  });
27540
28628
  }
@@ -27552,7 +28640,7 @@ function recordRoot(abs, env) {
27552
28640
  return null;
27553
28641
  let dir = abs;
27554
28642
  while (!isDirectory(dir)) {
27555
- const up = dirname5(dir);
28643
+ const up = dirname6(dir);
27556
28644
  if (up === dir)
27557
28645
  return null;
27558
28646
  dir = up;
@@ -27563,7 +28651,7 @@ function recordFileAt(abs, env) {
27563
28651
  const root = recordRoot(abs, env);
27564
28652
  if (root === null || !isInside(abs, root))
27565
28653
  return null;
27566
- const rel = toSlash(relative4(root, abs));
28654
+ const rel = toSlash(relative5(root, abs));
27567
28655
  if (rel === "")
27568
28656
  return null;
27569
28657
  const row = recordPatternFor(rel);
@@ -27573,11 +28661,11 @@ function recordFilesUnder(folder, env) {
27573
28661
  const root = recordRoot(folder, env);
27574
28662
  if (root === null)
27575
28663
  return [];
27576
- const relFolder = toSlash(relative4(root, folder));
28664
+ const relFolder = toSlash(relative5(root, folder));
27577
28665
  const starts = /* @__PURE__ */ new Set();
27578
28666
  for (const { pattern } of PROJECT_RECORD_PATTERNS) {
27579
28667
  if (relFolder === "" || patternOwns(relFolder, pattern))
27580
- starts.add(join11(root, pattern));
28668
+ starts.add(join12(root, pattern));
27581
28669
  else if (patternOwns(pattern, relFolder))
27582
28670
  starts.add(folder);
27583
28671
  }
@@ -27591,7 +28679,7 @@ function recordFilesUnder(folder, env) {
27591
28679
  seen.add(p);
27592
28680
  let st;
27593
28681
  try {
27594
- st = lstatSync(p);
28682
+ st = lstatSync2(p);
27595
28683
  } catch {
27596
28684
  continue;
27597
28685
  }
@@ -27603,7 +28691,7 @@ function recordFilesUnder(folder, env) {
27603
28691
  continue;
27604
28692
  }
27605
28693
  for (const n of names)
27606
- stack.push(join11(p, n));
28694
+ stack.push(join12(p, n));
27607
28695
  } else if (st.isFile()) {
27608
28696
  const r = recordFileAt(p, env);
27609
28697
  if (r !== null)
@@ -27616,7 +28704,7 @@ function recordHistory(files, env) {
27616
28704
  const out = /* @__PURE__ */ new Map();
27617
28705
  const byTree = /* @__PURE__ */ new Map();
27618
28706
  for (const abs of files) {
27619
- const top = env.repoRoot(dirname5(abs));
28707
+ const top = env.repoRoot(dirname6(abs));
27620
28708
  if (top === null || !isInside(abs, top))
27621
28709
  continue;
27622
28710
  const list = byTree.get(top) ?? [];
@@ -27624,7 +28712,7 @@ function recordHistory(files, env) {
27624
28712
  byTree.set(top, list);
27625
28713
  }
27626
28714
  for (const [top, list] of byTree) {
27627
- const specs = list.map((abs) => toSlash(relative4(top, abs)));
28715
+ const specs = list.map((abs) => toSlash(relative5(top, abs)));
27628
28716
  const tracked = trackedOf(specs, top, env);
27629
28717
  if (tracked === null)
27630
28718
  continue;
@@ -27705,14 +28793,7 @@ function oneLine(text) {
27705
28793
  return text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").replace(/\s+/g, " ").trim();
27706
28794
  }
27707
28795
  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
- });
28796
+ const staked = orderedStaked(facts);
27716
28797
  const reason = reasonSentence(description);
27717
28798
  if (staked.length === 0)
27718
28799
  return couldNotTellQuestion(description);
@@ -27721,15 +28802,45 @@ function questionFor(facts, description) {
27721
28802
  return `Halfcycle: ${whatOf(only)} ${reason} If you allow it, ${lostOf(only)} ${NOTHING_LOST_ON_NO}`;
27722
28803
  }
27723
28804
  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}`;
28805
+ return `Halfcycle: ${shown2.join(" ")}${moreLine(staked.length - shown2.length)} ${reason} ${NOTHING_LOST_ON_NO}`;
28806
+ }
28807
+ function orderedStaked(facts) {
28808
+ const said = /* @__PURE__ */ new Set();
28809
+ 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) => {
28810
+ const part = `${whatOf(f)} ${lostOf(f)}`;
28811
+ if (said.has(part))
28812
+ return false;
28813
+ said.add(part);
28814
+ return true;
28815
+ });
28816
+ }
28817
+ function moreLine(more) {
28818
+ return more > 0 ? ` And ${more} more risky ${more === 1 ? "step" : "steps"}.` : "";
28819
+ }
28820
+ var UNNAMED_PUSH_WHAT = "Replace a line of work on the shared copy.";
28821
+ function whatsText(facts, extraPush = false) {
28822
+ const staked = orderedStaked(facts);
28823
+ const items = staked.map((f) => ({ order: riskyTypeOrder(f.type), what: whatOf(f) }));
28824
+ const pushOrder = riskyTypeOrder("rewrite-shared-history");
28825
+ if (extraPush && !staked.some((f) => f.type === "rewrite-shared-history")) {
28826
+ const at = items.findIndex((x) => x.order > pushOrder);
28827
+ items.splice(at < 0 ? items.length : at, 0, { order: pushOrder, what: UNNAMED_PUSH_WHAT });
28828
+ }
28829
+ const shown2 = items.slice(0, MAX_LISTED).map((x) => x.what);
28830
+ return `${shown2.join(" ")}${moreLine(items.length - shown2.length)}`;
28831
+ }
28832
+ function typeSentenceFor(facts, extraPush = false) {
28833
+ const whats = whatsText(facts, extraPush);
28834
+ return whats === "" ? AUTONOMY_COULD_NOT_TELL_SENTENCE : `Halfcycle: ${whats}`;
28835
+ }
28836
+ function savedWhatFor(facts) {
28837
+ return whatsText(facts);
27727
28838
  }
27728
28839
  function refusalText(question) {
27729
28840
  return `${question} ${TURN_QUESTIONS_ON_LINE}`;
27730
28841
  }
27731
28842
  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}`;
28843
+ 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
28844
  }
27734
28845
  function whatOf(f) {
27735
28846
  switch (f.type) {
@@ -27811,7 +28922,7 @@ function pushWhat(f) {
27811
28922
  const verb = lines2.length > 0 && lines2.every((l) => l.deletes) ? "Delete" : "Replace";
27812
28923
  const named = lines2.filter((l) => l.branch !== null).map((l) => l.branch);
27813
28924
  if (named.length === 0)
27814
- return `${verb} a line of work on the shared copy.`;
28925
+ return verb === "Replace" ? UNNAMED_PUSH_WHAT : `${verb} a line of work on the shared copy.`;
27815
28926
  return `${verb} the shared copy's ${named.length === 1 ? "line" : "lines"} of work ${listNames(named)}.`;
27816
28927
  }
27817
28928
  function pushLost(f) {
@@ -28013,16 +29124,71 @@ function answerModeFor(permissionMode) {
28013
29124
  return typeof permissionMode === "string" && QUESTIONS_ON_MODES.has(permissionMode) ? "questions-on" : "questions-off";
28014
29125
  }
28015
29126
  var REFUSED_IN_EVERY_MODE = /* @__PURE__ */ new Set(["outside-own-copy"]);
28016
- function decide(action, facts, mode) {
29127
+ function autonomousRowOf(type) {
29128
+ switch (type) {
29129
+ case "delete-files":
29130
+ case "discard-unsaved":
29131
+ case "delete-unmerged-work":
29132
+ case "shrink-record":
29133
+ return "save";
29134
+ case "rewrite-shared-history":
29135
+ case "wipe-data":
29136
+ return "wait-item-1";
29137
+ case "publish":
29138
+ case "deploy":
29139
+ return "publish";
29140
+ case "read-credentials":
29141
+ return "never";
29142
+ case "outside-own-copy":
29143
+ return "every-mode";
29144
+ default: {
29145
+ const unhandled = type;
29146
+ return unhandled;
29147
+ }
29148
+ }
29149
+ }
29150
+ function savedByAutonomy(type) {
29151
+ return autonomousRowOf(type) === "save";
29152
+ }
29153
+ function ownerWaitItemNumber(id) {
29154
+ return OWNER_WAIT_ITEM_IDS.indexOf(id) + 1;
29155
+ }
29156
+ function decide(action, facts, mode, mayPublishNow = () => false) {
28017
29157
  void action;
28018
29158
  if (facts.some((f) => REFUSED_IN_EVERY_MODE.has(f.type)))
28019
29159
  return "refuse";
28020
- return mode === "questions-on" ? "ask" : "refuse";
29160
+ switch (mode) {
29161
+ case "questions-on":
29162
+ return "ask";
29163
+ case "questions-off":
29164
+ return "refuse";
29165
+ case "autonomous":
29166
+ return decideAutonomous(facts, mayPublishNow);
29167
+ default: {
29168
+ const unhandled = mode;
29169
+ void unhandled;
29170
+ return "refuse";
29171
+ }
29172
+ }
29173
+ }
29174
+ function decideAutonomous(facts, mayPublishNow) {
29175
+ if (facts.length === 0)
29176
+ return { autonomous: "could-not-tell" };
29177
+ const rows = [...facts].sort((a, b) => riskyTypeOrder(a.type) - riskyTypeOrder(b.type)).map((f) => autonomousRowOf(f.type));
29178
+ if (rows.includes("never"))
29179
+ return { autonomous: "never-under-autonomy" };
29180
+ const firstWait = rows.find((r) => r === "wait-item-1" || r === "publish");
29181
+ if (firstWait === "wait-item-1")
29182
+ return { autonomous: "waits-for-owner", item: ownerWaitItemNumber("delete-users-data") };
29183
+ if (firstWait === "publish" && !mayPublishNow()) {
29184
+ return { autonomous: "waits-for-owner", item: ownerWaitItemNumber("publish-or-release") };
29185
+ }
29186
+ return rows.includes("save") ? { autonomous: "save-then-let-through" } : { autonomous: "let-through" };
28021
29187
  }
28022
29188
 
28023
29189
  // 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";
29190
+ import { readFileSync as readFileSync20 } from "node:fs";
29191
+ import { join as join13, posix as posix2, win32 as win322 } from "node:path";
28026
29192
  var OWN_COPY_START_TOOL = "Agent";
28027
29193
  var OWN_COPY_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
28028
29194
  var OWN_COPY_ISOLATION = "worktree";
@@ -28043,7 +29209,7 @@ function stringField2(o, key) {
28043
29209
  }
28044
29210
  function readOr(deps, path) {
28045
29211
  try {
28046
- return (deps.readFile ?? ((p) => readFileSync18(p, "utf-8")))(path);
29212
+ return (deps.readFile ?? ((p) => readFileSync20(p, "utf-8")))(path);
28047
29213
  } catch {
28048
29214
  return null;
28049
29215
  }
@@ -28083,7 +29249,7 @@ function typeCanChangeFiles(subagentType, deps) {
28083
29249
  return true;
28084
29250
  if (AGENT_FILE_NAME.test(type)) {
28085
29251
  for (const base of [deps.projectDir, deps.home]) {
28086
- const text = readOr(deps, join12(base, ".claude", "agents", `${type}.md`));
29252
+ const text = readOr(deps, join13(base, ".claude", "agents", `${type}.md`));
28087
29253
  if (text === null)
28088
29254
  continue;
28089
29255
  const tools = frontmatterTools(text);
@@ -28096,105 +29262,608 @@ function targetOf(call) {
28096
29262
  const t = call.toolInput;
28097
29263
  return call.toolName === "NotebookEdit" ? stringField2(t, "notebook_path") ?? stringField2(t, "file_path") : stringField2(t, "file_path");
28098
29264
  }
28099
- function partOf(top, rel, p, deps) {
28100
- const text = readOr(deps, p.join(top, ...SYSTEM_DESCRIPTION_REL.split("/")));
28101
- if (text === null)
28102
- return null;
29265
+ function partOf(top, rel, p, deps) {
29266
+ const text = readOr(deps, p.join(top, ...SYSTEM_DESCRIPTION_REL.split("/")));
29267
+ if (text === null)
29268
+ return null;
29269
+ try {
29270
+ const checked = checkSystemDescription(JSON.parse(text));
29271
+ if (!checked.ok)
29272
+ return null;
29273
+ const owner = ownerOf(rel, checked.description);
29274
+ return checked.description.parts.find((p2) => p2.id === owner)?.name ?? null;
29275
+ } catch {
29276
+ return null;
29277
+ }
29278
+ }
29279
+ function isMainCopy(c) {
29280
+ return c.top === c.main;
29281
+ }
29282
+ function judgeOwnCopy(input, call, deps) {
29283
+ try {
29284
+ if (call.toolName === OWN_COPY_START_TOOL)
29285
+ return judgeStart(input, call, deps);
29286
+ if (OWN_COPY_WRITE_TOOLS.has(call.toolName))
29287
+ return judgeChange(input, call, deps);
29288
+ return PASS;
29289
+ } catch {
29290
+ return PASS;
29291
+ }
29292
+ }
29293
+ function judgeStart(input, call, deps) {
29294
+ if (call.toolInput["isolation"] === OWN_COPY_ISOLATION)
29295
+ return PASS;
29296
+ if (!typeCanChangeFiles(call.toolInput["subagent_type"], deps))
29297
+ return PASS;
29298
+ const cwd = stringField2(input, "cwd");
29299
+ if (cwd === null || whichCopy(cwd, deps.walk) === null)
29300
+ return UNKNOWN;
29301
+ const record2 = readLiveness(stringField2(input, "session_id"), cwd, deps.liveness);
29302
+ if (!runsBesideOtherWork(call.toolInput, withoutAgent(record2, stringField2(input, "agent_id"))))
29303
+ return PASS;
29304
+ return { kind: "refused", facts: { type: "outside-own-copy", atStake: true, arm: "start" } };
29305
+ }
29306
+ function judgeChange(input, call, deps) {
29307
+ const cwd = stringField2(input, "cwd");
29308
+ const own = cwd === null ? null : whichCopy(cwd, deps.walk);
29309
+ if (cwd === null || own === null)
29310
+ return UNKNOWN;
29311
+ const given = targetOf(call);
29312
+ if (given === null)
29313
+ return PASS;
29314
+ const rules = deps.walk?.rules ?? machineRules();
29315
+ const p = rules === "windows" ? win322 : posix2;
29316
+ const place = copyHolding(given, deps.walk);
29317
+ if (place === null || place.copy.common !== own.common)
29318
+ return PASS;
29319
+ const target = place.copy;
29320
+ const agentId = stringField2(input, "agent_id");
29321
+ let arm = null;
29322
+ let record2;
29323
+ const live = () => record2 === void 0 ? record2 = readLiveness(stringField2(input, "session_id"), cwd, deps.liveness) : record2;
29324
+ if (agentId !== null) {
29325
+ if (target.top !== own.top)
29326
+ arm = "other-copy";
29327
+ else {
29328
+ const rec = live();
29329
+ const mainTop = rec?.main?.top ?? (isMainCopy(own) ? own.top : null);
29330
+ const others = withoutAgent(rec, agentId);
29331
+ if (mainTop === own.top && others !== null && others.agents.length > 0)
29332
+ arm = "main-copy-while-others";
29333
+ }
29334
+ } else if (target.top !== own.top && (live()?.agents ?? []).some((a) => a.top === target.top)) {
29335
+ arm = "agents-copy";
29336
+ }
29337
+ if (arm === null)
29338
+ return PASS;
29339
+ const inFolder = place.folder.length > target.top.length ? place.folder.slice(target.top.length + 1) : "";
29340
+ const rel = [inFolder, place.rest].filter((s) => s !== "").join(p.sep).split(p.sep).join("/");
29341
+ return {
29342
+ kind: "refused",
29343
+ facts: {
29344
+ type: "outside-own-copy",
29345
+ atStake: true,
29346
+ arm,
29347
+ part: partOf(target.top, rel, p, deps),
29348
+ file: rel,
29349
+ ownPath: p.join(own.top, ...rel.split("/")),
29350
+ ownIsMain: isMainCopy(own),
29351
+ targetIsMain: isMainCopy(target)
29352
+ }
29353
+ };
29354
+ }
29355
+
29356
+ // dist/risky/save.js
29357
+ import { randomBytes } from "node:crypto";
29358
+ 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";
29359
+ import { homedir as homedir8 } from "node:os";
29360
+ import { dirname as dirname7, isAbsolute as isAbsolute3, join as join14, resolve as resolve5 } from "node:path";
29361
+ var SAVE_LIMIT_BYTES = 2e8;
29362
+ var SAVE_BUDGET_MS = 5e3;
29363
+ function notSaved(reason) {
29364
+ return { saved: false, reason };
29365
+ }
29366
+ function savedFolderOf(engagementId, home = homedir8()) {
29367
+ if (!isEngagementId(engagementId))
29368
+ return null;
29369
+ return join14(home, HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
29370
+ }
29371
+ function saveFirst(staked, ctx) {
29372
+ const now = ctx.now ?? Date.now;
29373
+ const deadline = now() + (ctx.budgetMs ?? SAVE_BUDGET_MS);
29374
+ const folder = savedFolderOf(ctx.engagementId, ctx.home);
29375
+ if (folder === null)
29376
+ return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29377
+ const plan = planOf(staked);
29378
+ if (!plan.ok)
29379
+ return notSaved(plan.reason);
29380
+ const g = new SaveGit(plan.root, deadline, now);
29381
+ const top = g.run(["rev-parse", "--show-toplevel"]);
29382
+ if (!top.ok || realOrSelf2(top.value.trim()) !== plan.root)
29383
+ return notSaved(AUTONOMY_SAVE_NOT_COVERED_REASON);
29384
+ const head = g.run(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
29385
+ const headCommit2 = head.ok ? head.value.trim() : "";
29386
+ if (!/^[0-9a-f]{40,64}$/.test(headCommit2))
29387
+ return notSaved(AUTONOMY_SAVE_NO_HISTORY_REASON);
29388
+ try {
29389
+ pruneSaves(plan.root, ctx.engagementId, { ...ctx.home === void 0 ? {} : { home: ctx.home }, now, deadline });
29390
+ } catch {
29391
+ }
29392
+ const found2 = filesOf(plan, g, deadline);
29393
+ if (!found2.ok)
29394
+ return notSaved(found2.reason);
29395
+ let saveDir = null;
29396
+ try {
29397
+ const id = freshId(folder, ctx.newId);
29398
+ if (id === null)
29399
+ return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29400
+ saveDir = join14(folder, id);
29401
+ const repository = repositoryOf(g);
29402
+ if (repository === null)
29403
+ return failed(saveDir);
29404
+ writeFileSync12(join14(saveDir, SAVED_REPOSITORY_FILE_NAME), `${repository}
29405
+ `, { mode: 384 });
29406
+ const privateIndex = join14(saveDir, PRIVATE_INDEX_NAME);
29407
+ const commit = saveCommit(id, headCommit2, found2.tracked, plan.lines, privateIndex, g, now());
29408
+ rmSync2(privateIndex, { force: true });
29409
+ if (commit === null)
29410
+ return failed(saveDir);
29411
+ const copiesDir = join14(saveDir, SAVED_COPIES_DIR_NAME);
29412
+ for (const rel of found2.copies) {
29413
+ if (now() > deadline || !copyOne(join14(plan.root, rel), join14(copiesDir, rel)))
29414
+ return failed(saveDir);
29415
+ }
29416
+ const description = savedSchema.safeParse({
29417
+ id,
29418
+ savedAt: new Date(now()).toISOString(),
29419
+ ref: savedRefOf(id),
29420
+ what: ctx.what.slice(0, 2e3),
29421
+ paths: [
29422
+ ...found2.tracked.map((path) => ({ path, keptIn: "commit" })),
29423
+ ...found2.copies.map((path) => ({ path, keptIn: "copy" }))
29424
+ ],
29425
+ branches: plan.lines.filter((l) => l.name !== null).map((l) => ({ name: l.name, commit: l.commit }))
29426
+ });
29427
+ if (!description.success)
29428
+ return failed(saveDir);
29429
+ const file2 = join14(saveDir, SAVED_FILE_NAME);
29430
+ writeFileSync12(`${file2}.part`, `${JSON.stringify(description.data, null, 2)}
29431
+ `, { mode: 384 });
29432
+ renameSync10(`${file2}.part`, file2);
29433
+ ctx.beforeRef?.();
29434
+ const ref = g.run(["update-ref", "--no-deref", savedRefOf(id), commit, ""]);
29435
+ if (!ref.ok)
29436
+ return failed(saveDir);
29437
+ return { saved: true, id, description: description.data };
29438
+ } catch {
29439
+ return saveDir === null ? notSaved(AUTONOMY_SAVE_FAILED_REASON) : failed(saveDir);
29440
+ }
29441
+ }
29442
+ var PRIVATE_INDEX_NAME = "index.part";
29443
+ function failed(saveDir) {
29444
+ try {
29445
+ rmSync2(saveDir, { recursive: true, force: true });
29446
+ } catch {
29447
+ }
29448
+ return notSaved(AUTONOMY_SAVE_FAILED_REASON);
29449
+ }
29450
+ function planOf(staked) {
29451
+ if (staked.length === 0)
29452
+ return { ok: false, reason: AUTONOMY_COULD_NOT_TELL_REASON };
29453
+ let root = null;
29454
+ const specs = [];
29455
+ const lines2 = /* @__PURE__ */ new Map();
29456
+ for (const f of staked) {
29457
+ const t = f.save;
29458
+ if (t === void 0)
29459
+ return { ok: false, reason: f.cannotTell === void 0 ? AUTONOMY_SAVE_NOT_COVERED_REASON : AUTONOMY_COULD_NOT_TELL_REASON };
29460
+ const r = realOrSelf2(resolve5(t.repoRoot));
29461
+ if (root === null)
29462
+ root = r;
29463
+ else if (root !== r)
29464
+ return { ok: false, reason: AUTONOMY_SAVE_NOT_COVERED_REASON };
29465
+ for (const p of t.paths) {
29466
+ if (!isInsideRel(p))
29467
+ return { ok: false, reason: AUTONOMY_SAVE_NOT_COVERED_REASON };
29468
+ specs.push(t.keep === void 0 ? { path: p } : { path: p, keep: t.keep });
29469
+ }
29470
+ for (const l of t.lines)
29471
+ if (!lines2.has(l.commit) || l.name !== null)
29472
+ lines2.set(l.commit, l);
29473
+ }
29474
+ return { ok: true, root, specs, lines: [...lines2.values()] };
29475
+ }
29476
+ function isInsideRel(p) {
29477
+ if (p === ".")
29478
+ return true;
29479
+ if (p === "" || isAbsolute3(p) || p.includes("\\"))
29480
+ return false;
29481
+ return p.split("/").every((s) => s !== "" && s !== "." && s !== "..");
29482
+ }
29483
+ function under(rel, spec) {
29484
+ return spec === "." || rel === spec || rel.startsWith(`${spec}/`);
29485
+ }
29486
+ function reached(rel, specs) {
29487
+ return specs.some((s) => under(rel, s.path) && (s.keep === void 0 || s.keep(rel)));
29488
+ }
29489
+ function filesOf(plan, g, deadline) {
29490
+ if (plan.specs.length === 0)
29491
+ return { ok: true, tracked: [], copies: [] };
29492
+ const specs = [...new Set(plan.specs.map((s) => s.path))];
29493
+ const listed = g.run(["--literal-pathspecs", "ls-files", "-z", "--full-name", "--stage", "--", ...specs]);
29494
+ if (!listed.ok)
29495
+ return { ok: false, reason: AUTONOMY_SAVE_FAILED_REASON };
29496
+ const trackedAll = /* @__PURE__ */ new Set();
29497
+ const tracked = /* @__PURE__ */ new Set();
29498
+ for (const rec of listed.value.split("\0")) {
29499
+ const tab = rec.indexOf(" ");
29500
+ if (tab < 0)
29501
+ continue;
29502
+ const mode = rec.slice(0, rec.indexOf(" "));
29503
+ const path = rec.slice(tab + 1);
29504
+ if (mode === "160000")
29505
+ continue;
29506
+ trackedAll.add(path);
29507
+ if (!reached(path, plan.specs))
29508
+ continue;
29509
+ const st = lstatOrNull(join14(plan.root, path));
29510
+ if (st !== null && (st.isFile() || st.isSymbolicLink()))
29511
+ tracked.add(path);
29512
+ }
29513
+ const copies = /* @__PURE__ */ new Set();
29514
+ let bytes = 0;
29515
+ let seen = 0;
29516
+ const stack = [...specs];
29517
+ while (stack.length > 0) {
29518
+ const rel = stack.pop();
29519
+ if (rel === ".git")
29520
+ continue;
29521
+ const abs = rel === "." ? plan.root : join14(plan.root, rel);
29522
+ const st = lstatOrNull(abs);
29523
+ if (st === null)
29524
+ continue;
29525
+ if (st.isDirectory()) {
29526
+ let names;
29527
+ try {
29528
+ names = readdirSync9(abs);
29529
+ } catch {
29530
+ return { ok: false, reason: AUTONOMY_SAVE_FAILED_REASON };
29531
+ }
29532
+ for (const n of names) {
29533
+ const child = rel === "." ? n : `${rel}/${n}`;
29534
+ if (child !== ".git")
29535
+ stack.push(child);
29536
+ }
29537
+ } else if ((st.isFile() || st.isSymbolicLink()) && !trackedAll.has(rel) && !copies.has(rel) && reached(rel, plan.specs)) {
29538
+ copies.add(rel);
29539
+ bytes += st.size;
29540
+ if (bytes > SAVE_LIMIT_BYTES)
29541
+ return { ok: false, reason: AUTONOMY_SAVE_TOO_LARGE_REASON };
29542
+ }
29543
+ if (++seen % 1024 === 0 && Date.now() > deadline)
29544
+ return { ok: false, reason: AUTONOMY_SAVE_FAILED_REASON };
29545
+ }
29546
+ return { ok: true, tracked: [...tracked].sort(), copies: [...copies].sort() };
29547
+ }
29548
+ function copyOne(src, dst) {
29549
+ const st = lstatOrNull(src);
29550
+ if (st === null)
29551
+ return false;
29552
+ mkdirSync8(dirname7(dst), { recursive: true, mode: 448 });
29553
+ if (st.isSymbolicLink()) {
29554
+ symlinkSync(readlinkSync(src), dst);
29555
+ return true;
29556
+ }
29557
+ copyFileSync(src, dst, fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE);
29558
+ chmodSync2(dst, st.mode & 4095);
29559
+ return statSync8(dst).size === st.size;
29560
+ }
29561
+ function saveCommit(id, head, tracked, lines2, privateIndex, g, at) {
29562
+ const indexPath = g.run(["rev-parse", "--git-path", "index"]);
29563
+ if (!indexPath.ok)
29564
+ return null;
29565
+ const projectIndex = resolve5(g.root, indexPath.value.trim());
29566
+ const env = commitEnv(privateIndex, at);
29567
+ if (lstatOrNull(projectIndex) !== null)
29568
+ copyFileSync(projectIndex, privateIndex);
29569
+ else if (!g.run(["read-tree", head], env).ok)
29570
+ return null;
29571
+ const stagedTree = g.run(["write-tree"], env);
29572
+ if (!stagedTree.ok)
29573
+ return null;
29574
+ const staged = g.run(["commit-tree", "--no-gpg-sign", stagedTree.value.trim(), "-p", head, "-m", `Halfcycle save ${id}: staged changes`], env);
29575
+ if (!staged.ok)
29576
+ return null;
29577
+ if (tracked.length > 0 && !g.run(["update-index", "-z", "--stdin"], env, `${tracked.join("\0")}\0`).ok)
29578
+ return null;
29579
+ const tree = g.run(["write-tree"], env);
29580
+ if (!tree.ok)
29581
+ return null;
29582
+ const parents = [.../* @__PURE__ */ new Set([head, staged.value.trim(), ...lines2.map((l) => l.commit)])];
29583
+ const commit = g.run(["commit-tree", "--no-gpg-sign", tree.value.trim(), ...parents.flatMap((p) => ["-p", p]), "-m", `Halfcycle save ${id}`], env);
29584
+ if (!commit.ok)
29585
+ return null;
29586
+ const sha = commit.value.trim();
29587
+ return /^[0-9a-f]{40,64}$/.test(sha) ? sha : null;
29588
+ }
29589
+ function commitEnv(privateIndex, at) {
29590
+ const date5 = `${String(Math.floor(at / 1e3))} +0000`;
29591
+ return {
29592
+ GIT_INDEX_FILE: privateIndex,
29593
+ GIT_OPTIONAL_LOCKS: "0",
29594
+ GIT_AUTHOR_NAME: "Halfcycle",
29595
+ GIT_AUTHOR_EMAIL: "save@halfcycle.invalid",
29596
+ GIT_AUTHOR_DATE: date5,
29597
+ GIT_COMMITTER_NAME: "Halfcycle",
29598
+ GIT_COMMITTER_EMAIL: "save@halfcycle.invalid",
29599
+ GIT_COMMITTER_DATE: date5
29600
+ };
29601
+ }
29602
+ var LONGEST_CAP_MS = 2147483647;
29603
+ function wholeMsLeft(ms) {
29604
+ if (!(ms >= 1))
29605
+ return null;
29606
+ return Math.min(Math.floor(ms), LONGEST_CAP_MS);
29607
+ }
29608
+ var SaveGit = class {
29609
+ root;
29610
+ deadline;
29611
+ now;
29612
+ constructor(root, deadline, now) {
29613
+ this.root = root;
29614
+ this.deadline = deadline;
29615
+ this.now = now;
29616
+ }
29617
+ run(args2, env, input) {
29618
+ const left = wholeMsLeft(this.deadline - this.now());
29619
+ if (left === null)
29620
+ return { ok: false, message: "out of time" };
29621
+ return gitForSave(args2, this.root, left, {
29622
+ env: env ?? { GIT_OPTIONAL_LOCKS: "0" },
29623
+ ...input === void 0 ? {} : { input }
29624
+ });
29625
+ }
29626
+ };
29627
+ function freshId(folder, newId) {
29628
+ mkdirSync8(folder, { recursive: true, mode: 448 });
29629
+ for (let i = 0; i < 5; i++) {
29630
+ const id = newId?.() ?? defaultId();
29631
+ if (!savedIdSchema.safeParse(id).success)
29632
+ return null;
29633
+ try {
29634
+ mkdirSync8(join14(folder, id), { mode: 448 });
29635
+ return id;
29636
+ } catch (e) {
29637
+ if (e.code !== "EEXIST")
29638
+ return null;
29639
+ }
29640
+ }
29641
+ return null;
29642
+ }
29643
+ function defaultId() {
29644
+ const d = /* @__PURE__ */ new Date();
29645
+ const two = (n) => String(n).padStart(2, "0");
29646
+ const stamp = `${String(d.getUTCFullYear())}${two(d.getUTCMonth() + 1)}${two(d.getUTCDate())}-${two(d.getUTCHours())}${two(d.getUTCMinutes())}${two(d.getUTCSeconds())}`;
29647
+ const rand = [...randomBytes(4)].map((b) => "abcdefghijklmnopqrstuvwxyz0123456789"[b % 36]).join("");
29648
+ return `${stamp}-${rand}`;
29649
+ }
29650
+ function repositoryOf(g) {
29651
+ const r = g.run(["rev-parse", "--git-common-dir"]);
29652
+ if (!r.ok)
29653
+ return null;
29654
+ const dir = r.value.trim();
29655
+ return dir === "" ? null : realOrSelf2(resolve5(g.root, dir));
29656
+ }
29657
+ function markedRepository(dir) {
29658
+ try {
29659
+ const text = readFileSync21(join14(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
29660
+ return text === "" ? null : text;
29661
+ } catch {
29662
+ return null;
29663
+ }
29664
+ }
29665
+ function pruneSaves(root, engagementId, ctx = {}) {
29666
+ const folder = savedFolderOf(engagementId, ctx.home);
29667
+ if (folder === null)
29668
+ return;
29669
+ const now = ctx.now ?? Date.now;
29670
+ const g = new SaveGit(root, ctx.deadline ?? now() + SAVE_BUDGET_MS, now);
29671
+ const listed = g.run(["for-each-ref", "--format=%(refname)%00%(committerdate:unix)", SAVED_REF_PREFIX]);
29672
+ if (!listed.ok)
29673
+ return;
29674
+ const refs = /* @__PURE__ */ new Map();
29675
+ for (const line2 of listed.value.split("\n")) {
29676
+ const [name, when] = line2.split("\0");
29677
+ if (name === void 0 || when === void 0 || !name.startsWith(SAVED_REF_PREFIX))
29678
+ continue;
29679
+ refs.set(name.slice(SAVED_REF_PREFIX.length), Number(when) * 1e3);
29680
+ }
29681
+ const repository = repositoryOf(g);
29682
+ let names = [];
29683
+ try {
29684
+ names = readdirSync9(folder);
29685
+ } catch {
29686
+ names = [];
29687
+ }
29688
+ const here = /* @__PURE__ */ new Set();
29689
+ for (const id of names) {
29690
+ if (!savedIdSchema.safeParse(id).success)
29691
+ continue;
29692
+ here.add(id);
29693
+ const dir = join14(folder, id);
29694
+ const st = lstatOrNull(dir);
29695
+ const answer = savedPruneAnswer({
29696
+ refMadeAt: refs.get(id) ?? null,
29697
+ folderChangedAt: st !== null && st.isDirectory() ? st.mtimeMs : null,
29698
+ folderRepository: markedRepository(dir)
29699
+ }, { repository, now: now() });
29700
+ if (answer === "remove-folder")
29701
+ rmSync2(dir, { recursive: true, force: true });
29702
+ else if (answer === "remove-ref-then-folder" && g.run(["update-ref", "-d", savedRefOf(id)]).ok) {
29703
+ rmSync2(dir, { recursive: true, force: true });
29704
+ } else if (answer === "remove-ref")
29705
+ g.run(["update-ref", "-d", savedRefOf(id)]);
29706
+ }
29707
+ for (const [id, madeAt] of refs) {
29708
+ if (here.has(id) || !savedIdSchema.safeParse(id).success)
29709
+ continue;
29710
+ const answer = savedPruneAnswer({ refMadeAt: madeAt, folderChangedAt: null, folderRepository: null }, { repository, now: now() });
29711
+ if (answer === "remove-ref")
29712
+ g.run(["update-ref", "-d", savedRefOf(id)]);
29713
+ }
29714
+ }
29715
+ function lstatOrNull(p) {
29716
+ try {
29717
+ return lstatSync3(p);
29718
+ } catch {
29719
+ return null;
29720
+ }
29721
+ }
29722
+ function realOrSelf2(p) {
29723
+ try {
29724
+ return realpathSync7(p);
29725
+ } catch {
29726
+ return p;
29727
+ }
29728
+ }
29729
+
29730
+ // dist/autonomy-state.js
29731
+ import { spawnSync as spawnSync2 } from "node:child_process";
29732
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync22, renameSync as renameSync11, rmSync as rmSync3, writeFileSync as writeFileSync13 } from "node:fs";
29733
+ import { homedir as homedir9, platform as platform2 } from "node:os";
29734
+ import { dirname as dirname8, join as join15 } from "node:path";
29735
+ var AUTONOMY_STATE_FILENAME = "autonomy.json";
29736
+ var AUTONOMY_STATE_DEADLINE_MS = 1500;
29737
+ function autonomyStateFilePath(engagementId, home) {
29738
+ return join15(home ?? homedir9(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, AUTONOMY_STATE_FILENAME);
29739
+ }
29740
+ function readCachedAutonomy(engagementId, nowMs, home) {
29741
+ if (!isEngagementId(engagementId))
29742
+ return null;
29743
+ try {
29744
+ const parsed = autonomyStateFileSchema.safeParse(JSON.parse(readFileSync22(autonomyStateFilePath(engagementId, home), "utf-8")));
29745
+ if (!parsed.success || !parsed.data.on)
29746
+ return null;
29747
+ const age = nowMs - Date.parse(parsed.data.readAt);
29748
+ return Number.isFinite(age) && age >= 0 && age < AUTONOMY_STATE_FILE_MAX_AGE_MS ? parsed.data : null;
29749
+ } catch {
29750
+ return null;
29751
+ }
29752
+ }
29753
+ function confirmMayPublishNow(opts) {
29754
+ if (!isEngagementId(opts.engagementId) || CREDENTIAL_PLACEHOLDERS.includes(opts.bearer))
29755
+ return false;
29756
+ if (opts.timeoutMs <= 0)
29757
+ return false;
29758
+ try {
29759
+ const url2 = `${opts.controlOrigin}/engagements/${encodeURIComponent(opts.engagementId)}/autonomy/state`;
29760
+ const r = spawnSync2(process.execPath, ["--input-type=module", "-e", LIVE_READ_PROGRAM], {
29761
+ input: JSON.stringify({ url: url2, bearer: opts.bearer, ms: Math.max(1, opts.timeoutMs - 250) }),
29762
+ encoding: "utf-8",
29763
+ timeout: opts.timeoutMs,
29764
+ windowsHide: true
29765
+ });
29766
+ if (r.status !== 0 || typeof r.stdout !== "string")
29767
+ return false;
29768
+ const parsed = autonomyStateSchema.safeParse(JSON.parse(r.stdout));
29769
+ return parsed.success && parsed.data.on && parsed.data.mayPublish;
29770
+ } catch {
29771
+ return false;
29772
+ }
29773
+ }
29774
+ var LIVE_READ_PROGRAM = [
29775
+ "let t='';process.stdin.setEncoding('utf8');",
29776
+ "process.stdin.on('data',(c)=>{t+=c;});",
29777
+ "process.stdin.on('end',async()=>{try{const o=JSON.parse(t);",
29778
+ "const r=await fetch(o.url,{method:'GET',headers:{Authorization:'Bearer '+o.bearer},redirect:'manual',signal:AbortSignal.timeout(o.ms)});",
29779
+ "if(r.status<200||r.status>299)process.exit(1);process.stdout.write(JSON.stringify(await r.json()));process.exit(0);",
29780
+ "}catch{process.exit(1);}});"
29781
+ ].join("");
29782
+ function deleteAutonomyStateFile(engagementId, home) {
28103
29783
  try {
28104
- const checked = checkSystemDescription(JSON.parse(text));
28105
- if (!checked.ok)
28106
- return null;
28107
- const owner = ownerOf(rel, checked.description);
28108
- return checked.description.parts.find((p2) => p2.id === owner)?.name ?? null;
29784
+ rmSync3(autonomyStateFilePath(engagementId, home), { force: true });
28109
29785
  } catch {
28110
- return null;
28111
29786
  }
28112
29787
  }
28113
- function isMainCopy(c) {
28114
- return c.top === c.main;
28115
- }
28116
- function judgeOwnCopy(input, call, deps) {
29788
+ function writeAutonomyStateFile(engagementId, file2, home) {
29789
+ const target = autonomyStateFilePath(engagementId, home);
29790
+ const temp = join15(dirname8(target), `.${AUTONOMY_STATE_FILENAME}.${process.pid}.tmp`);
28117
29791
  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;
29792
+ mkdirSync9(dirname8(target), { recursive: true, mode: 448 });
29793
+ writeFileSync13(temp, `${JSON.stringify(file2)}
29794
+ `, { mode: 384 });
29795
+ if (platform2() !== "win32")
29796
+ chmodSync3(temp, 384);
29797
+ renameSync11(temp, target);
29798
+ return true;
28123
29799
  } catch {
28124
- return PASS;
29800
+ try {
29801
+ rmSync3(temp, { force: true });
29802
+ } catch {
29803
+ }
29804
+ return false;
28125
29805
  }
28126
29806
  }
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" } };
29807
+ async function readAutonomyStateFromControl(opts) {
29808
+ const controller = new AbortController();
29809
+ const timer = setTimeout(() => controller.abort(), opts.deadlineMs);
29810
+ try {
29811
+ const url2 = `${opts.controlOrigin}/engagements/${encodeURIComponent(opts.engagementId)}/autonomy/state`;
29812
+ const res = await opts.fetchFn(url2, {
29813
+ method: "GET",
29814
+ headers: { Authorization: `Bearer ${opts.bearer}` },
29815
+ redirect: "manual",
29816
+ signal: controller.signal
29817
+ });
29818
+ if (res.status < 200 || res.status > 299)
29819
+ return void 0;
29820
+ const parsed = autonomyStateSchema.safeParse(await res.json());
29821
+ return parsed.success ? parsed.data : void 0;
29822
+ } catch {
29823
+ return void 0;
29824
+ } finally {
29825
+ clearTimeout(timer);
29826
+ }
28139
29827
  }
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";
29828
+ async function cacheAutonomySetting(opts) {
29829
+ const { engagementId, home } = opts;
29830
+ if (!isEngagementId(engagementId))
29831
+ return;
29832
+ const bearer = opts.bearer;
29833
+ const usable = bearer !== void 0 && !CREDENTIAL_PLACEHOLDERS.includes(bearer) && opts.deadlineMs > 0;
29834
+ const state = usable ? await readAutonomyStateFromControl({
29835
+ controlOrigin: opts.controlOrigin,
29836
+ engagementId,
29837
+ bearer,
29838
+ fetchFn: opts.fetchFn,
29839
+ deadlineMs: opts.deadlineMs
29840
+ }) : void 0;
29841
+ if (state === void 0) {
29842
+ deleteAutonomyStateFile(engagementId, home);
29843
+ return;
29844
+ }
29845
+ const file2 = autonomyStateFileSchema.safeParse({ ...state, readAt: (/* @__PURE__ */ new Date()).toISOString() });
29846
+ if (!file2.success || !writeAutonomyStateFile(engagementId, file2.data, home)) {
29847
+ deleteAutonomyStateFile(engagementId, home);
28170
29848
  }
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
29849
  }
28189
29850
 
28190
29851
  // dist/hooks/pre-tool-use.js
28191
- import { homedir as homedir8 } from "node:os";
29852
+ import { performance } from "node:perf_hooks";
29853
+ import { homedir as homedir10 } from "node:os";
28192
29854
  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.";
29855
+ var PRE_TOOL_USE_LIMIT_MS = (HALFCYCLE_HOOK_WIRING.find((e) => e.event === "PreToolUse")?.timeout ?? 10) * 1e3;
29856
+ var AUTONOMY_REPLY_MARGIN_MS = 1e3;
29857
+ var MAY_PUBLISH_READ_MS = 3e3;
28193
29858
  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
29859
  var CHECKED = new Set(CHECKED_TOOLS);
29860
+ var RECORDED_ONLY = new Set(RECORDED_ONLY_TOOLS);
28195
29861
  function runPreToolUse(deps) {
28196
29862
  const input = parseObject(deps.input);
28197
- recordTimeEvent("action", input ?? {}, deps);
29863
+ const at = clockOf(deps)();
29864
+ const stamped = { ...deps, now: () => at };
29865
+ recordTimeEvent("action", input ?? {}, stamped);
29866
+ recordOwnShell(input, at, { ...deps.liveness?.dir === void 0 ? {} : { dir: deps.liveness.dir } });
28198
29867
  const seen = recordLiveness(input ?? {}, deps.liveness);
28199
29868
  const paceDeps = {
28200
29869
  ...deps.pace ?? {},
@@ -28204,6 +29873,10 @@ function runPreToolUse(deps) {
28204
29873
  };
28205
29874
  if (seen?.firstSeen === true && input !== null && typeof input["agent_id"] === "string")
28206
29875
  retirePendingStart(input, paceDeps);
29876
+ const { mode, autonomy } = answerModeOf(input, deps, at);
29877
+ const toolName = input === null ? void 0 : stringOf(input, "tool_name");
29878
+ if (toolName !== void 0 && RECORDED_ONLY.has(toolName))
29879
+ return recordedOnlyReply(mode);
28207
29880
  const call = input === null ? null : toolCallOf(input);
28208
29881
  if (call === null || !CHECKED.has(call.toolName))
28209
29882
  return unreadable(deps, input);
@@ -28226,7 +29899,6 @@ function runPreToolUse(deps) {
28226
29899
  }
28227
29900
  const own = ownCopyOf(input, call, deps);
28228
29901
  try {
28229
- const mode = answerModeFor(input["permission_mode"]);
28230
29902
  if (own.kind === "refused") {
28231
29903
  const staked = [...answer.silent ? [] : answer.staked, own.facts];
28232
29904
  if (decide(call, staked, mode) === "refuse") {
@@ -28235,6 +29907,11 @@ function runPreToolUse(deps) {
28235
29907
  `;
28236
29908
  }
28237
29909
  }
29910
+ if (mode === "autonomous") {
29911
+ const sendsSaves = call.toolName === "Bash" && typeof call.toolInput["command"] === "string" && pushesSavedRefs(call.toolInput["command"]);
29912
+ if (!answer.silent || sendsSaves)
29913
+ return autonomousReply(call, answer.silent ? [] : answer.staked, sendsSaves, mode, autonomy, deps);
29914
+ }
28238
29915
  if (answer.silent) {
28239
29916
  if (call.toolName === PACE_HELD_TOOL) {
28240
29917
  const held = paceHoldText(input, call.toolInput, paceDeps);
@@ -28255,12 +29932,100 @@ function runPreToolUse(deps) {
28255
29932
  return "";
28256
29933
  }
28257
29934
  }
29935
+ function clockOf(deps) {
29936
+ return deps.now ?? (() => /* @__PURE__ */ new Date());
29937
+ }
29938
+ function answerModeOf(input, deps, at) {
29939
+ const engagementId = readConfig(deps.env ?? process.env).config?.guardEngagementId;
29940
+ const cached2 = engagementId === void 0 ? null : readCachedAutonomy(engagementId, at.getTime(), deps.home);
29941
+ if (engagementId !== void 0 && cached2 !== null)
29942
+ return { mode: "autonomous", autonomy: { engagementId, mayPublish: cached2.mayPublish } };
29943
+ return { mode: answerModeFor(input === null ? void 0 : input["permission_mode"]), autonomy: null };
29944
+ }
29945
+ function autonomousReply(call, staked, sendsSaves, mode, autonomy, deps) {
29946
+ try {
29947
+ return autonomousAnswer(call, staked, sendsSaves, mode, autonomy, deps);
29948
+ } catch {
29949
+ return `${preToolUseReplyText("refuse", autonomyA12(typeSentenceFor(staked, sendsSaves), AUTONOMY_SAVE_FAILED_REASON))}
29950
+ `;
29951
+ }
29952
+ }
29953
+ function autonomousAnswer(call, staked, sendsSaves, mode, autonomy, deps) {
29954
+ const startedAt = deps.autonomy?.startedAtMs ?? performance.timeOrigin;
29955
+ const left = () => Math.floor(startedAt + PRE_TOOL_USE_LIMIT_MS - AUTONOMY_REPLY_MARGIN_MS - Date.now());
29956
+ const mayPublishNow = () => {
29957
+ if (!autonomy.mayPublish)
29958
+ return false;
29959
+ if (deps.autonomy?.mayPublishNow !== void 0)
29960
+ return deps.autonomy.mayPublishNow();
29961
+ const env = deps.env ?? process.env;
29962
+ const config2 = readConfig(env).config;
29963
+ if (config2 === null)
29964
+ return false;
29965
+ return confirmMayPublishNow({
29966
+ controlOrigin: readControlOrigin(env).origin,
29967
+ engagementId: autonomy.engagementId,
29968
+ bearer: config2.guardServiceToken,
29969
+ timeoutMs: Math.min(MAY_PUBLISH_READ_MS, left())
29970
+ });
29971
+ };
29972
+ const decided = decide(call, sendsSaves ? [...staked, { type: "rewrite-shared-history" }] : staked, mode, mayPublishNow);
29973
+ const refuse = (text) => `${preToolUseReplyText("refuse", text)}
29974
+ `;
29975
+ if (decided === "ask" || decided === "refuse")
29976
+ return refuse(autonomyA12(typeSentenceFor([]), AUTONOMY_COULD_NOT_TELL_REASON));
29977
+ switch (decided.autonomous) {
29978
+ case "waits-for-owner":
29979
+ return refuse(autonomyA10(typeSentenceFor(staked, sendsSaves), decided.item));
29980
+ case "never-under-autonomy":
29981
+ return refuse(autonomyA11(typeSentenceFor(staked, sendsSaves)));
29982
+ case "could-not-tell":
29983
+ return refuse(autonomyA12(typeSentenceFor([]), AUTONOMY_COULD_NOT_TELL_REASON));
29984
+ case "let-through":
29985
+ return "";
29986
+ case "save-then-let-through": {
29987
+ const kept = staked.filter((f) => savedByAutonomy(f.type));
29988
+ const what = savedWhatFor(kept);
29989
+ const budgetMs = Math.min(SAVE_BUDGET_MS, left());
29990
+ const saved = budgetMs <= 0 ? { saved: false, reason: AUTONOMY_SAVE_FAILED_REASON } : saveFirst(kept, {
29991
+ engagementId: autonomy.engagementId,
29992
+ what,
29993
+ budgetMs,
29994
+ ...deps.home === void 0 ? {} : { home: deps.home },
29995
+ ...deps.autonomy?.newId === void 0 ? {} : { newId: deps.autonomy.newId },
29996
+ ...deps.autonomy?.beforeRef === void 0 ? {} : { beforeRef: deps.autonomy.beforeRef }
29997
+ });
29998
+ if (!saved.saved)
29999
+ return refuse(autonomyA12(typeSentenceFor(saved.reason === AUTONOMY_COULD_NOT_TELL_REASON ? [] : staked), saved.reason));
30000
+ return `${preToolUseSavedText(autonomyA9(what, saved.id))}
30001
+ `;
30002
+ }
30003
+ default: {
30004
+ const unhandled = decided;
30005
+ return unhandled;
30006
+ }
30007
+ }
30008
+ }
30009
+ function recordedOnlyReply(mode) {
30010
+ switch (mode) {
30011
+ case "questions-on":
30012
+ case "questions-off":
30013
+ return "";
30014
+ case "autonomous":
30015
+ return `${preToolUseReplyText("refuse", AUTONOMY_A18)}
30016
+ `;
30017
+ default: {
30018
+ const unhandled = mode;
30019
+ return unhandled;
30020
+ }
30021
+ }
30022
+ }
28258
30023
  function ownCopyOf(input, call, deps) {
28259
30024
  try {
28260
30025
  const walk2 = deps.walk ?? deps.liveness?.walk;
28261
30026
  return judgeOwnCopy(input, call, {
28262
30027
  projectDir: deps.projectDir ?? projectFolder(deps.env ?? process.env),
28263
- home: deps.home ?? homedir8(),
30028
+ home: deps.home ?? homedir10(),
28264
30029
  ...walk2 === void 0 ? {} : { walk: walk2 },
28265
30030
  ...deps.liveness === void 0 ? {} : { liveness: deps.liveness }
28266
30031
  });
@@ -28300,6 +30065,8 @@ function unreadable(deps, input) {
28300
30065
  }
28301
30066
 
28302
30067
  // dist/hooks/session-diff.js
30068
+ import { unlinkSync as unlinkSync9 } from "node:fs";
30069
+ import { join as join16 } from "node:path";
28303
30070
  var STOP_HOOK_EVENT = {
28304
30071
  stop: "Stop",
28305
30072
  "subagent-stop": "SubagentStop"
@@ -28351,6 +30118,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
28351
30118
  }
28352
30119
  const repoRoot = repoRootResult.value;
28353
30120
  const sessionId = input.session_id;
30121
+ const folder = realPathOf(repoRoot);
28354
30122
  orphanSweep(repoRoot, (ref) => refExists(ref, repoRoot), sessionId, Date.now(), whichCopy(process.cwd())?.common ?? null);
28355
30123
  const base = resolveDiffBase2(repoRoot, sessionId);
28356
30124
  const diffBase = base.kind;
@@ -28359,9 +30127,16 @@ async function evaluateSession(input, kind, reply, readSeat) {
28359
30127
  reply.notice(localFaultText(`Could not get session diff: ${collected.message}`));
28360
30128
  return;
28361
30129
  }
28362
- const { getDiff } = collected;
28363
- const { evaluated: changedFiles2 } = partitionInstallerOutput(collected.files);
30130
+ const tracked = new Set(collected.files);
30131
+ const candidates = ownEditedFiles(repoRoot, sessionId).filter((relPath) => !tracked.has(relPath));
30132
+ const untracked = getUntrackedFiles(repoRoot, candidates);
30133
+ const created = untracked.ok ? untracked.value.filter((relPath) => !tracked.has(relPath)) : [];
30134
+ const getDiff = (relPath) => tracked.has(relPath) ? collected.getDiff(relPath) : getSingleFileDiff(join16(repoRoot, relPath), repoRoot);
30135
+ const { evaluated } = partitionInstallerOutput(collected.files);
30136
+ const { evaluated: createdEvaluated } = partitionInstallerOutput(created);
30137
+ const changedFiles2 = [...ownChangedFiles(repoRoot, evaluated, sessionId), ...createdEvaluated];
28364
30138
  if (changedFiles2.length === 0) {
30139
+ queueTurnEnd(keepBooks(sessionId, folder, { kind: "no-own-changes" }, kind === "stop"), reply);
28365
30140
  return;
28366
30141
  }
28367
30142
  const fileDiffEntries = changedFiles2.map((relPath) => ({
@@ -28372,6 +30147,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
28372
30147
  const digest = digestChangeState(baseKey, fileDiffEntries);
28373
30148
  const prior = readDedupe(repoRoot, sessionId);
28374
30149
  if (prior && prior.base === baseKey && prior.digest === digest) {
30150
+ queueTurnEnd(keepBooks(sessionId, folder, { kind: "unchanged" }, kind === "stop"), reply);
28375
30151
  return;
28376
30152
  }
28377
30153
  const context = loadChangeSetContext(repoRoot);
@@ -28450,30 +30226,56 @@ async function evaluateSession(input, kind, reply, readSeat) {
28450
30226
  warn: (msg) => process.stderr.write(msg + "\n")
28451
30227
  });
28452
30228
  writeDedupe(repoRoot, sessionId, { base: baseKey, digest });
28453
- actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true, reply);
30229
+ actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true, reply, sessionId, folder);
30230
+ }
30231
+ function keepBooks(sessionId, folder, outcome, topLevel) {
30232
+ const books = () => {
30233
+ const before = readFindings(sessionId);
30234
+ const { file: file2, news } = turnEndBookkeeping(before ?? emptyFindings(""), folder, outcome, topLevel, /* @__PURE__ */ new Date());
30235
+ if (before !== null && JSON.stringify(before) !== JSON.stringify(file2)) {
30236
+ if (file2.findings.length > 0)
30237
+ writeFindings(file2);
30238
+ else
30239
+ removeFindingsFile(sessionId);
30240
+ }
30241
+ return turnEndMessage(news);
30242
+ };
30243
+ const locked = withFindingsLock(sessionId, books);
30244
+ return locked === void 0 ? books() : locked;
28454
30245
  }
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);
30246
+ function removeFindingsFile(sessionId) {
30247
+ const path = findingsPath(sessionId);
30248
+ if (path === null)
28462
30249
  return;
30250
+ try {
30251
+ unlinkSync9(path);
30252
+ } catch {
28463
30253
  }
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");
30254
+ }
30255
+ function queueTurnEnd(words, reply) {
30256
+ if (words !== null)
30257
+ reply.notice(words);
30258
+ }
30259
+ function actOnEnvelope3(envelope, kind, neverBlock, reply, sessionId, folder) {
30260
+ const notRun = notRunMessage((envelope.notRun ?? []).map((n) => n.reason));
30261
+ const outcome = {
30262
+ kind: "evaluated",
30263
+ fired: envelope.guardsFired,
30264
+ notRun: (envelope.notRun ?? []).map((n) => n.patternRef)
30265
+ };
30266
+ const topLevel = kind === "stop";
28466
30267
  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}`);
30268
+ keepBooks(sessionId, folder, outcome, false);
30269
+ const reason = blockReason(envelope.guardsFired.map((g) => g.explanation));
30270
+ reply.block(notRun ? `${reason}
30271
+ ${notRun}` : reason, stoppedMessage(blockingExplanations(envelope.guardsFired)));
30272
+ }
30273
+ if (envelope.blocking && neverBlock) {
30274
+ reply.notice(couldNotFixMessage(blockingExplanations(envelope.guardsFired)));
28476
30275
  }
30276
+ queueTurnEnd(keepBooks(sessionId, folder, outcome, topLevel), reply);
30277
+ if (notRun)
30278
+ reply.notice(notRun);
28477
30279
  }
28478
30280
 
28479
30281
  // dist/hooks/stdin.js
@@ -28492,9 +30294,9 @@ async function readHookStdin() {
28492
30294
  }
28493
30295
  }
28494
30296
  function readStdin2() {
28495
- return new Promise((resolve6) => {
30297
+ return new Promise((resolve8) => {
28496
30298
  if (process.stdin.isTTY) {
28497
- resolve6("");
30299
+ resolve8("");
28498
30300
  return;
28499
30301
  }
28500
30302
  const chunks = [];
@@ -28504,7 +30306,7 @@ function readStdin2() {
28504
30306
  return;
28505
30307
  settled = true;
28506
30308
  clearTimeout(timer);
28507
- resolve6(Buffer.concat(chunks).toString("utf-8"));
30309
+ resolve8(Buffer.concat(chunks).toString("utf-8"));
28508
30310
  };
28509
30311
  const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
28510
30312
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -28584,6 +30386,22 @@ function matchScopePhraseShape(shape, shown2) {
28584
30386
  return { kind: "show-first", act: "send-back" };
28585
30387
  }
28586
30388
 
30389
+ // dist/hooks/autonomy-phrase.js
30390
+ var AUTONOMY_ON = /^autonomy on$/i;
30391
+ var AUTONOMY_OFF = /^autonomy off$/i;
30392
+ function autonomyPhraseShape(prompt) {
30393
+ if (typeof prompt !== "string")
30394
+ return null;
30395
+ const line2 = prompt.trim();
30396
+ if (LINE_BREAK.test(line2))
30397
+ return null;
30398
+ if (AUTONOMY_ON.test(line2))
30399
+ return "on";
30400
+ if (AUTONOMY_OFF.test(line2))
30401
+ return "off";
30402
+ return null;
30403
+ }
30404
+
28587
30405
  // dist/hooks/review-phrase.js
28588
30406
  var VERBS = [
28589
30407
  [/^accept /i, "accept"],
@@ -28650,20 +30468,20 @@ function matchReviewPhraseShape(shape, reviews) {
28650
30468
  }
28651
30469
 
28652
30470
  // 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";
30471
+ import { chmodSync as chmodSync4, readFileSync as readFileSync23, renameSync as renameSync12, unlinkSync as unlinkSync10, writeFileSync as writeFileSync14 } from "node:fs";
30472
+ import { homedir as homedir11, platform as platform3 } from "node:os";
30473
+ import { join as join17 } from "node:path";
28656
30474
  function recordDir(engagementId, home) {
28657
30475
  if (!isEngagementId(engagementId))
28658
30476
  return null;
28659
- return join13(home ?? homedir9(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
30477
+ return join17(home ?? homedir11(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
28660
30478
  }
28661
30479
  function readShownRecord(engagementId, home) {
28662
30480
  const dir = recordDir(engagementId, home);
28663
30481
  if (dir === null)
28664
30482
  return { entries: [], reviews: [] };
28665
30483
  try {
28666
- return parseShownRecord(readFileSync19(join13(dir, SHOWN_PLANS_FILENAME), "utf-8"));
30484
+ return parseShownRecord(readFileSync23(join17(dir, SHOWN_PLANS_FILENAME), "utf-8"));
28667
30485
  } catch {
28668
30486
  return { entries: [], reviews: [] };
28669
30487
  }
@@ -28684,24 +30502,24 @@ function moveListedReviewForward(engagementId, home, listed, reviewVersion, deci
28684
30502
  const dir = recordDir(engagementId, home);
28685
30503
  if (dir === null)
28686
30504
  return false;
28687
- const path = join13(dir, SHOWN_PLANS_FILENAME);
28688
- const current = parseShownRecord(readFileSync19(path, "utf-8"));
30505
+ const path = join17(dir, SHOWN_PLANS_FILENAME);
30506
+ const current = parseShownRecord(readFileSync23(path, "utf-8"));
28689
30507
  const at = current.reviews.findIndex((r) => r.phase === listed.phase && r.shownAt === listed.shownAt && r.reviewVersion === listed.reviewVersion);
28690
30508
  if (at === -1)
28691
30509
  return false;
28692
30510
  const acts = new Map(decided.map((d) => [d.part, d.act]));
28693
30511
  const reviews = current.reviews.map((review, i) => i === at ? { ...review, reviewVersion, parts: review.parts.map((p) => movedPart(p, acts.get(p.part))) } : review);
28694
30512
  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);
30513
+ temp = join17(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
30514
+ writeFileSync14(temp, body, { mode: 384 });
30515
+ if (platform3() !== "win32")
30516
+ chmodSync4(temp, 384);
30517
+ renameSync12(temp, path);
28700
30518
  return true;
28701
30519
  } catch {
28702
30520
  if (temp !== null) {
28703
30521
  try {
28704
- unlinkSync8(temp);
30522
+ unlinkSync10(temp);
28705
30523
  } catch {
28706
30524
  return false;
28707
30525
  }
@@ -28996,15 +30814,59 @@ function reviewLineForAnswer(context, answer) {
28996
30814
  }
28997
30815
  }
28998
30816
  }
30817
+ var AUTONOMY_NOTHING_CHANGED = "Nothing changed";
30818
+ function autonomyNotAttendedLine() {
30819
+ return line(`${AUTONOMY_NOTHING_CHANGED}: turning this on or off by typing counts only when a person types it into an open session.`);
30820
+ }
30821
+ function autonomyNotSignedInLine() {
30822
+ return line(`${AUTONOMY_NOTHING_CHANGED}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
30823
+ }
30824
+ function autonomyNotOwnerLine() {
30825
+ return line(`${AUTONOMY_NOTHING_CHANGED}: only the project's owner can turn this on or off.`);
30826
+ }
30827
+ function autonomyNoOwnerLine() {
30828
+ return line(`${AUTONOMY_NOTHING_CHANGED}: this project has no owner, so nobody can turn this on or off.`);
30829
+ }
30830
+ function autonomyNotOfferedLine() {
30831
+ return line(`${AUTONOMY_NOTHING_CHANGED}. ${TYPED_APPROVAL_OFF_SENTENCE}`);
30832
+ }
30833
+ function autonomyCannotReachLine() {
30834
+ return line(`${AUTONOMY_NOTHING_CHANGED}: Halfcycle could not be reached just now. Type the line again in a minute.`);
30835
+ }
30836
+ function autonomyLineForAnswer(answer) {
30837
+ switch (answer.outcome) {
30838
+ case "recorded":
30839
+ return line(answer.on ? AUTONOMY_A4 : AUTONOMY_A5);
30840
+ case "not-owner":
30841
+ return autonomyNotOwnerLine();
30842
+ case "no-owner":
30843
+ return autonomyNoOwnerLine();
30844
+ case "not-offered":
30845
+ return autonomyNotOfferedLine();
30846
+ case "unavailable":
30847
+ return autonomyCannotReachLine();
30848
+ default: {
30849
+ const unreachable = answer;
30850
+ return unreachable;
30851
+ }
30852
+ }
30853
+ }
28999
30854
 
29000
30855
  // dist/hooks/user-prompt-submit.js
29001
30856
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
29002
30857
  var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
29003
30858
  async function runUserPromptSubmit(opts) {
29004
30859
  let act = null;
30860
+ let autonomyAct = null;
29005
30861
  const review = { recognised: false };
29006
30862
  try {
29007
30863
  const prompt = promptOf(opts.input);
30864
+ const autonomyShape = autonomyPhraseShape(prompt);
30865
+ if (autonomyShape !== null) {
30866
+ autonomyAct = autonomyShape;
30867
+ return `${await decideAutonomy(autonomyShape, opts)}
30868
+ `;
30869
+ }
29008
30870
  const reviewShape = reviewPhraseShape(prompt);
29009
30871
  if (reviewShape !== null) {
29010
30872
  const out = await decideReview(reviewShape, opts, review);
@@ -29018,6 +30880,9 @@ async function runUserPromptSubmit(opts) {
29018
30880
  return `${await decide2(shape, opts)}
29019
30881
  `;
29020
30882
  } catch {
30883
+ if (autonomyAct !== null)
30884
+ return `${autonomyCannotReachLine()}
30885
+ `;
29021
30886
  if (review.recognised)
29022
30887
  return `${reviewCannotReachLine()}
29023
30888
  `;
@@ -29025,6 +30890,44 @@ async function runUserPromptSubmit(opts) {
29025
30890
  `;
29026
30891
  }
29027
30892
  }
30893
+ async function decideAutonomy(act, opts) {
30894
+ const env = opts.env ?? process.env;
30895
+ if (!readSessionAttended(env))
30896
+ return autonomyNotAttendedLine();
30897
+ const read = readConfig(env);
30898
+ if (read.config === null) {
30899
+ return readNeverConfiguredReason(env) === "not-this-account" ? autonomyNotOwnerLine() : autonomyNotSignedInLine();
30900
+ }
30901
+ const { guardServiceToken, guardEngagementId } = read.config;
30902
+ return postAutonomy(act, guardEngagementId, guardServiceToken, env, opts.fetchFn);
30903
+ }
30904
+ async function postAutonomy(act, engagementId, credential, env, fetchFn = globalThis.fetch) {
30905
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/autonomy/chat`;
30906
+ const controller = new AbortController();
30907
+ const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
30908
+ try {
30909
+ const response = await fetchFn(url2, {
30910
+ method: "POST",
30911
+ headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json" },
30912
+ body: JSON.stringify(autonomyChatRequestSchema.parse({ act })),
30913
+ signal: controller.signal
30914
+ });
30915
+ if (response.status === 401)
30916
+ return autonomyNotSignedInLine();
30917
+ if (response.status === 429 || response.status >= 500)
30918
+ return autonomyCannotReachLine();
30919
+ const parsed = autonomyChatResultSchema.safeParse(await response.json());
30920
+ if (!parsed.success)
30921
+ return autonomyCannotReachLine();
30922
+ if (parsed.data.outcome === "recorded" && response.status !== 200)
30923
+ return autonomyCannotReachLine();
30924
+ return autonomyLineForAnswer(parsed.data);
30925
+ } catch {
30926
+ return autonomyCannotReachLine();
30927
+ } finally {
30928
+ clearTimeout(timer);
30929
+ }
30930
+ }
29028
30931
  async function decideReview(shape, opts, progress) {
29029
30932
  const env = opts.env ?? process.env;
29030
30933
  if (shape.kind === "phase") {
@@ -29206,18 +31109,36 @@ async function post(act, entry, request, engagementId, credential, env, fetchFn
29206
31109
  }
29207
31110
  }
29208
31111
 
31112
+ // dist/hooks/user-prompt-findings.js
31113
+ function runUserPromptFindings(opts) {
31114
+ try {
31115
+ const parsed = JSON.parse(opts.input);
31116
+ if (typeof parsed !== "object" || parsed === null)
31117
+ return "";
31118
+ const sessionId = parsed["session_id"];
31119
+ const given = deliverFindings(sessionId, null);
31120
+ if (given.length === 0)
31121
+ return "";
31122
+ const project = realPathOf(projectFolder(opts.env ?? process.env));
31123
+ return `${userPromptContextReply(detailsBlock(given, project))}
31124
+ `;
31125
+ } catch {
31126
+ return "";
31127
+ }
31128
+ }
31129
+
29209
31130
  // 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";
31131
+ import { existsSync as existsSync8, readFileSync as readFileSync24, realpathSync as realpathSync8 } from "node:fs";
31132
+ import { join as join18, sep as sep4 } from "node:path";
31133
+ import { homedir as homedir12 } from "node:os";
29213
31134
  function defaultRegistryPath() {
29214
- return join14(homedir10(), ".halfcycle", "registry.json");
31135
+ return join18(homedir12(), ".halfcycle", "registry.json");
29215
31136
  }
29216
31137
  function readRegistry(registryPath = defaultRegistryPath()) {
29217
- if (!existsSync7(registryPath))
31138
+ if (!existsSync8(registryPath))
29218
31139
  return {};
29219
31140
  try {
29220
- const parsed = JSON.parse(readFileSync20(registryPath, "utf-8"));
31141
+ const parsed = JSON.parse(readFileSync24(registryPath, "utf-8"));
29221
31142
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
29222
31143
  return {};
29223
31144
  const out = {};
@@ -29232,7 +31153,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
29232
31153
  }
29233
31154
  function canonicalise(p) {
29234
31155
  try {
29235
- return realpathSync6(p);
31156
+ return realpathSync8(p);
29236
31157
  } catch {
29237
31158
  return p;
29238
31159
  }
@@ -29253,7 +31174,7 @@ function resolveEngagement(cwd, registry2) {
29253
31174
  function isSelfOrAncestor(ancestor, target) {
29254
31175
  if (ancestor === target)
29255
31176
  return true;
29256
- const withSep = ancestor.endsWith(sep3) ? ancestor : ancestor + sep3;
31177
+ const withSep = ancestor.endsWith(sep4) ? ancestor : ancestor + sep4;
29257
31178
  return target.startsWith(withSep);
29258
31179
  }
29259
31180
  function runResolveEngagement(cwd, registryPath) {
@@ -29265,13 +31186,13 @@ function runResolveEngagement(cwd, registryPath) {
29265
31186
  }
29266
31187
 
29267
31188
  // dist/refresh-credential.js
29268
- import { readFileSync as readFileSync22 } from "node:fs";
29269
- import { resolve as resolve5 } from "node:path";
31189
+ import { readFileSync as readFileSync26 } from "node:fs";
31190
+ import { resolve as resolve7 } from "node:path";
29270
31191
 
29271
31192
  // 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";
31193
+ import { readFileSync as readFileSync25 } from "node:fs";
31194
+ import { homedir as homedir13 } from "node:os";
31195
+ import { dirname as dirname9, resolve as resolve6 } from "node:path";
29275
31196
  function defaultWriteErr(text) {
29276
31197
  process.stderr.write(text);
29277
31198
  }
@@ -29284,12 +31205,12 @@ function headerLine(bearer, clientVersion) {
29284
31205
  }
29285
31206
  function projectRootFromBinary(binaryPath) {
29286
31207
  const depth = VENDORED_BIN_REL.split("/").length - 1;
29287
- return resolve4(dirname6(binaryPath), ...Array(depth).fill(".."));
31208
+ return resolve6(dirname9(binaryPath), ...Array(depth).fill(".."));
29288
31209
  }
29289
31210
  function readPinnedVersion(root) {
29290
31211
  let raw;
29291
31212
  try {
29292
- raw = readFileSync21(resolve4(root, BUNDLE_PIN_REL), "utf-8");
31213
+ raw = readFileSync25(resolve6(root, BUNDLE_PIN_REL), "utf-8");
29293
31214
  } catch {
29294
31215
  return void 0;
29295
31216
  }
@@ -29331,7 +31252,7 @@ function sentences(lines2) {
29331
31252
  }
29332
31253
  function badIdSentence(root) {
29333
31254
  return sentences([
29334
- `${resolve4(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
31255
+ `${resolve6(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
29335
31256
  'so no credential was read. Delete that file and run "npx halfcycle" again: this repository',
29336
31257
  "is then set up as a new Halfcycle project."
29337
31258
  ]);
@@ -29345,7 +31266,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
29345
31266
  ]);
29346
31267
  case "no-id":
29347
31268
  return sentences([
29348
- `${resolve4(root, BUNDLE_PIN_REL)} names no engagement id.`,
31269
+ `${resolve6(root, BUNDLE_PIN_REL)} names no engagement id.`,
29349
31270
  'run "npx halfcycle" here to rewrite it.'
29350
31271
  ]);
29351
31272
  case "bad-id":
@@ -29362,7 +31283,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
29362
31283
  case "no-credential":
29363
31284
  return sentences([
29364
31285
  `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.`
31286
+ `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
31287
  ]);
29367
31288
  }
29368
31289
  }
@@ -29385,7 +31306,7 @@ async function refreshWithinDeadline(source, opts, writeErr) {
29385
31306
  }
29386
31307
  async function chooseBearer(opts) {
29387
31308
  const writeErr = opts.writeErr ?? defaultWriteErr;
29388
- const home = opts.home ?? homedir11();
31309
+ const home = opts.home ?? homedir13();
29389
31310
  const root = projectRootFromBinary(opts.binaryPath);
29390
31311
  const environmentToken = opts.environmentTokenFirst ? readEnvironmentToken() : void 0;
29391
31312
  const store = resolveStore(root, home);
@@ -29512,10 +31433,11 @@ async function refreshCredentialToken(opts) {
29512
31433
  var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
29513
31434
  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
31435
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
31436
+ var SESSION_START_PROBE_ATTEMPTS = 2;
29515
31437
  function registeredMcpUrl(projectRoot) {
29516
31438
  let parsed;
29517
31439
  try {
29518
- parsed = JSON.parse(readFileSync22(resolve5(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
31440
+ parsed = JSON.parse(readFileSync26(resolve7(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
29519
31441
  } catch {
29520
31442
  return void 0;
29521
31443
  }
@@ -29581,11 +31503,11 @@ async function postInitialize(url2, bearer, fetchFn, signal) {
29581
31503
  }
29582
31504
  return namesItsServer(body) ? void 0 : SERVICE_PROBLEM_SENTENCE;
29583
31505
  }
29584
- async function probeDelivery(url2, bearer, fetchFn, deadlineMs) {
31506
+ async function probeDelivery(url2, bearer, fetchFn, deadlineMs, maxAttempts) {
29585
31507
  const controller = new AbortController();
29586
31508
  const timer = setTimeout(() => controller.abort(), deadlineMs);
29587
31509
  try {
29588
- for (let attempt2 = 0; attempt2 < 2; attempt2++) {
31510
+ for (let attempt2 = 0; attempt2 < maxAttempts; attempt2++) {
29589
31511
  const outcome = await postInitialize(url2, bearer, fetchFn, controller.signal);
29590
31512
  if (outcome !== CUT_OFF)
29591
31513
  return outcome;
@@ -29611,7 +31533,7 @@ async function checkConnection(renewedToken, opts) {
29611
31533
  if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
29612
31534
  return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer };
29613
31535
  }
29614
- const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
31536
+ const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS, opts.maxAttempts ?? SESSION_START_PROBE_ATTEMPTS);
29615
31537
  return { sentence: outcome ?? CONNECTED_SENTENCE, url: url2, bearer };
29616
31538
  }
29617
31539
  function positionRequestBody(engagementId) {
@@ -29722,6 +31644,20 @@ async function runRefreshCredential(opts) {
29722
31644
  }
29723
31645
  }
29724
31646
  }
31647
+ if (CHECKED_SOURCES.has(input["source"])) {
31648
+ const config2 = readConfig().config;
31649
+ if (config2 !== null) {
31650
+ const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
31651
+ await cacheAutonomySetting({
31652
+ controlOrigin: readControlOrigin().origin,
31653
+ engagementId: config2.guardEngagementId,
31654
+ bearer: renewedToken ?? config2.guardServiceToken,
31655
+ fetchFn: opts.fetchFn ?? globalThis.fetch,
31656
+ home: opts.home,
31657
+ deadlineMs: Math.min(opts.autonomyStateDeadlineMs ?? AUTONOMY_STATE_DEADLINE_MS, remainingForMargin)
31658
+ });
31659
+ }
31660
+ }
29725
31661
  if (readConfig().config === null) {
29726
31662
  const remedySaid = parts.some(namesTheSignInCommand);
29727
31663
  parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid, pinnedEngagementId(projectRootFromBinary(opts.binaryPath))));
@@ -29748,8 +31684,8 @@ async function runRefreshCredential(opts) {
29748
31684
  import { fileURLToPath } from "node:url";
29749
31685
 
29750
31686
  // 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";
31687
+ import { readFileSync as readFileSync27, writeFileSync as writeFileSync15, mkdirSync as mkdirSync10, existsSync as existsSync9 } from "node:fs";
31688
+ import { dirname as dirname10 } from "node:path";
29753
31689
  function runCapture(provider, endpoint, opts = {}) {
29754
31690
  const repoRoot = opts.repoRoot ?? process.cwd();
29755
31691
  const raw = opts.input ?? readInputFile(opts.fromFile);
@@ -29784,8 +31720,8 @@ function runCapture(provider, endpoint, opts = {}) {
29784
31720
  const fixture = fixturePath(repoRoot, provider, slug);
29785
31721
  const manifestFile = manifestPath(repoRoot);
29786
31722
  try {
29787
- mkdirSync8(dirname7(fixture), { recursive: true });
29788
- writeFileSync12(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
31723
+ mkdirSync10(dirname10(fixture), { recursive: true });
31724
+ writeFileSync15(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
29789
31725
  } catch (err) {
29790
31726
  const msg = err instanceof Error ? err.message : String(err);
29791
31727
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -29816,7 +31752,7 @@ function readInputFile(fromFile) {
29816
31752
  if (!fromFile)
29817
31753
  return void 0;
29818
31754
  try {
29819
- return readFileSync23(fromFile, "utf-8");
31755
+ return readFileSync27(fromFile, "utf-8");
29820
31756
  } catch {
29821
31757
  return void 0;
29822
31758
  }
@@ -29827,11 +31763,11 @@ function relativeFixturePath(repoRoot, fixture) {
29827
31763
  }
29828
31764
 
29829
31765
  // 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";
31766
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
31767
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
31768
+ import { createWriteStream, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync28, renameSync as renameSync13, unlinkSync as unlinkSync11, writeFileSync as writeFileSync16 } from "node:fs";
31769
+ import { homedir as homedir14 } from "node:os";
31770
+ import { dirname as dirname11, join as join19 } from "node:path";
29835
31771
 
29836
31772
  // dist/test-counts.js
29837
31773
  function sum(values) {
@@ -29866,36 +31802,36 @@ function jestCounts(output) {
29866
31802
  if (lines2.length === 0)
29867
31803
  return null;
29868
31804
  let passed = 0;
29869
- let failed = 0;
31805
+ let failed2 = 0;
29870
31806
  for (const line2 of lines2) {
29871
31807
  passed += firstNumber(line2[1], /(\d+) passed/) ?? 0;
29872
- failed += firstNumber(line2[1], /(\d+) failed/) ?? 0;
31808
+ failed2 += firstNumber(line2[1], /(\d+) failed/) ?? 0;
29873
31809
  }
29874
- return { passed, failed };
31810
+ return { passed, failed: failed2 };
29875
31811
  }
29876
31812
  function vitestCounts(output) {
29877
31813
  const lines2 = [...output.matchAll(/^\s*Tests\s+(.+)$/gm)];
29878
31814
  if (lines2.length === 0)
29879
31815
  return null;
29880
31816
  let passed = 0;
29881
- let failed = 0;
31817
+ let failed2 = 0;
29882
31818
  for (const line2 of lines2) {
29883
31819
  passed += firstNumber(line2[1], /(\d+) passed/) ?? 0;
29884
- failed += firstNumber(line2[1], /(\d+) failed/) ?? 0;
31820
+ failed2 += firstNumber(line2[1], /(\d+) failed/) ?? 0;
29885
31821
  }
29886
- return { passed, failed };
31822
+ return { passed, failed: failed2 };
29887
31823
  }
29888
31824
  function pytestCounts(output) {
29889
31825
  const lines2 = [...output.matchAll(/^\W*((?:\d+ \w+(?:, )?)+)\s+in [\d.]+s\W*$/gm)];
29890
31826
  if (lines2.length === 0)
29891
31827
  return null;
29892
31828
  let passed = 0;
29893
- let failed = 0;
31829
+ let failed2 = 0;
29894
31830
  for (const line2 of lines2) {
29895
31831
  passed += firstNumber(line2[1], /(\d+) passed/) ?? 0;
29896
- failed += firstNumber(line2[1], /(\d+) failed/) ?? 0;
31832
+ failed2 += firstNumber(line2[1], /(\d+) failed/) ?? 0;
29897
31833
  }
29898
- return { passed, failed };
31834
+ return { passed, failed: failed2 };
29899
31835
  }
29900
31836
  function cargoTestCounts(output) {
29901
31837
  const lines2 = [...output.matchAll(/^test result: (?:ok|FAILED)\.\s*(\d+) passed;\s*(\d+) failed;/gm)];
@@ -29940,19 +31876,19 @@ function isProofTestPath(path) {
29940
31876
  function testRunsDir(engagementId, home) {
29941
31877
  if (!isEngagementId(engagementId))
29942
31878
  return null;
29943
- return join15(home ?? homedir12(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, TEST_RUNS_DIR_NAME);
31879
+ return join19(home ?? homedir14(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, TEST_RUNS_DIR_NAME);
29944
31880
  }
29945
31881
  function commandIdOf(command) {
29946
- return createHash4("sha256").update(command, "utf8").digest("hex").slice(0, 16);
31882
+ return createHash5("sha256").update(command, "utf8").digest("hex").slice(0, 16);
29947
31883
  }
29948
31884
  var PLAIN_WORD = /^[A-Za-z0-9_./:=@%+,-]+$/;
29949
- function commandFromWords(words, platform3 = process.platform) {
31885
+ function commandFromWords(words, platform4 = process.platform) {
29950
31886
  if (words.length === 1)
29951
31887
  return words[0];
29952
31888
  return words.map((word) => {
29953
31889
  if (PLAIN_WORD.test(word))
29954
31890
  return word;
29955
- return platform3 === "win32" ? `"${word.replace(/"/g, '""')}"` : quoteForShell(word);
31891
+ return platform4 === "win32" ? `"${word.replace(/"/g, '""')}"` : quoteForShell(word);
29956
31892
  }).join(" ");
29957
31893
  }
29958
31894
  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 +31962,21 @@ function parseProveArgs(args2) {
30026
31962
  };
30027
31963
  }
30028
31964
  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);
31965
+ mkdirSync11(dirname11(path), { recursive: true, mode: 448 });
31966
+ const tmp = `${path}.${process.pid}.${randomUUID3()}.tmp`;
31967
+ writeFileSync16(tmp, JSON.stringify(value), { mode: 384 });
31968
+ renameSync13(tmp, path);
30033
31969
  }
30034
31970
  function subjectKey(subject) {
30035
31971
  return subject.kind === "task" ? ["task", subject.phase, subject.task] : ["change", subject.change];
30036
31972
  }
30037
31973
  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`);
31974
+ const key = createHash5("sha256").update(JSON.stringify([...subjectKey(subject), label]), "utf8").digest("hex");
31975
+ return join19(dir, COMMANDS_DIR_NAME, `${key.slice(0, 32)}.json`);
30040
31976
  }
30041
31977
  function readKeptCheck(dir, subject, label) {
30042
31978
  try {
30043
- const raw = JSON.parse(readFileSync24(keptCheckPath(dir, subject, label), "utf-8"));
31979
+ const raw = JSON.parse(readFileSync28(keptCheckPath(dir, subject, label), "utf-8"));
30044
31980
  if (typeof raw.command !== "string" || raw.command === "" || typeof raw.commandId !== "string")
30045
31981
  return null;
30046
31982
  const b = raw.before;
@@ -30051,21 +31987,21 @@ function readKeptCheck(dir, subject, label) {
30051
31987
  }
30052
31988
  }
30053
31989
  function queuePath(dir, runId) {
30054
- return join15(dir, QUEUE_DIR_NAME, `${runId}.json`);
31990
+ return join19(dir, QUEUE_DIR_NAME, `${runId}.json`);
30055
31991
  }
30056
31992
  function readQueue(dir) {
30057
31993
  let names;
30058
31994
  try {
30059
- names = readdirSync9(join15(dir, QUEUE_DIR_NAME)).filter((n) => n.endsWith(".json"));
31995
+ names = readdirSync10(join19(dir, QUEUE_DIR_NAME)).filter((n) => n.endsWith(".json"));
30060
31996
  } catch {
30061
31997
  return [];
30062
31998
  }
30063
31999
  const reports = [];
30064
32000
  for (const name of names) {
30065
- const path = join15(dir, QUEUE_DIR_NAME, name);
32001
+ const path = join19(dir, QUEUE_DIR_NAME, name);
30066
32002
  let parsed = null;
30067
32003
  try {
30068
- parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync24(path, "utf-8")));
32004
+ parsed = testRunReportSchema.safeParse(JSON.parse(readFileSync28(path, "utf-8")));
30069
32005
  } catch {
30070
32006
  parsed = null;
30071
32007
  }
@@ -30078,7 +32014,7 @@ function readQueue(dir) {
30078
32014
  }
30079
32015
  function removeFile(path) {
30080
32016
  try {
30081
- unlinkSync9(path);
32017
+ unlinkSync11(path);
30082
32018
  } catch {
30083
32019
  }
30084
32020
  }
@@ -30118,13 +32054,13 @@ async function sendQueuedRuns(dir, config2, controlOrigin, fetchFn) {
30118
32054
  }
30119
32055
  return outcomes;
30120
32056
  }
30121
- function killTree(child, platform3, signal) {
32057
+ function killTree(child, platform4, signal) {
30122
32058
  const pid = child.pid;
30123
32059
  if (pid === void 0)
30124
32060
  return;
30125
32061
  try {
30126
- if (platform3 === "win32") {
30127
- spawnSync2("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
32062
+ if (platform4 === "win32") {
32063
+ spawnSync3("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
30128
32064
  } else {
30129
32065
  process.kill(-pid, signal);
30130
32066
  }
@@ -30136,16 +32072,16 @@ function killTree(child, platform3, signal) {
30136
32072
  }
30137
32073
  }
30138
32074
  function runCommand(command, repoRoot, logPath, env, deps) {
30139
- const platform3 = deps.platform ?? process.platform;
32075
+ const platform4 = deps.platform ?? process.platform;
30140
32076
  const out = deps.stdout ?? process.stdout;
30141
32077
  const err = deps.stderr ?? process.stderr;
30142
32078
  const deadline = deps.deadlineMs ?? PROVE_RUN_DEADLINE_MS;
30143
32079
  const childEnv = { ...env };
30144
32080
  delete childEnv[SERVICE_TOKEN_VAR];
30145
- mkdirSync9(dirname8(logPath), { recursive: true, mode: 448 });
32081
+ mkdirSync11(dirname11(logPath), { recursive: true, mode: 448 });
30146
32082
  const log = createWriteStream(logPath, { mode: 384 });
30147
32083
  const began = process.hrtime.bigint();
30148
- return new Promise((resolve6) => {
32084
+ return new Promise((resolve8) => {
30149
32085
  let timedOut = false;
30150
32086
  let started = true;
30151
32087
  let settled = false;
@@ -30154,15 +32090,15 @@ function runCommand(command, repoRoot, logPath, env, deps) {
30154
32090
  env: childEnv,
30155
32091
  shell: true,
30156
32092
  stdio: ["ignore", "pipe", "pipe"],
30157
- detached: platform3 !== "win32",
32093
+ detached: platform4 !== "win32",
30158
32094
  windowsHide: true
30159
32095
  });
30160
- const onSignal = (signal) => killTree(child, platform3, signal);
32096
+ const onSignal = (signal) => killTree(child, platform4, signal);
30161
32097
  process.on("SIGINT", onSignal);
30162
32098
  process.on("SIGTERM", onSignal);
30163
32099
  const timer = setTimeout(() => {
30164
32100
  timedOut = true;
30165
- killTree(child, platform3, "SIGKILL");
32101
+ killTree(child, platform4, "SIGKILL");
30166
32102
  }, deadline);
30167
32103
  child.stdout?.on("data", (chunk) => {
30168
32104
  out.write(chunk);
@@ -30180,7 +32116,7 @@ function runCommand(command, repoRoot, logPath, env, deps) {
30180
32116
  process.off("SIGINT", onSignal);
30181
32117
  process.off("SIGTERM", onSignal);
30182
32118
  const durationMs = Number((process.hrtime.bigint() - began) / 1000000n);
30183
- log.end(() => resolve6({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
32119
+ log.end(() => resolve8({ exitCode: timedOut || !started ? null : code, timedOut, started, durationMs }));
30184
32120
  };
30185
32121
  child.on("error", (e) => {
30186
32122
  started = false;
@@ -30301,14 +32237,14 @@ ${PROVE_USAGE}`);
30301
32237
  }
30302
32238
  if (request.when === "before" && changedOutsideTests > 0)
30303
32239
  say(lateBeforeLine(changedOutsideTests));
30304
- const runId = randomUUID2();
30305
- const logPath = join15(dir, `${runId}.log`);
32240
+ const runId = randomUUID3();
32241
+ const logPath = join19(dir, `${runId}.log`);
30306
32242
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
30307
32243
  const deadline = deps.deadlineMs ?? PROVE_RUN_DEADLINE_MS;
30308
32244
  const end = await runCommand(command, repoRoot, logPath, env, deps);
30309
32245
  let output = "";
30310
32246
  try {
30311
- output = readFileSync24(logPath, "utf-8");
32247
+ output = readFileSync28(logPath, "utf-8");
30312
32248
  } catch {
30313
32249
  output = "";
30314
32250
  }
@@ -30358,7 +32294,7 @@ ${PROVE_USAGE}`);
30358
32294
 
30359
32295
  // dist/statusline.js
30360
32296
  import { spawn as spawn3 } from "node:child_process";
30361
- import { readFileSync as readFileSync25 } from "node:fs";
32297
+ import { readFileSync as readFileSync29 } from "node:fs";
30362
32298
  import nodePath5 from "node:path";
30363
32299
  var OWN_STATUS_LINE_DEADLINE_MS = 5e3;
30364
32300
  var PERSON_SETTINGS_FILENAME = "settings.json";
@@ -30396,7 +32332,7 @@ function halfcycleStatusLineOf(figures) {
30396
32332
  }
30397
32333
  function personStatusLineCommand(configFolder) {
30398
32334
  try {
30399
- const v = JSON.parse(readFileSync25(nodePath5.join(configFolder, PERSON_SETTINGS_FILENAME), "utf-8"));
32335
+ const v = JSON.parse(readFileSync29(nodePath5.join(configFolder, PERSON_SETTINGS_FILENAME), "utf-8"));
30400
32336
  const line2 = v["statusLine"];
30401
32337
  if (typeof line2 !== "object" || line2 === null)
30402
32338
  return null;
@@ -30420,14 +32356,14 @@ function personCommandEnv(env) {
30420
32356
  }
30421
32357
  return out;
30422
32358
  }
30423
- var runOwnCommandReal = (command, input, env) => new Promise((resolve6) => {
32359
+ var runOwnCommandReal = (command, input, env) => new Promise((resolve8) => {
30424
32360
  let out = "";
30425
32361
  let settled = false;
30426
32362
  const done = (v) => {
30427
32363
  if (settled)
30428
32364
  return;
30429
32365
  settled = true;
30430
- resolve6(v);
32366
+ resolve8(v);
30431
32367
  };
30432
32368
  try {
30433
32369
  const child = spawn3(command, {
@@ -30490,6 +32426,43 @@ async function runStatusline(raw, deps = {}) {
30490
32426
  `;
30491
32427
  }
30492
32428
 
32429
+ // dist/check-connection.js
32430
+ function connectionKind(bearer, sentence) {
32431
+ if (bearer === NOT_THIS_ACCOUNT_PLACEHOLDER)
32432
+ return "other-account";
32433
+ if (bearer === UPDATE_CLAUDE_CODE_PLACEHOLDER)
32434
+ return "update-claude-code";
32435
+ if (bearer === NO_CREDENTIAL_PLACEHOLDER)
32436
+ return "not-signed-in";
32437
+ if (sentence === CONNECTED_SENTENCE)
32438
+ return "connected";
32439
+ if (sentence === CANNOT_REACH_SENTENCE)
32440
+ return "cannot-reach";
32441
+ if (sentence === SERVICE_PROBLEM_SENTENCE)
32442
+ return "service-problem";
32443
+ if (sentence === NO_CREDENTIAL_SENTENCE)
32444
+ return "signed-out";
32445
+ throw new Error("the connection check gave an outcome with no kind");
32446
+ }
32447
+ async function checkConnectionLine(opts) {
32448
+ const result = await checkConnection(void 0, {
32449
+ ...opts,
32450
+ maxAttempts: 1,
32451
+ writeErr: opts.writeErr ?? discardReason
32452
+ });
32453
+ if (result === void 0)
32454
+ return { kind: "not-registered", sentence: null };
32455
+ return { kind: connectionKind(result.bearer, result.sentence), sentence: result.sentence };
32456
+ }
32457
+ async function runCheckConnection(opts) {
32458
+ const line2 = await checkConnectionLine(opts);
32459
+ return `${JSON.stringify(line2)}
32460
+ `;
32461
+ }
32462
+ function discardReason() {
32463
+ return void 0;
32464
+ }
32465
+
30493
32466
  // dist/bin.js
30494
32467
  var args = process.argv.slice(2);
30495
32468
  async function main() {
@@ -30572,6 +32545,18 @@ async function main() {
30572
32545
  process.stdout.write(line2, () => process.exit(0));
30573
32546
  return;
30574
32547
  }
32548
+ if (cmd === "check-connection") {
32549
+ let line2;
32550
+ try {
32551
+ line2 = await runCheckConnection({ binaryPath: fileURLToPath(import.meta.url) });
32552
+ } catch {
32553
+ process.stderr.write("halfcycle-runner: the connection could not be checked.\n");
32554
+ process.exit(1);
32555
+ return;
32556
+ }
32557
+ process.stdout.write(line2, () => process.exit(0));
32558
+ return;
32559
+ }
30575
32560
  if (cmd === "mcp-headers") {
30576
32561
  const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
30577
32562
  process.stdout.write(line2, () => process.exit(0));
@@ -30650,8 +32635,18 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
30650
32635
  process.stdout.write(reply, () => process.exit(0));
30651
32636
  return;
30652
32637
  }
32638
+ if (sub === "user-prompt-findings") {
32639
+ let reply = "";
32640
+ try {
32641
+ reply = runUserPromptFindings({ input: await readStdin2() });
32642
+ } catch {
32643
+ reply = "";
32644
+ }
32645
+ process.stdout.write(reply, () => process.exit(0));
32646
+ return;
32647
+ }
30653
32648
  process.stderr.write(`halfcycle-runner: unknown hook "${sub ?? ""}"
30654
- Valid hooks: post-tool-use, pre-tool-use, stop, subagent-stop, user-prompt-submit
32649
+ Valid hooks: post-tool-use, pre-tool-use, stop, subagent-stop, user-prompt-submit, user-prompt-findings
30655
32650
  `);
30656
32651
  process.exit(1);
30657
32652
  return;
@@ -30663,7 +32658,9 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
30663
32658
  halfcycle-runner hook stop
30664
32659
  halfcycle-runner hook subagent-stop
30665
32660
  halfcycle-runner hook user-prompt-submit
32661
+ halfcycle-runner hook user-prompt-findings
30666
32662
  halfcycle-runner refresh-credential
32663
+ halfcycle-runner check-connection
30667
32664
  halfcycle-runner mcp-headers
30668
32665
  halfcycle-runner observe
30669
32666
  halfcycle-runner observe --change <sortingId>
@@ -30678,10 +32675,10 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
30678
32675
  process.exit(1);
30679
32676
  }
30680
32677
  function readStdin3() {
30681
- return new Promise((resolve6, reject) => {
32678
+ return new Promise((resolve8, reject) => {
30682
32679
  const chunks = [];
30683
32680
  process.stdin.on("data", (chunk) => chunks.push(chunk));
30684
- process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8")));
32681
+ process.stdin.on("end", () => resolve8(Buffer.concat(chunks).toString("utf-8")));
30685
32682
  process.stdin.on("error", reject);
30686
32683
  });
30687
32684
  }