halfcycle 0.13.0 → 0.14.0-rc.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.
@@ -10,6 +10,7 @@ halfcycle_pt_skip() {
10
10
  hc_pt_live_text= hc_pt_live_sid= hc_pt_live_agent= hc_pt_live_dir=
11
11
  hc_pt_own_file= hc_pt_own_agent_id= hc_pt_own_tool_use_id=
12
12
  halfcycle_pt_agent "$@" || return 1
13
+ halfcycle_pt_notes || return 1
13
14
  halfcycle_pt_quiet || return 1
14
15
  halfcycle_pt_live || return 1
15
16
  halfcycle_pt_own_copy || return 1
@@ -24,6 +25,18 @@ halfcycle_pt_agent() {
24
25
  [ "$#" -eq 4 ] && [ "$3" = --agent ] && [ "$4" = codex ] && { hc_pt_agent=codex; return 0; }
25
26
  return 1
26
27
  }
28
+ halfcycle_pt_notes() {
29
+ local LC_ALL=C hc_val hc_dir hc_re='^[A-Za-z0-9-]{1,128}$'
30
+ halfcycle_pt_string agent_id
31
+ case $? in 0) [ -z "$hc_val" ] || return 0 ;; 1) ;; *) return 1 ;; esac
32
+ halfcycle_pt_string session_id
33
+ case $? in 0) ;; 1) return 0 ;; *) return 1 ;; esac
34
+ [[ $hc_val =~ $hc_re ]] || return 0
35
+ hc_dir=${TMPDIR:-/tmp}
36
+ while [ "${hc_dir%/}" != "$hc_dir" ]; do hc_dir=${hc_dir%/}; done
37
+ [ -e "$hc_dir/halfcycle-notes-$hc_val.json" ] && return 1
38
+ return 0
39
+ }
27
40
  halfcycle_pt_own_read() {
28
41
  local LC_ALL=C hc_val hc_dir hc_re='^[A-Za-z0-9_-]{1,128}$'
29
42
  halfcycle_pt_string tool_name || return 1
@@ -62,7 +62,7 @@
62
62
  * home is the failure mode it exists to prevent.
63
63
  */
64
64
  /** The verbs the CLI dispatches. Anything else after `halfcycle ` is unknown. */
65
- export declare const CLI_VERBS: readonly ["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", "allow", "size", "regression", "crew"];
65
+ export declare const CLI_VERBS: readonly ["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", "allow", "size", "regression", "crew", "remove-copy"];
66
66
  export type CliVerb = (typeof CLI_VERBS)[number];
67
67
  /** A flag every invocation of the verb must carry. */
68
68
  export interface RequiredFlag {
@@ -1 +1 @@
1
- {"version":3,"file":"cli-contract.d.ts","sourceRoot":"","sources":["../src/cli-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAKH,iFAAiF;AACjF,eAAO,MAAM,SAAS,0TAA2T,CAAC;AAElV,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;CAClD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAgMD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAElE;AAED,iDAAiD;AACjD,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAAE,CAyBxF;AAuBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMtE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvE;AASD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;CAErB,CAAC;AA2BX;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAa1F"}
1
+ {"version":3,"file":"cli-contract.d.ts","sourceRoot":"","sources":["../src/cli-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAKH,iFAAiF;AACjF,eAAO,MAAM,SAAS,yUAA0U,CAAC;AAEjW,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;CAClD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAiMD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAElE;AAED,iDAAiD;AACjD,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EAAE,CAyBxF;AAuBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMtE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvE;AASD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;CAErB,CAAC;AA2BX;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAa1F"}
package/dist/cli.js CHANGED
@@ -2394,6 +2394,9 @@ function requestShapeLineText(line) {
2394
2394
  return ordered(`${cut > 0 ? prefix.slice(0, cut) : ""}\u2026`);
2395
2395
  }
2396
2396
 
2397
+ // ../core/dist/worker-copy-caches.js
2398
+ var WORKER_COPY_CACHE_FOLDERS = ["__pycache__", ".pytest_cache", ".mypy_cache", "node_modules"];
2399
+
2397
2400
  // dist/request-shapes.js
2398
2401
  var folder = join(homedir(), HALFCYCLE_DIR_NAME, REQUEST_SHAPES_DIR_NAME);
2399
2402
  var logPath = join(folder, REQUEST_SHAPES_LOG_FILENAME);
@@ -6408,6 +6411,7 @@ var pacingBasisTaskSchema = z24.object({
6408
6411
  human: z24.boolean(),
6409
6412
  kind: buildTaskKindSchema.optional()
6410
6413
  }).strict();
6414
+ var pacingBasisDoneTaskSchema = pacingBasisTaskSchema.extend({ done: z24.boolean() }).strict();
6411
6415
  var pacingBasisSchema = z24.object({
6412
6416
  outcome: z24.literal("basis"),
6413
6417
  claudePlan: z24.string().nullable().optional(),
@@ -6418,6 +6422,7 @@ var pacingBasisSchema = z24.object({
6418
6422
  medianTaskMinutes: z24.number().nonnegative().nullable(),
6419
6423
  medianTaskTokens: z24.number().nonnegative().nullable()
6420
6424
  }).strict();
6425
+ var pacingBasisDoneSchema = pacingBasisSchema.extend({ tasks: z24.array(pacingBasisDoneTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT) }).strict();
6421
6426
  var paceSuggestionSchema = z24.object({
6422
6427
  number: z24.number().int().min(1).max(PACE_AGENTS_MAX),
6423
6428
  minutes: z24.number().int().nonnegative().nullable(),
@@ -9490,12 +9495,15 @@ var CREW_TIER_WORDS = {
9490
9495
  fast: "You work quickly on clear, well-defined work, and say so when a task needs more thought."
9491
9496
  };
9492
9497
  var CREW_REVIEWER_WORDS = "You check the work you are given and never change it: you read it, run what you need to, and report what you find.";
9493
- var CREW_REVIEWER_TOOLS = "Read, Grep, Glob, Bash";
9494
- var CREW_MEMBER_TOOLS = "Read, Edit, Write, Bash, Grep, Glob";
9498
+ var CREW_SHELL_RULE = "Read and search files with your file tools, not the shell. Run one plain shell command per call, on one line: no cd first, and nothing joined to it with &&, ;, a pipe, a loop or a here-document.";
9499
+ var CREW_HALFCYCLE_TOOLS = `mcp__${MCP_SERVER_KEY}__*`;
9500
+ var CREW_REVIEWER_TOOLS = `Read, Grep, Glob, Bash, ${CREW_HALFCYCLE_TOOLS}`;
9501
+ var CREW_MEMBER_TOOLS = `Read, Edit, Write, Bash, Grep, Glob, ${CREW_HALFCYCLE_TOOLS}`;
9495
9502
  function crewProfileBody(role, group, tier) {
9496
9503
  const lines = [`You are a ${CREW_ROLE_WORDS[role]} on this project's build crew.`, CREW_TIER_WORDS[tier]];
9497
9504
  if (group === "reviewer")
9498
9505
  lines.push(CREW_REVIEWER_WORDS);
9506
+ lines.push(CREW_SHELL_RULE);
9499
9507
  return `${lines.join("\n")}
9500
9508
  `;
9501
9509
  }
@@ -10495,8 +10503,8 @@ ${GENERATED_USER_PROMPT_FINDINGS_HEADER} the Halfcycle installer, now also for C
10495
10503
  # whatever Halfcycle's checks found at an earlier turn end, once. Any arguments
10496
10504
  # this script was given (a Codex row passes its agent's name) are handed on.
10497
10505
  #
10498
- # AN ORDINARY PROMPT STARTS NOTHING. With no findings file for this session, this
10499
- # script reads its session id with a bash regex (no node) and exits 0 with no
10506
+ # AN ORDINARY PROMPT STARTS NOTHING. With no findings file and no notes file for
10507
+ # this session, this script reads its session id with a bash regex (no node) and exits 0 with no
10500
10508
  # output once it finds no file \u2014 node and grep never start.
10501
10509
  trap 'exit 0' ERR
10502
10510
  set -e
@@ -10520,7 +10528,8 @@ fi
10520
10528
  HC_FINDINGS_DIR="\${TMPDIR:-/tmp}"
10521
10529
  HC_FINDINGS_DIR="\${HC_FINDINGS_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
10522
10530
  HC_FINDINGS_FILE="\${HC_FINDINGS_DIR}/halfcycle-findings-\${HC_SESSION_ID}.json"
10523
- if [ ! -f "$HC_FINDINGS_FILE" ]; then
10531
+ HC_NOTES_FILE="\${HC_FINDINGS_DIR}/halfcycle-notes-\${HC_SESSION_ID}.json"
10532
+ if [ ! -f "$HC_FINDINGS_FILE" ] && [ ! -f "$HC_NOTES_FILE" ]; then
10524
10533
  exit 0
10525
10534
  fi
10526
10535
 
@@ -11796,8 +11805,8 @@ function installSelfBuild(options) {
11796
11805
  // dist/worker-copies.js
11797
11806
  import { spawnSync } from "node:child_process";
11798
11807
  import { existsSync as existsSync7, lstatSync as lstatSync2, readdirSync, realpathSync, rmdirSync, rmSync as rmSync3 } from "node:fs";
11799
- import { isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve2, sep } from "node:path";
11800
- var WORKER_COPY_CACHE_FOLDERS = ["__pycache__", ".pytest_cache", ".mypy_cache", "node_modules"];
11808
+ import { basename, dirname as dirname5, isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve2, sep } from "node:path";
11809
+ var REMOVE_COPY_USAGE = `${ownCommand()} remove-copy <path>`;
11801
11810
  var COPIES_SHOWN = ".claude/worktrees/";
11802
11811
  var CACHE_NAMES = new Set(WORKER_COPY_CACHE_FOLDERS);
11803
11812
  var NOTHING = { removed: 0, kept: [], message: void 0 };
@@ -11837,27 +11846,18 @@ function tidy(projectDir) {
11837
11846
  const mainPath = canonical(main2.path);
11838
11847
  if (!samePath(mainPath, canonical(top.out.trim())))
11839
11848
  return NOTHING;
11840
- const copiesDir = join9(root, ".claude", "worktrees");
11849
+ const project = { root, mainPath, copiesDir: join9(root, ".claude", "worktrees") };
11841
11850
  let removed = 0;
11842
11851
  const kept = [];
11843
11852
  for (const tree of worktrees.slice(1)) {
11844
11853
  if (tree.bare)
11845
11854
  continue;
11846
11855
  const at = canonical(tree.path);
11847
- if (samePath(at, mainPath) || !isInside(copiesDir, at))
11856
+ const settled = settleCopy(project, tree, at);
11857
+ if (settled === "not-ours" || settled === "gone")
11848
11858
  continue;
11849
- if (!existsSync7(at))
11850
- continue;
11851
- const shown2 = relative2(root, at).split(sep).join("/");
11852
- const held = tree.locked ? "work" : holdsWork(at, mainPath);
11853
- if (held === "work") {
11854
- kept.push(shown2);
11855
- continue;
11856
- }
11857
- clearCaches(at, at);
11858
- const remove = held === "duplicates" ? ["worktree", "remove", "--force", at] : ["worktree", "remove", at];
11859
- if (!git2(root, remove).ok) {
11860
- kept.push(shown2);
11859
+ if (settled !== "removed") {
11860
+ kept.push(shownPath(root, at));
11861
11861
  continue;
11862
11862
  }
11863
11863
  removed += 1;
@@ -11866,6 +11866,128 @@ function tidy(projectDir) {
11866
11866
  }
11867
11867
  return { removed, kept, message: lineFor(removed, kept) };
11868
11868
  }
11869
+ function settleCopy(project, tree, at) {
11870
+ if (samePath(at, project.mainPath) || !isInside(project.copiesDir, at))
11871
+ return "not-ours";
11872
+ if (!existsSync7(at))
11873
+ return "gone";
11874
+ if (tree.locked)
11875
+ return "running";
11876
+ const held = holdsWork(at, project.mainPath);
11877
+ if (held === "work")
11878
+ return "changes";
11879
+ clearCaches(at, at);
11880
+ const remove = held === "duplicates" ? ["worktree", "remove", "--force", at] : ["worktree", "remove", at];
11881
+ return git2(project.root, remove).ok ? "removed" : "changes";
11882
+ }
11883
+ function shownPath(root, at) {
11884
+ return relative2(root, at).split(sep).join("/");
11885
+ }
11886
+ var GitSilent = class extends Error {
11887
+ };
11888
+ var GIT_SILENT = {
11889
+ line: "[halfcycle] The worker copy was not removed, because git did not answer.",
11890
+ exitCode: 1,
11891
+ verdict: false
11892
+ };
11893
+ function removeWorkerCopy(cwd, given) {
11894
+ try {
11895
+ return removeOne(cwd, given);
11896
+ } catch {
11897
+ return GIT_SILENT;
11898
+ }
11899
+ }
11900
+ function removeOne(cwd, given) {
11901
+ const at = canonicalLoose(resolve2(cwd, given));
11902
+ const notACopy = {
11903
+ line: `[halfcycle] ${given} is not a worker copy under ${COPIES_SHOWN}, so nothing was removed.`,
11904
+ exitCode: 1,
11905
+ verdict: true
11906
+ };
11907
+ const top = git2(cwd, ["rev-parse", "--show-toplevel"]);
11908
+ if (!top.ran)
11909
+ throw new GitSilent();
11910
+ if (!top.ok)
11911
+ return notACopy;
11912
+ const listed = git2(cwd, ["worktree", "list", "--porcelain"]);
11913
+ if (!listed.ok)
11914
+ throw new GitSilent();
11915
+ const worktrees = parseWorktrees(listed.out);
11916
+ const main2 = worktrees[0];
11917
+ if (main2 === void 0 || main2.bare)
11918
+ throw new GitSilent();
11919
+ const mainPath = canonical(main2.path);
11920
+ if (!samePath(mainPath, canonical(top.out.trim()))) {
11921
+ return {
11922
+ line: `[halfcycle] Run this from the project's main folder, not from inside a worker copy. Nothing was removed.`,
11923
+ exitCode: 1,
11924
+ verdict: true
11925
+ };
11926
+ }
11927
+ const project = { root: mainPath, mainPath, copiesDir: join9(mainPath, ".claude", "worktrees") };
11928
+ if (!samePath(dirname5(at), project.copiesDir) || samePath(at, mainPath))
11929
+ return notACopy;
11930
+ const shown2 = shownPath(mainPath, at);
11931
+ const folder2 = basename(at);
11932
+ const tree = worktrees.slice(1).find((entry) => !entry.bare && samePath(canonicalLoose(entry.path), at));
11933
+ const done = (line) => ({ line, exitCode: 0, verdict: false });
11934
+ const kept = (why) => done(why === "running" ? `[halfcycle] Kept the worker copy ${shown2}: an agent is still working in it. Nothing was removed.` : `[halfcycle] Kept the worker copy ${shown2}: it holds changes that are not saved anywhere else. Nothing was removed.`);
11935
+ const goneLine = `[halfcycle] The worker copy ${shown2} is already gone.`;
11936
+ if (tree === void 0) {
11937
+ if (entryExists(at))
11938
+ return notACopy;
11939
+ return done(withBranch(goneLine, deleteCopyBranch(mainPath, folder2), false));
11940
+ }
11941
+ const settled = settleCopy(project, tree, at);
11942
+ if (settled === "not-ours")
11943
+ return notACopy;
11944
+ if (settled === "running" || settled === "changes")
11945
+ return kept(settled);
11946
+ if (settled === "removed")
11947
+ return done(withBranch(`[halfcycle] Removed the worker copy ${shown2}.`, deleteCopyBranch(mainPath, folder2), true));
11948
+ if (tree.locked)
11949
+ return kept("running");
11950
+ if (tree.detached && (tree.head === void 0 || !onSomeRef(mainPath, tree.head)))
11951
+ return kept("changes");
11952
+ const unlisted = git2(mainPath, ["worktree", "remove", tree.path]);
11953
+ if (!unlisted.ran)
11954
+ throw new GitSilent();
11955
+ if (!unlisted.ok)
11956
+ return kept("changes");
11957
+ return done(withBranch(goneLine, deleteCopyBranch(mainPath, folder2), false));
11958
+ }
11959
+ function deleteCopyBranch(root, folder2) {
11960
+ const name = `worktree-${folder2}`;
11961
+ if (!git2(root, ["show-ref", "--verify", "--quiet", `refs/heads/${name}`]).ok)
11962
+ return { name, outcome: "none" };
11963
+ return { name, outcome: git2(root, ["branch", "-d", name]).ok ? "deleted" : "kept" };
11964
+ }
11965
+ function withBranch(line, branch, namesDeleted) {
11966
+ if (branch.outcome === "none")
11967
+ return line;
11968
+ if (branch.outcome === "deleted")
11969
+ return namesDeleted ? `${line.slice(0, -1)} and its branch ${branch.name}.` : line;
11970
+ return `${line} Kept its branch ${branch.name}, which holds commits that are not merged.`;
11971
+ }
11972
+ function entryExists(path) {
11973
+ try {
11974
+ lstatSync2(path);
11975
+ return true;
11976
+ } catch {
11977
+ return false;
11978
+ }
11979
+ }
11980
+ function canonicalLoose(path) {
11981
+ const full = resolve2(path);
11982
+ try {
11983
+ return realpathSync(full);
11984
+ } catch {
11985
+ const parent = dirname5(full);
11986
+ if (parent === full)
11987
+ return full;
11988
+ return join9(canonicalLoose(parent), basename(full));
11989
+ }
11990
+ }
11869
11991
  function lineFor(removed, kept) {
11870
11992
  if (removed === 0 && kept.length === 0)
11871
11993
  return void 0;
@@ -11890,10 +12012,14 @@ function parseWorktrees(porcelain) {
11890
12012
  let current;
11891
12013
  for (const line of porcelain.split(/\r?\n/)) {
11892
12014
  if (line.startsWith("worktree ")) {
11893
- current = { path: line.slice("worktree ".length), branch: void 0, locked: false, bare: false };
12015
+ current = { path: line.slice("worktree ".length), head: void 0, branch: void 0, detached: false, locked: false, bare: false };
11894
12016
  trees.push(current);
11895
12017
  } else if (current === void 0) {
11896
12018
  continue;
12019
+ } else if (line.startsWith("HEAD ")) {
12020
+ current.head = line.slice("HEAD ".length);
12021
+ } else if (line === "detached") {
12022
+ current.detached = true;
11897
12023
  } else if (line.startsWith("branch refs/heads/")) {
11898
12024
  current.branch = line.slice("branch refs/heads/".length);
11899
12025
  } else if (line === "locked" || line.startsWith("locked ")) {
@@ -11905,6 +12031,10 @@ function parseWorktrees(porcelain) {
11905
12031
  return trees;
11906
12032
  }
11907
12033
  function holdsWork(copyDir, mainDir) {
12034
+ if (operationInProgress(copyDir))
12035
+ return "work";
12036
+ if (!onSomeRef(copyDir, "HEAD"))
12037
+ return "work";
11908
12038
  const status = git2(copyDir, ["status", "--porcelain", "-z", "--untracked-files=all"]);
11909
12039
  if (!status.ok)
11910
12040
  return "work";
@@ -11926,6 +12056,20 @@ function holdsWork(copyDir, mainDir) {
11926
12056
  }
11927
12057
  return duplicates ? "duplicates" : "clean";
11928
12058
  }
12059
+ var IN_PROGRESS_MARKS = ["rebase-merge", "rebase-apply", "MERGE_HEAD", "CHERRY_PICK_HEAD"];
12060
+ function operationInProgress(copyDir) {
12061
+ const paths = git2(copyDir, ["rev-parse", ...IN_PROGRESS_MARKS.flatMap((mark) => ["--git-path", mark])]);
12062
+ if (!paths.ok)
12063
+ return true;
12064
+ const lines = paths.out.split(/\r?\n/).filter((line) => line !== "");
12065
+ if (lines.length !== IN_PROGRESS_MARKS.length)
12066
+ return true;
12067
+ return lines.some((line) => entryExists(resolve2(copyDir, line)));
12068
+ }
12069
+ function onSomeRef(cwd, commit) {
12070
+ const holding = git2(cwd, ["for-each-ref", "--count=1", "--format=%(refname)", "--contains", commit]);
12071
+ return holding.ok && holding.out.trim() !== "";
12072
+ }
11929
12073
  function duplicatesMain(copyDir, mainDir, path) {
11930
12074
  if (path === "" || path.endsWith("/"))
11931
12075
  return false;
@@ -13421,7 +13565,7 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
13421
13565
  }
13422
13566
 
13423
13567
  // dist/project-name.js
13424
- import { basename, resolve as resolve3 } from "node:path";
13568
+ import { basename as basename2, resolve as resolve3 } from "node:path";
13425
13569
  var MAX_DERIVED_NAME_LEN = 100;
13426
13570
  var EMPTY_SEGMENTS = /* @__PURE__ */ new Set(["", ".", "..", "/", "\\"]);
13427
13571
  function repositoryNameFromRemote(remote) {
@@ -13441,7 +13585,7 @@ function isLocalRemote(remote) {
13441
13585
  function deriveProjectName(targetRepoRoot) {
13442
13586
  const remote = readRemote(targetRepoRoot);
13443
13587
  const fromRemote = remote && !isLocalRemote(remote) ? repositoryNameFromRemote(remote) : void 0;
13444
- const candidate = fromRemote ?? basename(resolve3(targetRepoRoot));
13588
+ const candidate = fromRemote ?? basename2(resolve3(targetRepoRoot));
13445
13589
  const name = candidate.trim();
13446
13590
  if (EMPTY_SEGMENTS.has(name))
13447
13591
  return void 0;
@@ -13532,7 +13676,7 @@ async function joinPinnedEngagement(serviceUrl, engagementId, targetRepo, deps =
13532
13676
  // dist/pre-create-checks.js
13533
13677
  import { spawnSync as spawnSync2 } from "node:child_process";
13534
13678
  import { existsSync as existsSync8, readFileSync as readFileSync9, realpathSync as realpathSync3 } from "node:fs";
13535
- import { delimiter, dirname as dirname5, join as join11 } from "node:path";
13679
+ import { delimiter, dirname as dirname6, join as join11 } from "node:path";
13536
13680
  var KEPT_FOLDER_ID_LINE = `Nothing on the Halfcycle service needs undoing, and this folder keeps its project id, so running "${ownBareCommand()}" again picks up where it left off.`;
13537
13681
  var PreCreateRefused = class extends Error {
13538
13682
  constructor(message) {
@@ -13574,7 +13718,7 @@ function refuseSystemFolder(targetRepo, platform6 = process.platform) {
13574
13718
  }
13575
13719
  function refuseInsideProject(targetRepo) {
13576
13720
  let folder2 = realOrLiteral(targetRepo);
13577
- for (let parent = dirname5(folder2); parent !== folder2; folder2 = parent, parent = dirname5(parent)) {
13721
+ for (let parent = dirname6(folder2); parent !== folder2; folder2 = parent, parent = dirname6(parent)) {
13578
13722
  if (existsSync8(join11(parent, BUNDLE_PIN_REL))) {
13579
13723
  throw new PreCreateRefused(`${targetRepo} is inside the Halfcycle project at ${parent} \u2014 open that folder in your coding agent instead, or run "${ownBareCommand()}" in a folder outside it.`);
13580
13724
  }
@@ -13702,7 +13846,7 @@ function isSelfBuildInstallForm(engagementIdArg, engagementTypeArg) {
13702
13846
  // dist/local-allow.js
13703
13847
  import { execFileSync as execFileSync3 } from "node:child_process";
13704
13848
  import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync11, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
13705
- import { dirname as dirname6, join as join13 } from "node:path";
13849
+ import { dirname as dirname7, join as join13 } from "node:path";
13706
13850
  var LOCAL_SETTINGS_REL2 = ".claude/settings.local.json";
13707
13851
  var LOCAL_ALLOW_RECORD_REL = ".halfcycle/local-allow.json";
13708
13852
  var LOCAL_ALLOW_EDIT_MODE = "acceptEdits";
@@ -13758,6 +13902,7 @@ var LOCAL_ALLOW_RULES = [
13758
13902
  bash("bun add"),
13759
13903
  bash("gh repo view"),
13760
13904
  bash("gh issue create"),
13905
+ bash("gh pr merge"),
13761
13906
  bash("go mod tidy"),
13762
13907
  bash("go get"),
13763
13908
  bash("cargo add"),
@@ -13797,7 +13942,7 @@ var LOCAL_ALLOW_RULES = [
13797
13942
  bash("tr"),
13798
13943
  HALFCYCLE_TOOLS_RULE
13799
13944
  ];
13800
- var LOCAL_ALLOW_QUESTION = "Let your coding agent work without asking on this computer? It would run your tests and builds, install packages, run local tools, make and move files, use git locally, run Halfcycle, and edit files in this project without a permission prompt. Opening a ticket on GitHub does not ask either. Pushing, publishing, deploying to production and deleting still ask. This is saved only on this computer, in .claude/settings.local.json, which is not committed.\nType yes to allow it, or press Enter to keep asking: ";
13945
+ var LOCAL_ALLOW_QUESTION = "Let your coding agent work without asking on this computer? It would run your tests and builds, install packages, run local tools, make and move files, use git locally, run Halfcycle, and edit files in this project without a permission prompt. Opening a ticket and merging reviewed work on GitHub do not ask either. Pushing, publishing, deploying to production and deleting still ask. This is saved only on this computer, in .claude/settings.local.json, which is not committed.\nType yes to allow it, or press Enter to keep asking: ";
13801
13946
  var LOCAL_ALLOW_ON_LINE = `Your coding agent can now work without asking on this computer. To undo it, run: ${ownCommand()} allow --remove`;
13802
13947
  var LOCAL_ALLOW_OFF_LINE = `Your coding agent will ask before it runs commands. To stop most of those questions on this computer, run: ${ownCommand()} allow`;
13803
13948
  var LOCAL_ALLOW_TRACKED_LINE = "Your .claude/settings.local.json is committed to git, so Halfcycle will not add rules to it. Remove it from git first if you want them.";
@@ -13837,7 +13982,7 @@ function readRecord2(repo) {
13837
13982
  }
13838
13983
  function writeRecord(repo, record2) {
13839
13984
  const path = join13(repo, LOCAL_ALLOW_RECORD_REL);
13840
- mkdirSync7(dirname6(path), { recursive: true });
13985
+ mkdirSync7(dirname7(path), { recursive: true });
13841
13986
  writeFileSync7(path, JSON.stringify({ rules: record2.rules, defaultMode: record2.defaultMode, before: record2.before }, null, 2) + "\n");
13842
13987
  }
13843
13988
  function holdsLocalAllowRecord(repo) {
@@ -13888,7 +14033,7 @@ function writeLocalAllow(repo) {
13888
14033
  if (setsMode)
13889
14034
  permissions["defaultMode"] = LOCAL_ALLOW_EDIT_MODE;
13890
14035
  const next = { ...settings, permissions };
13891
- mkdirSync7(dirname6(path), { recursive: true });
14036
+ mkdirSync7(dirname7(path), { recursive: true });
13892
14037
  writeFileSync7(path, render(next, text));
13893
14038
  const earlier = readRecord2(repo);
13894
14039
  writeRecord(repo, {
@@ -14009,7 +14154,7 @@ function approveOwnServer(repo) {
14009
14154
  const plan = planOwnServerApproval(repo, registersOwnServer(repo));
14010
14155
  if (plan.after !== null) {
14011
14156
  const path = join13(repo, LOCAL_SETTINGS_REL2);
14012
- mkdirSync7(dirname6(path), { recursive: true });
14157
+ mkdirSync7(dirname7(path), { recursive: true });
14013
14158
  writeFileSync7(path, plan.after);
14014
14159
  }
14015
14160
  return plan.outcome;
@@ -14754,7 +14899,7 @@ function codexInstallLine(codex) {
14754
14899
 
14755
14900
  // dist/build-record/write.js
14756
14901
  import { existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync as rmSync7 } from "node:fs";
14757
- import { basename as basename2, join as join17, resolve as resolve4, relative as relative3, isAbsolute as isAbsolute4 } from "node:path";
14902
+ import { basename as basename3, join as join17, resolve as resolve4, relative as relative3, isAbsolute as isAbsolute4 } from "node:path";
14758
14903
 
14759
14904
  // dist/build-record/render-v2.js
14760
14905
  var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
@@ -15426,7 +15571,7 @@ function replaceOne(io, tmp, finalPath, text) {
15426
15571
  var BUILD_RECORD_DIR = join17("docs", "build-records");
15427
15572
  function assertInsideOutDir(outDir, candidate, identity) {
15428
15573
  const rel = relative3(outDir, resolve4(candidate));
15429
- const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute4(rel) && rel === basename2(rel);
15574
+ const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute4(rel) && rel === basename3(rel);
15430
15575
  if (!isPlainFilename) {
15431
15576
  throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
15432
15577
  }
@@ -15517,7 +15662,7 @@ import { join as join23 } from "node:path";
15517
15662
 
15518
15663
  // dist/draw/write.js
15519
15664
  import { closeSync as closeSync2, lstatSync as lstatSync4, mkdirSync as mkdirSync9, openSync as openSync2, readFileSync as readFileSync15, readSync, realpathSync as realpathSync7, rmSync as rmSync8 } from "node:fs";
15520
- import { dirname as dirname7, isAbsolute as isAbsolute5, join as join18, relative as relative4, resolve as resolve5, sep as sep2 } from "node:path";
15665
+ import { dirname as dirname8, isAbsolute as isAbsolute5, join as join18, relative as relative4, resolve as resolve5, sep as sep2 } from "node:path";
15521
15666
  var DOCS_DIR = "docs";
15522
15667
  var DIAGRAMS_DIR = "diagrams";
15523
15668
  var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
@@ -15610,7 +15755,7 @@ function checkDiagramPlace(paths) {
15610
15755
  }
15611
15756
  let ancestor = paths.dir;
15612
15757
  while (lstatOrNull2(ancestor) === null) {
15613
- const parent = dirname7(ancestor);
15758
+ const parent = dirname8(ancestor);
15614
15759
  if (parent === ancestor)
15615
15760
  break;
15616
15761
  ancestor = parent;
@@ -15851,7 +15996,7 @@ function isMissingRoute(method, status, body) {
15851
15996
  import { join as join21 } from "node:path";
15852
15997
 
15853
15998
  // dist/cli-contract.js
15854
- 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", "allow", "size", "regression", "crew"];
15999
+ 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", "allow", "size", "regression", "crew", "remove-copy"];
15855
16000
  var CONTRACTS = [
15856
16001
  { verb: "install", required: [], conditional: [] },
15857
16002
  { verb: "check-drift", required: [], conditional: [] },
@@ -15871,6 +16016,7 @@ var CONTRACTS = [
15871
16016
  { verb: "doctor", required: [], conditional: [] },
15872
16017
  { verb: "restore", required: [], conditional: [] },
15873
16018
  { verb: "allow", required: [], conditional: [] },
16019
+ { verb: "remove-copy", required: [], conditional: [] },
15874
16020
  {
15875
16021
  verb: "request-shapes",
15876
16022
  required: [],
@@ -16322,7 +16468,7 @@ async function runSystem(repoRoot, deps) {
16322
16468
 
16323
16469
  // dist/build-record/write-change.js
16324
16470
  import { existsSync as existsSync14, mkdirSync as mkdirSync10 } from "node:fs";
16325
- import { basename as basename3, isAbsolute as isAbsolute6, join as join20, relative as relative5, resolve as resolve6 } from "node:path";
16471
+ import { basename as basename4, isAbsolute as isAbsolute6, join as join20, relative as relative5, resolve as resolve6 } from "node:path";
16326
16472
 
16327
16473
  // dist/build-record/render-change.js
16328
16474
  function counted2(n, one, many) {
@@ -16478,7 +16624,7 @@ var InvalidChangeIdError = class extends Error {
16478
16624
  };
16479
16625
  function assertInsideChangeDir(outDir, candidate, id) {
16480
16626
  const rel = relative5(outDir, resolve6(candidate));
16481
- const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute6(rel) && rel === basename3(rel);
16627
+ const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute6(rel) && rel === basename4(rel);
16482
16628
  if (!isPlainFilename)
16483
16629
  throw new InvalidChangeIdError(id);
16484
16630
  }
@@ -19568,7 +19714,7 @@ function runProveChild(binary, credential, repoRoot, words, deps = {}) {
19568
19714
  // dist/uninstall.js
19569
19715
  import { existsSync as existsSync17, lstatSync as lstatSync5, readFileSync as readFileSync23, readdirSync as readdirSync2, rmSync as rmSync9, rmdirSync as rmdirSync2, writeFileSync as writeFileSync9 } from "node:fs";
19570
19716
  import { homedir as homedir7 } from "node:os";
19571
- import { dirname as dirname8, join as join28, relative as relative7, sep as sep3 } from "node:path";
19717
+ import { dirname as dirname9, join as join28, relative as relative7, sep as sep3 } from "node:path";
19572
19718
  var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
19573
19719
  function parseUninstallArgs(args2) {
19574
19720
  let removeCredential = false;
@@ -19669,7 +19815,7 @@ function validate(options, rules) {
19669
19815
  }
19670
19816
  if (engagementId !== void 0) {
19671
19817
  const expectedParent = join28(home ?? homedir7(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
19672
- if (dirname8(engagementStateDir(engagementId, home)) !== expectedParent) {
19818
+ if (dirname9(engagementStateDir(engagementId, home)) !== expectedParent) {
19673
19819
  throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
19674
19820
  }
19675
19821
  }
@@ -20452,7 +20598,7 @@ function filesUnder(root, rel) {
20452
20598
  walk(rel);
20453
20599
  return out;
20454
20600
  }
20455
- function shownPath(path, home) {
20601
+ function shownPath2(path, home) {
20456
20602
  const base = home ?? homedir7();
20457
20603
  const rel = relative7(base, path);
20458
20604
  const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep3);
@@ -20483,7 +20629,7 @@ function renderUninstallReport(result, home) {
20483
20629
  const c = result.credential;
20484
20630
  switch (c.kind) {
20485
20631
  case "still-held":
20486
- out.push(`[halfcycle] This machine still holds this project's credential, at ${shownPath(c.dir, home)}.`);
20632
+ out.push(`[halfcycle] This machine still holds this project's credential, at ${shownPath2(c.dir, home)}.`);
20487
20633
  out.push(" Every checkout and worktree of this project on this machine uses it.");
20488
20634
  out.push(` To remove it too: ${ownCommand()} uninstall ${REMOVE_CREDENTIAL_FLAG}`);
20489
20635
  break;
@@ -20491,18 +20637,18 @@ function renderUninstallReport(result, home) {
20491
20637
  out.push(`[halfcycle] This machine may still hold a credential for this project, but the project id in ${BUNDLE_PIN_REL} is not valid, so it is not shown as a path.`);
20492
20638
  break;
20493
20639
  case "removed":
20494
- out.push(`[halfcycle] Removed this machine's credential for this project, at ${shownPath(c.dir, home)}.`);
20640
+ out.push(`[halfcycle] Removed this machine's credential for this project, at ${shownPath2(c.dir, home)}.`);
20495
20641
  out.push(" Every other checkout and worktree of this project on this machine used it too. Each stops reaching");
20496
20642
  out.push(` Halfcycle until \`${ownCommand()}\` is run there again.`);
20497
20643
  break;
20498
20644
  case "not-held":
20499
- out.push(`[halfcycle] This machine holds no credential for this project (nothing at ${shownPath(c.dir, home)}).`);
20645
+ out.push(`[halfcycle] This machine holds no credential for this project (nothing at ${shownPath2(c.dir, home)}).`);
20500
20646
  break;
20501
20647
  case "no-project-id":
20502
20648
  out.push(`[halfcycle] No credential was removed: ${BUNDLE_PIN_REL} is missing, so there is no project id to find it by.`);
20503
20649
  break;
20504
20650
  case "failed":
20505
- out.push(`[halfcycle] Could not remove this machine's credential for this project, at ${shownPath(c.dir, home)}: ${c.reason}. Run "${ownCommand()} uninstall ${REMOVE_CREDENTIAL_FLAG}" again to finish.`);
20651
+ out.push(`[halfcycle] Could not remove this machine's credential for this project, at ${shownPath2(c.dir, home)}: ${c.reason}. Run "${ownCommand()} uninstall ${REMOVE_CREDENTIAL_FLAG}" again to finish.`);
20506
20652
  break;
20507
20653
  case "nothing-to-say":
20508
20654
  break;
@@ -20512,7 +20658,7 @@ function renderUninstallReport(result, home) {
20512
20658
  }
20513
20659
 
20514
20660
  // dist/banner-facts.js
20515
- import { basename as basename4, resolve as resolve8 } from "node:path";
20661
+ import { basename as basename5, resolve as resolve8 } from "node:path";
20516
20662
  var BRAND_URL = "halfcycle.ai";
20517
20663
  var TAGLINE = [
20518
20664
  "Fewer cycles.",
@@ -20632,7 +20778,7 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
20632
20778
  }
20633
20779
  function safeBasename(dir) {
20634
20780
  try {
20635
- const name = basename4(resolve8(dir)).trim();
20781
+ const name = basename5(resolve8(dir)).trim();
20636
20782
  return name === "" ? "project" : name;
20637
20783
  } catch {
20638
20784
  return "project";
@@ -21910,6 +22056,9 @@ var USAGE = ` ${ownCommand()} [install] [target-repo] [engagement-id] [self-bui
21910
22056
  let your coding agent work without asking on this computer, for tests, builds, installs, local
21911
22057
  tools, files and local git; pushing, publishing, deploying, deleting and GitHub still ask;
21912
22058
  --remove takes out what this added and keeps everything of yours
22059
+ ${REMOVE_COPY_USAGE}
22060
+ remove one worker copy under .claude/worktrees/ and its worktree- branch, from the project's
22061
+ main folder; a copy that may hold work is kept
21913
22062
  ${ownCommand()} uninstall [${REMOVE_CREDENTIAL_FLAG}]
21914
22063
  take out what the installer added to this project, keeping anything you changed
21915
22064
  ${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
@@ -22089,6 +22238,22 @@ ${USAGE}`);
22089
22238
  process.exitCode = refused3 ? 1 : 0;
22090
22239
  return;
22091
22240
  }
22241
+ if (cmd === "remove-copy") {
22242
+ const given = rest[0];
22243
+ if (rest.length !== 1 || given === void 0 || given === "" || given.startsWith("-")) {
22244
+ process.stderr.write(`halfcycle remove-copy: usage: ${REMOVE_COPY_USAGE}
22245
+ `);
22246
+ process.exitCode = 2;
22247
+ return;
22248
+ }
22249
+ const done = removeWorkerCopy(process.cwd(), given);
22250
+ if (done.verdict)
22251
+ exitIsAVerdict();
22252
+ (done.exitCode === 0 ? process.stdout : process.stderr).write(`${done.line}
22253
+ `);
22254
+ process.exitCode = done.exitCode;
22255
+ return;
22256
+ }
22092
22257
  if (installArm) {
22093
22258
  const engagementIdArg = positionals[1];
22094
22259
  const engagementTypeRaw = positionals[2] ?? "client";