halfcycle 0.3.12 → 0.3.13

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)) {
@@ -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
 
@@ -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();
@@ -3059,11 +3066,351 @@ var PLACEHOLDERS = {
3059
3066
  "--evidence": '"\u2026"'
3060
3067
  };
3061
3068
 
3069
+ // dist/banner.js
3070
+ var MARK_FULL = {
3071
+ width: 32,
3072
+ discRows: 7,
3073
+ lines: [
3074
+ " :+oshhhhso+:",
3075
+ " :sdMMMMMMMMMMMMds:",
3076
+ " :hMMMMMMMMMMMMMMMMMMh:",
3077
+ " oMMMMMMMMMMMMMMMMMMMMMMo",
3078
+ " oMMMMMMMMMMMMMMMMMMMMMMMMo",
3079
+ " NMMMMMMMMMMMMMMMMMMMMMMMMN",
3080
+ " :ssssssssssssssssssssssssss:",
3081
+ " +dddddddddddddddddddddddddd+",
3082
+ " Nd dN",
3083
+ " oMy yMo",
3084
+ " oMh: :hMo",
3085
+ " :hMy- -yMh:",
3086
+ " :sdNys++--++syNds:",
3087
+ " :+oshhhhso+:"
3088
+ ]
3089
+ };
3090
+ var MARK_NARROW = {
3091
+ width: 24,
3092
+ discRows: 5,
3093
+ lines: [
3094
+ " -ohdMMMMdho-",
3095
+ " +dMMMMMMMMMMMMd+",
3096
+ " yMMMMMMMMMMMMMMMMy",
3097
+ " sMMMMMMMMMMMMMMMMMMs",
3098
+ " yhhhhhhhhhhhhhhhhhhy",
3099
+ " NdhhhhhhhhhhhhhhhhdN",
3100
+ " sd ds",
3101
+ " yd: :dy",
3102
+ " +dy+ +yd+",
3103
+ " -ohhhhhhhho-"
3104
+ ]
3105
+ };
3106
+ var UNICODE_GLYPHS = {
3107
+ rule: "\u2500",
3108
+ swatch: "\u2584",
3109
+ dot: "\xB7",
3110
+ dash: "\u2014"
3111
+ };
3112
+ var ASCII_GLYPHS = {
3113
+ rule: "-",
3114
+ swatch: "#",
3115
+ dot: "|",
3116
+ dash: "-"
3117
+ };
3118
+ function toAscii(text) {
3119
+ let out = text;
3120
+ for (const key of Object.keys(UNICODE_GLYPHS)) {
3121
+ out = out.split(UNICODE_GLYPHS[key]).join(ASCII_GLYPHS[key]);
3122
+ }
3123
+ return out.replace(/[^ -~]/g, "?");
3124
+ }
3125
+ var INK = {
3126
+ plain: "",
3127
+ dim: "2",
3128
+ bold: "1",
3129
+ /** Yellow — the nearest ANSI seat for the brand's gold, used for labels and the URL. */
3130
+ key: "33",
3131
+ keyBold: "33;1"
3132
+ };
3133
+ var SWATCH_SGR = ["90", "31", "32", "33", "34", "35", "36", "37"];
3134
+ var ESC = "\x1B";
3135
+ var RESET = `${ESC}[0m`;
3136
+ function paint(segments, colour) {
3137
+ return segments.map((s) => colour && s.sgr !== "" ? `${ESC}[${s.sgr}m${s.text}${RESET}` : s.text).join("");
3138
+ }
3139
+ var GAP = 4;
3140
+ var FULL_LAYOUT = { art: MARK_FULL, label: 13, rule: 28, palette: true, minColumns: 76 };
3141
+ var NARROW_LAYOUT = { art: MARK_NARROW, label: 10, rule: 20, palette: false, minColumns: 48 };
3142
+ var DEFAULT_COLUMNS = 80;
3143
+ function usableColumns(environment) {
3144
+ const columns = environment.columns;
3145
+ return columns !== void 0 && columns > 0 ? columns : DEFAULT_COLUMNS;
3146
+ }
3147
+ function isSet(value) {
3148
+ return value !== void 0 && value !== "";
3149
+ }
3150
+ function plainTextRequested(env) {
3151
+ return isSet(env["NO_COLOR"]) || env["TERM"] === "dumb";
3152
+ }
3153
+ function decideCut(environment = {}) {
3154
+ const env = environment.env ?? {};
3155
+ if (environment.quiet === true)
3156
+ return "none";
3157
+ if (environment.isTTY !== true)
3158
+ return "line";
3159
+ if (plainTextRequested(env))
3160
+ return "line";
3161
+ const columns = usableColumns(environment);
3162
+ if (columns >= FULL_LAYOUT.minColumns)
3163
+ return "full";
3164
+ if (columns >= NARROW_LAYOUT.minColumns)
3165
+ return "narrow";
3166
+ return "line";
3167
+ }
3168
+ function decideColour(environment = {}) {
3169
+ const env = environment.env ?? {};
3170
+ const force = env["FORCE_COLOR"];
3171
+ if (isSet(force))
3172
+ return force !== "0";
3173
+ if (plainTextRequested(env))
3174
+ return false;
3175
+ return environment.isTTY === true;
3176
+ }
3177
+ function decideUnicode(environment = {}) {
3178
+ const env = environment.env ?? {};
3179
+ if ((environment.platform ?? "") !== "win32") {
3180
+ const locale = env["LC_ALL"] ?? env["LC_CTYPE"] ?? env["LANG"] ?? "";
3181
+ return locale === "" || /utf-?8/i.test(locale);
3182
+ }
3183
+ return isSet(env["WT_SESSION"]) || isSet(env["TERMINUS_SUBLIME"]) || env["ConEmuTask"] === "{cmd::Cmder}" || env["TERM_PROGRAM"] === "vscode";
3184
+ }
3185
+ function padEnd(text, width) {
3186
+ return text + " ".repeat(Math.max(0, width - text.length));
3187
+ }
3188
+ function clip(text, width) {
3189
+ if (width < 8 || text.length <= width)
3190
+ return text;
3191
+ return `${text.slice(0, width - 3)}...`;
3192
+ }
3193
+ function paletteRow(swatch) {
3194
+ const row = [];
3195
+ SWATCH_SGR.forEach((sgr, i) => {
3196
+ if (i > 0)
3197
+ row.push({ text: " ", sgr: INK.plain });
3198
+ row.push({ text: swatch.repeat(2), sgr });
3199
+ });
3200
+ return row;
3201
+ }
3202
+ function factColumn(content, layout, glyphs, text, valueWidth) {
3203
+ const rows = [];
3204
+ const head = text(content.head);
3205
+ const at = head.indexOf("@");
3206
+ rows.push(at < 0 ? [{ text: head, sgr: INK.keyBold }] : [
3207
+ { text: head.slice(0, at), sgr: INK.keyBold },
3208
+ { text: "@", sgr: INK.dim },
3209
+ { text: head.slice(at + 1), sgr: INK.bold }
3210
+ ]);
3211
+ rows.push([{ text: glyphs.rule.repeat(layout.rule), sgr: INK.dim }]);
3212
+ const narrow = layout.art === MARK_NARROW;
3213
+ for (const fact2 of content.facts) {
3214
+ const raw = narrow ? fact2.narrow : fact2.value;
3215
+ if (raw === void 0)
3216
+ continue;
3217
+ rows.push([
3218
+ { text: padEnd(`${text(fact2.label)}:`, layout.label), sgr: INK.key },
3219
+ { text: clip(text(raw), valueWidth), sgr: INK.plain }
3220
+ ]);
3221
+ }
3222
+ if (layout.palette) {
3223
+ rows.push([]);
3224
+ rows.push(paletteRow(glyphs.swatch));
3225
+ }
3226
+ return rows;
3227
+ }
3228
+ function renderBlock(content, layout, environment) {
3229
+ const unicode = decideUnicode(environment);
3230
+ const colour = decideColour(environment);
3231
+ const glyphs = unicode ? UNICODE_GLYPHS : ASCII_GLYPHS;
3232
+ const text = (raw) => unicode ? raw : toAscii(raw);
3233
+ const art = layout.art;
3234
+ const valueWidth = usableColumns(environment) - (art.width + GAP + layout.label);
3235
+ const right = factColumn(content, layout, glyphs, text, valueWidth);
3236
+ const top = Math.max(0, Math.floor((art.lines.length - right.length) / 2));
3237
+ const height = Math.max(art.lines.length, right.length + top);
3238
+ const lines = [];
3239
+ for (let i = 0; i < height; i++) {
3240
+ const artLine = art.lines[i] ?? "";
3241
+ const segments = [];
3242
+ if (artLine.length > 0) {
3243
+ segments.push({ text: artLine, sgr: i < art.discRows ? INK.plain : INK.dim });
3244
+ }
3245
+ const rightRow = right[i - top];
3246
+ if (rightRow !== void 0 && rightRow.length > 0) {
3247
+ segments.push({ text: " ".repeat(Math.max(0, art.width + GAP - artLine.length)), sgr: INK.plain });
3248
+ segments.push(...rightRow);
3249
+ }
3250
+ lines.push(segments);
3251
+ }
3252
+ const tagline = layout.art === MARK_NARROW ? content.tagNarrow ?? content.tag : content.tag;
3253
+ lines.push([]);
3254
+ tagline.forEach((line, i) => {
3255
+ lines.push([{ text: ` ${text(line)}`, sgr: i === 0 ? INK.bold : INK.dim }]);
3256
+ });
3257
+ lines.push([]);
3258
+ lines.push([{ text: ` ${text(content.url)}`, sgr: INK.key }]);
3259
+ return lines.map((segments) => paint(segments, colour)).join("\n");
3260
+ }
3261
+ function renderLine(content, environment) {
3262
+ const unicode = decideUnicode(environment);
3263
+ const dot = unicode ? UNICODE_GLYPHS.dot : ASCII_GLYPHS.dot;
3264
+ const named = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
3265
+ const raw = `${named} ${dot} ${content.url}`;
3266
+ return unicode ? raw : toAscii(raw);
3267
+ }
3268
+ function renderBanner(content, environment = {}) {
3269
+ switch (decideCut(environment)) {
3270
+ case "none":
3271
+ return "";
3272
+ case "line":
3273
+ return renderLine(content, environment);
3274
+ case "narrow":
3275
+ return renderBlock(content, NARROW_LAYOUT, environment);
3276
+ case "full":
3277
+ return renderBlock(content, FULL_LAYOUT, environment);
3278
+ }
3279
+ }
3280
+ function printBanner(sink, content, environment = {}) {
3281
+ const text = renderBanner(content, environment);
3282
+ if (text === "")
3283
+ return;
3284
+ sink.write(`${text}
3285
+ `);
3286
+ }
3287
+ function currentEnvironment(quiet2) {
3288
+ return {
3289
+ columns: process.stdout.columns,
3290
+ isTTY: process.stdout.isTTY === true,
3291
+ quiet: quiet2,
3292
+ env: process.env,
3293
+ platform: process.platform
3294
+ };
3295
+ }
3296
+
3297
+ // dist/banner-facts.js
3298
+ import { execFileSync as execFileSync3 } from "node:child_process";
3299
+ import { readFileSync as readFileSync8 } from "node:fs";
3300
+ import { basename as basename2, join as join9, resolve as resolve3 } from "node:path";
3301
+ var BRAND_URL = "halfcycle.ai";
3302
+ var TAGLINE = [
3303
+ "Fewer cycles.",
3304
+ "The thinking moves earlier. So does the finish."
3305
+ ];
3306
+ var TAGLINE_NARROW = [
3307
+ "Fewer cycles.",
3308
+ "The thinking moves earlier.",
3309
+ "So does the finish."
3310
+ ];
3311
+ function bundleVersion() {
3312
+ try {
3313
+ const pkg = JSON.parse(readFileSync8(join9(BUNDLE_ROOT, "package.json"), "utf-8"));
3314
+ return typeof pkg.version === "string" ? pkg.version : void 0;
3315
+ } catch {
3316
+ return void 0;
3317
+ }
3318
+ }
3319
+ function claudeCodeVersion() {
3320
+ try {
3321
+ const raw = execFileSync3("claude", ["--version"], {
3322
+ encoding: "utf-8",
3323
+ timeout: 3e3,
3324
+ stdio: ["ignore", "pipe", "ignore"]
3325
+ });
3326
+ return /(\d+\.\d+\.\d+)/.exec(raw)?.[1];
3327
+ } catch {
3328
+ return void 0;
3329
+ }
3330
+ }
3331
+ var REAL_PROBES = {
3332
+ bundleVersion,
3333
+ claudeCodeVersion,
3334
+ projectName: (dir) => {
3335
+ try {
3336
+ return deriveProjectName(dir);
3337
+ } catch {
3338
+ return void 0;
3339
+ }
3340
+ },
3341
+ commitCount: (dir) => {
3342
+ try {
3343
+ return readCommitCount(dir);
3344
+ } catch {
3345
+ return null;
3346
+ }
3347
+ },
3348
+ installedVersion: (dir) => {
3349
+ try {
3350
+ return readBundlePin(dir)?.version;
3351
+ } catch {
3352
+ return void 0;
3353
+ }
3354
+ },
3355
+ planeHost: (env) => {
3356
+ try {
3357
+ const resolved = resolveControlOrigin(env);
3358
+ return resolved.source === "default" ? BRAND_URL : new URL(resolved.origin).host;
3359
+ } catch {
3360
+ return void 0;
3361
+ }
3362
+ }
3363
+ };
3364
+ function fact(label, value, narrow) {
3365
+ if (value === void 0 || value === "")
3366
+ return void 0;
3367
+ return narrow === void 0 ? { label, value } : { label, value, narrow };
3368
+ }
3369
+ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBES) {
3370
+ const dot = UNICODE_GLYPHS.dot;
3371
+ const dash = UNICODE_GLYPHS.dash;
3372
+ const version = probes.bundleVersion();
3373
+ const name = probes.projectName(projectDir) ?? safeBasename(projectDir);
3374
+ const commits = probes.commitCount(projectDir);
3375
+ const installed = probes.installedVersion(projectDir);
3376
+ const project = commits === null ? name : `${name} ${dot} git ${dot} ${commits} ${commits === 1 ? "commit" : "commits"}`;
3377
+ const status = installed === void 0 ? `new ${dash} nothing installed yet` : `installed ${dash} v${installed}`;
3378
+ const facts = [
3379
+ fact("Version", version, version),
3380
+ fact("Claude Code", probes.claudeCodeVersion()),
3381
+ fact("Project", project, name),
3382
+ fact("Status", status, installed === void 0 ? "new" : "installed"),
3383
+ fact("Plane", probes.planeHost(env))
3384
+ ].filter((f) => f !== void 0);
3385
+ return {
3386
+ head: `halfcycle@${name}`,
3387
+ facts,
3388
+ tag: TAGLINE,
3389
+ tagNarrow: TAGLINE_NARROW,
3390
+ url: BRAND_URL,
3391
+ // `''` when even this package's own manifest could not be read. The one-line cut
3392
+ // drops the whole `vX.Y.Z` clause in that case rather than printing a bare `v`.
3393
+ version: version ?? ""
3394
+ };
3395
+ }
3396
+ function safeBasename(dir) {
3397
+ try {
3398
+ const name = basename2(resolve3(dir)).trim();
3399
+ return name === "" ? "project" : name;
3400
+ } catch {
3401
+ return "project";
3402
+ }
3403
+ }
3404
+
3062
3405
  // dist/bin.js
3063
3406
  var ASSUME_YES_FLAGS = /* @__PURE__ */ new Set(["--yes", "-y"]);
3407
+ var QUIET_FLAGS = /* @__PURE__ */ new Set(["--quiet", "-q"]);
3408
+ var VERBOSE_FLAGS = /* @__PURE__ */ new Set(["--verbose", "-v"]);
3064
3409
  var rawArgs = process.argv.slice(2);
3065
3410
  var assumeYes = rawArgs.some((a) => ASSUME_YES_FLAGS.has(a));
3066
- var args = rawArgs.filter((a) => !ASSUME_YES_FLAGS.has(a));
3411
+ var quiet = rawArgs.some((a) => QUIET_FLAGS.has(a));
3412
+ var verbose = rawArgs.some((a) => VERBOSE_FLAGS.has(a));
3413
+ var args = rawArgs.filter((a) => !ASSUME_YES_FLAGS.has(a) && !QUIET_FLAGS.has(a) && !VERBOSE_FLAGS.has(a));
3067
3414
  var MIN_HEADERS_HELPER_VERSION = "2.1.118";
3068
3415
  var MIN_HEADER_ROTATION_VERSION = "2.1.193";
3069
3416
  function compareVersions(a, b) {
@@ -3077,10 +3424,10 @@ function compareVersions(a, b) {
3077
3424
  }
3078
3425
  return 0;
3079
3426
  }
3080
- function reportClaudeCodeVersion() {
3427
+ function reportClaudeCodeVersion(verbose2) {
3081
3428
  let raw;
3082
3429
  try {
3083
- raw = execFileSync3("claude", ["--version"], {
3430
+ raw = execFileSync4("claude", ["--version"], {
3084
3431
  encoding: "utf-8",
3085
3432
  timeout: 5e3,
3086
3433
  stdio: ["ignore", "pipe", "ignore"]
@@ -3093,13 +3440,18 @@ function reportClaudeCodeVersion() {
3093
3440
  return;
3094
3441
  const version = found[1];
3095
3442
  if (compareVersions(version, MIN_HEADERS_HELPER_VERSION) >= 0) {
3096
- process.stdout.write(`[halfcycle] Claude Code ${version} detected \u2014 new enough for the credential helper.
3443
+ if (verbose2) {
3444
+ process.stdout.write(`[halfcycle] Claude Code ${version} detected \u2014 new enough for the credential helper.
3097
3445
  `);
3446
+ }
3098
3447
  return;
3099
3448
  }
3100
3449
  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
3450
  `);
3102
3451
  }
3452
+ function shortId(id) {
3453
+ return id.length > 10 ? `${id.slice(0, 8)}\u2026` : id;
3454
+ }
3103
3455
  var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|client]
3104
3456
  install into target (default: the current directory)
3105
3457
  --yes / -y: use this machine's saved sign-in without being asked
@@ -3107,10 +3459,13 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
3107
3459
  halfcycle build-record <phase-id> [--repo <root>]
3108
3460
  halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
3109
3461
  halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
3462
+
3463
+ --quiet / -q: print no opening banner (any command)
3464
+ --verbose / -v: print full run detail (paths written, merged, skipped) on install
3110
3465
  `;
3111
3466
  function isHalfcycleMonorepo(dir) {
3112
3467
  try {
3113
- const pkg = JSON.parse(readFileSync8(join9(dir, "package.json"), "utf-8"));
3468
+ const pkg = JSON.parse(readFileSync9(join10(dir, "package.json"), "utf-8"));
3114
3469
  return pkg.name === "halfcycle-monorepo";
3115
3470
  } catch {
3116
3471
  return false;
@@ -3118,6 +3473,11 @@ function isHalfcycleMonorepo(dir) {
3118
3473
  }
3119
3474
  async function main() {
3120
3475
  const [cmd, ...rest] = args;
3476
+ const bareTarget = cmd !== void 0 && !cmd.startsWith("-") && !/^(check-drift|build-record|open-phase|close-phase)$/.test(cmd);
3477
+ const installArm = cmd === "install" || cmd === void 0 || bareTarget;
3478
+ const positionals = cmd === "install" ? rest : args;
3479
+ const targetRepo = installArm ? positionals[0] ?? process.cwd() : process.cwd();
3480
+ printBanner(process.stdout, openingBannerContent(targetRepo), currentEnvironment(quiet));
3121
3481
  if (cmd === "--help" || cmd === "-h" || cmd === "help") {
3122
3482
  process.stdout.write(`halfcycle \u2014 install and drive the Halfcycle method in a repository.
3123
3483
 
@@ -3126,10 +3486,7 @@ ${USAGE}`);
3126
3486
  process.exit(0);
3127
3487
  return;
3128
3488
  }
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();
3489
+ if (installArm) {
3133
3490
  const engagementIdArg = positionals[1];
3134
3491
  const engagementTypeRaw = positionals[2] ?? "client";
3135
3492
  if (isHalfcycleMonorepo(targetRepo)) {
@@ -3151,19 +3508,26 @@ ${USAGE}`);
3151
3508
  try {
3152
3509
  const controlOrigin = resolveControlOrigin(process.env);
3153
3510
  const serviceUrl = controlOrigin.origin;
3154
- process.stdout.write(`[halfcycle] Halfcycle plane: ${controlOriginNote(controlOrigin)}
3511
+ if (verbose || controlOrigin.source !== "default") {
3512
+ process.stdout.write(`[halfcycle] Halfcycle plane: ${controlOriginNote(controlOrigin)}
3155
3513
  `);
3514
+ }
3156
3515
  const pinned = readPinnedEngagement(targetRepo);
3157
3516
  const requestedId = engagementIdArg ?? pinned?.engagementId;
3158
3517
  const reusable = pinned !== null && pinned.engagementId === requestedId ? pinned.credential : void 0;
3159
3518
  let engagementId;
3160
3519
  let credential;
3161
3520
  let actingAccountId;
3521
+ let foundLine = "";
3162
3522
  if (reusable !== void 0 && pinned !== null) {
3163
3523
  engagementId = pinned.engagementId;
3164
3524
  credential = reusable;
3165
- process.stdout.write(`[halfcycle] Re-using the engagement already pinned in this repo: ${pinned.engagementId}
3525
+ actingAccountId = readBundlePin(targetRepo)?.accountId;
3526
+ foundLine = `already set up here \u2014 reusing engagement ${shortId(pinned.engagementId)}`;
3527
+ if (verbose) {
3528
+ process.stdout.write(`[halfcycle] Re-using the engagement already pinned in this repo: ${pinned.engagementId}
3166
3529
  `);
3530
+ }
3167
3531
  if (pinned.fromLegacyEnvLocal) {
3168
3532
  process.stdout.write(`[halfcycle] Its credential is in this repository's .env.local \u2014 an install from before
3169
3533
  [halfcycle] credentials moved out of the tree. It is being copied to
@@ -3185,8 +3549,11 @@ ${USAGE}`);
3185
3549
  // an absent value, never `undefined` verbatim.
3186
3550
  controlTelemetryUrl: joined.controlTelemetryUrl
3187
3551
  };
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.
3552
+ foundLine = `this repo is pinned to a shared project \u2014 joining it as you (${shortId(joined.engagementId)})`;
3553
+ if (verbose) {
3554
+ 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
3555
  `);
3556
+ }
3190
3557
  } else {
3191
3558
  const created = await createOwnedEngagement(serviceUrl, targetRepo, { assumeYes });
3192
3559
  engagementId = created.engagementId;
@@ -3204,8 +3571,11 @@ ${USAGE}`);
3204
3571
  // emission is fail-open on that absence.
3205
3572
  controlTelemetryUrl: created.controlTelemetryUrl
3206
3573
  };
3207
- process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
3574
+ foundLine = `no Halfcycle project here yet \u2014 creating one (${shortId(created.engagementId)})`;
3575
+ if (verbose) {
3576
+ process.stdout.write(`[halfcycle] Created engagement ${created.engagementId}
3208
3577
  `);
3578
+ }
3209
3579
  }
3210
3580
  const result = await install({
3211
3581
  targetRepo,
@@ -3214,17 +3584,19 @@ ${USAGE}`);
3214
3584
  credential,
3215
3585
  accountId: actingAccountId
3216
3586
  });
3217
- process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
3587
+ if (verbose) {
3588
+ process.stdout.write(`[halfcycle] Installed v${result.version} into ${targetRepo}
3218
3589
  `);
3219
- process.stdout.write(`[halfcycle] Written: ${result.writtenPaths.length} paths
3590
+ process.stdout.write(`[halfcycle] Written: ${result.writtenPaths.length} paths
3220
3591
  `);
3221
- if (result.mergedPaths.length > 0) {
3222
- process.stdout.write(`[halfcycle] Merged: ${result.mergedPaths.join(", ")}
3592
+ if (result.mergedPaths.length > 0) {
3593
+ process.stdout.write(`[halfcycle] Merged: ${result.mergedPaths.join(", ")}
3223
3594
  `);
3224
- }
3225
- if (result.skippedPaths.length > 0) {
3226
- process.stdout.write(`[halfcycle] Skipped (already present): ${result.skippedPaths.join(", ")}
3595
+ }
3596
+ if (result.skippedPaths.length > 0) {
3597
+ process.stdout.write(`[halfcycle] Skipped (already present): ${result.skippedPaths.join(", ")}
3227
3598
  `);
3599
+ }
3228
3600
  }
3229
3601
  if (result.collidedPaths.length > 0) {
3230
3602
  process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${result.collidedPaths.join(", ")}
@@ -3242,22 +3614,37 @@ ${USAGE}`);
3242
3614
  }
3243
3615
  const probe = await probeMcpOrigin(credential.mcpUrl);
3244
3616
  if (probe.reached) {
3245
- process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
3617
+ if (verbose) {
3618
+ process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
3246
3619
  `);
3620
+ }
3247
3621
  } else {
3248
3622
  process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
3249
3623
  [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
3624
  `);
3251
3625
  }
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.
3626
+ if (verbose) {
3627
+ 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.
3628
+ `);
3629
+ 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.
3630
+ `);
3631
+ 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).
3632
+ `);
3633
+ 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.
3634
+ `);
3635
+ }
3636
+ reportClaudeCodeVersion(verbose);
3637
+ const identity = await describeAccount(credential.serviceUrl, credential.token);
3638
+ const written = result.writtenPaths.length;
3639
+ const merged = result.mergedPaths.length;
3640
+ process.stdout.write(`[halfcycle] Signed in as ${accountLabel(identity, actingAccountId)}
3253
3641
  `);
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.
3642
+ process.stdout.write(`[halfcycle] Found: ${foundLine}
3255
3643
  `);
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).
3644
+ process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, guards armed on every edit
3257
3645
  `);
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.
3646
+ 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
3647
  `);
3260
- reportClaudeCodeVersion();
3261
3648
  try {
3262
3649
  const minted = await mintBoardEnterCode(credential.serviceUrl, credential.token);
3263
3650
  process.stdout.write(`[halfcycle] Your board (first visit): ${minted.boardUrl}
@@ -3282,17 +3669,17 @@ ${USAGE}`);
3282
3669
  return;
3283
3670
  }
3284
3671
  if (cmd === "check-drift") {
3285
- const targetRepo = rest[0];
3286
- if (!targetRepo) {
3672
+ const targetRepo2 = rest[0];
3673
+ if (!targetRepo2) {
3287
3674
  process.stderr.write("halfcycle-bundle: usage: halfcycle-bundle check-drift <target-repo>\n");
3288
3675
  process.exit(1);
3289
3676
  return;
3290
3677
  }
3291
3678
  try {
3292
- const { drifted, installed, current } = checkDrift(targetRepo);
3679
+ const { drifted, installed, current } = checkDrift(targetRepo2);
3293
3680
  if (drifted) {
3294
3681
  process.stdout.write(`[halfcycle-bundle] DRIFT DETECTED: installed=${installed ?? "none"} current=${current}
3295
- [halfcycle-bundle] Re-run "halfcycle-bundle install ${targetRepo}" to sync.
3682
+ [halfcycle-bundle] Re-run "halfcycle-bundle install ${targetRepo2}" to sync.
3296
3683
  `);
3297
3684
  process.exit(2);
3298
3685
  } else {
@@ -3317,12 +3704,12 @@ ${USAGE}`);
3317
3704
  }
3318
3705
  const phaseId = Number(phaseArg);
3319
3706
  try {
3320
- const inputPath = join9(repoRoot, ".workbench", "build-record", `phase-${phaseId}.input.json`);
3321
- const input = JSON.parse(readFileSync8(inputPath, "utf-8"));
3707
+ const inputPath = join10(repoRoot, ".workbench", "build-record", `phase-${phaseId}.input.json`);
3708
+ const input = JSON.parse(readFileSync9(inputPath, "utf-8"));
3322
3709
  const result = assemblePhaseBuildRecord({
3323
3710
  repoRoot,
3324
- phasesDir: join9(repoRoot, "docs", "phases"),
3325
- guardEvalLogDir: input.guardEvalLogDir ?? join9(repoRoot, ".workbench", "guard-eval-log"),
3711
+ phasesDir: join10(repoRoot, "docs", "phases"),
3712
+ guardEvalLogDir: input.guardEvalLogDir ?? join10(repoRoot, ".workbench", "guard-eval-log"),
3326
3713
  phaseId,
3327
3714
  narrated: input.narrated,
3328
3715
  touchedInvariants: input.touchedInvariants