halfcycle 0.4.0 → 0.4.2

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/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // dist/install.js
2
2
  import { execFileSync as execFileSync2 } from "node:child_process";
3
3
  import { createHash as createHash2 } from "node:crypto";
4
- import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
5
- import { dirname as dirname2, join as join5, posix, relative } from "node:path";
4
+ import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
5
+ import { dirname as dirname3, join as join5, posix, relative } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  // ../core/dist/guard-record.js
@@ -120,58 +120,70 @@ var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
120
120
  var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
121
121
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
122
122
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
123
+ function hookCommand(script, verb) {
124
+ const invocation = `bash "$CLAUDE_PROJECT_DIR"/${script}`;
125
+ return verb === void 0 ? invocation : `${invocation} ${verb}`;
126
+ }
123
127
  var HALFCYCLE_HOOK_WIRING = [
124
128
  {
125
129
  event: "SessionStart",
126
130
  matcher: null,
127
131
  timeout: 5,
128
- command: `bash ./${SESSION_START_MARKER_REL}`,
132
+ command: hookCommand(SESSION_START_MARKER_REL),
129
133
  script: SESSION_START_MARKER_REL
130
134
  },
131
135
  {
132
136
  event: "SessionStart",
133
137
  matcher: null,
134
138
  timeout: 10,
135
- command: `bash ./${GUARD_RUNNER_REL} refresh-credential`,
139
+ command: hookCommand(GUARD_RUNNER_REL, "refresh-credential"),
136
140
  script: GUARD_RUNNER_REL
137
141
  },
138
142
  {
139
143
  event: "UserPromptSubmit",
140
144
  matcher: null,
141
145
  timeout: 5,
142
- command: `bash ./${USER_PROMPT_REMINDER_REL}`,
146
+ command: hookCommand(USER_PROMPT_REMINDER_REL),
143
147
  script: USER_PROMPT_REMINDER_REL
144
148
  },
145
149
  {
146
150
  event: "PostToolUse",
147
151
  matcher: WRITE_CAPABLE_TOOLS.join("|"),
148
152
  timeout: 60,
149
- command: `bash ./${GUARD_RUNNER_REL} hook post-tool-use`,
153
+ command: hookCommand(GUARD_RUNNER_REL, "hook post-tool-use"),
150
154
  script: GUARD_RUNNER_REL
151
155
  },
152
156
  {
153
157
  event: "Stop",
154
158
  matcher: null,
155
159
  timeout: 60,
156
- command: `bash ./${GUARD_RUNNER_REL} hook stop`,
160
+ command: hookCommand(GUARD_RUNNER_REL, "hook stop"),
157
161
  script: GUARD_RUNNER_REL
158
162
  },
159
163
  {
160
164
  event: "SubagentStop",
161
165
  matcher: null,
162
166
  timeout: 60,
163
- command: `bash ./${GUARD_RUNNER_REL} hook subagent-stop`,
167
+ command: hookCommand(GUARD_RUNNER_REL, "hook subagent-stop"),
164
168
  script: GUARD_RUNNER_REL
165
169
  },
166
170
  {
167
171
  event: "SessionEnd",
168
172
  matcher: null,
169
173
  timeout: 5,
170
- command: `bash ./${SESSION_END_MARKER_REL}`,
174
+ command: hookCommand(SESSION_END_MARKER_REL),
171
175
  script: SESSION_END_MARKER_REL
172
176
  }
173
177
  ];
174
- var RETIRED_HOOK_COMMANDS = [];
178
+ var RETIRED_HOOK_COMMANDS = [
179
+ "bash ./.claude/hooks/session-start-marker.sh",
180
+ "bash ./.claude/hooks/guard-runner.sh refresh-credential",
181
+ "bash ./.claude/hooks/user-prompt-reminder.sh",
182
+ "bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
183
+ "bash ./.claude/hooks/guard-runner.sh hook stop",
184
+ "bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
185
+ "bash ./.claude/hooks/session-end-marker.sh"
186
+ ];
175
187
  var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
176
188
  ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
177
189
  ...RETIRED_HOOK_COMMANDS
@@ -1217,10 +1229,11 @@ halfcycle_env_file() {
1217
1229
 
1218
1230
  // dist/identity.js
1219
1231
  import { execFileSync } from "node:child_process";
1220
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1221
- import { isAbsolute, join as join3 } from "node:path";
1232
+ import { existsSync, linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
1233
+ import { dirname as dirname2, isAbsolute, join as join3 } from "node:path";
1222
1234
  import { randomUUID } from "node:crypto";
1223
1235
  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.';
1236
+ var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
1224
1237
  var REPOSITORY_LOCATING_GIT_VARS = [
1225
1238
  "GIT_ALTERNATE_OBJECT_DIRECTORIES",
1226
1239
  "GIT_COMMON_DIR",
@@ -1301,21 +1314,45 @@ function mintOrReadIdentity(targetRepoRoot) {
1301
1314
  const path = join3(targetRepoRoot, ".halfcycle", "project.json");
1302
1315
  if (existsSync(path)) {
1303
1316
  const onDisk = JSON.parse(readFileSync2(path, "utf-8"));
1304
- const identity2 = {
1317
+ const identity = {
1305
1318
  projectId: onDisk.projectId ?? "",
1306
1319
  rootCommit: onDisk.rootCommit ?? null,
1307
1320
  remote: onDisk.remote ?? null,
1308
1321
  note: PROJECT_IDENTITY_NOTE
1309
1322
  };
1310
- return { identity: identity2, minted: false };
1323
+ return { identity, minted: false };
1311
1324
  }
1312
- const identity = {
1325
+ return { identity: mintIdentity(targetRepoRoot), minted: true };
1326
+ }
1327
+ function mintIdentity(targetRepoRoot) {
1328
+ return {
1313
1329
  projectId: randomUUID(),
1314
1330
  rootCommit: readRootCommit(targetRepoRoot),
1315
1331
  remote: readRemote(targetRepoRoot),
1316
1332
  note: PROJECT_IDENTITY_NOTE
1317
1333
  };
1318
- return { identity, minted: true };
1334
+ }
1335
+ function serializeProjectIdentity(identity) {
1336
+ return JSON.stringify(identity, null, 2) + "\n";
1337
+ }
1338
+ function createProjectIdentityFile(targetRepoRoot) {
1339
+ const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
1340
+ mkdirSync2(dirname2(path), { recursive: true });
1341
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
1342
+ try {
1343
+ writeFileSync2(temporary, serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
1344
+ encoding: "utf-8",
1345
+ flag: "wx"
1346
+ });
1347
+ try {
1348
+ linkSync(temporary, path);
1349
+ } catch (err) {
1350
+ if (err.code !== "EEXIST")
1351
+ throw err;
1352
+ }
1353
+ } finally {
1354
+ rmSync(temporary, { force: true });
1355
+ }
1319
1356
  }
1320
1357
 
1321
1358
  // dist/install-manifest.js
@@ -1600,6 +1637,13 @@ function mcpEndpointUrl(origin) {
1600
1637
  return `${origin.trim().replace(/\/+$/, "")}${MCP_ENDPOINT_PATH}`;
1601
1638
  }
1602
1639
 
1640
+ // dist/node-floor.js
1641
+ var NODE_FLOOR_MAJOR = 18;
1642
+ var NODE_FLOOR_SENTENCE_HEAD = `Halfcycle needs Node ${NODE_FLOOR_MAJOR} or newer; you have `;
1643
+ function nodeFloorProgram(guarded) {
1644
+ return `if(+process.versions.node.split('.')[0]<${NODE_FLOOR_MAJOR}){console.error('${NODE_FLOOR_SENTENCE_HEAD}'+process.versions.node);process.exitCode=1}else{${guarded}}`;
1645
+ }
1646
+
1603
1647
  // dist/project-page.js
1604
1648
  function projectPageUrl(engagementId) {
1605
1649
  return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
@@ -1615,7 +1659,7 @@ function mergeSettings(existing, generated) {
1615
1659
  merged.$schema = generated.$schema;
1616
1660
  }
1617
1661
  const generatedHooks = generated.hooks ?? {};
1618
- const halfcycleCommands = /* @__PURE__ */ new Set();
1662
+ const halfcycleCommands = new Set(HALFCYCLE_HOOK_COMMANDS);
1619
1663
  for (const entries of Object.values(generatedHooks)) {
1620
1664
  for (const entry of entries) {
1621
1665
  for (const h of entry.hooks)
@@ -1648,7 +1692,7 @@ function mergePermissions(existing, generated) {
1648
1692
  }
1649
1693
 
1650
1694
  // dist/scan.js
1651
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
1695
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
1652
1696
  import { join as join4 } from "node:path";
1653
1697
  var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
1654
1698
  var HALFCYCLE_STATE_NOTE = [
@@ -1692,8 +1736,8 @@ function runBootstrapScan(targetRepo) {
1692
1736
  return { state: existing, ran: false };
1693
1737
  }
1694
1738
  const state = scanLayers(targetRepo);
1695
- mkdirSync2(join4(targetRepo, ".halfcycle"), { recursive: true });
1696
- writeFileSync2(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
1739
+ mkdirSync3(join4(targetRepo, ".halfcycle"), { recursive: true });
1740
+ writeFileSync3(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
1697
1741
  return { state, ran: true };
1698
1742
  }
1699
1743
  function mergeWithRecords(state, layersWithRealRecords) {
@@ -1715,7 +1759,7 @@ function mergeWithRecords(state, layersWithRealRecords) {
1715
1759
  // dist/install.js
1716
1760
  var ZONE_B_DIR = ".workbench";
1717
1761
  var __filename = fileURLToPath(import.meta.url);
1718
- var __dirname = dirname2(__filename);
1762
+ var __dirname = dirname3(__filename);
1719
1763
  function findBundleRoot(startDir) {
1720
1764
  let candidate = startDir;
1721
1765
  for (let i = 0; i < 4; i++) {
@@ -1725,7 +1769,7 @@ function findBundleRoot(startDir) {
1725
1769
  return candidate;
1726
1770
  } catch {
1727
1771
  }
1728
- candidate = dirname2(candidate);
1772
+ candidate = dirname3(candidate);
1729
1773
  }
1730
1774
  return join5(startDir, "..");
1731
1775
  }
@@ -1739,7 +1783,7 @@ function resolveVendoredBinary() {
1739
1783
  if (existsSync3(candidate))
1740
1784
  return candidate;
1741
1785
  }
1742
- throw new Error(`[bundle install] 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(", ")}.`);
1786
+ 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(", ")}.`);
1743
1787
  }
1744
1788
  var WRITE_ALLOWLIST = [
1745
1789
  ".claude/commands",
@@ -1761,9 +1805,11 @@ function isAllowlisted(targetRelPath) {
1761
1805
  return normalised === p || normalised.startsWith(p + "/");
1762
1806
  });
1763
1807
  }
1764
- var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
1765
1808
  var CREW_ROSTER_REL = ".halfcycle/crew.json";
1766
1809
  var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
1810
+ function capturedIndexScaffold() {
1811
+ return join5(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
1812
+ }
1767
1813
  var GITIGNORE_REL = ".gitignore";
1768
1814
  var GITATTRIBUTES_REL = ".gitattributes";
1769
1815
  function commandStubRel(name) {
@@ -1773,8 +1819,9 @@ var RETIRED_MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
1773
1819
  var RETIRED_WRITE_SET_FILES = [RETIRED_MCP_HEADERS_HELPER_REL];
1774
1820
  var RETIRED_DENY_PATTERNS = [];
1775
1821
  function closedWriteSet() {
1822
+ const commands = readPluginManifest().commands ?? [];
1776
1823
  const files = /* @__PURE__ */ new Set([
1777
- ...(readPluginManifest().commands ?? []).map((cmd) => commandStubRel(cmd.name)),
1824
+ ...commands.map((cmd) => commandStubRel(cmd.name)),
1778
1825
  ...Object.keys(OWNED_GENERATED_HEADERS),
1779
1826
  VENDORED_BIN_REL,
1780
1827
  CREW_ROSTER_REL,
@@ -1803,8 +1850,27 @@ function closedWriteSet() {
1803
1850
  gitattributesLines: /* @__PURE__ */ new Set([
1804
1851
  GITATTRIBUTES_HEADER,
1805
1852
  ...lfRules([...Object.keys(OWNED_GENERATED_HEADERS), ...RETIRED_WRITE_SET_FILES])
1806
- ])
1853
+ ]),
1854
+ generatedSha256: sha256OfWhatThisVersionWrites(commands)
1855
+ };
1856
+ }
1857
+ function sha256OfWhatThisVersionWrites(commands) {
1858
+ const out = /* @__PURE__ */ new Map();
1859
+ const fromFile = (rel, src) => {
1860
+ if (existsSync3(src))
1861
+ out.set(rel, sha256Hex(readFileSync4(src)));
1807
1862
  };
1863
+ for (const cmd of commands)
1864
+ fromFile(commandStubRel(cmd.name), join5(BUNDLE_ROOT, ".claude-plugin", cmd.path));
1865
+ for (const [rel, content] of hookScriptsThisVersionWrites())
1866
+ out.set(rel, sha256Hex(content));
1867
+ fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
1868
+ return out;
1869
+ }
1870
+ function bytesAreOurs(path, bytes, recorded, writeSet) {
1871
+ const hash = sha256Hex(bytes);
1872
+ const recordedHash = recorded !== void 0 && "sha256" in recorded ? recorded.sha256 : void 0;
1873
+ return hash === recordedHash || hash === writeSet.generatedSha256.get(path);
1808
1874
  }
1809
1875
  function readPreviousManifest(targetRepo) {
1810
1876
  try {
@@ -1823,7 +1889,7 @@ function noteFile(draft, targetRepo, rel, outcome) {
1823
1889
  function recordSettingsMerge(draft, existing, generated, preexisted) {
1824
1890
  const generatedHooks = generated.hooks ?? {};
1825
1891
  const hookCommands = Object.values(generatedHooks).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
1826
- const ours = new Set(hookCommands);
1892
+ const ours = /* @__PURE__ */ new Set([...hookCommands, ...HALFCYCLE_HOOK_COMMANDS]);
1827
1893
  const existingHooks = isPlainObject(existing.hooks) ? existing.hooks : void 0;
1828
1894
  const heldOurs = Object.values(existingHooks ?? {}).some((entries) => Array.isArray(entries) && entries.some((entry) => isHalfcycleEntry(entry, ours)));
1829
1895
  const existingDeny = Array.isArray(existing.permissions?.deny) ? existing.permissions.deny : [];
@@ -1865,13 +1931,13 @@ function recordMcpAdoptionOnly(draft, existingText) {
1865
1931
  return;
1866
1932
  recordMcp(draft, { created: false, adopted: true, mcpServersCreated: false });
1867
1933
  }
1868
- function recordGitignoreReconcile(draft, before, after) {
1869
- const adopted = draft.gitignore === void 0 && before !== null ? adoptOlderGitignoreBlock(before) : "";
1870
- const appended = appendedText(before, after);
1871
- recordGitignore(draft, before === null && after !== null, [adopted, appended]);
1934
+ function recordGitignoreReconcile(draft, before, after, writeSet) {
1935
+ const adopted = draft.gitignore === void 0 && before !== null ? adoptBlockUnderHeader(before, GITIGNORE_HEADER, writeSet.gitignoreLines) : "";
1936
+ recordGitignore(draft, before === null && after !== null, [adopted, appendedText(before, after)]);
1872
1937
  }
1873
- function recordGitattributesReconcile(draft, before, after) {
1874
- recordGitattributes(draft, before === null && after !== null, [appendedText(before, after)]);
1938
+ function recordGitattributesReconcile(draft, before, after, writeSet) {
1939
+ const adopted = draft.gitattributes === void 0 && before !== null ? adoptBlockUnderHeader(before, GITATTRIBUTES_HEADER, writeSet.gitattributesLines) : "";
1940
+ recordGitattributes(draft, before === null && after !== null, [adopted, appendedText(before, after)]);
1875
1941
  }
1876
1942
  function appendedText(before, after) {
1877
1943
  if (after === null || after === before)
@@ -1887,7 +1953,7 @@ function assertManifestInWriteSet(manifest, writeSet) {
1887
1953
  ...manifest.createdDirs.filter((p) => !writeSet.dirs.has(p))
1888
1954
  ];
1889
1955
  if (strays.length > 0) {
1890
- throw new Error(`[bundle install] the install record names paths outside the write set: ${strays.join(", ")}`);
1956
+ throw new Error(`[halfcycle] the install record names paths outside the write set: ${strays.join(", ")}`);
1891
1957
  }
1892
1958
  }
1893
1959
  function findNotShared(targetRepo, manifest, collidedPaths) {
@@ -1939,6 +2005,13 @@ function lstatOrNull(path) {
1939
2005
  return null;
1940
2006
  }
1941
2007
  }
2008
+ function readFileOrNull(path) {
2009
+ try {
2010
+ return readFileSync4(path, "utf-8");
2011
+ } catch {
2012
+ return null;
2013
+ }
2014
+ }
1942
2015
  function isPlainObject(value) {
1943
2016
  return value !== null && typeof value === "object" && !Array.isArray(value);
1944
2017
  }
@@ -1947,63 +2020,73 @@ function readPluginManifest() {
1947
2020
  const raw = readFileSync4(manifestPath, "utf-8");
1948
2021
  return JSON.parse(raw);
1949
2022
  }
1950
- function copyManifestCommands(manifest, targetRepo, report, draft) {
2023
+ function copyManifestCommands(manifest, targetRepo, report, draft, writeSet) {
1951
2024
  for (const cmd of manifest.commands ?? []) {
1952
2025
  const srcFile = join5(BUNDLE_ROOT, ".claude-plugin", cmd.path);
1953
2026
  if (!existsSync3(srcFile)) {
1954
- throw new Error(`[bundle install] plugin.json declares command "${cmd.name}" at "${cmd.path}", but no file exists there (${srcFile}). The manifest and commands/ must agree.`);
2027
+ 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.`);
1955
2028
  }
1956
2029
  const rel = commandStubRel(cmd.name);
1957
2030
  const destFile = join5(targetRepo, rel);
1958
2031
  const content = readFileSync4(srcFile, "utf-8");
1959
- const outcome = writeCollisionSafe(destFile, targetRepo, content);
2032
+ const recorded = draft.files.get(rel);
2033
+ const { outcome, replacedExisting } = writeCollisionSafe(destFile, targetRepo, content, (current) => bytesAreOurs(rel, current, recorded, writeSet));
1960
2034
  record(report, outcome, rel);
2035
+ if (replacedExisting)
2036
+ report.replacedPaths.push(rel);
1961
2037
  noteFile(draft, targetRepo, rel, outcome);
1962
2038
  }
1963
2039
  }
1964
2040
  function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
1965
2041
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
1966
2042
  if (!isAllowlisted(rel)) {
1967
- throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2043
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
1968
2044
  }
1969
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
1970
- writeFileSync3(targetAbsPath, content, "utf-8");
2045
+ if (readFileOrNull(targetAbsPath) === content)
2046
+ return;
2047
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2048
+ writeFileSync4(targetAbsPath, content, "utf-8");
1971
2049
  writtenPaths.push(rel);
1972
2050
  }
1973
- function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
2051
+ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content, isOurs) {
1974
2052
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
1975
2053
  if (!isAllowlisted(rel)) {
1976
- throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2054
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
1977
2055
  }
2056
+ let replacedExisting = false;
1978
2057
  if (existsSync3(targetAbsPath)) {
1979
- const current = readFileSync4(targetAbsPath, "utf-8");
1980
- return current === content ? "skipped" : "collided";
2058
+ const current = readFileSync4(targetAbsPath);
2059
+ if (current.toString("utf-8") === content)
2060
+ return { outcome: "skipped", replacedExisting: false };
2061
+ if (!isOurs(current))
2062
+ return { outcome: "collided", replacedExisting: false };
2063
+ replacedExisting = true;
1981
2064
  }
1982
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
1983
- writeFileSync3(targetAbsPath, content, "utf-8");
1984
- return "written";
2065
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2066
+ writeFileSync4(targetAbsPath, content, "utf-8");
2067
+ return { outcome: "written", replacedExisting };
1985
2068
  }
1986
2069
  var INSTALLER_OWNED_DIR = ".halfcycle/";
1987
2070
  function writeOwned(targetAbsPath, targetRepoRoot, content) {
1988
2071
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
1989
2072
  if (!isAllowlisted(rel)) {
1990
- throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2073
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
1991
2074
  }
1992
2075
  if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
1993
- throw new Error(`[bundle install] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (writeOwnedGenerated) rather than assumed from where it sits.`);
2076
+ throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (writeOwnedGenerated) rather than assumed from where it sits.`);
1994
2077
  }
1995
2078
  if (existsSync3(targetAbsPath) && readFileSync4(targetAbsPath, "utf-8") === content) {
1996
2079
  return "skipped";
1997
2080
  }
1998
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
1999
- writeFileSync3(targetAbsPath, content, "utf-8");
2081
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2082
+ writeFileSync4(targetAbsPath, content, "utf-8");
2000
2083
  return "written";
2001
2084
  }
2002
2085
  var GENERATED_HEADER_LINE_INDEX = 1;
2003
2086
  function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
2004
2087
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
2005
2088
  if (!isAllowlisted(rel)) {
2006
- throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2089
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2007
2090
  }
2008
2091
  let replacedExisting = false;
2009
2092
  if (existsSync3(targetAbsPath)) {
@@ -2015,8 +2098,8 @@ function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMa
2015
2098
  return { outcome: "collided", replacedExisting: false };
2016
2099
  replacedExisting = true;
2017
2100
  }
2018
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
2019
- writeFileSync3(targetAbsPath, content, "utf-8");
2101
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2102
+ writeFileSync4(targetAbsPath, content, "utf-8");
2020
2103
  return { outcome: "written", replacedExisting };
2021
2104
  }
2022
2105
  function recordOwnedGenerated(report, targetAbsPath, targetRepoRoot, content, generatedMarker, rel) {
@@ -2054,6 +2137,26 @@ var DENY_PATTERNS = [
2054
2137
  "Bash(git commit --no-verify:*)"
2055
2138
  ];
2056
2139
  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.";
2140
+ function readExistingSettings(targetRepo) {
2141
+ const settingsPath = join5(targetRepo, SETTINGS_REL);
2142
+ return existsSync3(settingsPath) ? JSON.parse(readFileSync4(settingsPath, "utf-8")) : void 0;
2143
+ }
2144
+ var SettingsParseRefused = class extends Error {
2145
+ constructor(message) {
2146
+ super(message);
2147
+ this.name = "SettingsParseRefused";
2148
+ }
2149
+ };
2150
+ function settingsParseRefusal(targetRepo, err) {
2151
+ const file = join5(targetRepo, SETTINGS_REL);
2152
+ const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
2153
+ let where = "";
2154
+ if (offset !== void 0) {
2155
+ const before = readFileSync4(file, "utf-8").slice(0, Number(offset)).split("\n");
2156
+ where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
2157
+ }
2158
+ return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
2159
+ }
2057
2160
  function generateSettingsJson() {
2058
2161
  const hooks = {};
2059
2162
  for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
@@ -2118,6 +2221,10 @@ if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
2118
2221
  export HALFCYCLE_NOT_THIS_ACCOUNT=1
2119
2222
  fi
2120
2223
 
2224
+ # On a Node older than Halfcycle supports, the runner cannot even be read, so
2225
+ # say that in one line instead of handing it a stack trace.
2226
+ node -e "${nodeFloorProgram("")}" || exit
2227
+
2121
2228
  exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
2122
2229
  `;
2123
2230
  }
@@ -2130,7 +2237,7 @@ function noNodeReplyShell() {
2130
2237
  hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: NO_NODE_SENTENCE }
2131
2238
  });
2132
2239
  if (reply.includes("'")) {
2133
- throw new Error("[bundle install] the no-Node notice cannot be single-quoted; refusing to write the session-start script.");
2240
+ throw new Error("[halfcycle] the no-Node notice cannot be single-quoted; refusing to write the session-start script.");
2134
2241
  }
2135
2242
  return `if ! command -v node >/dev/null 2>&1; then
2136
2243
  printf '%s\\n' '${reply}'
@@ -2293,13 +2400,13 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
2293
2400
  if (!isTrackedAndClean(targetRepo, rel))
2294
2401
  return "kept-uncommitted";
2295
2402
  try {
2296
- rmSync(path);
2403
+ rmSync2(path);
2297
2404
  } catch {
2298
2405
  return "failed";
2299
2406
  }
2300
2407
  return "removed";
2301
2408
  }
2302
- var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
2409
+ var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
2303
2410
  `const p=require('path'),f=require('fs'),c=require('child_process');`,
2304
2411
  `let d=process.cwd();`,
2305
2412
  `for(;;){`,
@@ -2312,7 +2419,7 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
2312
2419
  `d=u}`,
2313
2420
  `console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
2314
2421
  `process.exit(1)`
2315
- ].join("")}"`;
2422
+ ].join(""))}"`;
2316
2423
  var MCP_HEADERS_KNOWN_HEADER_HASHES = [
2317
2424
  "37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
2318
2425
  "6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
@@ -2326,6 +2433,14 @@ var OWNED_GENERATED_HEADERS = {
2326
2433
  ".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
2327
2434
  ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
2328
2435
  };
2436
+ function hookScriptsThisVersionWrites() {
2437
+ return [
2438
+ [".claude/hooks/guard-runner.sh", generateGuardRunnerWrapper()],
2439
+ [".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
2440
+ [".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
2441
+ [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
2442
+ ];
2443
+ }
2329
2444
  function generateMcpRegistration(existing, mcpOrigin) {
2330
2445
  let base = { mcpServers: {} };
2331
2446
  if (existing !== null) {
@@ -2346,13 +2461,12 @@ function generateMcpRegistration(existing, mcpOrigin) {
2346
2461
  }
2347
2462
  var REQUIRED_GITIGNORE_ENTRIES = [".halfcycle/state.json", `${ZONE_B_DIR}/`];
2348
2463
  var GITIGNORE_HEADER = "# Halfcycle \u2014 machine-local secrets/state and Zone-B (never push to client remote)";
2349
- function adoptOlderGitignoreBlock(before) {
2350
- const claimable = /* @__PURE__ */ new Set([...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]);
2464
+ function adoptBlockUnderHeader(before, header, claimable) {
2351
2465
  const claimed = [];
2352
2466
  let underHeader = false;
2353
2467
  const lines = before.split("\n");
2354
2468
  for (const [i, line] of lines.entries()) {
2355
- if (line === GITIGNORE_HEADER) {
2469
+ if (line === header) {
2356
2470
  if (i > 0 && lines[i - 1] === "")
2357
2471
  claimed.push("");
2358
2472
  claimed.push(line);
@@ -2375,7 +2489,7 @@ function reconcileGitignore(targetRepo) {
2375
2489
  try {
2376
2490
  if (!existsSync3(gitignorePath)) {
2377
2491
  const body = [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n";
2378
- writeFileSync3(gitignorePath, body, "utf-8");
2492
+ writeFileSync4(gitignorePath, body, "utf-8");
2379
2493
  return "written";
2380
2494
  }
2381
2495
  const existing = readFileSync4(gitignorePath, "utf-8");
@@ -2386,7 +2500,7 @@ function reconcileGitignore(targetRepo) {
2386
2500
  const suffix = (existing.endsWith("\n") ? "" : "\n") + `
2387
2501
  ${header}
2388
2502
  ` + toAdd.join("\n") + "\n";
2389
- writeFileSync3(gitignorePath, existing + suffix, "utf-8");
2503
+ writeFileSync4(gitignorePath, existing + suffix, "utf-8");
2390
2504
  return "written";
2391
2505
  } catch {
2392
2506
  return "failed";
@@ -2494,12 +2608,9 @@ function checkDrift(targetRepoRoot) {
2494
2608
  return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
2495
2609
  }
2496
2610
  function writeProjectIdentity(targetRepo) {
2497
- const path = join5(targetRepo, PROJECT_IDENTITY_REL);
2498
- const { identity } = mintOrReadIdentity(targetRepo);
2499
- const serialized = JSON.stringify(identity, null, 2) + "\n";
2500
- if (existsSync3(path) && readFileSync4(path, "utf-8") === serialized)
2611
+ if (existsSync3(join5(targetRepo, PROJECT_IDENTITY_REL)))
2501
2612
  return "skipped";
2502
- writeFileSync3(path, serialized, "utf-8");
2613
+ createProjectIdentityFile(targetRepo);
2503
2614
  return "written";
2504
2615
  }
2505
2616
  function writeEngagementCredential(credential, engagementId, home) {
@@ -2562,10 +2673,10 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
2562
2673
  const substantive = kept.filter((line) => line.trim() !== "" && !line.trim().startsWith("# Halfcycle"));
2563
2674
  try {
2564
2675
  if (substantive.length === 0) {
2565
- rmSync(path);
2676
+ rmSync2(path);
2566
2677
  return { outcome: "removed", unbacked: [], missingForAdoption: [] };
2567
2678
  }
2568
- writeFileSync3(path, kept.join("\n"), "utf-8");
2679
+ writeFileSync4(path, kept.join("\n"), "utf-8");
2569
2680
  return { outcome: "stripped", unbacked: [], missingForAdoption: [] };
2570
2681
  } catch {
2571
2682
  return { outcome: "failed", unbacked: [], missingForAdoption: [] };
@@ -2575,7 +2686,7 @@ async function install(options) {
2575
2686
  const { targetRepo, engagementId, engagementType, credential, home, accountId } = options;
2576
2687
  assertEngagementId(engagementId);
2577
2688
  if (!existsSync3(targetRepo)) {
2578
- throw new Error(`[bundle install] Target repo does not exist: ${targetRepo}`);
2689
+ throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
2579
2690
  }
2580
2691
  const manifest = readPluginManifest();
2581
2692
  const report = {
@@ -2588,18 +2699,7 @@ async function install(options) {
2588
2699
  const writeSet = closedWriteSet();
2589
2700
  const draft = startManifest(readPreviousManifest(targetRepo), (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
2590
2701
  const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
2591
- copyManifestCommands(manifest, targetRepo, report, draft);
2592
- const capturedManifest = join5(targetRepo, CAPTURED_INDEX_REL);
2593
- const srcManifest = join5(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
2594
- if (!existsSync3(capturedManifest)) {
2595
- writeAllowlisted(capturedManifest, targetRepo, readFileSync4(srcManifest, "utf-8"), report.writtenPaths);
2596
- noteFile(draft, targetRepo, CAPTURED_INDEX_REL, "written");
2597
- } else {
2598
- report.skippedPaths.push(CAPTURED_INDEX_REL);
2599
- if (readFileSync4(capturedManifest).equals(readFileSync4(srcManifest))) {
2600
- noteFile(draft, targetRepo, CAPTURED_INDEX_REL, "skipped");
2601
- }
2602
- }
2702
+ copyManifestCommands(manifest, targetRepo, report, draft, writeSet);
2603
2703
  const vendoredBinSrc = resolveVendoredBinary();
2604
2704
  const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
2605
2705
  const vendoredOutcome = recordOwned(report, vendoredBinDest, targetRepo, readFileSync4(vendoredBinSrc, "utf-8"), VENDORED_BIN_REL);
@@ -2607,21 +2707,20 @@ async function install(options) {
2607
2707
  const settingsPath = join5(targetRepo, SETTINGS_REL);
2608
2708
  const settingsPreexisted = existsSync3(settingsPath);
2609
2709
  const generatedSettings = JSON.parse(generateSettingsJson());
2610
- const existingSettings = settingsPreexisted ? JSON.parse(readFileSync4(settingsPath, "utf-8")) : {};
2710
+ let existingSettings;
2711
+ try {
2712
+ existingSettings = readExistingSettings(targetRepo) ?? {};
2713
+ } catch (err) {
2714
+ throw new SettingsParseRefused(settingsParseRefusal(targetRepo, err));
2715
+ }
2611
2716
  recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted);
2612
2717
  const mergedSettings = mergeSettings(existingSettings, generatedSettings);
2613
2718
  const mergedSettingsText = JSON.stringify(mergedSettings, null, 2) + "\n";
2614
- mkdirSync3(dirname2(settingsPath), { recursive: true });
2615
- writeFileSync3(settingsPath, mergedSettingsText, "utf-8");
2719
+ mkdirSync4(dirname3(settingsPath), { recursive: true });
2720
+ writeFileSync4(settingsPath, mergedSettingsText, "utf-8");
2616
2721
  (settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
2617
- for (const [name, content] of [
2618
- ["guard-runner.sh", generateGuardRunnerWrapper()],
2619
- ["session-end-marker.sh", generateSessionEndMarker()],
2620
- ["user-prompt-reminder.sh", generateUserPromptReminderHook()],
2621
- ["session-start-marker.sh", generateSessionStartMarker()]
2622
- ]) {
2623
- const rel = `.claude/hooks/${name}`;
2624
- const hookPath = join5(targetRepo, ".claude", "hooks", name);
2722
+ for (const [rel, content] of hookScriptsThisVersionWrites()) {
2723
+ const hookPath = join5(targetRepo, rel);
2625
2724
  const outcome = recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
2626
2725
  noteFile(draft, targetRepo, rel, outcome);
2627
2726
  }
@@ -2680,7 +2779,7 @@ async function install(options) {
2680
2779
  const gitignorePath = join5(targetRepo, GITIGNORE_REL);
2681
2780
  const gitignoreBefore = existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null;
2682
2781
  const gitignoreOutcome = reconcileGitignore(targetRepo);
2683
- recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null);
2782
+ recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null, writeSet);
2684
2783
  if (gitignoreOutcome === "failed") {
2685
2784
  report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
2686
2785
  } else {
@@ -2689,7 +2788,7 @@ async function install(options) {
2689
2788
  const gitattributesPath = join5(targetRepo, GITATTRIBUTES_REL);
2690
2789
  const gitattributesBefore = readTextOrNull(gitattributesPath);
2691
2790
  const gitattributesOutcome = reconcileGitattributes(targetRepo);
2692
- recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath));
2791
+ recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath), writeSet);
2693
2792
  if (gitattributesOutcome === "failed") {
2694
2793
  report.skippedPaths.push(`${GITATTRIBUTES_REL} (write failed \u2014 a clone made with core.autocrlf=true may check the hook scripts out with CRLF line endings)`);
2695
2794
  } else {
@@ -2812,16 +2911,22 @@ function planeRefusal(detail) {
2812
2911
  reason: typeof candidate.reason === "string" ? candidate.reason : void 0
2813
2912
  };
2814
2913
  }
2815
- var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}; it must be the CONTROL origin \u2014 the one that serves /engagements. The MCP server runs at a different address, and the platform supplies that one itself; you configure neither.`;
2914
+ var CONTROL_ORIGIN_HINT = `This CLI talks to the Halfcycle service at ${DEFAULT_CONTROL_ORIGIN} \u2014 the same address for every user, and not something you set. Halfcycle also uses a second, different address for editor commands, which is not yours to set either. If something else answered here, it is most likely a network problem or an outage rather than something to configure.`;
2816
2915
  async function createEngagement(baseUrl, name, credential, derivedName) {
2817
2916
  return requestEngagementValues(
2917
+ "POST",
2818
2918
  `${baseUrl.replace(/\/+$/, "")}/engagements`,
2819
2919
  // A stated name wins over a guess, and the body carries at most one of them —
2820
2920
  // the plane applies the same ordering, so the two cannot disagree about which
2821
2921
  // one a row was named from.
2822
2922
  name ? { name } : derivedName ? { derivedName } : {},
2823
2923
  credential,
2824
- { action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" }
2924
+ {
2925
+ action: "Create-engagement",
2926
+ nothingHappened: "No engagement was created",
2927
+ route: "POST /engagements",
2928
+ alsoNothingWritten: true
2929
+ }
2825
2930
  );
2826
2931
  }
2827
2932
  function withDeclaredTelemetryKey(raw) {
@@ -2830,12 +2935,12 @@ function withDeclaredTelemetryKey(raw) {
2830
2935
  const obj = raw;
2831
2936
  return "controlTelemetryUrl" in obj ? obj : { ...obj, controlTelemetryUrl: "" };
2832
2937
  }
2833
- async function requestEngagementValues(url, requestBody, credential, shape) {
2938
+ async function requestEngagementValues(method, url, requestBody, credential, shape) {
2834
2939
  const bearer = credential?.trim();
2835
2940
  let res;
2836
2941
  try {
2837
2942
  res = await fetch(url, {
2838
- method: "POST",
2943
+ method,
2839
2944
  headers: {
2840
2945
  "content-type": "application/json",
2841
2946
  ...bearer ? { authorization: `Bearer ${bearer}` } : {}
@@ -2843,36 +2948,36 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2843
2948
  body: JSON.stringify(requestBody)
2844
2949
  });
2845
2950
  } catch (err) {
2846
- throw new Error(`[bundle install] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2951
+ throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2847
2952
  }
2848
2953
  if (!res.ok) {
2849
2954
  const detail = await res.text().catch(() => "");
2850
2955
  const refusal = res.status === 404 ? void 0 : planeRefusal(detail);
2851
2956
  if (refusal) {
2852
- throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, refusal.reason, `[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
2957
+ throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, refusal.reason, `[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
2853
2958
  }
2854
2959
  const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
2855
- throw new Error(`[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2960
+ throw new Error(`[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2856
2961
  }
2857
2962
  const candidate = withDeclaredTelemetryKey(await res.json().catch(() => null));
2858
2963
  const declared = engagementCredentialResponseSchema.safeParse(candidate);
2859
2964
  const body = declared.success ? declared.data : candidate;
2860
2965
  if (!body || typeof body.engagementId !== "string" || typeof body.sessionToken !== "string") {
2861
- throw new Error(`[bundle install] ${shape.action} response from ${url} did not carry {engagementId, sessionToken}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2966
+ throw new Error(`[halfcycle] ${shape.action} response from ${url} did not carry {engagementId, sessionToken}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2862
2967
  }
2863
2968
  for (const [field, value] of [
2864
2969
  ["engagementId", body.engagementId],
2865
2970
  ["sessionToken", body.sessionToken]
2866
2971
  ]) {
2867
2972
  if (value.trim() === "") {
2868
- throw new Error(`[bundle install] The Halfcycle service at ${url} answered with a blank ${field} (the field is present but empty). An install cannot proceed on it: it would be written into this engagement's credential store, read back as a configured value, and every guard evaluation would fail against it for the life of the engagement. ${shape.nothingHappened} and nothing was written. ` + CONTROL_ORIGIN_HINT);
2973
+ throw new Error(`[halfcycle] The Halfcycle service at ${url} answered with a blank ${field} (the field is present but empty). An install cannot proceed on it: it would be written into this engagement's credential store, read back as a configured value, and every guard evaluation would fail against it for the life of the engagement. ${shape.nothingHappened}${shape.alsoNothingWritten ? " and nothing was written" : ""}. ` + CONTROL_ORIGIN_HINT);
2869
2974
  }
2870
2975
  }
2871
2976
  if (typeof body.mcpUrl !== "string" || body.mcpUrl.trim() === "") {
2872
- throw new Error(`[bundle install] The Halfcycle service at ${url} created an engagement but did not say where its MCP server lives (no mcpUrl on the response). Without that address the install would write an MCP registration pointing nowhere, so nothing was written. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
2977
+ throw new Error(`[halfcycle] The Halfcycle service at ${url} created an engagement but did not say where its MCP server lives (no mcpUrl on the response). Without that address the install would write an MCP registration pointing nowhere, so it does not continue. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
2873
2978
  }
2874
2979
  if (typeof body.guardUrl !== "string" || body.guardUrl.trim() === "") {
2875
- throw new Error(`[bundle install] The Halfcycle service at ${url} created an engagement but did not say where its guard service lives (no guardUrl on the response). Without that address the install would wire a guard hook that evaluates nothing, so nothing was written. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
2980
+ throw new Error(`[halfcycle] The Halfcycle service at ${url} created an engagement but did not say where its guard service lives (no guardUrl on the response). Without that address the install would wire a guard hook that evaluates nothing, so it does not continue. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
2876
2981
  }
2877
2982
  const controlTelemetryUrl = typeof body.controlTelemetryUrl === "string" && body.controlTelemetryUrl.trim() !== "" ? body.controlTelemetryUrl.trim().replace(/\/+$/, "") : void 0;
2878
2983
  if (!declared.success) {
@@ -2883,7 +2988,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2883
2988
  }
2884
2989
  return issue.path.length > 0 ? issue.path.map(String).join(".") : "(the body itself)";
2885
2990
  }).join(", ");
2886
- throw new Error(`[bundle install] ${shape.action} response from ${url} is not the credential response this CLI is built to read (${fields}). ${shape.nothingHappened}. Either that service is a different version from this installer, or ${url} is not a Halfcycle service.`);
2991
+ throw new Error(`[halfcycle] ${shape.action} response from ${url} is not the credential response this CLI is built to read (${fields}). ${shape.nothingHappened}. Either that service is a different version from this installer, or ${url} is not a Halfcycle service.`);
2887
2992
  }
2888
2993
  const consoleUrl = typeof body.consoleUrl === "string" && body.consoleUrl.trim() !== "" ? body.consoleUrl.trim() : void 0;
2889
2994
  const methodVersion = typeof body.methodVersion === "string" && body.methodVersion.trim() !== "" ? body.methodVersion.trim() : void 0;
@@ -2899,8 +3004,8 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2899
3004
  }
2900
3005
 
2901
3006
  // dist/setup/manifest.js
2902
- import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync4 } from "node:fs";
2903
- import { dirname as dirname3, join as join6, posix as posix2 } from "node:path";
3007
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync5 } from "node:fs";
3008
+ import { dirname as dirname4, join as join6, posix as posix2 } from "node:path";
2904
3009
  var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
2905
3010
  var DECLARED_DIR_PREFIX = "docs";
2906
3011
  var RESERVED_METHOD_PATH = "docs/method";
@@ -3011,8 +3116,8 @@ function applyFileRow(targetRepo, path, content) {
3011
3116
  const current = readFileSync5(abs, "utf-8");
3012
3117
  return current === content ? { path, outcome: "already-present", note: "already present and correct \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this file \u2014 left exactly as it was" };
3013
3118
  }
3014
- mkdirSync4(dirname3(abs), { recursive: true });
3015
- writeFileSync4(abs, content, "utf-8");
3119
+ mkdirSync5(dirname4(abs), { recursive: true });
3120
+ writeFileSync5(abs, content, "utf-8");
3016
3121
  return { path, outcome: "created", note: "created" };
3017
3122
  }
3018
3123
  function applySectionRow(targetRepo, path, heading, body) {
@@ -3022,8 +3127,8 @@ function applySectionRow(targetRepo, path, heading, body) {
3022
3127
  return symlinkEntry(path, link, false);
3023
3128
  const section = renderSection(heading, body);
3024
3129
  if (!existsSync4(abs)) {
3025
- mkdirSync4(dirname3(abs), { recursive: true });
3026
- writeFileSync4(abs, section, "utf-8");
3130
+ mkdirSync5(dirname4(abs), { recursive: true });
3131
+ writeFileSync5(abs, section, "utf-8");
3027
3132
  return { path, outcome: "created", note: `created, with a "${heading}" section` };
3028
3133
  }
3029
3134
  const existing = readFileSync5(abs, "utf-8");
@@ -3035,7 +3140,7 @@ function applySectionRow(targetRepo, path, heading, body) {
3035
3140
  };
3036
3141
  }
3037
3142
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
3038
- writeFileSync4(abs, existing + separator + section, "utf-8");
3143
+ writeFileSync5(abs, existing + separator + section, "utf-8");
3039
3144
  return {
3040
3145
  path,
3041
3146
  outcome: "section-added",
@@ -3051,7 +3156,7 @@ function applyDirectoryRow(targetRepo, path) {
3051
3156
  const kind = statSync(abs).isDirectory() ? "directory" : "file";
3052
3157
  return kind === "directory" ? { path: `${path}/`, outcome: "already-present", note: "already there \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this path \u2014 left exactly as it was" };
3053
3158
  }
3054
- mkdirSync4(abs, { recursive: true });
3159
+ mkdirSync5(abs, { recursive: true });
3055
3160
  return { path: `${path}/`, outcome: "created", note: "created" };
3056
3161
  }
3057
3162
  function applySetupWrites(targetRepo, rows) {
@@ -3734,7 +3839,7 @@ function renderBuildRecordV2Markdown(record2) {
3734
3839
  }
3735
3840
 
3736
3841
  // dist/build-record/write.js
3737
- import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeSync } from "node:fs";
3842
+ import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync3, writeSync } from "node:fs";
3738
3843
  import { basename, join as join8, resolve, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
3739
3844
  var BUILD_RECORD_DIR = join8("docs", "build-records");
3740
3845
  function assertInsideOutDir(outDir, candidate, identity) {
@@ -3750,7 +3855,7 @@ function createExclusiveOnDisk(path, text) {
3750
3855
  writeSync(fd, text);
3751
3856
  } catch (err) {
3752
3857
  closeSync(fd);
3753
- rmSync2(path, { force: true });
3858
+ rmSync3(path, { force: true });
3754
3859
  throw err;
3755
3860
  }
3756
3861
  closeSync(fd);
@@ -3814,7 +3919,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
3814
3919
  }
3815
3920
  const jsonText = serialiseServedRecord(rawRecord);
3816
3921
  const mdText = renderBuildRecordV2Markdown(record2);
3817
- mkdirSync5(outDir, { recursive: true });
3922
+ mkdirSync6(outDir, { recursive: true });
3818
3923
  let createdJson = false;
3819
3924
  try {
3820
3925
  io.createExclusive(jsonPath, jsonText);
@@ -3823,7 +3928,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
3823
3928
  return { kind: "created", jsonPath, mdPath };
3824
3929
  } catch (err) {
3825
3930
  if (createdJson)
3826
- rmSync2(jsonPath, { force: true });
3931
+ rmSync3(jsonPath, { force: true });
3827
3932
  if (!isEexist(err))
3828
3933
  throw err;
3829
3934
  }
@@ -3845,19 +3950,19 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
3845
3950
  try {
3846
3951
  replaceOne(io, jsonTmp, jsonPath, existingJson);
3847
3952
  } catch {
3848
- rmSync2(jsonPath, { force: true });
3953
+ rmSync3(jsonPath, { force: true });
3849
3954
  }
3850
3955
  throw err;
3851
3956
  }
3852
3957
  return { kind: "replaced", jsonPath, mdPath };
3853
3958
  }
3854
3959
  function replaceOne(io, tmp, finalPath, text) {
3855
- rmSync2(tmp, { force: true });
3960
+ rmSync3(tmp, { force: true });
3856
3961
  try {
3857
3962
  io.createExclusive(tmp, text);
3858
3963
  io.rename(tmp, finalPath);
3859
3964
  } catch (err) {
3860
- rmSync2(tmp, { force: true });
3965
+ rmSync3(tmp, { force: true });
3861
3966
  throw err;
3862
3967
  }
3863
3968
  }