halfcycle 0.4.14 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/bin.bundle.mjs +2525 -541
  3. package/bin/pre-tool-use-filter.sh +41 -5
  4. package/commands/agent-teams-orchestration.md +1 -1
  5. package/commands/consistency-check.md +1 -1
  6. package/commands/context-update.md +1 -1
  7. package/commands/halfcycle-design.md +1 -1
  8. package/commands/halfcycle-setup.md +1 -1
  9. package/commands/stack-assembly-worker.md +1 -1
  10. package/commands/task-execution.md +1 -1
  11. package/commands/task-graph.md +1 -1
  12. package/commands/write-spec.md +1 -1
  13. package/dist/bin.js +0 -0
  14. package/dist/build-record/render-v2.d.ts.map +1 -1
  15. package/dist/claude-plan.d.ts +24 -5
  16. package/dist/claude-plan.d.ts.map +1 -1
  17. package/dist/cli-contract.d.ts +1 -1
  18. package/dist/cli-contract.d.ts.map +1 -1
  19. package/dist/cli.js +2242 -565
  20. package/dist/cli.js.map +3 -3
  21. package/dist/close-phase.d.ts.map +1 -1
  22. package/dist/doctor.d.ts +34 -0
  23. package/dist/doctor.d.ts.map +1 -0
  24. package/dist/folder-refusals.d.ts +27 -0
  25. package/dist/folder-refusals.d.ts.map +1 -0
  26. package/dist/identity.d.ts +31 -1
  27. package/dist/identity.d.ts.map +1 -1
  28. package/dist/index.js +1276 -386
  29. package/dist/index.js.map +3 -3
  30. package/dist/install-plan.d.ts +112 -0
  31. package/dist/install-plan.d.ts.map +1 -0
  32. package/dist/install.d.ts +535 -1
  33. package/dist/install.d.ts.map +1 -1
  34. package/dist/plan.d.ts +16 -3
  35. package/dist/plan.d.ts.map +1 -1
  36. package/dist/pre-create-checks.d.ts +28 -0
  37. package/dist/pre-create-checks.d.ts.map +1 -1
  38. package/dist/preview.d.ts +40 -0
  39. package/dist/preview.d.ts.map +1 -0
  40. package/dist/restore.d.ts +58 -0
  41. package/dist/restore.d.ts.map +1 -0
  42. package/dist/review.d.ts.map +1 -1
  43. package/dist/scan.d.ts +11 -0
  44. package/dist/scan.d.ts.map +1 -1
  45. package/dist/setup/discovery.d.ts.map +1 -1
  46. package/dist/setup/rows.d.ts +10 -6
  47. package/dist/setup/rows.d.ts.map +1 -1
  48. package/package.json +12 -12
package/dist/cli.js CHANGED
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // dist/bin.js
4
- import { existsSync as existsSync9, realpathSync as realpathSync4, statSync, writeSync as writeSync2 } from "node:fs";
5
- import { homedir as homedir4 } from "node:os";
4
+ import { existsSync as existsSync12, statSync as statSync2, writeSync as writeSync3 } from "node:fs";
6
5
 
7
6
  // dist/install.js
8
7
  import { execFileSync as execFileSync2 } from "node:child_process";
9
8
  import { createHash as createHash2 } from "node:crypto";
10
- import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
9
+ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
11
10
  import { homedir as homedir2 } from "node:os";
12
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5, posix, relative, resolve } from "node:path";
11
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, posix as posix2, relative, resolve } from "node:path";
13
12
  import { fileURLToPath } from "node:url";
14
13
 
15
14
  // ../core/dist/guard-record.js
@@ -139,6 +138,7 @@ function parseAccountStoreText(raw) {
139
138
 
140
139
  // ../core/dist/hook-wiring.js
141
140
  var SETTINGS_REL = ".claude/settings.json";
141
+ var LOCAL_SETTINGS_REL = ".claude/settings.local.json";
142
142
  var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
143
143
  var PRE_FILTER_SHELL_FILENAME = "pre-tool-use-filter.sh";
144
144
  var PRE_FILTER_FUNCTION = "halfcycle_pt_skip";
@@ -146,9 +146,10 @@ var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
146
146
  var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
147
147
  var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
148
148
  var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
149
+ var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
149
150
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
150
151
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
151
- var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent)$";
152
+ var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
152
153
  function hookCommand(script, verb) {
153
154
  const invocation = `bash "$CLAUDE_PROJECT_DIR"/${script}`;
154
155
  return verb === void 0 ? invocation : `${invocation} ${verb}`;
@@ -182,6 +183,13 @@ var HALFCYCLE_HOOK_WIRING = [
182
183
  command: hookCommand(USER_PROMPT_SCOPE_REL),
183
184
  script: USER_PROMPT_SCOPE_REL
184
185
  },
186
+ {
187
+ event: "UserPromptSubmit",
188
+ matcher: null,
189
+ timeout: 10,
190
+ command: hookCommand(USER_PROMPT_FINDINGS_REL),
191
+ script: USER_PROMPT_FINDINGS_REL
192
+ },
185
193
  {
186
194
  event: "PreToolUse",
187
195
  matcher: PRE_TOOL_USE_MATCHER,
@@ -631,9 +639,9 @@ function waitsFor(tasks) {
631
639
  const n = tasks.length;
632
640
  const positionsOf = /* @__PURE__ */ new Map();
633
641
  tasks.forEach((task, i) => {
634
- const list = positionsOf.get(task.id);
635
- if (list)
636
- list.push(i);
642
+ const list2 = positionsOf.get(task.id);
643
+ if (list2)
644
+ list2.push(i);
637
645
  else
638
646
  positionsOf.set(task.id, [i]);
639
647
  });
@@ -759,17 +767,17 @@ function hasBit(bits, i) {
759
767
  function isComparedKind(task) {
760
768
  return task.kind === "build" || task.kind === "spec";
761
769
  }
762
- function firstDifference(plan, list) {
770
+ function firstDifference(plan, list2) {
763
771
  const planIds = new Set(plan.map((task) => task.id));
764
- const listIds = new Set(list.map((task) => task.id));
772
+ const listIds = new Set(list2.map((task) => task.id));
765
773
  for (const task of plan)
766
774
  if (!listIds.has(task.id))
767
775
  return `"${task.id}"`;
768
- for (const task of list)
776
+ for (const task of list2)
769
777
  if (!planIds.has(task.id))
770
778
  return `"${task.id}"`;
771
779
  const listDeps = /* @__PURE__ */ new Map();
772
- for (const task of list) {
780
+ for (const task of list2) {
773
781
  const deps = listDeps.get(task.id) ?? /* @__PURE__ */ new Set();
774
782
  for (const dep of task.dependsOn)
775
783
  deps.add(dep);
@@ -788,7 +796,7 @@ function firstDifference(plan, list) {
788
796
  if (!theirs.has(dep))
789
797
  return `"${task.id}" \u2192 "${dep}"`;
790
798
  }
791
- for (const task of list) {
799
+ for (const task of list2) {
792
800
  const ours = planDeps.get(task.id);
793
801
  for (const dep of task.dependsOn)
794
802
  if (!ours.has(dep))
@@ -1005,10 +1013,10 @@ function checkTaskPlan(plan, request) {
1005
1013
  }
1006
1014
  }
1007
1015
  lines.push(...placementLines(tasks, before));
1008
- const refused2 = lines.some((line) => line.refuses);
1009
- if (!refused2)
1016
+ const refused3 = lines.some((line) => line.refuses);
1017
+ if (!refused3)
1010
1018
  lines.push({ code: "P", text: taskPlanLineText.P(), refuses: false });
1011
- return { lines, refused: refused2 };
1019
+ return { lines, refused: refused3 };
1012
1020
  }
1013
1021
 
1014
1022
  // ../events/dist/result.js
@@ -1688,15 +1696,15 @@ function crossFieldProblems(raw) {
1688
1696
  ids.add(part.id);
1689
1697
  }
1690
1698
  if (typeof part.lane === "string" && typeof part.slot === "number") {
1691
- const place = `${part.lane}\0${part.slot}`;
1692
- const earlier = placed.get(place);
1699
+ const place2 = `${part.lane}\0${part.slot}`;
1700
+ const earlier = placed.get(place2);
1693
1701
  if (earlier !== void 0) {
1694
1702
  problems.push({
1695
1703
  path: `parts.${i}.slot`,
1696
1704
  message: `${quoted(parts[earlier].id)} already sits in lane ${shown(part.lane)} at slot ${part.slot}`
1697
1705
  });
1698
1706
  } else {
1699
- placed.set(place, i);
1707
+ placed.set(place2, i);
1700
1708
  }
1701
1709
  }
1702
1710
  if (typeof part.outside === "boolean" && Array.isArray(part.owns)) {
@@ -2181,8 +2189,9 @@ var SCOPE_PART_STATUSES = ["approved", "waiting"];
2181
2189
  var scopePartStatusSchema = z10.enum(SCOPE_PART_STATUSES);
2182
2190
  var SCOPE_ACTS = ["approve", "send-back"];
2183
2191
  var scopeActSchema = z10.enum(SCOPE_ACTS);
2184
- var SCOPE_GIVEN_IN = ["console", "chat"];
2192
+ var SCOPE_GIVEN_IN = ["console", "chat", "autonomy"];
2185
2193
  var scopeGivenInSchema = z10.enum(SCOPE_GIVEN_IN);
2194
+ var APPROVED_IN_ADVANCE_SENTENCE = "approved in advance by your autonomy setting";
2186
2195
  var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
2187
2196
  var scopeNoteWireSchema = z10.string().max(SCOPE_NOTE_MAX);
2188
2197
  var SCOPE_PART_VERDICTS = ["ok", "change"];
@@ -2213,7 +2222,8 @@ var phasePlanRecordedSchema = z10.object({
2213
2222
  typedApprovalOffered: z10.boolean(),
2214
2223
  reportedAt: utcIso8601,
2215
2224
  carriedFrom: z10.array(carriedFromEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX).optional(),
2216
- movedUp: planMovedUpSchema.optional()
2225
+ movedUp: planMovedUpSchema.optional(),
2226
+ givenIn: scopeGivenInSchema.optional()
2217
2227
  }).strict();
2218
2228
  var phasePlanRefusedSchema = z10.object({
2219
2229
  outcome: z10.literal("refused"),
@@ -4029,6 +4039,15 @@ var REVIEW_PART_CHANGES = Object.freeze([...PHASE_PLAN_CHANGE_KINDS, "not-in-pla
4029
4039
  var reviewPartChangeSchema = z20.enum(REVIEW_PART_CHANGES);
4030
4040
  var REVIEW_DECISIONS = Object.freeze(["accepted", "carried", "dropped"]);
4031
4041
  var reviewDecisionSchema = z20.enum(REVIEW_DECISIONS);
4042
+ var reviewGivenInSchema = scopeGivenInSchema.extract(["autonomy"]);
4043
+ var DECIDED_BY_AUTONOMY_SENTENCE = "decided by your autonomy setting";
4044
+ function checkReviewGivenIn(v, ctx) {
4045
+ if (v.givenIn === void 0)
4046
+ return;
4047
+ if (v.decision !== "accepted" && v.decision !== "carried") {
4048
+ ctx.addIssue({ code: "custom", path: ["givenIn"], message: "is present only on a part accepted or carried" });
4049
+ }
4050
+ }
4032
4051
  function actOf(decision) {
4033
4052
  if (decision === "accepted")
4034
4053
  return "accept";
@@ -4111,6 +4130,25 @@ var reviewRefusedSchema = z20.object({
4111
4130
  function bareReviewOutcome(outcome) {
4112
4131
  return z20.object({ outcome: z20.literal(outcome) }).strict();
4113
4132
  }
4133
+ var reviewClosedByAutonomySchema = z20.object({
4134
+ outcome: z20.literal("closed-by-autonomy"),
4135
+ accepted: z20.number().int().min(0).max(REVIEW_PARTS_MAX),
4136
+ carried: z20.number().int().min(0).max(REVIEW_PARTS_MAX)
4137
+ }).strict().refine((v) => v.accepted + v.carried <= REVIEW_PARTS_MAX, {
4138
+ message: "cannot exceed the parts a round holds",
4139
+ path: ["carried"]
4140
+ });
4141
+ var AUTONOMY_WAIT_REASONS = ["unchecked-claims", "built-without-approval"];
4142
+ var autonomyWaitReasonSchema = z20.enum(AUTONOMY_WAIT_REASONS);
4143
+ var reviewAutonomyWaitsSchema = z20.object({
4144
+ outcome: z20.literal("autonomy-waits"),
4145
+ reason: autonomyWaitReasonSchema,
4146
+ count: z20.number().int().min(1),
4147
+ parts: z20.array(z20.object({ part: systemPartIdSchema, name: systemPartSchema.shape.name }).strict()).min(1).max(REVIEW_PARTS_MAX)
4148
+ }).strict().refine((v) => v.reason !== "built-without-approval" || v.count === v.parts.length, {
4149
+ message: "counts the parts it names",
4150
+ path: ["count"]
4151
+ });
4114
4152
  var openReviewResultSchema = z20.discriminatedUnion("outcome", [
4115
4153
  reviewOpenedSchema,
4116
4154
  reviewTasksOpenSchema,
@@ -4119,7 +4157,9 @@ var openReviewResultSchema = z20.discriminatedUnion("outcome", [
4119
4157
  reviewOutstandingSchema,
4120
4158
  reviewCleanOverFindingsSchema,
4121
4159
  reviewRefusedSchema,
4122
- bareReviewOutcome("unavailable")
4160
+ bareReviewOutcome("unavailable"),
4161
+ reviewClosedByAutonomySchema,
4162
+ reviewAutonomyWaitsSchema
4123
4163
  ]);
4124
4164
  var reviewPhaseNotOpenSchema = z20.object({
4125
4165
  statusCode: z20.literal(409),
@@ -4233,6 +4273,7 @@ var reviewViewPartSchema = z20.object({
4233
4273
  decidable: z20.boolean(),
4234
4274
  waiting: z20.boolean(),
4235
4275
  decision: reviewDecisionSchema.optional(),
4276
+ givenIn: reviewGivenInSchema.optional(),
4236
4277
  reason: reviewNoteSchema.optional(),
4237
4278
  carriedTo: reviewCarriedToSchema.optional(),
4238
4279
  sentBack: z20.array(reviewSentBackSchema),
@@ -4260,6 +4301,7 @@ var reviewViewPartSchema = z20.object({
4260
4301
  if (v.change === "not-in-plan" && !v.built) {
4261
4302
  ctx.addIssue({ code: "custom", path: ["built"], message: "is always true for a part outside the plan" });
4262
4303
  }
4304
+ checkReviewGivenIn(v, ctx);
4263
4305
  });
4264
4306
  var reviewPhaseNumberSchema = z20.number().int().min(1).nullable();
4265
4307
  var phaseReviewNoneSchema = z20.object({
@@ -4354,6 +4396,7 @@ var recordReviewPartSchema = z21.object({
4354
4396
  built: z21.boolean(),
4355
4397
  builtWithoutApproval: z21.boolean(),
4356
4398
  decision: reviewDecisionSchema,
4399
+ givenIn: reviewGivenInSchema.optional(),
4357
4400
  reason: reviewNoteSchema.optional(),
4358
4401
  carriedTo: reviewCarriedToSchema.optional(),
4359
4402
  sentBack: z21.array(reviewSentBackSchema),
@@ -4367,6 +4410,7 @@ var recordReviewPartSchema = z21.object({
4367
4410
  if (v.carriedTo !== void 0 !== (v.decision === "carried")) {
4368
4411
  ctx.addIssue({ code: "custom", path: ["carriedTo"], message: "is present exactly on a carry" });
4369
4412
  }
4413
+ checkReviewGivenIn(v, ctx);
4370
4414
  });
4371
4415
  var SPINE_ORDER = boardGateNameSchema.options;
4372
4416
  var recordReviewGatesSchema = z21.array(recordReviewGateSchema).superRefine((gates, ctx) => {
@@ -4899,7 +4943,12 @@ var ownerWaitDeclareRequestSchema = z31.object({
4899
4943
  var OWNER_WAIT_DECLARE_REFUSALS = ["no-open-phase", "unknown-item", "say-if-undoable"];
4900
4944
  var ownerWaitDeclareResultSchema = z31.discriminatedUnion("outcome", [
4901
4945
  z31.object({ outcome: z31.literal("recorded"), id: z31.string().uuid(), say: z31.string().min(1) }).strict(),
4902
- z31.object({ outcome: z31.literal("go-ahead"), id: z31.string().uuid(), callId: z31.string().uuid(), say: z31.string().min(1) }).strict(),
4946
+ z31.object({
4947
+ outcome: z31.literal("go-ahead"),
4948
+ id: z31.string().uuid(),
4949
+ callId: z31.number().int().positive(),
4950
+ say: z31.string().min(1)
4951
+ }).strict(),
4903
4952
  z31.object({ outcome: z31.literal("refused"), reason: z31.enum(OWNER_WAIT_DECLARE_REFUSALS) }).strict()
4904
4953
  ]);
4905
4954
  var ownerWaitAddedItemSchema = z31.object({
@@ -5101,6 +5150,201 @@ function paceAllowedNowOf(k) {
5101
5150
  }
5102
5151
  var PACE_AGENTS_TOO_MANY = `Run at most ${String(PACE_AGENTS_MAX)} agents at once.`;
5103
5152
 
5153
+ // ../events/dist/autonomy.js
5154
+ import { z as z33 } from "zod";
5155
+ var AUTONOMY_CHANGED_IN = ["console", "chat"];
5156
+ var autonomyChangedInSchema = z33.enum(AUTONOMY_CHANGED_IN);
5157
+ var autonomySettingsSchema = z33.object({
5158
+ on: z33.boolean(),
5159
+ // whether builds run on their own
5160
+ mayPublish: z33.boolean(),
5161
+ // whether builds may publish or release without waiting
5162
+ maySpend: z33.boolean(),
5163
+ // whether builds may spend money or sign up without waiting
5164
+ changedAt: utcIso8601.nullable(),
5165
+ // when the setting last changed; null if never
5166
+ changedIn: autonomyChangedInSchema.nullable()
5167
+ // where it last changed; null if never
5168
+ }).strict().refine((v) => v.changedAt === null === (v.changedIn === null), {
5169
+ message: "says when and where it changed, or neither",
5170
+ path: ["changedIn"]
5171
+ });
5172
+ var autonomyChangeRequestSchema = z33.object({
5173
+ on: z33.boolean(),
5174
+ // the switch
5175
+ mayPublish: z33.boolean(),
5176
+ // the publish box
5177
+ maySpend: z33.boolean()
5178
+ // the spend box
5179
+ }).strict();
5180
+ var AUTONOMY_CHAT_ACTS = ["on", "off"];
5181
+ var autonomyChatActSchema = z33.enum(AUTONOMY_CHAT_ACTS);
5182
+ var autonomyChatRequestSchema = z33.object({
5183
+ act: autonomyChatActSchema
5184
+ // what the owner typed
5185
+ }).strict();
5186
+ var autonomyChatResultSchema = z33.discriminatedUnion("outcome", [
5187
+ z33.object({
5188
+ outcome: z33.literal("recorded"),
5189
+ on: z33.boolean()
5190
+ // whether builds now run on their own
5191
+ }).strict(),
5192
+ z33.object({ outcome: z33.literal("not-owner") }).strict(),
5193
+ z33.object({ outcome: z33.literal("no-owner") }).strict(),
5194
+ z33.object({ outcome: z33.literal("not-offered") }).strict(),
5195
+ z33.object({ outcome: z33.literal("unavailable") }).strict()
5196
+ ]);
5197
+ var autonomyStateSchema = z33.object({
5198
+ on: z33.boolean(),
5199
+ // whether builds run on their own
5200
+ mayPublish: z33.boolean(),
5201
+ // whether builds may publish or release without waiting
5202
+ maySpend: z33.boolean()
5203
+ // whether builds may spend money or sign up without waiting
5204
+ }).strict();
5205
+ var autonomyStateFileSchema = autonomyStateSchema.extend({
5206
+ readAt: utcIso8601
5207
+ // when this copy was read from Halfcycle
5208
+ }).strict();
5209
+ var AUTONOMY_STATE_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5210
+ var AUTONOMY_ACTIVITY_KINDS = ["went-ahead", "waiting", "approved", "closed", "waiting-on-owner"];
5211
+ var autonomyActivityKindSchema = z33.enum(AUTONOMY_ACTIVITY_KINDS);
5212
+ var autonomyActivityRowSchema = z33.object({
5213
+ kind: autonomyActivityKindSchema,
5214
+ // what happened
5215
+ phase: scopePhaseSchema,
5216
+ // the phase it happened in
5217
+ question: ownerWaitText.optional(),
5218
+ // the agent's question, for a decision
5219
+ chose: ownerWaitText.optional(),
5220
+ // what the agent chose or recommends, for a decision
5221
+ undoable: ownerWaitUndoableSchema.optional(),
5222
+ // whether the agent says it can be undone
5223
+ how: statementText(STATEMENT_MAX).optional(),
5224
+ // how to undo it, when it can be
5225
+ at: utcIso8601
5226
+ // when it happened
5227
+ }).strict().superRefine((row, ctx) => {
5228
+ const decision = row.kind === "went-ahead" || row.kind === "waiting";
5229
+ if (decision && row.question === void 0) {
5230
+ ctx.addIssue({ code: "custom", message: "a decision says its question", path: ["question"] });
5231
+ }
5232
+ if (!decision) {
5233
+ for (const key of ["question", "chose", "undoable", "how"]) {
5234
+ if (row[key] !== void 0) {
5235
+ ctx.addIssue({ code: "custom", message: "only a decision carries this", path: [key] });
5236
+ }
5237
+ }
5238
+ }
5239
+ if (row.kind === "went-ahead") {
5240
+ if (row.chose === void 0) {
5241
+ ctx.addIssue({ code: "custom", message: "a decision that went ahead says what was chosen", path: ["chose"] });
5242
+ }
5243
+ if (row.undoable === void 0) {
5244
+ ctx.addIssue({
5245
+ code: "custom",
5246
+ message: "a decision that went ahead says whether it can be undone",
5247
+ path: ["undoable"]
5248
+ });
5249
+ }
5250
+ }
5251
+ if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
5252
+ ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
5253
+ }
5254
+ if (row.kind === "went-ahead" && row.undoable === "yes" !== (row.how !== void 0)) {
5255
+ ctx.addIssue({ code: "custom", message: "says how to undo it exactly when it can be undone", path: ["how"] });
5256
+ }
5257
+ });
5258
+ var autonomyActivitySchema = z33.object({
5259
+ rows: z33.array(autonomyActivityRowSchema)
5260
+ // newest first
5261
+ }).strict();
5262
+ var savedIdSchema = z33.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
5263
+ var SAVED_REF_PREFIX = "refs/halfcycle/saved/";
5264
+ function savedRefOf(id) {
5265
+ return `${SAVED_REF_PREFIX}${id}`;
5266
+ }
5267
+ var SAVED_KEEP_MS = 7 * 24 * 60 * 60 * 1e3;
5268
+ var SAVED_DIR_NAME = "saved";
5269
+ var SAVED_FILE_NAME = "saved.json";
5270
+ var SAVED_COPIES_DIR_NAME = "files";
5271
+ var SAVED_REPOSITORY_FILE_NAME = "repository";
5272
+ var SAVED_HALF_WRITTEN_GRACE_MS = 6e4;
5273
+ function savedPruneAnswer(save, here) {
5274
+ const old = (t) => !Number.isFinite(t) || here.now - t > SAVED_KEEP_MS;
5275
+ if (save.refMadeAt !== null) {
5276
+ if (!old(save.refMadeAt))
5277
+ return "keep";
5278
+ return save.folderChangedAt === null ? "remove-ref" : "remove-ref-then-folder";
5279
+ }
5280
+ if (save.folderChangedAt === null || !Number.isFinite(save.folderChangedAt))
5281
+ return "keep";
5282
+ const ours = save.folderRepository === null || here.repository !== null && save.folderRepository === here.repository;
5283
+ const wait = ours ? SAVED_HALF_WRITTEN_GRACE_MS : SAVED_KEEP_MS;
5284
+ return here.now - save.folderChangedAt > wait ? "remove-folder" : "keep";
5285
+ }
5286
+ function isProjectRelativePath(p) {
5287
+ if (p.length === 0 || hasControlCharacter(p))
5288
+ return false;
5289
+ if (p.startsWith("/") || p.startsWith("\\") || /^[A-Za-z]:/.test(p))
5290
+ return false;
5291
+ return p.split(/[\\/]/).every((segment) => segment !== "" && windowsNameOf(segment) !== "" && !isGitFolderSegment(segment));
5292
+ }
5293
+ function windowsNameOf(segment) {
5294
+ return segment.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff]/g, "").split(":")[0].replace(/[. ]+$/, "").toLowerCase();
5295
+ }
5296
+ function isGitFolderSegment(segment) {
5297
+ const t = windowsNameOf(segment);
5298
+ return t === ".git" || /^git~\d+$/.test(t);
5299
+ }
5300
+ var SAVED_PATH_KEPT_IN = ["commit", "copy"];
5301
+ var savedPathSchema = z33.object({
5302
+ path: z33.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
5303
+ // the file, relative to the project
5304
+ keptIn: z33.enum(SAVED_PATH_KEPT_IN)
5305
+ // in the save's commit, or copied beside this file
5306
+ }).strict();
5307
+ var savedCommitSchema = z33.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
5308
+ var savedBranchSchema = z33.object({
5309
+ name: z33.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
5310
+ message: "must be a branch name"
5311
+ }),
5312
+ // the branch's name
5313
+ commit: savedCommitSchema
5314
+ // the commit it pointed at
5315
+ }).strict();
5316
+ var savedSchema = z33.object({
5317
+ id: savedIdSchema,
5318
+ // what restore is typed with
5319
+ savedAt: utcIso8601,
5320
+ // when the copy was made
5321
+ ref: z33.string().min(1),
5322
+ // where the saved commit is kept
5323
+ what: z33.string().min(1).max(2e3),
5324
+ // what the action would have changed, in words
5325
+ paths: z33.array(savedPathSchema),
5326
+ // every saved file
5327
+ branches: z33.array(savedBranchSchema)
5328
+ // every saved line of work
5329
+ }).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
5330
+ function autonomyA13(accepted, carried) {
5331
+ return `Your autonomy setting accepted the ${String(accepted)} built parts and carried ${String(carried)} unbuilt parts forward. The phase is closed.`;
5332
+ }
5333
+ function autonomyA14(count, parts) {
5334
+ return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests and decide those parts. See the console's review.`;
5335
+ }
5336
+ function autonomyA15(count) {
5337
+ return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
5338
+ }
5339
+ function namesOf(names) {
5340
+ if (names.length <= 1)
5341
+ return names[0] ?? "";
5342
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1] ?? ""}`;
5343
+ }
5344
+ function autonomyC8(agent, question2, chose) {
5345
+ return `${agent} decided "${question2}" without asking you, under your autonomy setting: chose "${chose}". It is on the list of things that wait for you, so it was sent to you as it was made.`;
5346
+ }
5347
+
5104
5348
  // ../events/dist/work-words.js
5105
5349
  var CALLS_HEADING = "Calls made without asking you";
5106
5350
  var CALLS_EXPLAINED = "These are the decisions the agents recorded taking on their own, each against a stated default. Halfcycle lists what they recorded; it cannot see a call an agent did not record.";
@@ -5473,45 +5717,64 @@ function mintIdentity(targetRepoRoot) {
5473
5717
  note: PROJECT_IDENTITY_NOTE
5474
5718
  };
5475
5719
  }
5476
- function repairRemoteCredential(targetRepoRoot) {
5477
- const path = join3(targetRepoRoot, ".halfcycle", "project.json");
5720
+ function plannedRemoteRepair(targetRepoRoot) {
5721
+ const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
5478
5722
  let onDisk;
5479
5723
  try {
5480
5724
  const parsed = JSON.parse(readFileSync3(path, "utf-8"));
5481
5725
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
5482
- return;
5726
+ return null;
5483
5727
  onDisk = parsed;
5484
5728
  } catch {
5485
- return;
5729
+ return null;
5486
5730
  }
5487
5731
  const storedRemote = onDisk.remote ?? null;
5488
5732
  const remote = redactRemoteCredential(storedRemote);
5489
5733
  if (remote === storedRemote)
5490
- return;
5491
- const identity = {
5734
+ return null;
5735
+ return serializeProjectIdentity({
5492
5736
  projectId: onDisk.projectId ?? "",
5493
5737
  rootCommit: onDisk.rootCommit ?? null,
5494
5738
  remote,
5495
5739
  note: PROJECT_IDENTITY_NOTE
5496
- };
5740
+ });
5741
+ }
5742
+ function replaceProjectIdentityFile(targetRepoRoot, text) {
5743
+ const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
5497
5744
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
5498
5745
  try {
5499
- writeFileSync3(temporary, serializeProjectIdentity(identity), { encoding: "utf-8", flag: "wx" });
5746
+ writeFileSync3(temporary, text, { encoding: "utf-8", flag: "wx" });
5500
5747
  renameSync(temporary, path);
5501
5748
  } catch {
5502
5749
  } finally {
5503
5750
  rmSync(temporary, { force: true });
5504
5751
  }
5505
5752
  }
5753
+ function planProjectIdentity(targetRepoRoot) {
5754
+ const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
5755
+ if (!existsSync(path)) {
5756
+ return { action: "add", after: serializeProjectIdentity(mintIdentity(targetRepoRoot)) };
5757
+ }
5758
+ const repaired = plannedRemoteRepair(targetRepoRoot);
5759
+ if (repaired !== null)
5760
+ return { action: "change", after: repaired };
5761
+ let current;
5762
+ try {
5763
+ current = readFileSync3(path, "utf-8");
5764
+ } catch {
5765
+ current = null;
5766
+ }
5767
+ return { action: "leave", after: current };
5768
+ }
5506
5769
  function serializeProjectIdentity(identity) {
5507
5770
  return JSON.stringify(identity, null, 2) + "\n";
5508
5771
  }
5509
- function createProjectIdentityFile(targetRepoRoot) {
5772
+ function createProjectIdentityFile(targetRepoRoot, text) {
5510
5773
  const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
5511
5774
  mkdirSync3(dirname2(path), { recursive: true });
5512
5775
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
5513
5776
  try {
5514
- writeFileSync3(temporary, serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
5777
+ writeFileSync3(temporary, text ?? serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
5515
5778
  encoding: "utf-8",
5516
5779
  flag: "wx"
5517
5780
  });
@@ -5526,6 +5789,10 @@ function createProjectIdentityFile(targetRepoRoot) {
5526
5789
  }
5527
5790
  }
5528
5791
 
5792
+ // dist/install-plan.js
5793
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
5794
+ import { join as join5, posix } from "node:path";
5795
+
5529
5796
  // dist/install-manifest.js
5530
5797
  import { Buffer as Buffer2 } from "node:buffer";
5531
5798
  import { createHash } from "node:crypto";
@@ -5589,10 +5856,10 @@ function stringArray(value, field) {
5589
5856
  return value;
5590
5857
  }
5591
5858
  function hashArray(value, field) {
5592
- const list = stringArray(value, field);
5593
- if (!list.every((v) => HEX64.test(v)))
5859
+ const list2 = stringArray(value, field);
5860
+ if (!list2.every((v) => HEX64.test(v)))
5594
5861
  throw new UnreadableManifestError(`"${field}" holds a value that is not a hash`);
5595
- return list;
5862
+ return list2;
5596
5863
  }
5597
5864
  function bool(value, field) {
5598
5865
  if (typeof value !== "boolean")
@@ -5824,6 +6091,448 @@ function union(a, b) {
5824
6091
  return [.../* @__PURE__ */ new Set([...a, ...b])];
5825
6092
  }
5826
6093
 
6094
+ // dist/scan.js
6095
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
6096
+ import { join as join4 } from "node:path";
6097
+ var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
6098
+ var HALFCYCLE_STATE_NOTE = [
6099
+ "This file is a local cache written ONCE when Halfcycle was installed. It is not a",
6100
+ "record of what this project has done, and it is never updated as work happens.",
6101
+ 'An empty or missing "layers" means the install-time scan found no pre-existing',
6102
+ "artefacts on disk \u2014 it does NOT mean nothing has been done since, and it is not",
6103
+ "evidence that any step is unfinished or any approval is outstanding.",
6104
+ "The authoritative position lives in the Halfcycle service: call the",
6105
+ "halfcycle_resolve tool to ask where this project stands. Do not answer that",
6106
+ "question from this file."
6107
+ ].join(" ");
6108
+ var LAYER_ARTEFACT_PATHS = {
6109
+ l0: ["ENGAGEMENT.md"],
6110
+ l1: ["docs/product-spec.md"],
6111
+ l2: ["docs/architecture-overview.md"],
6112
+ l3: ["docs/invariants.md"],
6113
+ l4: ["docs/features"],
6114
+ l5: ["docs/phases"]
6115
+ };
6116
+ function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
6117
+ const layers = {};
6118
+ for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
6119
+ const found = paths.filter((p) => existsSync2(join4(targetRepo, p)));
6120
+ if (found.length > 0) {
6121
+ layers[layer] = {
6122
+ value: "asserted",
6123
+ source: "bootstrap-scan",
6124
+ foundPaths: found,
6125
+ scannedAt
6126
+ };
6127
+ }
6128
+ }
6129
+ return { format: HALFCYCLE_STATE_FORMAT, note: HALFCYCLE_STATE_NOTE, layers };
6130
+ }
6131
+ var BOOTSTRAP_STATE_REL = ".halfcycle/state.json";
6132
+ function planBootstrapScan(targetRepo) {
6133
+ const statePath = join4(targetRepo, BOOTSTRAP_STATE_REL);
6134
+ if (existsSync2(statePath)) {
6135
+ const existing = JSON.parse(readFileSync4(statePath, "utf-8"));
6136
+ return { state: existing, text: null };
6137
+ }
6138
+ const state = scanLayers(targetRepo);
6139
+ return { state, text: JSON.stringify(state, null, 2) + "\n" };
6140
+ }
6141
+ function writeBootstrapState(targetRepo, text) {
6142
+ mkdirSync4(join4(targetRepo, ".halfcycle"), { recursive: true });
6143
+ writeFileSync4(join4(targetRepo, BOOTSTRAP_STATE_REL), text, "utf-8");
6144
+ }
6145
+
6146
+ // dist/install-plan.js
6147
+ var PENDING = /* @__PURE__ */ Symbol("pending");
6148
+ var YOURS = "a file of yours with the same name; Halfcycle will not touch it";
6149
+ var SAME = "already as Halfcycle would write it";
6150
+ var PREVIEW_WORDS = Object.freeze({
6151
+ command: {
6152
+ add: "a command your coding agent can run to take the next Halfcycle step",
6153
+ change: "a newer version of this Halfcycle command",
6154
+ leave: SAME,
6155
+ yours: YOURS
6156
+ },
6157
+ "check-script": {
6158
+ add: "a check Halfcycle runs before and after your agent's actions",
6159
+ change: "a newer version of this Halfcycle check",
6160
+ leave: SAME,
6161
+ yours: YOURS
6162
+ },
6163
+ settings: {
6164
+ add: "your coding agent's settings, with the checks Halfcycle runs",
6165
+ change: "adds the lines Halfcycle needs; your own lines stay as they are",
6166
+ leave: "already has the lines Halfcycle needs",
6167
+ yours: YOURS
6168
+ },
6169
+ "server-entry": {
6170
+ add: "tells your coding agent how to reach Halfcycle's service",
6171
+ change: "adds Halfcycle's entry; your own entries stay as they are",
6172
+ leave: "already has Halfcycle's entry",
6173
+ yours: YOURS
6174
+ },
6175
+ "ignore-list": {
6176
+ add: "keeps Halfcycle's files for this computer only out of git",
6177
+ change: "adds the lines Halfcycle needs; your own lines stay as they are",
6178
+ leave: "already has the lines Halfcycle needs",
6179
+ yours: YOURS
6180
+ },
6181
+ "line-endings": {
6182
+ add: "keeps Halfcycle's checks working in a Windows copy of this project",
6183
+ change: "adds the lines Halfcycle needs; your own lines stay as they are",
6184
+ leave: "already has the lines Halfcycle needs",
6185
+ yours: YOURS
6186
+ },
6187
+ "halfcycle-folder": {
6188
+ add: "a file in Halfcycle's own folder",
6189
+ change: "a newer version of this file in Halfcycle's own folder",
6190
+ leave: SAME,
6191
+ yours: YOURS
6192
+ },
6193
+ runner: {
6194
+ add: "the program Halfcycle's checks run",
6195
+ change: "a newer version of the program Halfcycle's checks run",
6196
+ leave: SAME,
6197
+ yours: YOURS
6198
+ },
6199
+ "captured-index": {
6200
+ add: "the list of real examples your project's tests are checked against",
6201
+ change: "a newer version of this list",
6202
+ leave: SAME,
6203
+ yours: YOURS
6204
+ },
6205
+ "old-sign-in": {
6206
+ add: "an older Halfcycle sign-in",
6207
+ change: "takes out an older Halfcycle sign-in; your own lines stay as they are",
6208
+ leave: "an older Halfcycle sign-in that cannot be moved yet; left as it is",
6209
+ yours: YOURS
6210
+ }
6211
+ });
6212
+ var kindTable;
6213
+ function kinds() {
6214
+ kindTable ??= /* @__PURE__ */ new Map([
6215
+ [".claude/hooks/guard-runner.sh", "check-script"],
6216
+ [".claude/hooks/user-prompt-reminder.sh", "check-script"],
6217
+ [".claude/hooks/user-prompt-scope.sh", "check-script"],
6218
+ [".claude/hooks/user-prompt-findings.sh", "check-script"],
6219
+ [".claude/hooks/session-end-marker.sh", "check-script"],
6220
+ [".claude/hooks/session-start-marker.sh", "check-script"],
6221
+ [SETTINGS_REL, "settings"],
6222
+ [MCP_REGISTRATION_REL, "server-entry"],
6223
+ [GITIGNORE_REL, "ignore-list"],
6224
+ [GITATTRIBUTES_REL, "line-endings"],
6225
+ [VENDORED_BIN_REL, "runner"],
6226
+ [CREW_ROSTER_REL, "halfcycle-folder"],
6227
+ [BUNDLE_PIN_REL, "halfcycle-folder"],
6228
+ [PROJECT_IDENTITY_REL, "halfcycle-folder"],
6229
+ [BOOTSTRAP_STATE_REL, "halfcycle-folder"],
6230
+ [INSTALL_MANIFEST_REL, "halfcycle-folder"],
6231
+ [RETIRED_MCP_HEADERS_HELPER_REL, "halfcycle-folder"],
6232
+ [LEGACY_CI_STANZA_REL, "halfcycle-folder"],
6233
+ [CAPTURED_INDEX_REL, "captured-index"],
6234
+ [LEGACY_ENV_LOCAL_REL, "old-sign-in"]
6235
+ ]);
6236
+ return kindTable;
6237
+ }
6238
+ function previewKindOf(rel) {
6239
+ if (/^\.claude\/commands\/[^/]+\.md$/.test(rel))
6240
+ return "command";
6241
+ return kinds().get(rel);
6242
+ }
6243
+ function kindOf(rel) {
6244
+ const kind = previewKindOf(rel);
6245
+ if (kind === void 0)
6246
+ throw new Error(`[halfcycle] the installer plans ${rel}, which has no preview words.`);
6247
+ return kind;
6248
+ }
6249
+ var STEPS = /* @__PURE__ */ new WeakMap();
6250
+ function planInstall(options) {
6251
+ const { targetRepo, engagementId, engagementType, credential, home, env, createdByAccountId } = options;
6252
+ const preview = engagementId === void 0;
6253
+ if (!preview) {
6254
+ assertEngagementId(engagementId);
6255
+ if (engagementType === void 0) {
6256
+ throw new Error("[halfcycle] a plan for a project needs its kind as well as its id.");
6257
+ }
6258
+ }
6259
+ if (!existsSync3(targetRepo)) {
6260
+ throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
6261
+ }
6262
+ const writeSet = closedWriteSet();
6263
+ const previous = readPreviousManifest(targetRepo);
6264
+ const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
6265
+ const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
6266
+ const vendored = planVendoredBinary(targetRepo);
6267
+ let settings;
6268
+ try {
6269
+ settings = planSettings(targetRepo, previous, home, env);
6270
+ } catch (err) {
6271
+ if (err instanceof SettingsParseRefused)
6272
+ return Object.freeze({ entries: [], outside: [], refusal: err.message });
6273
+ throw err;
6274
+ }
6275
+ const hooks = planHookScripts(targetRepo);
6276
+ const ciStanza = planLegacyCiStanzaRemoval(targetRepo);
6277
+ const mcp = planMcpRegistration(targetRepo, preview ? "pending" : credential);
6278
+ const mcpHelper = mcp.kind === "none" ? null : planMcpHeadersHelperRemoval(targetRepo);
6279
+ const gitignore = planGitignore(targetRepo);
6280
+ const gitattributes = planGitattributes(targetRepo);
6281
+ const identity = planProjectIdentity(targetRepo);
6282
+ if (identity.after === null)
6283
+ readFileSync5(join5(targetRepo, PROJECT_IDENTITY_REL));
6284
+ const credentialPlan = !preview && credential !== void 0 ? planEngagementCredential(credential, engagementId, home) : null;
6285
+ const credentialWrite = credentialPlan === null ? null : { outcome: credentialPlan.outcome, existed: credentialPlan.existed, path: engagementEnvPath(engagementId, home) };
6286
+ const storedAfter = credentialPlan?.storedAfter ?? (preview ? Object.fromEntries(ENGAGEMENT_ENV_KEYS.map((key) => [key, "supplied at sign-in"])) : readEngagementEnv(engagementId, home) ?? {});
6287
+ const legacy = migrateLegacyEnvLocal(targetRepo, storedAfter);
6288
+ const pin = preview ? planBundlePinPreview(targetRepo, version, home) : planBundlePin(targetRepo, version, engagementId, engagementType, createdByAccountId, credential?.methodVersion);
6289
+ const crew = planCrewRoster(targetRepo);
6290
+ const scan = { text: planBootstrapScan(targetRepo).text };
6291
+ const steps = {
6292
+ targetRepo,
6293
+ version,
6294
+ engagementId,
6295
+ engagementType,
6296
+ credential: preview ? void 0 : credential,
6297
+ home,
6298
+ writeSet,
6299
+ previous,
6300
+ dirsBefore,
6301
+ commands,
6302
+ vendored,
6303
+ settings,
6304
+ hooks,
6305
+ ciStanza,
6306
+ mcp,
6307
+ mcpHelper,
6308
+ gitignore,
6309
+ gitattributes,
6310
+ identity,
6311
+ credentialWrite,
6312
+ legacy,
6313
+ pin,
6314
+ crew,
6315
+ scan
6316
+ };
6317
+ const record2 = preview ? null : planInstallRecord(targetRepo, accountInstall(steps, predicted(steps)).manifestText);
6318
+ const plan = Object.freeze({
6319
+ entries: Object.freeze(entriesOf(steps, record2)),
6320
+ outside: Object.freeze(outsideOf(steps))
6321
+ });
6322
+ STEPS.set(plan, steps);
6323
+ return plan;
6324
+ }
6325
+ function predicted(steps) {
6326
+ const after = /* @__PURE__ */ new Map();
6327
+ const dirs = /* @__PURE__ */ new Set();
6328
+ const lands = (rel, text, writes) => {
6329
+ after.set(rel, text);
6330
+ if (!writes)
6331
+ return;
6332
+ for (let dir = posix.dirname(rel); dir !== "."; dir = posix.dirname(dir))
6333
+ dirs.add(dir);
6334
+ };
6335
+ const file = (planned) => {
6336
+ if (planned.outcome !== "collided")
6337
+ lands(planned.rel, planned.text, planned.outcome === "written");
6338
+ };
6339
+ steps.commands.forEach(file);
6340
+ file(steps.vendored);
6341
+ file(steps.settings.file);
6342
+ steps.hooks.forEach(file);
6343
+ if (steps.mcp.kind === "write")
6344
+ lands(MCP_REGISTRATION_REL, steps.mcp.text, steps.mcp.outcome !== "skipped");
6345
+ if (steps.identity.after !== null)
6346
+ lands(PROJECT_IDENTITY_REL, steps.identity.after, steps.identity.action === "add");
6347
+ if (steps.pin !== null)
6348
+ file(steps.pin);
6349
+ file(steps.crew);
6350
+ if (steps.scan.text !== null)
6351
+ lands(BOOTSTRAP_STATE_REL, steps.scan.text, true);
6352
+ const mergeAfter = (planned) => planned.outcome === "written" ? planned.after : planned.before;
6353
+ return {
6354
+ ciStanza: steps.ciStanza,
6355
+ mcpHelper: steps.mcpHelper,
6356
+ gitignore: steps.gitignore.outcome,
6357
+ gitignoreAfter: mergeAfter(steps.gitignore),
6358
+ gitattributes: steps.gitattributes.outcome,
6359
+ gitattributesAfter: mergeAfter(steps.gitattributes),
6360
+ credential: steps.credentialWrite?.outcome ?? null,
6361
+ legacy: steps.legacy,
6362
+ bytesAfter: (rel) => {
6363
+ const text = after.get(rel);
6364
+ if (text === void 0)
6365
+ throw new Error(`[halfcycle] the plan has no bytes for ${rel}.`);
6366
+ return Buffer.from(text, "utf-8");
6367
+ },
6368
+ dirExistsAfter: (dir) => steps.dirsBefore.has(dir) || dirs.has(dir),
6369
+ stateExistsAfter: true
6370
+ };
6371
+ }
6372
+ function entriesOf(steps, record2) {
6373
+ const { targetRepo } = steps;
6374
+ const entries = [];
6375
+ const fromFile = (planned) => {
6376
+ const kind = kindOf(planned.rel);
6377
+ if (planned.outcome === "written") {
6378
+ entries.push({ path: planned.rel, action: planned.replacedExisting ? "change" : "add", kind, bytes: planned.text });
6379
+ } else if (planned.outcome === "collided") {
6380
+ entries.push({ path: planned.rel, action: "leave", kind, bytes: null, yours: true });
6381
+ } else {
6382
+ entries.push({ path: planned.rel, action: "leave", kind, bytes: null });
6383
+ }
6384
+ };
6385
+ const retired = (rel, decided) => {
6386
+ if (decided === null || decided === "absent")
6387
+ return;
6388
+ if (decided === "removed")
6389
+ entries.push({ path: rel, action: "remove", kind: kindOf(rel), bytes: null });
6390
+ else if (decided === "kept-foreign")
6391
+ entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null, yours: true });
6392
+ else
6393
+ entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null });
6394
+ };
6395
+ const merge = (rel, planned) => {
6396
+ if (planned.outcome === "written" && planned.after !== null) {
6397
+ entries.push({ path: rel, action: planned.before === null ? "add" : "change", kind: kindOf(rel), bytes: planned.after });
6398
+ } else if (planned.before !== null || planned.outcome === "failed") {
6399
+ entries.push({ path: rel, action: "leave", kind: kindOf(rel), bytes: null });
6400
+ }
6401
+ };
6402
+ const pending = (rel) => {
6403
+ const there = existsSync3(join5(targetRepo, rel));
6404
+ entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
6405
+ };
6406
+ steps.commands.forEach(fromFile);
6407
+ fromFile(steps.vendored);
6408
+ const settings = steps.settings;
6409
+ entries.push(settings.file.outcome === "written" ? { path: SETTINGS_REL, action: settings.preexisted ? "change" : "add", kind: "settings", bytes: settings.file.text } : { path: SETTINGS_REL, action: "leave", kind: "settings", bytes: null });
6410
+ steps.hooks.forEach(fromFile);
6411
+ retired(LEGACY_CI_STANZA_REL, steps.ciStanza);
6412
+ const mcp = steps.mcp;
6413
+ if (mcp.kind === "write") {
6414
+ const action = mcp.outcome === "written" ? "add" : mcp.outcome === "merged" ? "change" : "leave";
6415
+ entries.push({ path: MCP_REGISTRATION_REL, action, kind: "server-entry", bytes: action === "leave" ? null : mcp.text });
6416
+ } else if (mcp.kind === "pending") {
6417
+ pending(MCP_REGISTRATION_REL);
6418
+ } else {
6419
+ entries.push({ path: MCP_REGISTRATION_REL, action: "leave", kind: "server-entry", bytes: null });
6420
+ }
6421
+ retired(RETIRED_MCP_HEADERS_HELPER_REL, steps.mcpHelper);
6422
+ merge(GITIGNORE_REL, steps.gitignore);
6423
+ merge(GITATTRIBUTES_REL, steps.gitattributes);
6424
+ const identity = steps.identity;
6425
+ entries.push({
6426
+ path: PROJECT_IDENTITY_REL,
6427
+ action: identity.action,
6428
+ kind: "halfcycle-folder",
6429
+ bytes: identity.action === "leave" ? null : identity.after
6430
+ });
6431
+ const legacy = steps.legacy;
6432
+ if (legacy.outcome === "removed") {
6433
+ entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "remove", kind: "old-sign-in", bytes: null });
6434
+ } else if (legacy.outcome === "stripped") {
6435
+ entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "change", kind: "old-sign-in", bytes: legacy.keptText ?? null });
6436
+ } else if (legacy.outcome !== "absent") {
6437
+ entries.push({ path: LEGACY_ENV_LOCAL_REL, action: "leave", kind: "old-sign-in", bytes: null });
6438
+ }
6439
+ if (steps.pin === null)
6440
+ pending(BUNDLE_PIN_REL);
6441
+ else
6442
+ fromFile(steps.pin);
6443
+ fromFile(steps.crew);
6444
+ entries.push(steps.scan.text !== null ? { path: BOOTSTRAP_STATE_REL, action: "add", kind: "halfcycle-folder", bytes: steps.scan.text } : { path: BOOTSTRAP_STATE_REL, action: "leave", kind: "halfcycle-folder", bytes: null });
6445
+ if (record2 === null)
6446
+ pending(INSTALL_MANIFEST_REL);
6447
+ else
6448
+ fromFile(record2);
6449
+ return entries;
6450
+ }
6451
+ function outsideOf(steps) {
6452
+ if (steps.engagementId === void 0) {
6453
+ const outside = [{ kind: "sign-in", path: null, action: "pending" }];
6454
+ if (!projectPinned(steps.targetRepo))
6455
+ outside.push({ kind: "new-project" });
6456
+ return outside;
6457
+ }
6458
+ const write = steps.credentialWrite;
6459
+ if (write === null)
6460
+ return [];
6461
+ const action = write.outcome === "skipped" ? "leave" : write.existed ? "change" : "add";
6462
+ return [{ kind: "sign-in", path: write.path, action }];
6463
+ }
6464
+ function projectPinned(targetRepo) {
6465
+ try {
6466
+ const id = readBundlePin(targetRepo)?.engagementId;
6467
+ return typeof id === "string" && id.trim() !== "";
6468
+ } catch {
6469
+ return false;
6470
+ }
6471
+ }
6472
+ function applyInstallPlan(plan) {
6473
+ if (plan.refusal !== void 0)
6474
+ throw new SettingsParseRefused(plan.refusal);
6475
+ const steps = STEPS.get(plan);
6476
+ if (steps === void 0)
6477
+ throw new Error("[halfcycle] only a plan made by planInstall can be applied.");
6478
+ const { targetRepo, engagementId, pin } = steps;
6479
+ if (engagementId === void 0 || pin === null || steps.mcp.kind === "pending") {
6480
+ throw new Error("[halfcycle] a preview cannot be applied: it is still waiting for the service.");
6481
+ }
6482
+ for (const command of steps.commands)
6483
+ putPlannedFile(targetRepo, command);
6484
+ putPlannedFile(targetRepo, steps.vendored);
6485
+ putPlannedFile(targetRepo, steps.settings.file);
6486
+ for (const hook of steps.hooks)
6487
+ putPlannedFile(targetRepo, hook);
6488
+ const ciStanza = steps.ciStanza === "removed" ? removePlannedRetiredFile(targetRepo, LEGACY_CI_STANZA_REL) : steps.ciStanza;
6489
+ putPlannedMcp(targetRepo, steps.mcp);
6490
+ const mcpHelper = steps.mcpHelper === "removed" ? removePlannedRetiredFile(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL) : steps.mcpHelper;
6491
+ const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
6492
+ const gitignorePath = join5(targetRepo, GITIGNORE_REL);
6493
+ const gitignoreAfter = existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
6494
+ const gitattributes = putPlannedGitattributes(targetRepo, steps.gitattributes);
6495
+ const gitattributesAfter = readTextOrNull(join5(targetRepo, GITATTRIBUTES_REL));
6496
+ writeProjectIdentity(targetRepo, steps.identity);
6497
+ const credential = steps.credential;
6498
+ const credentialOutcome = steps.credentialWrite !== null && credential !== void 0 ? writeEngagementCredential(credential, engagementId, steps.home) : null;
6499
+ const stored = readEngagementEnv(engagementId, steps.home) ?? {};
6500
+ const legacy = applyLegacyMigration(targetRepo, migrateLegacyEnvLocal(targetRepo, stored));
6501
+ writeBundlePin(targetRepo, pin, []);
6502
+ putPlannedFile(targetRepo, steps.crew);
6503
+ if (steps.scan.text !== null)
6504
+ writeBootstrapState(targetRepo, steps.scan.text);
6505
+ const { report, installManifest, manifestText } = accountInstall(steps, {
6506
+ ciStanza,
6507
+ mcpHelper,
6508
+ gitignore,
6509
+ gitignoreAfter,
6510
+ gitattributes,
6511
+ gitattributesAfter,
6512
+ credential: credentialOutcome,
6513
+ legacy,
6514
+ bytesAfter: (rel) => readFileSync5(join5(targetRepo, rel)),
6515
+ dirExistsAfter: (dir) => existsSync3(join5(targetRepo, dir)),
6516
+ stateExistsAfter: existsSync3(join5(targetRepo, BOOTSTRAP_STATE_REL))
6517
+ });
6518
+ const record2 = planInstallRecord(targetRepo, manifestText);
6519
+ putPlannedFile(targetRepo, record2);
6520
+ recordInstallRecord(report, record2);
6521
+ const notShared = findNotShared(targetRepo, installManifest, report.collidedPaths);
6522
+ return {
6523
+ version: steps.version,
6524
+ writtenPaths: report.writtenPaths,
6525
+ skippedPaths: report.skippedPaths,
6526
+ mergedPaths: report.mergedPaths,
6527
+ collidedPaths: report.collidedPaths,
6528
+ editedCollidedPaths: report.editedCollidedPaths,
6529
+ replacedPaths: report.replacedPaths,
6530
+ bootstrapScanRan: steps.scan.text !== null,
6531
+ legacyCredentialKept: legacy.outcome === "kept" ? { keys: legacy.unbacked, missing: legacy.missingForAdoption } : null,
6532
+ notShared
6533
+ };
6534
+ }
6535
+
5827
6536
  // dist/mcp-endpoint.js
5828
6537
  var MCP_ENDPOINT_PATH = "/mcp";
5829
6538
  function mcpEndpointUrl(origin) {
@@ -5903,56 +6612,6 @@ function mergePermissions(existing, generated, retiredOurs) {
5903
6612
  return result;
5904
6613
  }
5905
6614
 
5906
- // dist/scan.js
5907
- import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
5908
- import { join as join4 } from "node:path";
5909
- var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
5910
- var HALFCYCLE_STATE_NOTE = [
5911
- "This file is a local cache written ONCE when Halfcycle was installed. It is not a",
5912
- "record of what this project has done, and it is never updated as work happens.",
5913
- 'An empty or missing "layers" means the install-time scan found no pre-existing',
5914
- "artefacts on disk \u2014 it does NOT mean nothing has been done since, and it is not",
5915
- "evidence that any step is unfinished or any approval is outstanding.",
5916
- "The authoritative position lives in the Halfcycle service: call the",
5917
- "halfcycle_resolve tool to ask where this project stands. Do not answer that",
5918
- "question from this file."
5919
- ].join(" ");
5920
- var LAYER_ARTEFACT_PATHS = {
5921
- l0: ["ENGAGEMENT.md"],
5922
- l1: ["docs/product-spec.md"],
5923
- l2: ["docs/architecture-overview.md"],
5924
- l3: ["docs/invariants.md"],
5925
- l4: ["docs/features"],
5926
- l5: ["docs/phases"]
5927
- };
5928
- function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
5929
- const layers = {};
5930
- for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
5931
- const found = paths.filter((p) => existsSync2(join4(targetRepo, p)));
5932
- if (found.length > 0) {
5933
- layers[layer] = {
5934
- value: "asserted",
5935
- source: "bootstrap-scan",
5936
- foundPaths: found,
5937
- scannedAt
5938
- };
5939
- }
5940
- }
5941
- return { format: HALFCYCLE_STATE_FORMAT, note: HALFCYCLE_STATE_NOTE, layers };
5942
- }
5943
- var BOOTSTRAP_STATE_REL = ".halfcycle/state.json";
5944
- function runBootstrapScan(targetRepo) {
5945
- const statePath = join4(targetRepo, BOOTSTRAP_STATE_REL);
5946
- if (existsSync2(statePath)) {
5947
- const existing = JSON.parse(readFileSync4(statePath, "utf-8"));
5948
- return { state: existing, ran: false };
5949
- }
5950
- const state = scanLayers(targetRepo);
5951
- mkdirSync4(join4(targetRepo, ".halfcycle"), { recursive: true });
5952
- writeFileSync4(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
5953
- return { state, ran: true };
5954
- }
5955
-
5956
6615
  // dist/install.js
5957
6616
  function renderNotShared(finding) {
5958
6617
  const rule = `rule "${finding.pattern}" in ${finding.source}, line ${finding.line}`;
@@ -5967,36 +6626,36 @@ var __dirname = dirname3(__filename);
5967
6626
  function findBundleRoot(startDir) {
5968
6627
  let candidate = startDir;
5969
6628
  for (let i = 0; i < 4; i++) {
5970
- const manifest = join5(candidate, ".claude-plugin", "plugin.json");
6629
+ const manifest = join6(candidate, ".claude-plugin", "plugin.json");
5971
6630
  try {
5972
- if (existsSync3(manifest))
6631
+ if (existsSync4(manifest))
5973
6632
  return candidate;
5974
6633
  } catch {
5975
6634
  }
5976
6635
  candidate = dirname3(candidate);
5977
6636
  }
5978
- return join5(startDir, "..");
6637
+ return join6(startDir, "..");
5979
6638
  }
5980
6639
  var BUNDLE_ROOT = findBundleRoot(__dirname);
5981
6640
  function resolveVendoredBinary() {
5982
6641
  const candidates = [
5983
- join5(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
5984
- join5(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
6642
+ join6(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
6643
+ join6(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
5985
6644
  ];
5986
6645
  for (const candidate of candidates) {
5987
- if (existsSync3(candidate))
6646
+ if (existsSync4(candidate))
5988
6647
  return candidate;
5989
6648
  }
5990
6649
  throw new Error(`[halfcycle] Cannot find the self-contained guard binary bin.bundle.mjs. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
5991
6650
  }
5992
6651
  function resolvePreFilterShell() {
5993
6652
  const candidates = [
5994
- join5(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
5995
- join5(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
6653
+ join6(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
6654
+ join6(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
5996
6655
  ];
5997
6656
  for (const candidate of candidates) {
5998
- if (existsSync3(candidate))
5999
- return readFileSync5(candidate, "utf-8");
6657
+ if (existsSync4(candidate))
6658
+ return readFileSync6(candidate, "utf-8");
6000
6659
  }
6001
6660
  throw new Error(`[halfcycle] Cannot find ${PRE_FILTER_SHELL_FILENAME}. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
6002
6661
  }
@@ -6025,7 +6684,7 @@ function isAllowlisted(targetRelPath) {
6025
6684
  var CREW_ROSTER_REL = ".halfcycle/crew.json";
6026
6685
  var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
6027
6686
  function capturedIndexScaffold() {
6028
- return join5(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
6687
+ return join6(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
6029
6688
  }
6030
6689
  var GITIGNORE_REL = ".gitignore";
6031
6690
  var GITATTRIBUTES_REL = ".gitattributes";
@@ -6063,7 +6722,7 @@ function closedWriteSet() {
6063
6722
  const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, GITIGNORE_REL, GITATTRIBUTES_REL]);
6064
6723
  const dirs = /* @__PURE__ */ new Set();
6065
6724
  for (const path of [...files, ...merged]) {
6066
- for (let dir = posix.dirname(path); dir !== "."; dir = posix.dirname(dir))
6725
+ for (let dir = posix2.dirname(path); dir !== "."; dir = posix2.dirname(dir))
6067
6726
  dirs.add(dir);
6068
6727
  }
6069
6728
  const ownedDirs = new Set([...dirs].filter((dir) => `${dir}/`.startsWith(INSTALLER_OWNED_DIR)));
@@ -6087,11 +6746,11 @@ function closedWriteSet() {
6087
6746
  function sha256OfWhatThisVersionWrites(commands) {
6088
6747
  const out = /* @__PURE__ */ new Map();
6089
6748
  const fromFile = (rel, src) => {
6090
- if (existsSync3(src))
6091
- out.set(rel, fileSha256(readFileSync5(src)));
6749
+ if (existsSync4(src))
6750
+ out.set(rel, fileSha256(readFileSync6(src)));
6092
6751
  };
6093
6752
  for (const cmd of commands)
6094
- fromFile(commandStubRel(cmd.name), join5(BUNDLE_ROOT, ".claude-plugin", cmd.path));
6753
+ fromFile(commandStubRel(cmd.name), join6(BUNDLE_ROOT, ".claude-plugin", cmd.path));
6095
6754
  for (const [rel, content] of hookScriptsThisVersionWrites())
6096
6755
  out.set(rel, fileSha256(content));
6097
6756
  fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
@@ -6104,17 +6763,17 @@ function bytesAreOurs(path, bytes, recorded, writeSet) {
6104
6763
  }
6105
6764
  function readPreviousManifest(targetRepo) {
6106
6765
  try {
6107
- return parseManifest(readFileSync5(join5(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
6766
+ return parseManifest(readFileSync6(join6(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
6108
6767
  } catch {
6109
6768
  return null;
6110
6769
  }
6111
6770
  }
6112
- function noteFile(draft, targetRepo, rel, outcome) {
6771
+ function noteFile(draft, rel, outcome, after) {
6113
6772
  if (outcome === "collided") {
6114
6773
  recordCollision(draft, rel);
6115
6774
  return;
6116
6775
  }
6117
- recordInstalledFile(draft, rel, readFileSync5(join5(targetRepo, rel)));
6776
+ recordInstalledFile(draft, rel, after(rel));
6118
6777
  }
6119
6778
  function recordSettingsMerge(draft, existing, generated, preexisted, merged) {
6120
6779
  const generatedHooks = generated.hooks ?? {};
@@ -6203,11 +6862,11 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
6203
6862
  const collided = new Set(collidedPaths);
6204
6863
  const dirVerdicts = /* @__PURE__ */ new Map();
6205
6864
  const travelsThroughPlainDirs = (rel) => {
6206
- for (let dir = posix.dirname(rel); dir !== "."; dir = posix.dirname(dir)) {
6865
+ for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
6207
6866
  let plain = dirVerdicts.get(dir);
6208
6867
  if (plain === void 0) {
6209
- const abs = join5(targetRepo, dir);
6210
- plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join5(abs, ".git")) === null;
6868
+ const abs = join6(targetRepo, dir);
6869
+ plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join6(abs, ".git")) === null;
6211
6870
  dirVerdicts.set(dir, plain);
6212
6871
  }
6213
6872
  if (!plain)
@@ -6221,7 +6880,7 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
6221
6880
  ...merged,
6222
6881
  INSTALL_MANIFEST_REL
6223
6882
  ])
6224
- ].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join5(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
6883
+ ].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join6(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
6225
6884
  const ignored = readIgnoredPaths(targetRepo, paths);
6226
6885
  if (ignored === null)
6227
6886
  return null;
@@ -6244,7 +6903,7 @@ function lstatOrNull(path) {
6244
6903
  }
6245
6904
  function readFileOrNull(path) {
6246
6905
  try {
6247
- return readFileSync5(path, "utf-8");
6906
+ return readFileSync6(path, "utf-8");
6248
6907
  } catch {
6249
6908
  return null;
6250
6909
  }
@@ -6253,28 +6912,33 @@ function isPlainObject2(value) {
6253
6912
  return value !== null && typeof value === "object" && !Array.isArray(value);
6254
6913
  }
6255
6914
  function readPluginManifest() {
6256
- const manifestPath = join5(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
6257
- const raw = readFileSync5(manifestPath, "utf-8");
6915
+ const manifestPath = join6(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
6916
+ const raw = readFileSync6(manifestPath, "utf-8");
6258
6917
  return JSON.parse(raw);
6259
6918
  }
6260
- function copyManifestCommands(manifest, targetRepo, report, draft, writeSet) {
6261
- for (const cmd of manifest.commands ?? []) {
6262
- const srcFile = join5(BUNDLE_ROOT, ".claude-plugin", cmd.path);
6263
- if (!existsSync3(srcFile)) {
6919
+ function planManifestCommands(manifest, targetRepo, previous, writeSet) {
6920
+ const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
6921
+ return (manifest.commands ?? []).map((cmd) => {
6922
+ const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
6923
+ if (!existsSync4(srcFile)) {
6264
6924
  throw new Error(`[halfcycle] plugin.json declares command "${cmd.name}" at "${cmd.path}", but no file exists there (${srcFile}). The manifest and commands/ must agree.`);
6265
6925
  }
6266
6926
  const rel = commandStubRel(cmd.name);
6267
- const destFile = join5(targetRepo, rel);
6268
- const content = readFileSync5(srcFile, "utf-8");
6269
- const recorded = draft.files.get(rel);
6270
- const { outcome, replacedExisting } = writeCollisionSafe(destFile, targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
6271
- record(report, outcome, rel);
6272
- if (replacedExisting)
6273
- report.replacedPaths.push(rel);
6274
- if (outcome === "collided" && recorded !== void 0)
6275
- report.editedCollidedPaths.push(rel);
6276
- noteFile(draft, targetRepo, rel, outcome);
6927
+ const content = readFileSync6(srcFile, "utf-8");
6928
+ const recorded = recordedFiles.get(rel);
6929
+ const planned = decideCollisionSafe(join6(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
6930
+ return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
6931
+ });
6932
+ }
6933
+ function putPlannedFile(targetRepoRoot, planned) {
6934
+ if (planned.outcome !== "written")
6935
+ return;
6936
+ if (!isAllowlisted(planned.rel)) {
6937
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${planned.rel}". Only the method surface and scaffolding paths are writable.`);
6277
6938
  }
6939
+ const target = join6(targetRepoRoot, planned.rel);
6940
+ mkdirSync5(dirname3(target), { recursive: true });
6941
+ writeFileSync5(target, planned.text, "utf-8");
6278
6942
  }
6279
6943
  function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
6280
6944
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
@@ -6287,71 +6951,66 @@ function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths)
6287
6951
  writeFileSync5(targetAbsPath, content, "utf-8");
6288
6952
  writtenPaths.push(rel);
6289
6953
  }
6290
- function writeCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
6954
+ function decideCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
6291
6955
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
6292
6956
  if (!isAllowlisted(rel)) {
6293
6957
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
6294
6958
  }
6295
- let replacedExisting = false;
6296
- const current = existsSync3(targetAbsPath) ? readFileSync5(targetAbsPath) : null;
6959
+ const current = existsSync4(targetAbsPath) ? readFileSync6(targetAbsPath) : null;
6297
6960
  const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
6298
- if (current !== null) {
6299
- if (current.toString("utf-8") === text)
6300
- return { outcome: "skipped", replacedExisting: false };
6301
- if (!isOurs(current))
6302
- return { outcome: "collided", replacedExisting: false };
6303
- replacedExisting = true;
6304
- }
6305
- mkdirSync5(dirname3(targetAbsPath), { recursive: true });
6306
- writeFileSync5(targetAbsPath, text, "utf-8");
6307
- return { outcome: "written", replacedExisting };
6961
+ if (current === null)
6962
+ return { rel, text, outcome: "written", replacedExisting: false };
6963
+ if (current.toString("utf-8") === text)
6964
+ return { rel, text, outcome: "skipped", replacedExisting: false };
6965
+ if (!isOurs(current))
6966
+ return { rel, text, outcome: "collided", replacedExisting: false };
6967
+ return { rel, text, outcome: "written", replacedExisting: true };
6308
6968
  }
6309
6969
  var INSTALLER_OWNED_DIR = ".halfcycle/";
6310
6970
  function writeOwned(targetAbsPath, targetRepoRoot, content) {
6971
+ const planned = decideOwned(targetAbsPath, targetRepoRoot, content);
6972
+ putPlannedFile(targetRepoRoot, planned);
6973
+ return planned.outcome;
6974
+ }
6975
+ function decideOwned(targetAbsPath, targetRepoRoot, content) {
6311
6976
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
6312
6977
  if (!isAllowlisted(rel)) {
6313
6978
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
6314
6979
  }
6315
6980
  if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
6316
- throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (writeOwnedGenerated) rather than assumed from where it sits.`);
6981
+ throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (decideOwnedGenerated) rather than assumed from where it sits.`);
6317
6982
  }
6318
6983
  const current = readFileOrNull(targetAbsPath);
6319
6984
  const text = inLineEndingOf(current, content);
6320
6985
  if (current === text)
6321
- return "skipped";
6322
- mkdirSync5(dirname3(targetAbsPath), { recursive: true });
6323
- writeFileSync5(targetAbsPath, text, "utf-8");
6324
- return "written";
6986
+ return { rel, text, outcome: "skipped", replacedExisting: false };
6987
+ return { rel, text, outcome: "written", replacedExisting: existsSync4(targetAbsPath) };
6325
6988
  }
6326
6989
  var GENERATED_HEADER_LINE_INDEX = 1;
6327
- function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
6990
+ function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
6328
6991
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
6329
6992
  if (!isAllowlisted(rel)) {
6330
6993
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
6331
6994
  }
6332
- let replacedExisting = false;
6333
- if (existsSync3(targetAbsPath)) {
6334
- const current = readFileSync5(targetAbsPath, "utf-8");
6335
- if (current === content)
6336
- return { outcome: "skipped", replacedExisting: false };
6337
- const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
6338
- if (!headerLine.startsWith(generatedMarker))
6339
- return { outcome: "collided", replacedExisting: false };
6340
- replacedExisting = true;
6995
+ if (!existsSync4(targetAbsPath))
6996
+ return { rel, text: content, outcome: "written", replacedExisting: false };
6997
+ const current = readFileSync6(targetAbsPath, "utf-8");
6998
+ if (current === content)
6999
+ return { rel, text: content, outcome: "skipped", replacedExisting: false };
7000
+ const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
7001
+ if (!headerLine.startsWith(generatedMarker)) {
7002
+ return { rel, text: content, outcome: "collided", replacedExisting: false };
6341
7003
  }
6342
- mkdirSync5(dirname3(targetAbsPath), { recursive: true });
6343
- writeFileSync5(targetAbsPath, content, "utf-8");
6344
- return { outcome: "written", replacedExisting };
7004
+ return { rel, text: content, outcome: "written", replacedExisting: true };
6345
7005
  }
6346
- function recordOwnedGenerated(report, targetAbsPath, targetRepoRoot, content, generatedMarker, rel) {
6347
- const { outcome, replacedExisting } = writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker);
6348
- record(report, outcome, rel);
6349
- if (replacedExisting)
6350
- report.replacedPaths.push(rel);
6351
- return outcome;
7006
+ function recordOwnedGenerated(report, planned) {
7007
+ record(report, planned.outcome, planned.rel);
7008
+ if (planned.replacedExisting)
7009
+ report.replacedPaths.push(planned.rel);
7010
+ return planned.outcome;
6352
7011
  }
6353
7012
  function recordOwned(report, targetAbsPath, targetRepoRoot, content, rel) {
6354
- const existedBefore = existsSync3(targetAbsPath);
7013
+ const existedBefore = existsSync4(targetAbsPath);
6355
7014
  const outcome = writeOwned(targetAbsPath, targetRepoRoot, content);
6356
7015
  record(report, outcome, rel);
6357
7016
  if (outcome === "written" && existedBefore)
@@ -6373,8 +7032,8 @@ var DENY_PATTERNS = [
6373
7032
  ];
6374
7033
  var USER_PROMPT_REMINDER_SENTENCE = "Where this project stands, and what order to work in, is recorded in the Halfcycle service and nowhere in this repository \u2014 call halfcycle_resolve before you answer or plan, because the files here will look complete and current and still be wrong about position.";
6375
7034
  function readExistingSettings(targetRepo) {
6376
- const settingsPath = join5(targetRepo, SETTINGS_REL);
6377
- return existsSync3(settingsPath) ? JSON.parse(readFileSync5(settingsPath, "utf-8")) : void 0;
7035
+ const settingsPath = join6(targetRepo, SETTINGS_REL);
7036
+ return existsSync4(settingsPath) ? JSON.parse(readFileSync6(settingsPath, "utf-8")) : void 0;
6378
7037
  }
6379
7038
  var SettingsParseRefused = class extends Error {
6380
7039
  constructor(message) {
@@ -6383,11 +7042,11 @@ var SettingsParseRefused = class extends Error {
6383
7042
  }
6384
7043
  };
6385
7044
  function settingsParseRefusal(targetRepo, err) {
6386
- const file = join5(targetRepo, SETTINGS_REL);
7045
+ const file = join6(targetRepo, SETTINGS_REL);
6387
7046
  const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
6388
7047
  let where = "";
6389
7048
  if (offset !== void 0) {
6390
- const before = readFileSync5(file, "utf-8").slice(0, Number(offset)).split("\n");
7049
+ const before = readFileSync6(file, "utf-8").slice(0, Number(offset)).split("\n");
6391
7050
  where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
6392
7051
  }
6393
7052
  return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
@@ -6398,18 +7057,18 @@ var HALFCYCLE_STATUS_LINE = Object.freeze({
6398
7057
  command: 'bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/guard-runner.sh statusline'
6399
7058
  });
6400
7059
  function statusLineSetElsewhere(targetRepo, home = homedir2(), env = process.env) {
6401
- const places = [join5(targetRepo, ".claude", "settings.local.json"), join5(home, ".claude", "settings.json")];
7060
+ const places = [join6(targetRepo, ".claude", "settings.local.json"), join6(home, ".claude", "settings.json")];
6402
7061
  const configDir = env["CLAUDE_CONFIG_DIR"];
6403
7062
  if (configDir !== void 0 && configDir !== "") {
6404
- const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join5(home, configDir.slice(2)) : configDir;
6405
- places.push(join5(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
7063
+ const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join6(home, configDir.slice(2)) : configDir;
7064
+ places.push(join6(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
6406
7065
  }
6407
7066
  return places.some((path) => {
6408
- if (!existsSync3(path))
7067
+ if (!existsSync4(path))
6409
7068
  return false;
6410
7069
  let value;
6411
7070
  try {
6412
- value = JSON.parse(readFileSync5(path, "utf-8"));
7071
+ value = JSON.parse(readFileSync6(path, "utf-8"));
6413
7072
  } catch {
6414
7073
  return true;
6415
7074
  }
@@ -6646,6 +7305,10 @@ ${GENERATED_SESSION_END_HEADER} the Halfcycle installer.
6646
7305
  # written at session start. Deletion happens HERE and not at stop time: the stop
6647
7306
  # hook runs at the end of every turn, and deleting the marker there would throw
6648
7307
  # away the session's starting point for every later turn.
7308
+ #
7309
+ # Also removes the two small files this session may have left behind for its
7310
+ # checks to hand findings across turns, both named by this session's id alone,
7311
+ # with no fallback name to remove when there is none.
6649
7312
  INPUT="$(cat 2>/dev/null || true)"
6650
7313
 
6651
7314
  REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
@@ -6666,6 +7329,8 @@ rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.eval" 2>/dev/null || true
6666
7329
  if [ -n "$SESSION_ID" ]; then
6667
7330
  rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref" 2>/dev/null || true
6668
7331
  rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.eval" 2>/dev/null || true
7332
+ rm -f "\${MARKER_DIR}/halfcycle-findings-\${SESSION_ID}.json" 2>/dev/null || true
7333
+ rm -f "\${MARKER_DIR}/halfcycle-own-\${SESSION_ID}.jsonl" 2>/dev/null || true
6669
7334
  fi
6670
7335
  exit 0
6671
7336
  `;
@@ -6725,19 +7390,20 @@ function generateUserPromptScopeHook() {
6725
7390
  return `#!/usr/bin/env bash
6726
7391
  ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the close review.
6727
7392
  #
6728
- # Lets the owner of this project approve a phase's scope, or send it back, and
6729
- # decide one part of a phase's close review, by typing one line as the whole
6730
- # prompt in their own Claude Code session: "approve <phase>", "send back
6731
- # <phase>: <note>", "accept <phase>/<part>", "question <phase>/<part>: <words>",
6732
- # "carry <phase>/<part>: <words>" or "drop <phase>/<part>: <words>".
7393
+ # Lets the owner of this project approve a phase's scope, or send it back,
7394
+ # decide one part of a phase's close review, or turn builds running on their
7395
+ # own on or off, by typing one line as the whole prompt in their own Claude
7396
+ # Code session: "approve <phase>", "send back <phase>: <note>", "accept
7397
+ # <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
7398
+ # <words>", "drop <phase>/<part>: <words>", "autonomy on" or "autonomy off".
6733
7399
  #
6734
7400
  # AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
6735
7401
  # looks only at the start of the prompt's value. Unless the prompt begins, after
6736
7402
  # any whitespace (typed, or escaped in the input), with "approve ", "send back ",
6737
- # "accept ", "question ", "carry " or "drop ", in any case, it exits 0 with no
6738
- # output, and Node never starts. A quote inside any other field arrives escaped,
6739
- # so no other field (the folder the session is in, for one) can read as the
6740
- # start of the prompt.
7403
+ # "accept ", "question ", "carry ", "drop " or "autonomy ", in any case, it exits
7404
+ # 0 with no output, and Node never starts. A quote inside any other field arrives
7405
+ # escaped, so no other field (the folder the session is in, for one) can read as
7406
+ # the start of the prompt.
6741
7407
  #
6742
7408
  # Otherwise it finds the project root from its own location, never from the
6743
7409
  # folder the session has moved to, and hands the same input to the guard
@@ -6758,7 +7424,7 @@ IFS= read -r -d '' HC_INPUT || true
6758
7424
  # \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
6759
7425
  HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
6760
7426
  HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
6761
- if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop) "; then
7427
+ if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back|accept|question|carry|drop|autonomy) "; then
6762
7428
  exit 0
6763
7429
  fi
6764
7430
 
@@ -6771,16 +7437,62 @@ set +e
6771
7437
  exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
6772
7438
  `;
6773
7439
  }
6774
- var CI_STANZA_KNOWN_HEADER_HASHES = [
6775
- "b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
6776
- "674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
6777
- "d17bde608a6fec54a63cb934a28d88acc8babc205334b5ee84d4ed6ee219d340",
6778
- "85e71d3f6551977b241a28468f121ee4d1c4edef40a2c4dc1aa6f2f737ebd037"
6779
- ];
6780
- function isTrackedAndClean(targetRepo, relPath) {
6781
- try {
6782
- execFileSync2("git", ["-C", targetRepo, "ls-files", "--error-unmatch", "--", relPath], {
6783
- stdio: "ignore",
7440
+ var GENERATED_USER_PROMPT_FINDINGS_HEADER = "# Findings hand-over \u2014 generated by";
7441
+ function generateUserPromptFindingsHook() {
7442
+ return `#!/usr/bin/env bash
7443
+ ${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer.
7444
+ #
7445
+ # At the person's next message, before the agent starts, hands it the details of
7446
+ # whatever Halfcycle's checks found at an earlier turn end, once.
7447
+ #
7448
+ # AN ORDINARY PROMPT STARTS NOTHING. With no findings file for this session, this
7449
+ # script reads its session id with a bash regex (no node) and exits 0 with no
7450
+ # output once it finds no file \u2014 node and grep never start.
7451
+ trap 'exit 0' ERR
7452
+ set -e
7453
+
7454
+ HC_INPUT=""
7455
+ IFS= read -r -d '' HC_INPUT || true
7456
+
7457
+ # The session id, read with a bash regex held in a variable (a quoted operand to
7458
+ # =~ is read as a literal string, not a pattern). An id holding anything outside
7459
+ # [A-Za-z0-9-] cannot close the match and leaves this empty, which is read below
7460
+ # exactly like no session id at all.
7461
+ HC_SESSION_RE='"session_id"[[:space:]]*:[[:space:]]*"([A-Za-z0-9-]+)"'
7462
+ HC_SESSION_ID=""
7463
+ if [[ "$HC_INPUT" =~ $HC_SESSION_RE ]]; then
7464
+ HC_SESSION_ID="\${BASH_REMATCH[1]}"
7465
+ fi
7466
+ if [ -z "$HC_SESSION_ID" ]; then
7467
+ exit 0
7468
+ fi
7469
+
7470
+ HC_FINDINGS_DIR="\${TMPDIR:-/tmp}"
7471
+ HC_FINDINGS_DIR="\${HC_FINDINGS_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
7472
+ HC_FINDINGS_FILE="\${HC_FINDINGS_DIR}/halfcycle-findings-\${HC_SESSION_ID}.json"
7473
+ if [ ! -f "$HC_FINDINGS_FILE" ]; then
7474
+ exit 0
7475
+ fi
7476
+
7477
+ HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
7478
+ HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
7479
+ export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
7480
+
7481
+ trap - ERR
7482
+ set +e
7483
+ exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-findings <<<"$HC_INPUT"
7484
+ `;
7485
+ }
7486
+ var CI_STANZA_KNOWN_HEADER_HASHES = [
7487
+ "b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
7488
+ "674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
7489
+ "d17bde608a6fec54a63cb934a28d88acc8babc205334b5ee84d4ed6ee219d340",
7490
+ "85e71d3f6551977b241a28468f121ee4d1c4edef40a2c4dc1aa6f2f737ebd037"
7491
+ ];
7492
+ function isTrackedAndClean(targetRepo, relPath) {
7493
+ try {
7494
+ execFileSync2("git", ["-C", targetRepo, "ls-files", "--error-unmatch", "--", relPath], {
7495
+ stdio: "ignore",
6784
7496
  env: gitEnvForNamedRepo()
6785
7497
  });
6786
7498
  } catch {
@@ -6797,15 +7509,19 @@ function isTrackedAndClean(targetRepo, relPath) {
6797
7509
  return true;
6798
7510
  }
6799
7511
  function removeLegacyCiStanza(targetRepo) {
6800
- return removeRetiredGeneratedFile(targetRepo, ".halfcycle/ci-stanza.yml", 0, CI_STANZA_KNOWN_HEADER_HASHES);
7512
+ return removeRetiredGeneratedFile(targetRepo, LEGACY_CI_STANZA_REL, 0, CI_STANZA_KNOWN_HEADER_HASHES);
6801
7513
  }
6802
7514
  function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
6803
- const path = join5(targetRepo, rel);
6804
- if (!existsSync3(path))
7515
+ const decided = decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes);
7516
+ return decided === "removed" ? removePlannedRetiredFile(targetRepo, rel) : decided;
7517
+ }
7518
+ function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
7519
+ const path = join6(targetRepo, rel);
7520
+ if (!existsSync4(path))
6805
7521
  return "absent";
6806
7522
  let raw;
6807
7523
  try {
6808
- raw = readFileSync5(path, "utf-8");
7524
+ raw = readFileSync6(path, "utf-8");
6809
7525
  } catch {
6810
7526
  return "failed";
6811
7527
  }
@@ -6815,13 +7531,20 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
6815
7531
  return "kept-foreign";
6816
7532
  if (!isTrackedAndClean(targetRepo, rel))
6817
7533
  return "kept-uncommitted";
7534
+ return "removed";
7535
+ }
7536
+ function removePlannedRetiredFile(targetRepo, rel) {
6818
7537
  try {
6819
- rmSync2(path);
7538
+ rmSync2(join6(targetRepo, rel));
6820
7539
  } catch {
6821
7540
  return "failed";
6822
7541
  }
6823
7542
  return "removed";
6824
7543
  }
7544
+ var LEGACY_CI_STANZA_REL = ".halfcycle/ci-stanza.yml";
7545
+ function planLegacyCiStanzaRemoval(targetRepo) {
7546
+ return decideRetiredRemoval(targetRepo, LEGACY_CI_STANZA_REL, 0, CI_STANZA_KNOWN_HEADER_HASHES);
7547
+ }
6825
7548
  var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
6826
7549
  `const p=require('path'),f=require('fs'),c=require('child_process');`,
6827
7550
  `let d=process.cwd();`,
@@ -6840,13 +7563,14 @@ var MCP_HEADERS_KNOWN_HEADER_HASHES = [
6840
7563
  "37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
6841
7564
  "6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
6842
7565
  ];
6843
- function removeRetiredMcpHeadersHelper(targetRepo) {
6844
- return removeRetiredGeneratedFile(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
7566
+ function planMcpHeadersHelperRemoval(targetRepo) {
7567
+ return decideRetiredRemoval(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
6845
7568
  }
6846
7569
  var OWNED_GENERATED_HEADERS = {
6847
7570
  ".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
6848
7571
  ".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
6849
7572
  ".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
7573
+ ".claude/hooks/user-prompt-findings.sh": GENERATED_USER_PROMPT_FINDINGS_HEADER,
6850
7574
  ".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
6851
7575
  ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
6852
7576
  };
@@ -6856,6 +7580,7 @@ function hookScriptsThisVersionWrites() {
6856
7580
  [".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
6857
7581
  [".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
6858
7582
  [".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
7583
+ [".claude/hooks/user-prompt-findings.sh", generateUserPromptFindingsHook()],
6859
7584
  [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
6860
7585
  ];
6861
7586
  }
@@ -6909,23 +7634,26 @@ function gitignoreCovers(content) {
6909
7634
  const lines = new Set(content.split("\n").map((l) => l.trim()));
6910
7635
  return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
6911
7636
  }
6912
- function reconcileGitignore(targetRepo) {
6913
- const gitignorePath = join5(targetRepo, GITIGNORE_REL);
7637
+ function planGitignore(targetRepo) {
7638
+ const gitignorePath = join6(targetRepo, GITIGNORE_REL);
6914
7639
  const header = GITIGNORE_HEADER;
7640
+ const before = existsSync4(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : null;
7641
+ if (before === null) {
7642
+ return { before, after: [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n", outcome: "written" };
7643
+ }
7644
+ if (gitignoreCovers(before))
7645
+ return { before, after: before, outcome: "skipped" };
7646
+ const existingLines = new Set(before.split("\n").map((l) => l.trim()));
7647
+ const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
7648
+ const eol = lineEndingOf(before);
7649
+ const suffix = (before.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
7650
+ return { before, after: before + suffix, outcome: "written" };
7651
+ }
7652
+ function putPlannedGitignore(targetRepo, planned) {
7653
+ if (planned.outcome !== "written" || planned.after === null)
7654
+ return planned.outcome;
6915
7655
  try {
6916
- if (!existsSync3(gitignorePath)) {
6917
- const body = [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n";
6918
- writeFileSync5(gitignorePath, body, "utf-8");
6919
- return "written";
6920
- }
6921
- const existing = readFileSync5(gitignorePath, "utf-8");
6922
- if (gitignoreCovers(existing))
6923
- return "skipped";
6924
- const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
6925
- const toAdd = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !existingLines.has(entry));
6926
- const eol = lineEndingOf(existing);
6927
- const suffix = (existing.endsWith("\n") ? "" : eol) + eol + [header, ...toAdd].join(eol) + eol;
6928
- writeFileSync5(gitignorePath, existing + suffix, "utf-8");
7656
+ writeFileSync5(join6(targetRepo, GITIGNORE_REL), planned.after, "utf-8");
6929
7657
  return "written";
6930
7658
  } catch {
6931
7659
  return "failed";
@@ -6934,7 +7662,7 @@ function reconcileGitignore(targetRepo) {
6934
7662
  var GITATTRIBUTES_HEADER = "# Halfcycle \u2014 generated scripts keep LF line endings";
6935
7663
  function readTextOrNull(path) {
6936
7664
  try {
6937
- return readFileSync5(path, "utf-8");
7665
+ return readFileSync6(path, "utf-8");
6938
7666
  } catch {
6939
7667
  return null;
6940
7668
  }
@@ -6943,26 +7671,31 @@ function lfRules(paths) {
6943
7671
  return [...paths].filter((path) => path.endsWith(".sh")).map((path) => `${path} text eol=lf`);
6944
7672
  }
6945
7673
  var REQUIRED_GITATTRIBUTES_RULES = lfRules(Object.keys(OWNED_GENERATED_HEADERS));
6946
- function reconcileGitattributes(targetRepo) {
6947
- const path = join5(targetRepo, GITATTRIBUTES_REL);
7674
+ function planGitattributes(targetRepo) {
7675
+ const path = join6(targetRepo, GITATTRIBUTES_REL);
6948
7676
  const rules = REQUIRED_GITATTRIBUTES_RULES;
6949
- const routed = [];
7677
+ const before = readTextOrNull(path);
7678
+ if (lstatOrNull(path)?.isSymbolicLink() === true)
7679
+ return { before, after: before, outcome: "failed" };
7680
+ if (!existsSync4(path)) {
7681
+ return { before, after: [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", outcome: "written" };
7682
+ }
7683
+ if (before === null)
7684
+ return { before, after: null, outcome: "failed" };
7685
+ const present = new Set(before.split("\n").map((l) => l.trim()));
7686
+ const toAdd = rules.filter((rule) => !present.has(rule));
7687
+ if (toAdd.length === 0)
7688
+ return { before, after: before, outcome: "skipped" };
7689
+ const eol = lineEndingOf(before);
7690
+ const separator = before === "" ? "" : before.endsWith("\n") ? eol : eol + eol;
7691
+ const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
7692
+ return { before, after: before + suffix, outcome: "written" };
7693
+ }
7694
+ function putPlannedGitattributes(targetRepo, planned) {
7695
+ if (planned.outcome !== "written" || planned.after === null)
7696
+ return planned.outcome;
6950
7697
  try {
6951
- if (lstatOrNull(path)?.isSymbolicLink() === true)
6952
- return "failed";
6953
- if (!existsSync3(path)) {
6954
- writeAllowlisted(path, targetRepo, [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", routed);
6955
- return "written";
6956
- }
6957
- const existing = readFileSync5(path, "utf-8");
6958
- const present = new Set(existing.split("\n").map((l) => l.trim()));
6959
- const toAdd = rules.filter((rule) => !present.has(rule));
6960
- if (toAdd.length === 0)
6961
- return "skipped";
6962
- const eol = lineEndingOf(existing);
6963
- const separator = existing === "" ? "" : existing.endsWith("\n") ? eol : eol + eol;
6964
- const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join(eol) + eol;
6965
- writeAllowlisted(path, targetRepo, existing + suffix, routed);
7698
+ writeAllowlisted(join6(targetRepo, GITATTRIBUTES_REL), targetRepo, planned.after, []);
6966
7699
  return "written";
6967
7700
  } catch {
6968
7701
  return "failed";
@@ -6981,7 +7714,7 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
6981
7714
  function nonEmpty(value) {
6982
7715
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
6983
7716
  }
6984
- function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion, writtenPaths) {
7717
+ function planBundlePin(targetRepoRoot, version, engagementId, engagementType, createdByAccountId, methodVersion) {
6985
7718
  const previous = previousPinForEngagement(targetRepoRoot, engagementId);
6986
7719
  const carried = pinCreatedByAccountId(previous) ?? nonEmpty(createdByAccountId);
6987
7720
  const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
@@ -6996,9 +7729,39 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, c
6996
7729
  ...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
6997
7730
  ...projectPage !== void 0 ? { projectPage } : {}
6998
7731
  };
6999
- const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
7000
- const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(pin, null, 2) + "\n");
7001
- writeAllowlisted(pinPath, targetRepoRoot, pinText, writtenPaths);
7732
+ const current = readFileOrNull(join6(targetRepoRoot, BUNDLE_PIN_REL));
7733
+ const text = inLineEndingOf(current, JSON.stringify(pin, null, 2) + "\n");
7734
+ return {
7735
+ rel: BUNDLE_PIN_REL,
7736
+ text,
7737
+ outcome: current === text ? "skipped" : "written",
7738
+ replacedExisting: current !== null && current !== text
7739
+ };
7740
+ }
7741
+ function planBundlePinPreview(targetRepoRoot, version, home) {
7742
+ let existing;
7743
+ try {
7744
+ existing = readBundlePin(targetRepoRoot);
7745
+ } catch {
7746
+ return null;
7747
+ }
7748
+ if (existing === null || nonEmpty(existing.engagementId) === void 0 || existing.engagementType === void 0) {
7749
+ return null;
7750
+ }
7751
+ let pinned;
7752
+ try {
7753
+ pinned = readPinnedEngagement(targetRepoRoot, home);
7754
+ } catch {
7755
+ return null;
7756
+ }
7757
+ if (pinned === null || pinned.engagementId !== existing.engagementId || pinned.credential === void 0) {
7758
+ return null;
7759
+ }
7760
+ return planBundlePin(targetRepoRoot, version, existing.engagementId, existing.engagementType, void 0, void 0);
7761
+ }
7762
+ function writeBundlePin(targetRepoRoot, planned, writtenPaths) {
7763
+ const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
7764
+ writeAllowlisted(pinPath, targetRepoRoot, planned.text, writtenPaths);
7002
7765
  }
7003
7766
  function updatePinnedMethodVersion(repoRoot, engagementId, version) {
7004
7767
  let pin;
@@ -7009,7 +7772,7 @@ function updatePinnedMethodVersion(repoRoot, engagementId, version) {
7009
7772
  }
7010
7773
  if (pin === null || pin.engagementId !== engagementId)
7011
7774
  return;
7012
- const pinPath = join5(repoRoot, BUNDLE_PIN_REL);
7775
+ const pinPath = join6(repoRoot, BUNDLE_PIN_REL);
7013
7776
  const updated = { ...pin, methodVersion: version };
7014
7777
  const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(updated, null, 2) + "\n");
7015
7778
  try {
@@ -7029,17 +7792,17 @@ function renderCrewRoster() {
7029
7792
  }
7030
7793
  function writeCrewRoster(targetRepoRoot, report) {
7031
7794
  const rendered = renderCrewRoster();
7032
- const crewPath = join5(targetRepoRoot, CREW_ROSTER_REL);
7795
+ const crewPath = join6(targetRepoRoot, CREW_ROSTER_REL);
7033
7796
  return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
7034
7797
  }
7035
7798
  function pinCreatedByAccountId(pin) {
7036
7799
  return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
7037
7800
  }
7038
7801
  function readBundlePin(targetRepoRoot) {
7039
- const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
7040
- if (!existsSync3(pinPath))
7802
+ const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
7803
+ if (!existsSync4(pinPath))
7041
7804
  return null;
7042
- return JSON.parse(readFileSync5(pinPath, "utf-8"));
7805
+ return JSON.parse(readFileSync6(pinPath, "utf-8"));
7043
7806
  }
7044
7807
  function readPinnedEngagement(targetRepoRoot, home) {
7045
7808
  const pin = readBundlePin(targetRepoRoot);
@@ -7062,7 +7825,7 @@ function readPinnedEngagement(targetRepoRoot, home) {
7062
7825
  }
7063
7826
  function readLegacyEnvLocal(targetRepoRoot) {
7064
7827
  try {
7065
- return parseEnvText(readFileSync5(join5(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
7828
+ return parseEnvText(readFileSync6(join6(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
7066
7829
  } catch {
7067
7830
  return null;
7068
7831
  }
@@ -7100,15 +7863,32 @@ function checkDrift(targetRepoRoot) {
7100
7863
  const current = manifest.version;
7101
7864
  return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
7102
7865
  }
7103
- function writeProjectIdentity(targetRepo) {
7104
- if (existsSync3(join5(targetRepo, PROJECT_IDENTITY_REL))) {
7105
- repairRemoteCredential(targetRepo);
7106
- return "skipped";
7866
+ function writeProjectIdentity(targetRepo, planned, probe = false) {
7867
+ if (planned.action === "add" || probe) {
7868
+ createProjectIdentityFile(targetRepo, planned.after ?? void 0);
7107
7869
  }
7108
- createProjectIdentityFile(targetRepo);
7109
- return "written";
7870
+ if (planned.action === "change" && planned.after !== null)
7871
+ replaceProjectIdentityFile(targetRepo, planned.after);
7872
+ return planned.action === "add" ? "written" : "skipped";
7110
7873
  }
7111
7874
  function writeEngagementCredential(credential, engagementId, home) {
7875
+ return writeEngagementEnv(engagementId, engagementCredentialValues(credential, engagementId), home);
7876
+ }
7877
+ function planEngagementCredential(credential, engagementId, home) {
7878
+ let existing;
7879
+ try {
7880
+ existing = readFileSync6(engagementEnvPath(engagementId, home), "utf-8");
7881
+ } catch {
7882
+ existing = null;
7883
+ }
7884
+ const reconciled = reconcileEnvText2(existing, engagementCredentialValues(credential, engagementId));
7885
+ return {
7886
+ outcome: existing === reconciled ? "skipped" : "written",
7887
+ existed: existing !== null,
7888
+ storedAfter: parseEnvText(reconciled)
7889
+ };
7890
+ }
7891
+ function engagementCredentialValues(credential, engagementId) {
7112
7892
  const values = {
7113
7893
  HALFCYCLE_SERVICE_URL: credential.serviceUrl,
7114
7894
  // T-22 — the second origin, persisted for the developer and for any tool that
@@ -7136,16 +7916,16 @@ function writeEngagementCredential(credential, engagementId, home) {
7136
7916
  // telemetry origin, same as every other optional field on `DeviceCredential`.
7137
7917
  CONTROL_TELEMETRY_URL: credential.controlTelemetryUrl ?? ""
7138
7918
  };
7139
- return writeEngagementEnv(engagementId, values, home);
7919
+ return values;
7140
7920
  }
7141
7921
  var LEGACY_ENV_LOCAL_REL = ".env.local";
7142
7922
  function migrateLegacyEnvLocal(targetRepo, stored) {
7143
- const path = join5(targetRepo, LEGACY_ENV_LOCAL_REL);
7144
- if (!existsSync3(path))
7923
+ const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
7924
+ if (!existsSync4(path))
7145
7925
  return { outcome: "absent", unbacked: [], missingForAdoption: [] };
7146
7926
  let raw;
7147
7927
  try {
7148
- raw = readFileSync5(path, "utf-8");
7928
+ raw = readFileSync6(path, "utf-8");
7149
7929
  } catch {
7150
7930
  return { outcome: "failed", unbacked: [], missingForAdoption: [] };
7151
7931
  }
@@ -7166,24 +7946,91 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
7166
7946
  return !ours.has(line.slice(0, eq).replace(/^\s*export\s+/, "").trim());
7167
7947
  });
7168
7948
  const substantive = kept.filter((line) => line.trim() !== "" && !line.trim().startsWith("# Halfcycle"));
7949
+ if (substantive.length === 0)
7950
+ return { outcome: "removed", unbacked: [], missingForAdoption: [] };
7951
+ return { outcome: "stripped", unbacked: [], missingForAdoption: [], keptText: kept.join("\n") };
7952
+ }
7953
+ function applyLegacyMigration(targetRepo, decided) {
7954
+ const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
7169
7955
  try {
7170
- if (substantive.length === 0) {
7956
+ if (decided.outcome === "removed") {
7171
7957
  rmSync2(path);
7172
- return { outcome: "removed", unbacked: [], missingForAdoption: [] };
7958
+ return decided;
7959
+ }
7960
+ if (decided.outcome === "stripped" && decided.keptText !== void 0) {
7961
+ writeFileSync5(path, decided.keptText, "utf-8");
7962
+ return decided;
7173
7963
  }
7174
- writeFileSync5(path, kept.join("\n"), "utf-8");
7175
- return { outcome: "stripped", unbacked: [], missingForAdoption: [] };
7964
+ return decided;
7176
7965
  } catch {
7177
7966
  return { outcome: "failed", unbacked: [], missingForAdoption: [] };
7178
7967
  }
7179
7968
  }
7180
7969
  async function install(options) {
7181
- const { targetRepo, engagementId, engagementType, credential, home, createdByAccountId } = options;
7182
- assertEngagementId(engagementId);
7183
- if (!existsSync3(targetRepo)) {
7184
- throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
7185
- }
7970
+ return applyInstallPlan(planInstall(options));
7971
+ }
7972
+ function planCommandStubs(targetRepo, previous, writeSet) {
7186
7973
  const manifest = readPluginManifest();
7974
+ return { version: manifest.version, commands: planManifestCommands(manifest, targetRepo, previous, writeSet) };
7975
+ }
7976
+ function planVendoredBinary(targetRepo) {
7977
+ return decideOwned(join6(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
7978
+ }
7979
+ function planSettings(targetRepo, previous, home, env) {
7980
+ const settingsPath = join6(targetRepo, SETTINGS_REL);
7981
+ const preexisted = existsSync4(settingsPath);
7982
+ const generated = JSON.parse(generateSettingsJson());
7983
+ let existing;
7984
+ try {
7985
+ existing = readExistingSettings(targetRepo) ?? {};
7986
+ } catch (err) {
7987
+ throw new SettingsParseRefused(settingsParseRefusal(targetRepo, err));
7988
+ }
7989
+ const retiredOurs = retiredRulesThisInstallAdded(previous);
7990
+ const statusLine = statusLineSetElsewhere(targetRepo, home, env) ? void 0 : HALFCYCLE_STATUS_LINE;
7991
+ const merged = mergeSettings(existing, generated, retiredOurs, statusLine);
7992
+ const current = readFileOrNull(settingsPath);
7993
+ const text = inLineEndingOf(current, JSON.stringify(merged, null, 2) + "\n");
7994
+ const file = {
7995
+ rel: SETTINGS_REL,
7996
+ text,
7997
+ // Identical bytes are not rewritten; the report still names the file as merged or
7998
+ // written by whether it was there, as it always has.
7999
+ outcome: current === text ? "skipped" : "written",
8000
+ replacedExisting: false
8001
+ };
8002
+ return { file, preexisted, existing, generated, merged, retiredOurs };
8003
+ }
8004
+ function planHookScripts(targetRepo) {
8005
+ return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(join6(targetRepo, rel), targetRepo, content, OWNED_GENERATED_HEADERS[rel]));
8006
+ }
8007
+ function planMcpRegistration(targetRepo, credential) {
8008
+ const mcpPath = join6(targetRepo, MCP_REGISTRATION_REL);
8009
+ const existing = existsSync4(mcpPath) ? readFileSync6(mcpPath, "utf-8") : null;
8010
+ if (credential === void 0)
8011
+ return { kind: "none", existing };
8012
+ if (credential === "pending")
8013
+ return { kind: "pending", existing };
8014
+ const text = inLineEndingOf(existing, generateMcpRegistration(existing, credential.mcpUrl));
8015
+ const outcome = existing === null ? "written" : existing !== text ? "merged" : "skipped";
8016
+ return { kind: "write", existing, text, outcome };
8017
+ }
8018
+ function putPlannedMcp(targetRepo, planned) {
8019
+ if (planned.kind !== "write" || planned.outcome === "skipped")
8020
+ return;
8021
+ writeAllowlisted(join6(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
8022
+ }
8023
+ function planCrewRoster(targetRepo) {
8024
+ return decideOwned(join6(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
8025
+ }
8026
+ function planInstallRecord(targetRepo, text) {
8027
+ return decideOwned(join6(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
8028
+ }
8029
+ function accountInstall(steps, actual) {
8030
+ const { targetRepo, writeSet, pin } = steps;
8031
+ if (pin === null || steps.mcp.kind === "pending" || steps.engagementId === void 0) {
8032
+ throw new Error("[halfcycle] a plan still waiting for the service cannot be applied or recorded.");
8033
+ }
7187
8034
  const report = {
7188
8035
  writtenPaths: [],
7189
8036
  skippedPaths: [],
@@ -7192,38 +8039,22 @@ async function install(options) {
7192
8039
  editedCollidedPaths: [],
7193
8040
  replacedPaths: []
7194
8041
  };
7195
- const writeSet = closedWriteSet();
7196
- const draft = startManifest(readPreviousManifest(targetRepo), (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
7197
- const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
7198
- copyManifestCommands(manifest, targetRepo, report, draft, writeSet);
7199
- const vendoredBinSrc = resolveVendoredBinary();
7200
- const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
7201
- const vendoredOutcome = recordOwned(report, vendoredBinDest, targetRepo, readFileSync5(vendoredBinSrc, "utf-8"), VENDORED_BIN_REL);
7202
- noteFile(draft, targetRepo, VENDORED_BIN_REL, vendoredOutcome);
7203
- const settingsPath = join5(targetRepo, SETTINGS_REL);
7204
- const settingsPreexisted = existsSync3(settingsPath);
7205
- const generatedSettings = JSON.parse(generateSettingsJson());
7206
- let existingSettings;
7207
- try {
7208
- existingSettings = readExistingSettings(targetRepo) ?? {};
7209
- } catch (err) {
7210
- throw new SettingsParseRefused(settingsParseRefusal(targetRepo, err));
7211
- }
7212
- const retiredOurs = retiredRulesThisInstallAdded(draft.previous);
7213
- const statusLine = statusLineSetElsewhere(targetRepo, home, options.env) ? void 0 : HALFCYCLE_STATUS_LINE;
7214
- const mergedSettings = mergeSettings(existingSettings, generatedSettings, retiredOurs, statusLine);
7215
- recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted, mergedSettings);
7216
- recordDenyRetired(draft, retiredOurs.map((rule) => sha256Hex(rule)));
7217
- const mergedSettingsText = inLineEndingOf(readFileOrNull(settingsPath), JSON.stringify(mergedSettings, null, 2) + "\n");
7218
- mkdirSync5(dirname3(settingsPath), { recursive: true });
7219
- writeFileSync5(settingsPath, mergedSettingsText, "utf-8");
7220
- (settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
7221
- for (const [rel, content] of hookScriptsThisVersionWrites()) {
7222
- const hookPath = join5(targetRepo, rel);
7223
- const outcome = recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
7224
- noteFile(draft, targetRepo, rel, outcome);
7225
- }
7226
- switch (removeLegacyCiStanza(targetRepo)) {
8042
+ const draft = startManifest(steps.previous, (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
8043
+ for (const command of steps.commands) {
8044
+ recordOwnedGenerated(report, command);
8045
+ if (command.editedCollided)
8046
+ report.editedCollidedPaths.push(command.rel);
8047
+ noteFile(draft, command.rel, command.outcome, actual.bytesAfter);
8048
+ }
8049
+ noteFile(draft, VENDORED_BIN_REL, recordOwnedGenerated(report, steps.vendored), actual.bytesAfter);
8050
+ const settings = steps.settings;
8051
+ recordSettingsMerge(draft, settings.existing, settings.generated, settings.preexisted, settings.merged);
8052
+ recordDenyRetired(draft, settings.retiredOurs.map((rule) => sha256Hex(rule)));
8053
+ (settings.preexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
8054
+ for (const hook of steps.hooks) {
8055
+ noteFile(draft, hook.rel, recordOwnedGenerated(report, hook), actual.bytesAfter);
8056
+ }
8057
+ switch (actual.ciStanza) {
7227
8058
  case "removed":
7228
8059
  report.writtenPaths.push(".halfcycle/ci-stanza.yml (REMOVED \u2014 Halfcycle no longer generates this file)");
7229
8060
  break;
@@ -7239,20 +8070,17 @@ async function install(options) {
7239
8070
  case "absent":
7240
8071
  break;
7241
8072
  }
7242
- if (credential) {
7243
- const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
7244
- const existingMcp = existsSync3(mcpPath) ? readFileSync5(mcpPath, "utf-8") : null;
7245
- const mcpContent = inLineEndingOf(existingMcp, generateMcpRegistration(existingMcp, credential.mcpUrl));
7246
- recordMcpMerge(draft, existingMcp, mcpContent);
7247
- if (existingMcp === null) {
7248
- writeAllowlisted(mcpPath, targetRepo, mcpContent, report.writtenPaths);
7249
- } else if (existingMcp !== mcpContent) {
7250
- writeAllowlisted(mcpPath, targetRepo, mcpContent, report.mergedPaths);
7251
- } else {
8073
+ const mcp = steps.mcp;
8074
+ if (mcp.kind === "write") {
8075
+ recordMcpMerge(draft, mcp.existing, mcp.text);
8076
+ if (mcp.outcome === "written")
8077
+ report.writtenPaths.push(MCP_REGISTRATION_REL);
8078
+ else if (mcp.outcome === "merged")
8079
+ report.mergedPaths.push(MCP_REGISTRATION_REL);
8080
+ else
7252
8081
  report.skippedPaths.push(MCP_REGISTRATION_REL);
7253
- }
7254
8082
  const helperRel = RETIRED_MCP_HEADERS_HELPER_REL;
7255
- switch (removeRetiredMcpHeadersHelper(targetRepo)) {
8083
+ switch (actual.mcpHelper) {
7256
8084
  case "removed":
7257
8085
  recordRemovedFile(draft, helperRel);
7258
8086
  report.writtenPaths.push(`${helperRel} (REMOVED \u2014 Halfcycle no longer uses this file)`);
@@ -7267,43 +8095,36 @@ async function install(options) {
7267
8095
  report.skippedPaths.push(`${helperRel} (could not be removed \u2014 Halfcycle no longer uses this file, DELETE IT BY HAND)`);
7268
8096
  break;
7269
8097
  case "absent":
8098
+ case null:
7270
8099
  break;
7271
8100
  }
7272
8101
  } else {
7273
8102
  report.skippedPaths.push(`${MCP_REGISTRATION_REL} (no MCP origin \u2014 no credential supplied)`);
7274
- const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
7275
- if (existsSync3(mcpPath))
7276
- recordMcpAdoptionOnly(draft, readFileSync5(mcpPath, "utf-8"));
8103
+ if (mcp.existing !== null)
8104
+ recordMcpAdoptionOnly(draft, mcp.existing);
7277
8105
  }
7278
- const gitignorePath = join5(targetRepo, GITIGNORE_REL);
7279
- const gitignoreBefore = existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
7280
- const gitignoreOutcome = reconcileGitignore(targetRepo);
7281
- recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null, writeSet);
7282
- if (gitignoreOutcome === "failed") {
8106
+ recordGitignoreReconcile(draft, steps.gitignore.before, actual.gitignoreAfter, writeSet);
8107
+ if (actual.gitignore === "failed") {
7283
8108
  report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
7284
8109
  } else {
7285
- record(report, gitignoreOutcome, ".gitignore");
8110
+ record(report, actual.gitignore, ".gitignore");
7286
8111
  }
7287
- const gitattributesPath = join5(targetRepo, GITATTRIBUTES_REL);
7288
- const gitattributesBefore = readTextOrNull(gitattributesPath);
7289
- const gitattributesOutcome = reconcileGitattributes(targetRepo);
7290
- recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath), writeSet);
7291
- if (gitattributesOutcome === "failed") {
8112
+ recordGitattributesReconcile(draft, steps.gitattributes.before, actual.gitattributesAfter, writeSet);
8113
+ if (actual.gitattributes === "failed") {
7292
8114
  report.skippedPaths.push(`${GITATTRIBUTES_REL} (write failed \u2014 a clone made with core.autocrlf=true may check the hook scripts out with CRLF line endings)`);
7293
8115
  } else {
7294
- record(report, gitattributesOutcome, GITATTRIBUTES_REL);
8116
+ record(report, actual.gitattributes, GITATTRIBUTES_REL);
7295
8117
  }
7296
- const identityOutcome = writeProjectIdentity(targetRepo);
8118
+ const identityOutcome = steps.identity.action === "add" ? "written" : "skipped";
7297
8119
  record(report, identityOutcome, PROJECT_IDENTITY_REL);
7298
- noteFile(draft, targetRepo, PROJECT_IDENTITY_REL, identityOutcome);
7299
- const credentialPath = engagementEnvPath(engagementId, home);
7300
- if (credential) {
7301
- record(report, writeEngagementCredential(credential, engagementId, home), credentialPath);
8120
+ noteFile(draft, PROJECT_IDENTITY_REL, identityOutcome, actual.bytesAfter);
8121
+ const credentialPath = engagementEnvPath(steps.engagementId, steps.home);
8122
+ if (actual.credential !== null) {
8123
+ record(report, actual.credential, credentialPath);
7302
8124
  } else {
7303
8125
  report.skippedPaths.push(`${credentialPath} (no credential supplied)`);
7304
8126
  }
7305
- const stored = readEngagementEnv(engagementId, home) ?? {};
7306
- const migration = migrateLegacyEnvLocal(targetRepo, stored);
8127
+ const migration = actual.legacy;
7307
8128
  switch (migration.outcome) {
7308
8129
  case "removed":
7309
8130
  report.writtenPaths.push(".env.local (REMOVED \u2014 the credential now lives outside this repository)");
@@ -7320,32 +8141,22 @@ async function install(options) {
7320
8141
  case "absent":
7321
8142
  break;
7322
8143
  }
7323
- writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, createdByAccountId, credential?.methodVersion, report.writtenPaths);
7324
- noteFile(draft, targetRepo, BUNDLE_PIN_REL, "written");
7325
- noteFile(draft, targetRepo, CREW_ROSTER_REL, writeCrewRoster(targetRepo, report));
7326
- const scanResult = runBootstrapScan(targetRepo);
7327
- if (existsSync3(join5(targetRepo, BOOTSTRAP_STATE_REL)))
8144
+ if (pin.outcome === "written")
8145
+ report.writtenPaths.push(BUNDLE_PIN_REL);
8146
+ noteFile(draft, BUNDLE_PIN_REL, "written", actual.bytesAfter);
8147
+ noteFile(draft, CREW_ROSTER_REL, recordOwnedGenerated(report, steps.crew), actual.bytesAfter);
8148
+ if (actual.stateExistsAfter)
7328
8149
  recordPerMachineFile(draft, BOOTSTRAP_STATE_REL);
7329
8150
  for (const dir of writeSet.dirs) {
7330
- if (!dirsBefore.has(dir) && existsSync3(join5(targetRepo, dir)))
8151
+ if (!steps.dirsBefore.has(dir) && actual.dirExistsAfter(dir))
7331
8152
  recordCreatedDir(draft, dir);
7332
8153
  }
7333
8154
  const installManifest = finishManifest(draft);
7334
8155
  assertManifestInWriteSet(installManifest, writeSet);
7335
- recordOwned(report, join5(targetRepo, INSTALL_MANIFEST_REL), targetRepo, serializeManifest(installManifest), INSTALL_MANIFEST_REL);
7336
- const notShared = findNotShared(targetRepo, installManifest, report.collidedPaths);
7337
- return {
7338
- version: manifest.version,
7339
- writtenPaths: report.writtenPaths,
7340
- skippedPaths: report.skippedPaths,
7341
- mergedPaths: report.mergedPaths,
7342
- collidedPaths: report.collidedPaths,
7343
- editedCollidedPaths: report.editedCollidedPaths,
7344
- replacedPaths: report.replacedPaths,
7345
- bootstrapScanRan: scanResult.ran,
7346
- legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
7347
- notShared
7348
- };
8156
+ return { report, installManifest, manifestText: serializeManifest(installManifest) };
8157
+ }
8158
+ function recordInstallRecord(report, planned) {
8159
+ recordOwnedGenerated(report, planned);
7349
8160
  }
7350
8161
  var SELF_BUILD_GITIGNORE_REL = ".halfcycle/.gitignore";
7351
8162
  var SELF_BUILD_GITIGNORE_TEXT = "bin/\n";
@@ -7359,7 +8170,7 @@ function mcpHasHalfcycleEntry(text) {
7359
8170
  function installSelfBuild(options) {
7360
8171
  const { targetRepo, engagementId, credential, freshlyMinted, home } = options;
7361
8172
  assertEngagementId(engagementId);
7362
- if (!existsSync3(targetRepo)) {
8173
+ if (!existsSync4(targetRepo)) {
7363
8174
  throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
7364
8175
  }
7365
8176
  const report = {
@@ -7370,7 +8181,7 @@ function installSelfBuild(options) {
7370
8181
  editedCollidedPaths: [],
7371
8182
  replacedPaths: []
7372
8183
  };
7373
- const at = (rel) => join5(targetRepo, rel);
8184
+ const at = (rel) => join6(targetRepo, rel);
7374
8185
  const version = readPluginManifest().version;
7375
8186
  const existingMcp = readFileOrNull(at(MCP_REGISTRATION_REL));
7376
8187
  const mcpText = existingMcp === null || !mcpHasHalfcycleEntry(existingMcp) ? inLineEndingOf(existingMcp, generateMcpRegistration(existingMcp, credential.mcpUrl)) : null;
@@ -7381,17 +8192,17 @@ function installSelfBuild(options) {
7381
8192
  if (mcpText !== null) {
7382
8193
  writeAllowlisted(at(MCP_REGISTRATION_REL), targetRepo, mcpText, report.writtenPaths);
7383
8194
  }
7384
- if (!existsSync3(at(VENDORED_BIN_REL))) {
7385
- recordOwned(report, at(VENDORED_BIN_REL), targetRepo, readFileSync5(resolveVendoredBinary(), "utf-8"), VENDORED_BIN_REL);
8195
+ if (!existsSync4(at(VENDORED_BIN_REL))) {
8196
+ recordOwned(report, at(VENDORED_BIN_REL), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"), VENDORED_BIN_REL);
7386
8197
  }
7387
- if (!existsSync3(at(SELF_BUILD_GITIGNORE_REL))) {
8198
+ if (!existsSync4(at(SELF_BUILD_GITIGNORE_REL))) {
7388
8199
  writeAllowlisted(at(SELF_BUILD_GITIGNORE_REL), targetRepo, SELF_BUILD_GITIGNORE_TEXT, report.writtenPaths);
7389
8200
  }
7390
- if (!existsSync3(at(CREW_ROSTER_REL))) {
8201
+ if (!existsSync4(at(CREW_ROSTER_REL))) {
7391
8202
  writeCrewRoster(targetRepo, report);
7392
8203
  }
7393
- if (!existsSync3(at(BUNDLE_PIN_REL))) {
7394
- writeBundlePin(targetRepo, version, engagementId, "self-build", void 0, credential.methodVersion, report.writtenPaths);
8204
+ if (!existsSync4(at(BUNDLE_PIN_REL))) {
8205
+ writeBundlePin(targetRepo, planBundlePin(targetRepo, version, engagementId, "self-build", void 0, credential.methodVersion), report.writtenPaths);
7395
8206
  }
7396
8207
  return { writtenPaths: report.writtenPaths, credentialWritten };
7397
8208
  }
@@ -7507,14 +8318,14 @@ async function bindLoopback(env = process.env, engagementId) {
7507
8318
  else
7508
8319
  pending = callback;
7509
8320
  }, engagementId));
7510
- const bindProblem = await new Promise((resolve7) => {
8321
+ const bindProblem = await new Promise((resolve8) => {
7511
8322
  const onError = (err) => {
7512
- resolve7(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
8323
+ resolve8(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
7513
8324
  };
7514
8325
  server.once("error", onError);
7515
8326
  server.listen({ host: LOOPBACK_REDIRECT_HOST, port: wanted.port }, () => {
7516
8327
  server.removeListener("error", onError);
7517
- resolve7(null);
8328
+ resolve8(null);
7518
8329
  });
7519
8330
  });
7520
8331
  if (bindProblem !== null) {
@@ -7530,9 +8341,9 @@ async function bindLoopback(env = process.env, engagementId) {
7530
8341
  bound: true,
7531
8342
  target: { port: address.port, state },
7532
8343
  boundAddress: address.address,
7533
- waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve7) => {
8344
+ waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve8) => {
7534
8345
  if (pending !== null) {
7535
- resolve7(pending);
8346
+ resolve8(pending);
7536
8347
  return;
7537
8348
  }
7538
8349
  const startedAt = Date.now();
@@ -7543,7 +8354,7 @@ async function bindLoopback(env = process.env, engagementId) {
7543
8354
  clearTimeout(timer);
7544
8355
  deliver = null;
7545
8356
  abandon = null;
7546
- resolve7(value);
8357
+ resolve8(value);
7547
8358
  };
7548
8359
  const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
7549
8360
  deliver = finish;
@@ -7557,9 +8368,9 @@ async function bindLoopback(env = process.env, engagementId) {
7557
8368
  };
7558
8369
  }
7559
8370
  function closeServer(server) {
7560
- return new Promise((resolve7) => {
8371
+ return new Promise((resolve8) => {
7561
8372
  server.closeAllConnections();
7562
- server.close(() => resolve7());
8373
+ server.close(() => resolve8());
7563
8374
  });
7564
8375
  }
7565
8376
 
@@ -7582,7 +8393,7 @@ function defaultWrite(text) {
7582
8393
  process.stdout.write(text);
7583
8394
  }
7584
8395
  function defaultSleep(ms) {
7585
- return new Promise((resolve7) => setTimeout(resolve7, ms));
8396
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
7586
8397
  }
7587
8398
  function browserOpenCommand(os, url) {
7588
8399
  if (os === "darwin")
@@ -7629,12 +8440,12 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
7629
8440
  };
7630
8441
  }
7631
8442
  const { command, args: args2, windowsVerbatimArguments } = browserOpenCommand(os, url);
7632
- return new Promise((resolve7) => {
8443
+ return new Promise((resolve8) => {
7633
8444
  let child;
7634
8445
  try {
7635
8446
  child = spawnImpl(command, args2, { stdio: "ignore", env, windowsVerbatimArguments });
7636
8447
  } catch {
7637
- resolve7({ opened: false, reason: `${command} could not be run` });
8448
+ resolve8({ opened: false, reason: `${command} could not be run` });
7638
8449
  return;
7639
8450
  }
7640
8451
  let settled = false;
@@ -7643,7 +8454,7 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
7643
8454
  return;
7644
8455
  settled = true;
7645
8456
  clearTimeout(timer);
7646
- resolve7(attempt);
8457
+ resolve8(attempt);
7647
8458
  };
7648
8459
  const timer = setTimeout(() => {
7649
8460
  child.unref();
@@ -7788,17 +8599,17 @@ async function pollBesideLoopback(serviceUrl, started, wake, deps) {
7788
8599
  continue;
7789
8600
  }
7790
8601
  if (sleep === void 0) {
7791
- await new Promise((resolve7) => {
7792
- const timer = setTimeout(resolve7, started.pollIntervalMs);
8602
+ await new Promise((resolve8) => {
8603
+ const timer = setTimeout(resolve8, started.pollIntervalMs);
7793
8604
  interrupt = () => {
7794
8605
  clearTimeout(timer);
7795
- resolve7();
8606
+ resolve8();
7796
8607
  };
7797
8608
  });
7798
8609
  } else {
7799
- await new Promise((resolve7) => {
7800
- interrupt = resolve7;
7801
- void sleep(started.pollIntervalMs).then(resolve7);
8610
+ await new Promise((resolve8) => {
8611
+ interrupt = resolve8;
8612
+ void sleep(started.pollIntervalMs).then(resolve8);
7802
8613
  });
7803
8614
  }
7804
8615
  interrupt = null;
@@ -8074,8 +8885,8 @@ async function switchAccount(serviceUrl, opts = {}) {
8074
8885
  function writeToStdout(text) {
8075
8886
  process.stdout.write(text);
8076
8887
  }
8077
- async function replaceRefusedCredential(serviceUrl, refused2, opts = {}) {
8078
- if (refused2.source !== "stored")
8888
+ async function replaceRefusedCredential(serviceUrl, refused3, opts = {}) {
8889
+ if (refused3.source !== "stored")
8079
8890
  return null;
8080
8891
  forgetStoredCredential(serviceUrl, opts.home);
8081
8892
  (opts.write ?? writeToStdout)(`[halfcycle] Your saved Halfcycle sign-in was refused \u2014 signing you in again.
@@ -8420,9 +9231,9 @@ function wrongAccountVisibility(act) {
8420
9231
  async function askLine(question2) {
8421
9232
  const rl = createInterface({ input: process.stdin, output: process.stdout });
8422
9233
  try {
8423
- return await new Promise((resolve7) => {
8424
- rl.once("close", () => resolve7(""));
8425
- rl.question(question2).then(resolve7, () => resolve7(""));
9234
+ return await new Promise((resolve8) => {
9235
+ rl.once("close", () => resolve8(""));
9236
+ rl.question(question2).then(resolve8, () => resolve8(""));
8426
9237
  });
8427
9238
  } finally {
8428
9239
  rl.close();
@@ -8658,8 +9469,8 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
8658
9469
 
8659
9470
  // dist/pre-create-checks.js
8660
9471
  import { spawnSync } from "node:child_process";
8661
- import { existsSync as existsSync4, readFileSync as readFileSync6, realpathSync } from "node:fs";
8662
- import { delimiter, dirname as dirname4, join as join6 } from "node:path";
9472
+ import { existsSync as existsSync5, readFileSync as readFileSync7, realpathSync } from "node:fs";
9473
+ import { delimiter, dirname as dirname4, join as join7 } from "node:path";
8663
9474
  var KEPT_FOLDER_ID_LINE = 'Nothing on the Halfcycle service needs undoing, and this folder keeps its project id, so running "npx halfcycle" again picks up where it left off.';
8664
9475
  var PreCreateRefused = class extends Error {
8665
9476
  constructor(message) {
@@ -8702,7 +9513,7 @@ function refuseSystemFolder(targetRepo, platform6 = process.platform) {
8702
9513
  function refuseInsideProject(targetRepo) {
8703
9514
  let folder = realOrLiteral(targetRepo);
8704
9515
  for (let parent = dirname4(folder); parent !== folder; folder = parent, parent = dirname4(parent)) {
8705
- if (existsSync4(join6(parent, BUNDLE_PIN_REL))) {
9516
+ if (existsSync5(join7(parent, BUNDLE_PIN_REL))) {
8706
9517
  throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in Claude Code instead, or run "npx halfcycle" in a folder outside it.`);
8707
9518
  }
8708
9519
  }
@@ -8725,35 +9536,43 @@ async function offerGitForWindows() {
8725
9536
  return true;
8726
9537
  throw new PreCreateRefused(`Git for Windows is needed and is not installed \u2014 install it with "${GIT_INSTALL_COMMAND}" or from ${GIT_DOWNLOAD_PAGE}, then open a new terminal and run "npx halfcycle" again.`);
8727
9538
  }
8728
- function hasGitBash(env) {
8729
- const onPath = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "" && existsSync4(join6(dir, "git.exe"))).map((dir) => join6(dir, "..", "bin", "bash.exe"));
9539
+ function gitBashPath(env) {
9540
+ const onPath = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "" && existsSync5(join7(dir, "git.exe"))).map((dir) => join7(dir, "..", "bin", "bash.exe"));
8730
9541
  const candidates = [
8731
9542
  env["CLAUDE_CODE_GIT_BASH_PATH"],
8732
9543
  ...onPath,
8733
- env["ProgramFiles"] === void 0 ? void 0 : join6(env["ProgramFiles"], "Git", "bin", "bash.exe")
9544
+ env["ProgramFiles"] === void 0 ? void 0 : join7(env["ProgramFiles"], "Git", "bin", "bash.exe")
8734
9545
  ];
8735
- return candidates.some((file) => file !== void 0 && file !== "" && existsSync4(file));
9546
+ return candidates.find((file) => file !== void 0 && file !== "" && existsSync5(file));
9547
+ }
9548
+ function hasGitBash(env) {
9549
+ return gitBashPath(env) !== void 0;
8736
9550
  }
8737
9551
  function runWingetInstall() {
8738
9552
  return spawnSync("winget", [...GIT_INSTALL_ARGS], { stdio: "inherit" }).status === 0;
8739
9553
  }
9554
+ function unreadableProjectRefusal(targetRepo) {
9555
+ return `${join7(targetRepo, PROJECT_IDENTITY_REL)} could not be read as a Halfcycle project file \u2014 delete the .halfcycle folder in ${targetRepo} and run "npx halfcycle" again to start a new project here.`;
9556
+ }
9557
+ function cannotWriteRefusal(targetRepo) {
9558
+ return `cannot write to ${targetRepo} \u2014 run "npx halfcycle" in a folder you can write to.`;
9559
+ }
8740
9560
  function claimFolderId(targetRepo) {
9561
+ const planned = planProjectIdentity(targetRepo);
8741
9562
  try {
8742
- createProjectIdentityFile(targetRepo);
9563
+ writeProjectIdentity(targetRepo, planned, true);
8743
9564
  } catch {
8744
- throw new PreCreateRefused(`cannot write to ${targetRepo} \u2014 run "npx halfcycle" in a folder you can write to.`);
9565
+ throw new PreCreateRefused(cannotWriteRefusal(targetRepo));
8745
9566
  }
8746
- repairRemoteCredential(targetRepo);
8747
9567
  const folderId = readFolderId(targetRepo);
8748
- if (folderId === void 0) {
8749
- throw new PreCreateRefused(`${join6(targetRepo, PROJECT_IDENTITY_REL)} could not be read as a Halfcycle project file \u2014 delete the .halfcycle folder in ${targetRepo} and run "npx halfcycle" again to start a new project here.`);
8750
- }
9568
+ if (folderId === void 0)
9569
+ throw new PreCreateRefused(unreadableProjectRefusal(targetRepo));
8751
9570
  return folderId;
8752
9571
  }
8753
9572
  function readFolderId(targetRepo) {
8754
9573
  let folderId;
8755
9574
  try {
8756
- const text = readFileSync6(join6(targetRepo, PROJECT_IDENTITY_REL), "utf-8");
9575
+ const text = readFileSync7(join7(targetRepo, PROJECT_IDENTITY_REL), "utf-8");
8757
9576
  folderId = JSON.parse(text)?.projectId;
8758
9577
  } catch {
8759
9578
  return void 0;
@@ -8769,20 +9588,420 @@ function checkSettingsParse(targetRepo) {
8769
9588
  }
8770
9589
  }
8771
9590
 
9591
+ // dist/folder-refusals.js
9592
+ import { realpathSync as realpathSync2 } from "node:fs";
9593
+ import { homedir as homedir3 } from "node:os";
9594
+ function folderMissingRefusal(targetRepo) {
9595
+ return `halfcycle: no such directory: ${targetRepo}`;
9596
+ }
9597
+ function folderIsFileRefusal(targetRepo) {
9598
+ return `halfcycle: ${targetRepo} is a file \u2014 run "npx halfcycle" with a project folder instead.`;
9599
+ }
9600
+ function monorepoRefusal(targetRepo) {
9601
+ return `halfcycle: refusing to install into the Halfcycle monorepo itself (${targetRepo}).
9602
+ The bare form installs into the CURRENT directory, and this directory is the
9603
+ product's own source tree \u2014 installing here would write over tracked files.
9604
+ To see what this CLI does: halfcycle --help
9605
+ To install into a real project: halfcycle <path-to-that-repo>`;
9606
+ }
9607
+ function homeFolderRefusal(targetRepo) {
9608
+ return `halfcycle: refusing to set up in your home folder (${targetRepo}). Run "npx halfcycle" inside a project folder.`;
9609
+ }
9610
+ function isHomeFolder(dir) {
9611
+ try {
9612
+ const target = realpathSync2(dir);
9613
+ const home = realpathSync2(homedir3());
9614
+ return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
9615
+ } catch {
9616
+ return false;
9617
+ }
9618
+ }
9619
+
9620
+ // dist/preview.js
9621
+ import { accessSync, constants, existsSync as existsSync6, readFileSync as readFileSync9, statSync } from "node:fs";
9622
+ import { join as join9 } from "node:path";
9623
+
9624
+ // dist/halfcycle-monorepo.js
9625
+ import { readFileSync as readFileSync8 } from "node:fs";
9626
+ import { join as join8 } from "node:path";
9627
+ var MONOREPO_PACKAGE_NAME = "halfcycle-monorepo";
9628
+ function isHalfcycleMonorepo(dir) {
9629
+ try {
9630
+ const pkg = JSON.parse(readFileSync8(join8(dir, "package.json"), "utf-8"));
9631
+ return pkg.name === MONOREPO_PACKAGE_NAME;
9632
+ } catch {
9633
+ return false;
9634
+ }
9635
+ }
9636
+ function isSelfBuildInstallForm(engagementIdArg, engagementTypeArg) {
9637
+ return engagementIdArg !== void 0 && engagementTypeArg === "self-build";
9638
+ }
9639
+
9640
+ // dist/preview.js
9641
+ var CLAUDE_MD_REL = "CLAUDE.md";
9642
+ var AGENTS_IMPORT_LINE = "@AGENTS.md";
9643
+ var WORDS = {
9644
+ intro: "Halfcycle would set up this folder like this. Nothing has been written.",
9645
+ remove: "an older Halfcycle file this version no longer uses",
9646
+ claudeMdWillAdd: "already holds your own instructions; your first session will add one line to it, pointing to AGENTS.md",
9647
+ claudeMdAlreadyPoints: "already holds your own instructions, including the line pointing to AGENTS.md",
9648
+ signIn: " This project's sign-in, kept on this computer for you only, in your home folder's .halfcycle folder.",
9649
+ newProject: " A new project on Halfcycle's service for this folder, made when you sign in.",
9650
+ gitForWindows: " Git for Windows, which Claude Code needs: setting up will ask before installing it with winget.",
9651
+ firstSession: "Your first session will then write your project's own files as you answer its questions, starting with AGENTS.md and CLAUDE.md, which hold your project's own instructions.",
9652
+ toSetUp: `To set up for real, run: ${HALFCYCLE_COMMAND}`,
9653
+ planFailed: "halfcycle: setting up this folder would stop on a file that could not be read. Nothing has been written; fix that file, then run the preview again."
9654
+ };
9655
+ function runPreview(targetRepo, options) {
9656
+ const refusal2 = previewRefusal(targetRepo, options);
9657
+ if (refusal2 !== void 0)
9658
+ return { refusal: refusal2 };
9659
+ let plan;
9660
+ try {
9661
+ plan = planInstall({ targetRepo, env: options.env, home: options.home });
9662
+ } catch {
9663
+ return { refusal: WORDS.planFailed };
9664
+ }
9665
+ if (plan.refusal !== void 0)
9666
+ return { refusal: plan.refusal };
9667
+ return {
9668
+ text: renderPreview(plan, gitBashPath(options.env) === void 0 && options.platform === "win32", claudeMdLine(targetRepo))
9669
+ };
9670
+ }
9671
+ function claudeMdLine(targetRepo) {
9672
+ const path = join9(targetRepo, CLAUDE_MD_REL);
9673
+ if (!existsSync6(path))
9674
+ return void 0;
9675
+ let text;
9676
+ try {
9677
+ text = readFileSync9(path, "utf-8");
9678
+ } catch {
9679
+ return WORDS.claudeMdWillAdd;
9680
+ }
9681
+ return text.includes(AGENTS_IMPORT_LINE) ? WORDS.claudeMdAlreadyPoints : WORDS.claudeMdWillAdd;
9682
+ }
9683
+ function previewRefusal(targetRepo, options) {
9684
+ if (!existsSync6(targetRepo))
9685
+ return folderMissingRefusal(targetRepo);
9686
+ if (!statSync(targetRepo).isDirectory())
9687
+ return folderIsFileRefusal(targetRepo);
9688
+ if (isHalfcycleMonorepo(targetRepo))
9689
+ return monorepoRefusal(targetRepo);
9690
+ if (isHomeFolder(targetRepo))
9691
+ return homeFolderRefusal(targetRepo);
9692
+ try {
9693
+ refuseSystemFolder(targetRepo, options.platform);
9694
+ refuseInsideProject(targetRepo);
9695
+ } catch (err) {
9696
+ if (err instanceof PreCreateRefused)
9697
+ return err.message;
9698
+ throw err;
9699
+ }
9700
+ try {
9701
+ accessSync(targetRepo, constants.W_OK);
9702
+ } catch {
9703
+ return cannotWriteRefusal(targetRepo);
9704
+ }
9705
+ if (existsSync6(join9(targetRepo, PROJECT_IDENTITY_REL)) && readFolderId(targetRepo) === void 0) {
9706
+ return unreadableProjectRefusal(targetRepo);
9707
+ }
9708
+ try {
9709
+ checkSettingsParse(targetRepo);
9710
+ } catch (err) {
9711
+ if (err instanceof PreCreateRefused)
9712
+ return err.message;
9713
+ throw err;
9714
+ }
9715
+ return void 0;
9716
+ }
9717
+ function wordsOf(entry) {
9718
+ const words = PREVIEW_WORDS[entry.kind];
9719
+ switch (entry.action) {
9720
+ case "add":
9721
+ return words.add;
9722
+ case "change":
9723
+ return words.change;
9724
+ case "remove":
9725
+ return WORDS.remove;
9726
+ case "leave":
9727
+ return entry.yours === true ? words.yours : words.leave;
9728
+ }
9729
+ }
9730
+ var HEADINGS = [
9731
+ ["add", "Add:"],
9732
+ ["change", "Change:"],
9733
+ ["remove", "Remove:"],
9734
+ ["leave", "Leave alone:"]
9735
+ ];
9736
+ function outsideLine2(outside) {
9737
+ return outside.kind === "sign-in" ? WORDS.signIn : WORDS.newProject;
9738
+ }
9739
+ function renderPreview(plan, missingGitForWindows, claudeMdWords) {
9740
+ const lines = [WORDS.intro];
9741
+ for (const [action, heading] of HEADINGS) {
9742
+ const rows = plan.entries.filter((entry) => entry.action === action);
9743
+ const claudeMdRow = action === "leave" && claudeMdWords !== void 0;
9744
+ if (rows.length === 0 && !claudeMdRow)
9745
+ continue;
9746
+ lines.push("", heading);
9747
+ for (const entry of rows)
9748
+ lines.push(` ${entry.path} \u2014 ${wordsOf(entry)}`);
9749
+ if (claudeMdRow)
9750
+ lines.push(` ${CLAUDE_MD_REL} \u2014 ${claudeMdWords}`);
9751
+ }
9752
+ lines.push("", "Outside this folder:");
9753
+ for (const outside of plan.outside)
9754
+ lines.push(outsideLine2(outside));
9755
+ if (missingGitForWindows)
9756
+ lines.push(WORDS.gitForWindows);
9757
+ lines.push("", WORDS.firstSession, "", WORDS.toSetUp);
9758
+ return lines.join("\n");
9759
+ }
9760
+
9761
+ // dist/doctor.js
9762
+ import { spawnSync as spawnSync2 } from "node:child_process";
9763
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
9764
+ import { join as join10 } from "node:path";
9765
+ var GUARD_RUNNER_REL2 = ".claude/hooks/guard-runner.sh";
9766
+ var CONNECTION_VERB = "check-connection";
9767
+ var WRAPPER_TIMEOUT_MS = 3e4;
9768
+ var SIGNED_OUT_KINDS = ["not-signed-in", "other-account", "signed-out"];
9769
+ var NOT_CONNECTED_KINDS = ["update-claude-code", "cannot-reach", "service-problem"];
9770
+ var KNOWN_KINDS = ["connected", "not-registered", ...SIGNED_OUT_KINDS, ...NOT_CONNECTED_KINDS];
9771
+ function runDoctor(root, options) {
9772
+ const current = packageVersion();
9773
+ const installed = installedVersion(root);
9774
+ const upToDate = installed === current;
9775
+ const setUp = setUpLine(root, upToDate);
9776
+ let running = setUp.state === "no" ? { state: "not-run" } : startWrapper(root, options);
9777
+ if (setUp.state === "yes" && running.state === "answered" && running.kind === "not-registered") {
9778
+ running = failed(repairSentence());
9779
+ }
9780
+ const checksRunning = checksRunningLine(running);
9781
+ const connection = connectionLines(running);
9782
+ const version = upToDate ? { ok: true, text: `Up to date: yes (${current}).` } : { ok: false, text: versionNoLine(installed, current) };
9783
+ const lines = [setUp.line, connection.signedIn, connection.connected, checksRunning, version];
9784
+ const failing = lines.filter((line) => !line.ok).length;
9785
+ const closing = failing === 0 ? "Everything is working." : `${failing} of these need fixing; each line above says what to do.`;
9786
+ return {
9787
+ text: [...lines.map((line) => line.text), closing].join("\n"),
9788
+ exitCode: failing === 0 ? 0 : 1
9789
+ };
9790
+ }
9791
+ function setUpLine(root, upToDate) {
9792
+ if (!existsSync7(join10(root, BUNDLE_PIN_REL))) {
9793
+ return {
9794
+ state: "no",
9795
+ line: {
9796
+ ok: false,
9797
+ text: `Set up: no. Halfcycle is not set up in this folder. To set it up, run ${HALFCYCLE_COMMAND} here.`
9798
+ }
9799
+ };
9800
+ }
9801
+ const missing = [];
9802
+ if (!pinNamesProject(root))
9803
+ missing.push("Halfcycle's project record");
9804
+ if (!existsSync7(join10(root, INSTALL_MANIFEST_REL)))
9805
+ missing.push("the install record");
9806
+ if (!existsSync7(join10(root, VENDORED_BIN_REL)))
9807
+ missing.push("Halfcycle's check program");
9808
+ if (!mcpNamesServer(root))
9809
+ missing.push("Halfcycle's server entry");
9810
+ if (upToDate && !settingsRegisterEveryCommand(root))
9811
+ missing.push("the checks in your settings");
9812
+ if (missing.length === 0)
9813
+ return { state: "yes", line: { ok: true, text: "Set up: yes." } };
9814
+ return {
9815
+ state: "not-complete",
9816
+ line: {
9817
+ ok: false,
9818
+ text: `Set up: not complete. Missing: ${naturalList(missing)}. To put it back, run ${HALFCYCLE_COMMAND} here; nothing of yours is changed.`
9819
+ }
9820
+ };
9821
+ }
9822
+ function pinNamesProject(root) {
9823
+ try {
9824
+ const pin = readBundlePin(root);
9825
+ return typeof pin?.engagementId === "string" && pin.engagementId !== "";
9826
+ } catch {
9827
+ return false;
9828
+ }
9829
+ }
9830
+ function mcpNamesServer(root) {
9831
+ const parsed = readJson(join10(root, MCP_REGISTRATION_REL));
9832
+ if (!isObject(parsed) || !isObject(parsed["mcpServers"]))
9833
+ return false;
9834
+ const entry = parsed["mcpServers"][MCP_SERVER_KEY];
9835
+ return isObject(entry) && typeof entry["url"] === "string";
9836
+ }
9837
+ function settingsRegisterEveryCommand(root) {
9838
+ const parsed = readJson(join10(root, SETTINGS_REL));
9839
+ if (!isObject(parsed) || !isObject(parsed["hooks"]))
9840
+ return false;
9841
+ const registered = /* @__PURE__ */ new Set();
9842
+ for (const groups of Object.values(parsed["hooks"])) {
9843
+ if (!Array.isArray(groups))
9844
+ continue;
9845
+ for (const group of groups) {
9846
+ if (!isObject(group) || !Array.isArray(group["hooks"]))
9847
+ continue;
9848
+ for (const hook of group["hooks"]) {
9849
+ if (isObject(hook) && typeof hook["command"] === "string")
9850
+ registered.add(hook["command"]);
9851
+ }
9852
+ }
9853
+ }
9854
+ return HALFCYCLE_HOOK_WIRING.every((entry) => registered.has(entry.command));
9855
+ }
9856
+ function startWrapper(root, options) {
9857
+ const disabledIn = settingsDisablingHooks(root);
9858
+ if (disabledIn !== void 0) {
9859
+ return failed(`Checks running: no. Your settings turn off every check (${disabledIn}, "disableAllHooks"). Remove that line, then open a new session.`);
9860
+ }
9861
+ let shell = "bash";
9862
+ if (options.platform === "win32") {
9863
+ const gitBash = gitBashPath(options.env);
9864
+ if (gitBash === void 0) {
9865
+ return failed("Checks running: no. Claude Code needs Git for Windows to run Halfcycle's checks. Install it, then open a new session.");
9866
+ }
9867
+ shell = gitBash;
9868
+ }
9869
+ const wrapper = join10(root, GUARD_RUNNER_REL2);
9870
+ if (!existsSync7(wrapper))
9871
+ return failed(repairSentence());
9872
+ const run = spawnSync2(shell, [wrapper, CONNECTION_VERB], {
9873
+ cwd: root,
9874
+ env: { ...options.env, CLAUDE_PROJECT_DIR: root },
9875
+ encoding: "utf-8",
9876
+ timeout: WRAPPER_TIMEOUT_MS,
9877
+ windowsHide: true
9878
+ });
9879
+ if (run.error !== void 0 || run.status === null)
9880
+ return failed(repairSentence());
9881
+ if (run.status === 127) {
9882
+ return failed(`Checks running: no. ${NO_NODE_SENTENCE}`);
9883
+ }
9884
+ if (run.status !== 0) {
9885
+ if (String(run.stderr).includes("Halfcycle needs Node")) {
9886
+ return failed(`Checks running: no. Halfcycle's checks need Node ${NODE_FLOOR_MAJOR} or newer. Install it, then open a new session.`);
9887
+ }
9888
+ return failed(repairSentence());
9889
+ }
9890
+ const answer = parseAnswer(String(run.stdout));
9891
+ if (answer === void 0)
9892
+ return failed(repairSentence());
9893
+ return { state: "answered", ...answer };
9894
+ }
9895
+ function settingsDisablingHooks(root) {
9896
+ for (const rel of [SETTINGS_REL, LOCAL_SETTINGS_REL]) {
9897
+ const parsed = readJson(join10(root, rel));
9898
+ if (isObject(parsed) && parsed["disableAllHooks"] === true)
9899
+ return rel;
9900
+ }
9901
+ return void 0;
9902
+ }
9903
+ function parseAnswer(stdout) {
9904
+ const last = stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "").pop();
9905
+ if (last === void 0)
9906
+ return void 0;
9907
+ let parsed;
9908
+ try {
9909
+ parsed = JSON.parse(last);
9910
+ } catch {
9911
+ return void 0;
9912
+ }
9913
+ if (!isObject(parsed) || typeof parsed["kind"] !== "string" || !KNOWN_KINDS.includes(parsed["kind"]))
9914
+ return void 0;
9915
+ const sentence = parsed["sentence"];
9916
+ if (sentence !== null && typeof sentence !== "string")
9917
+ return void 0;
9918
+ if (parsed["kind"] !== "not-registered" && typeof sentence !== "string")
9919
+ return void 0;
9920
+ return { kind: parsed["kind"], sentence };
9921
+ }
9922
+ function checksRunningLine(running) {
9923
+ if (running.state === "not-run")
9924
+ return notChecked("Checks running", "Set up");
9925
+ if (running.state === "failed")
9926
+ return running.line;
9927
+ return { ok: true, text: "Checks running: yes." };
9928
+ }
9929
+ function failed(text) {
9930
+ return { state: "failed", line: { ok: false, text } };
9931
+ }
9932
+ function repairSentence() {
9933
+ return `Checks running: no. Halfcycle's check could not start. To repair it, run ${HALFCYCLE_COMMAND} here.`;
9934
+ }
9935
+ function connectionLines(running) {
9936
+ if (running.state !== "answered") {
9937
+ return { signedIn: notChecked("Signed in", "Checks running"), connected: notChecked("Connected", "Checks running") };
9938
+ }
9939
+ const { kind, sentence } = running;
9940
+ if (kind === "not-registered") {
9941
+ return { signedIn: notChecked("Signed in", "Set up"), connected: notChecked("Connected", "Set up") };
9942
+ }
9943
+ if (kind === "connected") {
9944
+ return { signedIn: { ok: true, text: "Signed in: yes." }, connected: { ok: true, text: "Connected: yes." } };
9945
+ }
9946
+ if (SIGNED_OUT_KINDS.includes(kind)) {
9947
+ return {
9948
+ signedIn: { ok: false, text: `Signed in: no. ${sentence ?? ""}`.trimEnd() },
9949
+ connected: notChecked("Connected", "Signed in")
9950
+ };
9951
+ }
9952
+ return {
9953
+ signedIn: { ok: true, text: "Signed in: yes." },
9954
+ connected: { ok: false, text: `Connected: no. ${sentence ?? ""}`.trimEnd() }
9955
+ };
9956
+ }
9957
+ function installedVersion(root) {
9958
+ try {
9959
+ return checkDrift(root).installed;
9960
+ } catch {
9961
+ return null;
9962
+ }
9963
+ }
9964
+ function versionNoLine(installed, current) {
9965
+ if (installed === null) {
9966
+ return `Up to date: no. This folder has no Halfcycle version recorded; the newest is ${current}. To set it up, run ${HALFCYCLE_COMMAND} here.`;
9967
+ }
9968
+ return `Up to date: no. This folder has Halfcycle ${installed}; the newest is ${current}. To update, run ${HALFCYCLE_COMMAND} here.`;
9969
+ }
9970
+ function notChecked(check, earlier) {
9971
+ return { ok: false, text: `${check}: not checked, because ${earlier} is not working yet.` };
9972
+ }
9973
+ function naturalList(items) {
9974
+ if (items.length <= 1)
9975
+ return items.join("");
9976
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
9977
+ }
9978
+ function readJson(path) {
9979
+ if (!existsSync7(path))
9980
+ return void 0;
9981
+ try {
9982
+ return JSON.parse(readFileSync10(path, "utf-8"));
9983
+ } catch {
9984
+ return void 0;
9985
+ }
9986
+ }
9987
+ function isObject(value) {
9988
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9989
+ }
9990
+
8772
9991
  // dist/claude-plan.js
8773
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
9992
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
8774
9993
  import { platform as platform4 } from "node:os";
8775
- import { dirname as dirname5, join as join7 } from "node:path";
9994
+ import { dirname as dirname5, join as join11 } from "node:path";
8776
9995
  var CLAUDE_PLAN_FILENAME = "claude-plan.json";
8777
9996
  var PLAN_QUESTION_TRIES = 3;
8778
9997
  var SEND_TIMEOUT_MS = 5e3;
8779
9998
  var SETTINGS_PAGE_SEGMENT = "settings";
8780
9999
  function claudePlanPath(home) {
8781
- return join7(halfcycleHome(home), CLAUDE_PLAN_FILENAME);
10000
+ return join11(halfcycleHome(home), CLAUDE_PLAN_FILENAME);
8782
10001
  }
8783
10002
  function readRememberedClaudePlan(home) {
8784
10003
  try {
8785
- const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync7(claudePlanPath(home), "utf-8")));
10004
+ const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync11(claudePlanPath(home), "utf-8")));
8786
10005
  return parsed.success ? parsed.data.claudePlan : void 0;
8787
10006
  } catch {
8788
10007
  return void 0;
@@ -8853,6 +10072,8 @@ async function setupClaudePlan(input, deps) {
8853
10072
  }
8854
10073
  return { kind: "remembered", plan: remembered, sent };
8855
10074
  }
10075
+ if (deps.assumeYes === true)
10076
+ return { kind: "no-person" };
8856
10077
  let plan = null;
8857
10078
  for (let tries = 0; tries < PLAN_QUESTION_TRIES && plan === null; tries++) {
8858
10079
  plan = claudePlanFromAnswer(await deps.ask(`${CLAUDE_PLAN_QUESTION} `));
@@ -8866,22 +10087,6 @@ async function setupClaudePlan(input, deps) {
8866
10087
  }
8867
10088
  }
8868
10089
 
8869
- // dist/halfcycle-monorepo.js
8870
- import { readFileSync as readFileSync8 } from "node:fs";
8871
- import { join as join8 } from "node:path";
8872
- var MONOREPO_PACKAGE_NAME = "halfcycle-monorepo";
8873
- function isHalfcycleMonorepo(dir) {
8874
- try {
8875
- const pkg = JSON.parse(readFileSync8(join8(dir, "package.json"), "utf-8"));
8876
- return pkg.name === MONOREPO_PACKAGE_NAME;
8877
- } catch {
8878
- return false;
8879
- }
8880
- }
8881
- function isSelfBuildInstallForm(engagementIdArg, engagementTypeArg) {
8882
- return engagementIdArg !== void 0 && engagementTypeArg === "self-build";
8883
- }
8884
-
8885
10090
  // dist/probe-mcp.js
8886
10091
  var PROBE_TIMEOUT_MS = 5e3;
8887
10092
  async function probeMcpOrigin(mcpUrl, token) {
@@ -8936,8 +10141,8 @@ async function probeMcpOrigin(mcpUrl, token) {
8936
10141
 
8937
10142
  // dist/claude-version-probe.js
8938
10143
  import { execFileSync as execFileSync3 } from "node:child_process";
8939
- import { existsSync as existsSync5, realpathSync as realpathSync2 } from "node:fs";
8940
- import { delimiter as delimiter2, join as join9 } from "node:path";
10144
+ import { existsSync as existsSync8, realpathSync as realpathSync3 } from "node:fs";
10145
+ import { delimiter as delimiter2, join as join12 } from "node:path";
8941
10146
  function probeClaudeCodeVersion(timeoutMs, deps = {}) {
8942
10147
  const platform6 = deps.platform ?? process.platform;
8943
10148
  const exec = deps.exec ?? execFileSync3;
@@ -8974,10 +10179,10 @@ var INSTALL_METHOD_RULES = [
8974
10179
  function probeClaudeCodeInstallMethod(deps = {}) {
8975
10180
  const env = deps.env ?? process.env;
8976
10181
  const platform6 = deps.platform ?? process.platform;
8977
- const exists = deps.exists ?? existsSync5;
10182
+ const exists = deps.exists ?? existsSync8;
8978
10183
  const realpath = deps.realpath ?? ((file) => {
8979
10184
  try {
8980
- return realpathSync2(file);
10185
+ return realpathSync3(file);
8981
10186
  } catch {
8982
10187
  return file;
8983
10188
  }
@@ -8986,7 +10191,7 @@ function probeClaudeCodeInstallMethod(deps = {}) {
8986
10191
  const dirs = (env["PATH"] ?? "").split(delimiter2).filter((dir) => dir !== "");
8987
10192
  for (const dir of dirs) {
8988
10193
  for (const name of names) {
8989
- const file = join9(dir, name);
10194
+ const file = join12(dir, name);
8990
10195
  if (!exists(file))
8991
10196
  continue;
8992
10197
  const real = realpath(file);
@@ -8997,8 +10202,8 @@ function probeClaudeCodeInstallMethod(deps = {}) {
8997
10202
  }
8998
10203
 
8999
10204
  // dist/build-record/write.js
9000
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "node:fs";
9001
- import { basename as basename2, join as join10, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
10205
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "node:fs";
10206
+ import { basename as basename2, join as join13, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
9002
10207
 
9003
10208
  // dist/build-record/render-v2.js
9004
10209
  var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
@@ -9390,10 +10595,14 @@ function renderClose(heading, observed, attested, checks, p, closingRound = fals
9390
10595
  ];
9391
10596
  }
9392
10597
  var REVIEW_LABEL = "**Recorded by Halfcycle from the owner's review.** Each part the review held, and how the project's owner decided it. A line marked as the agent's is the agent's own answer, not Halfcycle's.";
10598
+ var REVIEW_LABEL_WITH_AUTONOMY = `**Recorded by Halfcycle from the owner's review.** Each part the review held, and how it was decided: by the project's owner, unless the part says it was ${DECIDED_BY_AUTONOMY_SENTENCE}. A line marked as the agent's is the agent's own answer, not Halfcycle's.`;
9393
10599
  var REVIEW_NO_PARTS = "The review had no parts, so it closed on one accept that names no part.";
9394
10600
  function approvalLine(approval) {
9395
10601
  if (approval.kind === "none")
9396
10602
  return "Built without an approved scope.";
10603
+ if (approval.givenIn === "autonomy") {
10604
+ return `Plan version ${String(approval.planVersion)} was ${APPROVED_IN_ADVANCE_SENTENCE}.`;
10605
+ }
9397
10606
  const where = approval.givenIn === "console" ? "in the console" : "in their own session";
9398
10607
  return `Scope approved by the project's owner, plan version ${String(approval.planVersion)}, ${where}.`;
9399
10608
  }
@@ -9407,8 +10616,11 @@ function decisionText(part, p) {
9407
10616
  return `Dropped: ${p.text(part.reason ?? "")}`;
9408
10617
  }
9409
10618
  }
10619
+ var DECIDED_BY_AUTONOMY_LINE = `${DECIDED_BY_AUTONOMY_SENTENCE.charAt(0).toUpperCase()}${DECIDED_BY_AUTONOMY_SENTENCE.slice(1)}.`;
9410
10620
  function reviewPartLines(part, p) {
9411
10621
  const lines = [`- ${p.itemStart(part.name)} \u2014 ${decisionText(part, p)}`];
10622
+ if (part.givenIn === "autonomy")
10623
+ lines.push(` - ${DECIDED_BY_AUTONOMY_LINE}`);
9412
10624
  const code = codeLinkLine(part.codeLink);
9413
10625
  if (code !== void 0)
9414
10626
  lines.push(` - ${p.text(code)}`);
@@ -9428,7 +10640,8 @@ function removalsLine(record2, p) {
9428
10640
  }
9429
10641
  function renderReview(record2, p) {
9430
10642
  const review = record2.review;
9431
- const blocks = ["## The owner's review", REVIEW_LABEL, approvalLine(review.approval)];
10643
+ const label = review.parts.some((part) => part.givenIn === "autonomy") ? REVIEW_LABEL_WITH_AUTONOMY : REVIEW_LABEL;
10644
+ const blocks = ["## The owner's review", label, approvalLine(review.approval)];
9432
10645
  blocks.push(review.parts.length === 0 ? REVIEW_NO_PARTS : review.parts.flatMap((part) => reviewPartLines(part, p)).join("\n"));
9433
10646
  const removals = removalsLine(record2, p);
9434
10647
  if (removals !== null)
@@ -9440,10 +10653,9 @@ function renderCalls(calls, p) {
9440
10653
  return [`## ${CALLS_HEADING}`, NO_CALLS_LINE];
9441
10654
  const lines = calls.flatMap((c) => {
9442
10655
  const by = p.itemStart(c.by);
9443
- return [
9444
- `- ${callMadeLine({ by, question: p.text(c.question), chose: p.text(c.chose), statedDefault: p.text(c.statedDefault) })}`,
9445
- ` - ${callUndoLine({ by: p.text(c.by), undoable: c.undoable, how: c.how === null ? null : p.text(c.how) })}`
9446
- ];
10656
+ const undo = ` - ${callUndoLine({ by: p.text(c.by), undoable: c.undoable, how: c.how === null ? null : p.text(c.how) })}`;
10657
+ const head = c.kind === "listed" ? autonomyC8(by, p.text(c.question), p.text(c.chose)) : callMadeLine({ by, question: p.text(c.question), chose: p.text(c.chose), statedDefault: p.text(c.statedDefault) });
10658
+ return [`- ${head}`, undo];
9447
10659
  });
9448
10660
  return [`## ${CALLS_HEADING}`, CALLS_EXPLAINED, lines.join("\n")];
9449
10661
  }
@@ -9543,7 +10755,7 @@ function renderBuildRecordV2Markdown(record2) {
9543
10755
  }
9544
10756
 
9545
10757
  // dist/build-record/pair-io.js
9546
- import { closeSync, openSync, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
10758
+ import { closeSync, openSync, readFileSync as readFileSync12, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
9547
10759
  function createExclusiveOnDisk(path, data) {
9548
10760
  const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
9549
10761
  const fd = openSync(path, "wx");
@@ -9574,7 +10786,7 @@ function isEexist(err) {
9574
10786
  }
9575
10787
  function readIfPresent(path) {
9576
10788
  try {
9577
- return readFileSync9(path, "utf-8");
10789
+ return readFileSync12(path, "utf-8");
9578
10790
  } catch {
9579
10791
  return null;
9580
10792
  }
@@ -9606,7 +10818,7 @@ function replaceOne(io, tmp, finalPath, text) {
9606
10818
  }
9607
10819
 
9608
10820
  // dist/build-record/write.js
9609
- var BUILD_RECORD_DIR = join10("docs", "build-records");
10821
+ var BUILD_RECORD_DIR = join13("docs", "build-records");
9610
10822
  function assertInsideOutDir(outDir, candidate, identity) {
9611
10823
  const rel = relative2(outDir, resolve3(candidate));
9612
10824
  const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute3(rel) && rel === basename2(rel);
@@ -9644,8 +10856,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
9644
10856
  }
9645
10857
  const outDir = resolve3(repoRoot, BUILD_RECORD_DIR);
9646
10858
  const segment = renderPhaseSegment(phase);
9647
- const jsonPath = join10(outDir, `${segment}.json`);
9648
- const mdPath = join10(outDir, `${segment}.md`);
10859
+ const jsonPath = join13(outDir, `${segment}.json`);
10860
+ const mdPath = join13(outDir, `${segment}.md`);
9649
10861
  const jsonTmp = `${jsonPath}.tmp`;
9650
10862
  const mdTmp = `${mdPath}.tmp`;
9651
10863
  for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
@@ -9659,7 +10871,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
9659
10871
  }
9660
10872
  const existingJson = readIfPresent(jsonPath);
9661
10873
  if (existingJson === null) {
9662
- return { kind: "in-the-way", path: existsSync6(jsonPath) ? jsonPath : mdPath };
10874
+ return { kind: "in-the-way", path: existsSync9(jsonPath) ? jsonPath : mdPath };
9663
10875
  }
9664
10876
  if (existingJson === jsonText) {
9665
10877
  replaceOne(io, mdTmp, mdPath, mdText);
@@ -9686,8 +10898,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
9686
10898
  import { relative as relative3 } from "node:path";
9687
10899
 
9688
10900
  // dist/open-phase.js
9689
- import { readFileSync as readFileSync10 } from "node:fs";
9690
- import { join as join11 } from "node:path";
10901
+ import { readFileSync as readFileSync13 } from "node:fs";
10902
+ import { join as join14 } from "node:path";
9691
10903
 
9692
10904
  // dist/close-phase.js
9693
10905
  import { execFile, execFileSync as execFileSync4 } from "node:child_process";
@@ -9771,7 +10983,7 @@ function readObserveLine(stdout) {
9771
10983
  function runRunnerChild(binary, args2, credential, repoRoot, readLine, fallback) {
9772
10984
  if (binary instanceof Error)
9773
10985
  return Promise.resolve(fallback);
9774
- return new Promise((resolve7) => {
10986
+ return new Promise((resolve8) => {
9775
10987
  try {
9776
10988
  execFile(process.execPath, [binary, ...args2], {
9777
10989
  cwd: repoRoot,
@@ -9781,10 +10993,10 @@ function runRunnerChild(binary, args2, credential, repoRoot, readLine, fallback)
9781
10993
  windowsHide: true
9782
10994
  }, (err, stdout) => {
9783
10995
  const stopped = err !== null && err.killed === true;
9784
- resolve7(stopped || typeof stdout !== "string" ? fallback : readLine(stdout));
10996
+ resolve8(stopped || typeof stdout !== "string" ? fallback : readLine(stdout));
9785
10997
  });
9786
10998
  } catch {
9787
- resolve7(fallback);
10999
+ resolve8(fallback);
9788
11000
  }
9789
11001
  });
9790
11002
  }
@@ -9997,6 +11209,14 @@ function closeStepReport(phase, attempt, engagementId) {
9997
11209
  case "unavailable":
9998
11210
  stderr.push(`${REFUSED} the Halfcycle service could not open the review just now. Nothing was sent to the owner. Run this again in a few minutes.`);
9999
11211
  break;
11212
+ case "closed-by-autonomy":
11213
+ stdout.push(autonomyA13(answer.accepted, answer.carried));
11214
+ exitCode = 0;
11215
+ break;
11216
+ case "autonomy-waits":
11217
+ stdout.push(answer.reason === "unchecked-claims" ? autonomyA14(answer.count, answer.parts.map((p) => p.name)) : autonomyA15(answer.count));
11218
+ exitCode = 0;
11219
+ break;
10000
11220
  default:
10001
11221
  return assertNever2(answer);
10002
11222
  }
@@ -10168,7 +11388,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
10168
11388
  if (pinned !== null)
10169
11389
  assertEngagementId(pinned);
10170
11390
  const fromStore = pinned === null ? null : readEngagementEnv(pinned, home);
10171
- const looked = pinned === null ? `${join11(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
11391
+ const looked = pinned === null ? `${join14(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
10172
11392
  const values = {};
10173
11393
  for (const key of OPEN_PHASE_ENV_KEYS) {
10174
11394
  const value = (env[key] ?? fromStore?.[key] ?? "").trim();
@@ -10185,7 +11405,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
10185
11405
  }
10186
11406
  function readPinnedEngagementId(repoRoot) {
10187
11407
  try {
10188
- const pin = JSON.parse(readFileSync10(join11(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
11408
+ const pin = JSON.parse(readFileSync13(join14(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
10189
11409
  const id = pin[PIN_ENGAGEMENT_ID_FIELD];
10190
11410
  return typeof id === "string" && id !== "" ? id : null;
10191
11411
  } catch {
@@ -10404,12 +11624,12 @@ async function readPhaseWork(credential, phase) {
10404
11624
 
10405
11625
  // dist/system-report.js
10406
11626
  import { execFileSync as execFileSync5 } from "node:child_process";
10407
- import { readFileSync as readFileSync11 } from "node:fs";
10408
- import { join as join12 } from "node:path";
11627
+ import { readFileSync as readFileSync14 } from "node:fs";
11628
+ import { join as join15 } from "node:path";
10409
11629
  function readDescriptionFile(repoRoot) {
10410
11630
  let text;
10411
11631
  try {
10412
- text = readFileSync11(join12(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
11632
+ text = readFileSync14(join15(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
10413
11633
  } catch (err) {
10414
11634
  const code = err.code;
10415
11635
  return code === "ENOENT" || code === "ENOTDIR" ? { kind: "missing" } : { kind: "unreadable" };
@@ -10423,18 +11643,18 @@ function readDescriptionFile(repoRoot) {
10423
11643
  function problemPlace(raw, path) {
10424
11644
  if (path === "")
10425
11645
  return SYSTEM_DESCRIPTION_REL;
10426
- const [list, index] = path.split(".");
11646
+ const [list2, index] = path.split(".");
10427
11647
  const position = Number(index);
10428
11648
  if (!Number.isInteger(position) || position < 0)
10429
11649
  return path;
10430
- const items = typeof raw === "object" && raw !== null ? raw[list ?? ""] : void 0;
11650
+ const items = typeof raw === "object" && raw !== null ? raw[list2 ?? ""] : void 0;
10431
11651
  const item = Array.isArray(items) ? items[position] : void 0;
10432
11652
  const fields = typeof item === "object" && item !== null ? item : {};
10433
- if (list === "parts") {
11653
+ if (list2 === "parts") {
10434
11654
  const id = fields["id"];
10435
11655
  return typeof id === "string" && id !== "" ? `${path} (part "${id}")` : `${path} (part ${position + 1})`;
10436
11656
  }
10437
- if (list === "connections") {
11657
+ if (list2 === "connections") {
10438
11658
  const from = fields["from"];
10439
11659
  const to = fields["to"];
10440
11660
  return typeof from === "string" && typeof to === "string" ? `${path} (connection "${from}" to "${to}")` : `${path} (connection ${position + 1})`;
@@ -10633,12 +11853,12 @@ async function runSystem(repoRoot, deps) {
10633
11853
  }
10634
11854
 
10635
11855
  // dist/plan.js
10636
- import { existsSync as existsSync8, readFileSync as readFileSync14 } from "node:fs";
10637
- import { join as join17 } from "node:path";
11856
+ import { existsSync as existsSync11, readFileSync as readFileSync17 } from "node:fs";
11857
+ import { join as join20 } from "node:path";
10638
11858
 
10639
11859
  // dist/draw/write.js
10640
- import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync12, readSync, realpathSync as realpathSync3, rmSync as rmSync5 } from "node:fs";
10641
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join13, relative as relative4, resolve as resolve4, sep } from "node:path";
11860
+ import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync15, readSync, realpathSync as realpathSync4, rmSync as rmSync5 } from "node:fs";
11861
+ import { dirname as dirname6, isAbsolute as isAbsolute4, join as join16, relative as relative4, resolve as resolve4, sep } from "node:path";
10642
11862
  var DOCS_DIR = "docs";
10643
11863
  var DIAGRAMS_DIR = "diagrams";
10644
11864
  var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
@@ -10694,9 +11914,9 @@ function embedImageLink(base, doc) {
10694
11914
  }
10695
11915
  function diagramPaths(repoRoot, base) {
10696
11916
  const root = resolve4(repoRoot);
10697
- const dir = join13(root, DOCS_DIR, DIAGRAMS_DIR);
11917
+ const dir = join16(root, DOCS_DIR, DIAGRAMS_DIR);
10698
11918
  const names = fileNames(base);
10699
- const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join13(dir, names[which])]));
11919
+ const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join16(dir, names[which])]));
10700
11920
  return { repoRoot: root, dir, file };
10701
11921
  }
10702
11922
  function diagramRel(base, which) {
@@ -10725,7 +11945,7 @@ function isInside(outer, inner) {
10725
11945
  function checkDiagramPlace(paths) {
10726
11946
  let root;
10727
11947
  try {
10728
- root = realpathSync3(paths.repoRoot);
11948
+ root = realpathSync4(paths.repoRoot);
10729
11949
  } catch {
10730
11950
  return { ok: false, why: `the folder ${paths.repoRoot} could not be opened. Check the path, then run this again.` };
10731
11951
  }
@@ -10738,7 +11958,7 @@ function checkDiagramPlace(paths) {
10738
11958
  }
10739
11959
  let real;
10740
11960
  try {
10741
- real = realpathSync3(ancestor);
11961
+ real = realpathSync4(ancestor);
10742
11962
  } catch {
10743
11963
  return {
10744
11964
  ok: false,
@@ -10791,7 +12011,7 @@ function readExistingSet(paths) {
10791
12011
  if (!stat.isFile())
10792
12012
  return { ok: false, inTheWay: which, temp: false };
10793
12013
  try {
10794
- found[which] = readFileSync12(paths.file[which]);
12014
+ found[which] = readFileSync15(paths.file[which]);
10795
12015
  } catch {
10796
12016
  return { ok: false, inTheWay: which, temp: false };
10797
12017
  }
@@ -10883,9 +12103,9 @@ function sameBytes(a, b) {
10883
12103
  }
10884
12104
  function writeDiagramSet(repoRoot, base, set, io = NODE_RECORD_PAIR_IO) {
10885
12105
  const paths = diagramPaths(repoRoot, base);
10886
- const place = checkDiagramPlace(paths);
10887
- if (!place.ok)
10888
- return { kind: "unsafe", why: place.why };
12106
+ const place2 = checkDiagramPlace(paths);
12107
+ if (!place2.ok)
12108
+ return { kind: "unsafe", why: place2.why };
10889
12109
  const read = readExistingSet(paths);
10890
12110
  if (!read.ok)
10891
12111
  return { kind: "in-the-way", file: read.inTheWay, temp: read.temp };
@@ -10955,10 +12175,10 @@ function restore(io, paths, renamed, existing) {
10955
12175
  }
10956
12176
 
10957
12177
  // dist/change.js
10958
- import { join as join15 } from "node:path";
12178
+ import { join as join18 } from "node:path";
10959
12179
 
10960
12180
  // dist/cli-contract.js
10961
- var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "uninstall", "change", "pace", "prove"];
12181
+ var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "uninstall", "change", "pace", "prove", "preview", "doctor", "restore"];
10962
12182
  var CONTRACTS = [
10963
12183
  { verb: "install", required: [], conditional: [] },
10964
12184
  { verb: "check-drift", required: [], conditional: [] },
@@ -10971,6 +12191,9 @@ var CONTRACTS = [
10971
12191
  { verb: "uninstall", required: [], conditional: [] },
10972
12192
  { verb: "pace", required: [], conditional: [] },
10973
12193
  { verb: "prove", required: [], conditional: [] },
12194
+ { verb: "preview", required: [], conditional: [] },
12195
+ { verb: "doctor", required: [], conditional: [] },
12196
+ { verb: "restore", required: [], conditional: [] },
10974
12197
  {
10975
12198
  verb: "open-phase",
10976
12199
  required: [
@@ -11149,8 +12372,8 @@ function placeholderRefusal(verb, tokens) {
11149
12372
  }
11150
12373
 
11151
12374
  // dist/build-record/write-change.js
11152
- import { existsSync as existsSync7, mkdirSync as mkdirSync9 } from "node:fs";
11153
- import { basename as basename3, isAbsolute as isAbsolute5, join as join14, relative as relative5, resolve as resolve5 } from "node:path";
12375
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
12376
+ import { basename as basename3, isAbsolute as isAbsolute5, join as join17, relative as relative5, resolve as resolve5 } from "node:path";
11154
12377
 
11155
12378
  // dist/build-record/render-change.js
11156
12379
  function counted2(n, one, many) {
@@ -11295,7 +12518,7 @@ function renderChangeRecordMarkdown(record2) {
11295
12518
  }
11296
12519
 
11297
12520
  // dist/build-record/write-change.js
11298
- var CHANGE_RECORD_DIR = join14("docs", "build-records", "changes");
12521
+ var CHANGE_RECORD_DIR = join17("docs", "build-records", "changes");
11299
12522
  var InvalidChangeIdError = class extends Error {
11300
12523
  id;
11301
12524
  constructor(id) {
@@ -11315,8 +12538,8 @@ function writeChangeRecordPair(repoRoot, sortingId, rawRecord, record2, io = NOD
11315
12538
  if (!checked.success)
11316
12539
  throw new InvalidChangeIdError(sortingId);
11317
12540
  const outDir = resolve5(repoRoot, CHANGE_RECORD_DIR);
11318
- const jsonPath = join14(outDir, `${checked.data}.json`);
11319
- const mdPath = join14(outDir, `${checked.data}.md`);
12541
+ const jsonPath = join17(outDir, `${checked.data}.json`);
12542
+ const mdPath = join17(outDir, `${checked.data}.md`);
11320
12543
  const mdTmp = `${mdPath}.tmp`;
11321
12544
  for (const candidate of [jsonPath, mdPath, mdTmp]) {
11322
12545
  assertInsideChangeDir(outDir, candidate, sortingId);
@@ -11329,7 +12552,7 @@ function writeChangeRecordPair(repoRoot, sortingId, rawRecord, record2, io = NOD
11329
12552
  }
11330
12553
  const existingJson = readIfPresent(jsonPath);
11331
12554
  if (existingJson === null) {
11332
- return { kind: "in-the-way", path: existsSync7(jsonPath) ? jsonPath : mdPath };
12555
+ return { kind: "in-the-way", path: existsSync10(jsonPath) ? jsonPath : mdPath };
11333
12556
  }
11334
12557
  if (existingJson !== jsonText)
11335
12558
  return { kind: "in-the-way", path: jsonPath };
@@ -11592,7 +12815,7 @@ function writePairLines(deps, repoRoot, sortingId, found) {
11592
12815
  };
11593
12816
  }
11594
12817
  return {
11595
- stdout: `[halfcycle] Its record is in ${join15(CHANGE_RECORD_DIR, `${sortingId}.md`)}.
12818
+ stdout: `[halfcycle] Its record is in ${join18(CHANGE_RECORD_DIR, `${sortingId}.md`)}.
11596
12819
  `,
11597
12820
  stderr: "",
11598
12821
  ok: true
@@ -11831,24 +13054,24 @@ function runChange(repoRoot, args2, deps) {
11831
13054
  }
11832
13055
 
11833
13056
  // dist/shown-record.js
11834
- import { chmodSync as chmodSync4, mkdirSync as mkdirSync10, readFileSync as readFileSync13, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
13057
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync10, readFileSync as readFileSync16, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
11835
13058
  import { platform as platform5 } from "node:os";
11836
- import { join as join16 } from "node:path";
13059
+ import { join as join19 } from "node:path";
11837
13060
  function updateShownRecord(engagementId, update, home) {
11838
13061
  let temp = null;
11839
13062
  try {
11840
13063
  const dir = engagementStateDir(engagementId, home);
11841
- const path = join16(dir, SHOWN_PLANS_FILENAME);
13064
+ const path = join19(dir, SHOWN_PLANS_FILENAME);
11842
13065
  let current;
11843
13066
  try {
11844
- current = parseShownRecord(readFileSync13(path, "utf-8"));
13067
+ current = parseShownRecord(readFileSync16(path, "utf-8"));
11845
13068
  } catch {
11846
13069
  current = { entries: [], reviews: [] };
11847
13070
  }
11848
13071
  const next = update(current);
11849
13072
  const body = serialiseShownRecord(next);
11850
13073
  mkdirSync10(dir, { recursive: true, mode: 448 });
11851
- temp = join16(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
13074
+ temp = join19(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
11852
13075
  writeFileSync7(temp, body, { mode: 384 });
11853
13076
  if (platform5() !== "win32")
11854
13077
  chmodSync4(temp, 384);
@@ -11907,9 +13130,9 @@ function parsePlanArgs(argv) {
11907
13130
  function planProblemPlace(raw, fileRel, path) {
11908
13131
  if (path === "")
11909
13132
  return fileRel;
11910
- const [list, index] = path.split(".");
13133
+ const [list2, index] = path.split(".");
11911
13134
  const position = Number(index);
11912
- if (list !== "changes" || !Number.isInteger(position) || position < 0)
13135
+ if (list2 !== "changes" || !Number.isInteger(position) || position < 0)
11913
13136
  return path;
11914
13137
  const changes = typeof raw === "object" && raw !== null ? raw["changes"] : void 0;
11915
13138
  const item = Array.isArray(changes) ? changes[position] : void 0;
@@ -11925,7 +13148,7 @@ function readAndCheckPlan(repoRoot, phase, description, verb) {
11925
13148
  const fileRel = planFileRel(phase);
11926
13149
  let text;
11927
13150
  try {
11928
- text = readFileSync14(join17(repoRoot, fileRel), "utf-8");
13151
+ text = readFileSync17(join20(repoRoot, fileRel), "utf-8");
11929
13152
  } catch (err) {
11930
13153
  const code = err.code;
11931
13154
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -12017,7 +13240,8 @@ async function reportPhasePlan(credential, plan, descriptionVersion) {
12017
13240
  changedParts: answer.changedParts,
12018
13241
  typedApprovalOffered: answer.typedApprovalOffered,
12019
13242
  carriedFrom: answer.carriedFrom,
12020
- movedUp: answer.movedUp
13243
+ movedUp: answer.movedUp,
13244
+ givenIn: answer.givenIn
12021
13245
  };
12022
13246
  case "refused":
12023
13247
  return { kind: "refused", problems: answer.problems };
@@ -12080,7 +13304,7 @@ function removalOwedLines(verb, parts) {
12080
13304
  function redrawLine(repoRoot, phase) {
12081
13305
  let exists;
12082
13306
  try {
12083
- exists = existsSync8(join17(repoRoot, DIAGRAM_DIR_REL, embedImageName(phaseDiagramBase(phase))));
13307
+ exists = existsSync11(join20(repoRoot, DIAGRAM_DIR_REL, embedImageName(phaseDiagramBase(phase))));
12084
13308
  } catch {
12085
13309
  return null;
12086
13310
  }
@@ -12094,14 +13318,14 @@ function movedUpLines(movedUp) {
12094
13318
  ${nextSayLine(movedUp.next.say)}
12095
13319
  `;
12096
13320
  }
12097
- function planStateSentence(state) {
13321
+ function planStateSentence(state, givenIn) {
12098
13322
  switch (state) {
12099
13323
  case "waiting":
12100
13324
  return "Waiting for the owner's approval.";
12101
13325
  case "approved":
12102
- return "The owner has approved this plan.";
13326
+ return givenIn === "autonomy" ? `The plan was ${APPROVED_IN_ADVANCE_SENTENCE}.` : "The owner has approved this plan.";
12103
13327
  case "partly-stale":
12104
- return "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
13328
+ return givenIn === "autonomy" ? `The plan was ${APPROVED_IN_ADVANCE_SENTENCE}; the parts marked as changed wait for the owner's approval.` : "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
12105
13329
  case "sent-back":
12106
13330
  return "The owner sent this plan back. Change it to answer the owner's notes, then run this again.";
12107
13331
  }
@@ -12172,7 +13396,7 @@ async function runPlan(repoRoot, phase, show, deps) {
12172
13396
  }
12173
13397
  const shownPhase = String(plan.phase);
12174
13398
  const recorded = `[halfcycle] Plan for "${shownPhase}" recorded: plan version ${outcome.version}.`;
12175
- const stateSentence = planStateSentence(outcome.state);
13399
+ const stateSentence = planStateSentence(outcome.state, outcome.givenIn);
12176
13400
  const redraw = redrawLine(repoRoot, phase) ?? "";
12177
13401
  const moved = outcome.movedUp === void 0 ? "" : `${movedUpLines(outcome.movedUp)}`;
12178
13402
  if (!show)
@@ -12329,9 +13553,9 @@ Paste this into ${where}:
12329
13553
  }
12330
13554
  function localRefusal(repoRoot, base) {
12331
13555
  const paths = diagramPaths(repoRoot, base);
12332
- const place = checkDiagramPlace(paths);
12333
- if (!place.ok)
12334
- return `${P} ${place.why}
13556
+ const place2 = checkDiagramPlace(paths);
13557
+ if (!place2.ok)
13558
+ return `${P} ${place2.why}
12335
13559
  `;
12336
13560
  const read = readExistingSet(paths);
12337
13561
  if (!read.ok)
@@ -12478,8 +13702,8 @@ async function writeDrawOutcome(repoRoot, deps, outcome, target, warnings, canno
12478
13702
  }
12479
13703
 
12480
13704
  // dist/check-tasks.js
12481
- import { readFileSync as readFileSync15 } from "node:fs";
12482
- import { join as join18 } from "node:path";
13705
+ import { readFileSync as readFileSync18 } from "node:fs";
13706
+ import { join as join21 } from "node:path";
12483
13707
  var CHECK_TASKS_USAGE = `${HALFCYCLE_COMMAND} check-tasks <phase> [--with-task-list] [--repo <root>]`;
12484
13708
  function parseCheckTasksArgs(argv) {
12485
13709
  const positionals = [];
@@ -12515,7 +13739,7 @@ function issuePlace(rel, path) {
12515
13739
  function readJsonFile(repoRoot, rel, parse) {
12516
13740
  let text;
12517
13741
  try {
12518
- text = readFileSync15(join18(repoRoot, rel), "utf-8");
13742
+ text = readFileSync18(join21(repoRoot, rel), "utf-8");
12519
13743
  } catch (err) {
12520
13744
  const code = err.code;
12521
13745
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -12593,13 +13817,13 @@ function splitProveInvocation(words) {
12593
13817
  function runProveChild(binary, credential, repoRoot, words, deps = {}) {
12594
13818
  const out = deps.stdout ?? process.stdout;
12595
13819
  const err = deps.stderr ?? process.stderr;
12596
- return new Promise((resolve7) => {
13820
+ return new Promise((resolve8) => {
12597
13821
  let settled = false;
12598
13822
  const finish = (code) => {
12599
13823
  if (settled)
12600
13824
  return;
12601
13825
  settled = true;
12602
- resolve7(code);
13826
+ resolve8(code);
12603
13827
  };
12604
13828
  let child;
12605
13829
  try {
@@ -12681,6 +13905,17 @@ function sentBackInRound(part, round) {
12681
13905
  return part.sentBack.some((s) => s.round === round);
12682
13906
  }
12683
13907
  function partStatus(part, round) {
13908
+ const settled = settledStatus(part);
13909
+ if (settled !== void 0) {
13910
+ return part.givenIn === "autonomy" ? `${settled}, ${DECIDED_BY_AUTONOMY_SENTENCE}` : settled;
13911
+ }
13912
+ if (awaitingAnswer(part))
13913
+ return "Waiting for the agent's answer";
13914
+ if (sentBackInRound(part, round))
13915
+ return "Sent back, waiting for the next round";
13916
+ return "Waits for your decision";
13917
+ }
13918
+ function settledStatus(part) {
12684
13919
  switch (part.decision) {
12685
13920
  case "accepted":
12686
13921
  return "Accepted";
@@ -12689,13 +13924,8 @@ function partStatus(part, round) {
12689
13924
  case "dropped":
12690
13925
  return "Dropped";
12691
13926
  case void 0:
12692
- break;
13927
+ return void 0;
12693
13928
  }
12694
- if (awaitingAnswer(part))
12695
- return "Waiting for the agent's answer";
12696
- if (sentBackInRound(part, round))
12697
- return "Sent back, waiting for the next round";
12698
- return "Waits for your decision";
12699
13929
  }
12700
13930
  function taskStateWord(state) {
12701
13931
  switch (state) {
@@ -12791,10 +14021,10 @@ var CALLS_HEADING2 = "Calls made without asking you";
12791
14021
  var CALLS_SUBHEADING = "These are the decisions the agents recorded taking on their own, each against a stated default. Halfcycle lists what they recorded; it cannot see a call an agent did not record.";
12792
14022
  var NO_CALLS_LINE2 = "No agent recorded a call made without asking in this phase.";
12793
14023
  function callLines2(call) {
12794
- return [
12795
- `${call.by} decided "${call.question}": chose "${call.chose}". The stated default was "${call.statedDefault}".`,
12796
- call.undoable === "yes" ? `${call.by} says this can be undone: "${call.how}"` : `${call.by} says this cannot be undone.`
12797
- ];
14024
+ const undo = callUndoLine({ by: call.by, undoable: call.undoable, how: call.how });
14025
+ if (call.kind === "listed")
14026
+ return [autonomyC8(call.by, call.question, call.chose), undo];
14027
+ return [callMadeLine({ by: call.by, question: call.question, chose: call.chose, statedDefault: call.statedDefault }), undo];
12798
14028
  }
12799
14029
  function callsSectionLines(calls) {
12800
14030
  const lines = [CALLS_HEADING2, CALLS_SUBHEADING];
@@ -13010,9 +14240,9 @@ async function runReviewAnswer(repoRoot, phase, part, answer, deps) {
13010
14240
  }
13011
14241
 
13012
14242
  // dist/uninstall.js
13013
- import { lstatSync as lstatSync3, readFileSync as readFileSync16, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
13014
- import { homedir as homedir3 } from "node:os";
13015
- import { dirname as dirname7, join as join19, relative as relative6, sep as sep2 } from "node:path";
14243
+ import { lstatSync as lstatSync3, readFileSync as readFileSync19, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
14244
+ import { homedir as homedir4 } from "node:os";
14245
+ import { dirname as dirname7, join as join22, relative as relative6, sep as sep2 } from "node:path";
13016
14246
  var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
13017
14247
  function parseUninstallArgs(args2) {
13018
14248
  let removeCredential = false;
@@ -13038,7 +14268,7 @@ function lexists(path) {
13038
14268
  function symlinkOnPath(root, rel) {
13039
14269
  let current = root;
13040
14270
  for (const segment of rel.split("/")) {
13041
- current = join19(current, segment);
14271
+ current = join22(current, segment);
13042
14272
  try {
13043
14273
  if (lstatSync3(current).isSymbolicLink())
13044
14274
  return true;
@@ -13060,13 +14290,13 @@ function readMergedJson(targetRepo, rule) {
13060
14290
  const { rel } = rule;
13061
14291
  if (symlinkOnPath(targetRepo, rel))
13062
14292
  return { state: "symlink" };
13063
- const path = join19(targetRepo, rel);
14293
+ const path = join22(targetRepo, rel);
13064
14294
  if (!lexists(path))
13065
14295
  return { state: "absent" };
13066
14296
  let text;
13067
14297
  let value;
13068
14298
  try {
13069
- text = readFileSync16(path, "utf-8");
14299
+ text = readFileSync19(path, "utf-8");
13070
14300
  } catch {
13071
14301
  text = "";
13072
14302
  }
@@ -13083,15 +14313,15 @@ function readMergedJson(targetRepo, rule) {
13083
14313
  }
13084
14314
  function validate(options, rules) {
13085
14315
  const { targetRepo, removeCredential, home } = options;
13086
- const manifestPath = join19(targetRepo, INSTALL_MANIFEST_REL);
13087
- const pinPath = join19(targetRepo, BUNDLE_PIN_REL);
14316
+ const manifestPath = join22(targetRepo, INSTALL_MANIFEST_REL);
14317
+ const pinPath = join22(targetRepo, BUNDLE_PIN_REL);
13088
14318
  if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
13089
14319
  throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
13090
14320
  }
13091
14321
  let manifest = null;
13092
14322
  if (lexists(manifestPath)) {
13093
14323
  try {
13094
- manifest = parseManifest(readFileSync16(manifestPath, "utf-8"));
14324
+ manifest = parseManifest(readFileSync19(manifestPath, "utf-8"));
13095
14325
  } catch (err) {
13096
14326
  const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
13097
14327
  throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "${HALFCYCLE_COMMAND}" once to rewrite it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
@@ -13101,7 +14331,7 @@ function validate(options, rules) {
13101
14331
  let pinReadable = true;
13102
14332
  if (lexists(pinPath)) {
13103
14333
  try {
13104
- const pin = JSON.parse(readFileSync16(pinPath, "utf-8"));
14334
+ const pin = JSON.parse(readFileSync19(pinPath, "utf-8"));
13105
14335
  const id = isPlainObject3(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
13106
14336
  engagementId = typeof id === "string" && id !== "" ? id : void 0;
13107
14337
  } catch {
@@ -13125,7 +14355,7 @@ function validate(options, rules) {
13125
14355
  throw new Refusal(`the project id in ${BUNDLE_PIN_REL} is not a valid Halfcycle project id, so it cannot name a folder on this machine. Nothing was removed, here or under ~/.halfcycle.`);
13126
14356
  }
13127
14357
  if (engagementId !== void 0) {
13128
- const expectedParent = join19(home ?? homedir3(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
14358
+ const expectedParent = join22(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
13129
14359
  if (dirname7(engagementStateDir(engagementId, home)) !== expectedParent) {
13130
14360
  throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
13131
14361
  }
@@ -13149,7 +14379,7 @@ function mergedFileRules(writeSet) {
13149
14379
  function writeBack(targetRepo, rel, current, next, lists, name) {
13150
14380
  if (next === current)
13151
14381
  return;
13152
- const path = join19(targetRepo, rel);
14382
+ const path = join22(targetRepo, rel);
13153
14383
  try {
13154
14384
  if (next === null) {
13155
14385
  rmSync6(path);
@@ -13166,7 +14396,7 @@ function statusLineClaimedFromTree(targetRepo, writeSet) {
13166
14396
  try {
13167
14397
  if (symlinkOnPath(targetRepo, SETTINGS_REL))
13168
14398
  return {};
13169
- const value = JSON.parse(readFileSync16(join19(targetRepo, SETTINGS_REL), "utf-8"));
14399
+ const value = JSON.parse(readFileSync19(join22(targetRepo, SETTINGS_REL), "utf-8"));
13170
14400
  if (!isPlainObject3(value) || !("statusLine" in value))
13171
14401
  return {};
13172
14402
  const hash = canonicalSha256(value["statusLine"]);
@@ -13180,7 +14410,7 @@ function noRecord(targetRepo, writeSet) {
13180
14410
  const claim = (rel, header, lines) => {
13181
14411
  let text;
13182
14412
  try {
13183
- text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync16(join19(targetRepo, rel), "utf-8");
14413
+ text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync19(join22(targetRepo, rel), "utf-8");
13184
14414
  } catch {
13185
14415
  text = "";
13186
14416
  }
@@ -13233,11 +14463,11 @@ function ourMcpServers(value) {
13233
14463
  }
13234
14464
  function unmergeSettings(value, rec, writeSet) {
13235
14465
  let changed = false;
13236
- const refused2 = [];
14466
+ const refused3 = [];
13237
14467
  const commands = new Set(writeSet.hookCommands);
13238
14468
  const denyHashes = rec.denyAddedSha256.filter((h) => writeSet.denyRuleSha256.has(h));
13239
14469
  if (denyHashes.length < rec.denyAddedSha256.length) {
13240
- refused2.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
14470
+ refused3.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
13241
14471
  }
13242
14472
  const hooks = value["hooks"];
13243
14473
  if (isPlainObject3(hooks)) {
@@ -13307,7 +14537,7 @@ function unmergeSettings(value, rec, writeSet) {
13307
14537
  value["$schema"] = rec.schemaBefore;
13308
14538
  changed = true;
13309
14539
  }
13310
- return { changed, refused: refused2 };
14540
+ return { changed, refused: refused3 };
13311
14541
  }
13312
14542
  function unmergeMcp(value, rec) {
13313
14543
  const servers = value["mcpServers"];
@@ -13411,7 +14641,7 @@ function lastOccurrence(text, block) {
13411
14641
  }
13412
14642
  function coveredPathExists(targetRepo, line) {
13413
14643
  const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
13414
- return path !== "" && lexists(join19(targetRepo, path));
14644
+ return path !== "" && lexists(join22(targetRepo, path));
13415
14645
  }
13416
14646
  function selfBuildMergedFileRules() {
13417
14647
  return {
@@ -13424,7 +14654,7 @@ function removeSelfBuildOwnedFile(targetRepo, rel, lists, name) {
13424
14654
  name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
13425
14655
  return;
13426
14656
  }
13427
- const abs = join19(targetRepo, rel);
14657
+ const abs = join22(targetRepo, rel);
13428
14658
  let isFile;
13429
14659
  try {
13430
14660
  isFile = lstatSync3(abs).isFile();
@@ -13448,10 +14678,10 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
13448
14678
  name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
13449
14679
  return;
13450
14680
  }
13451
- const abs = join19(targetRepo, rel);
14681
+ const abs = join22(targetRepo, rel);
13452
14682
  let bytes;
13453
14683
  try {
13454
- bytes = readFileSync16(abs);
14684
+ bytes = readFileSync19(abs);
13455
14685
  } catch {
13456
14686
  return;
13457
14687
  }
@@ -13473,7 +14703,7 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
13473
14703
  function removeSelfBuildEmptyDir(targetRepo, dir, lists, name) {
13474
14704
  if (symlinkOnPath(targetRepo, dir))
13475
14705
  return;
13476
- const abs = join19(targetRepo, dir);
14706
+ const abs = join22(targetRepo, dir);
13477
14707
  try {
13478
14708
  if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
13479
14709
  return;
@@ -13507,9 +14737,9 @@ function uninstallSelfBuild(options) {
13507
14737
  throw err;
13508
14738
  }
13509
14739
  const { targetRepo } = options;
13510
- const pinExisted = lexists(join19(targetRepo, BUNDLE_PIN_REL));
13511
- const name = (list, text, _path) => {
13512
- list.push(text);
14740
+ const pinExisted = lexists(join22(targetRepo, BUNDLE_PIN_REL));
14741
+ const name = (list2, text, _path) => {
14742
+ list2.push(text);
13513
14743
  };
13514
14744
  applyMergedJson(targetRepo, rules.mcp, checked.mcp, true, result, name, (value) => ({
13515
14745
  changed: unmergeMcp(value, BEFORE_UNKNOWN_MCP).changed,
@@ -13557,15 +14787,15 @@ function uninstall(options) {
13557
14787
  const manifest = checked.manifest ?? noRecord(targetRepo, writeSet);
13558
14788
  const deleteWhenEmpty = (created) => created || checked.manifest === null;
13559
14789
  const named = /* @__PURE__ */ new Set();
13560
- const name = (list, text, path) => {
13561
- list.push(text);
14790
+ const name = (list2, text, path) => {
14791
+ list2.push(text);
13562
14792
  if (path !== void 0)
13563
14793
  named.add(path);
13564
14794
  };
13565
14795
  const settingsEmptyGoes = deleteWhenEmpty(manifest.settings.created);
13566
14796
  applyMergedJson(targetRepo, rules.settings, checked.settings, settingsEmptyGoes, result, name, (value) => {
13567
- const { changed, refused: refused2 } = unmergeSettings(value, manifest.settings, writeSet);
13568
- for (const note of refused2)
14797
+ const { changed, refused: refused3 } = unmergeSettings(value, manifest.settings, writeSet);
14798
+ for (const note of refused3)
13569
14799
  name(result.leftAlone, note);
13570
14800
  return { changed, adopted: manifest.settings.adopted };
13571
14801
  });
@@ -13610,7 +14840,7 @@ function uninstall(options) {
13610
14840
  const gitattributes = manifest.gitattributes ?? { created: false, appended: [] };
13611
14841
  trimAppendedFile(targetRepo, gitattributes, deleteWhenEmpty(gitattributes.created), gitattributesRules(writeSet), result, name);
13612
14842
  const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0 && result.keptIgnoreLines.length === 0;
13613
- if (checked.manifest === null && !lexists(join19(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
14843
+ if (checked.manifest === null && !lexists(join22(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
13614
14844
  return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
13615
14845
  }
13616
14846
  const credential = handleCredential(options, checked);
@@ -13618,7 +14848,7 @@ function uninstall(options) {
13618
14848
  if (result.failed.length === 0 && !credentialFailed) {
13619
14849
  removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
13620
14850
  try {
13621
- rmSync6(join19(targetRepo, INSTALL_MANIFEST_REL), { force: true });
14851
+ rmSync6(join22(targetRepo, INSTALL_MANIFEST_REL), { force: true });
13622
14852
  named.add(INSTALL_MANIFEST_REL);
13623
14853
  } catch {
13624
14854
  name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
@@ -13653,7 +14883,7 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
13653
14883
  return;
13654
14884
  const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
13655
14885
  writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
13656
- if (adopted && lexists(join19(targetRepo, rel))) {
14886
+ if (adopted && lexists(join22(targetRepo, rel))) {
13657
14887
  name(lists.leftAlone, `${rel} (Halfcycle's hooks were taken out. There is no record of this file from before the install, so a permission rule or "$schema" Halfcycle added may still be there \u2014 it cannot be told apart from your own)`, rel);
13658
14888
  }
13659
14889
  }
@@ -13666,7 +14896,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
13666
14896
  name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
13667
14897
  return;
13668
14898
  }
13669
- const abs = join19(targetRepo, path);
14899
+ const abs = join22(targetRepo, path);
13670
14900
  let isFile;
13671
14901
  try {
13672
14902
  isFile = lstatSync3(abs).isFile();
@@ -13680,7 +14910,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
13680
14910
  if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
13681
14911
  let bytes;
13682
14912
  try {
13683
- bytes = readFileSync16(abs);
14913
+ bytes = readFileSync19(abs);
13684
14914
  } catch {
13685
14915
  name(lists.failed, path, path);
13686
14916
  return;
@@ -13700,7 +14930,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
13700
14930
  function removeEmptyDir(targetRepo, dir, writeSet, lists, name) {
13701
14931
  if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
13702
14932
  return;
13703
- const abs = join19(targetRepo, dir);
14933
+ const abs = join22(targetRepo, dir);
13704
14934
  try {
13705
14935
  if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
13706
14936
  return;
@@ -13719,10 +14949,10 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
13719
14949
  name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
13720
14950
  return [];
13721
14951
  }
13722
- const path = join19(targetRepo, rel);
14952
+ const path = join22(targetRepo, rel);
13723
14953
  let before;
13724
14954
  try {
13725
- before = readFileSync16(path, "utf-8");
14955
+ before = readFileSync19(path, "utf-8");
13726
14956
  } catch {
13727
14957
  return [];
13728
14958
  }
@@ -13766,16 +14996,16 @@ function filesUnder(root, rel) {
13766
14996
  const walk = (dirRel) => {
13767
14997
  let names;
13768
14998
  try {
13769
- if (!lstatSync3(join19(root, dirRel)).isDirectory())
14999
+ if (!lstatSync3(join22(root, dirRel)).isDirectory())
13770
15000
  return;
13771
- names = readdirSync(join19(root, dirRel));
15001
+ names = readdirSync(join22(root, dirRel));
13772
15002
  } catch {
13773
15003
  return;
13774
15004
  }
13775
15005
  for (const entry of names.sort()) {
13776
15006
  const childRel = `${dirRel}/${entry}`;
13777
15007
  try {
13778
- if (lstatSync3(join19(root, childRel)).isDirectory())
15008
+ if (lstatSync3(join22(root, childRel)).isDirectory())
13779
15009
  walk(childRel);
13780
15010
  else
13781
15011
  out.push(childRel);
@@ -13788,7 +15018,7 @@ function filesUnder(root, rel) {
13788
15018
  return out;
13789
15019
  }
13790
15020
  function shownPath(path, home) {
13791
- const base = home ?? homedir3();
15021
+ const base = home ?? homedir4();
13792
15022
  const rel = relative6(base, path);
13793
15023
  const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep2);
13794
15024
  return `${under ? `~/${rel.split(sep2).join("/")}` : path}/`;
@@ -13847,8 +15077,8 @@ function renderUninstallReport(result, home) {
13847
15077
  }
13848
15078
 
13849
15079
  // dist/banner-facts.js
13850
- import { readFileSync as readFileSync17 } from "node:fs";
13851
- import { basename as basename4, join as join20, resolve as resolve6 } from "node:path";
15080
+ import { readFileSync as readFileSync20 } from "node:fs";
15081
+ import { basename as basename4, join as join23, resolve as resolve6 } from "node:path";
13852
15082
  var BRAND_URL = "halfcycle.ai";
13853
15083
  var TAGLINE = [
13854
15084
  "Fewer cycles.",
@@ -13861,7 +15091,7 @@ var TAGLINE_NARROW = [
13861
15091
  ];
13862
15092
  function bundleVersion() {
13863
15093
  try {
13864
- const pkg = JSON.parse(readFileSync17(join20(BUNDLE_ROOT, "package.json"), "utf-8"));
15094
+ const pkg = JSON.parse(readFileSync20(join23(BUNDLE_ROOT, "package.json"), "utf-8"));
13865
15095
  return typeof pkg.version === "string" ? pkg.version : void 0;
13866
15096
  } catch {
13867
15097
  return void 0;
@@ -14005,13 +15235,13 @@ function runPaceChild(binary, credential, repoRoot, args2, deps = {}) {
14005
15235
  if (binary instanceof Error)
14006
15236
  return Promise.resolve({ code: 1, stdout: "" });
14007
15237
  const err = deps.stderr ?? process.stderr;
14008
- return new Promise((resolve7) => {
15238
+ return new Promise((resolve8) => {
14009
15239
  let settled = false;
14010
15240
  const done = (output) => {
14011
15241
  if (settled)
14012
15242
  return;
14013
15243
  settled = true;
14014
- resolve7(output);
15244
+ resolve8(output);
14015
15245
  };
14016
15246
  try {
14017
15247
  const child = spawn3(process.execPath, [binary, "pace", ...args2], {
@@ -14090,6 +15320,424 @@ async function runPace(repoRoot, start, deps) {
14090
15320
  `, stderr: "", exitCode: 0 };
14091
15321
  }
14092
15322
 
15323
+ // dist/restore.js
15324
+ import { spawnSync as spawnSync3 } from "node:child_process";
15325
+ import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync21, readlinkSync, realpathSync as realpathSync5, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
15326
+ import { homedir as homedir5 } from "node:os";
15327
+ import { join as join24, resolve as resolve7 } from "node:path";
15328
+ var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
15329
+ function parseRestoreArgs(rest) {
15330
+ if (rest.length > 1)
15331
+ return { ok: false };
15332
+ const first = rest[0];
15333
+ if (first !== void 0 && first.startsWith("-"))
15334
+ return { ok: false };
15335
+ return { ok: true, id: first };
15336
+ }
15337
+ var LIST_COMMAND = `${HALFCYCLE_COMMAND} restore`;
15338
+ function runRestore(cwd, id, options = {}) {
15339
+ const now = options.now ?? Date.now;
15340
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
15341
+ if (!top.ok || top.text.trim() === "") {
15342
+ return refused2("This folder is not in a project kept with git, so Halfcycle has saved nothing here to bring back.");
15343
+ }
15344
+ const root = realOrSelf(top.text.trim());
15345
+ const engagementId = pinnedEngagement(root);
15346
+ if (engagementId === null) {
15347
+ return refused2(`Halfcycle is not set up in this project, so it has saved nothing here. To set it up, run: ${HALFCYCLE_COMMAND}`);
15348
+ }
15349
+ const folder = join24(options.home ?? homedir5(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
15350
+ const refs = savedRefs(root);
15351
+ if (refs === null)
15352
+ return refused2("Halfcycle could not read the saved copies in this project, so nothing was put back.");
15353
+ pruneAtRestore(root, folder, refs, now());
15354
+ for (const [saveId, madeAt] of [...refs]) {
15355
+ const answer = savedPruneAnswer({ refMadeAt: madeAt, folderChangedAt: 0, folderRepository: null }, { repository: null, now: now() });
15356
+ if (answer !== "keep")
15357
+ refs.delete(saveId);
15358
+ }
15359
+ if (id === void 0)
15360
+ return list(root, folder, refs);
15361
+ return restoreOne(root, folder, refs, id);
15362
+ }
15363
+ function refused2(line) {
15364
+ return { stdout: "", stderr: `${line}
15365
+ `, exitCode: 1 };
15366
+ }
15367
+ function list(root, folder, refs) {
15368
+ const rows = [];
15369
+ for (const [id, at] of refs) {
15370
+ const saved = readSaved(folder, id);
15371
+ if (saved !== null)
15372
+ rows.push({ id, at, what: saved.what });
15373
+ }
15374
+ if (rows.length === 0) {
15375
+ return {
15376
+ stdout: "Nothing has been saved in this project in the last 7 days, so there is nothing to bring back.\n",
15377
+ stderr: "",
15378
+ exitCode: 0
15379
+ };
15380
+ }
15381
+ rows.sort((a, b) => b.at - a.at || (a.id < b.id ? 1 : -1));
15382
+ const lines = rows.map((r) => ` ${r.id} saved ${whenOf(r.at)}: ${printable(r.what)}`);
15383
+ return {
15384
+ stdout: `Saved copies you can bring back, newest first (each is kept for 7 days):
15385
+ ${lines.join("\n")}
15386
+ To bring one back, run: ${LIST_COMMAND} <id>
15387
+ `,
15388
+ stderr: "",
15389
+ exitCode: 0
15390
+ };
15391
+ }
15392
+ function whenOf(ms) {
15393
+ if (!Number.isFinite(ms))
15394
+ return "at an unknown time";
15395
+ return `${new Date(ms).toISOString().slice(0, 16).replace("T", " ")} UTC`;
15396
+ }
15397
+ function printable(text) {
15398
+ return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
15399
+ }
15400
+ function restoreOne(root, folder, refs, rawId) {
15401
+ const valid = savedIdSchema.safeParse(rawId);
15402
+ const named = valid.success ? `"${rawId}"` : "with that name";
15403
+ if (!valid.success || !refs.has(rawId)) {
15404
+ return refused2(`No saved copy ${named} is kept in this project. Saved copies are kept for 7 days. To see the ones there are, run: ${LIST_COMMAND}`);
15405
+ }
15406
+ const id = valid.data;
15407
+ const saved = readSaved(folder, id);
15408
+ if (saved === null)
15409
+ return refused2(`The saved copy "${id}" could not be read, so nothing was put back.`);
15410
+ const ref = savedRefOf(id);
15411
+ const out = { back: [], already: [], left: [] };
15412
+ const seen = /* @__PURE__ */ new Set();
15413
+ const fromCommit = [];
15414
+ for (const p of saved.paths) {
15415
+ if (seen.has(p.path))
15416
+ continue;
15417
+ seen.add(p.path);
15418
+ if (insideGitFolder(p.path)) {
15419
+ out.left.push(`${p.path}: it is inside git's own folder, which restore never writes`);
15420
+ continue;
15421
+ }
15422
+ if (p.keptIn === "commit")
15423
+ fromCommit.push(p.path);
15424
+ else
15425
+ restoreCopy(root, join24(folder, id), p.path, out);
15426
+ }
15427
+ if (fromCommit.length > 0)
15428
+ restoreFromCommit(root, ref, fromCommit, out);
15429
+ restoreLines(root, ref, saved, out);
15430
+ const parts = [];
15431
+ if (out.back.length > 0)
15432
+ parts.push(`Put back from the saved copy ${id}:
15433
+ ${out.back.map((l) => ` ${l}`).join("\n")}`);
15434
+ else
15435
+ parts.push(`Nothing was put back from the saved copy ${id}.`);
15436
+ if (out.already.length > 0)
15437
+ parts.push(`Already as saved:
15438
+ ${out.already.map((l) => ` ${l}`).join("\n")}`);
15439
+ if (out.left.length > 0)
15440
+ parts.push(`Left as they are:
15441
+ ${out.left.map((l) => ` ${l}`).join("\n")}`);
15442
+ return { stdout: `${parts.join("\n")}
15443
+ `, stderr: "", exitCode: out.left.length > 0 ? 1 : 0 };
15444
+ }
15445
+ var MOVE_AWAY = "move it away and run this again to bring back the saved one";
15446
+ function insideGitFolder(rel) {
15447
+ return rel.split(/[\\/]/).some(isGitFolderSegment);
15448
+ }
15449
+ function makeFolders(root, rel) {
15450
+ const segments = rel.split("/");
15451
+ let at = root;
15452
+ for (let i = 0; i < segments.length - 1; i++) {
15453
+ at = join24(at, segments[i]);
15454
+ const shown2 = segments.slice(0, i + 1).join("/");
15455
+ let st = lstatOrNull3(at);
15456
+ if (st === null) {
15457
+ try {
15458
+ mkdirSync11(at);
15459
+ } catch {
15460
+ }
15461
+ st = lstatOrNull3(at);
15462
+ }
15463
+ if (st === null)
15464
+ return "its folder could not be made";
15465
+ if (st.isSymbolicLink())
15466
+ return `${shown2} is a link now, and restore never writes through a link`;
15467
+ if (!st.isDirectory())
15468
+ return `${shown2} is a file now, where a folder is needed; ${MOVE_AWAY}`;
15469
+ }
15470
+ return null;
15471
+ }
15472
+ function place(root, rel, content, out) {
15473
+ const abs = join24(root, rel);
15474
+ const there = lstatOrNull3(abs);
15475
+ if (there !== null) {
15476
+ if (sameAs(abs, there, content))
15477
+ out.already.push(rel);
15478
+ else
15479
+ out.left.push(`${rel}: something is there now; ${MOVE_AWAY}`);
15480
+ return;
15481
+ }
15482
+ const blocked = makeFolders(root, rel);
15483
+ if (blocked !== null) {
15484
+ out.left.push(`${rel}: ${blocked}`);
15485
+ return;
15486
+ }
15487
+ try {
15488
+ if ("link" in content) {
15489
+ symlinkSync(content.link, abs);
15490
+ } else {
15491
+ const fd = openSync3(abs, "wx", 384);
15492
+ try {
15493
+ let off = 0;
15494
+ while (off < content.bytes.length)
15495
+ off += writeSync2(fd, content.bytes, off, content.bytes.length - off);
15496
+ fchmodSync(fd, content.mode);
15497
+ } finally {
15498
+ closeSync3(fd);
15499
+ }
15500
+ }
15501
+ out.back.push(rel);
15502
+ } catch (e) {
15503
+ const code = e.code;
15504
+ out.left.push(code === "EEXIST" ? `${rel}: something is there now; ${MOVE_AWAY}` : `${rel}: it could not be written here`);
15505
+ }
15506
+ }
15507
+ function sameAs(abs, st, content) {
15508
+ try {
15509
+ if ("link" in content)
15510
+ return st.isSymbolicLink() && readlinkSync(abs) === content.link;
15511
+ return st.isFile() && readFileSync21(abs).equals(content.bytes);
15512
+ } catch {
15513
+ return false;
15514
+ }
15515
+ }
15516
+ function restoreCopy(root, saveDir, rel, out) {
15517
+ const segments = rel.split("/");
15518
+ let at = saveDir;
15519
+ const chain = [SAVED_COPIES_DIR_NAME, ...segments];
15520
+ for (let i = 0; i < chain.length - 1; i++) {
15521
+ at = join24(at, chain[i]);
15522
+ const st2 = lstatOrNull3(at);
15523
+ if (st2 === null || st2.isSymbolicLink() || !st2.isDirectory()) {
15524
+ out.left.push(`${rel}: the saved copy does not hold it`);
15525
+ return;
15526
+ }
15527
+ }
15528
+ const src = join24(at, chain[chain.length - 1]);
15529
+ const st = lstatOrNull3(src);
15530
+ try {
15531
+ if (st !== null && st.isSymbolicLink())
15532
+ return place(root, rel, { link: readlinkSync(src) }, out);
15533
+ if (st !== null && st.isFile())
15534
+ return place(root, rel, { bytes: readFileSync21(src), mode: st.mode & 511 }, out);
15535
+ } catch {
15536
+ }
15537
+ out.left.push(`${rel}: the saved copy does not hold it`);
15538
+ }
15539
+ function restoreFromCommit(root, ref, rels, out) {
15540
+ const entries = /* @__PURE__ */ new Map();
15541
+ for (let i = 0; i < rels.length; i += 200) {
15542
+ const chunk = rels.slice(i, i + 200);
15543
+ const listed = git2(root, ["--literal-pathspecs", "ls-tree", "-z", "--full-tree", `${ref}^{commit}`, "--", ...chunk]);
15544
+ if (!listed.ok) {
15545
+ for (const rel of rels)
15546
+ out.left.push(`${rel}: the saved copy could not be read`);
15547
+ return;
15548
+ }
15549
+ for (const rec of listed.text.split("\0")) {
15550
+ const tab = rec.indexOf(" ");
15551
+ if (tab < 0)
15552
+ continue;
15553
+ const [mode, type, object] = rec.slice(0, tab).split(" ");
15554
+ if (type === "blob" && mode !== void 0 && object !== void 0)
15555
+ entries.set(rec.slice(tab + 1), { mode, object });
15556
+ }
15557
+ }
15558
+ const objects = [...new Set([...entries.values()].map((e) => e.object))];
15559
+ const blobs = objects.length === 0 ? /* @__PURE__ */ new Map() : readBlobs(root, objects);
15560
+ for (const rel of rels) {
15561
+ const entry = entries.get(rel);
15562
+ const bytes = entry === void 0 ? void 0 : blobs?.get(entry.object);
15563
+ if (entry === void 0 || bytes === void 0) {
15564
+ out.left.push(`${rel}: the saved copy does not hold it`);
15565
+ continue;
15566
+ }
15567
+ if (entry.mode === "120000")
15568
+ place(root, rel, { link: bytes.toString("utf-8") }, out);
15569
+ else
15570
+ place(root, rel, { bytes, mode: entry.mode === "100755" ? 493 : 420 }, out);
15571
+ }
15572
+ }
15573
+ function readBlobs(root, objects) {
15574
+ const r = spawnSync3("git", ["cat-file", "--batch"], {
15575
+ cwd: root,
15576
+ env: gitEnvForNamedRepo(),
15577
+ input: `${objects.join("\n")}
15578
+ `,
15579
+ maxBuffer: 1024 * 1024 * 1024
15580
+ });
15581
+ if (r.status !== 0 || !Buffer.isBuffer(r.stdout))
15582
+ return null;
15583
+ const out = /* @__PURE__ */ new Map();
15584
+ const buf = r.stdout;
15585
+ let at = 0;
15586
+ while (at < buf.length) {
15587
+ const nl = buf.indexOf(10, at);
15588
+ if (nl < 0)
15589
+ break;
15590
+ const [object, type, size] = buf.subarray(at, nl).toString("utf-8").split(" ");
15591
+ if (type !== "blob" || object === void 0 || size === void 0)
15592
+ return null;
15593
+ const n = Number(size);
15594
+ out.set(object, Buffer.from(buf.subarray(nl + 1, nl + 1 + n)));
15595
+ at = nl + 1 + n + 1;
15596
+ }
15597
+ return out;
15598
+ }
15599
+ function restoreLines(root, ref, saved, out) {
15600
+ if (saved.branches.length === 0)
15601
+ return;
15602
+ const parents = git2(root, ["rev-list", "--parents", "-n", "1", `${ref}^{commit}`]);
15603
+ const held = new Set(parents.ok ? parents.text.trim().split(/\s+/).slice(1) : []);
15604
+ for (const b of saved.branches) {
15605
+ const shown2 = `the line of work "${b.name}"`;
15606
+ const full = `refs/heads/${b.name}`;
15607
+ if (!git2(root, ["check-ref-format", full]).ok) {
15608
+ out.left.push(`${shown2}: its name cannot be used`);
15609
+ continue;
15610
+ }
15611
+ if (!held.has(b.commit)) {
15612
+ out.left.push(`${shown2}: the saved copy does not hold its commit`);
15613
+ continue;
15614
+ }
15615
+ const now = git2(root, ["rev-parse", "--verify", "--quiet", `${full}^{commit}`]);
15616
+ if (now.ok && now.text.trim() !== "") {
15617
+ if (now.text.trim() === b.commit)
15618
+ out.already.push(`${shown2}, at its saved commit ${b.commit.slice(0, 12)}`);
15619
+ else
15620
+ out.left.push(`${shown2}: one with this name exists now; rename it and run this again to bring back the saved one`);
15621
+ continue;
15622
+ }
15623
+ const made = git2(root, ["update-ref", "--no-deref", full, b.commit, ""]);
15624
+ if (made.ok)
15625
+ out.back.push(`${shown2}, at its saved commit ${b.commit.slice(0, 12)}`);
15626
+ else
15627
+ out.left.push(`${shown2}: it could not be made here, perhaps because another line of work's name is in the way`);
15628
+ }
15629
+ }
15630
+ function pinnedEngagement(root) {
15631
+ try {
15632
+ const pin = JSON.parse(readFileSync21(join24(root, BUNDLE_PIN_REL), "utf-8"));
15633
+ return isEngagementId(pin.engagementId) ? pin.engagementId : null;
15634
+ } catch {
15635
+ return null;
15636
+ }
15637
+ }
15638
+ function savedRefs(root) {
15639
+ const listed = git2(root, ["for-each-ref", "--format=%(refname)%00%(committerdate:unix)", SAVED_REF_PREFIX]);
15640
+ if (!listed.ok)
15641
+ return null;
15642
+ const refs = /* @__PURE__ */ new Map();
15643
+ for (const line of listed.text.split("\n")) {
15644
+ const [name, when] = line.split("\0");
15645
+ if (name === void 0 || when === void 0 || !name.startsWith(SAVED_REF_PREFIX))
15646
+ continue;
15647
+ const id = name.slice(SAVED_REF_PREFIX.length);
15648
+ if (savedIdSchema.safeParse(id).success)
15649
+ refs.set(id, Number(when) * 1e3);
15650
+ }
15651
+ return refs;
15652
+ }
15653
+ function readSaved(folder, id) {
15654
+ const dir = join24(folder, id);
15655
+ const st = lstatOrNull3(dir);
15656
+ if (st === null || !st.isDirectory())
15657
+ return null;
15658
+ const file = join24(dir, SAVED_FILE_NAME);
15659
+ const fst = lstatOrNull3(file);
15660
+ if (fst === null || !fst.isFile())
15661
+ return null;
15662
+ try {
15663
+ const parsed = savedSchema.safeParse(JSON.parse(readFileSync21(file, "utf-8")));
15664
+ return parsed.success && parsed.data.id === id ? parsed.data : null;
15665
+ } catch {
15666
+ return null;
15667
+ }
15668
+ }
15669
+ function pruneAtRestore(root, folder, refs, now) {
15670
+ try {
15671
+ const common = git2(root, ["rev-parse", "--git-common-dir"]);
15672
+ const repository = common.ok && common.text.trim() !== "" ? realOrSelf(resolve7(root, common.text.trim())) : null;
15673
+ let names = [];
15674
+ try {
15675
+ names = readdirSync2(folder);
15676
+ } catch {
15677
+ names = [];
15678
+ }
15679
+ const here = /* @__PURE__ */ new Set();
15680
+ for (const id of names) {
15681
+ if (!savedIdSchema.safeParse(id).success)
15682
+ continue;
15683
+ here.add(id);
15684
+ const dir = join24(folder, id);
15685
+ const st = lstatOrNull3(dir);
15686
+ const answer = savedPruneAnswer({
15687
+ refMadeAt: refs.get(id) ?? null,
15688
+ folderChangedAt: st !== null && st.isDirectory() ? st.mtimeMs : null,
15689
+ folderRepository: markedRepository(dir)
15690
+ }, { repository, now });
15691
+ if (answer === "remove-folder")
15692
+ rmSync7(dir, { recursive: true, force: true });
15693
+ else if (answer === "remove-ref-then-folder" && git2(root, ["update-ref", "-d", savedRefOf(id)]).ok) {
15694
+ refs.delete(id);
15695
+ rmSync7(dir, { recursive: true, force: true });
15696
+ } else if (answer === "remove-ref" && git2(root, ["update-ref", "-d", savedRefOf(id)]).ok) {
15697
+ refs.delete(id);
15698
+ }
15699
+ }
15700
+ for (const [id, madeAt] of [...refs]) {
15701
+ if (here.has(id))
15702
+ continue;
15703
+ const answer = savedPruneAnswer({ refMadeAt: madeAt, folderChangedAt: null, folderRepository: null }, { repository, now });
15704
+ if (answer === "remove-ref" && git2(root, ["update-ref", "-d", savedRefOf(id)]).ok)
15705
+ refs.delete(id);
15706
+ }
15707
+ } catch {
15708
+ }
15709
+ }
15710
+ function markedRepository(dir) {
15711
+ try {
15712
+ const text = readFileSync21(join24(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
15713
+ return text === "" ? null : text;
15714
+ } catch {
15715
+ return null;
15716
+ }
15717
+ }
15718
+ function git2(cwd, args2) {
15719
+ try {
15720
+ const r = spawnSync3("git", args2, { cwd, encoding: "utf-8", env: gitEnvForNamedRepo(), maxBuffer: 64 * 1024 * 1024 });
15721
+ return r.status === 0 ? { ok: true, text: r.stdout ?? "" } : { ok: false };
15722
+ } catch {
15723
+ return { ok: false };
15724
+ }
15725
+ }
15726
+ function lstatOrNull3(p) {
15727
+ try {
15728
+ return lstatSync4(p);
15729
+ } catch {
15730
+ return null;
15731
+ }
15732
+ }
15733
+ function realOrSelf(p) {
15734
+ try {
15735
+ return realpathSync5(p);
15736
+ } catch {
15737
+ return p;
15738
+ }
15739
+ }
15740
+
14093
15741
  // dist/bin.js
14094
15742
  var ASSUME_YES_FLAGS = /* @__PURE__ */ new Set(["--yes", "-y"]);
14095
15743
  var QUIET_FLAGS = /* @__PURE__ */ new Set(["--quiet", "-q"]);
@@ -14162,6 +15810,13 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
14162
15810
  ${PACE_USAGE}
14163
15811
  suggest how many agents this build should run at once, and for how long; with no flag, how
14164
15812
  many may start right now
15813
+ ${HALFCYCLE_COMMAND} preview [folder]
15814
+ show what setting up this folder would add, change, remove or leave alone; writes nothing
15815
+ ${HALFCYCLE_COMMAND} doctor
15816
+ check this folder: set up, signed in, connected, checks running, up to date; says what to do for each one that is not; changes nothing
15817
+ ${RESTORE_USAGE}
15818
+ list the copies Halfcycle saved before a change it let through, or bring one back; never
15819
+ replaces a file or a line of work that exists now
14165
15820
  ${HALFCYCLE_COMMAND} uninstall [${REMOVE_CREDENTIAL_FLAG}]
14166
15821
  take out what the installer added to this project, keeping anything you changed
14167
15822
  ${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
@@ -14172,15 +15827,6 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
14172
15827
 
14173
15828
  Support: hello@halfcycle.ai
14174
15829
  `;
14175
- function isHomeFolder(dir) {
14176
- try {
14177
- const target = realpathSync4(dir);
14178
- const home = realpathSync4(homedir4());
14179
- return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
14180
- } catch {
14181
- return false;
14182
- }
14183
- }
14184
15830
  var RETIRED_VERBS = /* @__PURE__ */ new Map([
14185
15831
  ["ci", '"ci" was removed \u2014 CI setup is part of your project, not Halfcycle']
14186
15832
  ]);
@@ -14278,34 +15924,65 @@ ${USAGE}`);
14278
15924
  process.exit(0);
14279
15925
  return;
14280
15926
  }
15927
+ if (cmd === "preview") {
15928
+ const outcome = runPreview(rest[0] ?? process.cwd(), { env: process.env, platform: process.platform });
15929
+ if ("refusal" in outcome) {
15930
+ process.stderr.write(`${outcome.refusal}
15931
+ `);
15932
+ process.exitCode = 1;
15933
+ return;
15934
+ }
15935
+ process.stdout.write(`${outcome.text}
15936
+ `);
15937
+ return;
15938
+ }
15939
+ if (cmd === "doctor") {
15940
+ const report = runDoctor(process.cwd(), { env: process.env, platform: process.platform });
15941
+ process.stdout.write(`${report.text}
15942
+ `);
15943
+ process.exitCode = report.exitCode;
15944
+ return;
15945
+ }
15946
+ if (cmd === "restore") {
15947
+ const parsed = parseRestoreArgs(rest);
15948
+ if (!parsed.ok) {
15949
+ process.stderr.write(`halfcycle restore: usage: ${RESTORE_USAGE}
15950
+ `);
15951
+ process.exitCode = 2;
15952
+ return;
15953
+ }
15954
+ const run = runRestore(process.cwd(), parsed.id);
15955
+ if (run.stdout !== "")
15956
+ process.stdout.write(run.stdout);
15957
+ if (run.stderr !== "")
15958
+ process.stderr.write(run.stderr);
15959
+ process.exitCode = run.exitCode;
15960
+ return;
15961
+ }
14281
15962
  if (installArm) {
14282
15963
  const engagementIdArg = positionals[1];
14283
15964
  const engagementTypeRaw = positionals[2] ?? "client";
14284
- if (!existsSync9(targetRepo)) {
14285
- process.stderr.write(`halfcycle: no such directory: ${targetRepo}
15965
+ if (!existsSync12(targetRepo)) {
15966
+ process.stderr.write(`${folderMissingRefusal(targetRepo)}
14286
15967
  `);
14287
15968
  process.exit(1);
14288
15969
  return;
14289
15970
  }
14290
- if (!statSync(targetRepo).isDirectory()) {
14291
- process.stderr.write(`halfcycle: ${targetRepo} is a file \u2014 run "npx halfcycle" with a project folder instead.
15971
+ if (!statSync2(targetRepo).isDirectory()) {
15972
+ process.stderr.write(`${folderIsFileRefusal(targetRepo)}
14292
15973
  `);
14293
15974
  process.exit(1);
14294
15975
  return;
14295
15976
  }
14296
15977
  const selfBuild = isHalfcycleMonorepo(targetRepo);
14297
15978
  if (selfBuild && !isSelfBuildInstallForm(engagementIdArg, positionals[2])) {
14298
- process.stderr.write(`halfcycle: refusing to install into the Halfcycle monorepo itself (${targetRepo}).
14299
- The bare form installs into the CURRENT directory, and this directory is the
14300
- product's own source tree \u2014 installing here would write over tracked files.
14301
- To see what this CLI does: halfcycle --help
14302
- To install into a real project: halfcycle <path-to-that-repo>
15979
+ process.stderr.write(`${monorepoRefusal(targetRepo)}
14303
15980
  `);
14304
15981
  process.exit(1);
14305
15982
  return;
14306
15983
  }
14307
15984
  if (isHomeFolder(targetRepo)) {
14308
- process.stderr.write(`halfcycle: refusing to set up in your home folder (${targetRepo}). Run "npx halfcycle" inside a project folder.
15985
+ process.stderr.write(`${homeFolderRefusal(targetRepo)}
14309
15986
  `);
14310
15987
  process.exit(1);
14311
15988
  return;
@@ -14324,7 +16001,7 @@ ${USAGE}`);
14324
16001
  const kept = readFolderId(targetRepo) !== void 0 ? `${KEPT_FOLDER_ID_LINE}
14325
16002
  ` : "";
14326
16003
  try {
14327
- writeSync2(2, `halfcycle: interrupted.
16004
+ writeSync3(2, `halfcycle: interrupted.
14328
16005
  ${kept}`);
14329
16006
  } catch {
14330
16007
  }
@@ -14573,7 +16250,7 @@ ${PROJECT_RESET_LINE}
14573
16250
  process.stdout.write(`[halfcycle] Method version: ${credential.methodVersion}
14574
16251
  `);
14575
16252
  }
14576
- await setupClaudePlan({ serviceUrl: credential?.serviceUrl ?? serviceUrl, engagementId, ownsProject, consoleUrl }, { interactive: () => isInteractiveRun(), ask: askLine, write: (text) => process.stdout.write(text) });
16253
+ await setupClaudePlan({ serviceUrl: credential?.serviceUrl ?? serviceUrl, engagementId, ownsProject, consoleUrl }, { interactive: () => isInteractiveRun(), assumeYes, ask: askLine, write: (text) => process.stdout.write(text) });
14577
16254
  if (checks.gitInstalled) {
14578
16255
  process.stdout.write(`[halfcycle] Git for Windows is installed. Open a new terminal before starting Claude Code.
14579
16256
  `);