halfcycle 0.4.0 → 0.4.2

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.4.0",
3
+ "version": "0.4.2",
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
@@ -165,13 +165,9 @@ Nothing you already had is changed. Existing settings are preserved, and a name
165
165
  ## Requirements
166
166
 
167
167
  - Claude Code **2.1.118+**
168
- - Node 20+
168
+ - Node 18+
169
169
  - A git repository
170
- - On Windows, if you use PowerShell: let it run the scripts Node installs, once per user. Otherwise PowerShell refuses `npx` with *running scripts is disabled on this system*:
171
-
172
- ```powershell
173
- Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
174
- ```
170
+ - On Windows, in PowerShell, use `npx.cmd` instead of `npx` (PowerShell's default settings block the `npx` script; `npx.cmd` always works, with nothing to change).
175
171
 
176
172
  ## Getting started
177
173
 
@@ -181,19 +177,19 @@ git init
181
177
  npx halfcycle
182
178
  ```
183
179
 
184
- In Windows PowerShell, separate the first two commands with `;` instead of `&&`:
180
+ In Windows PowerShell, separate the first two commands with `;` instead of `&&`, and run `npx.cmd` instead of `npx`:
185
181
 
186
182
  ```powershell
187
183
  mkdir my-project; cd my-project
188
184
  git init
189
- npx halfcycle
185
+ npx.cmd halfcycle
190
186
  ```
191
187
 
192
188
  Then open the folder in Claude Code and run `/halfcycle-setup`, and answer the questions it asks about your project.
193
189
 
194
190
  **Accept the workspace trust prompt when Claude Code shows it.** The install writes a credential helper and a set of hooks that are shell commands, so Claude Code asks before it runs them. It is expected.
195
191
 
196
- **A second prompt follows — it is not the trust prompt again.** After you accept the trust prompt, Claude Code asks whether to use a new MCP server. The cursor starts on *Continue without using this MCP server*; choose to use the server instead, or Halfcycle will not connect.
192
+ **A second prompt follows — it is not the trust prompt again.** After you accept the trust prompt, Claude Code asks whether to use a new MCP server. If halfcycle is the only one listed, the cursor starts on *Continue without using this MCP server*; choose to use the server instead, or Halfcycle will not connect. If several new servers are listed together, halfcycle is already checked — leave it that way.
197
193
 
198
194
  You will get a board link on install. That is where the work becomes visible to anyone who is not you.
199
195
 
@@ -14664,58 +14664,70 @@ var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
14664
14664
  var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
14665
14665
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
14666
14666
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
14667
+ function hookCommand(script, verb) {
14668
+ const invocation = `bash "$CLAUDE_PROJECT_DIR"/${script}`;
14669
+ return verb === void 0 ? invocation : `${invocation} ${verb}`;
14670
+ }
14667
14671
  var HALFCYCLE_HOOK_WIRING = [
14668
14672
  {
14669
14673
  event: "SessionStart",
14670
14674
  matcher: null,
14671
14675
  timeout: 5,
14672
- command: `bash ./${SESSION_START_MARKER_REL}`,
14676
+ command: hookCommand(SESSION_START_MARKER_REL),
14673
14677
  script: SESSION_START_MARKER_REL
14674
14678
  },
14675
14679
  {
14676
14680
  event: "SessionStart",
14677
14681
  matcher: null,
14678
14682
  timeout: 10,
14679
- command: `bash ./${GUARD_RUNNER_REL} refresh-credential`,
14683
+ command: hookCommand(GUARD_RUNNER_REL, "refresh-credential"),
14680
14684
  script: GUARD_RUNNER_REL
14681
14685
  },
14682
14686
  {
14683
14687
  event: "UserPromptSubmit",
14684
14688
  matcher: null,
14685
14689
  timeout: 5,
14686
- command: `bash ./${USER_PROMPT_REMINDER_REL}`,
14690
+ command: hookCommand(USER_PROMPT_REMINDER_REL),
14687
14691
  script: USER_PROMPT_REMINDER_REL
14688
14692
  },
14689
14693
  {
14690
14694
  event: "PostToolUse",
14691
14695
  matcher: WRITE_CAPABLE_TOOLS.join("|"),
14692
14696
  timeout: 60,
14693
- command: `bash ./${GUARD_RUNNER_REL} hook post-tool-use`,
14697
+ command: hookCommand(GUARD_RUNNER_REL, "hook post-tool-use"),
14694
14698
  script: GUARD_RUNNER_REL
14695
14699
  },
14696
14700
  {
14697
14701
  event: "Stop",
14698
14702
  matcher: null,
14699
14703
  timeout: 60,
14700
- command: `bash ./${GUARD_RUNNER_REL} hook stop`,
14704
+ command: hookCommand(GUARD_RUNNER_REL, "hook stop"),
14701
14705
  script: GUARD_RUNNER_REL
14702
14706
  },
14703
14707
  {
14704
14708
  event: "SubagentStop",
14705
14709
  matcher: null,
14706
14710
  timeout: 60,
14707
- command: `bash ./${GUARD_RUNNER_REL} hook subagent-stop`,
14711
+ command: hookCommand(GUARD_RUNNER_REL, "hook subagent-stop"),
14708
14712
  script: GUARD_RUNNER_REL
14709
14713
  },
14710
14714
  {
14711
14715
  event: "SessionEnd",
14712
14716
  matcher: null,
14713
14717
  timeout: 5,
14714
- command: `bash ./${SESSION_END_MARKER_REL}`,
14718
+ command: hookCommand(SESSION_END_MARKER_REL),
14715
14719
  script: SESSION_END_MARKER_REL
14716
14720
  }
14717
14721
  ];
14718
- var RETIRED_HOOK_COMMANDS = [];
14722
+ var RETIRED_HOOK_COMMANDS = [
14723
+ "bash ./.claude/hooks/session-start-marker.sh",
14724
+ "bash ./.claude/hooks/guard-runner.sh refresh-credential",
14725
+ "bash ./.claude/hooks/user-prompt-reminder.sh",
14726
+ "bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
14727
+ "bash ./.claude/hooks/guard-runner.sh hook stop",
14728
+ "bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
14729
+ "bash ./.claude/hooks/session-end-marker.sh"
14730
+ ];
14719
14731
  var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
14720
14732
  ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
14721
14733
  ...RETIRED_HOOK_COMMANDS
@@ -15364,6 +15376,9 @@ function infraWarningText() {
15364
15376
  function localFaultText(message) {
15365
15377
  return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
15366
15378
  }
15379
+ function noRepositoryText(failure) {
15380
+ return failure.gitMissing ? "[Halfcycle] WARNING: Git is not installed on this machine, and Halfcycle's checks need it, so nothing will be checked in this session. Install Git, then start a new session." : localFaultText(`Could not find git repo root: ${failure.message}`);
15381
+ }
15367
15382
  function contractFaultText(message) {
15368
15383
  return `[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
15369
15384
  This is not a service outage: the guard service answered. The suspect is this repository's own installed halfcycle runner, which is out of date with what the service now sends. Update it: run "npx halfcycle" in this repository, then retry. Session continues (fail-open).`;
@@ -15699,7 +15714,7 @@ async function attempt(url2, token, request, fetchFn, retry) {
15699
15714
 
15700
15715
  // dist/git.js
15701
15716
  import { spawnSync } from "node:child_process";
15702
- import { readFileSync } from "node:fs";
15717
+ import { existsSync, readFileSync } from "node:fs";
15703
15718
  import { relative } from "node:path";
15704
15719
  var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
15705
15720
  function git(args2, cwd, timeoutMs) {
@@ -15710,7 +15725,8 @@ function git(args2, cwd, timeoutMs) {
15710
15725
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
15711
15726
  });
15712
15727
  if (result.error) {
15713
- return { ok: false, message: `git error: ${result.error.message}` };
15728
+ const missing = result.error.code === "ENOENT" && existsSync(cwd);
15729
+ return { ok: false, message: `git error: ${result.error.message}`, ...missing ? { gitMissing: true } : {} };
15714
15730
  }
15715
15731
  if (result.status !== 0) {
15716
15732
  const stderr = result.stderr?.trim() ?? "";
@@ -15738,15 +15754,18 @@ function getSingleFileDiff(absFilePath, repoRoot) {
15738
15754
  return "";
15739
15755
  }
15740
15756
  }
15757
+ function workingTreeBase(repoRoot) {
15758
+ return refExists("HEAD", repoRoot) ? "HEAD" : EMPTY_TREE_SHA;
15759
+ }
15741
15760
  function getWorkingTreeChangedFiles(repoRoot) {
15742
- const result = git(["diff", "HEAD", "--name-only", "--diff-filter=d"], repoRoot);
15761
+ const result = git(["diff", workingTreeBase(repoRoot), "--name-only", "--diff-filter=d"], repoRoot);
15743
15762
  if (!result.ok)
15744
15763
  return result;
15745
15764
  const files = result.value.split("\n").map((l) => l.trim()).filter(Boolean);
15746
15765
  return { ok: true, value: files };
15747
15766
  }
15748
15767
  function getWorkingTreeFileDiff(relPath, repoRoot) {
15749
- const result = git(["diff", "HEAD", "--", relPath], repoRoot);
15768
+ const result = git(["diff", workingTreeBase(repoRoot), "--", relPath], repoRoot);
15750
15769
  if (result.ok && result.value.trim())
15751
15770
  return result.value;
15752
15771
  return "";
@@ -16590,7 +16609,7 @@ function resolveWireFields(lines, decls) {
16590
16609
  }
16591
16610
 
16592
16611
  // dist/fixtures-manifest.js
16593
- import { existsSync, mkdirSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
16612
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
16594
16613
  import { dirname, join as join2 } from "node:path";
16595
16614
  function manifestPath(repoRoot) {
16596
16615
  return join2(repoRoot, "test", "fixtures", "captured", "manifest.json");
@@ -16629,7 +16648,7 @@ function upsertManifest(repoRoot, entry) {
16629
16648
  }
16630
16649
  function removeFixtureIfExists(path) {
16631
16650
  try {
16632
- if (existsSync(path))
16651
+ if (existsSync2(path))
16633
16652
  unlinkSync(path);
16634
16653
  } catch {
16635
16654
  }
@@ -17251,7 +17270,7 @@ function cleanRunSummary(verdict, scope) {
17251
17270
  }
17252
17271
 
17253
17272
  // dist/credential-store.js
17254
- import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
17273
+ import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
17255
17274
  import { homedir as homedir2, platform } from "node:os";
17256
17275
  import { dirname as dirname3, join as join5 } from "node:path";
17257
17276
  var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
@@ -17260,7 +17279,7 @@ function composeEngagementEnvPath(engagementId, home) {
17260
17279
  return join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, ENGAGEMENT_ENV_FILENAME);
17261
17280
  }
17262
17281
  function hasNotThisAccountMarker(engagementId, home) {
17263
- return existsSync2(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, NOT_THIS_ACCOUNT_MARKER_FILENAME));
17282
+ return existsSync3(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, NOT_THIS_ACCOUNT_MARKER_FILENAME));
17264
17283
  }
17265
17284
  function readAccountCredential(controlOrigin, home) {
17266
17285
  let raw;
@@ -17450,10 +17469,143 @@ async function evaluateWithRefresh(opts) {
17450
17469
  return { result: retry, token: refreshed.token, remedy: HOOK_REMEDY };
17451
17470
  }
17452
17471
 
17472
+ // dist/hooks/marker.js
17473
+ import { createHash } from "node:crypto";
17474
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17475
+ import { tmpdir } from "node:os";
17476
+ import { join as join6 } from "node:path";
17477
+ var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
17478
+ var MARKER_PREFIX = "halfcycle-session-";
17479
+ var MARKER_SUFFIX = ".ref";
17480
+ var DEDUPE_SUFFIX = ".eval";
17481
+ function markerDir() {
17482
+ const raw = process.env["TMPDIR"] ?? tmpdir();
17483
+ return raw.replace(/\/+$/, "");
17484
+ }
17485
+ function repoHash(repoRoot) {
17486
+ return createHash("sha256").update(repoRoot).digest("hex").slice(0, 12);
17487
+ }
17488
+ function markerFileName(repoRoot, sessionId) {
17489
+ const hash2 = repoHash(repoRoot);
17490
+ const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17491
+ return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
17492
+ }
17493
+ function markerPath(repoRoot, sessionId) {
17494
+ return join6(markerDir(), markerFileName(repoRoot, sessionId));
17495
+ }
17496
+ function dedupePath(repoRoot, sessionId) {
17497
+ const hash2 = repoHash(repoRoot);
17498
+ const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17499
+ return join6(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17500
+ }
17501
+ function readMarker(repoRoot, sessionId) {
17502
+ const path = markerPath(repoRoot, sessionId);
17503
+ if (!existsSync4(path))
17504
+ return null;
17505
+ try {
17506
+ return readFileSync6(path, "utf-8").trim() || null;
17507
+ } catch {
17508
+ return null;
17509
+ }
17510
+ }
17511
+ function deleteMarker(repoRoot, sessionId) {
17512
+ for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17513
+ try {
17514
+ if (existsSync4(path))
17515
+ unlinkSync3(path);
17516
+ } catch {
17517
+ }
17518
+ }
17519
+ }
17520
+ var NO_REPO_NOTICE_SUFFIX = ".norepo";
17521
+ function firstNoRepoNoticeThisSession(folder, sessionId) {
17522
+ if (!sessionId)
17523
+ return true;
17524
+ const path = join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}${NO_REPO_NOTICE_SUFFIX}`);
17525
+ try {
17526
+ writeFileSync3(path, "", { encoding: "utf-8", flag: "wx" });
17527
+ return true;
17528
+ } catch (err) {
17529
+ return err.code !== "EEXIST";
17530
+ }
17531
+ }
17532
+ function readDedupe(repoRoot, sessionId) {
17533
+ const path = dedupePath(repoRoot, sessionId);
17534
+ if (!existsSync4(path))
17535
+ return null;
17536
+ try {
17537
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
17538
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
17539
+ return parsed;
17540
+ }
17541
+ return null;
17542
+ } catch {
17543
+ return null;
17544
+ }
17545
+ }
17546
+ function writeDedupe(repoRoot, sessionId, state) {
17547
+ try {
17548
+ writeFileSync3(dedupePath(repoRoot, sessionId), JSON.stringify(state), "utf-8");
17549
+ } catch {
17550
+ }
17551
+ }
17552
+ function digestChangeState(base, entries) {
17553
+ const h = createHash("sha256");
17554
+ h.update(base);
17555
+ h.update("\0");
17556
+ for (const e of [...entries].sort((a, b) => a.path.localeCompare(b.path))) {
17557
+ h.update(e.path);
17558
+ h.update("\0");
17559
+ h.update(e.diffContent);
17560
+ h.update("\0");
17561
+ }
17562
+ return h.digest("hex");
17563
+ }
17564
+ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17565
+ const dir = markerDir();
17566
+ const hash2 = repoHash(repoRoot);
17567
+ const keepName = keepSessionId ? markerFileName(repoRoot, keepSessionId) : markerFileName(repoRoot);
17568
+ let names;
17569
+ try {
17570
+ names = readdirSync3(dir);
17571
+ } catch {
17572
+ return;
17573
+ }
17574
+ for (const name of names) {
17575
+ if (!name.startsWith(`${MARKER_PREFIX}${hash2}`) || !name.endsWith(MARKER_SUFFIX))
17576
+ continue;
17577
+ if (name === keepName)
17578
+ continue;
17579
+ const full = join6(dir, name);
17580
+ let unusable = false;
17581
+ try {
17582
+ const ref = readFileSync6(full, "utf-8").trim();
17583
+ if (!ref || !refKnown(ref))
17584
+ unusable = true;
17585
+ } catch {
17586
+ unusable = true;
17587
+ }
17588
+ if (!unusable) {
17589
+ try {
17590
+ const age = now - statSync(full).mtimeMs;
17591
+ if (age > MARKER_TTL_MS)
17592
+ unusable = true;
17593
+ } catch {
17594
+ unusable = true;
17595
+ }
17596
+ }
17597
+ if (unusable) {
17598
+ const mid = name.slice(`${MARKER_PREFIX}${hash2}`.length, -MARKER_SUFFIX.length);
17599
+ const sid = mid.startsWith("-") ? mid.slice(1) : void 0;
17600
+ deleteMarker(repoRoot, sid);
17601
+ }
17602
+ }
17603
+ }
17604
+
17453
17605
  // dist/seat-configuration.js
17454
- import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
17606
+ import { existsSync as existsSync5, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
17455
17607
  import { homedir as homedir3 } from "node:os";
17456
- import { join as join6, relative as relative3, resolve } from "node:path";
17608
+ import { join as join7, relative as relative3, resolve } from "node:path";
17457
17609
  function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
17458
17610
  try {
17459
17611
  const facts = read(repoRoot);
@@ -17488,7 +17640,7 @@ function isPlainObject2(value) {
17488
17640
  function readSettingsFile(projectDir, rel) {
17489
17641
  let raw;
17490
17642
  try {
17491
- raw = readFileSync6(join6(projectDir, rel), "utf-8");
17643
+ raw = readFileSync7(join7(projectDir, rel), "utf-8");
17492
17644
  } catch (err) {
17493
17645
  const code = err.code;
17494
17646
  const state = code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
@@ -17560,7 +17712,7 @@ function readPathStates(projectDir, repoRoot) {
17560
17712
  const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
17561
17713
  if (!status.ok)
17562
17714
  return unknown2();
17563
- const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync3(join6(projectDir, rel)));
17715
+ const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync5(join7(projectDir, rel)));
17564
17716
  return mapped ?? unknown2();
17565
17717
  }
17566
17718
  function mapWiringStatus(output, prefix, paths, onDisk) {
@@ -17625,7 +17777,9 @@ async function evaluateEdit(reply, readSeat) {
17625
17777
  return;
17626
17778
  const repoRootResult = getRepoRoot(process.cwd());
17627
17779
  if (!repoRootResult.ok) {
17628
- reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17780
+ if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
17781
+ reply.notice(noRepositoryText(repoRootResult));
17782
+ }
17629
17783
  return;
17630
17784
  }
17631
17785
  const repoRoot = repoRootResult.value;
@@ -17740,127 +17894,6 @@ function readStdin() {
17740
17894
  });
17741
17895
  }
17742
17896
 
17743
- // dist/hooks/marker.js
17744
- import { createHash } from "node:crypto";
17745
- import { existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17746
- import { tmpdir } from "node:os";
17747
- import { join as join7 } from "node:path";
17748
- var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
17749
- var MARKER_PREFIX = "halfcycle-session-";
17750
- var MARKER_SUFFIX = ".ref";
17751
- var DEDUPE_SUFFIX = ".eval";
17752
- function markerDir() {
17753
- const raw = process.env["TMPDIR"] ?? tmpdir();
17754
- return raw.replace(/\/+$/, "");
17755
- }
17756
- function repoHash(repoRoot) {
17757
- return createHash("sha256").update(repoRoot).digest("hex").slice(0, 12);
17758
- }
17759
- function markerFileName(repoRoot, sessionId) {
17760
- const hash2 = repoHash(repoRoot);
17761
- const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17762
- return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
17763
- }
17764
- function markerPath(repoRoot, sessionId) {
17765
- return join7(markerDir(), markerFileName(repoRoot, sessionId));
17766
- }
17767
- function dedupePath(repoRoot, sessionId) {
17768
- const hash2 = repoHash(repoRoot);
17769
- const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17770
- return join7(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17771
- }
17772
- function readMarker(repoRoot, sessionId) {
17773
- const path = markerPath(repoRoot, sessionId);
17774
- if (!existsSync4(path))
17775
- return null;
17776
- try {
17777
- return readFileSync7(path, "utf-8").trim() || null;
17778
- } catch {
17779
- return null;
17780
- }
17781
- }
17782
- function deleteMarker(repoRoot, sessionId) {
17783
- for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17784
- try {
17785
- if (existsSync4(path))
17786
- unlinkSync3(path);
17787
- } catch {
17788
- }
17789
- }
17790
- }
17791
- function readDedupe(repoRoot, sessionId) {
17792
- const path = dedupePath(repoRoot, sessionId);
17793
- if (!existsSync4(path))
17794
- return null;
17795
- try {
17796
- const parsed = JSON.parse(readFileSync7(path, "utf-8"));
17797
- if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
17798
- return parsed;
17799
- }
17800
- return null;
17801
- } catch {
17802
- return null;
17803
- }
17804
- }
17805
- function writeDedupe(repoRoot, sessionId, state) {
17806
- try {
17807
- writeFileSync3(dedupePath(repoRoot, sessionId), JSON.stringify(state), "utf-8");
17808
- } catch {
17809
- }
17810
- }
17811
- function digestChangeState(base, entries) {
17812
- const h = createHash("sha256");
17813
- h.update(base);
17814
- h.update("\0");
17815
- for (const e of [...entries].sort((a, b) => a.path.localeCompare(b.path))) {
17816
- h.update(e.path);
17817
- h.update("\0");
17818
- h.update(e.diffContent);
17819
- h.update("\0");
17820
- }
17821
- return h.digest("hex");
17822
- }
17823
- function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17824
- const dir = markerDir();
17825
- const hash2 = repoHash(repoRoot);
17826
- const keepName = keepSessionId ? markerFileName(repoRoot, keepSessionId) : markerFileName(repoRoot);
17827
- let names;
17828
- try {
17829
- names = readdirSync3(dir);
17830
- } catch {
17831
- return;
17832
- }
17833
- for (const name of names) {
17834
- if (!name.startsWith(`${MARKER_PREFIX}${hash2}`) || !name.endsWith(MARKER_SUFFIX))
17835
- continue;
17836
- if (name === keepName)
17837
- continue;
17838
- const full = join7(dir, name);
17839
- let unusable = false;
17840
- try {
17841
- const ref = readFileSync7(full, "utf-8").trim();
17842
- if (!ref || !refKnown(ref))
17843
- unusable = true;
17844
- } catch {
17845
- unusable = true;
17846
- }
17847
- if (!unusable) {
17848
- try {
17849
- const age = now - statSync(full).mtimeMs;
17850
- if (age > MARKER_TTL_MS)
17851
- unusable = true;
17852
- } catch {
17853
- unusable = true;
17854
- }
17855
- }
17856
- if (unusable) {
17857
- const mid = name.slice(`${MARKER_PREFIX}${hash2}`.length, -MARKER_SUFFIX.length);
17858
- const sid = mid.startsWith("-") ? mid.slice(1) : void 0;
17859
- deleteMarker(repoRoot, sid);
17860
- }
17861
- }
17862
- }
17863
-
17864
17897
  // dist/hooks/session-diff.js
17865
17898
  var STOP_HOOK_EVENT = {
17866
17899
  stop: "Stop",
@@ -17904,7 +17937,9 @@ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
17904
17937
  async function evaluateSession(input, kind, reply, readSeat) {
17905
17938
  const repoRootResult = getRepoRoot(process.cwd());
17906
17939
  if (!repoRootResult.ok) {
17907
- reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17940
+ if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
17941
+ reply.notice(noRepositoryText(repoRootResult));
17942
+ }
17908
17943
  return;
17909
17944
  }
17910
17945
  const repoRoot = repoRootResult.value;
@@ -18079,14 +18114,14 @@ async function runStop() {
18079
18114
  }
18080
18115
 
18081
18116
  // dist/resolve-engagement.js
18082
- import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18117
+ import { existsSync as existsSync6, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18083
18118
  import { join as join8, sep as sep2 } from "node:path";
18084
18119
  import { homedir as homedir4 } from "node:os";
18085
18120
  function defaultRegistryPath() {
18086
18121
  return join8(homedir4(), ".halfcycle", "registry.json");
18087
18122
  }
18088
18123
  function readRegistry(registryPath = defaultRegistryPath()) {
18089
- if (!existsSync5(registryPath))
18124
+ if (!existsSync6(registryPath))
18090
18125
  return {};
18091
18126
  try {
18092
18127
  const parsed = JSON.parse(readFileSync8(registryPath, "utf-8"));
@@ -18376,6 +18411,7 @@ async function refreshCredentialToken(opts) {
18376
18411
  return { token: refreshed.token };
18377
18412
  }
18378
18413
  var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
18414
+ var CONNECTED_SENTENCE = `Halfcycle's sign-in works and its service answered. If Halfcycle's tools are missing, turn on ${MCP_SERVER_KEY} in /mcp; if /mcp does not list it, run "claude mcp reset-project-choices" in this folder and restart Claude Code.`;
18379
18415
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
18380
18416
  function registeredMcpUrl(projectRoot) {
18381
18417
  let parsed;
@@ -18461,7 +18497,8 @@ async function checkConnection(renewedToken, opts) {
18461
18497
  if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
18462
18498
  return PLACEHOLDER_SENTENCES[bearer];
18463
18499
  }
18464
- return probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
18500
+ const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
18501
+ return outcome ?? CONNECTED_SENTENCE;
18465
18502
  }
18466
18503
  function defaultWriteErr2(text) {
18467
18504
  process.stderr.write(text);
@@ -18489,8 +18526,11 @@ async function runRefreshCredential(opts) {
18489
18526
  const remedySaid = parts.some(namesTheSignInCommand);
18490
18527
  parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid));
18491
18528
  }
18492
- for (const said of renewalSaid)
18529
+ for (const said of renewalSaid) {
18530
+ if (namesTheSignInCommand(said) && parts.some(namesTheSignInCommand))
18531
+ continue;
18493
18532
  parts.push(said.trimEnd());
18533
+ }
18494
18534
  return parts.length === 0 ? "" : `${nonBlockingReply("SessionStart", parts.join("\n"))}
18495
18535
  `;
18496
18536
  } catch (err) {
@@ -18508,7 +18548,7 @@ async function runRefreshCredential(opts) {
18508
18548
  import { fileURLToPath } from "node:url";
18509
18549
 
18510
18550
  // dist/capture.js
18511
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync6 } from "node:fs";
18551
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
18512
18552
  import { dirname as dirname5 } from "node:path";
18513
18553
  function runCapture(provider, endpoint, opts = {}) {
18514
18554
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -4,8 +4,8 @@ description: Set up a repository for a Halfcycle engagement
4
4
 
5
5
  Resolve `/halfcycle-setup` for this engagement's current state, execute what comes back, report that the step ran, and resolve again — repeating until the response says there is nothing left (`hasNextStep` false); pausing for a human answer is not the end of the run. Do not improvise the procedure. Say where things stand at the boundaries of the run — when it starts, when it moves to a new layer, and when it ends — rather than on every turn, and pause only where a step asks you to. At one of those boundaries, and at most once per run, point them at this project's own page on the web and say what is there: how the project is going, the agents working on it once there are any, and their other projects alongside it. Use the address this install already records for the project's page rather than building one from an example, and give them the address itself — a page they are told about but cannot reach is worse than not mentioning it. Some steps of the Halfcycle method are judged by Halfcycle rather than by this session, or need a person to do something outside this session, so a quiet run is not a claim that nothing was due — say so when a run ends. Everything you say to the person is in plain words. Name the Halfcycle method the first time and say what it is in the same breath — the process that will guide the two of you through building this — because bare "the method" means nothing to someone who has not met it. After that, talk about their product and never about how any of this works underneath: no paths or files of ours, no hooks, settings, commands, layers or step names, and never Halfcycle's own machinery as the subject of a sentence. Say what now exists, what it is for, and what you need from them, in their terms. The one thing you never soften and never leave out is what did NOT happen — say that plainly too. If you are asked to jump ahead: when that is what comes next anyway, just do it; when Halfcycle refuses, relay its reason word for word and invent no procedure of your own; when it can be served but skips something, do it and say what is being skipped and what that costs. Coming back later is fine — a fresh session re-orients from what this project has recorded, so ask to carry on in plain words and you get the right next step and where things stand.
6
6
 
7
- If resolution fails, report exactly this and stop:
7
+ If resolution fails, report whichever applies and stop:
8
8
 
9
9
  > Halfcycle didn't answer for `/halfcycle-setup`. **Nothing has run.**
10
- > Allow any Halfcycle tool you declined. If Claude Code does not trust this folder yet, open
11
- > it there and accept the workspace-trust prompt. Then run this again, or retry once online.
10
+ > Service error — Halfcycle's service is down, try again shortly. Tool declined — run `/mcp` and reconnect it, then run this again.
11
+ > Folder not yet trusted — Claude Code does not trust this folder yet, open it there and accept the workspace-trust prompt, then run this again.