halfcycle 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,15 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // dist/bin.js
4
- import { existsSync as existsSync12, statSync as statSync2, writeSync as writeSync3 } from "node:fs";
5
-
6
- // dist/install.js
7
- import { execFileSync as execFileSync2 } from "node:child_process";
8
- import { createHash as createHash2 } from "node:crypto";
9
- import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
10
- import { homedir as homedir2 } from "node:os";
11
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, posix as posix2, relative, resolve } from "node:path";
12
- import { fileURLToPath } from "node:url";
3
+ // dist/request-shapes.js
4
+ import { appendFileSync, existsSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
13
7
 
14
8
  // ../core/dist/guard-record.js
15
9
  import { z } from "zod";
@@ -512,6 +506,7 @@ var TASK_PLAN_FILE = "docs/build-records/task-plan.json";
512
506
  var TASK_LIST_FILE = "docs/build-records/build-tasks.json";
513
507
  var CRITERIA_RECORD_FILE = "docs/build-records/criteria-check.json";
514
508
  var AGENT_NAME_MAX = 32;
509
+ var SMALL_PLAN_BUILD_TASKS_MAX = 6;
515
510
  var TASK_PLAN_TASKS_MAX = 500;
516
511
  var TASK_PLAN_ID_MAX = 64;
517
512
  var TASK_PLAN_BUILDS_NONE = "none";
@@ -622,14 +617,14 @@ var taskPlanLineText = {
622
617
  O8NoSuchPart: (a, parts, file, partIds) => parts.length === 1 ? `"${a}" builds "${parts[0]}", which is not a part of the system: ${file} has no part with that id. Use an id from ${file}: ${partIds.join(", ")}.` : `"${a}" builds ${parts.map((x) => `"${x}"`).join(", ")}, which are not parts of the system: ${file} has no part with those ids. Use ids from ${file}: ${partIds.join(", ")}.`,
623
618
  O8Unusable: (file, why) => `${file} is there but could not be used, so no task was checked for building a part outside this phase's plan: ${why}. Fix it, then run this again.`,
624
619
  C1Form: (a, x, first) => `"${a}" names "${x}", which is not a criterion. Write it as <spec name> AC<number>, for example "${first} AC1".`,
625
- C1Number: (a, x, folder, slug, n) => `"${a}" names "${x}", but ${folder}/${slug}.md has no AC${n}.`,
620
+ C1Number: (a, x, folder2, slug, n) => `"${a}" names "${x}", but ${folder2}/${slug}.md has no AC${n}.`,
626
621
  C2: (a, x) => `"${a}" names "${x}", which was retired. Remove it, or name the criterion that replaced it.`,
627
622
  C3: (slug, n) => `${slug} AC${n} is not delivered by any build task. Add "${slug} AC${n}" to the criteria of the build task that delivers it.`,
628
623
  C4: (a) => `"${a}" does not say which acceptance criteria it delivers. Add "criteria" with each one as <spec name> AC<number>. If it delivers none, write "criteria": "${TASK_PLAN_CRITERIA_NONE}".`,
629
624
  C5: (a, x, slug) => `"${a}" names "${x}", but "${slug}" is not in the plan's specs. Add it to "specs" if it belongs to this phase, or remove the citation.`,
630
- C6None: (folder) => `The plan does not say which feature designs this phase has. Add "specs" with the name of each file in ${folder}/ this phase builds, without ".md". If it has none, write "specs": "${TASK_PLAN_SPECS_NONE}".`,
631
- C6Malformed: (x, folder) => `The plan's specs name "${x}", which is not a spec name. Use the name of a file in ${folder}/, without ".md".`,
632
- C6Missing: (slug, folder) => `The plan's specs name "${slug}", but there is no ${folder}/${slug}.md.`,
625
+ C6None: (folder2) => `The plan does not say which feature designs this phase has. Add "specs" with the name of each file in ${folder2}/ this phase builds, without ".md". If it has none, write "specs": "${TASK_PLAN_SPECS_NONE}".`,
626
+ C6Malformed: (x, folder2) => `The plan's specs name "${x}", which is not a spec name. Use the name of a file in ${folder2}/, without ".md".`,
627
+ C6Missing: (slug, folder2) => `The plan's specs name "${slug}", but there is no ${folder2}/${slug}.md.`,
633
628
  B1: (s, t, x, more) => `"${s}" and "${t}" both own ${x}${more === 0 ? "" : more === 1 ? " and 1 more path" : ` and ${more} more paths`}. A file belongs to one spec: keep it in the Owns of the spec that mainly changes it, and let the other spec's tasks list it in what they touch.`,
634
629
  B2: (s, x) => `"${s}" lists "${x}" under Owns, which is not a path inside this project. Use a path from the project's top folder; end a folder with /.`,
635
630
  N2: (a, files) => `"${a}" changes files that none of its specs own: ${files.join(", ")}. If that is intended there is nothing to do; otherwise add them to a spec's Owns.`,
@@ -644,7 +639,7 @@ var taskPlanLineText = {
644
639
  V1: () => 'The plan has no check of the finished phase against its acceptance criteria. Add a task of kind "check" with "criteriaCheck": true, held by a crew member who builds none of this phase, that waits for every build task.',
645
640
  V2: (w, c) => `"${w}" is a walk, and it does not wait for the check of the phase against its acceptance criteria. Add "${c}" to its dependsOn.`,
646
641
  V3Unordered: (c, b) => `"${c}" checks the finished phase, but "${b}" could still be running while it does. Add "${b}" to the dependsOn of "${c}", or, if "${b}" fixes what "${c}" found, name it in ${CRITERIA_RECORD_FILE} and make it wait for "${c}".`,
647
- V3Ships: (b, c) => `"${b}" ships to users and waits for "${c}", the check of the finished phase. A deploy or a release comes before that check, so the check sees what is live: add "${b}" to the dependsOn of "${c}" and stop "${b}" waiting for "${c}".`,
642
+ V3Ships: (b, c) => `"${b}" ships to users and waits for "${c}", the check of the finished phase, and no later check waits for "${b}", so no check sees what is live. Add "${b}" to the dependsOn of "${c}" and stop "${b}" waiting for "${c}", or add a check of the phase that waits for "${b}".`,
648
643
  V3Unchecked: (b, c) => `"${b}" waits for "${c}", the check of the finished phase, and no later check waits for "${b}", so no check sees what it builds. Add "${b}" to the dependsOn of "${c}" and stop "${b}" waiting for "${c}", or add a check of the phase that waits for "${b}".`,
649
644
  V4: (x) => `"${x}" does not say which crew member holds it. Add "agent" with that member's callsign.`,
650
645
  V5: (c, m, b) => `"${c}" checks the finished phase, but ${m} also holds "${b}", which builds part of it. Give "${c}" to a crew member who holds no build task in this phase.`,
@@ -711,8 +706,8 @@ function firstSharedItem(a, b) {
711
706
  return other.shown;
712
707
  }
713
708
  }
714
- for (const folder of b.folders) {
715
- if (file.key.startsWith(folder.key))
709
+ for (const folder2 of b.folders) {
710
+ if (file.key.startsWith(folder2.key))
716
711
  return file.shown;
717
712
  }
718
713
  }
@@ -740,8 +735,8 @@ function sharedPaths(a, b) {
740
735
  add(other);
741
736
  }
742
737
  }
743
- for (const folder of b.folders) {
744
- if (file.key.startsWith(folder.key))
738
+ for (const folder2 of b.folders) {
739
+ if (file.key.startsWith(folder2.key))
745
740
  add(file);
746
741
  }
747
742
  }
@@ -1013,19 +1008,19 @@ function liveEdgeCriterionNumbers(criteria) {
1013
1008
  return liveCriterionNumbers(criteria).filter((n) => edge.has(n));
1014
1009
  }
1015
1010
  function specLines(plan, specFiles) {
1016
- const folder = specFiles.folder;
1011
+ const folder2 = specFiles.folder;
1017
1012
  if (plan.specs === TASK_PLAN_SPECS_NONE)
1018
1013
  return [];
1019
1014
  if (plan.specs === void 0 || plan.specs.length === 0) {
1020
- return [refusal("C6", taskPlanLineText.C6None(folder))];
1015
+ return [refusal("C6", taskPlanLineText.C6None(folder2))];
1021
1016
  }
1022
1017
  const criteriaOf = specCriteriaBySlug(specFiles);
1023
1018
  const lines = [];
1024
1019
  for (const name of distinct(plan.specs)) {
1025
1020
  if (!isPlanSpecName(name)) {
1026
- lines.push(refusal("C6", taskPlanLineText.C6Malformed(name, folder)));
1021
+ lines.push(refusal("C6", taskPlanLineText.C6Malformed(name, folder2)));
1027
1022
  } else if ((criteriaOf.get(name) ?? null) === null) {
1028
- lines.push(refusal("C6", taskPlanLineText.C6Missing(name, folder)));
1023
+ lines.push(refusal("C6", taskPlanLineText.C6Missing(name, folder2)));
1029
1024
  }
1030
1025
  }
1031
1026
  const covered = /* @__PURE__ */ new Set();
@@ -1169,7 +1164,7 @@ function ownershipNotes(plan, specFiles) {
1169
1164
  continue;
1170
1165
  const { files: ownedFiles, folders: ownedFolders } = pathItemsOf(ownPaths);
1171
1166
  const ownedKeys = new Set(ownedFiles.map((f) => f.key));
1172
- const unowned = pathItemsOf(task.touches?.files ?? []).files.filter((touched) => !ownedKeys.has(touched.key) && !ownedFolders.some((folder) => touched.key.startsWith(folder.key)));
1167
+ const unowned = pathItemsOf(task.touches?.files ?? []).files.filter((touched) => !ownedKeys.has(touched.key) && !ownedFolders.some((folder2) => touched.key.startsWith(folder2.key)));
1173
1168
  if (unowned.length > 0) {
1174
1169
  lines.push(note("N2", taskPlanLineText.N2(task.id, distinct(unowned.map((f) => f.shown)))));
1175
1170
  }
@@ -1212,8 +1207,9 @@ function placementLines(tasks, before) {
1212
1207
  if (q !== void 0)
1213
1208
  batch.set(a, q);
1214
1209
  });
1210
+ const asksForBatches = checkPositions.length > 0 || !isSmallPlan(tasks);
1215
1211
  tasks.forEach((task, a) => {
1216
- if (build[a] && !batch.has(a))
1212
+ if (asksForBatches && build[a] && !batch.has(a))
1217
1213
  lines.push(refusal("K1", taskPlanLineText.K1(task.id)));
1218
1214
  });
1219
1215
  for (let i = 0; i < checkPositions.length; i += 1) {
@@ -1307,8 +1303,11 @@ function placementLines(tasks, before) {
1307
1303
  function isCriteriaCheck(task) {
1308
1304
  return task.kind === "check" && task.criteriaCheck === true;
1309
1305
  }
1310
- function lacksCriteriaCheck(tasks) {
1311
- return tasks.some((task) => task.kind === "build") && !tasks.some(isCriteriaCheck);
1306
+ function isSmallPlan(tasks) {
1307
+ return tasks.filter((task) => task.kind === "build").length <= SMALL_PLAN_BUILD_TASKS_MAX;
1308
+ }
1309
+ function printsV1(tasks) {
1310
+ return tasks.some((task) => task.kind === "build") && !tasks.some(isCriteriaCheck) && !isSmallPlan(tasks);
1312
1311
  }
1313
1312
  function mustNameHolder(task) {
1314
1313
  return mustNameParts(task) || isCriteriaCheck(task);
@@ -1329,7 +1328,7 @@ function criteriaCheckLines(tasks, before, record2) {
1329
1328
  const walks = positions((task) => task.kind === "walk");
1330
1329
  const releases = positions((task) => task.ships === "release");
1331
1330
  const exempt = (w) => releases.some((r) => waits(r, w));
1332
- if (lacksCriteriaCheck(tasks)) {
1331
+ if (printsV1(tasks)) {
1333
1332
  lines.push(refusal("V1", taskPlanLineText.V1()));
1334
1333
  } else {
1335
1334
  const last = [...checks].reverse().find((c) => !checks.some((other) => other !== c && waits(other, c)));
@@ -1348,10 +1347,10 @@ function criteriaCheckLines(tasks, before, record2) {
1348
1347
  const taskWaits = waits(b, c);
1349
1348
  if (!checkWaits && !taskWaits) {
1350
1349
  lines.push(refusal("V3", taskPlanLineText.V3Unordered(cid, bid)));
1351
- } else if (taskWaits && !fixes.has(bid)) {
1350
+ } else if (taskWaits && !checks.some((other) => other !== c && waits(other, b))) {
1352
1351
  if (tasks[b].ships !== void 0) {
1353
1352
  lines.push(refusal("V3", taskPlanLineText.V3Ships(bid, cid)));
1354
- } else if (!checks.some((other) => other !== c && waits(other, b))) {
1353
+ } else if (!fixes.has(bid)) {
1355
1354
  lines.push(refusal("V3", taskPlanLineText.V3Unchecked(bid, cid)));
1356
1355
  }
1357
1356
  }
@@ -1480,7 +1479,7 @@ function checkTaskPlan(plan, request) {
1480
1479
  const system = request.system;
1481
1480
  const criteriaOf = specCriteriaBySlug(request.specFiles);
1482
1481
  const phasePlan = request.phasePlan;
1483
- const noCriteriaCheck = lacksCriteriaCheck(tasks);
1482
+ const v1Prints = printsV1(tasks);
1484
1483
  if (phasePlan !== void 0 && "unusable" in phasePlan) {
1485
1484
  lines.push(refusal("O8", taskPlanLineText.O8Unusable(phasePlan.file, phasePlanUnusableWhy(phasePlan.unusable, request.phase))));
1486
1485
  }
@@ -1546,7 +1545,7 @@ function checkTaskPlan(plan, request) {
1546
1545
  lines.push(line);
1547
1546
  }
1548
1547
  }
1549
- if (!noCriteriaCheck && mustNameHolder(task) && task.agent === void 0) {
1548
+ if (!v1Prints && mustNameHolder(task) && task.agent === void 0) {
1550
1549
  lines.push(refusal("V4", taskPlanLineText.V4(task.id)));
1551
1550
  }
1552
1551
  const cheap = cheapFirstLine(task, request.crew);
@@ -1601,11 +1600,11 @@ var criteriaRecordLineText = {
1601
1600
  M2: (file, x) => `${file} is from "${x}", which is not a check of the phase against its criteria in the plan. Name the check task that made it.`,
1602
1601
  M3: (file, m, c, k) => `${file} is by ${m}, but "${c}" is held by ${k}. The record names the crew member who holds the check.`,
1603
1602
  M4: (crit) => `${crit} has no row. Check it, and add it as met with what shows it, or as a fix with the gap and the fix task.`,
1604
- M5: (file, x, folder) => `${file} has a row for "${x}", which is not a live criterion of this phase's features in ${folder}. Remove it, or correct it.`,
1603
+ M5: (file, x, folder2) => `${file} has a row for "${x}", which is not a live criterion of this phase's features in ${folder2}. Remove it, or correct it.`,
1605
1604
  M6: (crit) => `${crit} has more than one row. Keep one.`,
1606
1605
  M7: (crit, x) => `${crit} is fixed by "${x}", but the plan has no build task "${x}" that names ${crit} in its criteria. Add the task, or name it there.`,
1607
- M8Unmarked: (crit, folder, slug) => `${crit} is not marked an edge case in ${folder}/${slug}.md, so its row does not carry "edge".`,
1608
- M8Marked: (crit, folder, slug) => `${crit} is marked an edge case in ${folder}/${slug}.md, so its row carries "edge": true.`,
1606
+ M8Unmarked: (crit, folder2, slug) => `${crit} is not marked an edge case in ${folder2}/${slug}.md, so its row does not carry "edge".`,
1607
+ M8Marked: (crit, folder2, slug) => `${crit} is marked an edge case in ${folder2}/${slug}.md, so its row carries "edge": true.`,
1609
1608
  M9Evidence: (crit, t) => `${crit} is an edge case, so it is met only by the test written in "${t}", run on a task that builds it. Give its evidence as a test with "writtenIn": "${t}" and the task that built it.`,
1610
1609
  M9NoTestTask: (crit) => `${crit} is an edge case, and the plan has no task that writes its test, so it cannot be counted as met. Run check-tasks and add that task.`,
1611
1610
  P: (met, fix) => `The record covers every criterion of this phase's features: ${met} met, ${fix} to fix.`
@@ -1647,14 +1646,14 @@ function slugOf(criterion) {
1647
1646
  function refusal2(code, text) {
1648
1647
  return { code, text, refuses: true };
1649
1648
  }
1650
- function edgeRowLines(row, isEdge, testTask, plan, folder) {
1649
+ function edgeRowLines(row, isEdge, testTask, plan, folder2) {
1651
1650
  const crit = row.criterion;
1652
1651
  const lines = [];
1653
1652
  const marked = row.edge === true;
1654
1653
  if (marked && !isEdge)
1655
- lines.push(refusal2("M8", criteriaRecordLineText.M8Unmarked(crit, folder, slugOf(crit))));
1654
+ lines.push(refusal2("M8", criteriaRecordLineText.M8Unmarked(crit, folder2, slugOf(crit))));
1656
1655
  if (!marked && isEdge)
1657
- lines.push(refusal2("M8", criteriaRecordLineText.M8Marked(crit, folder, slugOf(crit))));
1656
+ lines.push(refusal2("M8", criteriaRecordLineText.M8Marked(crit, folder2, slugOf(crit))));
1658
1657
  if (!isEdge || row.status !== "met")
1659
1658
  return lines;
1660
1659
  if (testTask === null) {
@@ -2161,6 +2160,237 @@ function readyTasks(rows) {
2161
2160
  return { ready: startable.map((entry) => entry.id), person };
2162
2161
  }
2163
2162
 
2163
+ // ../core/dist/request-shape.js
2164
+ var REQUEST_SHAPES_DIR_NAME = "debug";
2165
+ var REQUEST_SHAPES_SWITCH_FILENAME = "request-shapes.on";
2166
+ var REQUEST_SHAPES_LOG_FILENAME = "request-shapes.jsonl";
2167
+ var REQUEST_SHAPE_MAX_DEPTH = 8;
2168
+ var REQUEST_SHAPE_MAX_ARRAY_SHAPES = 10;
2169
+ var REQUEST_SHAPE_MAX_KEY_LENGTH = 64;
2170
+ var REQUEST_SHAPE_MAX_LINE_BYTES = 16 * 1024;
2171
+ var KEY_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
2172
+ var PATH_WORD = /^[a-z-]+$/;
2173
+ function byteLength(text) {
2174
+ return new TextEncoder().encode(text).length;
2175
+ }
2176
+ function keyOf(key) {
2177
+ return key.length <= REQUEST_SHAPE_MAX_KEY_LENGTH && KEY_NAME.test(key) ? key : "<key>";
2178
+ }
2179
+ function shapeOfValue(value, depth) {
2180
+ if (value === null)
2181
+ return "null";
2182
+ if (typeof value === "string")
2183
+ return "string";
2184
+ if (typeof value === "number")
2185
+ return "number";
2186
+ if (typeof value === "boolean")
2187
+ return "boolean";
2188
+ if (depth > REQUEST_SHAPE_MAX_DEPTH)
2189
+ return "\u2026";
2190
+ if (Array.isArray(value)) {
2191
+ const seen = [];
2192
+ for (const element of value) {
2193
+ if (seen.length >= REQUEST_SHAPE_MAX_ARRAY_SHAPES)
2194
+ break;
2195
+ const shape = shapeOfValue(element, depth + 1);
2196
+ if (!seen.includes(shape))
2197
+ seen.push(shape);
2198
+ }
2199
+ return `[${seen.join("|")}]`;
2200
+ }
2201
+ if (typeof value === "object") {
2202
+ const entries = Object.entries(value);
2203
+ if (entries.length === 0)
2204
+ return "{}";
2205
+ return `{ ${entries.map(([k, v]) => `${keyOf(k)}: ${shapeOfValue(v, depth + 1)}`).join(", ")} }`;
2206
+ }
2207
+ return "null";
2208
+ }
2209
+ function requestShapeOf(body) {
2210
+ let parsed;
2211
+ try {
2212
+ parsed = JSON.parse(body);
2213
+ } catch {
2214
+ return `text ${byteLength(body)} bytes`;
2215
+ }
2216
+ return shapeOfValue(parsed, 1);
2217
+ }
2218
+ function requestPathShapeOf(pathname) {
2219
+ return pathname.split("/").map((segment) => segment === "" || PATH_WORD.test(segment) ? segment : ":x").join("/");
2220
+ }
2221
+ function distinct2(names) {
2222
+ const out = [];
2223
+ for (const name of names)
2224
+ if (!out.includes(name))
2225
+ out.push(name);
2226
+ return out;
2227
+ }
2228
+ function isObject(value) {
2229
+ return typeof value === "object" && value !== null;
2230
+ }
2231
+ function isRequestLike(value) {
2232
+ return isObject(value) && !(value instanceof URL) && typeof value["url"] === "string" && typeof value["method"] === "string";
2233
+ }
2234
+ function headerNamesOf(headers) {
2235
+ if (!isObject(headers))
2236
+ return [];
2237
+ const names = [];
2238
+ if (Array.isArray(headers)) {
2239
+ for (const pair of headers)
2240
+ if (Array.isArray(pair) && typeof pair[0] === "string")
2241
+ names.push(pair[0].toLowerCase());
2242
+ return names;
2243
+ }
2244
+ const forEach = headers["forEach"];
2245
+ if (typeof forEach === "function") {
2246
+ forEach.call(headers, (_value, key) => {
2247
+ if (typeof key === "string")
2248
+ names.push(key.toLowerCase());
2249
+ });
2250
+ return names;
2251
+ }
2252
+ return Object.keys(headers).map((k) => k.toLowerCase());
2253
+ }
2254
+ function bytesShape(bytes) {
2255
+ let text;
2256
+ try {
2257
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2258
+ } catch {
2259
+ return `text ${bytes.byteLength} bytes`;
2260
+ }
2261
+ return requestShapeOf(text);
2262
+ }
2263
+ function bodyShapeOf(body) {
2264
+ if (body === void 0 || body === null)
2265
+ return null;
2266
+ if (typeof body === "string")
2267
+ return requestShapeOf(body);
2268
+ if (body instanceof URLSearchParams)
2269
+ return `text ${byteLength(body.toString())} bytes`;
2270
+ if (body instanceof ArrayBuffer)
2271
+ return bytesShape(new Uint8Array(body));
2272
+ if (ArrayBuffer.isView(body))
2273
+ return bytesShape(new Uint8Array(body.buffer, body.byteOffset, body.byteLength));
2274
+ if (isObject(body) && typeof body["size"] === "number" && typeof body["arrayBuffer"] === "function") {
2275
+ return `text ${body["size"]} bytes`;
2276
+ }
2277
+ return "stream";
2278
+ }
2279
+ function requestLineOf(input, init) {
2280
+ const request = isRequestLike(input) ? input : void 0;
2281
+ const address = request !== void 0 ? request.url : input instanceof URL ? input.href : String(input);
2282
+ let url;
2283
+ try {
2284
+ url = new URL(address);
2285
+ } catch {
2286
+ url = void 0;
2287
+ }
2288
+ const options = isObject(init) ? init : {};
2289
+ const method = (typeof options["method"] === "string" ? options["method"] : request?.method ?? "GET").toUpperCase();
2290
+ const headers = distinct2([
2291
+ ...request !== void 0 ? headerNamesOf(request.headers) : [],
2292
+ ...headerNamesOf(options["headers"])
2293
+ ]);
2294
+ const shape = options["body"] !== void 0 && options["body"] !== null ? bodyShapeOf(options["body"]) : request !== void 0 && request.body !== null && request.body !== void 0 ? "stream" : null;
2295
+ return {
2296
+ method,
2297
+ host: url?.host ?? "",
2298
+ path: url !== void 0 ? requestPathShapeOf(url.pathname) : ":x",
2299
+ query: url !== void 0 ? distinct2([...url.searchParams.keys()]) : [],
2300
+ headers,
2301
+ shape
2302
+ };
2303
+ }
2304
+ function requestShapeLineText(line) {
2305
+ const ordered = (shape) => JSON.stringify({
2306
+ at: line.at,
2307
+ process: line.process,
2308
+ method: line.method,
2309
+ host: line.host,
2310
+ path: line.path,
2311
+ query: line.query,
2312
+ headers: line.headers,
2313
+ shape,
2314
+ status: line.status
2315
+ });
2316
+ const whole = ordered(line.shape);
2317
+ if (byteLength(whole) <= REQUEST_SHAPE_MAX_LINE_BYTES || line.shape === null)
2318
+ return whole;
2319
+ const room = REQUEST_SHAPE_MAX_LINE_BYTES - byteLength(ordered("")) - byteLength("\u2026");
2320
+ if (room <= 0)
2321
+ return ordered("\u2026");
2322
+ let end = Math.min(line.shape.length, room);
2323
+ for (let over = byteLength(line.shape.slice(0, end)) - room; over > 0; over = byteLength(line.shape.slice(0, end)) - room) {
2324
+ end -= over;
2325
+ }
2326
+ const prefix = line.shape.slice(0, Math.max(0, end));
2327
+ const cut = Math.max(prefix.lastIndexOf(", "), prefix.lastIndexOf("|"));
2328
+ return ordered(`${cut > 0 ? prefix.slice(0, cut) : ""}\u2026`);
2329
+ }
2330
+
2331
+ // dist/request-shapes.js
2332
+ var folder = join(homedir(), HALFCYCLE_DIR_NAME, REQUEST_SHAPES_DIR_NAME);
2333
+ var logPath = join(folder, REQUEST_SHAPES_LOG_FILENAME);
2334
+ var switchedOn = existsSync(join(folder, REQUEST_SHAPES_SWITCH_FILENAME));
2335
+ var knownVerbs = [];
2336
+ function knowRequestShapeVerbs(verbs) {
2337
+ knownVerbs = verbs;
2338
+ }
2339
+ function processName() {
2340
+ const verb = process.argv[2];
2341
+ return verb !== void 0 && knownVerbs.includes(verb) ? `cli ${verb}` : "cli other";
2342
+ }
2343
+ function write(call, status) {
2344
+ if (call === void 0)
2345
+ return;
2346
+ try {
2347
+ const text = requestShapeLineText({ at: (/* @__PURE__ */ new Date()).toISOString(), process: processName(), ...call, status });
2348
+ appendFileSync(logPath, `${text}
2349
+ `, { mode: 384 });
2350
+ } catch {
2351
+ }
2352
+ }
2353
+ function describe(args2) {
2354
+ try {
2355
+ return requestLineOf(args2[0], args2[1]);
2356
+ } catch {
2357
+ return void 0;
2358
+ }
2359
+ }
2360
+ if (switchedOn) {
2361
+ const original = globalThis.fetch;
2362
+ if (typeof original === "function") {
2363
+ globalThis.fetch = function recordedFetch(...args2) {
2364
+ const call = describe(args2);
2365
+ let answer;
2366
+ try {
2367
+ answer = original.apply(this, args2);
2368
+ } catch (err) {
2369
+ write(call, "unreachable");
2370
+ throw err;
2371
+ }
2372
+ return answer.then((res) => {
2373
+ write(call, res.status);
2374
+ return res;
2375
+ }, (err) => {
2376
+ write(call, "unreachable");
2377
+ throw err;
2378
+ });
2379
+ };
2380
+ }
2381
+ }
2382
+
2383
+ // dist/bin.js
2384
+ import { existsSync as existsSync14, statSync as statSync2, writeSync as writeSync3 } from "node:fs";
2385
+
2386
+ // dist/install.js
2387
+ import { execFileSync as execFileSync2 } from "node:child_process";
2388
+ import { createHash as createHash2 } from "node:crypto";
2389
+ import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
2390
+ import { homedir as homedir3 } from "node:os";
2391
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7, posix as posix2, relative, resolve } from "node:path";
2392
+ import { fileURLToPath } from "node:url";
2393
+
2164
2394
  // ../events/dist/result.js
2165
2395
  import { z as z3 } from "zod";
2166
2396
  var firedGuardSchema = z3.object({
@@ -4083,9 +4313,9 @@ function whereOf(owns) {
4083
4313
  return isBareFileName(lastSegment) ? `in ${pattern}` : `under ${pattern}/`;
4084
4314
  }
4085
4315
  if (lastSegment === "*") {
4086
- const folder = segments.slice(0, -1).join("/");
4087
- if (folder !== "" && !hasWildcard(folder))
4088
- return `in the ${folder} folder`;
4316
+ const folder2 = segments.slice(0, -1).join("/");
4317
+ if (folder2 !== "" && !hasWildcard(folder2))
4318
+ return `in the ${folder2} folder`;
4089
4319
  }
4090
4320
  const prefix = leadingLiteralPrefix(segments);
4091
4321
  if (ownsEverythingUnderPrefix(segments))
@@ -6361,6 +6591,10 @@ var limitHeldSchema = z33.object({
6361
6591
  raiseBy: z33.number().int().nonnegative(),
6362
6592
  working: z33.number().int().nonnegative()
6363
6593
  }).strict();
6594
+ var paceUsageSchema = z33.object({
6595
+ used: z33.number().int().nonnegative(),
6596
+ limit: z33.number().int().positive()
6597
+ }).strict();
6364
6598
  var paceRunResultSchema = z33.discriminatedUnion("outcome", [
6365
6599
  z33.object({
6366
6600
  outcome: z33.literal("paced"),
@@ -6372,7 +6606,8 @@ var paceRunResultSchema = z33.discriminatedUnion("outcome", [
6372
6606
  keepFreePercent: keepFreePercentSchema.optional(),
6373
6607
  suggestion: paceSuggestionSchema.optional(),
6374
6608
  limit: z33.literal("reached").optional(),
6375
- limitHeld: limitHeldSchema.optional()
6609
+ limitHeld: limitHeldSchema.optional(),
6610
+ usage: paceUsageSchema.optional()
6376
6611
  }).strict(),
6377
6612
  z33.object({ outcome: z33.literal("not-paced"), reason: z33.enum(PACE_NOT_PACED_REASONS) }).strict()
6378
6613
  ]);
@@ -6463,6 +6698,12 @@ function paceS2Of(window, k, until, now, keepFreePercent) {
6463
6698
  function paceAllowedNowOf(k) {
6464
6699
  return k === 0 ? "Start no more agents now." : `Up to ${String(k)} more ${k === 1 ? "agent" : "agents"} can start now.`;
6465
6700
  }
6701
+ function paceBuildUpToOf(n) {
6702
+ return `This build runs up to ${paceAgentsOf(n)} at once.`;
6703
+ }
6704
+ function limitUsedOf(used, limit) {
6705
+ return `This phase has used ${tokenCountWordsOf(used)} of its limit of ${tokenCountWordsOf(limit)}.`;
6706
+ }
6466
6707
  var PACE_AGENTS_TOO_MANY = `Run at most ${String(PACE_AGENTS_MAX)} agents at once.`;
6467
6708
  var LIMIT_HISTORY_MAX = 20;
6468
6709
  var LIMIT_PARTS = ["half", "three-quarters", "nine-tenths", "limit"];
@@ -6821,9 +7062,13 @@ function routeStartLineOf(holder, task, tier) {
6821
7062
  function routeStartUnknownLineOf(holder, task) {
6822
7063
  return `Start ${holder} on ${task}.`;
6823
7064
  }
7065
+ function routePassedLineOf(task, tier) {
7066
+ return `${task} has passed its review, built on the ${tier} tier. There is nothing to start.`;
7067
+ }
6824
7068
  function routeNoSuchTaskLineOf(task) {
6825
7069
  return `"${task}" is not a task in the plan. Check the task id, then run this again.`;
6826
7070
  }
7071
+ var ROUTING_FILENAME = "routing.json";
6827
7072
  var routingRecordSchema = z36.object({
6828
7073
  phase: z36.string().min(1).max(200),
6829
7074
  cheapStarted: z36.array(z36.string().min(1).max(200)).max(2e3)
@@ -6879,20 +7124,26 @@ function seenModelsLineOf(task, models) {
6879
7124
  return `Seen in your coding agent's own records: ${task} ran on ${joinedOf(models)}.`;
6880
7125
  }
6881
7126
 
7127
+ // ../events/dist/checks-report-words.js
7128
+ var CHECKS_NO_REPORT_YET_STATE = "Set up, no report yet";
7129
+ var CHECKS_NO_REPORT_YET_WHEN = "Halfcycle's checks report after the first file edit they can see. A new shell file counts once added to git.";
7130
+ var CHECKS_IF_STILL_UNREPORTED = "If edits go unreported: Codex, approve /hooks; Claude Code, trust this folder.";
7131
+ var CHECKS_GONE_QUIET = "Halfcycle's checks have not reported from this machine lately.";
7132
+
6882
7133
  // dist/engagement-credential.js
6883
7134
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
6884
7135
  import { platform as platform2 } from "node:os";
6885
- import { join as join2 } from "node:path";
7136
+ import { join as join3 } from "node:path";
6886
7137
 
6887
7138
  // dist/account-credential.js
6888
7139
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6889
- import { homedir, platform } from "node:os";
6890
- import { dirname, join } from "node:path";
7140
+ import { homedir as homedir2, platform } from "node:os";
7141
+ import { dirname, join as join2 } from "node:path";
6891
7142
  function halfcycleHome(home) {
6892
- return join(home ?? homedir(), HALFCYCLE_DIR_NAME);
7143
+ return join2(home ?? homedir2(), HALFCYCLE_DIR_NAME);
6893
7144
  }
6894
7145
  function accountStorePath(home) {
6895
- return join(halfcycleHome(home), ACCOUNT_STORE_FILENAME);
7146
+ return join2(halfcycleHome(home), ACCOUNT_STORE_FILENAME);
6896
7147
  }
6897
7148
  function readStore(home) {
6898
7149
  try {
@@ -6958,13 +7209,13 @@ function assertEngagementId(engagementId) {
6958
7209
  }
6959
7210
  function engagementStateDir(engagementId, home) {
6960
7211
  assertEngagementId(engagementId);
6961
- return join2(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
7212
+ return join3(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
6962
7213
  }
6963
7214
  function engagementEnvPath(engagementId, home) {
6964
- return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
7215
+ return join3(engagementStateDir(engagementId, home), ENV_FILENAME);
6965
7216
  }
6966
7217
  function notThisAccountMarkerPath(engagementId, home) {
6967
- return join2(engagementStateDir(engagementId, home), NOT_THIS_ACCOUNT_MARKER_FILENAME);
7218
+ return join3(engagementStateDir(engagementId, home), NOT_THIS_ACCOUNT_MARKER_FILENAME);
6968
7219
  }
6969
7220
  function writeNotThisAccountMarker(engagementId, home) {
6970
7221
  const dir = engagementStateDir(engagementId, home);
@@ -7089,8 +7340,8 @@ halfcycle_env_file() {
7089
7340
 
7090
7341
  // dist/identity.js
7091
7342
  import { execFileSync } from "node:child_process";
7092
- import { existsSync, linkSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
7093
- import { dirname as dirname2, isAbsolute, join as join3 } from "node:path";
7343
+ import { existsSync as existsSync2, linkSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
7344
+ import { dirname as dirname2, isAbsolute, join as join4 } from "node:path";
7094
7345
  import { randomUUID } from "node:crypto";
7095
7346
  var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
7096
7347
  var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
@@ -7221,7 +7472,7 @@ function readIgnoredPaths(targetRepoRoot, paths) {
7221
7472
  const toplevel = git(targetRepoRoot, ["rev-parse", "--show-toplevel"]);
7222
7473
  if (toplevel === null)
7223
7474
  return null;
7224
- return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source: join3(toplevel, entry.source) });
7475
+ return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source: join4(toplevel, entry.source) });
7225
7476
  }
7226
7477
  function mintIdentity(targetRepoRoot) {
7227
7478
  return {
@@ -7232,7 +7483,7 @@ function mintIdentity(targetRepoRoot) {
7232
7483
  };
7233
7484
  }
7234
7485
  function plannedRemoteRepair(targetRepoRoot) {
7235
- const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
7486
+ const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
7236
7487
  let onDisk;
7237
7488
  try {
7238
7489
  const parsed = JSON.parse(readFileSync3(path, "utf-8"));
@@ -7254,7 +7505,7 @@ function plannedRemoteRepair(targetRepoRoot) {
7254
7505
  });
7255
7506
  }
7256
7507
  function replaceProjectIdentityFile(targetRepoRoot, text) {
7257
- const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
7508
+ const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
7258
7509
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
7259
7510
  try {
7260
7511
  writeFileSync3(temporary, text, { encoding: "utf-8", flag: "wx" });
@@ -7265,8 +7516,8 @@ function replaceProjectIdentityFile(targetRepoRoot, text) {
7265
7516
  }
7266
7517
  }
7267
7518
  function planProjectIdentity(targetRepoRoot) {
7268
- const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
7269
- if (!existsSync(path)) {
7519
+ const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
7520
+ if (!existsSync2(path)) {
7270
7521
  return { action: "add", after: serializeProjectIdentity(mintIdentity(targetRepoRoot)) };
7271
7522
  }
7272
7523
  const repaired = plannedRemoteRepair(targetRepoRoot);
@@ -7284,7 +7535,7 @@ function serializeProjectIdentity(identity) {
7284
7535
  return JSON.stringify(identity, null, 2) + "\n";
7285
7536
  }
7286
7537
  function createProjectIdentityFile(targetRepoRoot, text) {
7287
- const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
7538
+ const path = join4(targetRepoRoot, PROJECT_IDENTITY_REL);
7288
7539
  mkdirSync3(dirname2(path), { recursive: true });
7289
7540
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
7290
7541
  try {
@@ -7304,8 +7555,8 @@ function createProjectIdentityFile(targetRepoRoot, text) {
7304
7555
  }
7305
7556
 
7306
7557
  // dist/install-plan.js
7307
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
7308
- import { join as join5, posix } from "node:path";
7558
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
7559
+ import { join as join6, posix } from "node:path";
7309
7560
 
7310
7561
  // dist/install-manifest.js
7311
7562
  import { Buffer as Buffer2 } from "node:buffer";
@@ -7656,8 +7907,8 @@ function union(a, b) {
7656
7907
  }
7657
7908
 
7658
7909
  // dist/scan.js
7659
- import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
7660
- import { join as join4 } from "node:path";
7910
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
7911
+ import { join as join5 } from "node:path";
7661
7912
  var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
7662
7913
  var HALFCYCLE_STATE_NOTE = [
7663
7914
  "This file is a local cache written ONCE when Halfcycle was installed. It is not a",
@@ -7680,7 +7931,7 @@ var LAYER_ARTEFACT_PATHS = {
7680
7931
  function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOString()) {
7681
7932
  const layers = {};
7682
7933
  for (const [layer, paths] of Object.entries(LAYER_ARTEFACT_PATHS)) {
7683
- const found = paths.filter((p) => existsSync2(join4(targetRepo, p)));
7934
+ const found = paths.filter((p) => existsSync3(join5(targetRepo, p)));
7684
7935
  if (found.length > 0) {
7685
7936
  layers[layer] = {
7686
7937
  value: "asserted",
@@ -7694,8 +7945,8 @@ function scanLayers(targetRepo, scannedAt = (/* @__PURE__ */ new Date()).toISOSt
7694
7945
  }
7695
7946
  var BOOTSTRAP_STATE_REL = ".halfcycle/state.json";
7696
7947
  function planBootstrapScan(targetRepo) {
7697
- const statePath = join4(targetRepo, BOOTSTRAP_STATE_REL);
7698
- if (existsSync2(statePath)) {
7948
+ const statePath = join5(targetRepo, BOOTSTRAP_STATE_REL);
7949
+ if (existsSync3(statePath)) {
7699
7950
  const existing = JSON.parse(readFileSync4(statePath, "utf-8"));
7700
7951
  return { state: existing, text: null };
7701
7952
  }
@@ -7703,8 +7954,8 @@ function planBootstrapScan(targetRepo) {
7703
7954
  return { state, text: JSON.stringify(state, null, 2) + "\n" };
7704
7955
  }
7705
7956
  function writeBootstrapState(targetRepo, text) {
7706
- mkdirSync4(join4(targetRepo, ".halfcycle"), { recursive: true });
7707
- writeFileSync4(join4(targetRepo, BOOTSTRAP_STATE_REL), text, "utf-8");
7957
+ mkdirSync4(join5(targetRepo, ".halfcycle"), { recursive: true });
7958
+ writeFileSync4(join5(targetRepo, BOOTSTRAP_STATE_REL), text, "utf-8");
7708
7959
  }
7709
7960
 
7710
7961
  // dist/install-plan.js
@@ -7854,12 +8105,12 @@ function planInstall(options) {
7854
8105
  throw new Error("[halfcycle] a plan for a project needs its kind as well as its id.");
7855
8106
  }
7856
8107
  }
7857
- if (!existsSync3(targetRepo)) {
8108
+ if (!existsSync4(targetRepo)) {
7858
8109
  throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
7859
8110
  }
7860
8111
  const writeSet = closedWriteSet();
7861
8112
  const previous = readPreviousManifest(targetRepo);
7862
- const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
8113
+ const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync4(join6(targetRepo, dir))));
7863
8114
  const { version, commands } = planCommandStubs(targetRepo, previous, writeSet);
7864
8115
  const codexSkills = planCodexSkills(targetRepo, previous, writeSet);
7865
8116
  const vendored = planVendoredBinary(targetRepo);
@@ -7881,7 +8132,7 @@ function planInstall(options) {
7881
8132
  const gitattributes = planGitattributes(targetRepo);
7882
8133
  const identity = planProjectIdentity(targetRepo);
7883
8134
  if (identity.after === null)
7884
- readFileSync5(join5(targetRepo, PROJECT_IDENTITY_REL));
8135
+ readFileSync5(join6(targetRepo, PROJECT_IDENTITY_REL));
7885
8136
  const credentialPlan = !preview && credential !== void 0 ? planEngagementCredential(credential, engagementId, home) : null;
7886
8137
  const credentialWrite = credentialPlan === null ? null : { outcome: credentialPlan.outcome, existed: credentialPlan.existed, path: engagementEnvPath(engagementId, home) };
7887
8138
  const storedAfter = credentialPlan?.storedAfter ?? (preview ? Object.fromEntries(ENGAGEMENT_ENV_KEYS.map((key) => [key, "supplied at sign-in"])) : readEngagementEnv(engagementId, home) ?? {});
@@ -8014,7 +8265,7 @@ function entriesOf(steps, record2) {
8014
8265
  }
8015
8266
  };
8016
8267
  const pending = (rel) => {
8017
- const there = existsSync3(join5(targetRepo, rel));
8268
+ const there = existsSync4(join6(targetRepo, rel));
8018
8269
  entries.push({ path: rel, action: there ? "change" : "add", kind: kindOf(rel), bytes: PENDING });
8019
8270
  };
8020
8271
  steps.commands.forEach(fromFile);
@@ -8062,7 +8313,7 @@ function entriesOf(steps, record2) {
8062
8313
  action: "leave",
8063
8314
  kind: "codex-server",
8064
8315
  bytes: null,
8065
- ...existsSync3(join5(targetRepo, CODEX_CONFIG_REL2)) ? { yours: true } : {}
8316
+ ...existsSync4(join6(targetRepo, CODEX_CONFIG_REL2)) ? { yours: true } : {}
8066
8317
  });
8067
8318
  break;
8068
8319
  }
@@ -8103,11 +8354,11 @@ function outsideOf(steps) {
8103
8354
  outside.push({ kind: "new-project" });
8104
8355
  return outside;
8105
8356
  }
8106
- const write = steps.credentialWrite;
8107
- if (write === null)
8357
+ const write2 = steps.credentialWrite;
8358
+ if (write2 === null)
8108
8359
  return [];
8109
- const action = write.outcome === "skipped" ? "leave" : write.existed ? "change" : "add";
8110
- return [{ kind: "sign-in", path: write.path, action }];
8360
+ const action = write2.outcome === "skipped" ? "leave" : write2.existed ? "change" : "add";
8361
+ return [{ kind: "sign-in", path: write2.path, action }];
8111
8362
  }
8112
8363
  function projectPinned(targetRepo) {
8113
8364
  try {
@@ -8141,10 +8392,10 @@ function applyInstallPlan(plan) {
8141
8392
  putPlannedCodexHooks(targetRepo, steps.codexHooks);
8142
8393
  putPlannedCodexConfig(targetRepo, steps.codexConfig);
8143
8394
  const gitignore = putPlannedGitignore(targetRepo, steps.gitignore);
8144
- const gitignorePath = join5(targetRepo, GITIGNORE_REL);
8145
- const gitignoreAfter = existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
8395
+ const gitignorePath = join6(targetRepo, GITIGNORE_REL);
8396
+ const gitignoreAfter = existsSync4(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : null;
8146
8397
  const gitattributes = putPlannedGitattributes(targetRepo, steps.gitattributes);
8147
- const gitattributesAfter = readTextOrNull(join5(targetRepo, GITATTRIBUTES_REL));
8398
+ const gitattributesAfter = readTextOrNull(join6(targetRepo, GITATTRIBUTES_REL));
8148
8399
  writeProjectIdentity(targetRepo, steps.identity);
8149
8400
  const credential = steps.credential;
8150
8401
  const credentialOutcome = steps.credentialWrite !== null && credential !== void 0 ? writeEngagementCredential(credential, engagementId, steps.home) : null;
@@ -8165,9 +8416,9 @@ function applyInstallPlan(plan) {
8165
8416
  gitattributesAfter,
8166
8417
  credential: credentialOutcome,
8167
8418
  legacy,
8168
- bytesAfter: (rel) => readFileSync5(join5(targetRepo, rel)),
8169
- dirExistsAfter: (dir) => existsSync3(join5(targetRepo, dir)),
8170
- stateExistsAfter: existsSync3(join5(targetRepo, BOOTSTRAP_STATE_REL))
8419
+ bytesAfter: (rel) => readFileSync5(join6(targetRepo, rel)),
8420
+ dirExistsAfter: (dir) => existsSync4(join6(targetRepo, dir)),
8421
+ stateExistsAfter: existsSync4(join6(targetRepo, BOOTSTRAP_STATE_REL))
8171
8422
  });
8172
8423
  const record2 = planInstallRecord(targetRepo, manifestText);
8173
8424
  putPlannedFile(targetRepo, record2);
@@ -8524,10 +8775,10 @@ function crewProfileRel(agent, callsign) {
8524
8775
  return agent === "claude-code" ? `${CLAUDE_AGENTS_DIR}/${callsign}.md` : `${CODEX_AGENTS_DIR}/${callsign.toLowerCase()}.toml`;
8525
8776
  }
8526
8777
  function crewProfileTarget(targetRepo, rel) {
8527
- const folder = rel.startsWith(`${CLAUDE_AGENTS_DIR}/`) ? CLAUDE_AGENTS_DIR : CODEX_AGENTS_DIR;
8528
- const target = resolve(join6(targetRepo, rel));
8529
- if (dirname3(target) !== resolve(targetRepo, folder)) {
8530
- throw new Error(`[halfcycle] crew profile "${rel}" does not sit directly in ${folder}.`);
8778
+ const folder2 = rel.startsWith(`${CLAUDE_AGENTS_DIR}/`) ? CLAUDE_AGENTS_DIR : CODEX_AGENTS_DIR;
8779
+ const target = resolve(join7(targetRepo, rel));
8780
+ if (dirname3(target) !== resolve(targetRepo, folder2)) {
8781
+ throw new Error(`[halfcycle] crew profile "${rel}" does not sit directly in ${folder2}.`);
8531
8782
  }
8532
8783
  return target;
8533
8784
  }
@@ -8614,35 +8865,35 @@ var __dirname = dirname3(__filename);
8614
8865
  function findBundleRoot(startDir) {
8615
8866
  let candidate = startDir;
8616
8867
  for (let i = 0; i < 4; i++) {
8617
- const manifest = join6(candidate, ".claude-plugin", "plugin.json");
8868
+ const manifest = join7(candidate, ".claude-plugin", "plugin.json");
8618
8869
  try {
8619
- if (existsSync4(manifest))
8870
+ if (existsSync5(manifest))
8620
8871
  return candidate;
8621
8872
  } catch {
8622
8873
  }
8623
8874
  candidate = dirname3(candidate);
8624
8875
  }
8625
- return join6(startDir, "..");
8876
+ return join7(startDir, "..");
8626
8877
  }
8627
8878
  var BUNDLE_ROOT = findBundleRoot(__dirname);
8628
8879
  function resolveVendoredBinary() {
8629
8880
  const candidates = [
8630
- join6(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
8631
- join6(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
8881
+ join7(BUNDLE_ROOT, "bin", "bin.bundle.mjs"),
8882
+ join7(BUNDLE_ROOT, "..", "runner", "dist", "bin.bundle.mjs")
8632
8883
  ];
8633
8884
  for (const candidate of candidates) {
8634
- if (existsSync4(candidate))
8885
+ if (existsSync5(candidate))
8635
8886
  return candidate;
8636
8887
  }
8637
8888
  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(", ")}.`);
8638
8889
  }
8639
8890
  function resolvePreFilterShell() {
8640
8891
  const candidates = [
8641
- join6(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
8642
- join6(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
8892
+ join7(BUNDLE_ROOT, "bin", PRE_FILTER_SHELL_FILENAME),
8893
+ join7(BUNDLE_ROOT, "..", "runner", "dist", PRE_FILTER_SHELL_FILENAME)
8643
8894
  ];
8644
8895
  for (const candidate of candidates) {
8645
- if (existsSync4(candidate))
8896
+ if (existsSync5(candidate))
8646
8897
  return readFileSync6(candidate, "utf-8");
8647
8898
  }
8648
8899
  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(", ")}.`);
@@ -8682,7 +8933,7 @@ function isAllowlisted(targetRelPath) {
8682
8933
  var CREW_ROSTER_REL = ".halfcycle/crew.json";
8683
8934
  var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
8684
8935
  function capturedIndexScaffold() {
8685
- return join6(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
8936
+ return join7(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
8686
8937
  }
8687
8938
  var GITIGNORE_REL = ".gitignore";
8688
8939
  var GITATTRIBUTES_REL = ".gitattributes";
@@ -8757,13 +9008,13 @@ function closedWriteSet() {
8757
9008
  function sha256OfWhatThisVersionWrites(commands) {
8758
9009
  const out = /* @__PURE__ */ new Map();
8759
9010
  const fromFile = (rel, src) => {
8760
- if (existsSync4(src))
9011
+ if (existsSync5(src))
8761
9012
  out.set(rel, fileSha256(readFileSync6(src)));
8762
9013
  };
8763
9014
  for (const cmd of commands) {
8764
- const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
9015
+ const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
8765
9016
  fromFile(commandStubRel(cmd.name), srcFile);
8766
- if (existsSync4(srcFile)) {
9017
+ if (existsSync5(srcFile)) {
8767
9018
  out.set(skillRel(cmd.name), fileSha256(Buffer.from(codexSkillContent(cmd.name, readFileSync6(srcFile, "utf-8")), "utf-8")));
8768
9019
  }
8769
9020
  }
@@ -8781,7 +9032,7 @@ function bytesAreOurs(path, bytes, recorded, writeSet) {
8781
9032
  }
8782
9033
  function readPreviousManifest(targetRepo) {
8783
9034
  try {
8784
- return parseManifest(readFileSync6(join6(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
9035
+ return parseManifest(readFileSync6(join7(targetRepo, INSTALL_MANIFEST_REL), "utf-8"));
8785
9036
  } catch {
8786
9037
  return null;
8787
9038
  }
@@ -8885,8 +9136,8 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
8885
9136
  for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
8886
9137
  let plain = dirVerdicts.get(dir);
8887
9138
  if (plain === void 0) {
8888
- const abs = join6(targetRepo, dir);
8889
- plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join6(abs, ".git")) === null;
9139
+ const abs = join7(targetRepo, dir);
9140
+ plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join7(abs, ".git")) === null;
8890
9141
  dirVerdicts.set(dir, plain);
8891
9142
  }
8892
9143
  if (!plain)
@@ -8900,7 +9151,7 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
8900
9151
  ...merged,
8901
9152
  INSTALL_MANIFEST_REL
8902
9153
  ])
8903
- ].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join6(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
9154
+ ].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join7(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
8904
9155
  const ignored = readIgnoredPaths(targetRepo, paths);
8905
9156
  if (ignored === null)
8906
9157
  return null;
@@ -8932,7 +9183,7 @@ function isPlainObject2(value) {
8932
9183
  return value !== null && typeof value === "object" && !Array.isArray(value);
8933
9184
  }
8934
9185
  function readPluginManifest() {
8935
- const manifestPath = join6(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
9186
+ const manifestPath = join7(BUNDLE_ROOT, ".claude-plugin", "plugin.json");
8936
9187
  const raw = readFileSync6(manifestPath, "utf-8");
8937
9188
  return JSON.parse(raw);
8938
9189
  }
@@ -8988,14 +9239,14 @@ ${transformed}`;
8988
9239
  function planManifestCommands(manifest, targetRepo, previous, writeSet) {
8989
9240
  const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
8990
9241
  return (manifest.commands ?? []).map((cmd) => {
8991
- const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
8992
- if (!existsSync4(srcFile)) {
9242
+ const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
9243
+ if (!existsSync5(srcFile)) {
8993
9244
  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.`);
8994
9245
  }
8995
9246
  const rel = commandStubRel(cmd.name);
8996
9247
  const content = readFileSync6(srcFile, "utf-8");
8997
9248
  const recorded = recordedFiles.get(rel);
8998
- const planned = decideCollisionSafe(join6(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
9249
+ const planned = decideCollisionSafe(join7(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
8999
9250
  return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
9000
9251
  });
9001
9252
  }
@@ -9005,7 +9256,7 @@ function putPlannedFile(targetRepoRoot, planned) {
9005
9256
  if (!isAllowlisted(planned.rel)) {
9006
9257
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${planned.rel}". Only the method surface and scaffolding paths are writable.`);
9007
9258
  }
9008
- const target = join6(targetRepoRoot, planned.rel);
9259
+ const target = join7(targetRepoRoot, planned.rel);
9009
9260
  mkdirSync5(dirname3(target), { recursive: true });
9010
9261
  writeFileSync5(target, planned.text, "utf-8");
9011
9262
  }
@@ -9025,7 +9276,7 @@ function decideCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
9025
9276
  if (!isAllowlisted(rel)) {
9026
9277
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
9027
9278
  }
9028
- const current = existsSync4(targetAbsPath) ? readFileSync6(targetAbsPath) : null;
9279
+ const current = existsSync5(targetAbsPath) ? readFileSync6(targetAbsPath) : null;
9029
9280
  const text = inLineEndingOf(current?.toString("utf-8") ?? null, content);
9030
9281
  if (current === null)
9031
9282
  return { rel, text, outcome: "written", replacedExisting: false };
@@ -9053,7 +9304,7 @@ function decideOwned(targetAbsPath, targetRepoRoot, content) {
9053
9304
  const text = inLineEndingOf(current, content);
9054
9305
  if (current === text)
9055
9306
  return { rel, text, outcome: "skipped", replacedExisting: false };
9056
- return { rel, text, outcome: "written", replacedExisting: existsSync4(targetAbsPath) };
9307
+ return { rel, text, outcome: "written", replacedExisting: existsSync5(targetAbsPath) };
9057
9308
  }
9058
9309
  var GENERATED_HEADER_LINE_INDEX = 1;
9059
9310
  function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
@@ -9061,7 +9312,7 @@ function decideOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedM
9061
9312
  if (!isAllowlisted(rel)) {
9062
9313
  throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
9063
9314
  }
9064
- if (!existsSync4(targetAbsPath))
9315
+ if (!existsSync5(targetAbsPath))
9065
9316
  return { rel, text: content, outcome: "written", replacedExisting: false };
9066
9317
  const current = readFileSync6(targetAbsPath, "utf-8");
9067
9318
  if (current === content)
@@ -9079,7 +9330,7 @@ function recordOwnedGenerated(report, planned) {
9079
9330
  return planned.outcome;
9080
9331
  }
9081
9332
  function recordOwned(report, targetAbsPath, targetRepoRoot, content, rel) {
9082
- const existedBefore = existsSync4(targetAbsPath);
9333
+ const existedBefore = existsSync5(targetAbsPath);
9083
9334
  const outcome = writeOwned(targetAbsPath, targetRepoRoot, content);
9084
9335
  record(report, outcome, rel);
9085
9336
  if (outcome === "written" && existedBefore)
@@ -9101,8 +9352,8 @@ var DENY_PATTERNS = [
9101
9352
  ];
9102
9353
  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.";
9103
9354
  function readExistingSettings(targetRepo) {
9104
- const settingsPath = join6(targetRepo, SETTINGS_REL);
9105
- return existsSync4(settingsPath) ? JSON.parse(readFileSync6(settingsPath, "utf-8")) : void 0;
9355
+ const settingsPath = join7(targetRepo, SETTINGS_REL);
9356
+ return existsSync5(settingsPath) ? JSON.parse(readFileSync6(settingsPath, "utf-8")) : void 0;
9106
9357
  }
9107
9358
  var SettingsParseRefused = class extends Error {
9108
9359
  constructor(message) {
@@ -9111,7 +9362,7 @@ var SettingsParseRefused = class extends Error {
9111
9362
  }
9112
9363
  };
9113
9364
  function settingsParseRefusal(targetRepo, err) {
9114
- const file = join6(targetRepo, SETTINGS_REL);
9365
+ const file = join7(targetRepo, SETTINGS_REL);
9115
9366
  const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
9116
9367
  let where = "";
9117
9368
  if (offset !== void 0) {
@@ -9125,15 +9376,15 @@ var HALFCYCLE_STATUS_LINE = Object.freeze({
9125
9376
  type: "command",
9126
9377
  command: 'bash "$CLAUDE_PROJECT_DIR"/.claude/hooks/guard-runner.sh statusline'
9127
9378
  });
9128
- function statusLineSetElsewhere(targetRepo, home = homedir2(), env = process.env) {
9129
- const places = [join6(targetRepo, ".claude", "settings.local.json"), join6(home, ".claude", "settings.json")];
9379
+ function statusLineSetElsewhere(targetRepo, home = homedir3(), env = process.env) {
9380
+ const places = [join7(targetRepo, ".claude", "settings.local.json"), join7(home, ".claude", "settings.json")];
9130
9381
  const configDir = env["CLAUDE_CONFIG_DIR"];
9131
9382
  if (configDir !== void 0 && configDir !== "") {
9132
- const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join6(home, configDir.slice(2)) : configDir;
9133
- places.push(join6(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
9383
+ const expanded = configDir === "~" ? home : configDir.startsWith("~/") ? join7(home, configDir.slice(2)) : configDir;
9384
+ places.push(join7(isAbsolute2(expanded) ? expanded : resolve(expanded), "settings.json"));
9134
9385
  }
9135
9386
  return places.some((path) => {
9136
- if (!existsSync4(path))
9387
+ if (!existsSync5(path))
9137
9388
  return false;
9138
9389
  let value;
9139
9390
  try {
@@ -9478,7 +9729,8 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
9478
9729
  # <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
9479
9730
  # <words>", "drop <phase>/<part>: <words>", "reject <phase>/<task>: <words>",
9480
9731
  # "autonomy on" or "autonomy off". Also lets the person who is typing let one
9481
- # step Halfcycle paused run once, by typing "allow" as the whole prompt, and change
9732
+ # step Halfcycle paused run once, by typing "allow", or "allow" and the step's
9733
+ # number ("allow 2"), as the whole prompt, and change
9482
9734
  # how a build is paced, by typing "faster", "run 5 agents", "keep 30% free" or
9483
9735
  # "use up to 70%" as the whole prompt, and answer a phase's usage limit by
9484
9736
  # typing "keep going", "stop", "limit it to 1.5 times" or "limit it to 300
@@ -9488,7 +9740,8 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
9488
9740
  # looks only at the start of the prompt's value. Unless the prompt begins, after
9489
9741
  # any whitespace (typed, or escaped in the input), with "approve ", "send back ",
9490
9742
  # "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case,
9491
- # or is "allow", one of the five pacing phrases or one of the usage-limit phrases
9743
+ # or is "allow" (alone, or with spaces and a number of up to four digits), one
9744
+ # of the five pacing phrases or one of the usage-limit phrases
9492
9745
  # and nothing else but whitespace,
9493
9746
  # it exits 0 with no output, and Node never starts. A quote inside any other field arrives
9494
9747
  # escaped, so no other field (the folder the session is in, for one) can read as
@@ -9516,7 +9769,7 @@ IFS= read -r -d '' HC_INPUT || true
9516
9769
  # \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
9517
9770
  HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
9518
9771
  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'
9519
- if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*((approve|send back|accept|question|carry|drop|reject|autonomy|decline) |allow($HC_LEAD)*\\"|(faster|go faster|run [0-9]{1,3} agents?|keep [0-9]{1,3}% free|use up to [0-9]{1,3}%|keep going|stop|stop here|limit it to [0-9]{1,9}(\\.[0-9]{1,2})? (times|million|billion|million tokens|billion tokens))($HC_LEAD)*\\")"; then
9772
+ if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*((approve|send back|accept|question|carry|drop|reject|autonomy|decline) |allow( +[0-9]{1,4})?($HC_LEAD)*\\"|(faster|go faster|run [0-9]{1,3} agents?|keep [0-9]{1,3}% free|use up to [0-9]{1,3}%|keep going|stop|stop here|limit it to [0-9]{1,9}(\\.[0-9]{1,2})? (times|million|billion|million tokens|billion tokens))($HC_LEAD)*\\")"; then
9520
9773
  exit 0
9521
9774
  fi
9522
9775
 
@@ -9609,8 +9862,8 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
9609
9862
  return decided === "removed" ? removePlannedRetiredFile(targetRepo, rel) : decided;
9610
9863
  }
9611
9864
  function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
9612
- const path = join6(targetRepo, rel);
9613
- if (!existsSync4(path))
9865
+ const path = join7(targetRepo, rel);
9866
+ if (!existsSync5(path))
9614
9867
  return "absent";
9615
9868
  let raw;
9616
9869
  try {
@@ -9628,7 +9881,7 @@ function decideRetiredRemoval(targetRepo, rel, headerLineIndex, knownHeaderHashe
9628
9881
  }
9629
9882
  function removePlannedRetiredFile(targetRepo, rel) {
9630
9883
  try {
9631
- rmSync2(join6(targetRepo, rel));
9884
+ rmSync2(join7(targetRepo, rel));
9632
9885
  } catch {
9633
9886
  return "failed";
9634
9887
  }
@@ -10004,9 +10257,9 @@ function gitignoreCovers(content) {
10004
10257
  return REQUIRED_GITIGNORE_ENTRIES.every((entry) => lines.has(entry));
10005
10258
  }
10006
10259
  function planGitignore(targetRepo) {
10007
- const gitignorePath = join6(targetRepo, GITIGNORE_REL);
10260
+ const gitignorePath = join7(targetRepo, GITIGNORE_REL);
10008
10261
  const header = GITIGNORE_HEADER;
10009
- const before = existsSync4(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : null;
10262
+ const before = existsSync5(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : null;
10010
10263
  if (before === null) {
10011
10264
  return { before, after: [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n", outcome: "written" };
10012
10265
  }
@@ -10022,7 +10275,7 @@ function putPlannedGitignore(targetRepo, planned) {
10022
10275
  if (planned.outcome !== "written" || planned.after === null)
10023
10276
  return planned.outcome;
10024
10277
  try {
10025
- writeFileSync5(join6(targetRepo, GITIGNORE_REL), planned.after, "utf-8");
10278
+ writeFileSync5(join7(targetRepo, GITIGNORE_REL), planned.after, "utf-8");
10026
10279
  return "written";
10027
10280
  } catch {
10028
10281
  return "failed";
@@ -10041,12 +10294,12 @@ function lfRules(paths) {
10041
10294
  }
10042
10295
  var REQUIRED_GITATTRIBUTES_RULES = lfRules(Object.keys(OWNED_GENERATED_HEADERS));
10043
10296
  function planGitattributes(targetRepo) {
10044
- const path = join6(targetRepo, GITATTRIBUTES_REL);
10297
+ const path = join7(targetRepo, GITATTRIBUTES_REL);
10045
10298
  const rules = REQUIRED_GITATTRIBUTES_RULES;
10046
10299
  const before = readTextOrNull(path);
10047
10300
  if (lstatOrNull(path)?.isSymbolicLink() === true)
10048
10301
  return { before, after: before, outcome: "failed" };
10049
- if (!existsSync4(path)) {
10302
+ if (!existsSync5(path)) {
10050
10303
  return { before, after: [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", outcome: "written" };
10051
10304
  }
10052
10305
  if (before === null)
@@ -10064,7 +10317,7 @@ function putPlannedGitattributes(targetRepo, planned) {
10064
10317
  if (planned.outcome !== "written" || planned.after === null)
10065
10318
  return planned.outcome;
10066
10319
  try {
10067
- writeAllowlisted(join6(targetRepo, GITATTRIBUTES_REL), targetRepo, planned.after, []);
10320
+ writeAllowlisted(join7(targetRepo, GITATTRIBUTES_REL), targetRepo, planned.after, []);
10068
10321
  return "written";
10069
10322
  } catch {
10070
10323
  return "failed";
@@ -10098,7 +10351,7 @@ function planBundlePin(targetRepoRoot, version, engagementId, engagementType, cr
10098
10351
  ...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
10099
10352
  ...projectPage !== void 0 ? { projectPage } : {}
10100
10353
  };
10101
- const current = readFileOrNull(join6(targetRepoRoot, BUNDLE_PIN_REL));
10354
+ const current = readFileOrNull(join7(targetRepoRoot, BUNDLE_PIN_REL));
10102
10355
  const text = inLineEndingOf(current, JSON.stringify(pin, null, 2) + "\n");
10103
10356
  return {
10104
10357
  rel: BUNDLE_PIN_REL,
@@ -10129,7 +10382,7 @@ function planBundlePinPreview(targetRepoRoot, version, home) {
10129
10382
  return planBundlePin(targetRepoRoot, version, existing.engagementId, existing.engagementType, void 0, void 0);
10130
10383
  }
10131
10384
  function writeBundlePin(targetRepoRoot, planned, writtenPaths) {
10132
- const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
10385
+ const pinPath = join7(targetRepoRoot, BUNDLE_PIN_REL);
10133
10386
  writeAllowlisted(pinPath, targetRepoRoot, planned.text, writtenPaths);
10134
10387
  }
10135
10388
  function updatePinnedMethodVersion(repoRoot, engagementId, version) {
@@ -10141,7 +10394,7 @@ function updatePinnedMethodVersion(repoRoot, engagementId, version) {
10141
10394
  }
10142
10395
  if (pin === null || pin.engagementId !== engagementId)
10143
10396
  return;
10144
- const pinPath = join6(repoRoot, BUNDLE_PIN_REL);
10397
+ const pinPath = join7(repoRoot, BUNDLE_PIN_REL);
10145
10398
  const updated = { ...pin, methodVersion: version };
10146
10399
  const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(updated, null, 2) + "\n");
10147
10400
  try {
@@ -10161,15 +10414,15 @@ function renderCrewRoster() {
10161
10414
  }
10162
10415
  function writeCrewRoster(targetRepoRoot, report) {
10163
10416
  const rendered = renderCrewRoster();
10164
- const crewPath = join6(targetRepoRoot, CREW_ROSTER_REL);
10417
+ const crewPath = join7(targetRepoRoot, CREW_ROSTER_REL);
10165
10418
  return recordOwned(report, crewPath, targetRepoRoot, rendered, CREW_ROSTER_REL);
10166
10419
  }
10167
10420
  function pinCreatedByAccountId(pin) {
10168
10421
  return nonEmpty(pin?.createdByAccountId) ?? nonEmpty(pin?.accountId);
10169
10422
  }
10170
10423
  function readBundlePin(targetRepoRoot) {
10171
- const pinPath = join6(targetRepoRoot, BUNDLE_PIN_REL);
10172
- if (!existsSync4(pinPath))
10424
+ const pinPath = join7(targetRepoRoot, BUNDLE_PIN_REL);
10425
+ if (!existsSync5(pinPath))
10173
10426
  return null;
10174
10427
  return JSON.parse(readFileSync6(pinPath, "utf-8"));
10175
10428
  }
@@ -10194,7 +10447,7 @@ function readPinnedEngagement(targetRepoRoot, home) {
10194
10447
  }
10195
10448
  function readLegacyEnvLocal(targetRepoRoot) {
10196
10449
  try {
10197
- return parseEnvText(readFileSync6(join6(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
10450
+ return parseEnvText(readFileSync6(join7(targetRepoRoot, LEGACY_ENV_LOCAL_REL), "utf-8"));
10198
10451
  } catch {
10199
10452
  return null;
10200
10453
  }
@@ -10289,8 +10542,8 @@ function engagementCredentialValues(credential, engagementId) {
10289
10542
  }
10290
10543
  var LEGACY_ENV_LOCAL_REL = ".env.local";
10291
10544
  function migrateLegacyEnvLocal(targetRepo, stored) {
10292
- const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
10293
- if (!existsSync4(path))
10545
+ const path = join7(targetRepo, LEGACY_ENV_LOCAL_REL);
10546
+ if (!existsSync5(path))
10294
10547
  return { outcome: "absent", unbacked: [], missingForAdoption: [] };
10295
10548
  let raw;
10296
10549
  try {
@@ -10320,7 +10573,7 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
10320
10573
  return { outcome: "stripped", unbacked: [], missingForAdoption: [], keptText: kept.join("\n") };
10321
10574
  }
10322
10575
  function applyLegacyMigration(targetRepo, decided) {
10323
- const path = join6(targetRepo, LEGACY_ENV_LOCAL_REL);
10576
+ const path = join7(targetRepo, LEGACY_ENV_LOCAL_REL);
10324
10577
  try {
10325
10578
  if (decided.outcome === "removed") {
10326
10579
  rmSync2(path);
@@ -10346,23 +10599,23 @@ function planCodexSkills(targetRepo, previous, writeSet) {
10346
10599
  const manifest = readPluginManifest();
10347
10600
  const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
10348
10601
  return (manifest.commands ?? []).map((cmd) => {
10349
- const srcFile = join6(BUNDLE_ROOT, ".claude-plugin", cmd.path);
10350
- if (!existsSync4(srcFile)) {
10602
+ const srcFile = join7(BUNDLE_ROOT, ".claude-plugin", cmd.path);
10603
+ if (!existsSync5(srcFile)) {
10351
10604
  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.`);
10352
10605
  }
10353
10606
  const rel = skillRel(cmd.name);
10354
10607
  const content = codexSkillContent(cmd.name, readFileSync6(srcFile, "utf-8"));
10355
10608
  const recorded = recordedFiles.get(rel);
10356
- const planned = decideCollisionSafe(join6(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
10609
+ const planned = decideCollisionSafe(join7(targetRepo, rel), targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
10357
10610
  return { ...planned, editedCollided: planned.outcome === "collided" && recorded !== void 0 };
10358
10611
  });
10359
10612
  }
10360
10613
  function planVendoredBinary(targetRepo) {
10361
- return decideOwned(join6(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
10614
+ return decideOwned(join7(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs"), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"));
10362
10615
  }
10363
10616
  function planSettings(targetRepo, previous, home, env) {
10364
- const settingsPath = join6(targetRepo, SETTINGS_REL);
10365
- const preexisted = existsSync4(settingsPath);
10617
+ const settingsPath = join7(targetRepo, SETTINGS_REL);
10618
+ const preexisted = existsSync5(settingsPath);
10366
10619
  const generated = JSON.parse(generateSettingsJson());
10367
10620
  let existing;
10368
10621
  try {
@@ -10386,11 +10639,11 @@ function planSettings(targetRepo, previous, home, env) {
10386
10639
  return { file, preexisted, existing, generated, merged, retiredOurs };
10387
10640
  }
10388
10641
  function planHookScripts(targetRepo) {
10389
- return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(join6(targetRepo, rel), targetRepo, content, OWNED_GENERATED_HEADERS[rel]));
10642
+ return hookScriptsThisVersionWrites().map(([rel, content]) => decideOwnedGenerated(join7(targetRepo, rel), targetRepo, content, OWNED_GENERATED_HEADERS[rel]));
10390
10643
  }
10391
10644
  function planMcpRegistration(targetRepo, credential) {
10392
- const mcpPath = join6(targetRepo, MCP_REGISTRATION_REL);
10393
- const existing = existsSync4(mcpPath) ? readFileSync6(mcpPath, "utf-8") : null;
10645
+ const mcpPath = join7(targetRepo, MCP_REGISTRATION_REL);
10646
+ const existing = existsSync5(mcpPath) ? readFileSync6(mcpPath, "utf-8") : null;
10394
10647
  if (credential === void 0)
10395
10648
  return { kind: "none", existing };
10396
10649
  if (credential === "pending")
@@ -10402,11 +10655,11 @@ function planMcpRegistration(targetRepo, credential) {
10402
10655
  function putPlannedMcp(targetRepo, planned) {
10403
10656
  if (planned.kind !== "write" || planned.outcome === "skipped")
10404
10657
  return;
10405
- writeAllowlisted(join6(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
10658
+ writeAllowlisted(join7(targetRepo, MCP_REGISTRATION_REL), targetRepo, planned.text, []);
10406
10659
  }
10407
10660
  function fileInFolderPlace(root, rel) {
10408
10661
  for (let dir = posix2.dirname(rel); dir !== "."; dir = posix2.dirname(dir)) {
10409
- const found = lstatOrNull(join6(root, dir));
10662
+ const found = lstatOrNull(join7(root, dir));
10410
10663
  if (found !== null && !found.isDirectory())
10411
10664
  return true;
10412
10665
  }
@@ -10415,7 +10668,7 @@ function fileInFolderPlace(root, rel) {
10415
10668
  function symlinkOnPath(root, rel) {
10416
10669
  let current = root;
10417
10670
  for (const segment of rel.split("/")) {
10418
- current = join6(current, segment);
10671
+ current = join7(current, segment);
10419
10672
  try {
10420
10673
  if (lstatSync(current).isSymbolicLink())
10421
10674
  return true;
@@ -10436,10 +10689,10 @@ function planCodexHooks(targetRepo) {
10436
10689
  });
10437
10690
  if (symlinkOnPath(targetRepo, CODEX_HOOKS_REL2))
10438
10691
  return leave("link", null);
10439
- const folder = lstatOrNull(join6(targetRepo, posix2.dirname(CODEX_HOOKS_REL2)));
10440
- if (folder !== null && !folder.isDirectory())
10692
+ const folder2 = lstatOrNull(join7(targetRepo, posix2.dirname(CODEX_HOOKS_REL2)));
10693
+ if (folder2 !== null && !folder2.isDirectory())
10441
10694
  return leave("unreadable", null);
10442
- const path = join6(targetRepo, CODEX_HOOKS_REL2);
10695
+ const path = join7(targetRepo, CODEX_HOOKS_REL2);
10443
10696
  let before = null;
10444
10697
  if (lstatOrNull(path) !== null) {
10445
10698
  try {
@@ -10477,13 +10730,13 @@ function planCodexHooks(targetRepo) {
10477
10730
  function putPlannedCodexHooks(targetRepo, planned) {
10478
10731
  if (planned.outcome !== "written" || planned.text === null)
10479
10732
  return;
10480
- writeAllowlisted(join6(targetRepo, CODEX_HOOKS_REL2), targetRepo, planned.text, []);
10733
+ writeAllowlisted(join7(targetRepo, CODEX_HOOKS_REL2), targetRepo, planned.text, []);
10481
10734
  }
10482
10735
  function readCodexConfig(targetRepo) {
10483
- const folder = lstatOrNull(join6(targetRepo, posix2.dirname(CODEX_CONFIG_REL2)));
10484
- if (folder !== null && !folder.isDirectory())
10736
+ const folder2 = lstatOrNull(join7(targetRepo, posix2.dirname(CODEX_CONFIG_REL2)));
10737
+ if (folder2 !== null && !folder2.isDirectory())
10485
10738
  return "unreadable";
10486
- const path = join6(targetRepo, CODEX_CONFIG_REL2);
10739
+ const path = join7(targetRepo, CODEX_CONFIG_REL2);
10487
10740
  const stat = lstatOrNull(path);
10488
10741
  if (stat === null)
10489
10742
  return { text: null };
@@ -10537,10 +10790,10 @@ function planCodexConfig(targetRepo, credential) {
10537
10790
  function putPlannedCodexConfig(targetRepo, planned) {
10538
10791
  if (planned.outcome !== "written" || planned.text === null)
10539
10792
  return;
10540
- writeAllowlisted(join6(targetRepo, CODEX_CONFIG_REL2), targetRepo, planned.text, []);
10793
+ writeAllowlisted(join7(targetRepo, CODEX_CONFIG_REL2), targetRepo, planned.text, []);
10541
10794
  }
10542
10795
  function planCrewRoster(targetRepo) {
10543
- return decideOwned(join6(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
10796
+ return decideOwned(join7(targetRepo, CREW_ROSTER_REL), targetRepo, renderCrewRoster());
10544
10797
  }
10545
10798
  function planCrewProfiles(targetRepo, previous, writeSet) {
10546
10799
  const recordedFiles = new Map((previous?.files ?? []).filter((file) => writeSet.files.has(file.path)).map((file) => [file.path, file]));
@@ -10555,7 +10808,7 @@ function planCrewProfiles(targetRepo, previous, writeSet) {
10555
10808
  });
10556
10809
  }
10557
10810
  function planInstallRecord(targetRepo, text) {
10558
- return decideOwned(join6(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
10811
+ return decideOwned(join7(targetRepo, INSTALL_MANIFEST_REL), targetRepo, text);
10559
10812
  }
10560
10813
  function accountInstall(steps, actual) {
10561
10814
  const { targetRepo, writeSet, pin } = steps;
@@ -10783,7 +11036,7 @@ function mcpHasHalfcycleEntry(text) {
10783
11036
  function writeMissingSelfBuildCrewProfiles(targetRepoRoot, report) {
10784
11037
  for (const { rel, text } of renderCrewProfiles()) {
10785
11038
  const target = crewProfileTarget(targetRepoRoot, rel);
10786
- if (existsSync4(target))
11039
+ if (existsSync5(target))
10787
11040
  continue;
10788
11041
  writeAllowlisted(target, targetRepoRoot, text, report.writtenPaths);
10789
11042
  }
@@ -10791,7 +11044,7 @@ function writeMissingSelfBuildCrewProfiles(targetRepoRoot, report) {
10791
11044
  function installSelfBuild(options) {
10792
11045
  const { targetRepo, engagementId, credential, freshlyMinted, home } = options;
10793
11046
  assertEngagementId(engagementId);
10794
- if (!existsSync4(targetRepo)) {
11047
+ if (!existsSync5(targetRepo)) {
10795
11048
  throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
10796
11049
  }
10797
11050
  const report = {
@@ -10802,7 +11055,7 @@ function installSelfBuild(options) {
10802
11055
  editedCollidedPaths: [],
10803
11056
  replacedPaths: []
10804
11057
  };
10805
- const at = (rel) => join6(targetRepo, rel);
11058
+ const at = (rel) => join7(targetRepo, rel);
10806
11059
  const version = readPluginManifest().version;
10807
11060
  const existingMcp = readFileOrNull(at(MCP_REGISTRATION_REL));
10808
11061
  const mcpText = existingMcp === null || !mcpHasHalfcycleEntry(existingMcp) ? inLineEndingOf(existingMcp, generateMcpRegistration(existingMcp, credential.mcpUrl)) : null;
@@ -10813,17 +11066,17 @@ function installSelfBuild(options) {
10813
11066
  if (mcpText !== null) {
10814
11067
  writeAllowlisted(at(MCP_REGISTRATION_REL), targetRepo, mcpText, report.writtenPaths);
10815
11068
  }
10816
- if (!existsSync4(at(VENDORED_BIN_REL))) {
11069
+ if (!existsSync5(at(VENDORED_BIN_REL))) {
10817
11070
  recordOwned(report, at(VENDORED_BIN_REL), targetRepo, readFileSync6(resolveVendoredBinary(), "utf-8"), VENDORED_BIN_REL);
10818
11071
  }
10819
- if (!existsSync4(at(SELF_BUILD_GITIGNORE_REL))) {
11072
+ if (!existsSync5(at(SELF_BUILD_GITIGNORE_REL))) {
10820
11073
  writeAllowlisted(at(SELF_BUILD_GITIGNORE_REL), targetRepo, SELF_BUILD_GITIGNORE_TEXT, report.writtenPaths);
10821
11074
  }
10822
- if (!existsSync4(at(CREW_ROSTER_REL))) {
11075
+ if (!existsSync5(at(CREW_ROSTER_REL))) {
10823
11076
  writeCrewRoster(targetRepo, report);
10824
11077
  }
10825
11078
  writeMissingSelfBuildCrewProfiles(targetRepo, report);
10826
- if (!existsSync4(at(BUNDLE_PIN_REL))) {
11079
+ if (!existsSync5(at(BUNDLE_PIN_REL))) {
10827
11080
  writeBundlePin(targetRepo, planBundlePin(targetRepo, version, engagementId, "self-build", void 0, credential.methodVersion), report.writtenPaths);
10828
11081
  }
10829
11082
  return { writtenPaths: report.writtenPaths, credentialWritten };
@@ -11159,7 +11412,7 @@ function localDeadline(started) {
11159
11412
  return new Date(started.expiresAt).getTime() + started.pollIntervalMs * DEADLINE_SLACK_INTERVALS;
11160
11413
  }
11161
11414
  async function pollForCredential(serviceUrl, started, deps = {}) {
11162
- const write = deps.write ?? defaultWrite;
11415
+ const write2 = deps.write ?? defaultWrite;
11163
11416
  const sleep = deps.sleep ?? defaultSleep;
11164
11417
  const now = deps.now ?? Date.now;
11165
11418
  const url = tokenUrlFor(serviceUrl);
@@ -11181,7 +11434,7 @@ async function pollForCredential(serviceUrl, started, deps = {}) {
11181
11434
  }
11182
11435
  if (now() - announcedAt >= (deps.stillWaitingMs ?? STILL_WAITING_MS)) {
11183
11436
  announcedAt = now();
11184
- write(stillWaitingLine(announcedAt - startedAt, started));
11437
+ write2(stillWaitingLine(announcedAt - startedAt, started));
11185
11438
  }
11186
11439
  await sleep(interval);
11187
11440
  }
@@ -11240,13 +11493,13 @@ async function pollBesideLoopback(serviceUrl, started, wake, deps) {
11240
11493
  }
11241
11494
  }
11242
11495
  async function signIn(serviceUrl, deps = {}) {
11243
- const write = deps.write ?? defaultWrite;
11496
+ const write2 = deps.write ?? defaultWrite;
11244
11497
  const now = deps.now ?? Date.now;
11245
11498
  const env = deps.env ?? process.env;
11246
11499
  if (isNonInteractive(env)) {
11247
11500
  throw new SignInRefused(SIGN_IN_TIMED_OUT, SIGN_IN_NON_INTERACTIVE_MESSAGE);
11248
11501
  }
11249
- write(`[halfcycle] Halfcycle needs an account before it can ${deps.reason ?? DEFAULT_SIGN_IN_REASON} \u2014 signing you in through your browser.
11502
+ write2(`[halfcycle] Halfcycle needs an account before it can ${deps.reason ?? DEFAULT_SIGN_IN_REASON} \u2014 signing you in through your browser.
11250
11503
  `);
11251
11504
  const bind = await bindLoopback(env, deps.onwardEngagementId);
11252
11505
  let closed = false;
@@ -11262,17 +11515,17 @@ async function signIn(serviceUrl, deps = {}) {
11262
11515
  const url = loopbackVerificationUrl(started.verificationUrl, bind.target);
11263
11516
  const attempt = await openBrowser(url, env);
11264
11517
  if (attempt.opened) {
11265
- write(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. If a browser did not open, go to: ${started.verificationUrl}. This terminal is waiting.
11518
+ write2(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. If a browser did not open, go to: ${started.verificationUrl}. This terminal is waiting.
11266
11519
  `);
11267
11520
  const startedAt = now();
11268
11521
  const redirect = bind.waitForCallback({
11269
11522
  timeoutMs: localDeadline(started) - startedAt,
11270
- onStillWaiting: (elapsed) => write(stillWaitingLine(elapsed, started)),
11523
+ onStillWaiting: (elapsed) => write2(stillWaitingLine(elapsed, started)),
11271
11524
  stillWaitingMs: deps.stillWaitingMs ?? STILL_WAITING_MS
11272
11525
  });
11273
11526
  const approved = await pollBesideLoopback(serviceUrl, started, redirect.then(() => void 0), deps);
11274
11527
  await closeListener();
11275
- write(`[halfcycle] Signed in \u2014 continuing.
11528
+ write2(`[halfcycle] Signed in \u2014 continuing.
11276
11529
  `);
11277
11530
  return {
11278
11531
  accountId: approved.accountId,
@@ -11281,10 +11534,10 @@ async function signIn(serviceUrl, deps = {}) {
11281
11534
  };
11282
11535
  }
11283
11536
  await closeListener();
11284
- write(`[halfcycle] Falling back to the code-based sign-in: ${attempt.reason}.
11537
+ write2(`[halfcycle] Falling back to the code-based sign-in: ${attempt.reason}.
11285
11538
  `);
11286
11539
  } else {
11287
- write(`[halfcycle] Falling back to the code-based sign-in: ${bind.reason}.
11540
+ write2(`[halfcycle] Falling back to the code-based sign-in: ${bind.reason}.
11288
11541
  `);
11289
11542
  }
11290
11543
  return await deviceCodeSignIn(serviceUrl, started, env, deps);
@@ -11293,21 +11546,21 @@ async function signIn(serviceUrl, deps = {}) {
11293
11546
  }
11294
11547
  }
11295
11548
  async function deviceCodeSignIn(serviceUrl, started, env, deps) {
11296
- const write = deps.write ?? defaultWrite;
11297
- write(`[halfcycle] Your sign-in code: ${started.userCode}
11549
+ const write2 = deps.write ?? defaultWrite;
11550
+ write2(`[halfcycle] Your sign-in code: ${started.userCode}
11298
11551
  `);
11299
11552
  const attempt = await openBrowser(started.verificationUrl, env);
11300
11553
  if (attempt.opened) {
11301
- write(`[halfcycle] Opened your browser at ${started.verificationUrl}
11554
+ write2(`[halfcycle] Opened your browser at ${started.verificationUrl}
11302
11555
  `);
11303
11556
  } else {
11304
- write(`[halfcycle] Open this address to sign in: ${started.verificationUrl}
11557
+ write2(`[halfcycle] Open this address to sign in: ${started.verificationUrl}
11305
11558
  `);
11306
11559
  }
11307
- write(`[halfcycle] Check the code on that page matches the one above, then sign in. This terminal is waiting.
11560
+ write2(`[halfcycle] Check the code on that page matches the one above, then sign in. This terminal is waiting.
11308
11561
  `);
11309
11562
  const approved = await pollForCredential(serviceUrl, started, deps);
11310
- write(`[halfcycle] Signed in \u2014 continuing.
11563
+ write2(`[halfcycle] Signed in \u2014 continuing.
11311
11564
  `);
11312
11565
  return {
11313
11566
  accountId: approved.accountId,
@@ -11947,40 +12200,40 @@ function identitySwitchRemedy(home, interactive) {
11947
12200
  return interactive ? `To act as a different account: run this again and ${SWITCH_ACCOUNT_ANSWER}, ${setOrRemove} to be asked for a sign-in next time.` : `To act as a different account: ${setOrRemove} and run this again with a terminal to sign in.`;
11948
12201
  }
11949
12202
  async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
11950
- const write = deps.write ?? ((text) => process.stdout.write(text));
12203
+ const write2 = deps.write ?? ((text) => process.stdout.write(text));
11951
12204
  const colour = deps.colour ?? decideColour({ isTTY: process.stdout.isTTY === true, env: deps.env ?? process.env });
11952
12205
  const identity = await describeAccount(serviceUrl, credential.credential, deps.fetchImpl);
11953
12206
  const plainLabel = accountLabel(identity, credential.accountId, deps.verbose === true);
11954
12207
  const label = plainLabel !== void 0 ? ink("keyBold", plainLabel, colour) : "";
11955
12208
  const acting = actingAs(credential, identity);
11956
12209
  const script = armScript(credential, act, accountLine(identity, label), plainLabel, deps.home);
11957
- write(script.preamble);
12210
+ write2(script.preamble);
11958
12211
  if (!script.asks)
11959
12212
  return acting;
11960
12213
  if (deps.assumeYes === true) {
11961
- write(`[halfcycle] Continuing as that account without asking, because --yes was given.
12214
+ write2(`[halfcycle] Continuing as that account without asking, because --yes was given.
11962
12215
  `);
11963
12216
  return acting;
11964
12217
  }
11965
12218
  const interactive = deps.interactive ?? isInteractiveRun(deps.env);
11966
12219
  if (!interactive) {
11967
12220
  if (script.noTerminalNote !== "")
11968
- write(script.noTerminalNote);
12221
+ write2(script.noTerminalNote);
11969
12222
  return acting;
11970
12223
  }
11971
12224
  const ask = deps.ask ?? askLine;
11972
- write("\n");
12225
+ write2("\n");
11973
12226
  const decision = readDecision(await ask(question(colour)));
11974
12227
  if (decision === "proceed")
11975
12228
  return acting;
11976
12229
  if (decision === "switch-account") {
11977
- write(script.switchOpening);
12230
+ write2(script.switchOpening);
11978
12231
  const replacement = await switchAccount(serviceUrl, deps);
11979
12232
  const replacementIdentity = await describeAccount(serviceUrl, replacement.credential, deps.fetchImpl);
11980
- write(`[halfcycle] Signed in. This install will ${actClause(act)}.
12233
+ write2(`[halfcycle] Signed in. This install will ${actClause(act)}.
11981
12234
  `);
11982
12235
  if (script.afterSwitch !== "")
11983
- write(script.afterSwitch);
12236
+ write2(script.afterSwitch);
11984
12237
  return actingAs(replacement, replacementIdentity);
11985
12238
  }
11986
12239
  throw new SignInRefused(IDENTITY_DECLINED, script.stopRemedy);
@@ -12091,8 +12344,8 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
12091
12344
 
12092
12345
  // dist/pre-create-checks.js
12093
12346
  import { spawnSync } from "node:child_process";
12094
- import { existsSync as existsSync5, readFileSync as readFileSync7, realpathSync } from "node:fs";
12095
- import { delimiter, dirname as dirname4, join as join7 } from "node:path";
12347
+ import { existsSync as existsSync6, readFileSync as readFileSync7, realpathSync } from "node:fs";
12348
+ import { delimiter, dirname as dirname4, join as join8 } from "node:path";
12096
12349
  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.';
12097
12350
  var PreCreateRefused = class extends Error {
12098
12351
  constructor(message) {
@@ -12122,29 +12375,29 @@ var SYSTEM_FOLDERS = [
12122
12375
  "C:\\Program Files",
12123
12376
  "C:\\Program Files (x86)"
12124
12377
  ];
12125
- function refuseSystemFolder(targetRepo, platform5 = process.platform) {
12126
- const key = (folder) => {
12127
- const real = realOrLiteral(folder);
12128
- return platform5 === "win32" ? real.toLowerCase() : real;
12378
+ function refuseSystemFolder(targetRepo, platform6 = process.platform) {
12379
+ const key = (folder2) => {
12380
+ const real = realOrLiteral(folder2);
12381
+ return platform6 === "win32" ? real.toLowerCase() : real;
12129
12382
  };
12130
12383
  const target = key(targetRepo);
12131
- if (SYSTEM_FOLDERS.some((folder) => key(folder) === target)) {
12384
+ if (SYSTEM_FOLDERS.some((folder2) => key(folder2) === target)) {
12132
12385
  throw new PreCreateRefused(`${targetRepo} is a system folder \u2014 run "npx halfcycle" inside a project folder.`);
12133
12386
  }
12134
12387
  }
12135
12388
  function refuseInsideProject(targetRepo) {
12136
- let folder = realOrLiteral(targetRepo);
12137
- for (let parent = dirname4(folder); parent !== folder; folder = parent, parent = dirname4(parent)) {
12138
- if (existsSync5(join7(parent, BUNDLE_PIN_REL))) {
12389
+ let folder2 = realOrLiteral(targetRepo);
12390
+ for (let parent = dirname4(folder2); parent !== folder2; folder2 = parent, parent = dirname4(parent)) {
12391
+ if (existsSync6(join8(parent, BUNDLE_PIN_REL))) {
12139
12392
  throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in your coding agent instead, or run "npx halfcycle" in a folder outside it.`);
12140
12393
  }
12141
12394
  }
12142
12395
  }
12143
- function realOrLiteral(folder) {
12396
+ function realOrLiteral(folder2) {
12144
12397
  try {
12145
- return realpathSync(folder);
12398
+ return realpathSync(folder2);
12146
12399
  } catch {
12147
- return folder;
12400
+ return folder2;
12148
12401
  }
12149
12402
  }
12150
12403
  var GIT_INSTALL_ARGS = ["install", "--id", "Git.Git", "-e", "--source", "winget"];
@@ -12159,13 +12412,13 @@ async function offerGitForWindows() {
12159
12412
  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.`);
12160
12413
  }
12161
12414
  function gitBashPath(env) {
12162
- const onPath = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "" && existsSync5(join7(dir, "git.exe"))).map((dir) => join7(dir, "..", "bin", "bash.exe"));
12415
+ const onPath = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "" && existsSync6(join8(dir, "git.exe"))).map((dir) => join8(dir, "..", "bin", "bash.exe"));
12163
12416
  const candidates = [
12164
12417
  env["CLAUDE_CODE_GIT_BASH_PATH"],
12165
12418
  ...onPath,
12166
- env["ProgramFiles"] === void 0 ? void 0 : join7(env["ProgramFiles"], "Git", "bin", "bash.exe")
12419
+ env["ProgramFiles"] === void 0 ? void 0 : join8(env["ProgramFiles"], "Git", "bin", "bash.exe")
12167
12420
  ];
12168
- return candidates.find((file) => file !== void 0 && file !== "" && existsSync5(file));
12421
+ return candidates.find((file) => file !== void 0 && file !== "" && existsSync6(file));
12169
12422
  }
12170
12423
  function hasGitBash(env) {
12171
12424
  return gitBashPath(env) !== void 0;
@@ -12174,7 +12427,7 @@ function runWingetInstall() {
12174
12427
  return spawnSync("winget", [...GIT_INSTALL_ARGS], { stdio: "inherit" }).status === 0;
12175
12428
  }
12176
12429
  function unreadableProjectRefusal(targetRepo) {
12177
- 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.`;
12430
+ return `${join8(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.`;
12178
12431
  }
12179
12432
  function cannotWriteRefusal(targetRepo) {
12180
12433
  return `cannot write to ${targetRepo} \u2014 run "npx halfcycle" in a folder you can write to.`;
@@ -12194,7 +12447,7 @@ function claimFolderId(targetRepo) {
12194
12447
  function readFolderId(targetRepo) {
12195
12448
  let folderId;
12196
12449
  try {
12197
- const text = readFileSync7(join7(targetRepo, PROJECT_IDENTITY_REL), "utf-8");
12450
+ const text = readFileSync7(join8(targetRepo, PROJECT_IDENTITY_REL), "utf-8");
12198
12451
  folderId = JSON.parse(text)?.projectId;
12199
12452
  } catch {
12200
12453
  return void 0;
@@ -12212,7 +12465,7 @@ function checkSettingsParse(targetRepo) {
12212
12465
 
12213
12466
  // dist/folder-refusals.js
12214
12467
  import { realpathSync as realpathSync2 } from "node:fs";
12215
- import { homedir as homedir3 } from "node:os";
12468
+ import { homedir as homedir4 } from "node:os";
12216
12469
  function folderMissingRefusal(targetRepo) {
12217
12470
  return `halfcycle: no such directory: ${targetRepo}`;
12218
12471
  }
@@ -12232,7 +12485,7 @@ function homeFolderRefusal(targetRepo) {
12232
12485
  function isHomeFolder(dir) {
12233
12486
  try {
12234
12487
  const target = realpathSync2(dir);
12235
- const home = realpathSync2(homedir3());
12488
+ const home = realpathSync2(homedir4());
12236
12489
  return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
12237
12490
  } catch {
12238
12491
  return false;
@@ -12240,16 +12493,16 @@ function isHomeFolder(dir) {
12240
12493
  }
12241
12494
 
12242
12495
  // dist/preview.js
12243
- import { accessSync, constants, existsSync as existsSync6, readFileSync as readFileSync9, statSync } from "node:fs";
12244
- import { join as join9 } from "node:path";
12496
+ import { accessSync, constants, existsSync as existsSync7, readFileSync as readFileSync9, statSync } from "node:fs";
12497
+ import { join as join10 } from "node:path";
12245
12498
 
12246
12499
  // dist/halfcycle-monorepo.js
12247
12500
  import { readFileSync as readFileSync8 } from "node:fs";
12248
- import { join as join8 } from "node:path";
12501
+ import { join as join9 } from "node:path";
12249
12502
  var MONOREPO_PACKAGE_NAME = "halfcycle-monorepo";
12250
12503
  function isHalfcycleMonorepo(dir) {
12251
12504
  try {
12252
- const pkg = JSON.parse(readFileSync8(join8(dir, "package.json"), "utf-8"));
12505
+ const pkg = JSON.parse(readFileSync8(join9(dir, "package.json"), "utf-8"));
12253
12506
  return pkg.name === MONOREPO_PACKAGE_NAME;
12254
12507
  } catch {
12255
12508
  return false;
@@ -12291,8 +12544,8 @@ function runPreview(targetRepo, options) {
12291
12544
  };
12292
12545
  }
12293
12546
  function claudeMdLine(targetRepo) {
12294
- const path = join9(targetRepo, CLAUDE_MD_REL);
12295
- if (!existsSync6(path))
12547
+ const path = join10(targetRepo, CLAUDE_MD_REL);
12548
+ if (!existsSync7(path))
12296
12549
  return void 0;
12297
12550
  let text;
12298
12551
  try {
@@ -12303,7 +12556,7 @@ function claudeMdLine(targetRepo) {
12303
12556
  return text.includes(AGENTS_IMPORT_LINE) ? WORDS.claudeMdAlreadyPoints : WORDS.claudeMdWillAdd;
12304
12557
  }
12305
12558
  function previewRefusal(targetRepo, options) {
12306
- if (!existsSync6(targetRepo))
12559
+ if (!existsSync7(targetRepo))
12307
12560
  return folderMissingRefusal(targetRepo);
12308
12561
  if (!statSync(targetRepo).isDirectory())
12309
12562
  return folderIsFileRefusal(targetRepo);
@@ -12324,7 +12577,7 @@ function previewRefusal(targetRepo, options) {
12324
12577
  } catch {
12325
12578
  return cannotWriteRefusal(targetRepo);
12326
12579
  }
12327
- if (existsSync6(join9(targetRepo, PROJECT_IDENTITY_REL)) && readFolderId(targetRepo) === void 0) {
12580
+ if (existsSync7(join10(targetRepo, PROJECT_IDENTITY_REL)) && readFolderId(targetRepo) === void 0) {
12328
12581
  return unreadableProjectRefusal(targetRepo);
12329
12582
  }
12330
12583
  try {
@@ -12382,9 +12635,9 @@ function renderPreview(plan, missingGitForWindows, claudeMdWords) {
12382
12635
 
12383
12636
  // dist/doctor.js
12384
12637
  import { spawnSync as spawnSync2 } from "node:child_process";
12385
- import { existsSync as existsSync7, readFileSync as readFileSync10, realpathSync as realpathSync3 } from "node:fs";
12386
- import { homedir as homedir4 } from "node:os";
12387
- import { join as join10 } from "node:path";
12638
+ import { existsSync as existsSync8, readFileSync as readFileSync10, realpathSync as realpathSync3 } from "node:fs";
12639
+ import { homedir as homedir5 } from "node:os";
12640
+ import { join as join11 } from "node:path";
12388
12641
  var GUARD_RUNNER_REL2 = ".claude/hooks/guard-runner.sh";
12389
12642
  var CONNECTION_VERB = "check-connection";
12390
12643
  var WRAPPER_TIMEOUT_MS = 3e4;
@@ -12406,16 +12659,17 @@ function runDoctor(root, options) {
12406
12659
  const version = offline ? notChecked("Up to date", "Connected") : upToDate ? { ok: true, text: `Up to date: yes (${current}).` } : { ok: false, text: versionNoLine(installed, current) };
12407
12660
  const lines = [setUp.line, connection.signedIn, connection.connected, checksRunning, version];
12408
12661
  const failing = lines.filter((line) => !line.ok && !line.cascading).length;
12409
- const closing = failing === 0 ? "Everything is working." : `${failing} of these ${failing === 1 ? "needs" : "need"} fixing; each line above says what to do.`;
12410
- const codex = codexLine(root, options);
12411
- const printed = codex === void 0 ? lines : [...lines.slice(0, 4), codex, ...lines.slice(4)];
12662
+ const report = running.state === "answered" ? running.report : "none";
12663
+ const closing = failing === 0 ? closingWithNothingFailing(report) : `${failing} of these ${failing === 1 ? "needs" : "need"} fixing; each line above says what to do.`;
12664
+ const extras = [codexLine(root, options), reportsLine(report)].filter((line) => line !== void 0);
12665
+ const printed = [...lines.slice(0, 4), ...extras, ...lines.slice(4)];
12412
12666
  return {
12413
12667
  text: [...printed.map((line) => line.text), closing].join("\n"),
12414
12668
  exitCode: failing === 0 ? 0 : 1
12415
12669
  };
12416
12670
  }
12417
12671
  function setUpLine(root, upToDate) {
12418
- if (!existsSync7(join10(root, BUNDLE_PIN_REL))) {
12672
+ if (!existsSync8(join11(root, BUNDLE_PIN_REL))) {
12419
12673
  return {
12420
12674
  state: "no",
12421
12675
  hooksMissing: false,
@@ -12429,9 +12683,9 @@ function setUpLine(root, upToDate) {
12429
12683
  const missing = [];
12430
12684
  if (!pinNamesProject(root))
12431
12685
  missing.push("Halfcycle's project record");
12432
- if (!existsSync7(join10(root, INSTALL_MANIFEST_REL)))
12686
+ if (!existsSync8(join11(root, INSTALL_MANIFEST_REL)))
12433
12687
  missing.push("the install record");
12434
- if (!existsSync7(join10(root, VENDORED_BIN_REL)))
12688
+ if (!existsSync8(join11(root, VENDORED_BIN_REL)))
12435
12689
  missing.push("Halfcycle's check program");
12436
12690
  if (!mcpNamesServer(root))
12437
12691
  missing.push("Halfcycle's server entry");
@@ -12457,25 +12711,25 @@ function pinNamesProject(root) {
12457
12711
  }
12458
12712
  }
12459
12713
  function mcpNamesServer(root) {
12460
- const parsed = readJson(join10(root, MCP_REGISTRATION_REL));
12461
- if (!isObject(parsed) || !isObject(parsed["mcpServers"]))
12714
+ const parsed = readJson(join11(root, MCP_REGISTRATION_REL));
12715
+ if (!isObject2(parsed) || !isObject2(parsed["mcpServers"]))
12462
12716
  return false;
12463
12717
  const entry = parsed["mcpServers"][MCP_SERVER_KEY];
12464
- return isObject(entry) && typeof entry["url"] === "string";
12718
+ return isObject2(entry) && typeof entry["url"] === "string";
12465
12719
  }
12466
12720
  function settingsRegisterEveryCommand(root) {
12467
- const parsed = readJson(join10(root, SETTINGS_REL));
12468
- if (!isObject(parsed) || !isObject(parsed["hooks"]))
12721
+ const parsed = readJson(join11(root, SETTINGS_REL));
12722
+ if (!isObject2(parsed) || !isObject2(parsed["hooks"]))
12469
12723
  return false;
12470
12724
  const registered = /* @__PURE__ */ new Set();
12471
12725
  for (const groups of Object.values(parsed["hooks"])) {
12472
12726
  if (!Array.isArray(groups))
12473
12727
  continue;
12474
12728
  for (const group of groups) {
12475
- if (!isObject(group) || !Array.isArray(group["hooks"]))
12729
+ if (!isObject2(group) || !Array.isArray(group["hooks"]))
12476
12730
  continue;
12477
12731
  for (const hook of group["hooks"]) {
12478
- if (isObject(hook) && typeof hook["command"] === "string")
12732
+ if (isObject2(hook) && typeof hook["command"] === "string")
12479
12733
  registered.add(hook["command"]);
12480
12734
  }
12481
12735
  }
@@ -12495,8 +12749,8 @@ function startWrapper(root, options) {
12495
12749
  }
12496
12750
  shell = gitBash;
12497
12751
  }
12498
- const wrapper = join10(root, GUARD_RUNNER_REL2);
12499
- if (!existsSync7(wrapper))
12752
+ const wrapper = join11(root, GUARD_RUNNER_REL2);
12753
+ if (!existsSync8(wrapper))
12500
12754
  return failed(repairSentence());
12501
12755
  const run = spawnSync2(shell, [wrapper, CONNECTION_VERB], {
12502
12756
  cwd: root,
@@ -12523,8 +12777,8 @@ function startWrapper(root, options) {
12523
12777
  }
12524
12778
  function settingsDisablingHooks(root) {
12525
12779
  for (const rel of [SETTINGS_REL, LOCAL_SETTINGS_REL]) {
12526
- const parsed = readJson(join10(root, rel));
12527
- if (isObject(parsed) && parsed["disableAllHooks"] === true)
12780
+ const parsed = readJson(join11(root, rel));
12781
+ if (isObject2(parsed) && parsed["disableAllHooks"] === true)
12528
12782
  return rel;
12529
12783
  }
12530
12784
  return void 0;
@@ -12539,14 +12793,16 @@ function parseAnswer(stdout) {
12539
12793
  } catch {
12540
12794
  return void 0;
12541
12795
  }
12542
- if (!isObject(parsed) || typeof parsed["kind"] !== "string" || !KNOWN_KINDS.includes(parsed["kind"]))
12796
+ if (!isObject2(parsed) || typeof parsed["kind"] !== "string" || !KNOWN_KINDS.includes(parsed["kind"]))
12543
12797
  return void 0;
12544
12798
  const sentence = parsed["sentence"];
12545
12799
  if (sentence !== null && typeof sentence !== "string")
12546
12800
  return void 0;
12547
12801
  if (parsed["kind"] !== "not-registered" && typeof sentence !== "string")
12548
12802
  return void 0;
12549
- return { kind: parsed["kind"], sentence };
12803
+ const said = parsed["report"];
12804
+ const report = parsed["kind"] === "connected" && (said === "no-report-yet" || said === "gone-quiet") ? said : "none";
12805
+ return { kind: parsed["kind"], sentence, report };
12550
12806
  }
12551
12807
  function checksRunningLine(running, hooksMissing) {
12552
12808
  if (running.state === "not-run")
@@ -12587,6 +12843,17 @@ function connectionLines(running) {
12587
12843
  connected: { ok: false, text: `Connected: no. ${sentence ?? ""}`.trimEnd() }
12588
12844
  };
12589
12845
  }
12846
+ function reportsLine(report) {
12847
+ if (report === "no-report-yet") {
12848
+ return { ok: true, text: `Reports: ${CHECKS_NO_REPORT_YET_STATE}. ${CHECKS_NO_REPORT_YET_WHEN} ${CHECKS_IF_STILL_UNREPORTED}` };
12849
+ }
12850
+ return report === "gone-quiet" ? { ok: true, text: `Reports: ${CHECKS_GONE_QUIET}` } : void 0;
12851
+ }
12852
+ function closingWithNothingFailing(report) {
12853
+ if (report === "no-report-yet")
12854
+ return `${CHECKS_NO_REPORT_YET_STATE}.`;
12855
+ return report === "gone-quiet" ? CHECKS_GONE_QUIET : "Everything is working.";
12856
+ }
12590
12857
  function installedVersion(root) {
12591
12858
  try {
12592
12859
  return checkDrift(root).installed;
@@ -12610,8 +12877,8 @@ var CODEX_EVENT_SNAKE = {
12610
12877
  SessionEnd: "session_end"
12611
12878
  };
12612
12879
  function codexLine(root, options) {
12613
- const hooksPath = join10(root, CODEX_HOOKS_REL2);
12614
- if (!existsSync7(hooksPath))
12880
+ const hooksPath = join11(root, CODEX_HOOKS_REL2);
12881
+ if (!existsSync8(hooksPath))
12615
12882
  return void 0;
12616
12883
  const positions = codexEntryPositions(hooksPath);
12617
12884
  if (positions === void 0) {
@@ -12624,7 +12891,7 @@ function codexLine(root, options) {
12624
12891
  }
12625
12892
  function codexEntryPositions(hooksPath) {
12626
12893
  const parsed = readJson(hooksPath);
12627
- if (!isObject(parsed) || !isObject(parsed["hooks"]))
12894
+ if (!isObject2(parsed) || !isObject2(parsed["hooks"]))
12628
12895
  return void 0;
12629
12896
  const positions = [];
12630
12897
  for (const entry of AGENT_PROFILES.codex.wiring) {
@@ -12634,11 +12901,11 @@ function codexEntryPositions(hooksPath) {
12634
12901
  let found;
12635
12902
  for (let g = 0; g < groups.length && found === void 0; g += 1) {
12636
12903
  const group = groups[g];
12637
- if (!isObject(group) || !Array.isArray(group["hooks"]))
12904
+ if (!isObject2(group) || !Array.isArray(group["hooks"]))
12638
12905
  continue;
12639
12906
  for (let h = 0; h < group["hooks"].length; h += 1) {
12640
12907
  const hook = group["hooks"][h];
12641
- if (isObject(hook) && hook["command"] === entry.command) {
12908
+ if (isObject2(hook) && hook["command"] === entry.command) {
12642
12909
  found = { group: g, hook: h };
12643
12910
  break;
12644
12911
  }
@@ -12652,7 +12919,7 @@ function codexEntryPositions(hooksPath) {
12652
12919
  }
12653
12920
  function codexApprovalStatus(root, positions, options) {
12654
12921
  const codexHome = options.env["CODEX_HOME"];
12655
- const configPath = join10(codexHome !== void 0 && codexHome !== "" ? codexHome : join10(homedir4(), ".codex"), "config.toml");
12922
+ const configPath = join11(codexHome !== void 0 && codexHome !== "" ? codexHome : join11(homedir5(), ".codex"), "config.toml");
12656
12923
  let text;
12657
12924
  try {
12658
12925
  text = readFileSync10(configPath, "utf-8");
@@ -12661,7 +12928,7 @@ function codexApprovalStatus(root, positions, options) {
12661
12928
  }
12662
12929
  let hooksPath;
12663
12930
  try {
12664
- hooksPath = join10(realpathSync3(root), CODEX_HOOKS_REL2);
12931
+ hooksPath = join11(realpathSync3(root), CODEX_HOOKS_REL2);
12665
12932
  } catch {
12666
12933
  return "not-approved";
12667
12934
  }
@@ -12691,7 +12958,7 @@ function naturalList(items) {
12691
12958
  return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
12692
12959
  }
12693
12960
  function readJson(path) {
12694
- if (!existsSync7(path))
12961
+ if (!existsSync8(path))
12695
12962
  return void 0;
12696
12963
  try {
12697
12964
  return JSON.parse(readFileSync10(path, "utf-8"));
@@ -12699,7 +12966,7 @@ function readJson(path) {
12699
12966
  return void 0;
12700
12967
  }
12701
12968
  }
12702
- function isObject(value) {
12969
+ function isObject2(value) {
12703
12970
  return typeof value === "object" && value !== null && !Array.isArray(value);
12704
12971
  }
12705
12972
 
@@ -12757,13 +13024,13 @@ async function probeMcpOrigin(mcpUrl, token) {
12757
13024
 
12758
13025
  // dist/claude-version-probe.js
12759
13026
  import { execFileSync as execFileSync3 } from "node:child_process";
12760
- import { existsSync as existsSync8, realpathSync as realpathSync4 } from "node:fs";
12761
- import { delimiter as delimiter2, join as join11 } from "node:path";
13027
+ import { existsSync as existsSync9, realpathSync as realpathSync4 } from "node:fs";
13028
+ import { delimiter as delimiter2, join as join12 } from "node:path";
12762
13029
  function execVersionCommand(command, timeoutMs, deps) {
12763
- const platform5 = deps.platform ?? process.platform;
13030
+ const platform6 = deps.platform ?? process.platform;
12764
13031
  const exec = deps.exec ?? execFileSync3;
12765
13032
  try {
12766
- return platform5 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", `"${command} --version"`], {
13033
+ return platform6 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", `"${command} --version"`], {
12767
13034
  encoding: "utf-8",
12768
13035
  timeout: timeoutMs,
12769
13036
  stdio: ["ignore", "pipe", "ignore"],
@@ -12800,8 +13067,8 @@ var INSTALL_METHOD_RULES = [
12800
13067
  ];
12801
13068
  function probeClaudeCodeInstallMethod(deps = {}) {
12802
13069
  const env = deps.env ?? process.env;
12803
- const platform5 = deps.platform ?? process.platform;
12804
- const exists = deps.exists ?? existsSync8;
13070
+ const platform6 = deps.platform ?? process.platform;
13071
+ const exists = deps.exists ?? existsSync9;
12805
13072
  const realpath = deps.realpath ?? ((file) => {
12806
13073
  try {
12807
13074
  return realpathSync4(file);
@@ -12809,11 +13076,11 @@ function probeClaudeCodeInstallMethod(deps = {}) {
12809
13076
  return file;
12810
13077
  }
12811
13078
  });
12812
- const names = platform5 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
13079
+ const names = platform6 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
12813
13080
  const dirs = (env["PATH"] ?? "").split(delimiter2).filter((dir) => dir !== "");
12814
13081
  for (const dir of dirs) {
12815
13082
  for (const name of names) {
12816
- const file = join11(dir, name);
13083
+ const file = join12(dir, name);
12817
13084
  if (!exists(file))
12818
13085
  continue;
12819
13086
  const real = realpath(file);
@@ -12848,8 +13115,8 @@ function codexInstallLine(codex) {
12848
13115
  }
12849
13116
 
12850
13117
  // dist/build-record/write.js
12851
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "node:fs";
12852
- import { basename as basename2, join as join12, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
13118
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "node:fs";
13119
+ import { basename as basename2, join as join13, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
12853
13120
 
12854
13121
  // dist/build-record/render-v2.js
12855
13122
  var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
@@ -13498,7 +13765,7 @@ function replaceOne(io, tmp, finalPath, text) {
13498
13765
  }
13499
13766
 
13500
13767
  // dist/build-record/write.js
13501
- var BUILD_RECORD_DIR = join12("docs", "build-records");
13768
+ var BUILD_RECORD_DIR = join13("docs", "build-records");
13502
13769
  function assertInsideOutDir(outDir, candidate, identity) {
13503
13770
  const rel = relative2(outDir, resolve3(candidate));
13504
13771
  const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute3(rel) && rel === basename2(rel);
@@ -13536,8 +13803,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
13536
13803
  }
13537
13804
  const outDir = resolve3(repoRoot, BUILD_RECORD_DIR);
13538
13805
  const segment = renderPhaseSegment(phase);
13539
- const jsonPath = join12(outDir, `${segment}.json`);
13540
- const mdPath = join12(outDir, `${segment}.md`);
13806
+ const jsonPath = join13(outDir, `${segment}.json`);
13807
+ const mdPath = join13(outDir, `${segment}.md`);
13541
13808
  const jsonTmp = `${jsonPath}.tmp`;
13542
13809
  const mdTmp = `${mdPath}.tmp`;
13543
13810
  for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
@@ -13551,7 +13818,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
13551
13818
  }
13552
13819
  const existingJson = readIfPresent(jsonPath);
13553
13820
  if (existingJson === null) {
13554
- return { kind: "in-the-way", path: existsSync9(jsonPath) ? jsonPath : mdPath };
13821
+ return { kind: "in-the-way", path: existsSync10(jsonPath) ? jsonPath : mdPath };
13555
13822
  }
13556
13823
  if (existingJson === jsonText) {
13557
13824
  replaceOne(io, mdTmp, mdPath, mdText);
@@ -13579,20 +13846,20 @@ import { relative as relative5 } from "node:path";
13579
13846
 
13580
13847
  // dist/open-phase.js
13581
13848
  import { readFileSync as readFileSync17 } from "node:fs";
13582
- import { join as join20 } from "node:path";
13849
+ import { join as join21 } from "node:path";
13583
13850
 
13584
13851
  // dist/close-phase.js
13585
13852
  import { execFile, execFileSync as execFileSync5 } from "node:child_process";
13586
13853
  import { readFileSync as readFileSync16 } from "node:fs";
13587
- import { join as join19 } from "node:path";
13854
+ import { join as join20 } from "node:path";
13588
13855
 
13589
13856
  // dist/plan.js
13590
- import { existsSync as existsSync11, readFileSync as readFileSync15 } from "node:fs";
13591
- import { join as join18 } from "node:path";
13857
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "node:fs";
13858
+ import { join as join19 } from "node:path";
13592
13859
 
13593
13860
  // dist/draw/write.js
13594
13861
  import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync12, readSync, realpathSync as realpathSync5, rmSync as rmSync5 } from "node:fs";
13595
- import { dirname as dirname5, isAbsolute as isAbsolute4, join as join13, relative as relative3, resolve as resolve4, sep } from "node:path";
13862
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join14, relative as relative3, resolve as resolve4, sep } from "node:path";
13596
13863
  var DOCS_DIR = "docs";
13597
13864
  var DIAGRAMS_DIR = "diagrams";
13598
13865
  var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
@@ -13648,9 +13915,9 @@ function embedImageLink(base, doc) {
13648
13915
  }
13649
13916
  function diagramPaths(repoRoot, base) {
13650
13917
  const root = resolve4(repoRoot);
13651
- const dir = join13(root, DOCS_DIR, DIAGRAMS_DIR);
13918
+ const dir = join14(root, DOCS_DIR, DIAGRAMS_DIR);
13652
13919
  const names = fileNames(base);
13653
- const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join13(dir, names[which])]));
13920
+ const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join14(dir, names[which])]));
13654
13921
  return { repoRoot: root, dir, file };
13655
13922
  }
13656
13923
  function diagramRel(base, which) {
@@ -13923,10 +14190,10 @@ function isMissingRoute(method, status, body) {
13923
14190
  }
13924
14191
 
13925
14192
  // dist/change.js
13926
- import { join as join16 } from "node:path";
14193
+ import { join as join17 } from "node:path";
13927
14194
 
13928
14195
  // dist/cli-contract.js
13929
- var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "check-criteria", "check-report", "ready", "uninstall", "change", "pace", "route", "prove", "preview", "doctor", "restore"];
14196
+ var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "check-criteria", "check-report", "ready", "uninstall", "change", "pace", "route", "prove", "preview", "doctor", "restore", "request-shapes"];
13930
14197
  var CONTRACTS = [
13931
14198
  { verb: "install", required: [], conditional: [] },
13932
14199
  { verb: "check-drift", required: [], conditional: [] },
@@ -13945,6 +14212,17 @@ var CONTRACTS = [
13945
14212
  { verb: "preview", required: [], conditional: [] },
13946
14213
  { verb: "doctor", required: [], conditional: [] },
13947
14214
  { verb: "restore", required: [], conditional: [] },
14215
+ {
14216
+ verb: "request-shapes",
14217
+ required: [],
14218
+ conditional: [],
14219
+ forms: [
14220
+ { form: "on", required: [], conditional: [] },
14221
+ { form: "off", required: [], conditional: [] },
14222
+ { form: "show", required: [], conditional: [] },
14223
+ { form: "clear", required: [], conditional: [] }
14224
+ ]
14225
+ },
13948
14226
  {
13949
14227
  verb: "check-report",
13950
14228
  required: [
@@ -14136,11 +14414,11 @@ function placeholderRefusal(verb, tokens) {
14136
14414
  // dist/system-report.js
14137
14415
  import { execFileSync as execFileSync4 } from "node:child_process";
14138
14416
  import { readFileSync as readFileSync13 } from "node:fs";
14139
- import { join as join14 } from "node:path";
14417
+ import { join as join15 } from "node:path";
14140
14418
  function readDescriptionFile(repoRoot) {
14141
14419
  let text;
14142
14420
  try {
14143
- text = readFileSync13(join14(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
14421
+ text = readFileSync13(join15(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
14144
14422
  } catch (err) {
14145
14423
  const code = err.code;
14146
14424
  return code === "ENOENT" || code === "ENOTDIR" ? { kind: "missing" } : { kind: "unreadable" };
@@ -14364,8 +14642,8 @@ async function runSystem(repoRoot, deps) {
14364
14642
  }
14365
14643
 
14366
14644
  // dist/build-record/write-change.js
14367
- import { existsSync as existsSync10, mkdirSync as mkdirSync8 } from "node:fs";
14368
- import { basename as basename3, isAbsolute as isAbsolute5, join as join15, relative as relative4, resolve as resolve5 } from "node:path";
14645
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
14646
+ import { basename as basename3, isAbsolute as isAbsolute5, join as join16, relative as relative4, resolve as resolve5 } from "node:path";
14369
14647
 
14370
14648
  // dist/build-record/render-change.js
14371
14649
  function counted2(n, one, many) {
@@ -14510,7 +14788,7 @@ function renderChangeRecordMarkdown(record2) {
14510
14788
  }
14511
14789
 
14512
14790
  // dist/build-record/write-change.js
14513
- var CHANGE_RECORD_DIR = join15("docs", "build-records", "changes");
14791
+ var CHANGE_RECORD_DIR = join16("docs", "build-records", "changes");
14514
14792
  var InvalidChangeIdError = class extends Error {
14515
14793
  id;
14516
14794
  constructor(id) {
@@ -14530,8 +14808,8 @@ function writeChangeRecordPair(repoRoot, sortingId, rawRecord, record2, io = NOD
14530
14808
  if (!checked.success)
14531
14809
  throw new InvalidChangeIdError(sortingId);
14532
14810
  const outDir = resolve5(repoRoot, CHANGE_RECORD_DIR);
14533
- const jsonPath = join15(outDir, `${checked.data}.json`);
14534
- const mdPath = join15(outDir, `${checked.data}.md`);
14811
+ const jsonPath = join16(outDir, `${checked.data}.json`);
14812
+ const mdPath = join16(outDir, `${checked.data}.md`);
14535
14813
  const mdTmp = `${mdPath}.tmp`;
14536
14814
  for (const candidate of [jsonPath, mdPath, mdTmp]) {
14537
14815
  assertInsideChangeDir(outDir, candidate, sortingId);
@@ -14544,7 +14822,7 @@ function writeChangeRecordPair(repoRoot, sortingId, rawRecord, record2, io = NOD
14544
14822
  }
14545
14823
  const existingJson = readIfPresent(jsonPath);
14546
14824
  if (existingJson === null) {
14547
- return { kind: "in-the-way", path: existsSync10(jsonPath) ? jsonPath : mdPath };
14825
+ return { kind: "in-the-way", path: existsSync11(jsonPath) ? jsonPath : mdPath };
14548
14826
  }
14549
14827
  if (existingJson !== jsonText)
14550
14828
  return { kind: "in-the-way", path: jsonPath };
@@ -14807,7 +15085,7 @@ function writePairLines(deps, repoRoot, sortingId, found) {
14807
15085
  };
14808
15086
  }
14809
15087
  return {
14810
- stdout: `[halfcycle] Its record is in ${join16(CHANGE_RECORD_DIR, `${sortingId}.md`)}.
15088
+ stdout: `[halfcycle] Its record is in ${join17(CHANGE_RECORD_DIR, `${sortingId}.md`)}.
14811
15089
  `,
14812
15090
  stderr: "",
14813
15091
  ok: true
@@ -15048,12 +15326,12 @@ function runChange(repoRoot, args2, deps) {
15048
15326
  // dist/shown-record.js
15049
15327
  import { chmodSync as chmodSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
15050
15328
  import { platform as platform4 } from "node:os";
15051
- import { join as join17 } from "node:path";
15329
+ import { join as join18 } from "node:path";
15052
15330
  function updateShownRecord(engagementId, update, home) {
15053
15331
  let temp = null;
15054
15332
  try {
15055
15333
  const dir = engagementStateDir(engagementId, home);
15056
- const path = join17(dir, SHOWN_PLANS_FILENAME);
15334
+ const path = join18(dir, SHOWN_PLANS_FILENAME);
15057
15335
  let current;
15058
15336
  try {
15059
15337
  current = parseShownRecord(readFileSync14(path, "utf-8"));
@@ -15063,7 +15341,7 @@ function updateShownRecord(engagementId, update, home) {
15063
15341
  const next = update(current);
15064
15342
  const body = serialiseShownRecord(next);
15065
15343
  mkdirSync9(dir, { recursive: true, mode: 448 });
15066
- temp = join17(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
15344
+ temp = join18(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
15067
15345
  writeFileSync6(temp, body, { mode: 384 });
15068
15346
  if (platform4() !== "win32")
15069
15347
  chmodSync3(temp, 384);
@@ -15140,7 +15418,7 @@ function readAndCheckPlan(repoRoot, phase, description, verb) {
15140
15418
  const fileRel = planFileRel(phase);
15141
15419
  let text;
15142
15420
  try {
15143
- text = readFileSync15(join18(repoRoot, fileRel), "utf-8");
15421
+ text = readFileSync15(join19(repoRoot, fileRel), "utf-8");
15144
15422
  } catch (err) {
15145
15423
  const code = err.code;
15146
15424
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -15296,7 +15574,7 @@ function removalOwedLines(verb, parts) {
15296
15574
  function redrawLine(repoRoot, phase) {
15297
15575
  let exists;
15298
15576
  try {
15299
- exists = existsSync11(join18(repoRoot, DIAGRAM_DIR_REL, embedImageName(phaseDiagramBase(phase))));
15577
+ exists = existsSync12(join19(repoRoot, DIAGRAM_DIR_REL, embedImageName(phaseDiagramBase(phase))));
15300
15578
  } catch {
15301
15579
  return null;
15302
15580
  }
@@ -15917,7 +16195,7 @@ function readTimeLine(stdout) {
15917
16195
  }
15918
16196
  }
15919
16197
  function runTimeChild(binary, credential, repoRoot) {
15920
- return runRunnerChild(binary, ["time"], credential, repoRoot, readTimeLine, TIME_UNREACHABLE);
16198
+ return runRunnerChild(binary, ["time", "--final"], credential, repoRoot, readTimeLine, TIME_UNREACHABLE);
15921
16199
  }
15922
16200
  function runProveQueueChild(binary, credential, repoRoot) {
15923
16201
  return runRunnerChild(binary, ["prove", "--send-queued"], credential, repoRoot, () => void 0, void 0);
@@ -16243,7 +16521,7 @@ var PHASE_MODELS_FILENAME = "phase-models.json";
16243
16521
  var PACING_FOLDER = "pacing";
16244
16522
  function pacingFile(engagementId, name, home) {
16245
16523
  try {
16246
- return join19(engagementStateDir(engagementId, home), PACING_FOLDER, name);
16524
+ return join20(engagementStateDir(engagementId, home), PACING_FOLDER, name);
16247
16525
  } catch {
16248
16526
  return null;
16249
16527
  }
@@ -16397,7 +16675,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
16397
16675
  if (pinned !== null)
16398
16676
  assertEngagementId(pinned);
16399
16677
  const fromStore = pinned === null ? null : readEngagementEnv(pinned, home);
16400
- const looked = pinned === null ? `${join20(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
16678
+ const looked = pinned === null ? `${join21(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
16401
16679
  const values = {};
16402
16680
  for (const key of OPEN_PHASE_ENV_KEYS) {
16403
16681
  const value = (env[key] ?? fromStore?.[key] ?? "").trim();
@@ -16414,7 +16692,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
16414
16692
  }
16415
16693
  function readPinnedEngagementId(repoRoot) {
16416
16694
  try {
16417
- const pin = JSON.parse(readFileSync17(join20(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
16695
+ const pin = JSON.parse(readFileSync17(join21(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
16418
16696
  const id = pin[PIN_ENGAGEMENT_ID_FIELD];
16419
16697
  return typeof id === "string" && id !== "" ? id : null;
16420
16698
  } catch {
@@ -16895,7 +17173,7 @@ async function writeDrawOutcome(repoRoot, deps, outcome, target, warnings, canno
16895
17173
 
16896
17174
  // dist/check-tasks.js
16897
17175
  import { readFileSync as readFileSync18 } from "node:fs";
16898
- import { join as join21, resolve as resolve6 } from "node:path";
17176
+ import { join as join22, resolve as resolve6 } from "node:path";
16899
17177
  var CHECK_TASKS_USAGE = `${HALFCYCLE_COMMAND} check-tasks <phase> [--with-task-list] [--repo <root>]`;
16900
17178
  function parseCheckTasksArgs(argv) {
16901
17179
  const positionals = [];
@@ -16993,7 +17271,7 @@ function readPhasePlanParts(repoRoot, phase, description) {
16993
17271
  const file = planFileRel(phase);
16994
17272
  let text;
16995
17273
  try {
16996
- text = readFileSync18(join21(repoRoot, file), "utf-8");
17274
+ text = readFileSync18(join22(repoRoot, file), "utf-8");
16997
17275
  } catch (err) {
16998
17276
  const code = err.code;
16999
17277
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -17026,7 +17304,7 @@ function readPlanSpecFiles(repoRoot, plan) {
17026
17304
  const rel = `${SPEC_FOLDER}/${slug}.md`;
17027
17305
  let text;
17028
17306
  try {
17029
- text = readFileSync18(join21(repoRoot, rel), "utf-8");
17307
+ text = readFileSync18(join22(repoRoot, rel), "utf-8");
17030
17308
  } catch (err) {
17031
17309
  const code = err.code;
17032
17310
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -17417,8 +17695,8 @@ function runProveChild(binary, credential, repoRoot, words, deps = {}) {
17417
17695
 
17418
17696
  // dist/uninstall.js
17419
17697
  import { lstatSync as lstatSync3, readFileSync as readFileSync20, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync7 } from "node:fs";
17420
- import { homedir as homedir5 } from "node:os";
17421
- import { dirname as dirname6, join as join22, relative as relative6, sep as sep2 } from "node:path";
17698
+ import { homedir as homedir6 } from "node:os";
17699
+ import { dirname as dirname6, join as join23, relative as relative6, sep as sep2 } from "node:path";
17422
17700
  var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
17423
17701
  function parseUninstallArgs(args2) {
17424
17702
  let removeCredential = false;
@@ -17453,7 +17731,7 @@ function readMergedJson(targetRepo, rule) {
17453
17731
  const { rel } = rule;
17454
17732
  if (symlinkOnPath(targetRepo, rel))
17455
17733
  return { state: "symlink" };
17456
- const path = join22(targetRepo, rel);
17734
+ const path = join23(targetRepo, rel);
17457
17735
  if (!lexists(path))
17458
17736
  return { state: "absent" };
17459
17737
  let text;
@@ -17476,8 +17754,8 @@ function readMergedJson(targetRepo, rule) {
17476
17754
  }
17477
17755
  function validate(options, rules) {
17478
17756
  const { targetRepo, removeCredential, home } = options;
17479
- const manifestPath = join22(targetRepo, INSTALL_MANIFEST_REL);
17480
- const pinPath = join22(targetRepo, BUNDLE_PIN_REL);
17757
+ const manifestPath = join23(targetRepo, INSTALL_MANIFEST_REL);
17758
+ const pinPath = join23(targetRepo, BUNDLE_PIN_REL);
17481
17759
  if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
17482
17760
  throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
17483
17761
  }
@@ -17518,7 +17796,7 @@ function validate(options, rules) {
17518
17796
  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.`);
17519
17797
  }
17520
17798
  if (engagementId !== void 0) {
17521
- const expectedParent = join22(home ?? homedir5(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
17799
+ const expectedParent = join23(home ?? homedir6(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
17522
17800
  if (dirname6(engagementStateDir(engagementId, home)) !== expectedParent) {
17523
17801
  throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
17524
17802
  }
@@ -17544,7 +17822,7 @@ function mergedFileRules(writeSet) {
17544
17822
  function writeBack(targetRepo, rel, current, next, lists, name) {
17545
17823
  if (next === current)
17546
17824
  return;
17547
- const path = join22(targetRepo, rel);
17825
+ const path = join23(targetRepo, rel);
17548
17826
  try {
17549
17827
  if (next === null) {
17550
17828
  rmSync6(path);
@@ -17561,7 +17839,7 @@ function statusLineClaimedFromTree(targetRepo, writeSet) {
17561
17839
  try {
17562
17840
  if (symlinkOnPath(targetRepo, SETTINGS_REL))
17563
17841
  return {};
17564
- const value = JSON.parse(readFileSync20(join22(targetRepo, SETTINGS_REL), "utf-8"));
17842
+ const value = JSON.parse(readFileSync20(join23(targetRepo, SETTINGS_REL), "utf-8"));
17565
17843
  if (!isPlainObject3(value) || !("statusLine" in value))
17566
17844
  return {};
17567
17845
  const hash = canonicalSha256(value["statusLine"]);
@@ -17575,7 +17853,7 @@ function noRecord(targetRepo, writeSet) {
17575
17853
  const claim = (rel, header, lines) => {
17576
17854
  let text;
17577
17855
  try {
17578
- text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync20(join22(targetRepo, rel), "utf-8");
17856
+ text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync20(join23(targetRepo, rel), "utf-8");
17579
17857
  } catch {
17580
17858
  text = "";
17581
17859
  }
@@ -17816,7 +18094,7 @@ function lastOccurrence(text, block) {
17816
18094
  }
17817
18095
  function coveredPathExists(targetRepo, line) {
17818
18096
  const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
17819
- return path !== "" && lexists(join22(targetRepo, path));
18097
+ return path !== "" && lexists(join23(targetRepo, path));
17820
18098
  }
17821
18099
  function selfBuildMergedFileRules() {
17822
18100
  return {
@@ -17830,7 +18108,7 @@ function removeSelfBuildOwnedFile(targetRepo, rel, lists, name) {
17830
18108
  name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
17831
18109
  return;
17832
18110
  }
17833
- const abs = join22(targetRepo, rel);
18111
+ const abs = join23(targetRepo, rel);
17834
18112
  let isFile;
17835
18113
  try {
17836
18114
  isFile = lstatSync3(abs).isFile();
@@ -17854,7 +18132,7 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
17854
18132
  name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
17855
18133
  return;
17856
18134
  }
17857
- const abs = join22(targetRepo, rel);
18135
+ const abs = join23(targetRepo, rel);
17858
18136
  let bytes;
17859
18137
  try {
17860
18138
  bytes = readFileSync20(abs);
@@ -17881,7 +18159,7 @@ function removeSelfBuildCrewProfile(targetRepo, rel, text, lists, name) {
17881
18159
  name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
17882
18160
  return;
17883
18161
  }
17884
- const abs = join22(targetRepo, rel);
18162
+ const abs = join23(targetRepo, rel);
17885
18163
  let bytes;
17886
18164
  try {
17887
18165
  bytes = readFileSync20(abs);
@@ -17906,7 +18184,7 @@ function removeSelfBuildCrewProfile(targetRepo, rel, text, lists, name) {
17906
18184
  function removeSelfBuildEmptyDir(targetRepo, dir, lists, name) {
17907
18185
  if (symlinkOnPath(targetRepo, dir))
17908
18186
  return;
17909
- const abs = join22(targetRepo, dir);
18187
+ const abs = join23(targetRepo, dir);
17910
18188
  try {
17911
18189
  if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
17912
18190
  return;
@@ -17940,7 +18218,7 @@ function uninstallSelfBuild(options) {
17940
18218
  throw err;
17941
18219
  }
17942
18220
  const { targetRepo } = options;
17943
- const pinExisted = lexists(join22(targetRepo, BUNDLE_PIN_REL));
18221
+ const pinExisted = lexists(join23(targetRepo, BUNDLE_PIN_REL));
17944
18222
  const name = (list2, text, _path) => {
17945
18223
  list2.push(text);
17946
18224
  };
@@ -18056,7 +18334,7 @@ function uninstall(options) {
18056
18334
  const gitattributes = manifest.gitattributes ?? { created: false, appended: [] };
18057
18335
  trimAppendedFile(targetRepo, gitattributes, deleteWhenEmpty(gitattributes.created), gitattributesRules(writeSet), result, name);
18058
18336
  const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0 && result.keptIgnoreLines.length === 0;
18059
- if (checked.manifest === null && !lexists(join22(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
18337
+ if (checked.manifest === null && !lexists(join23(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
18060
18338
  return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
18061
18339
  }
18062
18340
  const credential = handleCredential(options, checked);
@@ -18064,7 +18342,7 @@ function uninstall(options) {
18064
18342
  if (result.failed.length === 0 && !credentialFailed) {
18065
18343
  removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
18066
18344
  try {
18067
- rmSync6(join22(targetRepo, INSTALL_MANIFEST_REL), { force: true });
18345
+ rmSync6(join23(targetRepo, INSTALL_MANIFEST_REL), { force: true });
18068
18346
  named2.add(INSTALL_MANIFEST_REL);
18069
18347
  } catch {
18070
18348
  name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
@@ -18099,13 +18377,13 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
18099
18377
  return;
18100
18378
  const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
18101
18379
  writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
18102
- if (adopted && lexists(join22(targetRepo, rel))) {
18380
+ if (adopted && lexists(join23(targetRepo, rel))) {
18103
18381
  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);
18104
18382
  }
18105
18383
  }
18106
18384
  function removeCodexConfigBlock(targetRepo, record2, deleteWhenEmpty, lists, name) {
18107
18385
  const rel = CODEX_CONFIG_REL2;
18108
- const path = join22(targetRepo, rel);
18386
+ const path = join23(targetRepo, rel);
18109
18387
  if (symlinkOnPath(targetRepo, rel)) {
18110
18388
  if (lexists(path))
18111
18389
  name(lists.leftAlone, `${rel} (a symbolic link, or inside one \u2014 not followed)`, rel);
@@ -18150,7 +18428,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
18150
18428
  name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
18151
18429
  return;
18152
18430
  }
18153
- const abs = join22(targetRepo, path);
18431
+ const abs = join23(targetRepo, path);
18154
18432
  let isFile;
18155
18433
  try {
18156
18434
  isFile = lstatSync3(abs).isFile();
@@ -18184,7 +18462,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
18184
18462
  function removeEmptyDir(targetRepo, dir, writeSet, lists, name) {
18185
18463
  if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
18186
18464
  return;
18187
- const abs = join22(targetRepo, dir);
18465
+ const abs = join23(targetRepo, dir);
18188
18466
  try {
18189
18467
  if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
18190
18468
  return;
@@ -18203,7 +18481,7 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
18203
18481
  name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
18204
18482
  return [];
18205
18483
  }
18206
- const path = join22(targetRepo, rel);
18484
+ const path = join23(targetRepo, rel);
18207
18485
  let before;
18208
18486
  try {
18209
18487
  before = readFileSync20(path, "utf-8");
@@ -18250,16 +18528,16 @@ function filesUnder(root, rel) {
18250
18528
  const walk = (dirRel) => {
18251
18529
  let names;
18252
18530
  try {
18253
- if (!lstatSync3(join22(root, dirRel)).isDirectory())
18531
+ if (!lstatSync3(join23(root, dirRel)).isDirectory())
18254
18532
  return;
18255
- names = readdirSync(join22(root, dirRel));
18533
+ names = readdirSync(join23(root, dirRel));
18256
18534
  } catch {
18257
18535
  return;
18258
18536
  }
18259
18537
  for (const entry of names.sort()) {
18260
18538
  const childRel = `${dirRel}/${entry}`;
18261
18539
  try {
18262
- if (lstatSync3(join22(root, childRel)).isDirectory())
18540
+ if (lstatSync3(join23(root, childRel)).isDirectory())
18263
18541
  walk(childRel);
18264
18542
  else
18265
18543
  out.push(childRel);
@@ -18272,7 +18550,7 @@ function filesUnder(root, rel) {
18272
18550
  return out;
18273
18551
  }
18274
18552
  function shownPath(path, home) {
18275
- const base = home ?? homedir5();
18553
+ const base = home ?? homedir6();
18276
18554
  const rel = relative6(base, path);
18277
18555
  const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep2);
18278
18556
  return `${under ? `~/${rel.split(sep2).join("/")}` : path}/`;
@@ -18332,7 +18610,7 @@ function renderUninstallReport(result, home) {
18332
18610
 
18333
18611
  // dist/banner-facts.js
18334
18612
  import { readFileSync as readFileSync21 } from "node:fs";
18335
- import { basename as basename4, join as join23, resolve as resolve7 } from "node:path";
18613
+ import { basename as basename4, join as join24, resolve as resolve7 } from "node:path";
18336
18614
  var BRAND_URL = "halfcycle.ai";
18337
18615
  var TAGLINE = [
18338
18616
  "Fewer cycles.",
@@ -18345,7 +18623,7 @@ var TAGLINE_NARROW = [
18345
18623
  ];
18346
18624
  function bundleVersion() {
18347
18625
  try {
18348
- const pkg = JSON.parse(readFileSync21(join23(BUNDLE_ROOT, "package.json"), "utf-8"));
18626
+ const pkg = JSON.parse(readFileSync21(join24(BUNDLE_ROOT, "package.json"), "utf-8"));
18349
18627
  return typeof pkg.version === "string" ? pkg.version : void 0;
18350
18628
  } catch {
18351
18629
  return void 0;
@@ -18441,7 +18719,7 @@ function safeBasename(dir) {
18441
18719
  // dist/pace.js
18442
18720
  import { spawn as spawn3 } from "node:child_process";
18443
18721
  import { readFileSync as readFileSync22 } from "node:fs";
18444
- import { join as join24 } from "node:path";
18722
+ import { join as join25 } from "node:path";
18445
18723
  var PACE_USAGE = `${HALFCYCLE_COMMAND} pace [--agent claude-code|codex] [--start <phase> [--agents <n>]] [--repo <root>]`;
18446
18724
  function parsePaceArgs(argv) {
18447
18725
  let repoRoot;
@@ -18555,7 +18833,7 @@ function notPacedLine(reason) {
18555
18833
  }
18556
18834
  function readPromptScript(repoRoot) {
18557
18835
  try {
18558
- return readFileSync22(join24(repoRoot, ".claude", "hooks", "user-prompt-scope.sh"), "utf-8");
18836
+ return readFileSync22(join25(repoRoot, ".claude", "hooks", "user-prompt-scope.sh"), "utf-8");
18559
18837
  } catch {
18560
18838
  return null;
18561
18839
  }
@@ -18593,14 +18871,17 @@ async function runPace(repoRoot, start, deps, agent = null) {
18593
18871
  return { stdout: `${paceStartLinesOf(result.suggestion, behind).join("\n")}
18594
18872
  `, stderr: "", exitCode: 0 };
18595
18873
  }
18596
- const lines = [paceAllowedNowOf(result.allowedNow)];
18874
+ const share = result.keepFreePercent ?? KEEP_FREE_DEFAULT;
18875
+ const lines = [paceBuildUpToOf(result.number), paceAllowedNowOf(result.allowedNow), share === 0 ? PACE_P5 : paceP3Of(share)];
18876
+ if (result.limit !== "reached" && result.usage !== void 0) {
18877
+ lines.push(limitUsedOf(result.usage.used, result.usage.limit));
18878
+ }
18597
18879
  if (result.limit === "reached" && result.limitHeld !== void 0) {
18598
18880
  const held = result.limitHeld;
18599
18881
  lines.push(limitReachedLineOf(held.used, held.limit, held.working, held.raiseBy));
18600
18882
  }
18601
18883
  if (result.windowKind !== void 0 && result.until !== void 0) {
18602
18884
  const until = new Date(result.until);
18603
- const share = result.keepFreePercent ?? KEEP_FREE_DEFAULT;
18604
18885
  if (result.pace === "slowing")
18605
18886
  lines.push(paceS1Of(result.windowKind, result.allowedNow, until, deps.now(), share));
18606
18887
  else if (result.pace === "holding")
@@ -18611,10 +18892,9 @@ async function runPace(repoRoot, start, deps, agent = null) {
18611
18892
  }
18612
18893
 
18613
18894
  // dist/route.js
18614
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync23, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "node:fs";
18615
- import { dirname as dirname7, join as join25 } from "node:path";
18895
+ import { readFileSync as readFileSync23 } from "node:fs";
18896
+ import { join as join26 } from "node:path";
18616
18897
  var ROUTE_USAGE = `${HALFCYCLE_COMMAND} route <phase> <task> [--agent claude-code|codex] [--repo <root>]`;
18617
- var ROUTING_FILENAME = "routing.json";
18618
18898
  var ROUTE_WORK_READ_DEADLINE_MS = 5e3;
18619
18899
  function parseRouteArgs(argv) {
18620
18900
  const positionals = [];
@@ -18656,7 +18936,7 @@ function parseRouteArgs(argv) {
18656
18936
  return { ok: true, phase: positionals[0], task: positionals[1], agent: agent ?? "claude-code", repoRoot };
18657
18937
  }
18658
18938
  function routingPath(engagementId, home) {
18659
- return join25(engagementStateDir(engagementId, home), ROUTING_FILENAME);
18939
+ return join26(engagementStateDir(engagementId, home), ROUTING_FILENAME);
18660
18940
  }
18661
18941
  function readCheapStarted(path, phase) {
18662
18942
  try {
@@ -18668,19 +18948,11 @@ function readCheapStarted(path, phase) {
18668
18948
  return [];
18669
18949
  }
18670
18950
  }
18671
- function recordCheapStarted(path, phase, task) {
18672
- const started = readCheapStarted(path, phase);
18673
- const record2 = { phase, cheapStarted: started.includes(task) ? started : [...started, task] };
18674
- mkdirSync10(dirname7(path), { recursive: true, mode: 448 });
18675
- const tmp = `${path}.${String(process.pid)}.tmp`;
18676
- writeFileSync8(tmp, `${JSON.stringify(record2)}
18677
- `, { mode: 384 });
18678
- renameSync4(tmp, path);
18679
- }
18680
- function attemptsOf(read, task) {
18951
+ function workOf(read, task) {
18681
18952
  if (read.kind !== "work")
18682
18953
  return null;
18683
- return read.work.tasks.find((t) => t.task === task)?.attempts ?? [];
18954
+ const entry = read.work.tasks.find((t) => t.task === task);
18955
+ return { attempts: entry?.attempts ?? [], passed: entry?.standing === "passed" };
18684
18956
  }
18685
18957
  function lastTierOf(attempts) {
18686
18958
  return [...attempts].sort((a, b) => b.attempt - a.attempt)[0].builderTier;
@@ -18734,9 +19006,11 @@ async function runRoute(repoRoot, phase, taskId, agent, deps) {
18734
19006
  } catch {
18735
19007
  return answer(routeStartLineOf(holder, taskId, member.tier));
18736
19008
  }
18737
- const attempts = attemptsOf(await readWorkInTime(deps, credential, phase), taskId);
18738
- if (attempts !== null && attempts.length > 0) {
18739
- return answer(routeStartLineOf(holder, taskId, tierAboveOf(lastTierOf(attempts))));
19009
+ const work = workOf(await readWorkInTime(deps, credential, phase), taskId);
19010
+ if (work !== null && work.attempts.length > 0) {
19011
+ if (work.passed)
19012
+ return answer(routePassedLineOf(taskId, lastTierOf(work.attempts)));
19013
+ return answer(routeStartLineOf(holder, taskId, tierAboveOf(lastTierOf(work.attempts))));
18740
19014
  }
18741
19015
  if (agent === "codex" && !hasCheaperRole(member))
18742
19016
  return answer(routeStartLineOf(holder, taskId, member.tier));
@@ -18748,19 +19022,14 @@ async function runRoute(repoRoot, phase, taskId, agent, deps) {
18748
19022
  }
18749
19023
  if (readCheapStarted(path, phase).includes(taskId))
18750
19024
  return answer(routeStartLineOf(holder, taskId, member.tier));
18751
- try {
18752
- recordCheapStarted(path, phase, taskId);
18753
- } catch {
18754
- return answer(routeStartLineOf(holder, taskId, member.tier));
18755
- }
18756
19025
  return answer(agent === "codex" ? routeCheaperCodexLineOf(holder, taskId) : routeCheaperClaudeLineOf(holder, taskId, AGENT_PROFILES["claude-code"].tierModels.fast.model));
18757
19026
  }
18758
19027
 
18759
19028
  // dist/restore.js
18760
19029
  import { spawnSync as spawnSync3 } from "node:child_process";
18761
- import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync24, readlinkSync, realpathSync as realpathSync6, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
18762
- import { homedir as homedir6 } from "node:os";
18763
- import { join as join26, resolve as resolve8 } from "node:path";
19030
+ import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync10, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync24, readlinkSync, realpathSync as realpathSync6, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
19031
+ import { homedir as homedir7 } from "node:os";
19032
+ import { join as join27, resolve as resolve8 } from "node:path";
18764
19033
  var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
18765
19034
  function parseRestoreArgs(rest) {
18766
19035
  if (rest.length > 1)
@@ -18782,28 +19051,28 @@ function runRestore(cwd, id, options = {}) {
18782
19051
  if (engagementId === null) {
18783
19052
  return refused2(`Halfcycle is not set up in this project, so it has saved nothing here. To set it up, run: ${HALFCYCLE_COMMAND}`);
18784
19053
  }
18785
- const folder = join26(options.home ?? homedir6(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
19054
+ const folder2 = join27(options.home ?? homedir7(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
18786
19055
  const refs = savedRefs(root);
18787
19056
  if (refs === null)
18788
19057
  return refused2("Halfcycle could not read the saved copies in this project, so nothing was put back.");
18789
- pruneAtRestore(root, folder, refs, now());
19058
+ pruneAtRestore(root, folder2, refs, now());
18790
19059
  for (const [saveId, madeAt] of [...refs]) {
18791
19060
  const answer = savedPruneAnswer({ refMadeAt: madeAt, folderChangedAt: 0, folderRepository: null }, { repository: null, now: now() });
18792
19061
  if (answer !== "keep")
18793
19062
  refs.delete(saveId);
18794
19063
  }
18795
19064
  if (id === void 0)
18796
- return list(root, folder, refs);
18797
- return restoreOne(root, folder, refs, id);
19065
+ return list(root, folder2, refs);
19066
+ return restoreOne(root, folder2, refs, id);
18798
19067
  }
18799
19068
  function refused2(line) {
18800
19069
  return { stdout: "", stderr: `${line}
18801
19070
  `, exitCode: 1 };
18802
19071
  }
18803
- function list(root, folder, refs) {
19072
+ function list(root, folder2, refs) {
18804
19073
  const rows = [];
18805
19074
  for (const [id, at] of refs) {
18806
- const saved = readSaved(folder, id);
19075
+ const saved = readSaved(folder2, id);
18807
19076
  if (saved !== null)
18808
19077
  rows.push({ id, at, what: saved.what });
18809
19078
  }
@@ -18833,14 +19102,14 @@ function whenOf(ms) {
18833
19102
  function printable(text) {
18834
19103
  return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
18835
19104
  }
18836
- function restoreOne(root, folder, refs, rawId) {
19105
+ function restoreOne(root, folder2, refs, rawId) {
18837
19106
  const valid = savedIdSchema.safeParse(rawId);
18838
19107
  const named2 = valid.success ? `"${rawId}"` : "with that name";
18839
19108
  if (!valid.success || !refs.has(rawId)) {
18840
19109
  return refused2(`No saved copy ${named2} is kept in this project. Saved copies are kept for 7 days. To see the ones there are, run: ${LIST_COMMAND}`);
18841
19110
  }
18842
19111
  const id = valid.data;
18843
- const saved = readSaved(folder, id);
19112
+ const saved = readSaved(folder2, id);
18844
19113
  if (saved === null)
18845
19114
  return refused2(`The saved copy "${id}" could not be read, so nothing was put back.`);
18846
19115
  const ref = savedRefOf(id);
@@ -18858,7 +19127,7 @@ function restoreOne(root, folder, refs, rawId) {
18858
19127
  if (p.keptIn === "commit")
18859
19128
  fromCommit.push(p.path);
18860
19129
  else
18861
- restoreCopy(root, join26(folder, id), p.path, out);
19130
+ restoreCopy(root, join27(folder2, id), p.path, out);
18862
19131
  }
18863
19132
  if (fromCommit.length > 0)
18864
19133
  restoreFromCommit(root, ref, fromCommit, out);
@@ -18886,12 +19155,12 @@ function makeFolders(root, rel) {
18886
19155
  const segments = rel.split("/");
18887
19156
  let at = root;
18888
19157
  for (let i = 0; i < segments.length - 1; i++) {
18889
- at = join26(at, segments[i]);
19158
+ at = join27(at, segments[i]);
18890
19159
  const shown2 = segments.slice(0, i + 1).join("/");
18891
19160
  let st = lstatOrNull3(at);
18892
19161
  if (st === null) {
18893
19162
  try {
18894
- mkdirSync11(at);
19163
+ mkdirSync10(at);
18895
19164
  } catch {
18896
19165
  }
18897
19166
  st = lstatOrNull3(at);
@@ -18906,7 +19175,7 @@ function makeFolders(root, rel) {
18906
19175
  return null;
18907
19176
  }
18908
19177
  function place(root, rel, content, out) {
18909
- const abs = join26(root, rel);
19178
+ const abs = join27(root, rel);
18910
19179
  const there = lstatOrNull3(abs);
18911
19180
  if (there !== null) {
18912
19181
  if (sameAs(abs, there, content))
@@ -18954,14 +19223,14 @@ function restoreCopy(root, saveDir, rel, out) {
18954
19223
  let at = saveDir;
18955
19224
  const chain = [SAVED_COPIES_DIR_NAME, ...segments];
18956
19225
  for (let i = 0; i < chain.length - 1; i++) {
18957
- at = join26(at, chain[i]);
19226
+ at = join27(at, chain[i]);
18958
19227
  const st2 = lstatOrNull3(at);
18959
19228
  if (st2 === null || st2.isSymbolicLink() || !st2.isDirectory()) {
18960
19229
  out.left.push(`${rel}: the saved copy does not hold it`);
18961
19230
  return;
18962
19231
  }
18963
19232
  }
18964
- const src = join26(at, chain[chain.length - 1]);
19233
+ const src = join27(at, chain[chain.length - 1]);
18965
19234
  const st = lstatOrNull3(src);
18966
19235
  try {
18967
19236
  if (st !== null && st.isSymbolicLink())
@@ -19065,7 +19334,7 @@ function restoreLines(root, ref, saved, out) {
19065
19334
  }
19066
19335
  function pinnedEngagement(root) {
19067
19336
  try {
19068
- const pin = JSON.parse(readFileSync24(join26(root, BUNDLE_PIN_REL), "utf-8"));
19337
+ const pin = JSON.parse(readFileSync24(join27(root, BUNDLE_PIN_REL), "utf-8"));
19069
19338
  return isEngagementId(pin.engagementId) ? pin.engagementId : null;
19070
19339
  } catch {
19071
19340
  return null;
@@ -19086,12 +19355,12 @@ function savedRefs(root) {
19086
19355
  }
19087
19356
  return refs;
19088
19357
  }
19089
- function readSaved(folder, id) {
19090
- const dir = join26(folder, id);
19358
+ function readSaved(folder2, id) {
19359
+ const dir = join27(folder2, id);
19091
19360
  const st = lstatOrNull3(dir);
19092
19361
  if (st === null || !st.isDirectory())
19093
19362
  return null;
19094
- const file = join26(dir, SAVED_FILE_NAME);
19363
+ const file = join27(dir, SAVED_FILE_NAME);
19095
19364
  const fst = lstatOrNull3(file);
19096
19365
  if (fst === null || !fst.isFile())
19097
19366
  return null;
@@ -19102,13 +19371,13 @@ function readSaved(folder, id) {
19102
19371
  return null;
19103
19372
  }
19104
19373
  }
19105
- function pruneAtRestore(root, folder, refs, now) {
19374
+ function pruneAtRestore(root, folder2, refs, now) {
19106
19375
  try {
19107
19376
  const common = git2(root, ["rev-parse", "--git-common-dir"]);
19108
19377
  const repository = common.ok && common.text.trim() !== "" ? realOrSelf(resolve8(root, common.text.trim())) : null;
19109
19378
  let names = [];
19110
19379
  try {
19111
- names = readdirSync2(folder);
19380
+ names = readdirSync2(folder2);
19112
19381
  } catch {
19113
19382
  names = [];
19114
19383
  }
@@ -19117,7 +19386,7 @@ function pruneAtRestore(root, folder, refs, now) {
19117
19386
  if (!savedIdSchema.safeParse(id).success)
19118
19387
  continue;
19119
19388
  here.add(id);
19120
- const dir = join26(folder, id);
19389
+ const dir = join27(folder2, id);
19121
19390
  const st = lstatOrNull3(dir);
19122
19391
  const answer = savedPruneAnswer({
19123
19392
  refMadeAt: refs.get(id) ?? null,
@@ -19145,7 +19414,7 @@ function pruneAtRestore(root, folder, refs, now) {
19145
19414
  }
19146
19415
  function markedRepository(dir) {
19147
19416
  try {
19148
- const text = readFileSync24(join26(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
19417
+ const text = readFileSync24(join27(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
19149
19418
  return text === "" ? null : text;
19150
19419
  } catch {
19151
19420
  return null;
@@ -19174,7 +19443,110 @@ function realOrSelf(p) {
19174
19443
  }
19175
19444
  }
19176
19445
 
19446
+ // dist/request-shapes-verb.js
19447
+ import { chmodSync as chmodSync4, existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync25, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "node:fs";
19448
+ import { homedir as homedir8, platform as platform5 } from "node:os";
19449
+ import { join as join28 } from "node:path";
19450
+ var REQUEST_SHAPES_USAGE = `${HALFCYCLE_COMMAND} request-shapes on|off|show|clear`;
19451
+ var REQUEST_SHAPES_FORMS = ["on", "off", "show", "clear"];
19452
+ var REQUEST_SHAPES_ON_LINE = "Halfcycle will write down the shape of every request this computer sends to it, never what is in them, until you run request-shapes off.";
19453
+ var REQUEST_SHAPES_OFF_LINE = "Halfcycle has stopped writing down requests. What it wrote is still there: run request-shapes show to read it, or request-shapes clear to remove it.";
19454
+ var REQUEST_SHAPES_EMPTY_LINE = "No requests have been written down.";
19455
+ var REQUEST_SHAPES_CLEARED_LINE = "Halfcycle has removed the requests it wrote down.";
19456
+ function parseRequestShapesArgs(rest) {
19457
+ if (rest.length !== 1)
19458
+ return null;
19459
+ const form = rest[0];
19460
+ return REQUEST_SHAPES_FORMS.includes(form ?? "") ? form : null;
19461
+ }
19462
+ function asLoggedLine(text) {
19463
+ let value;
19464
+ try {
19465
+ value = JSON.parse(text);
19466
+ } catch {
19467
+ return null;
19468
+ }
19469
+ if (typeof value !== "object" || value === null)
19470
+ return null;
19471
+ const v = value;
19472
+ const str = (k) => typeof v[k] === "string" ? v[k] : "";
19473
+ return {
19474
+ at: str("at"),
19475
+ process: str("process"),
19476
+ method: str("method"),
19477
+ host: str("host"),
19478
+ path: str("path"),
19479
+ query: Array.isArray(v["query"]) ? v["query"].filter((q) => typeof q === "string") : [],
19480
+ shape: typeof v["shape"] === "string" ? v["shape"] : null,
19481
+ status: typeof v["status"] === "number" || typeof v["status"] === "string" ? v["status"] : ""
19482
+ };
19483
+ }
19484
+ function formatLine(line) {
19485
+ const at = line.at.replace(/\.\d+Z$/, "Z");
19486
+ const query = line.query.length > 0 ? `?${line.query.join("&")}` : "";
19487
+ const head = `${at} ${line.process.padEnd(22)} ${line.method.padEnd(4)} ${line.host} ${line.path}${query} -> ${String(line.status)}`;
19488
+ return line.shape === null ? head : `${head}
19489
+ ${line.shape}`;
19490
+ }
19491
+ function showLog(logPath2) {
19492
+ let text = "";
19493
+ try {
19494
+ text = existsSync13(logPath2) ? readFileSync25(logPath2, "utf-8") : "";
19495
+ } catch {
19496
+ text = "";
19497
+ }
19498
+ const lines = text.split("\n").filter((l) => l.trim() !== "").map(asLoggedLine).filter((l) => l !== null);
19499
+ if (lines.length === 0)
19500
+ return `${REQUEST_SHAPES_EMPTY_LINE}
19501
+ `;
19502
+ const count = lines.length === 1 ? "1 request." : `${lines.length} requests.`;
19503
+ return `${lines.map(formatLine).join("\n")}
19504
+ ${count}
19505
+ `;
19506
+ }
19507
+ function ownerOnly(path, mode) {
19508
+ if (platform5() !== "win32")
19509
+ chmodSync4(path, mode);
19510
+ }
19511
+ function runRequestShapes(form, home = homedir8()) {
19512
+ const folder2 = join28(home, HALFCYCLE_DIR_NAME, REQUEST_SHAPES_DIR_NAME);
19513
+ const switchPath = join28(folder2, REQUEST_SHAPES_SWITCH_FILENAME);
19514
+ const logPath2 = join28(folder2, REQUEST_SHAPES_LOG_FILENAME);
19515
+ try {
19516
+ switch (form) {
19517
+ case "on":
19518
+ mkdirSync11(folder2, { recursive: true, mode: 448 });
19519
+ ownerOnly(folder2, 448);
19520
+ writeFileSync8(switchPath, "", { mode: 384 });
19521
+ ownerOnly(switchPath, 384);
19522
+ return { stdout: `${REQUEST_SHAPES_ON_LINE}
19523
+ `, stderr: "", exitCode: 0 };
19524
+ case "off":
19525
+ rmSync8(switchPath, { force: true });
19526
+ return { stdout: `${REQUEST_SHAPES_OFF_LINE}
19527
+ `, stderr: "", exitCode: 0 };
19528
+ case "show":
19529
+ return { stdout: showLog(logPath2), stderr: "", exitCode: 0 };
19530
+ case "clear":
19531
+ if (existsSync13(logPath2)) {
19532
+ writeFileSync8(logPath2, "", { mode: 384 });
19533
+ ownerOnly(logPath2, 384);
19534
+ }
19535
+ return { stdout: `${REQUEST_SHAPES_CLEARED_LINE}
19536
+ `, stderr: "", exitCode: 0 };
19537
+ }
19538
+ } catch {
19539
+ return {
19540
+ stdout: "",
19541
+ stderr: `halfcycle request-shapes: Halfcycle could not change its settings folder in your home folder. Check that you can write to it, then run this again.
19542
+ `,
19543
+ exitCode: 1
19544
+ };
19545
+ }
19546
+ }
19547
+
19177
19548
  // dist/bin.js
19549
+ knowRequestShapeVerbs(CLI_VERBS);
19178
19550
  var ASSUME_YES_FLAGS = /* @__PURE__ */ new Set(["--yes", "-y"]);
19179
19551
  var QUIET_FLAGS = /* @__PURE__ */ new Set(["--quiet", "-q"]);
19180
19552
  var VERBOSE_FLAGS = /* @__PURE__ */ new Set(["--verbose", "-v"]);
@@ -19267,6 +19639,9 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
19267
19639
  show what setting up this folder would add, change, remove or leave alone; writes nothing
19268
19640
  ${HALFCYCLE_COMMAND} doctor
19269
19641
  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
19642
+ ${REQUEST_SHAPES_USAGE}
19643
+ write down the shape of every request this computer sends to Halfcycle, never what is in
19644
+ them; show what was written; empty it; stop
19270
19645
  ${RESTORE_USAGE}
19271
19646
  list the copies Halfcycle saved before a change it let through, or bring one back; never
19272
19647
  replaces a file or a line of work that exists now
@@ -19396,6 +19771,22 @@ ${USAGE}`);
19396
19771
  process.exitCode = report.exitCode;
19397
19772
  return;
19398
19773
  }
19774
+ if (cmd === "request-shapes") {
19775
+ const form = parseRequestShapesArgs(rest);
19776
+ if (form === null) {
19777
+ process.stderr.write(`halfcycle request-shapes: usage: ${REQUEST_SHAPES_USAGE}
19778
+ `);
19779
+ process.exitCode = 2;
19780
+ return;
19781
+ }
19782
+ const run = runRequestShapes(form);
19783
+ if (run.stdout !== "")
19784
+ process.stdout.write(run.stdout);
19785
+ if (run.stderr !== "")
19786
+ process.stderr.write(run.stderr);
19787
+ process.exitCode = run.exitCode;
19788
+ return;
19789
+ }
19399
19790
  if (cmd === "restore") {
19400
19791
  const parsed = parseRestoreArgs(rest);
19401
19792
  if (!parsed.ok) {
@@ -19415,7 +19806,7 @@ ${USAGE}`);
19415
19806
  if (installArm) {
19416
19807
  const engagementIdArg = positionals[1];
19417
19808
  const engagementTypeRaw = positionals[2] ?? "client";
19418
- if (!existsSync12(targetRepo)) {
19809
+ if (!existsSync14(targetRepo)) {
19419
19810
  process.stderr.write(`${folderMissingRefusal(targetRepo)}
19420
19811
  `);
19421
19812
  process.exit(1);