halfcycle 0.3.36 → 0.3.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -205,6 +205,15 @@ function looksLikeSecret(text) {
205
205
  return false;
206
206
  }
207
207
 
208
+ // ../core/dist/mcp-registration.js
209
+ var MCP_REGISTRATION_REL = ".mcp.json";
210
+ var MCP_SERVER_KEY = "halfcycle";
211
+
212
+ // ../core/dist/shell-quote.js
213
+ function quoteForShell(value) {
214
+ return `'${value.replace(/'/g, `'\\''`)}'`;
215
+ }
216
+
208
217
  // ../events/dist/result.js
209
218
  import { z as z2 } from "zod";
210
219
  var firedGuardSchema = z2.object({
@@ -574,9 +583,14 @@ var closedBySchema = z4.discriminatedUnion("kind", [
574
583
  z4.object({ kind: z4.literal("account-removed") }).strict(),
575
584
  z4.object({ kind: z4.literal("not-recorded") }).strict()
576
585
  ]);
586
+ var STEP_NAME_MAX = 200;
587
+ var stepNameSchema = plainText(STEP_NAME_MAX).refine((v) => v.trim().length > 0, {
588
+ message: "must not be blank"
589
+ });
577
590
  var unfinishedStepSchema = z4.object({
578
591
  stepId: plainText(),
579
- state: z4.enum(["parked", "failed"])
592
+ state: z4.enum(["parked", "failed"]),
593
+ name: stepNameSchema.optional()
580
594
  }).strict();
581
595
  var crossedSchema = z4.discriminatedUnion("kind", [
582
596
  z4.object({ kind: z4.literal("none") }).strict(),
@@ -994,13 +1008,28 @@ var engagementCredentialResponseSchema = z7.object({
994
1008
  sessionToken: z7.string(),
995
1009
  mcpUrl: z7.string(),
996
1010
  guardUrl: z7.string(),
997
- controlTelemetryUrl: z7.string()
1011
+ controlTelemetryUrl: z7.string(),
1012
+ methodVersion: z7.string().optional(),
1013
+ consoleUrl: z7.string().optional()
998
1014
  }).strict();
999
1015
  var ciTokenExchangeResponseSchema = z7.object({
1000
1016
  token: z7.string(),
1001
1017
  expiresAt: utcIso8601,
1002
1018
  engagementId: z7.string()
1003
1019
  }).strict();
1020
+ var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
1021
+ var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
1022
+ var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
1023
+ var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
1024
+ 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.';
1025
+ 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}.`;
1026
+ 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.";
1027
+ 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.";
1028
+ var PLACEHOLDER_SENTENCES = Object.freeze({
1029
+ [NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
1030
+ [NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
1031
+ [UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
1032
+ });
1004
1033
 
1005
1034
  // ../events/dist/phase-record.js
1006
1035
  import { z as z8 } from "zod";
@@ -1720,7 +1749,7 @@ var WRITE_ALLOWLIST = [
1720
1749
  ".halfcycle/bin",
1721
1750
  "docs/build-records",
1722
1751
  "test/fixtures/captured",
1723
- ".mcp.json",
1752
+ MCP_REGISTRATION_REL,
1724
1753
  ".gitattributes",
1725
1754
  ZONE_B_DIR
1726
1755
  ];
@@ -2093,6 +2122,20 @@ exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
2093
2122
  }
2094
2123
  var GENERATED_SESSION_START_HEADER = "# Session-start marker \u2014 generated by";
2095
2124
  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)"`;
2125
+ 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)"`;
2126
+ function noNodeReplyShell() {
2127
+ const reply = JSON.stringify({
2128
+ systemMessage: NO_NODE_SENTENCE,
2129
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: NO_NODE_SENTENCE }
2130
+ });
2131
+ if (reply.includes("'")) {
2132
+ throw new Error("[bundle install] the no-Node notice cannot be single-quoted; refusing to write the session-start script.");
2133
+ }
2134
+ return `if ! command -v node >/dev/null 2>&1; then
2135
+ printf '%s\\n' '${reply}'
2136
+ exit 0
2137
+ fi`;
2138
+ }
2096
2139
  function generateSessionStartMarker() {
2097
2140
  return `#!/usr/bin/env bash
2098
2141
  ${GENERATED_SESSION_START_HEADER} the Halfcycle installer.
@@ -2101,10 +2144,15 @@ ${GENERATED_SESSION_START_HEADER} the Halfcycle installer.
2101
2144
  # it began instead of only against the latest commit. The session id comes in on
2102
2145
  # stdin with the hook input; without one, the older session-less name is written
2103
2146
  # and the stop-time check compares against the latest commit as it used to.
2147
+ #
2148
+ # Without Node nothing Halfcycle installs here can run, so this says so once and
2149
+ # writes no marker.
2150
+ ${noNodeReplyShell()}
2104
2151
  INPUT="$(cat 2>/dev/null || true)"
2105
2152
  REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
2106
2153
  if [ -z "$REPO_ROOT" ]; then exit 0; fi
2107
- HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
2154
+ ${REPO_HASH_FROM_ROOT_SH}
2155
+ if [ -z "$HASH" ]; then exit 0; fi
2108
2156
  ${SESSION_ID_FROM_HOOK_INPUT_SH}
2109
2157
  MARKER_DIR="\${TMPDIR:-/tmp}"
2110
2158
  MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
@@ -2114,64 +2162,6 @@ else
2114
2162
  MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}.ref"
2115
2163
  fi
2116
2164
  git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
2117
-
2118
- # ---------------------------------------------------------------------------
2119
- # STANDING GUARD-COVERAGE STATEMENT (T-27; two arms since T-02b).
2120
- #
2121
- # A repository with no credential on this machine is UNGUARDED: the PostToolUse
2122
- # hook will find nothing to evaluate with, and it says so once per edit, after
2123
- # the edit. This says it once, up front, before anything is written \u2014 which is
2124
- # the one thing a session-start hook can do that nothing else can.
2125
- #
2126
- # TWO REASONS TO HAVE NO CREDENTIAL, TWO DIFFERENT THINGS TO SAY (T-02b,
2127
- # solo-project-ownership \u2014 outcome B). "You have not signed in here yet" is
2128
- # fixed by re-running the installer; "this is not your project" is permanent,
2129
- # and the same remedy would send the reader in a circle forever. The two are
2130
- # told apart by HALFCYCLE_ENV_NOT_THIS_ACCOUNT, set by \`halfcycle_env_file\`
2131
- # below from a machine-level marker beside the credential store.
2132
- #
2133
- # Everything below runs in a SUBSHELL: it sources a credential store, and a
2134
- # session-start hook must not leak an engagement's variables into whatever the
2135
- # editor runs next. Any failure inside it is swallowed; this script's exit
2136
- # status is 0 either way, and the marker above has already been written.
2137
- # ---------------------------------------------------------------------------
2138
- ${engagementResolutionShell()}
2139
-
2140
- (
2141
- if halfcycle_env_file "$REPO_ROOT"; then
2142
- set -a
2143
- . "$HALFCYCLE_ENV_FILE"
2144
- set +a
2145
- fi
2146
- if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
2147
- # OUTCOME B (T-02b, solo-project-ownership). This is not the missing-keys
2148
- # case below wearing different words: there is nothing to fix, and the loop
2149
- # that follows is skipped entirely rather than run and then overridden \u2014 a
2150
- # repository whose credential is absent for THIS reason must never be told
2151
- # to re-run the installer, which can never obtain one for it.
2152
- echo "[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked."
2153
- echo "[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is"
2154
- echo "[halfcycle] evaluated, and nothing you do is reported as having passed."
2155
- echo "[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming."
2156
- echo "[halfcycle] Ask the project's owner for view access."
2157
- echo "[halfcycle] If this account is wrong, sign in as the one that owns the project and run"
2158
- echo "[halfcycle] \\"npx halfcycle\\" again."
2159
- else
2160
- hc_missing=""
2161
- for hc_key in ${GUARD_COVERAGE_REQUIRED_KEYS.join(" ")}; do
2162
- eval "hc_value=\\\${$hc_key:-}"
2163
- if [ -z "$hc_value" ]; then hc_missing="$hc_missing $hc_key"; fi
2164
- done
2165
- if [ -n "$hc_missing" ]; then
2166
- echo "[halfcycle] NO GUARD COVERAGE IN THIS REPOSITORY. Nothing will evaluate the edits made in"
2167
- echo "[halfcycle] this session: this machine has no Halfcycle credential for it, so the guard hook"
2168
- echo "[halfcycle] has nothing to call with (missing:$hc_missing)."
2169
- echo "[halfcycle] Run \\"npx halfcycle\\" in this repository to fix it \u2014 it signs you in through your"
2170
- echo "[halfcycle] browser if needed, writes the credential to \\$HOME/.halfcycle outside this tree,"
2171
- echo "[halfcycle] and needs nothing configured first."
2172
- fi
2173
- fi
2174
- ) 2>/dev/null || true
2175
2165
  exit 0
2176
2166
  `;
2177
2167
  }
@@ -2187,7 +2177,8 @@ INPUT="$(cat 2>/dev/null || true)"
2187
2177
 
2188
2178
  REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
2189
2179
  if [ -z "$REPO_ROOT" ]; then exit 0; fi
2190
- HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
2180
+ ${REPO_HASH_FROM_ROOT_SH}
2181
+ if [ -z "$HASH" ]; then exit 0; fi
2191
2182
 
2192
2183
  # The session id arrives on stdin as part of the hook payload. Empty when absent.
2193
2184
  ${SESSION_ID_FROM_HOOK_INPUT_SH}
@@ -2307,8 +2298,6 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
2307
2298
  }
2308
2299
  return "removed";
2309
2300
  }
2310
- var MCP_REGISTRATION_REL = ".mcp.json";
2311
- var MCP_SERVER_KEY = "halfcycle";
2312
2301
  var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
2313
2302
  `const p=require('path'),f=require('fs'),c=require('child_process');`,
2314
2303
  `let d=process.cwd();`,
@@ -2451,9 +2440,10 @@ function previousPinForEngagement(targetRepoRoot, engagementId) {
2451
2440
  function nonEmpty(value) {
2452
2441
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
2453
2442
  }
2454
- function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, accountId, writtenPaths) {
2443
+ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, accountId, methodVersion, writtenPaths) {
2455
2444
  const previous = previousPinForEngagement(targetRepoRoot, engagementId);
2456
2445
  const carried = nonEmpty(accountId) ?? nonEmpty(previous?.accountId);
2446
+ const carriedMethodVersion = nonEmpty(methodVersion) ?? nonEmpty(previous?.methodVersion);
2457
2447
  const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
2458
2448
  const projectPage = projectPageUrl(engagementId);
2459
2449
  const pin = {
@@ -2462,6 +2452,7 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, a
2462
2452
  engagementType,
2463
2453
  installedAt,
2464
2454
  ...carried !== void 0 ? { accountId: carried } : {},
2455
+ ...carriedMethodVersion !== void 0 ? { methodVersion: carriedMethodVersion } : {},
2465
2456
  ...projectPage !== void 0 ? { projectPage } : {}
2466
2457
  };
2467
2458
  const pinPath = join5(targetRepoRoot, BUNDLE_PIN_REL);
@@ -2499,7 +2490,7 @@ function checkDrift(targetRepoRoot) {
2499
2490
  const pin = readBundlePin(targetRepoRoot);
2500
2491
  const installed = pin?.version ?? null;
2501
2492
  const current = manifest.version;
2502
- return { drifted: installed !== current, installed, current };
2493
+ return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
2503
2494
  }
2504
2495
  function writeProjectIdentity(targetRepo) {
2505
2496
  const path = join5(targetRepo, PROJECT_IDENTITY_REL);
@@ -2730,7 +2721,7 @@ async function install(options) {
2730
2721
  case "absent":
2731
2722
  break;
2732
2723
  }
2733
- writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, accountId, report.writtenPaths);
2724
+ writeBundlePin(targetRepo, manifest.version, engagementId, engagementType, accountId, credential?.methodVersion, report.writtenPaths);
2734
2725
  noteFile(draft, targetRepo, BUNDLE_PIN_REL, "written");
2735
2726
  noteFile(draft, targetRepo, CREW_ROSTER_REL, writeCrewRoster(targetRepo, report));
2736
2727
  const scanResult = runBootstrapScan(targetRepo);
@@ -2820,7 +2811,7 @@ function planeRefusal(detail) {
2820
2811
  reason: typeof candidate.reason === "string" ? candidate.reason : void 0
2821
2812
  };
2822
2813
  }
2823
- var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}; it must be the CONTROL origin \u2014 the one that serves /engagements and /board/enter-codes. The MCP server runs at a different address, and the platform supplies that one itself; you configure neither.`;
2814
+ var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}; it must be the CONTROL origin \u2014 the one that serves /engagements. The MCP server runs at a different address, and the platform supplies that one itself; you configure neither.`;
2824
2815
  async function createEngagement(baseUrl, name, credential, derivedName) {
2825
2816
  return requestEngagementValues(
2826
2817
  `${baseUrl.replace(/\/+$/, "")}/engagements`,
@@ -2893,43 +2884,16 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
2893
2884
  }).join(", ");
2894
2885
  throw new Error(`[bundle install] ${shape.action} response from ${url} is not the credential response this CLI is built to read (${fields}). ${shape.nothingHappened}. Either that service is a different version from this installer, or ${url} is not a Halfcycle service.`);
2895
2886
  }
2887
+ const consoleUrl = typeof body.consoleUrl === "string" && body.consoleUrl.trim() !== "" ? body.consoleUrl.trim() : void 0;
2888
+ const methodVersion = typeof body.methodVersion === "string" && body.methodVersion.trim() !== "" ? body.methodVersion.trim() : void 0;
2896
2889
  return {
2897
2890
  engagementId: body.engagementId,
2898
2891
  sessionToken: body.sessionToken,
2899
2892
  mcpUrl: body.mcpUrl.trim().replace(/\/+$/, ""),
2900
2893
  guardUrl: body.guardUrl.trim().replace(/\/+$/, ""),
2901
- ...controlTelemetryUrl !== void 0 ? { controlTelemetryUrl } : {}
2902
- };
2903
- }
2904
-
2905
- // dist/mint-board-code.js
2906
- async function mintBoardEnterCode(baseUrl, sessionToken) {
2907
- const url = `${baseUrl.replace(/\/+$/, "")}/board/enter-codes`;
2908
- let res;
2909
- try {
2910
- res = await fetch(url, {
2911
- method: "POST",
2912
- headers: {
2913
- "content-type": "application/json",
2914
- authorization: `Bearer ${sessionToken}`
2915
- },
2916
- body: "{}"
2917
- });
2918
- } catch (err) {
2919
- throw new Error(`[bundle install] Could not reach the Halfcycle board mint endpoint at ${url}: ${err instanceof Error ? err.message : String(err)}.`);
2920
- }
2921
- if (!res.ok) {
2922
- throw new Error(`[bundle install] Board code mint failed: ${url} returned ${res.status}.`);
2923
- }
2924
- const body = await res.json().catch(() => null);
2925
- if (!body || typeof body.enterCode !== "string" || typeof body.engagementId !== "string" || typeof body.boardUrl !== "string" || typeof body.expiresAt !== "string") {
2926
- throw new Error(`[bundle install] Board code mint response from ${url} did not carry {enterCode, engagementId, boardUrl, expiresAt}.`);
2927
- }
2928
- return {
2929
- enterCode: body.enterCode,
2930
- engagementId: body.engagementId,
2931
- boardUrl: body.boardUrl,
2932
- expiresAt: body.expiresAt
2894
+ ...controlTelemetryUrl !== void 0 ? { controlTelemetryUrl } : {},
2895
+ ...consoleUrl !== void 0 ? { consoleUrl } : {},
2896
+ ...methodVersion !== void 0 ? { methodVersion } : {}
2933
2897
  };
2934
2898
  }
2935
2899
 
@@ -3384,20 +3348,127 @@ function buildEngagementRecord(input) {
3384
3348
 
3385
3349
  // dist/build-record/render-v2.js
3386
3350
  var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
3387
- var MARKDOWN_SPECIAL = /* @__PURE__ */ new Set(["\\", "`", "*", "_", "[", "]", "<", "|", "~", "#", "&", "$"]);
3351
+ var SPACE = new RegExp("\\p{Zs}", "u");
3352
+ var WORD = /[\p{L}\p{N}]/u;
3353
+ var ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/;
3354
+ var CHARACTER_REFERENCE = /^#?[A-Za-z0-9]+;/;
3355
+ var PAIRED = ["*", "_", "~", "$"];
3356
+ function codeSpanMask(cps) {
3357
+ const mask = cps.map(() => false);
3358
+ const runs = [];
3359
+ for (let i = 0; i < cps.length; ) {
3360
+ if (cps[i] !== "`") {
3361
+ i++;
3362
+ continue;
3363
+ }
3364
+ let j = i;
3365
+ while (j < cps.length && cps[j] === "`")
3366
+ j++;
3367
+ runs.push({ start: i, length: j - i });
3368
+ i = j;
3369
+ }
3370
+ for (let k = 0; k < runs.length; ) {
3371
+ const open = runs[k];
3372
+ let m = k + 1;
3373
+ while (m < runs.length && runs[m].length !== open.length)
3374
+ m++;
3375
+ if (m === runs.length) {
3376
+ k++;
3377
+ continue;
3378
+ }
3379
+ const close = runs[m];
3380
+ for (let i = open.start; i < close.start + close.length; i++)
3381
+ mask[i] = true;
3382
+ k = m + 1;
3383
+ }
3384
+ return mask;
3385
+ }
3386
+ function escapeMask(cps, inSpan) {
3387
+ const n = cps.length;
3388
+ const escape = cps.map(() => false);
3389
+ const isSpace = (i) => i >= 0 && i < n && SPACE.test(cps[i]) && !HIDDEN.test(cps[i]);
3390
+ const isWord = (i) => i >= 0 && i < n && WORD.test(cps[i]);
3391
+ const rest = (i) => cps.slice(i).join("");
3392
+ for (let i = 0; i < n; i++) {
3393
+ if (inSpan[i])
3394
+ continue;
3395
+ const ch = cps[i];
3396
+ const next = i + 1 < n ? cps[i + 1] : null;
3397
+ switch (ch) {
3398
+ case "\\":
3399
+ escape[i] = next === null || ASCII_PUNCTUATION.test(next);
3400
+ break;
3401
+ case "<":
3402
+ escape[i] = next !== null && !isSpace(i + 1) && rest(i + 1).includes(">");
3403
+ break;
3404
+ case "&":
3405
+ escape[i] = CHARACTER_REFERENCE.test(rest(i + 1));
3406
+ break;
3407
+ case "]":
3408
+ escape[i] = next === "(";
3409
+ break;
3410
+ case "[":
3411
+ case "{":
3412
+ escape[i] = (next === "+" || next === "-") && (rest(i + 2).includes(`${next}]`) || rest(i + 2).includes(`${next}}`));
3413
+ break;
3414
+ default:
3415
+ break;
3416
+ }
3417
+ }
3418
+ for (let i = 0; i < n; ) {
3419
+ if (inSpan[i] || cps[i] !== "#") {
3420
+ i++;
3421
+ continue;
3422
+ }
3423
+ let j = i;
3424
+ while (j < n && cps[j] === "#" && !inSpan[j])
3425
+ j++;
3426
+ let k = j;
3427
+ while (k < n && isSpace(k))
3428
+ k++;
3429
+ if (k === n && (i === 0 || isSpace(i - 1)))
3430
+ for (let m = i; m < j; m++)
3431
+ escape[m] = true;
3432
+ i = j;
3433
+ }
3434
+ for (const c of PAIRED) {
3435
+ const live = [];
3436
+ for (let i = 0; i < n; ) {
3437
+ if (inSpan[i] || cps[i] !== c) {
3438
+ i++;
3439
+ continue;
3440
+ }
3441
+ let j = i;
3442
+ while (j < n && cps[j] === c && !inSpan[j])
3443
+ j++;
3444
+ const inert = c !== "$" && (isSpace(i - 1) && isSpace(j) || c === "_" && isWord(i - 1) && isWord(j));
3445
+ if (!inert)
3446
+ live.push({ start: i, end: j });
3447
+ i = j;
3448
+ }
3449
+ if (live.length < 2)
3450
+ continue;
3451
+ for (const run of live)
3452
+ for (let m = run.start; m < run.end; m++)
3453
+ escape[m] = true;
3454
+ }
3455
+ return escape;
3456
+ }
3388
3457
  function printRecordText(text) {
3389
3458
  if (text === "")
3390
3459
  return "(blank)";
3460
+ const cps = [...text];
3461
+ const escape = escapeMask(cps, codeSpanMask(cps));
3391
3462
  let out = "";
3392
- for (const ch of text) {
3463
+ cps.forEach((ch, i) => {
3393
3464
  if (HIDDEN.test(ch)) {
3394
3465
  out += `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
3395
- } else if (MARKDOWN_SPECIAL.has(ch)) {
3466
+ } else if (escape[i]) {
3396
3467
  out += `\\${ch}`;
3397
3468
  } else {
3398
3469
  out += ch;
3399
3470
  }
3400
- }
3471
+ });
3401
3472
  return out;
3402
3473
  }
3403
3474
  function makePrinter() {
@@ -3487,12 +3558,18 @@ function headline(observed, attested, checks) {
3487
3558
  if (problemsReported) {
3488
3559
  return "**Closed with problems reported.** Halfcycle saw nothing outstanding, and the person closing the phase reported problems found in its acceptance walk.";
3489
3560
  }
3490
- return checks === "not-observed" ? "**Closed with nothing outstanding, but no checks ran.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met, but Halfcycle has no record of any check running during this phase, so this is not a clean result." : "**Closed cleanly.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met.";
3561
+ return checks === "not-observed" ? "**Closed with nothing outstanding, but no check on record.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met, but Halfcycle has no record of any check running during this phase, so this is not a clean result." : "**Closed cleanly.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met.";
3491
3562
  case "unfinished-steps":
3492
3563
  case "clean-over-findings":
3493
- return met ? "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase stated its finish conditions were met anyway." : "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase chose to override it.";
3564
+ if (!met) {
3565
+ return "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase chose to override it.";
3566
+ }
3567
+ return problemsReported ? "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase closed it as it stood, reporting problems found in its acceptance walk." : "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase stated its finish conditions were met anyway.";
3494
3568
  case "not-recorded":
3495
- return met ? "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase stated its finish conditions were met." : "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase chose to override.";
3569
+ if (!met) {
3570
+ return "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase chose to override.";
3571
+ }
3572
+ return problemsReported ? "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase reported problems found in its acceptance walk." : "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase stated its finish conditions were met.";
3496
3573
  }
3497
3574
  }
3498
3575
  function closedAtText(closedAt, p) {
@@ -3511,30 +3588,38 @@ function signedInAs(closedBy, p) {
3511
3588
  return "not recorded for this close";
3512
3589
  }
3513
3590
  }
3591
+ var OUTSTANDING_LABEL = "- Outstanding work in Halfcycle's own records of this phase (the method's steps, and problems recorded against the phase): ";
3514
3592
  function outstanding(crossed, p) {
3515
3593
  switch (crossed.kind) {
3516
3594
  case "none":
3517
- return ["- Outstanding work Halfcycle could see: none."];
3595
+ return [`${OUTSTANDING_LABEL}none.`];
3518
3596
  case "not-recorded":
3519
- return ["- Outstanding work Halfcycle could see: not recorded for this close."];
3597
+ return [`${OUTSTANDING_LABEL}not recorded for this close.`];
3520
3598
  case "clean-over-findings":
3521
- return [
3522
- `- Outstanding work Halfcycle could see: ${counted(crossed.findings, "problem", "problems")} recorded against this phase.`
3523
- ];
3599
+ return [`${OUTSTANDING_LABEL}${counted(crossed.findings, "problem", "problems")} recorded against this phase.`];
3524
3600
  case "unfinished-steps":
3525
3601
  return [
3526
- `- Outstanding work Halfcycle could see: ${counted(crossed.steps.length, "step", "steps")} of the method left unfinished. Each is shown by the method's own label for it:`,
3527
- ...crossed.steps.map((step) => ` - Step: ${p.text(step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
3602
+ `${OUTSTANDING_LABEL}${counted(crossed.steps.length, "step", "steps")} of the method left unfinished. ${shownBy(crossed.steps)}`,
3603
+ ...crossed.steps.map((step) => ` - Step: ${p.text(step.name ?? step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
3528
3604
  ];
3529
3605
  }
3530
3606
  }
3531
- function machines(coverage) {
3607
+ function shownBy(steps) {
3608
+ const named = steps.filter((s) => s.name !== void 0).length;
3609
+ if (named === 0)
3610
+ return "Each is shown by the method's own label for it:";
3611
+ if (named === steps.length)
3612
+ return "Each is shown by the method's own name for it:";
3613
+ return "Each is shown by the method's own name for it, or by its label where the record holds no name:";
3614
+ }
3615
+ function machines(coverage, checks) {
3532
3616
  const label = "- Computers signed in to Halfcycle for this project when the phase closed: ";
3533
3617
  if (coverage === null)
3534
3618
  return `${label}not recorded.`;
3535
3619
  if (coverage.enrolled === 0)
3536
3620
  return `${label}none.`;
3537
- return `${label}${String(coverage.enrolled)}. Of these, ${String(coverage.conformant)} had reported Halfcycle's checks as fully set up.`;
3621
+ const line = `${label}${String(coverage.enrolled)}. Of these, ${String(coverage.conformant)} had reported Halfcycle's checks as fully set up.`;
3622
+ return checks === "not-observed" && coverage.conformant > 0 ? `${line} Set up is not the same as running: Halfcycle has no record of any check running during this phase.` : line;
3538
3623
  }
3539
3624
  function walkLines(walk, p) {
3540
3625
  const label = "> - Acceptance walk (the finished work tried as a user would, by someone other than its builder): ";
@@ -3554,9 +3639,15 @@ function walkLines(walk, p) {
3554
3639
  function statedOrNot(value, p) {
3555
3640
  return value === null ? "Not stated by the client." : p.text(value);
3556
3641
  }
3642
+ function decisionLine(attested) {
3643
+ if (attested.decision === "override") {
3644
+ return "> - Decision: close the phase by override, without stating its finish conditions were met.";
3645
+ }
3646
+ return attested.walk !== null && attested.walk.verdict === "defects" ? "> - Decision: close the phase as it stood, with the problems its acceptance walk found." : "> - Decision: the phase's agreed finish conditions were met.";
3647
+ }
3557
3648
  function renderStated(attested, p) {
3558
3649
  const lines = [
3559
- attested.decision === "done-when-met" ? "> - Decision: the phase's agreed finish conditions were met." : "> - Decision: close the phase by override, without stating its finish conditions were met.",
3650
+ decisionLine(attested),
3560
3651
  `> - Name given: ${attested.actor === null ? "removed with the account that made this close" : p.text(attested.actor)}`,
3561
3652
  `> - Reason given: ${attested.reason === null ? "none." : p.text(attested.reason)}`,
3562
3653
  ...walkLines(attested.walk, p),
@@ -3584,7 +3675,7 @@ function renderClose(heading, observed, attested, checks, p) {
3584
3675
  `- Closed at: ${closedAtText(observed.closedAt, p)}`,
3585
3676
  `- Signed in as: ${signedInAs(observed.closedBy, p)}`,
3586
3677
  ...outstanding(observed.crossed, p),
3587
- machines(observed.seatCoverage)
3678
+ machines(observed.seatCoverage, checks)
3588
3679
  ].join("\n"),
3589
3680
  STATED_LABEL,
3590
3681
  renderStated(attested, p)
@@ -3847,8 +3938,7 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
3847
3938
  };
3848
3939
  }
3849
3940
  function refetchCommand(phase) {
3850
- const quoted = /^[A-Za-z0-9._+-]+$/.test(phase) ? phase : `'${phase.replace(/'/g, `'\\''`)}'`;
3851
- return `npx halfcycle@latest build-record ${quoted}`;
3941
+ return `npx halfcycle@latest build-record ${quoteForShell(phase)}`;
3852
3942
  }
3853
3943
  export {
3854
3944
  BUILD_RECORD_DIR,
@@ -3878,7 +3968,6 @@ export {
3878
3968
  layerDocumentRow,
3879
3969
  mergeSettings,
3880
3970
  mergeWithRecords,
3881
- mintBoardEnterCode,
3882
3971
  mintOrReadIdentity,
3883
3972
  obtainBuildRecord,
3884
3973
  questionsToPut,