halfcycle 0.3.40 → 0.4.1

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.40",
3
+ "version": "0.4.1",
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
 
@@ -148,14 +148,12 @@ This matters enough to be specific about.
148
148
 
149
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
@@ -169,11 +167,7 @@ Nothing you already had is changed. Existing settings are preserved, and a name
169
167
  - Claude Code **2.1.118+**
170
168
  - Node 20+
171
169
  - A git repository
172
- - 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*:
173
-
174
- ```powershell
175
- Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
176
- ```
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).
177
171
 
178
172
  ## Getting started
179
173
 
@@ -183,19 +177,19 @@ git init
183
177
  npx halfcycle
184
178
  ```
185
179
 
186
- 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`:
187
181
 
188
182
  ```powershell
189
183
  mkdir my-project; cd my-project
190
184
  git init
191
- npx halfcycle
185
+ npx.cmd halfcycle
192
186
  ```
193
187
 
194
188
  Then open the folder in Claude Code and run `/halfcycle-setup`, and answer the questions it asks about your project.
195
189
 
196
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.
197
191
 
198
- **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.
199
193
 
200
194
  You will get a board link on install. That is where the work becomes visible to anyone who is not you.
201
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
@@ -14820,7 +14832,7 @@ function readMcpServerUrl() {
14820
14832
  }
14821
14833
  function readExcerptConfig() {
14822
14834
  const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
14823
- const enabled = (raw ?? "").trim().toLowerCase() === "true";
14835
+ const enabled = (raw ?? "").trim().toLowerCase() !== "false";
14824
14836
  const readCap = (name, fallback) => {
14825
14837
  const v = process.env[name];
14826
14838
  if (v === void 0)
@@ -15701,8 +15713,10 @@ async function attempt(url2, token, request, fetchFn, retry) {
15701
15713
  import { spawnSync } from "node:child_process";
15702
15714
  import { readFileSync } from "node:fs";
15703
15715
  import { relative } from "node:path";
15716
+ var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
15704
15717
  function git(args2, cwd, timeoutMs) {
15705
- const result = spawnSync("git", args2, {
15718
+ const argv = COLOR_AWARE_SUBCOMMANDS.has(args2[0] ?? "") ? [args2[0], "--no-color", ...args2.slice(1)] : args2;
15719
+ const result = spawnSync("git", argv, {
15706
15720
  cwd,
15707
15721
  encoding: "utf-8",
15708
15722
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
@@ -15736,15 +15750,18 @@ function getSingleFileDiff(absFilePath, repoRoot) {
15736
15750
  return "";
15737
15751
  }
15738
15752
  }
15753
+ function workingTreeBase(repoRoot) {
15754
+ return refExists("HEAD", repoRoot) ? "HEAD" : EMPTY_TREE_SHA;
15755
+ }
15739
15756
  function getWorkingTreeChangedFiles(repoRoot) {
15740
- const result = git(["diff", "HEAD", "--name-only", "--diff-filter=d"], repoRoot);
15757
+ const result = git(["diff", workingTreeBase(repoRoot), "--name-only", "--diff-filter=d"], repoRoot);
15741
15758
  if (!result.ok)
15742
15759
  return result;
15743
15760
  const files = result.value.split("\n").map((l) => l.trim()).filter(Boolean);
15744
15761
  return { ok: true, value: files };
15745
15762
  }
15746
15763
  function getWorkingTreeFileDiff(relPath, repoRoot) {
15747
- const result = git(["diff", "HEAD", "--", relPath], repoRoot);
15764
+ const result = git(["diff", workingTreeBase(repoRoot), "--", relPath], repoRoot);
15748
15765
  if (result.ok && result.value.trim())
15749
15766
  return result.value;
15750
15767
  return "";
@@ -16816,13 +16833,21 @@ function produceExcerpts(diffContent, descriptors, caps) {
16816
16833
  const excerpts = [];
16817
16834
  const seen = /* @__PURE__ */ new Set();
16818
16835
  const lines = diffContent.split("\n");
16819
- let lineNo = 0;
16836
+ let newLineNo;
16820
16837
  for (const raw of lines) {
16821
- if (raw.startsWith("-") && !raw.startsWith("---"))
16838
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
16839
+ if (hunk) {
16840
+ newLineNo = Number.parseInt(hunk[1], 10);
16822
16841
  continue;
16823
- if (raw.startsWith("+++") || raw.startsWith("---"))
16842
+ }
16843
+ if (newLineNo === void 0)
16844
+ continue;
16845
+ if (raw.startsWith("\\"))
16824
16846
  continue;
16825
- lineNo += 1;
16847
+ if (raw.startsWith("-"))
16848
+ continue;
16849
+ const lineNo = newLineNo;
16850
+ newLineNo += 1;
16826
16851
  if (!raw.startsWith("+"))
16827
16852
  continue;
16828
16853
  const text = raw.slice(1).trim();
@@ -17092,7 +17117,10 @@ async function runCi() {
17092
17117
  };
17093
17118
  });
17094
17119
  const context = loadChangeSetContext(repoRoot);
17095
- const changeSet = buildChangeSet(fileDiffEntries, { ...context, excerpts: readExcerptConfig() });
17120
+ const changeSet = buildChangeSet(fileDiffEntries, {
17121
+ ...context,
17122
+ excerpts: { enabled: false, ...EXCERPT_DEFAULTS }
17123
+ });
17096
17124
  const configResult = readConfig();
17097
17125
  if (!configResult.config) {
17098
17126
  const oidc = readActionsOidcEnv();
@@ -17437,10 +17465,143 @@ async function evaluateWithRefresh(opts) {
17437
17465
  return { result: retry, token: refreshed.token, remedy: HOOK_REMEDY };
17438
17466
  }
17439
17467
 
17468
+ // dist/hooks/marker.js
17469
+ import { createHash } from "node:crypto";
17470
+ import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17471
+ import { tmpdir } from "node:os";
17472
+ import { join as join6 } from "node:path";
17473
+ var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
17474
+ var MARKER_PREFIX = "halfcycle-session-";
17475
+ var MARKER_SUFFIX = ".ref";
17476
+ var DEDUPE_SUFFIX = ".eval";
17477
+ function markerDir() {
17478
+ const raw = process.env["TMPDIR"] ?? tmpdir();
17479
+ return raw.replace(/\/+$/, "");
17480
+ }
17481
+ function repoHash(repoRoot) {
17482
+ return createHash("sha256").update(repoRoot).digest("hex").slice(0, 12);
17483
+ }
17484
+ function markerFileName(repoRoot, sessionId) {
17485
+ const hash2 = repoHash(repoRoot);
17486
+ const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17487
+ return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
17488
+ }
17489
+ function markerPath(repoRoot, sessionId) {
17490
+ return join6(markerDir(), markerFileName(repoRoot, sessionId));
17491
+ }
17492
+ function dedupePath(repoRoot, sessionId) {
17493
+ const hash2 = repoHash(repoRoot);
17494
+ const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17495
+ return join6(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17496
+ }
17497
+ function readMarker(repoRoot, sessionId) {
17498
+ const path = markerPath(repoRoot, sessionId);
17499
+ if (!existsSync3(path))
17500
+ return null;
17501
+ try {
17502
+ return readFileSync6(path, "utf-8").trim() || null;
17503
+ } catch {
17504
+ return null;
17505
+ }
17506
+ }
17507
+ function deleteMarker(repoRoot, sessionId) {
17508
+ for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17509
+ try {
17510
+ if (existsSync3(path))
17511
+ unlinkSync3(path);
17512
+ } catch {
17513
+ }
17514
+ }
17515
+ }
17516
+ var NO_REPO_NOTICE_SUFFIX = ".norepo";
17517
+ function firstNoRepoNoticeThisSession(folder, sessionId) {
17518
+ if (!sessionId)
17519
+ return true;
17520
+ const path = join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}${NO_REPO_NOTICE_SUFFIX}`);
17521
+ try {
17522
+ writeFileSync3(path, "", { encoding: "utf-8", flag: "wx" });
17523
+ return true;
17524
+ } catch (err) {
17525
+ return err.code !== "EEXIST";
17526
+ }
17527
+ }
17528
+ function readDedupe(repoRoot, sessionId) {
17529
+ const path = dedupePath(repoRoot, sessionId);
17530
+ if (!existsSync3(path))
17531
+ return null;
17532
+ try {
17533
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
17534
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
17535
+ return parsed;
17536
+ }
17537
+ return null;
17538
+ } catch {
17539
+ return null;
17540
+ }
17541
+ }
17542
+ function writeDedupe(repoRoot, sessionId, state) {
17543
+ try {
17544
+ writeFileSync3(dedupePath(repoRoot, sessionId), JSON.stringify(state), "utf-8");
17545
+ } catch {
17546
+ }
17547
+ }
17548
+ function digestChangeState(base, entries) {
17549
+ const h = createHash("sha256");
17550
+ h.update(base);
17551
+ h.update("\0");
17552
+ for (const e of [...entries].sort((a, b) => a.path.localeCompare(b.path))) {
17553
+ h.update(e.path);
17554
+ h.update("\0");
17555
+ h.update(e.diffContent);
17556
+ h.update("\0");
17557
+ }
17558
+ return h.digest("hex");
17559
+ }
17560
+ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17561
+ const dir = markerDir();
17562
+ const hash2 = repoHash(repoRoot);
17563
+ const keepName = keepSessionId ? markerFileName(repoRoot, keepSessionId) : markerFileName(repoRoot);
17564
+ let names;
17565
+ try {
17566
+ names = readdirSync3(dir);
17567
+ } catch {
17568
+ return;
17569
+ }
17570
+ for (const name of names) {
17571
+ if (!name.startsWith(`${MARKER_PREFIX}${hash2}`) || !name.endsWith(MARKER_SUFFIX))
17572
+ continue;
17573
+ if (name === keepName)
17574
+ continue;
17575
+ const full = join6(dir, name);
17576
+ let unusable = false;
17577
+ try {
17578
+ const ref = readFileSync6(full, "utf-8").trim();
17579
+ if (!ref || !refKnown(ref))
17580
+ unusable = true;
17581
+ } catch {
17582
+ unusable = true;
17583
+ }
17584
+ if (!unusable) {
17585
+ try {
17586
+ const age = now - statSync(full).mtimeMs;
17587
+ if (age > MARKER_TTL_MS)
17588
+ unusable = true;
17589
+ } catch {
17590
+ unusable = true;
17591
+ }
17592
+ }
17593
+ if (unusable) {
17594
+ const mid = name.slice(`${MARKER_PREFIX}${hash2}`.length, -MARKER_SUFFIX.length);
17595
+ const sid = mid.startsWith("-") ? mid.slice(1) : void 0;
17596
+ deleteMarker(repoRoot, sid);
17597
+ }
17598
+ }
17599
+ }
17600
+
17440
17601
  // dist/seat-configuration.js
17441
- import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
17602
+ import { existsSync as existsSync4, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
17442
17603
  import { homedir as homedir3 } from "node:os";
17443
- import { join as join6, relative as relative3, resolve } from "node:path";
17604
+ import { join as join7, relative as relative3, resolve } from "node:path";
17444
17605
  function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
17445
17606
  try {
17446
17607
  const facts = read(repoRoot);
@@ -17475,7 +17636,7 @@ function isPlainObject2(value) {
17475
17636
  function readSettingsFile(projectDir, rel) {
17476
17637
  let raw;
17477
17638
  try {
17478
- raw = readFileSync6(join6(projectDir, rel), "utf-8");
17639
+ raw = readFileSync7(join7(projectDir, rel), "utf-8");
17479
17640
  } catch (err) {
17480
17641
  const code = err.code;
17481
17642
  const state = code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
@@ -17547,7 +17708,7 @@ function readPathStates(projectDir, repoRoot) {
17547
17708
  const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
17548
17709
  if (!status.ok)
17549
17710
  return unknown2();
17550
- const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync3(join6(projectDir, rel)));
17711
+ const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync4(join7(projectDir, rel)));
17551
17712
  return mapped ?? unknown2();
17552
17713
  }
17553
17714
  function mapWiringStatus(output, prefix, paths, onDisk) {
@@ -17612,7 +17773,9 @@ async function evaluateEdit(reply, readSeat) {
17612
17773
  return;
17613
17774
  const repoRootResult = getRepoRoot(process.cwd());
17614
17775
  if (!repoRootResult.ok) {
17615
- reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17776
+ if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
17777
+ reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17778
+ }
17616
17779
  return;
17617
17780
  }
17618
17781
  const repoRoot = repoRootResult.value;
@@ -17727,127 +17890,6 @@ function readStdin() {
17727
17890
  });
17728
17891
  }
17729
17892
 
17730
- // dist/hooks/marker.js
17731
- import { createHash } from "node:crypto";
17732
- import { existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17733
- import { tmpdir } from "node:os";
17734
- import { join as join7 } from "node:path";
17735
- var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
17736
- var MARKER_PREFIX = "halfcycle-session-";
17737
- var MARKER_SUFFIX = ".ref";
17738
- var DEDUPE_SUFFIX = ".eval";
17739
- function markerDir() {
17740
- const raw = process.env["TMPDIR"] ?? tmpdir();
17741
- return raw.replace(/\/+$/, "");
17742
- }
17743
- function repoHash(repoRoot) {
17744
- return createHash("sha256").update(repoRoot).digest("hex").slice(0, 12);
17745
- }
17746
- function markerFileName(repoRoot, sessionId) {
17747
- const hash2 = repoHash(repoRoot);
17748
- const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17749
- return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
17750
- }
17751
- function markerPath(repoRoot, sessionId) {
17752
- return join7(markerDir(), markerFileName(repoRoot, sessionId));
17753
- }
17754
- function dedupePath(repoRoot, sessionId) {
17755
- const hash2 = repoHash(repoRoot);
17756
- const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17757
- return join7(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17758
- }
17759
- function readMarker(repoRoot, sessionId) {
17760
- const path = markerPath(repoRoot, sessionId);
17761
- if (!existsSync4(path))
17762
- return null;
17763
- try {
17764
- return readFileSync7(path, "utf-8").trim() || null;
17765
- } catch {
17766
- return null;
17767
- }
17768
- }
17769
- function deleteMarker(repoRoot, sessionId) {
17770
- for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17771
- try {
17772
- if (existsSync4(path))
17773
- unlinkSync3(path);
17774
- } catch {
17775
- }
17776
- }
17777
- }
17778
- function readDedupe(repoRoot, sessionId) {
17779
- const path = dedupePath(repoRoot, sessionId);
17780
- if (!existsSync4(path))
17781
- return null;
17782
- try {
17783
- const parsed = JSON.parse(readFileSync7(path, "utf-8"));
17784
- if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
17785
- return parsed;
17786
- }
17787
- return null;
17788
- } catch {
17789
- return null;
17790
- }
17791
- }
17792
- function writeDedupe(repoRoot, sessionId, state) {
17793
- try {
17794
- writeFileSync3(dedupePath(repoRoot, sessionId), JSON.stringify(state), "utf-8");
17795
- } catch {
17796
- }
17797
- }
17798
- function digestChangeState(base, entries) {
17799
- const h = createHash("sha256");
17800
- h.update(base);
17801
- h.update("\0");
17802
- for (const e of [...entries].sort((a, b) => a.path.localeCompare(b.path))) {
17803
- h.update(e.path);
17804
- h.update("\0");
17805
- h.update(e.diffContent);
17806
- h.update("\0");
17807
- }
17808
- return h.digest("hex");
17809
- }
17810
- function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17811
- const dir = markerDir();
17812
- const hash2 = repoHash(repoRoot);
17813
- const keepName = keepSessionId ? markerFileName(repoRoot, keepSessionId) : markerFileName(repoRoot);
17814
- let names;
17815
- try {
17816
- names = readdirSync3(dir);
17817
- } catch {
17818
- return;
17819
- }
17820
- for (const name of names) {
17821
- if (!name.startsWith(`${MARKER_PREFIX}${hash2}`) || !name.endsWith(MARKER_SUFFIX))
17822
- continue;
17823
- if (name === keepName)
17824
- continue;
17825
- const full = join7(dir, name);
17826
- let unusable = false;
17827
- try {
17828
- const ref = readFileSync7(full, "utf-8").trim();
17829
- if (!ref || !refKnown(ref))
17830
- unusable = true;
17831
- } catch {
17832
- unusable = true;
17833
- }
17834
- if (!unusable) {
17835
- try {
17836
- const age = now - statSync(full).mtimeMs;
17837
- if (age > MARKER_TTL_MS)
17838
- unusable = true;
17839
- } catch {
17840
- unusable = true;
17841
- }
17842
- }
17843
- if (unusable) {
17844
- const mid = name.slice(`${MARKER_PREFIX}${hash2}`.length, -MARKER_SUFFIX.length);
17845
- const sid = mid.startsWith("-") ? mid.slice(1) : void 0;
17846
- deleteMarker(repoRoot, sid);
17847
- }
17848
- }
17849
- }
17850
-
17851
17893
  // dist/hooks/session-diff.js
17852
17894
  var STOP_HOOK_EVENT = {
17853
17895
  stop: "Stop",
@@ -17891,7 +17933,9 @@ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
17891
17933
  async function evaluateSession(input, kind, reply, readSeat) {
17892
17934
  const repoRootResult = getRepoRoot(process.cwd());
17893
17935
  if (!repoRootResult.ok) {
17894
- reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17936
+ if (firstNoRepoNoticeThisSession(process.cwd(), input.session_id)) {
17937
+ reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17938
+ }
17895
17939
  return;
17896
17940
  }
17897
17941
  const repoRoot = repoRootResult.value;
@@ -18363,6 +18407,7 @@ async function refreshCredentialToken(opts) {
18363
18407
  return { token: refreshed.token };
18364
18408
  }
18365
18409
  var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
18410
+ var CONNECTED_SENTENCE = "Halfcycle connected.";
18366
18411
  var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
18367
18412
  function registeredMcpUrl(projectRoot) {
18368
18413
  let parsed;
@@ -18448,7 +18493,8 @@ async function checkConnection(renewedToken, opts) {
18448
18493
  if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
18449
18494
  return PLACEHOLDER_SENTENCES[bearer];
18450
18495
  }
18451
- return probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
18496
+ const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
18497
+ return outcome ?? CONNECTED_SENTENCE;
18452
18498
  }
18453
18499
  function defaultWriteErr2(text) {
18454
18500
  process.stderr.write(text);
@@ -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.