halfcycle 0.3.40 → 0.4.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/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,22 @@ 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;
1808
1869
  }
1809
1870
  function readPreviousManifest(targetRepo) {
1810
1871
  try {
@@ -1823,7 +1884,7 @@ function noteFile(draft, targetRepo, rel, outcome) {
1823
1884
  function recordSettingsMerge(draft, existing, generated, preexisted) {
1824
1885
  const generatedHooks = generated.hooks ?? {};
1825
1886
  const hookCommands = Object.values(generatedHooks).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
1826
- const ours = new Set(hookCommands);
1887
+ const ours = /* @__PURE__ */ new Set([...hookCommands, ...HALFCYCLE_HOOK_COMMANDS]);
1827
1888
  const existingHooks = isPlainObject(existing.hooks) ? existing.hooks : void 0;
1828
1889
  const heldOurs = Object.values(existingHooks ?? {}).some((entries) => Array.isArray(entries) && entries.some((entry) => isHalfcycleEntry(entry, ours)));
1829
1890
  const existingDeny = Array.isArray(existing.permissions?.deny) ? existing.permissions.deny : [];
@@ -1865,13 +1926,13 @@ function recordMcpAdoptionOnly(draft, existingText) {
1865
1926
  return;
1866
1927
  recordMcp(draft, { created: false, adopted: true, mcpServersCreated: false });
1867
1928
  }
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]);
1929
+ function recordGitignoreReconcile(draft, before, after, writeSet) {
1930
+ const adopted = draft.gitignore === void 0 && before !== null ? adoptBlockUnderHeader(before, GITIGNORE_HEADER, writeSet.gitignoreLines) : "";
1931
+ recordGitignore(draft, before === null && after !== null, [adopted, appendedText(before, after)]);
1872
1932
  }
1873
- function recordGitattributesReconcile(draft, before, after) {
1874
- recordGitattributes(draft, before === null && after !== null, [appendedText(before, after)]);
1933
+ function recordGitattributesReconcile(draft, before, after, writeSet) {
1934
+ const adopted = draft.gitattributes === void 0 && before !== null ? adoptBlockUnderHeader(before, GITATTRIBUTES_HEADER, writeSet.gitattributesLines) : "";
1935
+ recordGitattributes(draft, before === null && after !== null, [adopted, appendedText(before, after)]);
1875
1936
  }
1876
1937
  function appendedText(before, after) {
1877
1938
  if (after === null || after === before)
@@ -1887,7 +1948,7 @@ function assertManifestInWriteSet(manifest, writeSet) {
1887
1948
  ...manifest.createdDirs.filter((p) => !writeSet.dirs.has(p))
1888
1949
  ];
1889
1950
  if (strays.length > 0) {
1890
- throw new Error(`[bundle install] the install record names paths outside the write set: ${strays.join(", ")}`);
1951
+ throw new Error(`[halfcycle] the install record names paths outside the write set: ${strays.join(", ")}`);
1891
1952
  }
1892
1953
  }
1893
1954
  function findNotShared(targetRepo, manifest, collidedPaths) {
@@ -1939,6 +2000,13 @@ function lstatOrNull(path) {
1939
2000
  return null;
1940
2001
  }
1941
2002
  }
2003
+ function readFileOrNull(path) {
2004
+ try {
2005
+ return readFileSync4(path, "utf-8");
2006
+ } catch {
2007
+ return null;
2008
+ }
2009
+ }
1942
2010
  function isPlainObject(value) {
1943
2011
  return value !== null && typeof value === "object" && !Array.isArray(value);
1944
2012
  }
@@ -1951,7 +2019,7 @@ function copyManifestCommands(manifest, targetRepo, report, draft) {
1951
2019
  for (const cmd of manifest.commands ?? []) {
1952
2020
  const srcFile = join5(BUNDLE_ROOT, ".claude-plugin", cmd.path);
1953
2021
  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.`);
2022
+ 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
2023
  }
1956
2024
  const rel = commandStubRel(cmd.name);
1957
2025
  const destFile = join5(targetRepo, rel);
@@ -1964,46 +2032,48 @@ function copyManifestCommands(manifest, targetRepo, report, draft) {
1964
2032
  function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
1965
2033
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
1966
2034
  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.`);
2035
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
1968
2036
  }
1969
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
1970
- writeFileSync3(targetAbsPath, content, "utf-8");
2037
+ if (readFileOrNull(targetAbsPath) === content)
2038
+ return;
2039
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2040
+ writeFileSync4(targetAbsPath, content, "utf-8");
1971
2041
  writtenPaths.push(rel);
1972
2042
  }
1973
2043
  function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
1974
2044
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
1975
2045
  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.`);
2046
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
1977
2047
  }
1978
2048
  if (existsSync3(targetAbsPath)) {
1979
2049
  const current = readFileSync4(targetAbsPath, "utf-8");
1980
2050
  return current === content ? "skipped" : "collided";
1981
2051
  }
1982
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
1983
- writeFileSync3(targetAbsPath, content, "utf-8");
2052
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2053
+ writeFileSync4(targetAbsPath, content, "utf-8");
1984
2054
  return "written";
1985
2055
  }
1986
2056
  var INSTALLER_OWNED_DIR = ".halfcycle/";
1987
2057
  function writeOwned(targetAbsPath, targetRepoRoot, content) {
1988
2058
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
1989
2059
  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.`);
2060
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
1991
2061
  }
1992
2062
  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.`);
2063
+ 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
2064
  }
1995
2065
  if (existsSync3(targetAbsPath) && readFileSync4(targetAbsPath, "utf-8") === content) {
1996
2066
  return "skipped";
1997
2067
  }
1998
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
1999
- writeFileSync3(targetAbsPath, content, "utf-8");
2068
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2069
+ writeFileSync4(targetAbsPath, content, "utf-8");
2000
2070
  return "written";
2001
2071
  }
2002
2072
  var GENERATED_HEADER_LINE_INDEX = 1;
2003
2073
  function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
2004
2074
  const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
2005
2075
  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.`);
2076
+ throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
2007
2077
  }
2008
2078
  let replacedExisting = false;
2009
2079
  if (existsSync3(targetAbsPath)) {
@@ -2015,8 +2085,8 @@ function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMa
2015
2085
  return { outcome: "collided", replacedExisting: false };
2016
2086
  replacedExisting = true;
2017
2087
  }
2018
- mkdirSync3(dirname2(targetAbsPath), { recursive: true });
2019
- writeFileSync3(targetAbsPath, content, "utf-8");
2088
+ mkdirSync4(dirname3(targetAbsPath), { recursive: true });
2089
+ writeFileSync4(targetAbsPath, content, "utf-8");
2020
2090
  return { outcome: "written", replacedExisting };
2021
2091
  }
2022
2092
  function recordOwnedGenerated(report, targetAbsPath, targetRepoRoot, content, generatedMarker, rel) {
@@ -2054,6 +2124,26 @@ var DENY_PATTERNS = [
2054
2124
  "Bash(git commit --no-verify:*)"
2055
2125
  ];
2056
2126
  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.";
2127
+ function readExistingSettings(targetRepo) {
2128
+ const settingsPath = join5(targetRepo, SETTINGS_REL);
2129
+ return existsSync3(settingsPath) ? JSON.parse(readFileSync4(settingsPath, "utf-8")) : void 0;
2130
+ }
2131
+ var SettingsParseRefused = class extends Error {
2132
+ constructor(message) {
2133
+ super(message);
2134
+ this.name = "SettingsParseRefused";
2135
+ }
2136
+ };
2137
+ function settingsParseRefusal(targetRepo, err) {
2138
+ const file = join5(targetRepo, SETTINGS_REL);
2139
+ const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
2140
+ let where = "";
2141
+ if (offset !== void 0) {
2142
+ const before = readFileSync4(file, "utf-8").slice(0, Number(offset)).split("\n");
2143
+ where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
2144
+ }
2145
+ return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
2146
+ }
2057
2147
  function generateSettingsJson() {
2058
2148
  const hooks = {};
2059
2149
  for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
@@ -2118,6 +2208,10 @@ if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
2118
2208
  export HALFCYCLE_NOT_THIS_ACCOUNT=1
2119
2209
  fi
2120
2210
 
2211
+ # On a Node older than Halfcycle supports, the runner cannot even be read, so
2212
+ # say that in one line instead of handing it a stack trace.
2213
+ node -e "${nodeFloorProgram("")}" || exit
2214
+
2121
2215
  exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
2122
2216
  `;
2123
2217
  }
@@ -2130,7 +2224,7 @@ function noNodeReplyShell() {
2130
2224
  hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: NO_NODE_SENTENCE }
2131
2225
  });
2132
2226
  if (reply.includes("'")) {
2133
- throw new Error("[bundle install] the no-Node notice cannot be single-quoted; refusing to write the session-start script.");
2227
+ throw new Error("[halfcycle] the no-Node notice cannot be single-quoted; refusing to write the session-start script.");
2134
2228
  }
2135
2229
  return `if ! command -v node >/dev/null 2>&1; then
2136
2230
  printf '%s\\n' '${reply}'
@@ -2293,13 +2387,13 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
2293
2387
  if (!isTrackedAndClean(targetRepo, rel))
2294
2388
  return "kept-uncommitted";
2295
2389
  try {
2296
- rmSync(path);
2390
+ rmSync2(path);
2297
2391
  } catch {
2298
2392
  return "failed";
2299
2393
  }
2300
2394
  return "removed";
2301
2395
  }
2302
- var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
2396
+ var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
2303
2397
  `const p=require('path'),f=require('fs'),c=require('child_process');`,
2304
2398
  `let d=process.cwd();`,
2305
2399
  `for(;;){`,
@@ -2312,7 +2406,7 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
2312
2406
  `d=u}`,
2313
2407
  `console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
2314
2408
  `process.exit(1)`
2315
- ].join("")}"`;
2409
+ ].join(""))}"`;
2316
2410
  var MCP_HEADERS_KNOWN_HEADER_HASHES = [
2317
2411
  "37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
2318
2412
  "6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
@@ -2326,6 +2420,14 @@ var OWNED_GENERATED_HEADERS = {
2326
2420
  ".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
2327
2421
  ".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
2328
2422
  };
2423
+ function hookScriptsThisVersionWrites() {
2424
+ return [
2425
+ [".claude/hooks/guard-runner.sh", generateGuardRunnerWrapper()],
2426
+ [".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
2427
+ [".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
2428
+ [".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
2429
+ ];
2430
+ }
2329
2431
  function generateMcpRegistration(existing, mcpOrigin) {
2330
2432
  let base = { mcpServers: {} };
2331
2433
  if (existing !== null) {
@@ -2346,13 +2448,12 @@ function generateMcpRegistration(existing, mcpOrigin) {
2346
2448
  }
2347
2449
  var REQUIRED_GITIGNORE_ENTRIES = [".halfcycle/state.json", `${ZONE_B_DIR}/`];
2348
2450
  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]);
2451
+ function adoptBlockUnderHeader(before, header, claimable) {
2351
2452
  const claimed = [];
2352
2453
  let underHeader = false;
2353
2454
  const lines = before.split("\n");
2354
2455
  for (const [i, line] of lines.entries()) {
2355
- if (line === GITIGNORE_HEADER) {
2456
+ if (line === header) {
2356
2457
  if (i > 0 && lines[i - 1] === "")
2357
2458
  claimed.push("");
2358
2459
  claimed.push(line);
@@ -2375,7 +2476,7 @@ function reconcileGitignore(targetRepo) {
2375
2476
  try {
2376
2477
  if (!existsSync3(gitignorePath)) {
2377
2478
  const body = [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n";
2378
- writeFileSync3(gitignorePath, body, "utf-8");
2479
+ writeFileSync4(gitignorePath, body, "utf-8");
2379
2480
  return "written";
2380
2481
  }
2381
2482
  const existing = readFileSync4(gitignorePath, "utf-8");
@@ -2386,7 +2487,7 @@ function reconcileGitignore(targetRepo) {
2386
2487
  const suffix = (existing.endsWith("\n") ? "" : "\n") + `
2387
2488
  ${header}
2388
2489
  ` + toAdd.join("\n") + "\n";
2389
- writeFileSync3(gitignorePath, existing + suffix, "utf-8");
2490
+ writeFileSync4(gitignorePath, existing + suffix, "utf-8");
2390
2491
  return "written";
2391
2492
  } catch {
2392
2493
  return "failed";
@@ -2494,12 +2595,9 @@ function checkDrift(targetRepoRoot) {
2494
2595
  return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
2495
2596
  }
2496
2597
  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)
2598
+ if (existsSync3(join5(targetRepo, PROJECT_IDENTITY_REL)))
2501
2599
  return "skipped";
2502
- writeFileSync3(path, serialized, "utf-8");
2600
+ createProjectIdentityFile(targetRepo);
2503
2601
  return "written";
2504
2602
  }
2505
2603
  function writeEngagementCredential(credential, engagementId, home) {
@@ -2562,10 +2660,10 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
2562
2660
  const substantive = kept.filter((line) => line.trim() !== "" && !line.trim().startsWith("# Halfcycle"));
2563
2661
  try {
2564
2662
  if (substantive.length === 0) {
2565
- rmSync(path);
2663
+ rmSync2(path);
2566
2664
  return { outcome: "removed", unbacked: [], missingForAdoption: [] };
2567
2665
  }
2568
- writeFileSync3(path, kept.join("\n"), "utf-8");
2666
+ writeFileSync4(path, kept.join("\n"), "utf-8");
2569
2667
  return { outcome: "stripped", unbacked: [], missingForAdoption: [] };
2570
2668
  } catch {
2571
2669
  return { outcome: "failed", unbacked: [], missingForAdoption: [] };
@@ -2575,7 +2673,7 @@ async function install(options) {
2575
2673
  const { targetRepo, engagementId, engagementType, credential, home, accountId } = options;
2576
2674
  assertEngagementId(engagementId);
2577
2675
  if (!existsSync3(targetRepo)) {
2578
- throw new Error(`[bundle install] Target repo does not exist: ${targetRepo}`);
2676
+ throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
2579
2677
  }
2580
2678
  const manifest = readPluginManifest();
2581
2679
  const report = {
@@ -2589,17 +2687,6 @@ async function install(options) {
2589
2687
  const draft = startManifest(readPreviousManifest(targetRepo), (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
2590
2688
  const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
2591
2689
  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
- }
2603
2690
  const vendoredBinSrc = resolveVendoredBinary();
2604
2691
  const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
2605
2692
  const vendoredOutcome = recordOwned(report, vendoredBinDest, targetRepo, readFileSync4(vendoredBinSrc, "utf-8"), VENDORED_BIN_REL);
@@ -2607,21 +2694,20 @@ async function install(options) {
2607
2694
  const settingsPath = join5(targetRepo, SETTINGS_REL);
2608
2695
  const settingsPreexisted = existsSync3(settingsPath);
2609
2696
  const generatedSettings = JSON.parse(generateSettingsJson());
2610
- const existingSettings = settingsPreexisted ? JSON.parse(readFileSync4(settingsPath, "utf-8")) : {};
2697
+ let existingSettings;
2698
+ try {
2699
+ existingSettings = readExistingSettings(targetRepo) ?? {};
2700
+ } catch (err) {
2701
+ throw new SettingsParseRefused(settingsParseRefusal(targetRepo, err));
2702
+ }
2611
2703
  recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted);
2612
2704
  const mergedSettings = mergeSettings(existingSettings, generatedSettings);
2613
2705
  const mergedSettingsText = JSON.stringify(mergedSettings, null, 2) + "\n";
2614
- mkdirSync3(dirname2(settingsPath), { recursive: true });
2615
- writeFileSync3(settingsPath, mergedSettingsText, "utf-8");
2706
+ mkdirSync4(dirname3(settingsPath), { recursive: true });
2707
+ writeFileSync4(settingsPath, mergedSettingsText, "utf-8");
2616
2708
  (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);
2709
+ for (const [rel, content] of hookScriptsThisVersionWrites()) {
2710
+ const hookPath = join5(targetRepo, rel);
2625
2711
  const outcome = recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
2626
2712
  noteFile(draft, targetRepo, rel, outcome);
2627
2713
  }
@@ -2680,7 +2766,7 @@ async function install(options) {
2680
2766
  const gitignorePath = join5(targetRepo, GITIGNORE_REL);
2681
2767
  const gitignoreBefore = existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null;
2682
2768
  const gitignoreOutcome = reconcileGitignore(targetRepo);
2683
- recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null);
2769
+ recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null, writeSet);
2684
2770
  if (gitignoreOutcome === "failed") {
2685
2771
  report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
2686
2772
  } else {
@@ -2689,7 +2775,7 @@ async function install(options) {
2689
2775
  const gitattributesPath = join5(targetRepo, GITATTRIBUTES_REL);
2690
2776
  const gitattributesBefore = readTextOrNull(gitattributesPath);
2691
2777
  const gitattributesOutcome = reconcileGitattributes(targetRepo);
2692
- recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath));
2778
+ recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath), writeSet);
2693
2779
  if (gitattributesOutcome === "failed") {
2694
2780
  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
2781
  } else {
@@ -2812,16 +2898,22 @@ function planeRefusal(detail) {
2812
2898
  reason: typeof candidate.reason === "string" ? candidate.reason : void 0
2813
2899
  };
2814
2900
  }
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.`;
2901
+ 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
2902
  async function createEngagement(baseUrl, name, credential, derivedName) {
2817
2903
  return requestEngagementValues(
2904
+ "POST",
2818
2905
  `${baseUrl.replace(/\/+$/, "")}/engagements`,
2819
2906
  // A stated name wins over a guess, and the body carries at most one of them —
2820
2907
  // the plane applies the same ordering, so the two cannot disagree about which
2821
2908
  // one a row was named from.
2822
2909
  name ? { name } : derivedName ? { derivedName } : {},
2823
2910
  credential,
2824
- { action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" }
2911
+ {
2912
+ action: "Create-engagement",
2913
+ nothingHappened: "No engagement was created",
2914
+ route: "POST /engagements",
2915
+ alsoNothingWritten: true
2916
+ }
2825
2917
  );
2826
2918
  }
2827
2919
  function withDeclaredTelemetryKey(raw) {
@@ -2830,12 +2922,12 @@ function withDeclaredTelemetryKey(raw) {
2830
2922
  const obj = raw;
2831
2923
  return "controlTelemetryUrl" in obj ? obj : { ...obj, controlTelemetryUrl: "" };
2832
2924
  }
2833
- async function requestEngagementValues(url, requestBody, credential, shape) {
2925
+ async function requestEngagementValues(method, url, requestBody, credential, shape) {
2834
2926
  const bearer = credential?.trim();
2835
2927
  let res;
2836
2928
  try {
2837
2929
  res = await fetch(url, {
2838
- method: "POST",
2930
+ method,
2839
2931
  headers: {
2840
2932
  "content-type": "application/json",
2841
2933
  ...bearer ? { authorization: `Bearer ${bearer}` } : {}
@@ -2843,36 +2935,36 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2843
2935
  body: JSON.stringify(requestBody)
2844
2936
  });
2845
2937
  } 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);
2938
+ 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
2939
  }
2848
2940
  if (!res.ok) {
2849
2941
  const detail = await res.text().catch(() => "");
2850
2942
  const refusal = res.status === 404 ? void 0 : planeRefusal(detail);
2851
2943
  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}.`);
2944
+ throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, refusal.reason, `[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
2853
2945
  }
2854
2946
  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);
2947
+ throw new Error(`[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2856
2948
  }
2857
2949
  const candidate = withDeclaredTelemetryKey(await res.json().catch(() => null));
2858
2950
  const declared = engagementCredentialResponseSchema.safeParse(candidate);
2859
2951
  const body = declared.success ? declared.data : candidate;
2860
2952
  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);
2953
+ throw new Error(`[halfcycle] ${shape.action} response from ${url} did not carry {engagementId, sessionToken}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2862
2954
  }
2863
2955
  for (const [field, value] of [
2864
2956
  ["engagementId", body.engagementId],
2865
2957
  ["sessionToken", body.sessionToken]
2866
2958
  ]) {
2867
2959
  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);
2960
+ 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
2961
  }
2870
2962
  }
2871
2963
  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.`);
2964
+ 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
2965
  }
2874
2966
  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.`);
2967
+ 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
2968
  }
2877
2969
  const controlTelemetryUrl = typeof body.controlTelemetryUrl === "string" && body.controlTelemetryUrl.trim() !== "" ? body.controlTelemetryUrl.trim().replace(/\/+$/, "") : void 0;
2878
2970
  if (!declared.success) {
@@ -2883,7 +2975,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2883
2975
  }
2884
2976
  return issue.path.length > 0 ? issue.path.map(String).join(".") : "(the body itself)";
2885
2977
  }).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.`);
2978
+ 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
2979
  }
2888
2980
  const consoleUrl = typeof body.consoleUrl === "string" && body.consoleUrl.trim() !== "" ? body.consoleUrl.trim() : void 0;
2889
2981
  const methodVersion = typeof body.methodVersion === "string" && body.methodVersion.trim() !== "" ? body.methodVersion.trim() : void 0;
@@ -2899,8 +2991,8 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2899
2991
  }
2900
2992
 
2901
2993
  // 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";
2994
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync5 } from "node:fs";
2995
+ import { dirname as dirname4, join as join6, posix as posix2 } from "node:path";
2904
2996
  var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
2905
2997
  var DECLARED_DIR_PREFIX = "docs";
2906
2998
  var RESERVED_METHOD_PATH = "docs/method";
@@ -3011,8 +3103,8 @@ function applyFileRow(targetRepo, path, content) {
3011
3103
  const current = readFileSync5(abs, "utf-8");
3012
3104
  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
3105
  }
3014
- mkdirSync4(dirname3(abs), { recursive: true });
3015
- writeFileSync4(abs, content, "utf-8");
3106
+ mkdirSync5(dirname4(abs), { recursive: true });
3107
+ writeFileSync5(abs, content, "utf-8");
3016
3108
  return { path, outcome: "created", note: "created" };
3017
3109
  }
3018
3110
  function applySectionRow(targetRepo, path, heading, body) {
@@ -3022,8 +3114,8 @@ function applySectionRow(targetRepo, path, heading, body) {
3022
3114
  return symlinkEntry(path, link, false);
3023
3115
  const section = renderSection(heading, body);
3024
3116
  if (!existsSync4(abs)) {
3025
- mkdirSync4(dirname3(abs), { recursive: true });
3026
- writeFileSync4(abs, section, "utf-8");
3117
+ mkdirSync5(dirname4(abs), { recursive: true });
3118
+ writeFileSync5(abs, section, "utf-8");
3027
3119
  return { path, outcome: "created", note: `created, with a "${heading}" section` };
3028
3120
  }
3029
3121
  const existing = readFileSync5(abs, "utf-8");
@@ -3035,7 +3127,7 @@ function applySectionRow(targetRepo, path, heading, body) {
3035
3127
  };
3036
3128
  }
3037
3129
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
3038
- writeFileSync4(abs, existing + separator + section, "utf-8");
3130
+ writeFileSync5(abs, existing + separator + section, "utf-8");
3039
3131
  return {
3040
3132
  path,
3041
3133
  outcome: "section-added",
@@ -3051,7 +3143,7 @@ function applyDirectoryRow(targetRepo, path) {
3051
3143
  const kind = statSync(abs).isDirectory() ? "directory" : "file";
3052
3144
  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
3145
  }
3054
- mkdirSync4(abs, { recursive: true });
3146
+ mkdirSync5(abs, { recursive: true });
3055
3147
  return { path: `${path}/`, outcome: "created", note: "created" };
3056
3148
  }
3057
3149
  function applySetupWrites(targetRepo, rows) {
@@ -3734,7 +3826,7 @@ function renderBuildRecordV2Markdown(record2) {
3734
3826
  }
3735
3827
 
3736
3828
  // 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";
3829
+ import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync3, writeSync } from "node:fs";
3738
3830
  import { basename, join as join8, resolve, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
3739
3831
  var BUILD_RECORD_DIR = join8("docs", "build-records");
3740
3832
  function assertInsideOutDir(outDir, candidate, identity) {
@@ -3750,7 +3842,7 @@ function createExclusiveOnDisk(path, text) {
3750
3842
  writeSync(fd, text);
3751
3843
  } catch (err) {
3752
3844
  closeSync(fd);
3753
- rmSync2(path, { force: true });
3845
+ rmSync3(path, { force: true });
3754
3846
  throw err;
3755
3847
  }
3756
3848
  closeSync(fd);
@@ -3814,7 +3906,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
3814
3906
  }
3815
3907
  const jsonText = serialiseServedRecord(rawRecord);
3816
3908
  const mdText = renderBuildRecordV2Markdown(record2);
3817
- mkdirSync5(outDir, { recursive: true });
3909
+ mkdirSync6(outDir, { recursive: true });
3818
3910
  let createdJson = false;
3819
3911
  try {
3820
3912
  io.createExclusive(jsonPath, jsonText);
@@ -3823,7 +3915,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
3823
3915
  return { kind: "created", jsonPath, mdPath };
3824
3916
  } catch (err) {
3825
3917
  if (createdJson)
3826
- rmSync2(jsonPath, { force: true });
3918
+ rmSync3(jsonPath, { force: true });
3827
3919
  if (!isEexist(err))
3828
3920
  throw err;
3829
3921
  }
@@ -3845,19 +3937,19 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
3845
3937
  try {
3846
3938
  replaceOne(io, jsonTmp, jsonPath, existingJson);
3847
3939
  } catch {
3848
- rmSync2(jsonPath, { force: true });
3940
+ rmSync3(jsonPath, { force: true });
3849
3941
  }
3850
3942
  throw err;
3851
3943
  }
3852
3944
  return { kind: "replaced", jsonPath, mdPath };
3853
3945
  }
3854
3946
  function replaceOne(io, tmp, finalPath, text) {
3855
- rmSync2(tmp, { force: true });
3947
+ rmSync3(tmp, { force: true });
3856
3948
  try {
3857
3949
  io.createExclusive(tmp, text);
3858
3950
  io.rename(tmp, finalPath);
3859
3951
  } catch (err) {
3860
- rmSync2(tmp, { force: true });
3952
+ rmSync3(tmp, { force: true });
3861
3953
  throw err;
3862
3954
  }
3863
3955
  }