halfcycle 0.3.39 → 0.4.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcycle",
3
- "version": "0.3.39",
3
+ "version": "0.4.0",
4
4
  "description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
5
5
  "commands": [
6
6
  {
package/README.md CHANGED
@@ -42,7 +42,7 @@ A method document cannot fix that, because a document cannot interrupt you. This
42
42
 
43
43
  Credit where it is due, and it is due. GStack, Matt Pocock's skills, GitHub Spec Kit, BMAD, OpenSpec and claude-flow are genuinely good, and between them they taught this whole category that the bottleneck is discipline rather than model intelligence. We took two things from them deliberately and say so: the one-question-at-a-time interrogation shape, and an install that is one command and then stays out of your way.
44
44
 
45
- The objection this usually meets is that it must be too much for a small project. It would be, if there were one setting. A folder you download has exactly one, so whoever wrote it had to choose who to disappoint, and it is usually the small project that gets buried and quietly drops the process in week three. This reads the work instead and sizes itself to it, which is the section further down.
45
+ The objection this usually meets is that it must be too much for a small project. It would be, if there were one setting. A folder you download has exactly one, so whoever wrote it had to choose who to disappoint, and it is usually the small project that gets buried and quietly drops the process in week three. Halfcycle sizes each piece of work with a diagnostic before it starts instead, and records which design documents it includes and which it skips, which is the section further down.
46
46
 
47
47
  Writing the process down was never the hard part. What a folder you download structurally cannot have is these three things.
48
48
 
@@ -146,16 +146,14 @@ This matters enough to be specific about.
146
146
 
147
147
  **Halfcycle never holds or proxies your Claude credential.** Inference runs on your own Claude Code, on your own subscription or seats. We are not a hosted runtime reselling you inference.
148
148
 
149
- **Your own Halfcycle credential lives outside this repository, in a file named `env` under your home directory — never in anything you'd commit.** Closing a phase rewrites it, to clear which phase was open, and it is also renewed there automatically to keep your connection current. Nothing else changes it.
149
+ **Your own Halfcycle credential lives outside this repository, in a file named `env` under your home directory — never in anything you'd commit.** Installing writes it. Opening or closing a phase rewrites it, to record which phase is open, and the credential in it is also renewed there automatically to keep your connection current. Uninstalling with `--remove-credential` deletes it. Nothing else changes it.
150
150
 
151
- **What the checks receive is the shape of a change, not its contents.** File paths, structural shapes, environment variable names and how they are used, the tables a write touches, route signatures, outbound host names. There is no file body and no diff in the request.
151
+ **What the checks receive is the shape of a change and short excerpts of the code being edited — never a whole file and never a diff.** File paths, structural shapes, environment variable names and how they are used, the tables a write touches, route signatures, outbound host names. There is no file body and no diff in the request.
152
152
 
153
- **Source excerpts are off, at both ends, for every engagement today.** The wire format does have a field for them, so the honest version of this is a conditional statement rather than a flat one, and it rests on three things rather than on our good intentions: your runner does not attach an excerpt unless you turn a flag on, and that flag is off by default; anything that does arrive for an engagement that has not opted in is stripped on our side before the checks or any model see it; and there is no way to opt in today, because nothing in the product can set that flag. If that ever changes, our privacy policy changes with it, and it is the policy — not this paragraph — that states the guarantee.
153
+ **Short excerpts of the code being edited are sent to Halfcycle to run its checks, under the terms you accepted at sign-in.** Our privacy policy, not this paragraph, states how we handle what you send.
154
154
 
155
155
  **What the readiness audit submits, and what it never carries.** `halfcycle_submit_findings` is how your session sends the audit's answers: a probe's status, a count, a repo-relative path, a short name. Never a file body, a diff or a source excerpt — the wire carries no field for prose, so a path or a name is the most it can carry. What Halfcycle keeps is what your session submitted, the judgement it reached, and which account and machine sign-in the submission came from; the documents themselves never reach it.
156
156
 
157
- **A promise, for the day a check does need to read something you wrote:** what you send it will be read and not kept: the finding and its evidence persist, the source does not, and training or benchmarking on it is foreclosed. It is written here now, labelled as a commitment rather than a feature, so the promise is public before the mechanism is.
158
-
159
157
  **Nothing method-shaped is left behind.** The runbooks, templates and question sets reach the session that asks for them, scoped to the step you are on, and are never at rest in your repository.
160
158
 
161
159
  ## What you keep
@@ -14820,7 +14820,7 @@ function readMcpServerUrl() {
14820
14820
  }
14821
14821
  function readExcerptConfig() {
14822
14822
  const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
14823
- const enabled = (raw ?? "").trim().toLowerCase() === "true";
14823
+ const enabled = (raw ?? "").trim().toLowerCase() !== "false";
14824
14824
  const readCap = (name, fallback) => {
14825
14825
  const v = process.env[name];
14826
14826
  if (v === void 0)
@@ -15358,8 +15358,8 @@ function hookReply(event) {
15358
15358
  }
15359
15359
  };
15360
15360
  }
15361
- function infraWarningText(message) {
15362
- return `[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. The session continues without checks.`;
15361
+ function infraWarningText() {
15362
+ return "[Halfcycle] WARNING: Halfcycle's check service could not be reached, so nothing was checked this time. The session continues without checks.";
15363
15363
  }
15364
15364
  function localFaultText(message) {
15365
15365
  return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
@@ -15701,8 +15701,10 @@ async function attempt(url2, token, request, fetchFn, retry) {
15701
15701
  import { spawnSync } from "node:child_process";
15702
15702
  import { readFileSync } from "node:fs";
15703
15703
  import { relative } from "node:path";
15704
+ var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
15704
15705
  function git(args2, cwd, timeoutMs) {
15705
- const result = spawnSync("git", args2, {
15706
+ const argv = COLOR_AWARE_SUBCOMMANDS.has(args2[0] ?? "") ? [args2[0], "--no-color", ...args2.slice(1)] : args2;
15707
+ const result = spawnSync("git", argv, {
15706
15708
  cwd,
15707
15709
  encoding: "utf-8",
15708
15710
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
@@ -16816,13 +16818,21 @@ function produceExcerpts(diffContent, descriptors, caps) {
16816
16818
  const excerpts = [];
16817
16819
  const seen = /* @__PURE__ */ new Set();
16818
16820
  const lines = diffContent.split("\n");
16819
- let lineNo = 0;
16821
+ let newLineNo;
16820
16822
  for (const raw of lines) {
16821
- if (raw.startsWith("-") && !raw.startsWith("---"))
16823
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
16824
+ if (hunk) {
16825
+ newLineNo = Number.parseInt(hunk[1], 10);
16826
+ continue;
16827
+ }
16828
+ if (newLineNo === void 0)
16829
+ continue;
16830
+ if (raw.startsWith("\\"))
16822
16831
  continue;
16823
- if (raw.startsWith("+++") || raw.startsWith("---"))
16832
+ if (raw.startsWith("-"))
16824
16833
  continue;
16825
- lineNo += 1;
16834
+ const lineNo = newLineNo;
16835
+ newLineNo += 1;
16826
16836
  if (!raw.startsWith("+"))
16827
16837
  continue;
16828
16838
  const text = raw.slice(1).trim();
@@ -17092,7 +17102,10 @@ async function runCi() {
17092
17102
  };
17093
17103
  });
17094
17104
  const context = loadChangeSetContext(repoRoot);
17095
- const changeSet = buildChangeSet(fileDiffEntries, { ...context, excerpts: readExcerptConfig() });
17105
+ const changeSet = buildChangeSet(fileDiffEntries, {
17106
+ ...context,
17107
+ excerpts: { enabled: false, ...EXCERPT_DEFAULTS }
17108
+ });
17096
17109
  const configResult = readConfig();
17097
17110
  if (!configResult.config) {
17098
17111
  const oidc = readActionsOidcEnv();
@@ -17668,7 +17681,7 @@ async function evaluateEdit(reply, readSeat) {
17668
17681
  reply.notice(contractFaultText(failure.message));
17669
17682
  return;
17670
17683
  }
17671
- reply.notice(infraWarningText(failure.reason));
17684
+ reply.notice(infraWarningText());
17672
17685
  return;
17673
17686
  }
17674
17687
  const telemetryConfig = readTelemetryConfig();
@@ -17964,7 +17977,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
17964
17977
  reply.notice(contractFaultText(failure.message));
17965
17978
  return;
17966
17979
  }
17967
- reply.notice(infraWarningText(failure.reason));
17980
+ reply.notice(infraWarningText());
17968
17981
  return;
17969
17982
  }
17970
17983
  const telemetryConfig = readTelemetryConfig();
package/dist/bin.js CHANGED
@@ -238,6 +238,7 @@ var MCP_SERVER_KEY = "halfcycle";
238
238
  function quoteForShell(value) {
239
239
  return `'${value.replace(/'/g, `'\\''`)}'`;
240
240
  }
241
+ var HALFCYCLE_COMMAND = "npx halfcycle@latest";
241
242
 
242
243
  // ../events/dist/result.js
243
244
  import { z as z2 } from "zod";
@@ -4593,31 +4594,44 @@ function renderCouldNotRun(entries, p) {
4593
4594
  }
4594
4595
  return lines.join("\n");
4595
4596
  }
4596
- function headline(observed, attested, checks) {
4597
+ function headlineText(observed, attested, checks) {
4597
4598
  const met = attested.decision === "done-when-met";
4598
4599
  const problemsReported = attested.walk !== null && attested.walk.verdict === "defects";
4599
4600
  switch (observed.crossed.kind) {
4600
4601
  case "none":
4601
4602
  if (!met) {
4602
- return "**Closed by override.** Halfcycle saw nothing outstanding, and the person closing the phase chose to override rather than state its finish conditions were met.";
4603
+ return ["Closed by override.", "Halfcycle saw nothing outstanding, and the person closing the phase chose to override rather than state its finish conditions were met."];
4603
4604
  }
4604
4605
  if (problemsReported) {
4605
- return "**Closed with problems reported.** Halfcycle saw nothing outstanding, and the person closing the phase reported problems found in its acceptance walk.";
4606
+ return ["Closed with problems reported.", "Halfcycle saw nothing outstanding, and the person closing the phase reported problems found in its acceptance walk."];
4606
4607
  }
4607
- 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.";
4608
+ 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."];
4608
4609
  case "unfinished-steps":
4609
4610
  case "clean-over-findings":
4610
4611
  if (!met) {
4611
- return "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase chose to override it.";
4612
+ return ["Closed over outstanding work.", "Halfcycle saw work still outstanding, and the person closing the phase chose to override it."];
4612
4613
  }
4613
- 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.";
4614
+ 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."];
4614
4615
  case "not-recorded":
4615
4616
  if (!met) {
4616
- return "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase chose to override.";
4617
+ return ["Outstanding work not recorded.", "Halfcycle has no record of what was outstanding at this close. The person closing the phase chose to override."];
4617
4618
  }
4618
- 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.";
4619
+ 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."];
4619
4620
  }
4620
4621
  }
4622
+ function headline(observed, attested, checks) {
4623
+ const [title, sentence] = headlineText(observed, attested, checks);
4624
+ return `**${title}** ${sentence}`;
4625
+ }
4626
+ function latestCloseHeadline(record2) {
4627
+ const attested = new Map(record2.attested.closes.map((c) => [c.sequence, c]));
4628
+ const ordered = [...record2.observed.closes].sort((a, b) => a.sequence - b.sequence);
4629
+ const latest = ordered[ordered.length - 1];
4630
+ const stated = latest === void 0 ? void 0 : attested.get(latest.sequence);
4631
+ if (latest === void 0 || stated === void 0)
4632
+ throw new Error(MISMATCHED_HALVES);
4633
+ return headlineText(latest, stated, record2.observed.checks.state).join(" ");
4634
+ }
4621
4635
  function closedAtText(closedAt, p) {
4622
4636
  const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?Z$/.exec(closedAt);
4623
4637
  return m === null ? p.text(closedAt) : `${m[1]} ${m[2]} UTC`;
@@ -4670,9 +4684,9 @@ function machines(coverage, checks) {
4670
4684
  function walkLines(walk, p) {
4671
4685
  const label = "> - Acceptance walk (the finished work tried as a user would, by someone other than its builder): ";
4672
4686
  if (walk === null)
4673
- return [`${label}none reported.`];
4687
+ return [`${label}the person closing the phase reported none.`];
4674
4688
  const lines = [
4675
- `${label}${walk.verdict === "clean" ? "reported as finding no problems." : "reported as finding problems."}`
4689
+ `${label}${walk.verdict === "clean" ? "the person closing the phase said it found no problems." : "the person closing the phase said it found problems."}`
4676
4690
  ];
4677
4691
  walk.findings.forEach((finding, i) => {
4678
4692
  lines.push(`> - Finding ${String(i + 1)}: ${p.text(finding)}`);
@@ -4963,7 +4977,8 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
4963
4977
  return {
4964
4978
  kind: "written",
4965
4979
  jsonPath: relative3(repoRoot, result.jsonPath),
4966
- mdPath: relative3(repoRoot, result.mdPath)
4980
+ mdPath: relative3(repoRoot, result.mdPath),
4981
+ headline: latestCloseHeadline(answer.record)
4967
4982
  };
4968
4983
  }
4969
4984
  if (isMissingRoute(res.status, raw)) {
@@ -4982,12 +4997,22 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
4982
4997
  };
4983
4998
  }
4984
4999
  function refetchCommand(phase) {
4985
- return `npx halfcycle@latest build-record ${quoteForShell(phase)}`;
5000
+ return `${HALFCYCLE_COMMAND} build-record ${quoteForShell(phase)}`;
4986
5001
  }
4987
5002
 
4988
5003
  // dist/open-phase.js
4989
5004
  import { readFileSync as readFileSync7 } from "node:fs";
4990
5005
  import { join as join7 } from "node:path";
5006
+ function describeOpen(opened) {
5007
+ const { phase, previousPhase } = opened;
5008
+ if (previousPhase === phase) {
5009
+ return phase === null ? "Already at project scope \u2014 nothing moved." : `Phase "${phase}" is already open \u2014 nothing moved.`;
5010
+ }
5011
+ const what = phase === null ? "Returned to project scope." : `Opened phase "${phase}".`;
5012
+ if (previousPhase === null)
5013
+ return phase === null ? what : `Moved from project scope to phase "${phase}".`;
5014
+ return opened.decision.blockingFinding === null ? `${what} Phase "${previousPhase}" before it had already been closed.` : `${what} Phase "${previousPhase}" before it was not closed, and this override is recorded.`;
5015
+ }
4991
5016
  var OpenPhaseRefused = class extends Error {
4992
5017
  status;
4993
5018
  finding;
@@ -5302,13 +5327,41 @@ function requirementMessage(verb, flag) {
5302
5327
  function runnableInvocation(verb, phase) {
5303
5328
  const contract = contractFor(verb);
5304
5329
  const flags = (contract?.required ?? []).map((req) => `${req.flag} ${PLACEHOLDERS[req.flag] ?? '"\u2026"'}`).join(" ");
5305
- return `halfcycle ${verb} ${quoteForShell(phase)}${flags ? ` ${flags}` : ""}`;
5330
+ return `${HALFCYCLE_COMMAND} ${verb} ${quoteForShell(phase)}${flags ? ` ${flags}` : ""}`;
5306
5331
  }
5307
5332
  var PLACEHOLDERS = {
5308
5333
  "--verdict": "<clean|defects>",
5309
5334
  "--actor": '"you"',
5310
- "--evidence": '"\u2026"'
5335
+ "--evidence": '"<what shows this phase is ready to start>"'
5311
5336
  };
5337
+ var REASON_PLACEHOLDER = {
5338
+ "open-phase": "<why you are opening it anyway>",
5339
+ "close-phase": "<why you are closing over this>"
5340
+ };
5341
+ function closeOverrideInvocation(phase, verdict, actor) {
5342
+ return `${HALFCYCLE_COMMAND} close-phase ${quoteForShell(phase)} --verdict ${verdict} --actor ${quoteForShell(actor)} --override --reason "${REASON_PLACEHOLDER["close-phase"]}"`;
5343
+ }
5344
+ var OWN_WORDS = {
5345
+ "--reason": "reason",
5346
+ "--evidence": "evidence",
5347
+ "--finding": "finding"
5348
+ };
5349
+ function isPlaceholder(value) {
5350
+ const v = value.trim();
5351
+ return v === "\u2026" || v === "..." || /^<[^<>]+>$/.test(v);
5352
+ }
5353
+ function placeholderRefusal(verb, tokens) {
5354
+ for (let i = 0; i < tokens.length - 1; i++) {
5355
+ const noun = OWN_WORDS[tokens[i]];
5356
+ const value = tokens[i + 1];
5357
+ if (noun === void 0 || value.startsWith("--"))
5358
+ continue;
5359
+ if (isPlaceholder(value)) {
5360
+ return `halfcycle ${verb}: ${tokens[i]} "${value.trim()}" is a placeholder from an example \u2014 replace it with your own ${noun}; nothing was sent.`;
5361
+ }
5362
+ }
5363
+ return null;
5364
+ }
5312
5365
 
5313
5366
  // dist/uninstall.js
5314
5367
  import { lstatSync as lstatSync2, readFileSync as readFileSync8, readdirSync, rmSync as rmSync3, rmdirSync, writeFileSync as writeFileSync5 } from "node:fs";
@@ -5370,7 +5423,7 @@ function readMergedJson(targetRepo, rel) {
5370
5423
  value = void 0;
5371
5424
  }
5372
5425
  if (!isPlainObject2(value)) {
5373
- throw new Refusal(`${rel} is not a JSON object, so Halfcycle cannot take its own entries out of it and nothing was removed. Fix it or delete it, then run "npx halfcycle uninstall" again.`);
5426
+ throw new Refusal(`${rel} is not a JSON object, so Halfcycle cannot take its own entries out of it and nothing was removed. Fix it or delete it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
5374
5427
  }
5375
5428
  return { state: "object", value };
5376
5429
  }
@@ -5384,14 +5437,14 @@ function validate(options) {
5384
5437
  if (!lexists(manifestPath)) {
5385
5438
  if (!lexists(pinPath))
5386
5439
  return "not-installed";
5387
- throw new Refusal(`Halfcycle was installed here by a version that kept no record of what it wrote, so nothing was removed. Run "npx halfcycle" once \u2014 it writes that record \u2014 then run "npx halfcycle uninstall".`);
5440
+ throw new Refusal(`Halfcycle was installed here by a version that kept no record of what it wrote, so nothing was removed. Run "${HALFCYCLE_COMMAND}" once \u2014 it writes that record \u2014 then run "${HALFCYCLE_COMMAND} uninstall".`);
5388
5441
  }
5389
5442
  let manifest;
5390
5443
  try {
5391
5444
  manifest = parseManifest(readFileSync8(manifestPath, "utf-8"));
5392
5445
  } catch (err) {
5393
5446
  const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
5394
- throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "npx halfcycle" once to rewrite it, then run "npx halfcycle uninstall" again.`);
5447
+ throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "${HALFCYCLE_COMMAND}" once to rewrite it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
5395
5448
  }
5396
5449
  let engagementId;
5397
5450
  let pinReadable = true;
@@ -5907,13 +5960,13 @@ function renderUninstallReport(result, home) {
5907
5960
  section("Kept, because it changed after Halfcycle wrote it \u2014 delete it yourself if you do not want it:", result.kept);
5908
5961
  section('Kept in .gitignore, because the path each line covers still exists \u2014 without the line, "git add ." would commit it:', result.keptIgnoreLines);
5909
5962
  section("Left alone \u2014 Halfcycle did not write these, or cannot tell that it did:", result.leftAlone);
5910
- section('Could not remove \u2014 fix the cause, then run "npx halfcycle uninstall" again to finish:', result.failed);
5963
+ section(`Could not remove \u2014 fix the cause, then run "${HALFCYCLE_COMMAND} uninstall" again to finish:`, result.failed);
5911
5964
  const c = result.credential;
5912
5965
  switch (c.kind) {
5913
5966
  case "still-held":
5914
5967
  out.push(`[halfcycle] This machine still holds this project's credential, at ${shownPath(c.dir, home)}.`);
5915
5968
  out.push(" Every checkout and worktree of this project on this machine uses it.");
5916
- out.push(` To remove it too: npx halfcycle uninstall ${REMOVE_CREDENTIAL_FLAG}`);
5969
+ out.push(` To remove it too: ${HALFCYCLE_COMMAND} uninstall ${REMOVE_CREDENTIAL_FLAG}`);
5917
5970
  break;
5918
5971
  case "invalid-id":
5919
5972
  out.push(`[halfcycle] This machine may still hold a credential for this project, but the project id in ${BUNDLE_PIN_REL} is not valid, so it is not shown as a path.`);
@@ -5921,7 +5974,7 @@ function renderUninstallReport(result, home) {
5921
5974
  case "removed":
5922
5975
  out.push(`[halfcycle] Removed this machine's credential for this project, at ${shownPath(c.dir, home)}.`);
5923
5976
  out.push(" Every other checkout and worktree of this project on this machine used it too. Each stops reaching");
5924
- out.push(" Halfcycle until `npx halfcycle` is run there again.");
5977
+ out.push(` Halfcycle until \`${HALFCYCLE_COMMAND}\` is run there again.`);
5925
5978
  break;
5926
5979
  case "not-held":
5927
5980
  out.push(`[halfcycle] This machine holds no credential for this project (nothing at ${shownPath(c.dir, home)}).`);
@@ -5930,7 +5983,7 @@ function renderUninstallReport(result, home) {
5930
5983
  out.push(`[halfcycle] No credential was removed: ${BUNDLE_PIN_REL} is missing, so there is no project id to find it by.`);
5931
5984
  break;
5932
5985
  case "failed":
5933
- out.push(`[halfcycle] Could not remove this machine's credential for this project, at ${shownPath(c.dir, home)}: ${c.reason}. Run "npx halfcycle uninstall ${REMOVE_CREDENTIAL_FLAG}" again to finish.`);
5986
+ out.push(`[halfcycle] Could not remove this machine's credential for this project, at ${shownPath(c.dir, home)}: ${c.reason}. Run "${HALFCYCLE_COMMAND} uninstall ${REMOVE_CREDENTIAL_FLAG}" again to finish.`);
5934
5987
  break;
5935
5988
  case "nothing-to-say":
5936
5989
  break;
@@ -6079,17 +6132,17 @@ function reportClaudeCodeVersion(verbose2, deps = {}) {
6079
6132
  function shortId(id) {
6080
6133
  return id.length > 10 ? `${id.slice(0, 8)}\u2026` : id;
6081
6134
  }
6082
- var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|client]
6135
+ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [self-build|client]
6083
6136
  install into target (default: the current directory)
6084
6137
  --yes / -y: use this machine's saved sign-in without being asked
6085
- halfcycle check-drift <target-repo>
6086
- halfcycle build-record <phase> [--repo <root>]
6138
+ ${HALFCYCLE_COMMAND} check-drift <target-repo>
6139
+ ${HALFCYCLE_COMMAND} build-record <phase> [--repo <root>]
6087
6140
  fetch a closed phase's Build Record again and write it into docs/build-records/
6088
- halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
6089
- halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
6141
+ ${HALFCYCLE_COMMAND} open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
6142
+ ${HALFCYCLE_COMMAND} close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
6090
6143
  [--thesis "\u2026"] [--outcome "\u2026"] [--deviation "\u2026"]\u2026
6091
6144
  close the phase and write its Build Record into docs/build-records/
6092
- halfcycle uninstall [${REMOVE_CREDENTIAL_FLAG}]
6145
+ ${HALFCYCLE_COMMAND} uninstall [${REMOVE_CREDENTIAL_FLAG}]
6093
6146
  take out what the installer added to this project, keeping anything you changed
6094
6147
  ${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
6095
6148
  checkout and worktree of that project on this machine shares
@@ -6470,13 +6523,23 @@ ${USAGE}`);
6470
6523
  const toProjectScope = rest.includes("--none");
6471
6524
  const phaseArg = firstPositional(rest);
6472
6525
  if (!toProjectScope && !phaseArg) {
6473
- process.stderr.write('halfcycle open-phase: name the phase to open, or pass --none to return to project scope.\n halfcycle open-phase <phase> --actor "you" --evidence "what makes the entry condition hold"\n halfcycle open-phase <phase> --actor "you" --override --reason "why you are opening it anyway"\n');
6526
+ process.stderr.write(`halfcycle open-phase: name the phase to open, or pass --none to return to project scope.
6527
+ ${HALFCYCLE_COMMAND} open-phase <phase> --actor "you" --evidence "<what shows this phase is ready to start>"
6528
+ ${HALFCYCLE_COMMAND} open-phase <phase> --actor "you" --override --reason "${REASON_PLACEHOLDER["open-phase"]}"
6529
+ `);
6474
6530
  process.exit(2);
6475
6531
  return;
6476
6532
  }
6477
6533
  const openShortfalls = missingRequirements("open-phase", rest);
6478
6534
  if (openShortfalls.length > 0) {
6479
6535
  process.stderr.write(`${openShortfalls[0].message}
6536
+ `);
6537
+ process.exit(2);
6538
+ return;
6539
+ }
6540
+ const openPlaceholder = placeholderRefusal("open-phase", rest);
6541
+ if (openPlaceholder !== null) {
6542
+ process.stderr.write(`${openPlaceholder}
6480
6543
  `);
6481
6544
  process.exit(2);
6482
6545
  return;
@@ -6491,15 +6554,8 @@ ${USAGE}`);
6491
6554
  try {
6492
6555
  const credential = readRepoCredential(repoRoot);
6493
6556
  const opened = await openPhase(credential, toProjectScope ? null : phaseArg, entry);
6494
- const what = opened.phase === null ? "project scope" : `phase "${opened.phase}"`;
6495
- const from = opened.previousPhase === null ? "project scope" : `phase "${opened.previousPhase}"`;
6496
- if (opened.previousPhase === opened.phase) {
6497
- process.stdout.write(opened.phase === null ? "[halfcycle] Already at project scope \u2014 nothing moved.\n" : `[halfcycle] Phase "${opened.phase}" is already open \u2014 nothing moved.
6498
- `);
6499
- } else {
6500
- process.stdout.write(`[halfcycle] Moved from ${from} to ${what}.
6557
+ process.stdout.write(`[halfcycle] ${describeOpen(opened)}
6501
6558
  `);
6502
- }
6503
6559
  process.stdout.write(`[halfcycle] Recorded as ${opened.decision.decision} by ${opened.decision.actor} at ${opened.decision.decidedAt}.
6504
6560
  `);
6505
6561
  if (opened.decision.blockingFinding) {
@@ -6529,7 +6585,7 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
6529
6585
  `);
6530
6586
  }
6531
6587
  if (err.status === 409) {
6532
- process.stderr.write(`halfcycle open-phase: close the phase first \u2014 \`${runnableInvocation("close-phase", err.previousPhase ?? "<phase>")}\` (add --finding "\u2026" once per finding when the verdict is defects) \u2014 or re-run with --override --reason "\u2026", which is recorded, not waived.
6588
+ process.stderr.write(`halfcycle open-phase: close the phase first \u2014 \`${runnableInvocation("close-phase", err.previousPhase ?? "<phase>")}\` (add --finding "<what is still wrong>" once per finding when the verdict is defects) \u2014 or re-run with --override --reason "${REASON_PLACEHOLDER["open-phase"]}", which is recorded, not waived.
6533
6589
  `);
6534
6590
  }
6535
6591
  process.exit(1);
@@ -6545,13 +6601,23 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
6545
6601
  const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
6546
6602
  const phaseArg = firstPositional(rest);
6547
6603
  if (!phaseArg) {
6548
- process.stderr.write('halfcycle close-phase: name the phase you are closing.\n halfcycle close-phase <phase> --verdict clean --actor "you"\n halfcycle close-phase <phase> --verdict defects --actor "you" --finding "what is still wrong"\n');
6604
+ process.stderr.write(`halfcycle close-phase: name the phase you are closing.
6605
+ ${HALFCYCLE_COMMAND} close-phase <phase> --verdict clean --actor "you"
6606
+ ${HALFCYCLE_COMMAND} close-phase <phase> --verdict defects --actor "you" --finding "<what is still wrong>"
6607
+ `);
6549
6608
  process.exit(2);
6550
6609
  return;
6551
6610
  }
6552
6611
  const closeShortfalls = missingRequirements("close-phase", rest);
6553
6612
  if (closeShortfalls.length > 0) {
6554
6613
  process.stderr.write(`${closeShortfalls[0].message}
6614
+ `);
6615
+ process.exit(2);
6616
+ return;
6617
+ }
6618
+ const closePlaceholder = placeholderRefusal("close-phase", rest);
6619
+ if (closePlaceholder !== null) {
6620
+ process.stderr.write(`${closePlaceholder}
6555
6621
  `);
6556
6622
  process.exit(2);
6557
6623
  return;
@@ -6607,7 +6673,7 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
6607
6673
  findings: findingsDetail.length,
6608
6674
  findingsDetail
6609
6675
  }, closeDecision, void 0, repoRoot, stated);
6610
- process.stdout.write(`[halfcycle] Phase "${phaseArg}" closed as ${verdict}${findingsDetail.length > 0 ? ` with ${findingsDetail.length} finding(s)` : ""} by ${closeDecision.actor}. The next phase can be opened.
6676
+ process.stdout.write(`[halfcycle] Phase "${phaseArg}" closed by ${closeDecision.actor}${findingsDetail.length > 0 ? `, who reported ${findingsDetail.length} finding(s)` : ""}. The next phase can be opened.
6611
6677
  `);
6612
6678
  if (closeDecision.decision === "override") {
6613
6679
  process.stdout.write(`[halfcycle] This close was an OVERRIDE: ${closeDecision.reason}
@@ -6624,7 +6690,7 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
6624
6690
  } else {
6625
6691
  process.stderr.write(notWrittenLines("close-phase", obtained, phaseArg));
6626
6692
  }
6627
- process.stdout.write(`[halfcycle] Open the next phase with: halfcycle open-phase <phase> --actor "you" --evidence "\u2026"
6693
+ process.stdout.write(`[halfcycle] Open the next phase with: ${HALFCYCLE_COMMAND} open-phase <phase> --actor "you" --evidence "<what shows this phase is ready to start>"
6628
6694
  `);
6629
6695
  if (closed.stampFailure) {
6630
6696
  process.stderr.write(`halfcycle close-phase: the phase closed, but this machine's record of it could not be cleared: ${closed.stampFailure.reason}
@@ -6645,7 +6711,7 @@ halfcycle close-phase: until ${closed.stampFailure.path} can be written, guard e
6645
6711
  `);
6646
6712
  process.stderr.write(`halfcycle close-phase: outstanding work \u2014 ${plainOutstandingFinding(err.finding)}
6647
6713
  `);
6648
- process.stderr.write(`halfcycle close-phase: close it anyway with \`halfcycle close-phase ${quoteForShell(phaseArg)} --verdict ${verdict} --actor ${quoteForShell(closeActor)} --override --reason "\u2026"\` \u2014 recorded, not waived.
6714
+ process.stderr.write(`halfcycle close-phase: close it anyway with \`${closeOverrideInvocation(phaseArg, verdict, closeActor)}\` \u2014 recorded, not waived.
6649
6715
  `);
6650
6716
  process.exit(1);
6651
6717
  return;
@@ -6656,7 +6722,9 @@ halfcycle close-phase: until ${closed.stampFailure.path} can be written, guard e
6656
6722
  process.stderr.write("halfcycle close-phase: Nothing was closed.\n");
6657
6723
  }
6658
6724
  if (err.code === "PhaseAlreadyClosed") {
6659
- process.stderr.write("halfcycle close-phase: to close a phase again, open it again first.\nhalfcycle close-phase: To get a closed phase's Build Record: npx halfcycle@latest build-record <the phase that closed>\n");
6725
+ process.stderr.write(`halfcycle close-phase: to close a phase again, open it again first.
6726
+ halfcycle close-phase: To get a closed phase's Build Record: ${HALFCYCLE_COMMAND} build-record <the phase that closed>
6727
+ `);
6660
6728
  }
6661
6729
  process.exit(1);
6662
6730
  return;
@@ -6701,6 +6769,7 @@ function writtenLines(written) {
6701
6769
  return `[halfcycle] Build Record written:
6702
6770
  ${written.mdPath}
6703
6771
  ${written.jsonPath}
6772
+ [halfcycle] The record says: ${written.headline}
6704
6773
  `;
6705
6774
  }
6706
6775
  function notWrittenLines(verb, outcome, phase) {