halfcycle 0.3.36 → 0.3.37

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
@@ -230,6 +230,10 @@ function looksLikeSecret(text) {
230
230
  return false;
231
231
  }
232
232
 
233
+ // ../core/dist/mcp-registration.js
234
+ var MCP_REGISTRATION_REL = ".mcp.json";
235
+ var MCP_SERVER_KEY = "halfcycle";
236
+
233
237
  // ../events/dist/result.js
234
238
  import { z as z2 } from "zod";
235
239
  var firedGuardSchema = z2.object({
@@ -1220,7 +1224,19 @@ var ciTokenExchangeResponseSchema = z10.object({
1220
1224
  expiresAt: utcIso8601,
1221
1225
  engagementId: z10.string()
1222
1226
  }).strict();
1227
+ var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
1228
+ var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
1229
+ var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
1223
1230
  var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
1231
+ var NO_CREDENTIAL_SENTENCE = 'Halfcycle is not connected: this machine has no working Halfcycle credential for this project. Run "npx halfcycle" in the project folder to sign in again.';
1232
+ var NOT_THIS_ACCOUNT_SENTENCE = `Halfcycle is not connected: this project belongs to another Halfcycle account. To switch, run "npx halfcycle" in this project and ${SWITCH_ACCOUNT_ANSWER}.`;
1233
+ var UPDATE_CLAUDE_CODE_SENTENCE = "Halfcycle is not connected: this Claude Code version does not name the server it connects to, so no credential was sent. Update Claude Code, then reopen.";
1234
+ var NO_NODE_SENTENCE = "Halfcycle is not connected: Node is not installed, or not on the path this session uses, so nothing here is checked. Install Node, then reopen.";
1235
+ var PLACEHOLDER_SENTENCES = Object.freeze({
1236
+ [NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
1237
+ [NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
1238
+ [UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
1239
+ });
1224
1240
 
1225
1241
  // ../events/dist/phase-record.js
1226
1242
  import { z as z11 } from "zod";
@@ -2001,7 +2017,7 @@ var WRITE_ALLOWLIST = [
2001
2017
  ".halfcycle/bin",
2002
2018
  "docs/build-records",
2003
2019
  "test/fixtures/captured",
2004
- ".mcp.json",
2020
+ MCP_REGISTRATION_REL,
2005
2021
  ".gitattributes",
2006
2022
  ZONE_B_DIR
2007
2023
  ];
@@ -2374,6 +2390,20 @@ exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
2374
2390
  }
2375
2391
  var GENERATED_SESSION_START_HEADER = "# Session-start marker \u2014 generated by";
2376
2392
  var SESSION_ID_FROM_HOOK_INPUT_SH = `SESSION_ID="$(printf '%s' "$INPUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);process.stdout.write(typeof j.session_id==="string"?j.session_id:"")}catch{process.stdout.write("")}})' 2>/dev/null || true)"`;
2393
+ var REPO_HASH_FROM_ROOT_SH = `HASH="$(printf '%s' "$REPO_ROOT" | node -e 'const c=[];process.stdin.on("data",d=>c.push(d)).on("end",()=>{process.stdout.write(require("node:crypto").createHash("sha256").update(Buffer.concat(c)).digest("hex").slice(0,12))})' 2>/dev/null || true)"`;
2394
+ function noNodeReplyShell() {
2395
+ const reply = JSON.stringify({
2396
+ systemMessage: NO_NODE_SENTENCE,
2397
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: NO_NODE_SENTENCE }
2398
+ });
2399
+ if (reply.includes("'")) {
2400
+ throw new Error("[bundle install] the no-Node notice cannot be single-quoted; refusing to write the session-start script.");
2401
+ }
2402
+ return `if ! command -v node >/dev/null 2>&1; then
2403
+ printf '%s\\n' '${reply}'
2404
+ exit 0
2405
+ fi`;
2406
+ }
2377
2407
  function generateSessionStartMarker() {
2378
2408
  return `#!/usr/bin/env bash
2379
2409
  ${GENERATED_SESSION_START_HEADER} the Halfcycle installer.
@@ -2382,10 +2412,15 @@ ${GENERATED_SESSION_START_HEADER} the Halfcycle installer.
2382
2412
  # it began instead of only against the latest commit. The session id comes in on
2383
2413
  # stdin with the hook input; without one, the older session-less name is written
2384
2414
  # and the stop-time check compares against the latest commit as it used to.
2415
+ #
2416
+ # Without Node nothing Halfcycle installs here can run, so this says so once and
2417
+ # writes no marker.
2418
+ ${noNodeReplyShell()}
2385
2419
  INPUT="$(cat 2>/dev/null || true)"
2386
2420
  REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
2387
2421
  if [ -z "$REPO_ROOT" ]; then exit 0; fi
2388
- HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
2422
+ ${REPO_HASH_FROM_ROOT_SH}
2423
+ if [ -z "$HASH" ]; then exit 0; fi
2389
2424
  ${SESSION_ID_FROM_HOOK_INPUT_SH}
2390
2425
  MARKER_DIR="\${TMPDIR:-/tmp}"
2391
2426
  MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
@@ -2395,64 +2430,6 @@ else
2395
2430
  MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}.ref"
2396
2431
  fi
2397
2432
  git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
2398
-
2399
- # ---------------------------------------------------------------------------
2400
- # STANDING GUARD-COVERAGE STATEMENT (T-27; two arms since T-02b).
2401
- #
2402
- # A repository with no credential on this machine is UNGUARDED: the PostToolUse
2403
- # hook will find nothing to evaluate with, and it says so once per edit, after
2404
- # the edit. This says it once, up front, before anything is written \u2014 which is
2405
- # the one thing a session-start hook can do that nothing else can.
2406
- #
2407
- # TWO REASONS TO HAVE NO CREDENTIAL, TWO DIFFERENT THINGS TO SAY (T-02b,
2408
- # solo-project-ownership \u2014 outcome B). "You have not signed in here yet" is
2409
- # fixed by re-running the installer; "this is not your project" is permanent,
2410
- # and the same remedy would send the reader in a circle forever. The two are
2411
- # told apart by HALFCYCLE_ENV_NOT_THIS_ACCOUNT, set by \`halfcycle_env_file\`
2412
- # below from a machine-level marker beside the credential store.
2413
- #
2414
- # Everything below runs in a SUBSHELL: it sources a credential store, and a
2415
- # session-start hook must not leak an engagement's variables into whatever the
2416
- # editor runs next. Any failure inside it is swallowed; this script's exit
2417
- # status is 0 either way, and the marker above has already been written.
2418
- # ---------------------------------------------------------------------------
2419
- ${engagementResolutionShell()}
2420
-
2421
- (
2422
- if halfcycle_env_file "$REPO_ROOT"; then
2423
- set -a
2424
- . "$HALFCYCLE_ENV_FILE"
2425
- set +a
2426
- fi
2427
- if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
2428
- # OUTCOME B (T-02b, solo-project-ownership). This is not the missing-keys
2429
- # case below wearing different words: there is nothing to fix, and the loop
2430
- # that follows is skipped entirely rather than run and then overridden \u2014 a
2431
- # repository whose credential is absent for THIS reason must never be told
2432
- # to re-run the installer, which can never obtain one for it.
2433
- echo "[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked."
2434
- echo "[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is"
2435
- echo "[halfcycle] evaluated, and nothing you do is reported as having passed."
2436
- echo "[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming."
2437
- echo "[halfcycle] Ask the project's owner for view access."
2438
- echo "[halfcycle] If this account is wrong, sign in as the one that owns the project and run"
2439
- echo "[halfcycle] \\"npx halfcycle\\" again."
2440
- else
2441
- hc_missing=""
2442
- for hc_key in ${GUARD_COVERAGE_REQUIRED_KEYS.join(" ")}; do
2443
- eval "hc_value=\\\${$hc_key:-}"
2444
- if [ -z "$hc_value" ]; then hc_missing="$hc_missing $hc_key"; fi
2445
- done
2446
- if [ -n "$hc_missing" ]; then
2447
- echo "[halfcycle] NO GUARD COVERAGE IN THIS REPOSITORY. Nothing will evaluate the edits made in"
2448
- echo "[halfcycle] this session: this machine has no Halfcycle credential for it, so the guard hook"
2449
- echo "[halfcycle] has nothing to call with (missing:$hc_missing)."
2450
- echo "[halfcycle] Run \\"npx halfcycle\\" in this repository to fix it \u2014 it signs you in through your"
2451
- echo "[halfcycle] browser if needed, writes the credential to \\$HOME/.halfcycle outside this tree,"
2452
- echo "[halfcycle] and needs nothing configured first."
2453
- fi
2454
- fi
2455
- ) 2>/dev/null || true
2456
2433
  exit 0
2457
2434
  `;
2458
2435
  }
@@ -2468,7 +2445,8 @@ INPUT="$(cat 2>/dev/null || true)"
2468
2445
 
2469
2446
  REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
2470
2447
  if [ -z "$REPO_ROOT" ]; then exit 0; fi
2471
- HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
2448
+ ${REPO_HASH_FROM_ROOT_SH}
2449
+ if [ -z "$HASH" ]; then exit 0; fi
2472
2450
 
2473
2451
  # The session id arrives on stdin as part of the hook payload. Empty when absent.
2474
2452
  ${SESSION_ID_FROM_HOOK_INPUT_SH}
@@ -2588,8 +2566,6 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
2588
2566
  }
2589
2567
  return "removed";
2590
2568
  }
2591
- var MCP_REGISTRATION_REL = ".mcp.json";
2592
- var MCP_SERVER_KEY = "halfcycle";
2593
2569
  var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
2594
2570
  `const p=require('path'),f=require('fs'),c=require('child_process');`,
2595
2571
  `let d=process.cwd();`,
@@ -3180,6 +3156,7 @@ async function bindLoopback(env = process.env, engagementId) {
3180
3156
  const state = randomBytes(32).toString("hex");
3181
3157
  let pending = null;
3182
3158
  let deliver = null;
3159
+ let abandon = null;
3183
3160
  const server = createServer(createCallbackHandler(state, (callback) => {
3184
3161
  if (deliver)
3185
3162
  deliver(callback);
@@ -3221,12 +3198,18 @@ async function bindLoopback(env = process.env, engagementId) {
3221
3198
  clearInterval(ticker);
3222
3199
  clearTimeout(timer);
3223
3200
  deliver = null;
3201
+ abandon = null;
3224
3202
  resolve4(value);
3225
3203
  };
3226
3204
  const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
3227
3205
  deliver = finish;
3206
+ abandon = () => finish(null);
3228
3207
  }),
3229
- close: () => closeServer(server)
3208
+ close: () => {
3209
+ if (abandon !== null)
3210
+ abandon();
3211
+ return closeServer(server);
3212
+ }
3230
3213
  };
3231
3214
  }
3232
3215
  function closeServer(server) {
@@ -3260,8 +3243,13 @@ function defaultSleep(ms) {
3260
3243
  function browserOpenCommand(os, url) {
3261
3244
  if (os === "darwin")
3262
3245
  return { command: "open", args: [url] };
3263
- if (os === "win32")
3264
- return { command: "cmd", args: ["/c", "start", "", url] };
3246
+ if (os === "win32") {
3247
+ return {
3248
+ command: "cmd.exe",
3249
+ args: ["/d", "/s", "/c", `"start "" "${url}""`],
3250
+ windowsVerbatimArguments: true
3251
+ };
3252
+ }
3265
3253
  return { command: "xdg-open", args: [url] };
3266
3254
  }
3267
3255
  function isOpenableUrl(url) {
@@ -3272,19 +3260,35 @@ function isOpenableUrl(url) {
3272
3260
  return false;
3273
3261
  }
3274
3262
  }
3263
+ function hasWin32UnsafeOpenerChars(url) {
3264
+ if (url.includes('"') || url.includes("%"))
3265
+ return true;
3266
+ for (let i = 0; i < url.length; i++) {
3267
+ const code = url.charCodeAt(i);
3268
+ if (code <= 31 || code === 127)
3269
+ return true;
3270
+ }
3271
+ return false;
3272
+ }
3275
3273
  var BROWSER_OPEN_GRACE_MS = 3e3;
3276
- async function openBrowser(url, env = process.env) {
3274
+ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl = spawn) {
3277
3275
  if ((env["HALFCYCLE_NO_BROWSER"] ?? "").trim() !== "") {
3278
3276
  return { opened: false, reason: "HALFCYCLE_NO_BROWSER is set in this environment" };
3279
3277
  }
3280
3278
  if (!isOpenableUrl(url)) {
3281
3279
  return { opened: false, reason: "the sign-in address is not an http(s) URL this CLI will open" };
3282
3280
  }
3283
- const { command, args: args2 } = browserOpenCommand(platform3(), url);
3281
+ if (os === "win32" && hasWin32UnsafeOpenerChars(url)) {
3282
+ return {
3283
+ opened: false,
3284
+ reason: 'the sign-in address holds a " or %, or a control character, this opener will not pass to cmd.exe on Windows'
3285
+ };
3286
+ }
3287
+ const { command, args: args2, windowsVerbatimArguments } = browserOpenCommand(os, url);
3284
3288
  return new Promise((resolve4) => {
3285
3289
  let child;
3286
3290
  try {
3287
- child = spawn(command, args2, { stdio: "ignore", env });
3291
+ child = spawnImpl(command, args2, { stdio: "ignore", env, windowsVerbatimArguments });
3288
3292
  } catch (err) {
3289
3293
  resolve4({
3290
3294
  opened: false,
@@ -3356,82 +3360,107 @@ function tryRefusal(body) {
3356
3360
  }
3357
3361
  return { status: candidate.status, message: candidate.message };
3358
3362
  }
3363
+ async function askControlOnce(tokenUrl, started) {
3364
+ let body;
3365
+ try {
3366
+ const res = await fetch(tokenUrl, {
3367
+ method: "POST",
3368
+ headers: { "content-type": "application/json" },
3369
+ body: JSON.stringify({ deviceCode: started.deviceCode })
3370
+ });
3371
+ body = await res.json().catch(() => null);
3372
+ } catch {
3373
+ return null;
3374
+ }
3375
+ const parsed = devicePollResponseSchema.safeParse(body);
3376
+ return parsed.success ? parsed.data : null;
3377
+ }
3378
+ function tokenUrlFor(serviceUrl) {
3379
+ return `${serviceUrl.replace(/\/+$/, "")}/auth/device/token`;
3380
+ }
3381
+ function localDeadline(started) {
3382
+ return new Date(started.expiresAt).getTime() + started.pollIntervalMs * DEADLINE_SLACK_INTERVALS;
3383
+ }
3359
3384
  async function pollForCredential(serviceUrl, started, deps = {}) {
3360
3385
  const write = deps.write ?? defaultWrite;
3361
3386
  const sleep = deps.sleep ?? defaultSleep;
3362
3387
  const now = deps.now ?? Date.now;
3363
- const url = `${serviceUrl.replace(/\/+$/, "")}/auth/device/token`;
3388
+ const url = tokenUrlFor(serviceUrl);
3364
3389
  const interval = started.pollIntervalMs;
3365
- const deadline = new Date(started.expiresAt).getTime() + interval * DEADLINE_SLACK_INTERVALS;
3390
+ const deadline = localDeadline(started);
3366
3391
  const startedAt = now();
3367
3392
  let announcedAt = startedAt;
3368
3393
  for (; ; ) {
3369
- let body;
3370
- let reachable = true;
3371
- try {
3372
- const res = await fetch(url, {
3373
- method: "POST",
3374
- headers: { "content-type": "application/json" },
3375
- body: JSON.stringify({ deviceCode: started.deviceCode })
3376
- });
3377
- body = await res.json().catch(() => null);
3378
- } catch {
3379
- reachable = false;
3380
- }
3381
- if (reachable) {
3382
- const parsed = devicePollResponseSchema.safeParse(body);
3383
- if (parsed.success) {
3384
- const answer = parsed.data;
3385
- if (answer.status === DEVICE_AUTH_STATUS.APPROVED)
3386
- return answer;
3387
- if (answer.status !== DEVICE_AUTH_STATUS.PENDING) {
3388
- throw new SignInRefused(answer.status, answer.message);
3389
- }
3394
+ const answer = await askControlOnce(url, started);
3395
+ if (answer !== null) {
3396
+ if (answer.status === DEVICE_AUTH_STATUS.APPROVED)
3397
+ return answer;
3398
+ if (answer.status !== DEVICE_AUTH_STATUS.PENDING) {
3399
+ throw new SignInRefused(answer.status, answer.message);
3390
3400
  }
3391
3401
  }
3392
3402
  if (now() >= deadline) {
3393
3403
  throw new SignInRefused(SIGN_IN_TIMED_OUT, SIGN_IN_TIMEOUT_MESSAGE);
3394
3404
  }
3395
- if (now() - announcedAt >= STILL_WAITING_MS) {
3405
+ if (now() - announcedAt >= (deps.stillWaitingMs ?? STILL_WAITING_MS)) {
3396
3406
  announcedAt = now();
3397
- write(`[halfcycle] Still waiting for the browser (${Math.round((announcedAt - startedAt) / 1e3)}s). Code: ${started.userCode}
3398
- `);
3407
+ write(stillWaitingLine(announcedAt - startedAt, started));
3399
3408
  }
3400
3409
  await sleep(interval);
3401
3410
  }
3402
3411
  }
3412
+ function stillWaitingLine(elapsedMs, started) {
3413
+ return `[halfcycle] Still waiting for the browser (${Math.round(elapsedMs / 1e3)}s). Code: ${started.userCode}
3414
+ `;
3415
+ }
3403
3416
  var LOOPBACK_TIMEOUT_MESSAGE = "Sign-in timed out: the browser never came back to this terminal. Nothing was created. Run the command again \u2014 or, if this machine cannot hand the browser back (a remote session, a container), run it with HALFCYCLE_NO_BROWSER=1 to use the code-based sign-in instead.";
3404
- var LOOPBACK_COLLECT_ATTEMPTS = 3;
3405
- async function collectDecidedCredential(serviceUrl, started, deps) {
3406
- const sleep = deps.sleep ?? defaultSleep;
3407
- const url = `${serviceUrl.replace(/\/+$/, "")}/auth/device/token`;
3408
- let lastProblem = LOOPBACK_TIMEOUT_MESSAGE;
3409
- for (let attempt = 0; attempt < LOOPBACK_COLLECT_ATTEMPTS; attempt++) {
3410
- if (attempt > 0)
3411
- await sleep(started.pollIntervalMs);
3412
- let body;
3413
- try {
3414
- const res = await fetch(url, {
3415
- method: "POST",
3416
- headers: { "content-type": "application/json" },
3417
- body: JSON.stringify({ deviceCode: started.deviceCode })
3418
- });
3419
- body = await res.json().catch(() => null);
3420
- } catch (err) {
3421
- 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.`;
3422
- continue;
3417
+ async function pollBesideLoopback(serviceUrl, started, wake, deps) {
3418
+ const sleep = deps.sleep;
3419
+ const now = deps.now ?? Date.now;
3420
+ const url = tokenUrlFor(serviceUrl);
3421
+ const deadline = localDeadline(started);
3422
+ let arrived = false;
3423
+ let spent = false;
3424
+ let interrupt = null;
3425
+ void wake.then(() => {
3426
+ arrived = true;
3427
+ if (interrupt !== null)
3428
+ interrupt();
3429
+ });
3430
+ for (; ; ) {
3431
+ const answer = await askControlOnce(url, started);
3432
+ if (answer !== null) {
3433
+ if (answer.status === DEVICE_AUTH_STATUS.APPROVED)
3434
+ return answer;
3435
+ if (answer.status !== DEVICE_AUTH_STATUS.PENDING) {
3436
+ throw new SignInRefused(answer.status, answer.message);
3437
+ }
3438
+ }
3439
+ if (now() >= deadline) {
3440
+ throw new SignInRefused(SIGN_IN_TIMED_OUT, LOOPBACK_TIMEOUT_MESSAGE);
3423
3441
  }
3424
- const parsed = devicePollResponseSchema.safeParse(body);
3425
- if (!parsed.success)
3442
+ if (arrived && !spent) {
3443
+ spent = true;
3426
3444
  continue;
3427
- const answer = parsed.data;
3428
- if (answer.status === DEVICE_AUTH_STATUS.APPROVED)
3429
- return answer;
3430
- if (answer.status !== DEVICE_AUTH_STATUS.PENDING) {
3431
- throw new SignInRefused(answer.status, answer.message);
3432
3445
  }
3446
+ if (sleep === void 0) {
3447
+ await new Promise((resolve4) => {
3448
+ const timer = setTimeout(resolve4, started.pollIntervalMs);
3449
+ interrupt = () => {
3450
+ clearTimeout(timer);
3451
+ resolve4();
3452
+ };
3453
+ });
3454
+ } else {
3455
+ await new Promise((resolve4) => {
3456
+ interrupt = resolve4;
3457
+ void sleep(started.pollIntervalMs).then(resolve4);
3458
+ });
3459
+ }
3460
+ interrupt = null;
3461
+ if (arrived)
3462
+ spent = true;
3433
3463
  }
3434
- throw new SignInRefused(SIGN_IN_TIMED_OUT, lastProblem);
3435
3464
  }
3436
3465
  async function signIn(serviceUrl, deps = {}) {
3437
3466
  const write = deps.write ?? defaultWrite;
@@ -3456,24 +3485,16 @@ async function signIn(serviceUrl, deps = {}) {
3456
3485
  const url = loopbackVerificationUrl(started.verificationUrl, bind.target);
3457
3486
  const attempt = await openBrowser(url, env);
3458
3487
  if (attempt.opened) {
3459
- write(`[halfcycle] Opened your browser to finish signing in. This terminal is waiting.
3488
+ write(`[halfcycle] Opened your browser to finish signing in. Code: ${started.userCode}. This terminal is waiting.
3460
3489
  `);
3461
- const deadline = new Date(started.expiresAt).getTime() + started.pollIntervalMs * DEADLINE_SLACK_INTERVALS;
3462
- const callback = await bind.waitForCallback({
3463
- timeoutMs: deadline - now(),
3464
- onStillWaiting: (elapsed) => (
3465
- // NO CODE IN THIS LINE. The device-code path prints the code here
3466
- // because a human is comparing it; on this path there is nothing to
3467
- // compare and criterion 1 is that none is displayed AT ALL.
3468
- write(`[halfcycle] Still waiting for the browser (${Math.round(elapsed / 1e3)}s).
3469
- `)
3470
- )
3490
+ const startedAt = now();
3491
+ const redirect = bind.waitForCallback({
3492
+ timeoutMs: localDeadline(started) - startedAt,
3493
+ onStillWaiting: (elapsed) => write(stillWaitingLine(elapsed, started)),
3494
+ stillWaitingMs: deps.stillWaitingMs ?? STILL_WAITING_MS
3471
3495
  });
3496
+ const approved = await pollBesideLoopback(serviceUrl, started, redirect.then(() => void 0), deps);
3472
3497
  await closeListener();
3473
- if (callback === null) {
3474
- throw new SignInRefused(SIGN_IN_TIMED_OUT, LOOPBACK_TIMEOUT_MESSAGE);
3475
- }
3476
- const approved = await collectDecidedCredential(serviceUrl, started, deps);
3477
3498
  write(`[halfcycle] Signed in as account ${approved.accountId} \u2014 continuing.
3478
3499
  `);
3479
3500
  return {
@@ -6209,10 +6230,13 @@ ${USAGE}`);
6209
6230
  if (result.notShared)
6210
6231
  process.stdout.write(`${renderNotShared(result.notShared)}
6211
6232
  `);
6233
+ if (credential) {
6234
+ process.stdout.write("[halfcycle] Commit the files Halfcycle set up here, so every copy of this project runs the same checks.\n");
6235
+ }
6212
6236
  if (credential) {
6213
6237
  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
6214
6238
  `);
6215
- process.stdout.write(`[halfcycle] Claude Code will also ask permission the first time Halfcycle needs to look up your next step. Choose allow \u2014 without it nothing can run.
6239
+ process.stdout.write(`[halfcycle] Claude Code will then ask whether to use a new MCP server. The cursor starts on "Continue without using this MCP server" \u2014 choose to use it instead; without it Halfcycle will not connect.
6216
6240
  `);
6217
6241
  }
6218
6242
  if (credential) {