halfcycle 0.3.12 → 0.3.14

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/bin.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // dist/bin.js
4
- import { execFileSync as execFileSync3 } from "node:child_process";
5
- import { readFileSync as readFileSync8 } from "node:fs";
6
- import { join as join9 } from "node:path";
4
+ import { execFileSync as execFileSync4 } from "node:child_process";
5
+ import { readFileSync as readFileSync9 } from "node:fs";
6
+ import { join as join10 } from "node:path";
7
7
 
8
8
  // dist/install.js
9
9
  import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
@@ -704,6 +704,13 @@ function readRootCommit(targetRepoRoot) {
704
704
  function readRemote(targetRepoRoot) {
705
705
  return git(targetRepoRoot, ["remote", "get-url", "origin"]);
706
706
  }
707
+ function readCommitCount(targetRepoRoot) {
708
+ const out = git(targetRepoRoot, ["rev-list", "--count", "HEAD"]);
709
+ if (out === null)
710
+ return null;
711
+ const count = Number.parseInt(out, 10);
712
+ return Number.isFinite(count) ? count : null;
713
+ }
707
714
  function mintOrReadIdentity(targetRepoRoot) {
708
715
  const path = join3(targetRepoRoot, ".halfcycle", "project.json");
709
716
  if (existsSync(path)) {
@@ -1618,7 +1625,7 @@ function resolveControlOrigin(env = process.env) {
1618
1625
  return { origin: DEFAULT_CONTROL_ORIGIN, source: "default" };
1619
1626
  }
1620
1627
  function controlOriginNote(resolved) {
1621
- return resolved.source === "environment" ? `${resolved.origin} (from HALFCYCLE_SERVICE_URL)` : `${resolved.origin} (the Halfcycle plane)`;
1628
+ return resolved.source === "environment" ? `${resolved.origin} (from HALFCYCLE_SERVICE_URL)` : `${resolved.origin} (the published one)`;
1622
1629
  }
1623
1630
 
1624
1631
  // dist/device-signin.js
@@ -1705,14 +1712,14 @@ async function bindLoopback(env = process.env) {
1705
1712
  else
1706
1713
  pending = callback;
1707
1714
  }));
1708
- const bindProblem = await new Promise((resolve3) => {
1715
+ const bindProblem = await new Promise((resolve4) => {
1709
1716
  const onError = (err) => {
1710
- resolve3(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
1717
+ resolve4(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
1711
1718
  };
1712
1719
  server.once("error", onError);
1713
1720
  server.listen({ host: LOOPBACK_REDIRECT_HOST, port: wanted.port }, () => {
1714
1721
  server.removeListener("error", onError);
1715
- resolve3(null);
1722
+ resolve4(null);
1716
1723
  });
1717
1724
  });
1718
1725
  if (bindProblem !== null) {
@@ -1728,9 +1735,9 @@ async function bindLoopback(env = process.env) {
1728
1735
  bound: true,
1729
1736
  target: { port: address.port, state },
1730
1737
  boundAddress: address.address,
1731
- waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve3) => {
1738
+ waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve4) => {
1732
1739
  if (pending !== null) {
1733
- resolve3(pending);
1740
+ resolve4(pending);
1734
1741
  return;
1735
1742
  }
1736
1743
  const startedAt = Date.now();
@@ -1740,7 +1747,7 @@ async function bindLoopback(env = process.env) {
1740
1747
  clearInterval(ticker);
1741
1748
  clearTimeout(timer);
1742
1749
  deliver = null;
1743
- resolve3(value);
1750
+ resolve4(value);
1744
1751
  };
1745
1752
  const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
1746
1753
  deliver = finish;
@@ -1749,9 +1756,9 @@ async function bindLoopback(env = process.env) {
1749
1756
  };
1750
1757
  }
1751
1758
  function closeServer(server) {
1752
- return new Promise((resolve3) => {
1759
+ return new Promise((resolve4) => {
1753
1760
  server.closeAllConnections();
1754
- server.close(() => resolve3());
1761
+ server.close(() => resolve4());
1755
1762
  });
1756
1763
  }
1757
1764
 
@@ -1765,7 +1772,7 @@ var SignInRefused = class extends Error {
1765
1772
  }
1766
1773
  };
1767
1774
  var SIGN_IN_TIMED_OUT = "timed-out";
1768
- var SIGN_IN_TIMEOUT_MESSAGE = "Sign-in timed out: the Halfcycle plane stopped answering while this terminal was waiting for the browser. Nothing was created. Check your connection and run the command again.";
1775
+ var SIGN_IN_TIMEOUT_MESSAGE = "Sign-in timed out: the Halfcycle service stopped answering while this terminal was waiting for the browser. Nothing was created. Check your connection and run the command again.";
1769
1776
  var SIGN_IN_NON_INTERACTIVE_MESSAGE = "Halfcycle needs a signed-in account and this looks like a non-interactive run (CI is set), so there is no browser to sign in with. Set HALFCYCLE_TOKEN in the environment to a Halfcycle credential and run this again. Nothing was created.";
1770
1777
  var DEADLINE_SLACK_INTERVALS = 1;
1771
1778
  var STILL_WAITING_MS = 15e3;
@@ -1773,7 +1780,7 @@ function defaultWrite(text) {
1773
1780
  process.stdout.write(text);
1774
1781
  }
1775
1782
  function defaultSleep(ms) {
1776
- return new Promise((resolve3) => setTimeout(resolve3, ms));
1783
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1777
1784
  }
1778
1785
  function browserOpenCommand(os, url) {
1779
1786
  if (os === "darwin")
@@ -1799,12 +1806,12 @@ async function openBrowser(url, env = process.env) {
1799
1806
  return { opened: false, reason: "the sign-in address is not an http(s) URL this CLI will open" };
1800
1807
  }
1801
1808
  const { command, args: args2 } = browserOpenCommand(platform3(), url);
1802
- return new Promise((resolve3) => {
1809
+ return new Promise((resolve4) => {
1803
1810
  let child;
1804
1811
  try {
1805
1812
  child = spawn(command, args2, { stdio: "ignore", env });
1806
1813
  } catch (err) {
1807
- resolve3({
1814
+ resolve4({
1808
1815
  opened: false,
1809
1816
  reason: `${command} could not be run (${err instanceof Error ? err.message : String(err)})`
1810
1817
  });
@@ -1816,7 +1823,7 @@ async function openBrowser(url, env = process.env) {
1816
1823
  return;
1817
1824
  settled = true;
1818
1825
  clearTimeout(timer);
1819
- resolve3(attempt);
1826
+ resolve4(attempt);
1820
1827
  };
1821
1828
  const timer = setTimeout(() => {
1822
1829
  child.unref();
@@ -1852,7 +1859,7 @@ async function startDeviceAuth(serviceUrl) {
1852
1859
  body: "{}"
1853
1860
  });
1854
1861
  } catch (err) {
1855
- throw new SignInRefused(SIGN_IN_TIMED_OUT, `Could not reach the Halfcycle plane at ${url} to start a sign-in: ${err instanceof Error ? err.message : String(err)}. Nothing was created.`);
1862
+ throw new SignInRefused(SIGN_IN_TIMED_OUT, `Could not reach the Halfcycle service at ${url} to start a sign-in: ${err instanceof Error ? err.message : String(err)}. Nothing was created.`);
1856
1863
  }
1857
1864
  const body = await res.json().catch(() => null);
1858
1865
  if (res.status !== 201) {
@@ -1861,7 +1868,7 @@ async function startDeviceAuth(serviceUrl) {
1861
1868
  }
1862
1869
  const parsed = deviceAuthStartResponseSchema.safeParse(body);
1863
1870
  if (!parsed.success) {
1864
- throw new SignInRefused(SIGN_IN_TIMED_OUT, `The Halfcycle plane at ${url} answered a sign-in start this client cannot read. Either that plane is older than this installer, or the address is not a Halfcycle control plane. Nothing was created.`);
1871
+ throw new SignInRefused(SIGN_IN_TIMED_OUT, `The Halfcycle service at ${url} answered a sign-in start this client cannot read. Either that service is older than this installer, or the address is not a Halfcycle service. Nothing was created.`);
1865
1872
  }
1866
1873
  return parsed.data;
1867
1874
  }
@@ -1936,7 +1943,7 @@ async function collectDecidedCredential(serviceUrl, started, deps) {
1936
1943
  });
1937
1944
  body = await res.json().catch(() => null);
1938
1945
  } catch (err) {
1939
- lastProblem = `The browser finished signing you in, but this terminal could not reach the Halfcycle plane to collect the credential: ${err instanceof Error ? err.message : String(err)}. Run the command again.`;
1946
+ lastProblem = `The browser finished signing you in, but this terminal could not reach the Halfcycle service to collect the credential: ${err instanceof Error ? err.message : String(err)}. Run the command again.`;
1940
1947
  continue;
1941
1948
  }
1942
1949
  const parsed = devicePollResponseSchema.safeParse(body);
@@ -2125,7 +2132,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2125
2132
  body: JSON.stringify(requestBody)
2126
2133
  });
2127
2134
  } catch (err) {
2128
- throw new Error(`[bundle install] Could not reach the Halfcycle control plane at ${url}: ${err instanceof Error ? err.message : String(err)}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2135
+ 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);
2129
2136
  }
2130
2137
  if (!res.ok) {
2131
2138
  const detail = await res.text().catch(() => "");
@@ -2133,7 +2140,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2133
2140
  if (refusal) {
2134
2141
  throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, `[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
2135
2142
  }
2136
- const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the control plane. ` : "";
2143
+ const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
2137
2144
  throw new Error(`[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
2138
2145
  }
2139
2146
  const body = await res.json().catch(() => null);
@@ -2145,14 +2152,14 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2145
2152
  ["sessionToken", body.sessionToken]
2146
2153
  ]) {
2147
2154
  if (value.trim() === "") {
2148
- throw new Error(`[bundle install] The Halfcycle plane 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);
2155
+ 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);
2149
2156
  }
2150
2157
  }
2151
2158
  if (typeof body.mcpUrl !== "string" || body.mcpUrl.trim() === "") {
2152
- throw new Error(`[bundle install] The Halfcycle plane 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 plane is older than this installer, or ${url} is not a Halfcycle control plane.`);
2159
+ 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.`);
2153
2160
  }
2154
2161
  if (typeof body.guardUrl !== "string" || body.guardUrl.trim() === "") {
2155
- throw new Error(`[bundle install] The Halfcycle plane 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 plane is older than this installer, or ${url} is not a Halfcycle control plane.`);
2162
+ 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.`);
2156
2163
  }
2157
2164
  const controlTelemetryUrl = typeof body.controlTelemetryUrl === "string" && body.controlTelemetryUrl.trim() !== "" ? body.controlTelemetryUrl.trim().replace(/\/+$/, "") : void 0;
2158
2165
  return {
@@ -2211,12 +2218,284 @@ function refusedCredentialRemedy(source) {
2211
2218
  case "stored":
2212
2219
  return "That was this machine's saved sign-in and it has been discarded. Run the command again to sign in through your browser.";
2213
2220
  case "browser-handoff":
2214
- return "That credential was issued by this sign-in moments ago, so signing in again would produce the same result. This is a problem on the Halfcycle plane rather than with your account.";
2221
+ return "That credential was issued by this sign-in moments ago, so signing in again would produce the same result. This is a problem on the Halfcycle service rather than with your account.";
2215
2222
  }
2216
2223
  }
2217
2224
 
2218
2225
  // dist/confirm-identity.js
2219
2226
  import { createInterface } from "node:readline/promises";
2227
+
2228
+ // dist/banner.js
2229
+ var MARK_FULL = {
2230
+ width: 32,
2231
+ discRows: 7,
2232
+ lines: [
2233
+ " :+oshhhhso+:",
2234
+ " :sdMMMMMMMMMMMMds:",
2235
+ " :hMMMMMMMMMMMMMMMMMMh:",
2236
+ " oMMMMMMMMMMMMMMMMMMMMMMo",
2237
+ " oMMMMMMMMMMMMMMMMMMMMMMMMo",
2238
+ " NMMMMMMMMMMMMMMMMMMMMMMMMN",
2239
+ " :ssssssssssssssssssssssssss:",
2240
+ " +dddddddddddddddddddddddddd+",
2241
+ " Nd dN",
2242
+ " oMy yMo",
2243
+ " oMh: :hMo",
2244
+ " :hMy- -yMh:",
2245
+ " :sdNys++--++syNds:",
2246
+ " :+oshhhhso+:"
2247
+ ]
2248
+ };
2249
+ var MARK_NARROW = {
2250
+ width: 24,
2251
+ discRows: 5,
2252
+ lines: [
2253
+ " -ohdMMMMdho-",
2254
+ " +dMMMMMMMMMMMMd+",
2255
+ " yMMMMMMMMMMMMMMMMy",
2256
+ " sMMMMMMMMMMMMMMMMMMs",
2257
+ " yhhhhhhhhhhhhhhhhhhy",
2258
+ " NdhhhhhhhhhhhhhhhhdN",
2259
+ " sd ds",
2260
+ " yd: :dy",
2261
+ " +dy+ +yd+",
2262
+ " -ohhhhhhhho-"
2263
+ ]
2264
+ };
2265
+ var UNICODE_GLYPHS = {
2266
+ rule: "\u2500",
2267
+ swatch: "\u2584",
2268
+ dot: "\xB7",
2269
+ dash: "\u2014"
2270
+ };
2271
+ var ASCII_GLYPHS = {
2272
+ rule: "-",
2273
+ swatch: "#",
2274
+ dot: "|",
2275
+ dash: "-"
2276
+ };
2277
+ function toAscii(text) {
2278
+ let out = text;
2279
+ for (const key of Object.keys(UNICODE_GLYPHS)) {
2280
+ out = out.split(UNICODE_GLYPHS[key]).join(ASCII_GLYPHS[key]);
2281
+ }
2282
+ return out.replace(/[^ -~]/g, "?");
2283
+ }
2284
+ var INK = {
2285
+ /** `--text` — the terminal's own foreground, never one we picked. */
2286
+ plain: "",
2287
+ /** `--muted` — dimmed default ink, so it is grey against whatever the background is. */
2288
+ dim: "2",
2289
+ /** `--faint` / `--faded` — bright black, the one ANSI grey that is a colour. */
2290
+ faint: "90",
2291
+ /** Not a colour: emphasis, used with whatever ink is already in force. */
2292
+ bold: "1",
2293
+ /** `--signal` — yellow, the nearest ANSI seat for the brand's gold. */
2294
+ key: "33",
2295
+ /** `--signal`, brighter: the one value on a screen that must be found first. */
2296
+ keyBold: "33;1",
2297
+ /** `--good` — the console's affirmative. */
2298
+ good: "32",
2299
+ /** `--bad` — the console's negative. */
2300
+ bad: "31"
2301
+ };
2302
+ var SWATCH_SGR = [
2303
+ INK.dim,
2304
+ INK.faint,
2305
+ INK.plain,
2306
+ INK.key,
2307
+ INK.keyBold,
2308
+ INK.good,
2309
+ INK.bad
2310
+ ];
2311
+ var ESC = "\x1B";
2312
+ var RESET = `${ESC}[0m`;
2313
+ function paint(segments, colour) {
2314
+ return segments.map((s) => colour && s.sgr !== "" ? `${ESC}[${s.sgr}m${s.text}${RESET}` : s.text).join("");
2315
+ }
2316
+ function ink(name, text, colour) {
2317
+ const sgr = INK[name];
2318
+ return colour && sgr !== "" ? `${ESC}[${sgr}m${text}${RESET}` : text;
2319
+ }
2320
+ var GAP = 4;
2321
+ var FULL_LAYOUT = { art: MARK_FULL, label: 13, rule: 28, palette: true, minColumns: 76 };
2322
+ var NARROW_LAYOUT = { art: MARK_NARROW, label: 10, rule: 20, palette: false, minColumns: 48 };
2323
+ var DEFAULT_COLUMNS = 80;
2324
+ function usableColumns(environment) {
2325
+ const columns = environment.columns;
2326
+ return columns !== void 0 && columns > 0 ? columns : DEFAULT_COLUMNS;
2327
+ }
2328
+ function isSet(value) {
2329
+ return value !== void 0 && value !== "";
2330
+ }
2331
+ function plainTextRequested(env) {
2332
+ return isSet(env["NO_COLOR"]) || env["TERM"] === "dumb";
2333
+ }
2334
+ function decideCut(environment = {}) {
2335
+ const env = environment.env ?? {};
2336
+ if (environment.quiet === true)
2337
+ return "none";
2338
+ if (environment.isTTY !== true)
2339
+ return "line";
2340
+ if (plainTextRequested(env))
2341
+ return "line";
2342
+ const columns = usableColumns(environment);
2343
+ if (columns >= FULL_LAYOUT.minColumns)
2344
+ return "full";
2345
+ if (columns >= NARROW_LAYOUT.minColumns)
2346
+ return "narrow";
2347
+ return "line";
2348
+ }
2349
+ function decideColour(environment = {}) {
2350
+ const env = environment.env ?? {};
2351
+ const force = env["FORCE_COLOR"];
2352
+ if (isSet(force))
2353
+ return force !== "0";
2354
+ if (plainTextRequested(env))
2355
+ return false;
2356
+ return environment.isTTY === true;
2357
+ }
2358
+ function decideUnicode(environment = {}) {
2359
+ const env = environment.env ?? {};
2360
+ if ((environment.platform ?? "") !== "win32") {
2361
+ const locale = env["LC_ALL"] ?? env["LC_CTYPE"] ?? env["LANG"] ?? "";
2362
+ return locale === "" || /utf-?8/i.test(locale);
2363
+ }
2364
+ return isSet(env["WT_SESSION"]) || isSet(env["TERMINUS_SUBLIME"]) || env["ConEmuTask"] === "{cmd::Cmder}" || env["TERM_PROGRAM"] === "vscode";
2365
+ }
2366
+ function padEnd(text, width) {
2367
+ return text + " ".repeat(Math.max(0, width - text.length));
2368
+ }
2369
+ function clip(text, width) {
2370
+ if (width < 8 || text.length <= width)
2371
+ return text;
2372
+ return `${text.slice(0, width - 3)}...`;
2373
+ }
2374
+ function paletteRow(swatch) {
2375
+ const row = [];
2376
+ SWATCH_SGR.forEach((sgr, i) => {
2377
+ if (i > 0)
2378
+ row.push({ text: " ", sgr: INK.plain });
2379
+ row.push({ text: swatch.repeat(2), sgr });
2380
+ });
2381
+ return row;
2382
+ }
2383
+ function factColumn(content, layout, glyphs, text, valueWidth) {
2384
+ const rows = [];
2385
+ const head = text(content.head);
2386
+ const at = head.indexOf("@");
2387
+ rows.push(at < 0 ? [{ text: head, sgr: INK.keyBold }] : [
2388
+ { text: head.slice(0, at), sgr: INK.keyBold },
2389
+ { text: "@", sgr: INK.dim },
2390
+ { text: head.slice(at + 1), sgr: INK.bold }
2391
+ ]);
2392
+ rows.push([{ text: glyphs.rule.repeat(layout.rule), sgr: INK.dim }]);
2393
+ const narrow = layout.art === MARK_NARROW;
2394
+ for (const fact2 of content.facts) {
2395
+ const raw = narrow ? fact2.narrow : fact2.value;
2396
+ if (raw === void 0)
2397
+ continue;
2398
+ rows.push([
2399
+ { text: padEnd(`${text(fact2.label)}:`, layout.label), sgr: INK.key },
2400
+ { text: clip(text(raw), valueWidth), sgr: INK.plain }
2401
+ ]);
2402
+ }
2403
+ if (layout.palette) {
2404
+ rows.push([]);
2405
+ rows.push(paletteRow(glyphs.swatch));
2406
+ }
2407
+ return rows;
2408
+ }
2409
+ var MARGIN = 2;
2410
+ function lastSentenceStart(line) {
2411
+ const at = line.lastIndexOf(". ");
2412
+ return at < 0 ? 0 : at + 2;
2413
+ }
2414
+ function taglineRow(line, first, last) {
2415
+ const indent = { text: " ", sgr: INK.plain };
2416
+ if (first)
2417
+ return [indent, { text: line, sgr: INK.keyBold }];
2418
+ if (!last)
2419
+ return [indent, { text: line, sgr: INK.dim }];
2420
+ const at = lastSentenceStart(line);
2421
+ if (at === 0)
2422
+ return [indent, { text: line, sgr: INK.key }];
2423
+ return [
2424
+ indent,
2425
+ { text: line.slice(0, at), sgr: INK.dim },
2426
+ { text: line.slice(at), sgr: INK.key }
2427
+ ];
2428
+ }
2429
+ function renderBlock(content, layout, environment) {
2430
+ const unicode = decideUnicode(environment);
2431
+ const colour = decideColour(environment);
2432
+ const glyphs = unicode ? UNICODE_GLYPHS : ASCII_GLYPHS;
2433
+ const text = (raw) => unicode ? raw : toAscii(raw);
2434
+ const art = layout.art;
2435
+ const valueWidth = usableColumns(environment) - (art.width + GAP + layout.label);
2436
+ const right = factColumn(content, layout, glyphs, text, valueWidth);
2437
+ const top = Math.max(0, Math.floor((art.lines.length - right.length) / 2));
2438
+ const height = Math.max(art.lines.length, right.length + top);
2439
+ const lines = Array.from({ length: MARGIN }, () => []);
2440
+ for (let i = 0; i < height; i++) {
2441
+ const artLine = art.lines[i] ?? "";
2442
+ const segments = [];
2443
+ if (artLine.length > 0) {
2444
+ segments.push({ text: artLine, sgr: i < art.discRows ? INK.plain : INK.dim });
2445
+ }
2446
+ const rightRow = right[i - top];
2447
+ if (rightRow !== void 0 && rightRow.length > 0) {
2448
+ segments.push({ text: " ".repeat(Math.max(0, art.width + GAP - artLine.length)), sgr: INK.plain });
2449
+ segments.push(...rightRow);
2450
+ }
2451
+ lines.push(segments);
2452
+ }
2453
+ const tagline = layout.art === MARK_NARROW ? content.tagNarrow ?? content.tag : content.tag;
2454
+ lines.push([]);
2455
+ tagline.forEach((line, i) => {
2456
+ lines.push(taglineRow(text(line), i === 0, i === tagline.length - 1));
2457
+ });
2458
+ for (let i = 0; i < MARGIN; i++)
2459
+ lines.push([]);
2460
+ return lines.map((segments) => paint(segments, colour)).join("\n");
2461
+ }
2462
+ function renderLine(content, environment) {
2463
+ const unicode = decideUnicode(environment);
2464
+ const dot = unicode ? UNICODE_GLYPHS.dot : ASCII_GLYPHS.dot;
2465
+ const named = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
2466
+ const raw = `${named} ${dot} ${content.url}`;
2467
+ return unicode ? raw : toAscii(raw);
2468
+ }
2469
+ function renderBanner(content, environment = {}) {
2470
+ switch (decideCut(environment)) {
2471
+ case "none":
2472
+ return "";
2473
+ case "line":
2474
+ return renderLine(content, environment);
2475
+ case "narrow":
2476
+ return renderBlock(content, NARROW_LAYOUT, environment);
2477
+ case "full":
2478
+ return renderBlock(content, FULL_LAYOUT, environment);
2479
+ }
2480
+ }
2481
+ function printBanner(sink, content, environment = {}) {
2482
+ const text = renderBanner(content, environment);
2483
+ if (text === "")
2484
+ return;
2485
+ sink.write(`${text}
2486
+ `);
2487
+ }
2488
+ function currentEnvironment(quiet2) {
2489
+ return {
2490
+ columns: process.stdout.columns,
2491
+ isTTY: process.stdout.isTTY === true,
2492
+ quiet: quiet2,
2493
+ env: process.env,
2494
+ platform: process.platform
2495
+ };
2496
+ }
2497
+
2498
+ // dist/confirm-identity.js
2220
2499
  var IDENTITY_TIMEOUT_MS = 5e3;
2221
2500
  var IDENTITY_DECLINED = "identity-declined";
2222
2501
  async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
@@ -2238,14 +2517,14 @@ async function describeAccount(serviceUrl, credential, fetchImpl = fetch) {
2238
2517
  clearTimeout(timer);
2239
2518
  }
2240
2519
  }
2241
- function accountLabel(identity, storedAccountId) {
2520
+ function accountLabel(identity, storedAccountId, verbose2 = false) {
2242
2521
  const accountId = identity?.accountId ?? storedAccountId;
2243
2522
  if (identity?.email !== void 0 && identity.email.trim() !== "") {
2244
- return accountId !== void 0 ? `${identity.email} (account ${accountId})` : identity.email;
2523
+ return verbose2 && accountId !== void 0 ? `${identity.email} (account ${accountId})` : identity.email;
2245
2524
  }
2246
2525
  if (accountId !== void 0)
2247
2526
  return `account ${accountId} (no email on record for it)`;
2248
- return "the account this credential belongs to, which this plane did not name";
2527
+ return "the account this credential belongs to, which this service did not name";
2249
2528
  }
2250
2529
  function onlyDate(iso) {
2251
2530
  if (iso === void 0)
@@ -2256,10 +2535,10 @@ function onlyDate(iso) {
2256
2535
  function actClause(act) {
2257
2536
  return act.kind === "create" ? `create a NEW project for that account, and install into ${act.targetRepo}` : `join the project this repository is pinned to (${act.engagementId}) as that account, and install into ${act.targetRepo}`;
2258
2537
  }
2259
- async function defaultAsk(question) {
2538
+ async function defaultAsk(question2) {
2260
2539
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2261
2540
  try {
2262
- return await rl.question(question);
2541
+ return await rl.question(question2);
2263
2542
  } finally {
2264
2543
  rl.close();
2265
2544
  process.stdin.pause();
@@ -2277,7 +2556,10 @@ function actingAs(credential, identity) {
2277
2556
  const accountId = identity?.accountId ?? credential.accountId;
2278
2557
  return accountId === void 0 ? credential : { ...credential, accountId };
2279
2558
  }
2280
- function armScript(credential, act, label, home) {
2559
+ function question(colour) {
2560
+ return `[halfcycle] Continue as that account? ${ink("good", "[y]", colour)} yes ${ink("key", "[s]", colour)} sign in as someone else ${ink("faint", "[anything else]", colour)} stop: `;
2561
+ }
2562
+ function armScript(credential, act, label, plainLabel, home) {
2281
2563
  switch (credential.source) {
2282
2564
  case "browser-handoff":
2283
2565
  return {
@@ -2303,7 +2585,7 @@ function armScript(credential, act, label, home) {
2303
2585
  noTerminalNote: "",
2304
2586
  switchOpening: `[halfcycle] Signing you in through your browser \u2014 this install will use that account instead.
2305
2587
  `,
2306
- stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. HALFCYCLE_TOKEN in this environment is the credential for ${label}, and it takes precedence over any saved sign-in \u2014 so unset it, or set it to your own account's credential, before running this again.`,
2588
+ stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. HALFCYCLE_TOKEN in this environment is the credential for ${plainLabel}, and it takes precedence over any saved sign-in \u2014 so unset it, or set it to your own account's credential, before running this again.`,
2307
2589
  // The switch is real for THIS run (the new credential is what gets used) and it
2308
2590
  // does not persist: `obtainCredential` reads the variable first, so the next run
2309
2591
  // is the same variable again. Said here rather than discovered tomorrow.
@@ -2329,7 +2611,7 @@ function armScript(credential, act, label, home) {
2329
2611
  `,
2330
2612
  switchOpening: `[halfcycle] Forgetting this machine's saved sign-in and signing you in through your browser.
2331
2613
  `,
2332
- stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. This machine's saved sign-in is for ${label}. Run this again and answer "s" to sign in as a different account, or remove ${accountStorePath(home)} to be asked for a sign-in next time.`,
2614
+ stopRemedy: `Stopped before anything was created \u2014 nothing was created and nothing was written. This machine's saved sign-in is for ${plainLabel}. Run this again and answer "s" to sign in as a different account, or remove ${accountStorePath(home)} to be asked for a sign-in next time.`,
2333
2615
  afterSwitch: ""
2334
2616
  };
2335
2617
  }
@@ -2337,10 +2619,12 @@ function armScript(credential, act, label, home) {
2337
2619
  }
2338
2620
  async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
2339
2621
  const write = deps.write ?? ((text) => process.stdout.write(text));
2622
+ const colour = deps.colour ?? decideColour({ isTTY: process.stdout.isTTY === true, env: deps.env ?? process.env });
2340
2623
  const identity = await describeAccount(serviceUrl, credential.credential, deps.fetchImpl);
2341
- const label = accountLabel(identity, credential.accountId);
2624
+ const plainLabel = accountLabel(identity, credential.accountId, deps.verbose === true);
2625
+ const label = ink("keyBold", plainLabel, colour);
2342
2626
  const acting = actingAs(credential, identity);
2343
- const script = armScript(credential, act, label, deps.home);
2627
+ const script = armScript(credential, act, label, plainLabel, deps.home);
2344
2628
  write(script.preamble);
2345
2629
  if (!script.asks)
2346
2630
  return acting;
@@ -2356,14 +2640,15 @@ async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
2356
2640
  return acting;
2357
2641
  }
2358
2642
  const ask = deps.ask ?? defaultAsk;
2359
- const decision = readDecision(await ask(`[halfcycle] Continue as that account? [y] yes [s] sign in as someone else [anything else] stop: `));
2643
+ write("\n");
2644
+ const decision = readDecision(await ask(question(colour)));
2360
2645
  if (decision === "proceed")
2361
2646
  return acting;
2362
2647
  if (decision === "switch-account") {
2363
2648
  write(script.switchOpening);
2364
2649
  const replacement = await switchAccount(serviceUrl, deps);
2365
2650
  const replacementIdentity = await describeAccount(serviceUrl, replacement.credential, deps.fetchImpl);
2366
- write(`[halfcycle] Signed in as ${accountLabel(replacementIdentity, replacement.accountId)}. This install will ${actClause(act)}.
2651
+ write(`[halfcycle] Signed in as ${ink("keyBold", accountLabel(replacementIdentity, replacement.accountId, deps.verbose === true), colour)}. This install will ${actClause(act)}.
2367
2652
  `);
2368
2653
  if (script.afterSwitch !== "")
2369
2654
  write(script.afterSwitch);
@@ -2875,7 +3160,7 @@ async function openPhase(credential, phase, entry) {
2875
3160
  body: JSON.stringify({ phase, entry })
2876
3161
  });
2877
3162
  } catch (err) {
2878
- throw new Error(`[halfcycle] Could not reach the Halfcycle control plane at ${url}: ${err instanceof Error ? err.message : String(err)}. No phase was opened.`);
3163
+ throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. No phase was opened.`);
2879
3164
  }
2880
3165
  if (!res.ok) {
2881
3166
  const body2 = await res.json().catch(() => null);
@@ -2928,7 +3213,7 @@ async function closePhase(credential, phase, outcome, close) {
2928
3213
  body: JSON.stringify({ phase, acceptanceOutcome: outcome, close })
2929
3214
  });
2930
3215
  } catch (err) {
2931
- throw new Error(`[halfcycle] Could not reach the Halfcycle control plane at ${url}: ${err instanceof Error ? err.message : String(err)}. The phase was not closed.`);
3216
+ throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. The phase was not closed.`);
2932
3217
  }
2933
3218
  if (!res.ok) {
2934
3219
  const body2 = await res.json().catch(() => null);
@@ -3059,11 +3344,127 @@ var PLACEHOLDERS = {
3059
3344
  "--evidence": '"\u2026"'
3060
3345
  };
3061
3346
 
3347
+ // dist/banner-facts.js
3348
+ import { execFileSync as execFileSync3 } from "node:child_process";
3349
+ import { readFileSync as readFileSync8 } from "node:fs";
3350
+ import { basename as basename2, join as join9, resolve as resolve3 } from "node:path";
3351
+ var BRAND_URL = "halfcycle.ai";
3352
+ var TAGLINE = [
3353
+ "Fewer cycles.",
3354
+ "The thinking moves earlier. So does the finish."
3355
+ ];
3356
+ var TAGLINE_NARROW = [
3357
+ "Fewer cycles.",
3358
+ "The thinking moves earlier.",
3359
+ "So does the finish."
3360
+ ];
3361
+ function bundleVersion() {
3362
+ try {
3363
+ const pkg = JSON.parse(readFileSync8(join9(BUNDLE_ROOT, "package.json"), "utf-8"));
3364
+ return typeof pkg.version === "string" ? pkg.version : void 0;
3365
+ } catch {
3366
+ return void 0;
3367
+ }
3368
+ }
3369
+ function claudeCodeVersion() {
3370
+ try {
3371
+ const raw = execFileSync3("claude", ["--version"], {
3372
+ encoding: "utf-8",
3373
+ timeout: 3e3,
3374
+ stdio: ["ignore", "pipe", "ignore"]
3375
+ });
3376
+ return /(\d+\.\d+\.\d+)/.exec(raw)?.[1];
3377
+ } catch {
3378
+ return void 0;
3379
+ }
3380
+ }
3381
+ var REAL_PROBES = {
3382
+ bundleVersion,
3383
+ claudeCodeVersion,
3384
+ projectName: (dir) => {
3385
+ try {
3386
+ return deriveProjectName(dir);
3387
+ } catch {
3388
+ return void 0;
3389
+ }
3390
+ },
3391
+ commitCount: (dir) => {
3392
+ try {
3393
+ return readCommitCount(dir);
3394
+ } catch {
3395
+ return null;
3396
+ }
3397
+ },
3398
+ installedVersion: (dir) => {
3399
+ try {
3400
+ return readBundlePin(dir)?.version;
3401
+ } catch {
3402
+ return void 0;
3403
+ }
3404
+ },
3405
+ websiteHost: (env) => {
3406
+ try {
3407
+ const resolved = resolveControlOrigin(env);
3408
+ return resolved.source === "default" ? BRAND_URL : new URL(resolved.origin).host;
3409
+ } catch {
3410
+ return void 0;
3411
+ }
3412
+ }
3413
+ };
3414
+ function fact(label, value, narrow) {
3415
+ if (value === void 0 || value === "")
3416
+ return void 0;
3417
+ return narrow === void 0 ? { label, value } : { label, value, narrow };
3418
+ }
3419
+ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBES) {
3420
+ const dot = UNICODE_GLYPHS.dot;
3421
+ const dash = UNICODE_GLYPHS.dash;
3422
+ const version = probes.bundleVersion();
3423
+ const name = probes.projectName(projectDir) ?? safeBasename(projectDir);
3424
+ const commits = probes.commitCount(projectDir);
3425
+ const installed = probes.installedVersion(projectDir);
3426
+ const project = commits === null ? name : `${name} ${dot} git ${dot} ${commits} ${commits === 1 ? "commit" : "commits"}`;
3427
+ const status = installed === void 0 ? `new ${dash} nothing installed yet` : `installed ${dash} v${installed}`;
3428
+ const facts = [
3429
+ fact("Version", version, version),
3430
+ fact("Claude Code", probes.claudeCodeVersion()),
3431
+ fact("Project", project, name),
3432
+ fact("Status", status, installed === void 0 ? "new" : "installed"),
3433
+ // `Website`, NOT `Plane` (W-3, #868). "Plane" is our internal word for the control
3434
+ // plane; the developer reading this has never heard it, and raw method vocabulary
3435
+ // reaching a client surface is a recorded finding class in this product. The row
3436
+ // answers *which Halfcycle is this*, and an address is the answer they recognise.
3437
+ fact("Website", probes.websiteHost(env))
3438
+ ].filter((f) => f !== void 0);
3439
+ return {
3440
+ head: `halfcycle@${name}`,
3441
+ facts,
3442
+ tag: TAGLINE,
3443
+ tagNarrow: TAGLINE_NARROW,
3444
+ url: BRAND_URL,
3445
+ // `''` when even this package's own manifest could not be read. The one-line cut
3446
+ // drops the whole `vX.Y.Z` clause in that case rather than printing a bare `v`.
3447
+ version: version ?? ""
3448
+ };
3449
+ }
3450
+ function safeBasename(dir) {
3451
+ try {
3452
+ const name = basename2(resolve3(dir)).trim();
3453
+ return name === "" ? "project" : name;
3454
+ } catch {
3455
+ return "project";
3456
+ }
3457
+ }
3458
+
3062
3459
  // dist/bin.js
3063
3460
  var ASSUME_YES_FLAGS = /* @__PURE__ */ new Set(["--yes", "-y"]);
3461
+ var QUIET_FLAGS = /* @__PURE__ */ new Set(["--quiet", "-q"]);
3462
+ var VERBOSE_FLAGS = /* @__PURE__ */ new Set(["--verbose", "-v"]);
3064
3463
  var rawArgs = process.argv.slice(2);
3065
3464
  var assumeYes = rawArgs.some((a) => ASSUME_YES_FLAGS.has(a));
3066
- var args = rawArgs.filter((a) => !ASSUME_YES_FLAGS.has(a));
3465
+ var quiet = rawArgs.some((a) => QUIET_FLAGS.has(a));
3466
+ var verbose = rawArgs.some((a) => VERBOSE_FLAGS.has(a));
3467
+ var args = rawArgs.filter((a) => !ASSUME_YES_FLAGS.has(a) && !QUIET_FLAGS.has(a) && !VERBOSE_FLAGS.has(a));
3067
3468
  var MIN_HEADERS_HELPER_VERSION = "2.1.118";
3068
3469
  var MIN_HEADER_ROTATION_VERSION = "2.1.193";
3069
3470
  function compareVersions(a, b) {
@@ -3077,10 +3478,10 @@ function compareVersions(a, b) {
3077
3478
  }
3078
3479
  return 0;
3079
3480
  }
3080
- function reportClaudeCodeVersion() {
3481
+ function reportClaudeCodeVersion(verbose2) {
3081
3482
  let raw;
3082
3483
  try {
3083
- raw = execFileSync3("claude", ["--version"], {
3484
+ raw = execFileSync4("claude", ["--version"], {
3084
3485
  encoding: "utf-8",
3085
3486
  timeout: 5e3,
3086
3487
  stdio: ["ignore", "pipe", "ignore"]
@@ -3093,13 +3494,18 @@ function reportClaudeCodeVersion() {
3093
3494
  return;
3094
3495
  const version = found[1];
3095
3496
  if (compareVersions(version, MIN_HEADERS_HELPER_VERSION) >= 0) {
3096
- process.stdout.write(`[halfcycle] Claude Code ${version} detected \u2014 new enough for the credential helper.
3497
+ if (verbose2) {
3498
+ process.stdout.write(`[halfcycle] Claude Code ${version} detected \u2014 new enough for the credential helper.
3097
3499
  `);
3500
+ }
3098
3501
  return;
3099
3502
  }
3100
3503
  process.stdout.write(`[halfcycle] WARNING: Claude Code ${version} is older than ${MIN_HEADERS_HELPER_VERSION}, which ignores the credential helper this install wrote. The Halfcycle server will connect with no credential and every call will fail \u2014 upgrade Claude Code, then re-open this folder.
3101
3504
  `);
3102
3505
  }
3506
+ function shortId(id) {
3507
+ return id.length > 10 ? `${id.slice(0, 8)}\u2026` : id;
3508
+ }
3103
3509
  var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|client]
3104
3510
  install into target (default: the current directory)
3105
3511
  --yes / -y: use this machine's saved sign-in without being asked
@@ -3107,10 +3513,13 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
3107
3513
  halfcycle build-record <phase-id> [--repo <root>]
3108
3514
  halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
3109
3515
  halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
3516
+
3517
+ --quiet / -q: print no opening banner (any command)
3518
+ --verbose / -v: print full run detail (paths written, merged, skipped) on install
3110
3519
  `;
3111
3520
  function isHalfcycleMonorepo(dir) {
3112
3521
  try {
3113
- const pkg = JSON.parse(readFileSync8(join9(dir, "package.json"), "utf-8"));
3522
+ const pkg = JSON.parse(readFileSync9(join10(dir, "package.json"), "utf-8"));
3114
3523
  return pkg.name === "halfcycle-monorepo";
3115
3524
  } catch {
3116
3525
  return false;
@@ -3118,6 +3527,11 @@ function isHalfcycleMonorepo(dir) {
3118
3527
  }
3119
3528
  async function main() {
3120
3529
  const [cmd, ...rest] = args;
3530
+ const bareTarget = cmd !== void 0 && !cmd.startsWith("-") && !/^(check-drift|build-record|open-phase|close-phase)$/.test(cmd);
3531
+ const installArm = cmd === "install" || cmd === void 0 || bareTarget;
3532
+ const positionals = cmd === "install" ? rest : args;
3533
+ const targetRepo = installArm ? positionals[0] ?? process.cwd() : process.cwd();
3534
+ printBanner(process.stdout, openingBannerContent(targetRepo), currentEnvironment(quiet));
3121
3535
  if (cmd === "--help" || cmd === "-h" || cmd === "help") {
3122
3536
  process.stdout.write(`halfcycle \u2014 install and drive the Halfcycle method in a repository.
3123
3537
 
@@ -3126,10 +3540,7 @@ ${USAGE}`);
3126
3540
  process.exit(0);
3127
3541
  return;
3128
3542
  }
3129
- const bareTarget = cmd !== void 0 && !cmd.startsWith("-") && !/^(check-drift|build-record|open-phase|close-phase)$/.test(cmd);
3130
- if (cmd === "install" || cmd === void 0 || bareTarget) {
3131
- const positionals = cmd === "install" ? rest : args;
3132
- const targetRepo = positionals[0] ?? process.cwd();
3543
+ if (installArm) {
3133
3544
  const engagementIdArg = positionals[1];
3134
3545
  const engagementTypeRaw = positionals[2] ?? "client";
3135
3546
  if (isHalfcycleMonorepo(targetRepo)) {
@@ -3151,19 +3562,26 @@ ${USAGE}`);
3151
3562
  try {
3152
3563
  const controlOrigin = resolveControlOrigin(process.env);
3153
3564
  const serviceUrl = controlOrigin.origin;
3154
- process.stdout.write(`[halfcycle] Halfcycle plane: ${controlOriginNote(controlOrigin)}
3565
+ if (verbose || controlOrigin.source !== "default") {
3566
+ process.stdout.write(`[halfcycle] Halfcycle service: ${controlOriginNote(controlOrigin)}
3155
3567
  `);
3568
+ }
3156
3569
  const pinned = readPinnedEngagement(targetRepo);
3157
3570
  const requestedId = engagementIdArg ?? pinned?.engagementId;
3158
3571
  const reusable = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
3159
3572
  let engagementId;
3160
3573
  let credential;
3161
3574
  let actingAccountId;
3575
+ let foundLine = "";
3162
3576
  if (reusable !== void 0 && pinned !== null) {
3163
3577
  engagementId = pinned.engagementId;
3164
3578
  credential = reusable;
3165
- process.stdout.write(`[halfcycle] Re-using the engagement already pinned in this repo: ${pinned.engagementId}
3579
+ actingAccountId = readBundlePin(targetRepo)?.accountId;
3580
+ foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
3581
+ if (verbose) {
3582
+ process.stdout.write(`[halfcycle] Re-using the engagement already pinned in this repo: ${pinned.engagementId}
3166
3583
  `);
3584
+ }
3167
3585
  if (pinned.fromLegacyEnvLocal) {
3168
3586
  process.stdout.write(`[halfcycle] Its credential is in this repository's .env.local \u2014 an install from before
3169
3587
  [halfcycle] credentials moved out of the tree. It is being copied to
@@ -3172,7 +3590,7 @@ ${USAGE}`);
3172
3590
  `);
3173
3591
  }
3174
3592
  } else if (requestedId !== void 0) {
3175
- const joined = await joinPinnedEngagement(serviceUrl, requestedId, targetRepo, { assumeYes });
3593
+ const joined = await joinPinnedEngagement(serviceUrl, requestedId, targetRepo, { assumeYes, verbose });
3176
3594
  engagementId = joined.engagementId;
3177
3595
  actingAccountId = joined.actingAccountId;
3178
3596
  credential = {
@@ -3185,10 +3603,13 @@ ${USAGE}`);
3185
3603
  // an absent value, never `undefined` verbatim.
3186
3604
  controlTelemetryUrl: joined.controlTelemetryUrl
3187
3605
  };
3188
- process.stdout.write(`[halfcycle] Joined engagement ${joined.engagementId} \u2014 this credential is yours. Everyone else's keeps working; nobody was signed out and nothing was pasted.
3606
+ foundLine = `this repo is pinned to a shared project \u2014 joining it as you (${shortId(joined.engagementId)})`;
3607
+ if (verbose) {
3608
+ process.stdout.write(`[halfcycle] Joined engagement ${joined.engagementId} \u2014 this credential is yours. Everyone else's keeps working; nobody was signed out and nothing was pasted.
3189
3609
  `);
3610
+ }
3190
3611
  } else {
3191
- const created = await createOwnedEngagement(serviceUrl, targetRepo, { assumeYes });
3612
+ const created = await createOwnedEngagement(serviceUrl, targetRepo, { assumeYes, verbose });
3192
3613
  engagementId = created.engagementId;
3193
3614
  actingAccountId = created.actingAccountId;
3194
3615
  credential = {
@@ -3204,8 +3625,11 @@ ${USAGE}`);
3204
3625
  // emission is fail-open on that absence.
3205
3626
  controlTelemetryUrl: created.controlTelemetryUrl
3206
3627
  };
3207
- process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
3628
+ foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
3629
+ if (verbose) {
3630
+ process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
3208
3631
  `);
3632
+ }
3209
3633
  }
3210
3634
  const result = await install({
3211
3635
  targetRepo,
@@ -3214,17 +3638,19 @@ ${USAGE}`);
3214
3638
  credential,
3215
3639
  accountId: actingAccountId
3216
3640
  });
3217
- process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
3641
+ if (verbose) {
3642
+ process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
3218
3643
  `);
3219
- process.stdout.write(`[halfcycle] Written: ${result.writtenPaths.length} paths
3644
+ process.stdout.write(`[halfcycle] Written: ${result.writtenPaths.length} paths
3220
3645
  `);
3221
- if (result.mergedPaths.length > 0) {
3222
- process.stdout.write(`[halfcycle] Merged: ${result.mergedPaths.join(", ")}
3646
+ if (result.mergedPaths.length > 0) {
3647
+ process.stdout.write(`[halfcycle] Merged: ${result.mergedPaths.join(", ")}
3223
3648
  `);
3224
- }
3225
- if (result.skippedPaths.length > 0) {
3226
- process.stdout.write(`[halfcycle] Skipped (already present): ${result.skippedPaths.join(", ")}
3649
+ }
3650
+ if (result.skippedPaths.length > 0) {
3651
+ process.stdout.write(`[halfcycle] Skipped (already present): ${result.skippedPaths.join(", ")}
3227
3652
  `);
3653
+ }
3228
3654
  }
3229
3655
  if (result.collidedPaths.length > 0) {
3230
3656
  process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${result.collidedPaths.join(", ")}
@@ -3242,22 +3668,37 @@ ${USAGE}`);
3242
3668
  }
3243
3669
  const probe = await probeMcpOrigin(credential.mcpUrl);
3244
3670
  if (probe.reached) {
3245
- process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
3671
+ if (verbose) {
3672
+ process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
3246
3673
  `);
3674
+ }
3247
3675
  } else {
3248
3676
  process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
3249
3677
  [halfcycle] The Halfcycle commands will have no method context until that address answers. This is the SECOND of two origins and the platform supplied it, so it is not the CONTROL origin (${credential.serviceUrl}) that is wrong \u2014 that one just worked. It is recorded as HALFCYCLE_MCP_URL in this engagement's credential store; re-run this installer once the service is up.
3250
3678
  `);
3251
3679
  }
3252
- process.stdout.write(`[halfcycle] Guard evaluation: ON \u2014 the hook evaluates against ${credential.guardUrl}. Credentials (${GUARD_COVERAGE_REQUIRED_KEYS.join(", ")}) are in ${engagementEnvPath(engagementId)}, outside this repository; the hook loads them itself, so you do not export anything and nothing here can be committed.
3680
+ if (verbose) {
3681
+ process.stdout.write(`[halfcycle] Guard evaluation: ON \u2014 the hook evaluates against ${credential.guardUrl}. Credentials (${GUARD_COVERAGE_REQUIRED_KEYS.join(", ")}) are in ${engagementEnvPath(engagementId)}, outside this repository; the hook loads them itself, so you do not export anything and nothing here can be committed.
3682
+ `);
3683
+ process.stdout.write(`[halfcycle] Your credential is at ${engagementEnvPath(engagementId)} \u2014 outside this repository, readable only by you. .mcp.json reads it at connection time through .halfcycle/mcp-headers.sh, so there is no token in this tree to commit and nothing for you to export.
3684
+ `);
3685
+ process.stdout.write(`[halfcycle] Expect a workspace trust prompt the first time you open this folder \u2014 the helper is a shell command, and Claude Code will not run it until you accept. Needs Claude Code ${MIN_HEADERS_HELPER_VERSION} or newer (${MIN_HEADER_ROTATION_VERSION}+ to re-authenticate a rotated token without restarting).
3686
+ `);
3687
+ process.stdout.write(`[halfcycle] If Halfcycle tools appear but every call returns 401, the helper could not read HALFCYCLE_TOKEN from ${engagementEnvPath(engagementId)} \u2014 re-run this installer.
3688
+ `);
3689
+ }
3690
+ reportClaudeCodeVersion(verbose);
3691
+ const identity = await describeAccount(credential.serviceUrl, credential.token);
3692
+ const written = result.writtenPaths.length;
3693
+ const merged = result.mergedPaths.length;
3694
+ process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId, verbose)}
3253
3695
  `);
3254
- process.stdout.write(`[halfcycle] Your credential is at ${engagementEnvPath(engagementId)} \u2014 outside this repository, readable only by you. .mcp.json reads it at connection time through .halfcycle/mcp-headers.sh, so there is no token in this tree to commit and nothing for you to export.
3696
+ process.stdout.write(`[halfcycle] Found: ${foundLine}
3255
3697
  `);
3256
- process.stdout.write(`[halfcycle] Expect a workspace trust prompt the first time you open this folder \u2014 the helper is a shell command, and Claude Code will not run it until you accept. Needs Claude Code ${MIN_HEADERS_HELPER_VERSION} or newer (${MIN_HEADER_ROTATION_VERSION}+ to re-authenticate a rotated token without restarting).
3698
+ process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, guards armed on every edit
3257
3699
  `);
3258
- process.stdout.write(`[halfcycle] If Halfcycle tools appear but every call returns 401, the helper could not read HALFCYCLE_TOKEN from ${engagementEnvPath(engagementId)} \u2014 re-run this installer.
3700
+ process.stdout.write(`[halfcycle] Next: open this folder in Claude Code \u2014 accept the workspace-trust prompt, it is expected \u2014 then run /halfcycle-setup and answer what it asks about your project
3259
3701
  `);
3260
- reportClaudeCodeVersion();
3261
3702
  try {
3262
3703
  const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
3263
3704
  process.stdout.write(`[halfcycle] Your board (first visit): ${minted.boardUrl}
@@ -3282,17 +3723,17 @@ ${USAGE}`);
3282
3723
  return;
3283
3724
  }
3284
3725
  if (cmd === "check-drift") {
3285
- const targetRepo = rest[0];
3286
- if (!targetRepo) {
3726
+ const targetRepo2 = rest[0];
3727
+ if (!targetRepo2) {
3287
3728
  process.stderr.write("halfcycle-bundle: usage: halfcycle-bundle check-drift <target-repo>\n");
3288
3729
  process.exit(1);
3289
3730
  return;
3290
3731
  }
3291
3732
  try {
3292
- const { drifted, installed, current } = checkDrift(targetRepo);
3733
+ const { drifted, installed, current } = checkDrift(targetRepo2);
3293
3734
  if (drifted) {
3294
3735
  process.stdout.write(`[halfcycle-bundle] DRIFT DETECTED: installed=${installed ?? "none"} current=${current}
3295
- [halfcycle-bundle] Re-run "halfcycle-bundle install ${targetRepo}" to sync.
3736
+ [halfcycle-bundle] Re-run "halfcycle-bundle install ${targetRepo2}" to sync.
3296
3737
  `);
3297
3738
  process.exit(2);
3298
3739
  } else {
@@ -3317,12 +3758,12 @@ ${USAGE}`);
3317
3758
  }
3318
3759
  const phaseId = Number(phaseArg);
3319
3760
  try {
3320
- const inputPath = join9(repoRoot, ".workbench", "build-record", `phase-${phaseId}.input.json`);
3321
- const input = JSON.parse(readFileSync8(inputPath, "utf-8"));
3761
+ const inputPath = join10(repoRoot, ".workbench", "build-record", `phase-${phaseId}.input.json`);
3762
+ const input = JSON.parse(readFileSync9(inputPath, "utf-8"));
3322
3763
  const result = assemblePhaseBuildRecord({
3323
3764
  repoRoot,
3324
- phasesDir: join9(repoRoot, "docs", "phases"),
3325
- guardEvalLogDir: input.guardEvalLogDir ?? join9(repoRoot, ".workbench", "guard-eval-log"),
3765
+ phasesDir: join10(repoRoot, "docs", "phases"),
3766
+ guardEvalLogDir: input.guardEvalLogDir ?? join10(repoRoot, ".workbench", "guard-eval-log"),
3326
3767
  phaseId,
3327
3768
  narrated: input.narrated,
3328
3769
  touchedInvariants: input.touchedInvariants