halfcycle 0.4.0 → 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 +4 -8
- package/bin/bin.bundle.mjs +171 -138
- 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 +4 -1
- package/dist/bin.js.map +0 -6
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// dist/install.js
|
|
2
2
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3
3
|
import { createHash as createHash2 } from "node:crypto";
|
|
4
|
-
import { existsSync as existsSync3, lstatSync, mkdirSync as
|
|
5
|
-
import { dirname as
|
|
4
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5
|
+
import { dirname as dirname3, join as join5, posix, relative } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
// ../core/dist/guard-record.js
|
|
@@ -120,58 +120,70 @@ var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
|
120
120
|
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
121
121
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
122
122
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
123
|
+
function hookCommand(script, verb) {
|
|
124
|
+
const invocation = `bash "$CLAUDE_PROJECT_DIR"/${script}`;
|
|
125
|
+
return verb === void 0 ? invocation : `${invocation} ${verb}`;
|
|
126
|
+
}
|
|
123
127
|
var HALFCYCLE_HOOK_WIRING = [
|
|
124
128
|
{
|
|
125
129
|
event: "SessionStart",
|
|
126
130
|
matcher: null,
|
|
127
131
|
timeout: 5,
|
|
128
|
-
command:
|
|
132
|
+
command: hookCommand(SESSION_START_MARKER_REL),
|
|
129
133
|
script: SESSION_START_MARKER_REL
|
|
130
134
|
},
|
|
131
135
|
{
|
|
132
136
|
event: "SessionStart",
|
|
133
137
|
matcher: null,
|
|
134
138
|
timeout: 10,
|
|
135
|
-
command:
|
|
139
|
+
command: hookCommand(GUARD_RUNNER_REL, "refresh-credential"),
|
|
136
140
|
script: GUARD_RUNNER_REL
|
|
137
141
|
},
|
|
138
142
|
{
|
|
139
143
|
event: "UserPromptSubmit",
|
|
140
144
|
matcher: null,
|
|
141
145
|
timeout: 5,
|
|
142
|
-
command:
|
|
146
|
+
command: hookCommand(USER_PROMPT_REMINDER_REL),
|
|
143
147
|
script: USER_PROMPT_REMINDER_REL
|
|
144
148
|
},
|
|
145
149
|
{
|
|
146
150
|
event: "PostToolUse",
|
|
147
151
|
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
148
152
|
timeout: 60,
|
|
149
|
-
command:
|
|
153
|
+
command: hookCommand(GUARD_RUNNER_REL, "hook post-tool-use"),
|
|
150
154
|
script: GUARD_RUNNER_REL
|
|
151
155
|
},
|
|
152
156
|
{
|
|
153
157
|
event: "Stop",
|
|
154
158
|
matcher: null,
|
|
155
159
|
timeout: 60,
|
|
156
|
-
command:
|
|
160
|
+
command: hookCommand(GUARD_RUNNER_REL, "hook stop"),
|
|
157
161
|
script: GUARD_RUNNER_REL
|
|
158
162
|
},
|
|
159
163
|
{
|
|
160
164
|
event: "SubagentStop",
|
|
161
165
|
matcher: null,
|
|
162
166
|
timeout: 60,
|
|
163
|
-
command:
|
|
167
|
+
command: hookCommand(GUARD_RUNNER_REL, "hook subagent-stop"),
|
|
164
168
|
script: GUARD_RUNNER_REL
|
|
165
169
|
},
|
|
166
170
|
{
|
|
167
171
|
event: "SessionEnd",
|
|
168
172
|
matcher: null,
|
|
169
173
|
timeout: 5,
|
|
170
|
-
command:
|
|
174
|
+
command: hookCommand(SESSION_END_MARKER_REL),
|
|
171
175
|
script: SESSION_END_MARKER_REL
|
|
172
176
|
}
|
|
173
177
|
];
|
|
174
|
-
var RETIRED_HOOK_COMMANDS = [
|
|
178
|
+
var RETIRED_HOOK_COMMANDS = [
|
|
179
|
+
"bash ./.claude/hooks/session-start-marker.sh",
|
|
180
|
+
"bash ./.claude/hooks/guard-runner.sh refresh-credential",
|
|
181
|
+
"bash ./.claude/hooks/user-prompt-reminder.sh",
|
|
182
|
+
"bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
|
|
183
|
+
"bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
184
|
+
"bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
|
|
185
|
+
"bash ./.claude/hooks/session-end-marker.sh"
|
|
186
|
+
];
|
|
175
187
|
var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
|
|
176
188
|
...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
|
|
177
189
|
...RETIRED_HOOK_COMMANDS
|
|
@@ -1217,10 +1229,11 @@ halfcycle_env_file() {
|
|
|
1217
1229
|
|
|
1218
1230
|
// dist/identity.js
|
|
1219
1231
|
import { execFileSync } from "node:child_process";
|
|
1220
|
-
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1221
|
-
import { isAbsolute, join as join3 } from "node:path";
|
|
1232
|
+
import { existsSync, linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1233
|
+
import { dirname as dirname2, isAbsolute, join as join3 } from "node:path";
|
|
1222
1234
|
import { randomUUID } from "node:crypto";
|
|
1223
1235
|
var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
|
|
1236
|
+
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
1224
1237
|
var REPOSITORY_LOCATING_GIT_VARS = [
|
|
1225
1238
|
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
1226
1239
|
"GIT_COMMON_DIR",
|
|
@@ -1301,21 +1314,45 @@ function mintOrReadIdentity(targetRepoRoot) {
|
|
|
1301
1314
|
const path = join3(targetRepoRoot, ".halfcycle", "project.json");
|
|
1302
1315
|
if (existsSync(path)) {
|
|
1303
1316
|
const onDisk = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1304
|
-
const
|
|
1317
|
+
const identity = {
|
|
1305
1318
|
projectId: onDisk.projectId ?? "",
|
|
1306
1319
|
rootCommit: onDisk.rootCommit ?? null,
|
|
1307
1320
|
remote: onDisk.remote ?? null,
|
|
1308
1321
|
note: PROJECT_IDENTITY_NOTE
|
|
1309
1322
|
};
|
|
1310
|
-
return { identity
|
|
1323
|
+
return { identity, minted: false };
|
|
1311
1324
|
}
|
|
1312
|
-
|
|
1325
|
+
return { identity: mintIdentity(targetRepoRoot), minted: true };
|
|
1326
|
+
}
|
|
1327
|
+
function mintIdentity(targetRepoRoot) {
|
|
1328
|
+
return {
|
|
1313
1329
|
projectId: randomUUID(),
|
|
1314
1330
|
rootCommit: readRootCommit(targetRepoRoot),
|
|
1315
1331
|
remote: readRemote(targetRepoRoot),
|
|
1316
1332
|
note: PROJECT_IDENTITY_NOTE
|
|
1317
1333
|
};
|
|
1318
|
-
|
|
1334
|
+
}
|
|
1335
|
+
function serializeProjectIdentity(identity) {
|
|
1336
|
+
return JSON.stringify(identity, null, 2) + "\n";
|
|
1337
|
+
}
|
|
1338
|
+
function createProjectIdentityFile(targetRepoRoot) {
|
|
1339
|
+
const path = join3(targetRepoRoot, PROJECT_IDENTITY_REL);
|
|
1340
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1341
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
1342
|
+
try {
|
|
1343
|
+
writeFileSync2(temporary, serializeProjectIdentity(mintIdentity(targetRepoRoot)), {
|
|
1344
|
+
encoding: "utf-8",
|
|
1345
|
+
flag: "wx"
|
|
1346
|
+
});
|
|
1347
|
+
try {
|
|
1348
|
+
linkSync(temporary, path);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
if (err.code !== "EEXIST")
|
|
1351
|
+
throw err;
|
|
1352
|
+
}
|
|
1353
|
+
} finally {
|
|
1354
|
+
rmSync(temporary, { force: true });
|
|
1355
|
+
}
|
|
1319
1356
|
}
|
|
1320
1357
|
|
|
1321
1358
|
// dist/install-manifest.js
|
|
@@ -1600,6 +1637,13 @@ function mcpEndpointUrl(origin) {
|
|
|
1600
1637
|
return `${origin.trim().replace(/\/+$/, "")}${MCP_ENDPOINT_PATH}`;
|
|
1601
1638
|
}
|
|
1602
1639
|
|
|
1640
|
+
// dist/node-floor.js
|
|
1641
|
+
var NODE_FLOOR_MAJOR = 18;
|
|
1642
|
+
var NODE_FLOOR_SENTENCE_HEAD = `Halfcycle needs Node ${NODE_FLOOR_MAJOR} or newer; you have `;
|
|
1643
|
+
function nodeFloorProgram(guarded) {
|
|
1644
|
+
return `if(+process.versions.node.split('.')[0]<${NODE_FLOOR_MAJOR}){console.error('${NODE_FLOOR_SENTENCE_HEAD}'+process.versions.node);process.exitCode=1}else{${guarded}}`;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1603
1647
|
// dist/project-page.js
|
|
1604
1648
|
function projectPageUrl(engagementId) {
|
|
1605
1649
|
return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
|
|
@@ -1615,7 +1659,7 @@ function mergeSettings(existing, generated) {
|
|
|
1615
1659
|
merged.$schema = generated.$schema;
|
|
1616
1660
|
}
|
|
1617
1661
|
const generatedHooks = generated.hooks ?? {};
|
|
1618
|
-
const halfcycleCommands =
|
|
1662
|
+
const halfcycleCommands = new Set(HALFCYCLE_HOOK_COMMANDS);
|
|
1619
1663
|
for (const entries of Object.values(generatedHooks)) {
|
|
1620
1664
|
for (const entry of entries) {
|
|
1621
1665
|
for (const h of entry.hooks)
|
|
@@ -1648,7 +1692,7 @@ function mergePermissions(existing, generated) {
|
|
|
1648
1692
|
}
|
|
1649
1693
|
|
|
1650
1694
|
// dist/scan.js
|
|
1651
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
1695
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1652
1696
|
import { join as join4 } from "node:path";
|
|
1653
1697
|
var HALFCYCLE_STATE_FORMAT = "halfcycle-state-file/v1";
|
|
1654
1698
|
var HALFCYCLE_STATE_NOTE = [
|
|
@@ -1692,8 +1736,8 @@ function runBootstrapScan(targetRepo) {
|
|
|
1692
1736
|
return { state: existing, ran: false };
|
|
1693
1737
|
}
|
|
1694
1738
|
const state = scanLayers(targetRepo);
|
|
1695
|
-
|
|
1696
|
-
|
|
1739
|
+
mkdirSync3(join4(targetRepo, ".halfcycle"), { recursive: true });
|
|
1740
|
+
writeFileSync3(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1697
1741
|
return { state, ran: true };
|
|
1698
1742
|
}
|
|
1699
1743
|
function mergeWithRecords(state, layersWithRealRecords) {
|
|
@@ -1715,7 +1759,7 @@ function mergeWithRecords(state, layersWithRealRecords) {
|
|
|
1715
1759
|
// dist/install.js
|
|
1716
1760
|
var ZONE_B_DIR = ".workbench";
|
|
1717
1761
|
var __filename = fileURLToPath(import.meta.url);
|
|
1718
|
-
var __dirname =
|
|
1762
|
+
var __dirname = dirname3(__filename);
|
|
1719
1763
|
function findBundleRoot(startDir) {
|
|
1720
1764
|
let candidate = startDir;
|
|
1721
1765
|
for (let i = 0; i < 4; i++) {
|
|
@@ -1725,7 +1769,7 @@ function findBundleRoot(startDir) {
|
|
|
1725
1769
|
return candidate;
|
|
1726
1770
|
} catch {
|
|
1727
1771
|
}
|
|
1728
|
-
candidate =
|
|
1772
|
+
candidate = dirname3(candidate);
|
|
1729
1773
|
}
|
|
1730
1774
|
return join5(startDir, "..");
|
|
1731
1775
|
}
|
|
@@ -1739,7 +1783,7 @@ function resolveVendoredBinary() {
|
|
|
1739
1783
|
if (existsSync3(candidate))
|
|
1740
1784
|
return candidate;
|
|
1741
1785
|
}
|
|
1742
|
-
throw new Error(`[
|
|
1786
|
+
throw new Error(`[halfcycle] Cannot find the self-contained guard binary bin.bundle.mjs. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
1743
1787
|
}
|
|
1744
1788
|
var WRITE_ALLOWLIST = [
|
|
1745
1789
|
".claude/commands",
|
|
@@ -1761,9 +1805,11 @@ function isAllowlisted(targetRelPath) {
|
|
|
1761
1805
|
return normalised === p || normalised.startsWith(p + "/");
|
|
1762
1806
|
});
|
|
1763
1807
|
}
|
|
1764
|
-
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
1765
1808
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
1766
1809
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
1810
|
+
function capturedIndexScaffold() {
|
|
1811
|
+
return join5(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
|
|
1812
|
+
}
|
|
1767
1813
|
var GITIGNORE_REL = ".gitignore";
|
|
1768
1814
|
var GITATTRIBUTES_REL = ".gitattributes";
|
|
1769
1815
|
function commandStubRel(name) {
|
|
@@ -1773,8 +1819,9 @@ var RETIRED_MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
|
1773
1819
|
var RETIRED_WRITE_SET_FILES = [RETIRED_MCP_HEADERS_HELPER_REL];
|
|
1774
1820
|
var RETIRED_DENY_PATTERNS = [];
|
|
1775
1821
|
function closedWriteSet() {
|
|
1822
|
+
const commands = readPluginManifest().commands ?? [];
|
|
1776
1823
|
const files = /* @__PURE__ */ new Set([
|
|
1777
|
-
...
|
|
1824
|
+
...commands.map((cmd) => commandStubRel(cmd.name)),
|
|
1778
1825
|
...Object.keys(OWNED_GENERATED_HEADERS),
|
|
1779
1826
|
VENDORED_BIN_REL,
|
|
1780
1827
|
CREW_ROSTER_REL,
|
|
@@ -1803,8 +1850,22 @@ function closedWriteSet() {
|
|
|
1803
1850
|
gitattributesLines: /* @__PURE__ */ new Set([
|
|
1804
1851
|
GITATTRIBUTES_HEADER,
|
|
1805
1852
|
...lfRules([...Object.keys(OWNED_GENERATED_HEADERS), ...RETIRED_WRITE_SET_FILES])
|
|
1806
|
-
])
|
|
1853
|
+
]),
|
|
1854
|
+
generatedSha256: sha256OfWhatThisVersionWrites(commands)
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
function sha256OfWhatThisVersionWrites(commands) {
|
|
1858
|
+
const out = /* @__PURE__ */ new Map();
|
|
1859
|
+
const fromFile = (rel, src) => {
|
|
1860
|
+
if (existsSync3(src))
|
|
1861
|
+
out.set(rel, sha256Hex(readFileSync4(src)));
|
|
1807
1862
|
};
|
|
1863
|
+
for (const cmd of commands)
|
|
1864
|
+
fromFile(commandStubRel(cmd.name), join5(BUNDLE_ROOT, ".claude-plugin", cmd.path));
|
|
1865
|
+
for (const [rel, content] of hookScriptsThisVersionWrites())
|
|
1866
|
+
out.set(rel, sha256Hex(content));
|
|
1867
|
+
fromFile(CAPTURED_INDEX_REL, capturedIndexScaffold());
|
|
1868
|
+
return out;
|
|
1808
1869
|
}
|
|
1809
1870
|
function readPreviousManifest(targetRepo) {
|
|
1810
1871
|
try {
|
|
@@ -1823,7 +1884,7 @@ function noteFile(draft, targetRepo, rel, outcome) {
|
|
|
1823
1884
|
function recordSettingsMerge(draft, existing, generated, preexisted) {
|
|
1824
1885
|
const generatedHooks = generated.hooks ?? {};
|
|
1825
1886
|
const hookCommands = Object.values(generatedHooks).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
|
|
1826
|
-
const ours = new Set(hookCommands);
|
|
1887
|
+
const ours = /* @__PURE__ */ new Set([...hookCommands, ...HALFCYCLE_HOOK_COMMANDS]);
|
|
1827
1888
|
const existingHooks = isPlainObject(existing.hooks) ? existing.hooks : void 0;
|
|
1828
1889
|
const heldOurs = Object.values(existingHooks ?? {}).some((entries) => Array.isArray(entries) && entries.some((entry) => isHalfcycleEntry(entry, ours)));
|
|
1829
1890
|
const existingDeny = Array.isArray(existing.permissions?.deny) ? existing.permissions.deny : [];
|
|
@@ -1865,13 +1926,13 @@ function recordMcpAdoptionOnly(draft, existingText) {
|
|
|
1865
1926
|
return;
|
|
1866
1927
|
recordMcp(draft, { created: false, adopted: true, mcpServersCreated: false });
|
|
1867
1928
|
}
|
|
1868
|
-
function recordGitignoreReconcile(draft, before, after) {
|
|
1869
|
-
const adopted = draft.gitignore === void 0 && before !== null ?
|
|
1870
|
-
|
|
1871
|
-
recordGitignore(draft, before === null && after !== null, [adopted, appended]);
|
|
1929
|
+
function recordGitignoreReconcile(draft, before, after, writeSet) {
|
|
1930
|
+
const adopted = draft.gitignore === void 0 && before !== null ? adoptBlockUnderHeader(before, GITIGNORE_HEADER, writeSet.gitignoreLines) : "";
|
|
1931
|
+
recordGitignore(draft, before === null && after !== null, [adopted, appendedText(before, after)]);
|
|
1872
1932
|
}
|
|
1873
|
-
function recordGitattributesReconcile(draft, before, after) {
|
|
1874
|
-
|
|
1933
|
+
function recordGitattributesReconcile(draft, before, after, writeSet) {
|
|
1934
|
+
const adopted = draft.gitattributes === void 0 && before !== null ? adoptBlockUnderHeader(before, GITATTRIBUTES_HEADER, writeSet.gitattributesLines) : "";
|
|
1935
|
+
recordGitattributes(draft, before === null && after !== null, [adopted, appendedText(before, after)]);
|
|
1875
1936
|
}
|
|
1876
1937
|
function appendedText(before, after) {
|
|
1877
1938
|
if (after === null || after === before)
|
|
@@ -1887,7 +1948,7 @@ function assertManifestInWriteSet(manifest, writeSet) {
|
|
|
1887
1948
|
...manifest.createdDirs.filter((p) => !writeSet.dirs.has(p))
|
|
1888
1949
|
];
|
|
1889
1950
|
if (strays.length > 0) {
|
|
1890
|
-
throw new Error(`[
|
|
1951
|
+
throw new Error(`[halfcycle] the install record names paths outside the write set: ${strays.join(", ")}`);
|
|
1891
1952
|
}
|
|
1892
1953
|
}
|
|
1893
1954
|
function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
@@ -1939,6 +2000,13 @@ function lstatOrNull(path) {
|
|
|
1939
2000
|
return null;
|
|
1940
2001
|
}
|
|
1941
2002
|
}
|
|
2003
|
+
function readFileOrNull(path) {
|
|
2004
|
+
try {
|
|
2005
|
+
return readFileSync4(path, "utf-8");
|
|
2006
|
+
} catch {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1942
2010
|
function isPlainObject(value) {
|
|
1943
2011
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1944
2012
|
}
|
|
@@ -1951,7 +2019,7 @@ function copyManifestCommands(manifest, targetRepo, report, draft) {
|
|
|
1951
2019
|
for (const cmd of manifest.commands ?? []) {
|
|
1952
2020
|
const srcFile = join5(BUNDLE_ROOT, ".claude-plugin", cmd.path);
|
|
1953
2021
|
if (!existsSync3(srcFile)) {
|
|
1954
|
-
throw new Error(`[
|
|
2022
|
+
throw new Error(`[halfcycle] plugin.json declares command "${cmd.name}" at "${cmd.path}", but no file exists there (${srcFile}). The manifest and commands/ must agree.`);
|
|
1955
2023
|
}
|
|
1956
2024
|
const rel = commandStubRel(cmd.name);
|
|
1957
2025
|
const destFile = join5(targetRepo, rel);
|
|
@@ -1964,46 +2032,48 @@ function copyManifestCommands(manifest, targetRepo, report, draft) {
|
|
|
1964
2032
|
function writeAllowlisted(targetAbsPath, targetRepoRoot, content, writtenPaths) {
|
|
1965
2033
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
1966
2034
|
if (!isAllowlisted(rel)) {
|
|
1967
|
-
throw new Error(`[
|
|
2035
|
+
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
1968
2036
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
2037
|
+
if (readFileOrNull(targetAbsPath) === content)
|
|
2038
|
+
return;
|
|
2039
|
+
mkdirSync4(dirname3(targetAbsPath), { recursive: true });
|
|
2040
|
+
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
1971
2041
|
writtenPaths.push(rel);
|
|
1972
2042
|
}
|
|
1973
2043
|
function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
|
|
1974
2044
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
1975
2045
|
if (!isAllowlisted(rel)) {
|
|
1976
|
-
throw new Error(`[
|
|
2046
|
+
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
1977
2047
|
}
|
|
1978
2048
|
if (existsSync3(targetAbsPath)) {
|
|
1979
2049
|
const current = readFileSync4(targetAbsPath, "utf-8");
|
|
1980
2050
|
return current === content ? "skipped" : "collided";
|
|
1981
2051
|
}
|
|
1982
|
-
|
|
1983
|
-
|
|
2052
|
+
mkdirSync4(dirname3(targetAbsPath), { recursive: true });
|
|
2053
|
+
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
1984
2054
|
return "written";
|
|
1985
2055
|
}
|
|
1986
2056
|
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
1987
2057
|
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
1988
2058
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
1989
2059
|
if (!isAllowlisted(rel)) {
|
|
1990
|
-
throw new Error(`[
|
|
2060
|
+
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
1991
2061
|
}
|
|
1992
2062
|
if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
|
|
1993
|
-
throw new Error(`[
|
|
2063
|
+
throw new Error(`[halfcycle] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (writeOwnedGenerated) rather than assumed from where it sits.`);
|
|
1994
2064
|
}
|
|
1995
2065
|
if (existsSync3(targetAbsPath) && readFileSync4(targetAbsPath, "utf-8") === content) {
|
|
1996
2066
|
return "skipped";
|
|
1997
2067
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2068
|
+
mkdirSync4(dirname3(targetAbsPath), { recursive: true });
|
|
2069
|
+
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
2000
2070
|
return "written";
|
|
2001
2071
|
}
|
|
2002
2072
|
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
2003
2073
|
function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
2004
2074
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
2005
2075
|
if (!isAllowlisted(rel)) {
|
|
2006
|
-
throw new Error(`[
|
|
2076
|
+
throw new Error(`[halfcycle] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
2007
2077
|
}
|
|
2008
2078
|
let replacedExisting = false;
|
|
2009
2079
|
if (existsSync3(targetAbsPath)) {
|
|
@@ -2015,8 +2085,8 @@ function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMa
|
|
|
2015
2085
|
return { outcome: "collided", replacedExisting: false };
|
|
2016
2086
|
replacedExisting = true;
|
|
2017
2087
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2088
|
+
mkdirSync4(dirname3(targetAbsPath), { recursive: true });
|
|
2089
|
+
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
2020
2090
|
return { outcome: "written", replacedExisting };
|
|
2021
2091
|
}
|
|
2022
2092
|
function recordOwnedGenerated(report, targetAbsPath, targetRepoRoot, content, generatedMarker, rel) {
|
|
@@ -2054,6 +2124,26 @@ var DENY_PATTERNS = [
|
|
|
2054
2124
|
"Bash(git commit --no-verify:*)"
|
|
2055
2125
|
];
|
|
2056
2126
|
var USER_PROMPT_REMINDER_SENTENCE = "Where this project stands, and what order to work in, is recorded in the Halfcycle service and nowhere in this repository \u2014 call halfcycle_resolve before you answer or plan, because the files here will look complete and current and still be wrong about position.";
|
|
2127
|
+
function readExistingSettings(targetRepo) {
|
|
2128
|
+
const settingsPath = join5(targetRepo, SETTINGS_REL);
|
|
2129
|
+
return existsSync3(settingsPath) ? JSON.parse(readFileSync4(settingsPath, "utf-8")) : void 0;
|
|
2130
|
+
}
|
|
2131
|
+
var SettingsParseRefused = class extends Error {
|
|
2132
|
+
constructor(message) {
|
|
2133
|
+
super(message);
|
|
2134
|
+
this.name = "SettingsParseRefused";
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
function settingsParseRefusal(targetRepo, err) {
|
|
2138
|
+
const file = join5(targetRepo, SETTINGS_REL);
|
|
2139
|
+
const offset = /position (\d+)/.exec(err instanceof Error ? err.message : "")?.[1];
|
|
2140
|
+
let where = "";
|
|
2141
|
+
if (offset !== void 0) {
|
|
2142
|
+
const before = readFileSync4(file, "utf-8").slice(0, Number(offset)).split("\n");
|
|
2143
|
+
where = ` (line ${before.length}, column ${before[before.length - 1].length + 1})`;
|
|
2144
|
+
}
|
|
2145
|
+
return `${file} could not be read as JSON${where} \u2014 fix it and run "npx halfcycle" again.`;
|
|
2146
|
+
}
|
|
2057
2147
|
function generateSettingsJson() {
|
|
2058
2148
|
const hooks = {};
|
|
2059
2149
|
for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
|
|
@@ -2118,6 +2208,10 @@ if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
|
2118
2208
|
export HALFCYCLE_NOT_THIS_ACCOUNT=1
|
|
2119
2209
|
fi
|
|
2120
2210
|
|
|
2211
|
+
# On a Node older than Halfcycle supports, the runner cannot even be read, so
|
|
2212
|
+
# say that in one line instead of handing it a stack trace.
|
|
2213
|
+
node -e "${nodeFloorProgram("")}" || exit
|
|
2214
|
+
|
|
2121
2215
|
exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
|
|
2122
2216
|
`;
|
|
2123
2217
|
}
|
|
@@ -2130,7 +2224,7 @@ function noNodeReplyShell() {
|
|
|
2130
2224
|
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: NO_NODE_SENTENCE }
|
|
2131
2225
|
});
|
|
2132
2226
|
if (reply.includes("'")) {
|
|
2133
|
-
throw new Error("[
|
|
2227
|
+
throw new Error("[halfcycle] the no-Node notice cannot be single-quoted; refusing to write the session-start script.");
|
|
2134
2228
|
}
|
|
2135
2229
|
return `if ! command -v node >/dev/null 2>&1; then
|
|
2136
2230
|
printf '%s\\n' '${reply}'
|
|
@@ -2293,13 +2387,13 @@ function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeade
|
|
|
2293
2387
|
if (!isTrackedAndClean(targetRepo, rel))
|
|
2294
2388
|
return "kept-uncommitted";
|
|
2295
2389
|
try {
|
|
2296
|
-
|
|
2390
|
+
rmSync2(path);
|
|
2297
2391
|
} catch {
|
|
2298
2392
|
return "failed";
|
|
2299
2393
|
}
|
|
2300
2394
|
return "removed";
|
|
2301
2395
|
}
|
|
2302
|
-
var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
|
|
2396
|
+
var MCP_HEADERS_HELPER_COMMAND = `node -e "${nodeFloorProgram([
|
|
2303
2397
|
`const p=require('path'),f=require('fs'),c=require('child_process');`,
|
|
2304
2398
|
`let d=process.cwd();`,
|
|
2305
2399
|
`for(;;){`,
|
|
@@ -2312,7 +2406,7 @@ var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
|
|
|
2312
2406
|
`d=u}`,
|
|
2313
2407
|
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
|
|
2314
2408
|
`process.exit(1)`
|
|
2315
|
-
].join("")}"`;
|
|
2409
|
+
].join(""))}"`;
|
|
2316
2410
|
var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
2317
2411
|
"37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
|
|
2318
2412
|
"6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
|
|
@@ -2326,6 +2420,14 @@ var OWNED_GENERATED_HEADERS = {
|
|
|
2326
2420
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
2327
2421
|
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
2328
2422
|
};
|
|
2423
|
+
function hookScriptsThisVersionWrites() {
|
|
2424
|
+
return [
|
|
2425
|
+
[".claude/hooks/guard-runner.sh", generateGuardRunnerWrapper()],
|
|
2426
|
+
[".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
|
|
2427
|
+
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
2428
|
+
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
2429
|
+
];
|
|
2430
|
+
}
|
|
2329
2431
|
function generateMcpRegistration(existing, mcpOrigin) {
|
|
2330
2432
|
let base = { mcpServers: {} };
|
|
2331
2433
|
if (existing !== null) {
|
|
@@ -2346,13 +2448,12 @@ function generateMcpRegistration(existing, mcpOrigin) {
|
|
|
2346
2448
|
}
|
|
2347
2449
|
var REQUIRED_GITIGNORE_ENTRIES = [".halfcycle/state.json", `${ZONE_B_DIR}/`];
|
|
2348
2450
|
var GITIGNORE_HEADER = "# Halfcycle \u2014 machine-local secrets/state and Zone-B (never push to client remote)";
|
|
2349
|
-
function
|
|
2350
|
-
const claimable = /* @__PURE__ */ new Set([...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]);
|
|
2451
|
+
function adoptBlockUnderHeader(before, header, claimable) {
|
|
2351
2452
|
const claimed = [];
|
|
2352
2453
|
let underHeader = false;
|
|
2353
2454
|
const lines = before.split("\n");
|
|
2354
2455
|
for (const [i, line] of lines.entries()) {
|
|
2355
|
-
if (line ===
|
|
2456
|
+
if (line === header) {
|
|
2356
2457
|
if (i > 0 && lines[i - 1] === "")
|
|
2357
2458
|
claimed.push("");
|
|
2358
2459
|
claimed.push(line);
|
|
@@ -2375,7 +2476,7 @@ function reconcileGitignore(targetRepo) {
|
|
|
2375
2476
|
try {
|
|
2376
2477
|
if (!existsSync3(gitignorePath)) {
|
|
2377
2478
|
const body = [header, ...REQUIRED_GITIGNORE_ENTRIES].join("\n") + "\n";
|
|
2378
|
-
|
|
2479
|
+
writeFileSync4(gitignorePath, body, "utf-8");
|
|
2379
2480
|
return "written";
|
|
2380
2481
|
}
|
|
2381
2482
|
const existing = readFileSync4(gitignorePath, "utf-8");
|
|
@@ -2386,7 +2487,7 @@ function reconcileGitignore(targetRepo) {
|
|
|
2386
2487
|
const suffix = (existing.endsWith("\n") ? "" : "\n") + `
|
|
2387
2488
|
${header}
|
|
2388
2489
|
` + toAdd.join("\n") + "\n";
|
|
2389
|
-
|
|
2490
|
+
writeFileSync4(gitignorePath, existing + suffix, "utf-8");
|
|
2390
2491
|
return "written";
|
|
2391
2492
|
} catch {
|
|
2392
2493
|
return "failed";
|
|
@@ -2494,12 +2595,9 @@ function checkDrift(targetRepoRoot) {
|
|
|
2494
2595
|
return { drifted: installed !== current, installed, current, methodVersion: pin?.methodVersion ?? null };
|
|
2495
2596
|
}
|
|
2496
2597
|
function writeProjectIdentity(targetRepo) {
|
|
2497
|
-
|
|
2498
|
-
const { identity } = mintOrReadIdentity(targetRepo);
|
|
2499
|
-
const serialized = JSON.stringify(identity, null, 2) + "\n";
|
|
2500
|
-
if (existsSync3(path) && readFileSync4(path, "utf-8") === serialized)
|
|
2598
|
+
if (existsSync3(join5(targetRepo, PROJECT_IDENTITY_REL)))
|
|
2501
2599
|
return "skipped";
|
|
2502
|
-
|
|
2600
|
+
createProjectIdentityFile(targetRepo);
|
|
2503
2601
|
return "written";
|
|
2504
2602
|
}
|
|
2505
2603
|
function writeEngagementCredential(credential, engagementId, home) {
|
|
@@ -2562,10 +2660,10 @@ function migrateLegacyEnvLocal(targetRepo, stored) {
|
|
|
2562
2660
|
const substantive = kept.filter((line) => line.trim() !== "" && !line.trim().startsWith("# Halfcycle"));
|
|
2563
2661
|
try {
|
|
2564
2662
|
if (substantive.length === 0) {
|
|
2565
|
-
|
|
2663
|
+
rmSync2(path);
|
|
2566
2664
|
return { outcome: "removed", unbacked: [], missingForAdoption: [] };
|
|
2567
2665
|
}
|
|
2568
|
-
|
|
2666
|
+
writeFileSync4(path, kept.join("\n"), "utf-8");
|
|
2569
2667
|
return { outcome: "stripped", unbacked: [], missingForAdoption: [] };
|
|
2570
2668
|
} catch {
|
|
2571
2669
|
return { outcome: "failed", unbacked: [], missingForAdoption: [] };
|
|
@@ -2575,7 +2673,7 @@ async function install(options) {
|
|
|
2575
2673
|
const { targetRepo, engagementId, engagementType, credential, home, accountId } = options;
|
|
2576
2674
|
assertEngagementId(engagementId);
|
|
2577
2675
|
if (!existsSync3(targetRepo)) {
|
|
2578
|
-
throw new Error(`[
|
|
2676
|
+
throw new Error(`[halfcycle] Target repo does not exist: ${targetRepo}`);
|
|
2579
2677
|
}
|
|
2580
2678
|
const manifest = readPluginManifest();
|
|
2581
2679
|
const report = {
|
|
@@ -2589,17 +2687,6 @@ async function install(options) {
|
|
|
2589
2687
|
const draft = startManifest(readPreviousManifest(targetRepo), (path) => writeSet.files.has(path), (path) => writeSet.dirs.has(path));
|
|
2590
2688
|
const dirsBefore = new Set([...writeSet.dirs].filter((dir) => existsSync3(join5(targetRepo, dir))));
|
|
2591
2689
|
copyManifestCommands(manifest, targetRepo, report, draft);
|
|
2592
|
-
const capturedManifest = join5(targetRepo, CAPTURED_INDEX_REL);
|
|
2593
|
-
const srcManifest = join5(BUNDLE_ROOT, "scaffolding", "test", "fixtures", "captured", "manifest.json");
|
|
2594
|
-
if (!existsSync3(capturedManifest)) {
|
|
2595
|
-
writeAllowlisted(capturedManifest, targetRepo, readFileSync4(srcManifest, "utf-8"), report.writtenPaths);
|
|
2596
|
-
noteFile(draft, targetRepo, CAPTURED_INDEX_REL, "written");
|
|
2597
|
-
} else {
|
|
2598
|
-
report.skippedPaths.push(CAPTURED_INDEX_REL);
|
|
2599
|
-
if (readFileSync4(capturedManifest).equals(readFileSync4(srcManifest))) {
|
|
2600
|
-
noteFile(draft, targetRepo, CAPTURED_INDEX_REL, "skipped");
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
2690
|
const vendoredBinSrc = resolveVendoredBinary();
|
|
2604
2691
|
const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
|
|
2605
2692
|
const vendoredOutcome = recordOwned(report, vendoredBinDest, targetRepo, readFileSync4(vendoredBinSrc, "utf-8"), VENDORED_BIN_REL);
|
|
@@ -2607,21 +2694,20 @@ async function install(options) {
|
|
|
2607
2694
|
const settingsPath = join5(targetRepo, SETTINGS_REL);
|
|
2608
2695
|
const settingsPreexisted = existsSync3(settingsPath);
|
|
2609
2696
|
const generatedSettings = JSON.parse(generateSettingsJson());
|
|
2610
|
-
|
|
2697
|
+
let existingSettings;
|
|
2698
|
+
try {
|
|
2699
|
+
existingSettings = readExistingSettings(targetRepo) ?? {};
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
throw new SettingsParseRefused(settingsParseRefusal(targetRepo, err));
|
|
2702
|
+
}
|
|
2611
2703
|
recordSettingsMerge(draft, existingSettings, generatedSettings, settingsPreexisted);
|
|
2612
2704
|
const mergedSettings = mergeSettings(existingSettings, generatedSettings);
|
|
2613
2705
|
const mergedSettingsText = JSON.stringify(mergedSettings, null, 2) + "\n";
|
|
2614
|
-
|
|
2615
|
-
|
|
2706
|
+
mkdirSync4(dirname3(settingsPath), { recursive: true });
|
|
2707
|
+
writeFileSync4(settingsPath, mergedSettingsText, "utf-8");
|
|
2616
2708
|
(settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
2617
|
-
for (const [
|
|
2618
|
-
|
|
2619
|
-
["session-end-marker.sh", generateSessionEndMarker()],
|
|
2620
|
-
["user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
2621
|
-
["session-start-marker.sh", generateSessionStartMarker()]
|
|
2622
|
-
]) {
|
|
2623
|
-
const rel = `.claude/hooks/${name}`;
|
|
2624
|
-
const hookPath = join5(targetRepo, ".claude", "hooks", name);
|
|
2709
|
+
for (const [rel, content] of hookScriptsThisVersionWrites()) {
|
|
2710
|
+
const hookPath = join5(targetRepo, rel);
|
|
2625
2711
|
const outcome = recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
|
|
2626
2712
|
noteFile(draft, targetRepo, rel, outcome);
|
|
2627
2713
|
}
|
|
@@ -2680,7 +2766,7 @@ async function install(options) {
|
|
|
2680
2766
|
const gitignorePath = join5(targetRepo, GITIGNORE_REL);
|
|
2681
2767
|
const gitignoreBefore = existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null;
|
|
2682
2768
|
const gitignoreOutcome = reconcileGitignore(targetRepo);
|
|
2683
|
-
recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null);
|
|
2769
|
+
recordGitignoreReconcile(draft, gitignoreBefore, existsSync3(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : null, writeSet);
|
|
2684
2770
|
if (gitignoreOutcome === "failed") {
|
|
2685
2771
|
report.skippedPaths.push(".gitignore (write failed \u2014 see credential refusal)");
|
|
2686
2772
|
} else {
|
|
@@ -2689,7 +2775,7 @@ async function install(options) {
|
|
|
2689
2775
|
const gitattributesPath = join5(targetRepo, GITATTRIBUTES_REL);
|
|
2690
2776
|
const gitattributesBefore = readTextOrNull(gitattributesPath);
|
|
2691
2777
|
const gitattributesOutcome = reconcileGitattributes(targetRepo);
|
|
2692
|
-
recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath));
|
|
2778
|
+
recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath), writeSet);
|
|
2693
2779
|
if (gitattributesOutcome === "failed") {
|
|
2694
2780
|
report.skippedPaths.push(`${GITATTRIBUTES_REL} (write failed \u2014 a clone made with core.autocrlf=true may check the hook scripts out with CRLF line endings)`);
|
|
2695
2781
|
} else {
|
|
@@ -2812,16 +2898,22 @@ function planeRefusal(detail) {
|
|
|
2812
2898
|
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
2813
2899
|
};
|
|
2814
2900
|
}
|
|
2815
|
-
var CONTROL_ORIGIN_HINT = `This CLI talks to ${DEFAULT_CONTROL_ORIGIN}
|
|
2901
|
+
var CONTROL_ORIGIN_HINT = `This CLI talks to the Halfcycle service at ${DEFAULT_CONTROL_ORIGIN} \u2014 the same address for every user, and not something you set. Halfcycle also uses a second, different address for editor commands, which is not yours to set either. If something else answered here, it is most likely a network problem or an outage rather than something to configure.`;
|
|
2816
2902
|
async function createEngagement(baseUrl, name, credential, derivedName) {
|
|
2817
2903
|
return requestEngagementValues(
|
|
2904
|
+
"POST",
|
|
2818
2905
|
`${baseUrl.replace(/\/+$/, "")}/engagements`,
|
|
2819
2906
|
// A stated name wins over a guess, and the body carries at most one of them —
|
|
2820
2907
|
// the plane applies the same ordering, so the two cannot disagree about which
|
|
2821
2908
|
// one a row was named from.
|
|
2822
2909
|
name ? { name } : derivedName ? { derivedName } : {},
|
|
2823
2910
|
credential,
|
|
2824
|
-
{
|
|
2911
|
+
{
|
|
2912
|
+
action: "Create-engagement",
|
|
2913
|
+
nothingHappened: "No engagement was created",
|
|
2914
|
+
route: "POST /engagements",
|
|
2915
|
+
alsoNothingWritten: true
|
|
2916
|
+
}
|
|
2825
2917
|
);
|
|
2826
2918
|
}
|
|
2827
2919
|
function withDeclaredTelemetryKey(raw) {
|
|
@@ -2830,12 +2922,12 @@ function withDeclaredTelemetryKey(raw) {
|
|
|
2830
2922
|
const obj = raw;
|
|
2831
2923
|
return "controlTelemetryUrl" in obj ? obj : { ...obj, controlTelemetryUrl: "" };
|
|
2832
2924
|
}
|
|
2833
|
-
async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
2925
|
+
async function requestEngagementValues(method, url, requestBody, credential, shape) {
|
|
2834
2926
|
const bearer = credential?.trim();
|
|
2835
2927
|
let res;
|
|
2836
2928
|
try {
|
|
2837
2929
|
res = await fetch(url, {
|
|
2838
|
-
method
|
|
2930
|
+
method,
|
|
2839
2931
|
headers: {
|
|
2840
2932
|
"content-type": "application/json",
|
|
2841
2933
|
...bearer ? { authorization: `Bearer ${bearer}` } : {}
|
|
@@ -2843,36 +2935,36 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2843
2935
|
body: JSON.stringify(requestBody)
|
|
2844
2936
|
});
|
|
2845
2937
|
} catch (err) {
|
|
2846
|
-
throw new Error(`[
|
|
2938
|
+
throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
2847
2939
|
}
|
|
2848
2940
|
if (!res.ok) {
|
|
2849
2941
|
const detail = await res.text().catch(() => "");
|
|
2850
2942
|
const refusal = res.status === 404 ? void 0 : planeRefusal(detail);
|
|
2851
2943
|
if (refusal) {
|
|
2852
|
-
throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, refusal.reason, `[
|
|
2944
|
+
throw new CreateEngagementRefused(res.status, refusal.message, refusal.error, refusal.reason, `[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${refusal.message} ${shape.nothingHappened}.`);
|
|
2853
2945
|
}
|
|
2854
2946
|
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
|
|
2855
|
-
throw new Error(`[
|
|
2947
|
+
throw new Error(`[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
2856
2948
|
}
|
|
2857
2949
|
const candidate = withDeclaredTelemetryKey(await res.json().catch(() => null));
|
|
2858
2950
|
const declared = engagementCredentialResponseSchema.safeParse(candidate);
|
|
2859
2951
|
const body = declared.success ? declared.data : candidate;
|
|
2860
2952
|
if (!body || typeof body.engagementId !== "string" || typeof body.sessionToken !== "string") {
|
|
2861
|
-
throw new Error(`[
|
|
2953
|
+
throw new Error(`[halfcycle] ${shape.action} response from ${url} did not carry {engagementId, sessionToken}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
2862
2954
|
}
|
|
2863
2955
|
for (const [field, value] of [
|
|
2864
2956
|
["engagementId", body.engagementId],
|
|
2865
2957
|
["sessionToken", body.sessionToken]
|
|
2866
2958
|
]) {
|
|
2867
2959
|
if (value.trim() === "") {
|
|
2868
|
-
throw new Error(`[
|
|
2960
|
+
throw new Error(`[halfcycle] The Halfcycle service at ${url} answered with a blank ${field} (the field is present but empty). An install cannot proceed on it: it would be written into this engagement's credential store, read back as a configured value, and every guard evaluation would fail against it for the life of the engagement. ${shape.nothingHappened}${shape.alsoNothingWritten ? " and nothing was written" : ""}. ` + CONTROL_ORIGIN_HINT);
|
|
2869
2961
|
}
|
|
2870
2962
|
}
|
|
2871
2963
|
if (typeof body.mcpUrl !== "string" || body.mcpUrl.trim() === "") {
|
|
2872
|
-
throw new Error(`[
|
|
2964
|
+
throw new Error(`[halfcycle] The Halfcycle service at ${url} created an engagement but did not say where its MCP server lives (no mcpUrl on the response). Without that address the install would write an MCP registration pointing nowhere, so it does not continue. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
|
|
2873
2965
|
}
|
|
2874
2966
|
if (typeof body.guardUrl !== "string" || body.guardUrl.trim() === "") {
|
|
2875
|
-
throw new Error(`[
|
|
2967
|
+
throw new Error(`[halfcycle] The Halfcycle service at ${url} created an engagement but did not say where its guard service lives (no guardUrl on the response). Without that address the install would wire a guard hook that evaluates nothing, so it does not continue. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
|
|
2876
2968
|
}
|
|
2877
2969
|
const controlTelemetryUrl = typeof body.controlTelemetryUrl === "string" && body.controlTelemetryUrl.trim() !== "" ? body.controlTelemetryUrl.trim().replace(/\/+$/, "") : void 0;
|
|
2878
2970
|
if (!declared.success) {
|
|
@@ -2883,7 +2975,7 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2883
2975
|
}
|
|
2884
2976
|
return issue.path.length > 0 ? issue.path.map(String).join(".") : "(the body itself)";
|
|
2885
2977
|
}).join(", ");
|
|
2886
|
-
throw new Error(`[
|
|
2978
|
+
throw new Error(`[halfcycle] ${shape.action} response from ${url} is not the credential response this CLI is built to read (${fields}). ${shape.nothingHappened}. Either that service is a different version from this installer, or ${url} is not a Halfcycle service.`);
|
|
2887
2979
|
}
|
|
2888
2980
|
const consoleUrl = typeof body.consoleUrl === "string" && body.consoleUrl.trim() !== "" ? body.consoleUrl.trim() : void 0;
|
|
2889
2981
|
const methodVersion = typeof body.methodVersion === "string" && body.methodVersion.trim() !== "" ? body.methodVersion.trim() : void 0;
|
|
@@ -2899,8 +2991,8 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
2899
2991
|
}
|
|
2900
2992
|
|
|
2901
2993
|
// dist/setup/manifest.js
|
|
2902
|
-
import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as
|
|
2903
|
-
import { dirname as
|
|
2994
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2995
|
+
import { dirname as dirname4, join as join6, posix as posix2 } from "node:path";
|
|
2904
2996
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
2905
2997
|
var DECLARED_DIR_PREFIX = "docs";
|
|
2906
2998
|
var RESERVED_METHOD_PATH = "docs/method";
|
|
@@ -3011,8 +3103,8 @@ function applyFileRow(targetRepo, path, content) {
|
|
|
3011
3103
|
const current = readFileSync5(abs, "utf-8");
|
|
3012
3104
|
return current === content ? { path, outcome: "already-present", note: "already present and correct \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this file \u2014 left exactly as it was" };
|
|
3013
3105
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3106
|
+
mkdirSync5(dirname4(abs), { recursive: true });
|
|
3107
|
+
writeFileSync5(abs, content, "utf-8");
|
|
3016
3108
|
return { path, outcome: "created", note: "created" };
|
|
3017
3109
|
}
|
|
3018
3110
|
function applySectionRow(targetRepo, path, heading, body) {
|
|
@@ -3022,8 +3114,8 @@ function applySectionRow(targetRepo, path, heading, body) {
|
|
|
3022
3114
|
return symlinkEntry(path, link, false);
|
|
3023
3115
|
const section = renderSection(heading, body);
|
|
3024
3116
|
if (!existsSync4(abs)) {
|
|
3025
|
-
|
|
3026
|
-
|
|
3117
|
+
mkdirSync5(dirname4(abs), { recursive: true });
|
|
3118
|
+
writeFileSync5(abs, section, "utf-8");
|
|
3027
3119
|
return { path, outcome: "created", note: `created, with a "${heading}" section` };
|
|
3028
3120
|
}
|
|
3029
3121
|
const existing = readFileSync5(abs, "utf-8");
|
|
@@ -3035,7 +3127,7 @@ function applySectionRow(targetRepo, path, heading, body) {
|
|
|
3035
3127
|
};
|
|
3036
3128
|
}
|
|
3037
3129
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
3038
|
-
|
|
3130
|
+
writeFileSync5(abs, existing + separator + section, "utf-8");
|
|
3039
3131
|
return {
|
|
3040
3132
|
path,
|
|
3041
3133
|
outcome: "section-added",
|
|
@@ -3051,7 +3143,7 @@ function applyDirectoryRow(targetRepo, path) {
|
|
|
3051
3143
|
const kind = statSync(abs).isDirectory() ? "directory" : "file";
|
|
3052
3144
|
return kind === "directory" ? { path: `${path}/`, outcome: "already-present", note: "already there \u2014 nothing written" } : { path, outcome: "left-alone", note: "you already own this path \u2014 left exactly as it was" };
|
|
3053
3145
|
}
|
|
3054
|
-
|
|
3146
|
+
mkdirSync5(abs, { recursive: true });
|
|
3055
3147
|
return { path: `${path}/`, outcome: "created", note: "created" };
|
|
3056
3148
|
}
|
|
3057
3149
|
function applySetupWrites(targetRepo, rows) {
|
|
@@ -3734,7 +3826,7 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
3734
3826
|
}
|
|
3735
3827
|
|
|
3736
3828
|
// dist/build-record/write.js
|
|
3737
|
-
import { closeSync, existsSync as existsSync6, mkdirSync as
|
|
3829
|
+
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync3, writeSync } from "node:fs";
|
|
3738
3830
|
import { basename, join as join8, resolve, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
|
|
3739
3831
|
var BUILD_RECORD_DIR = join8("docs", "build-records");
|
|
3740
3832
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
@@ -3750,7 +3842,7 @@ function createExclusiveOnDisk(path, text) {
|
|
|
3750
3842
|
writeSync(fd, text);
|
|
3751
3843
|
} catch (err) {
|
|
3752
3844
|
closeSync(fd);
|
|
3753
|
-
|
|
3845
|
+
rmSync3(path, { force: true });
|
|
3754
3846
|
throw err;
|
|
3755
3847
|
}
|
|
3756
3848
|
closeSync(fd);
|
|
@@ -3814,7 +3906,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
3814
3906
|
}
|
|
3815
3907
|
const jsonText = serialiseServedRecord(rawRecord);
|
|
3816
3908
|
const mdText = renderBuildRecordV2Markdown(record2);
|
|
3817
|
-
|
|
3909
|
+
mkdirSync6(outDir, { recursive: true });
|
|
3818
3910
|
let createdJson = false;
|
|
3819
3911
|
try {
|
|
3820
3912
|
io.createExclusive(jsonPath, jsonText);
|
|
@@ -3823,7 +3915,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
3823
3915
|
return { kind: "created", jsonPath, mdPath };
|
|
3824
3916
|
} catch (err) {
|
|
3825
3917
|
if (createdJson)
|
|
3826
|
-
|
|
3918
|
+
rmSync3(jsonPath, { force: true });
|
|
3827
3919
|
if (!isEexist(err))
|
|
3828
3920
|
throw err;
|
|
3829
3921
|
}
|
|
@@ -3845,19 +3937,19 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
|
|
|
3845
3937
|
try {
|
|
3846
3938
|
replaceOne(io, jsonTmp, jsonPath, existingJson);
|
|
3847
3939
|
} catch {
|
|
3848
|
-
|
|
3940
|
+
rmSync3(jsonPath, { force: true });
|
|
3849
3941
|
}
|
|
3850
3942
|
throw err;
|
|
3851
3943
|
}
|
|
3852
3944
|
return { kind: "replaced", jsonPath, mdPath };
|
|
3853
3945
|
}
|
|
3854
3946
|
function replaceOne(io, tmp, finalPath, text) {
|
|
3855
|
-
|
|
3947
|
+
rmSync3(tmp, { force: true });
|
|
3856
3948
|
try {
|
|
3857
3949
|
io.createExclusive(tmp, text);
|
|
3858
3950
|
io.rename(tmp, finalPath);
|
|
3859
3951
|
} catch (err) {
|
|
3860
|
-
|
|
3952
|
+
rmSync3(tmp, { force: true });
|
|
3861
3953
|
throw err;
|
|
3862
3954
|
}
|
|
3863
3955
|
}
|