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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +7 -13
- package/bin/bin.bundle.mjs +191 -145
- package/commands/halfcycle-setup.md +3 -3
- package/dist/bin.js +1 -6823
- package/dist/claude-version-probe.d.ts +32 -0
- package/dist/claude-version-probe.d.ts.map +1 -1
- package/dist/cli.js +7161 -0
- package/dist/cli.js.map +6 -0
- package/dist/confirm-identity.d.ts +26 -1
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +30 -0
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/identity.d.ts +50 -13
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +208 -116
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +57 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +11 -2
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/node-floor.d.ts +54 -0
- package/dist/node-floor.d.ts.map +1 -0
- package/dist/own-engagement.d.ts +25 -5
- package/dist/own-engagement.d.ts.map +1 -1
- package/dist/pre-create-checks.d.ts +61 -0
- package/dist/pre-create-checks.d.ts.map +1 -0
- package/dist/resolve-credential.d.ts.map +1 -1
- package/dist/uninstall.d.ts +31 -9
- package/dist/uninstall.d.ts.map +1 -1
- package/package.json +6 -2
- package/dist/bin.js.map +0 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
|
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
|
-
**
|
|
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,
|
|
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.
|
|
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
|
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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()
|
|
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
|
|
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",
|
|
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",
|
|
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
|
|
16836
|
+
let newLineNo;
|
|
16820
16837
|
for (const raw of lines) {
|
|
16821
|
-
|
|
16838
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
16839
|
+
if (hunk) {
|
|
16840
|
+
newLineNo = Number.parseInt(hunk[1], 10);
|
|
16822
16841
|
continue;
|
|
16823
|
-
|
|
16842
|
+
}
|
|
16843
|
+
if (newLineNo === void 0)
|
|
16844
|
+
continue;
|
|
16845
|
+
if (raw.startsWith("\\"))
|
|
16824
16846
|
continue;
|
|
16825
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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 =
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7
|
+
If resolution fails, report whichever applies and stop:
|
|
8
8
|
|
|
9
9
|
> Halfcycle didn't answer for `/halfcycle-setup`. **Nothing has run.**
|
|
10
|
-
>
|
|
11
|
-
> it there and accept the workspace-trust prompt
|
|
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.
|