halfcycle 0.3.34 → 0.3.36
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 +13 -0
- package/bin/bin.bundle.mjs +274 -43
- package/dist/banner-facts.d.ts +6 -2
- package/dist/banner-facts.d.ts.map +1 -1
- package/dist/bin.js +282 -298
- package/dist/bin.js.map +3 -3
- package/dist/build-record/render-v2.d.ts.map +1 -1
- package/dist/claude-version-probe.d.ts +44 -0
- package/dist/claude-version-probe.d.ts.map +1 -0
- package/dist/confirm-identity.d.ts +7 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +31 -23
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +158 -233
- package/dist/index.js.map +1 -1
- package/dist/install-manifest.d.ts +32 -4
- package/dist/install-manifest.d.ts.map +1 -1
- package/dist/install.d.ts +52 -16
- package/dist/install.d.ts.map +1 -1
- package/dist/open-phase.d.ts +2 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/probe-mcp.d.ts +39 -7
- package/dist/probe-mcp.d.ts.map +1 -1
- package/dist/uninstall.d.ts +1 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/bin.js
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { existsSync as existsSync5, readFileSync as readFileSync10, realpathSync, statSync } from "node:fs";
|
|
5
|
+
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
import { join as join10 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/install.js
|
|
@@ -35,6 +35,8 @@ var ENGAGEMENTS_DIR_NAME = "engagements";
|
|
|
35
35
|
var ENGAGEMENT_ENV_FILENAME = "env";
|
|
36
36
|
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
37
37
|
var ACCOUNT_STORE_FILENAME = "account.json";
|
|
38
|
+
var BUNDLE_PIN_REL = ".halfcycle/bundle.json";
|
|
39
|
+
var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
38
40
|
var ENGAGEMENT_ID_PATTERN = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
|
|
39
41
|
var ENGAGEMENT_ID_SHAPE = new RegExp(ENGAGEMENT_ID_PATTERN);
|
|
40
42
|
function isEngagementId(value) {
|
|
@@ -1218,6 +1220,7 @@ var ciTokenExchangeResponseSchema = z10.object({
|
|
|
1218
1220
|
expiresAt: utcIso8601,
|
|
1219
1221
|
engagementId: z10.string()
|
|
1220
1222
|
}).strict();
|
|
1223
|
+
var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
|
|
1221
1224
|
|
|
1222
1225
|
// ../events/dist/phase-record.js
|
|
1223
1226
|
import { z as z11 } from "zod";
|
|
@@ -1322,7 +1325,6 @@ function forgetStoredCredential(serviceUrl, home) {
|
|
|
1322
1325
|
var ENGAGEMENTS_DIR = ENGAGEMENTS_DIR_NAME;
|
|
1323
1326
|
var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
|
|
1324
1327
|
var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
|
|
1325
|
-
var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
1326
1328
|
var InvalidEngagementIdError = class extends Error {
|
|
1327
1329
|
constructor(engagementId) {
|
|
1328
1330
|
const shown = JSON.stringify(engagementId.length > 80 ? `${engagementId.slice(0, 80)}\u2026` : engagementId);
|
|
@@ -1640,6 +1642,15 @@ function bool(value, field) {
|
|
|
1640
1642
|
throw new UnreadableManifestError(`"${field}" is not true or false`);
|
|
1641
1643
|
return value;
|
|
1642
1644
|
}
|
|
1645
|
+
function appendedTextRecord(value, field) {
|
|
1646
|
+
if (!isRecord(value))
|
|
1647
|
+
throw new UnreadableManifestError(`"${field}" is not an object`);
|
|
1648
|
+
onlyKeys(value, ["created", "appended"], `"${field}"`);
|
|
1649
|
+
return {
|
|
1650
|
+
created: bool(value["created"], `${field}.created`),
|
|
1651
|
+
appended: stringArray(value["appended"], `${field}.appended`)
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1643
1654
|
function onlyKeys(value, allowed, where) {
|
|
1644
1655
|
const extra = Object.keys(value).filter((k) => !allowed.includes(k));
|
|
1645
1656
|
if (extra.length > 0)
|
|
@@ -1654,7 +1665,7 @@ function parseManifest(text) {
|
|
|
1654
1665
|
}
|
|
1655
1666
|
if (!isRecord(raw))
|
|
1656
1667
|
throw new UnreadableManifestError("it is not a JSON object");
|
|
1657
|
-
onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "gitignore"], "the manifest");
|
|
1668
|
+
onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "gitignore", "gitattributes"], "the manifest");
|
|
1658
1669
|
if (raw["format"] !== INSTALL_MANIFEST_FORMAT) {
|
|
1659
1670
|
throw new UnreadableManifestError(`its format is not ${INSTALL_MANIFEST_FORMAT}`);
|
|
1660
1671
|
}
|
|
@@ -1715,14 +1726,10 @@ function parseManifest(text) {
|
|
|
1715
1726
|
...typeof m["entrySha256"] === "string" ? { entrySha256: m["entrySha256"] } : {}
|
|
1716
1727
|
};
|
|
1717
1728
|
}
|
|
1718
|
-
|
|
1719
|
-
if (!isRecord(g))
|
|
1729
|
+
if (raw["gitignore"] === void 0)
|
|
1720
1730
|
throw new UnreadableManifestError('"gitignore" is missing');
|
|
1721
|
-
|
|
1722
|
-
const
|
|
1723
|
-
created: bool(g["created"], "gitignore.created"),
|
|
1724
|
-
appended: stringArray(g["appended"], "gitignore.appended")
|
|
1725
|
-
};
|
|
1731
|
+
const gitignore = appendedTextRecord(raw["gitignore"], "gitignore");
|
|
1732
|
+
const gitattributes = raw["gitattributes"] === void 0 ? void 0 : appendedTextRecord(raw["gitattributes"], "gitattributes");
|
|
1726
1733
|
const paths = files.map((f) => f.path);
|
|
1727
1734
|
if (new Set(paths).size !== paths.length)
|
|
1728
1735
|
throw new UnreadableManifestError('"files" names a path twice');
|
|
@@ -1737,7 +1744,8 @@ function parseManifest(text) {
|
|
|
1737
1744
|
leftAlone,
|
|
1738
1745
|
settings,
|
|
1739
1746
|
...mcp !== void 0 ? { mcp } : {},
|
|
1740
|
-
gitignore
|
|
1747
|
+
gitignore,
|
|
1748
|
+
...gitattributes !== void 0 ? { gitattributes } : {}
|
|
1741
1749
|
};
|
|
1742
1750
|
}
|
|
1743
1751
|
function startManifest(previous, isMember, isMemberDir) {
|
|
@@ -1753,12 +1761,16 @@ function startManifest(previous, isMember, isMemberDir) {
|
|
|
1753
1761
|
createdDirs: new Set((previous?.createdDirs ?? []).filter(isMemberDir)),
|
|
1754
1762
|
settings: previous?.settings,
|
|
1755
1763
|
mcp: previous?.mcp,
|
|
1756
|
-
gitignore: previous?.gitignore
|
|
1764
|
+
gitignore: previous?.gitignore,
|
|
1765
|
+
gitattributes: previous?.gitattributes
|
|
1757
1766
|
};
|
|
1758
1767
|
}
|
|
1759
1768
|
function recordInstalledFile(draft, path, bytes) {
|
|
1760
1769
|
draft.files.set(path, { path, sha256: sha256Hex(bytes) });
|
|
1761
1770
|
}
|
|
1771
|
+
function recordRemovedFile(draft, path) {
|
|
1772
|
+
draft.files.delete(path);
|
|
1773
|
+
}
|
|
1762
1774
|
function recordPerMachineFile(draft, path) {
|
|
1763
1775
|
draft.files.set(path, { path, perMachine: true });
|
|
1764
1776
|
}
|
|
@@ -1801,8 +1813,15 @@ function recordMcp(draft, observed) {
|
|
|
1801
1813
|
};
|
|
1802
1814
|
}
|
|
1803
1815
|
function recordGitignore(draft, created, appended) {
|
|
1804
|
-
|
|
1805
|
-
|
|
1816
|
+
draft.gitignore = carryAppended(draft.gitignore, created, appended);
|
|
1817
|
+
}
|
|
1818
|
+
function recordGitattributes(draft, created, appended) {
|
|
1819
|
+
if (draft.gitattributes === void 0 && !created && appended.every((a) => a === ""))
|
|
1820
|
+
return;
|
|
1821
|
+
draft.gitattributes = carryAppended(draft.gitattributes, created, appended);
|
|
1822
|
+
}
|
|
1823
|
+
function carryAppended(prev, created, appended) {
|
|
1824
|
+
return {
|
|
1806
1825
|
created: prev?.created ?? created,
|
|
1807
1826
|
appended: [...prev?.appended ?? [], ...appended.filter((a) => a !== "")]
|
|
1808
1827
|
};
|
|
@@ -1826,7 +1845,8 @@ function finishManifest(draft) {
|
|
|
1826
1845
|
denyCreated: false
|
|
1827
1846
|
},
|
|
1828
1847
|
...draft.mcp !== void 0 ? { mcp: draft.mcp } : {},
|
|
1829
|
-
gitignore: draft.gitignore ?? { created: false, appended: [] }
|
|
1848
|
+
gitignore: draft.gitignore ?? { created: false, appended: [] },
|
|
1849
|
+
...draft.gitattributes !== void 0 ? { gitattributes: draft.gitattributes } : {}
|
|
1830
1850
|
};
|
|
1831
1851
|
}
|
|
1832
1852
|
function union(a, b) {
|
|
@@ -1977,11 +1997,12 @@ var WRITE_ALLOWLIST = [
|
|
|
1977
1997
|
".claude/hooks",
|
|
1978
1998
|
".claude/settings.json",
|
|
1979
1999
|
".halfcycle",
|
|
1980
|
-
|
|
2000
|
+
BUNDLE_PIN_REL,
|
|
1981
2001
|
".halfcycle/bin",
|
|
1982
2002
|
"docs/build-records",
|
|
1983
2003
|
"test/fixtures/captured",
|
|
1984
2004
|
".mcp.json",
|
|
2005
|
+
".gitattributes",
|
|
1985
2006
|
ZONE_B_DIR
|
|
1986
2007
|
];
|
|
1987
2008
|
function isAllowlisted(targetRelPath) {
|
|
@@ -1991,15 +2012,16 @@ function isAllowlisted(targetRelPath) {
|
|
|
1991
2012
|
return normalised === p || normalised.startsWith(p + "/");
|
|
1992
2013
|
});
|
|
1993
2014
|
}
|
|
1994
|
-
var BUNDLE_PIN_REL = ".halfcycle/bundle.json";
|
|
1995
2015
|
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
1996
2016
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
1997
2017
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
1998
2018
|
var GITIGNORE_REL = ".gitignore";
|
|
2019
|
+
var GITATTRIBUTES_REL = ".gitattributes";
|
|
1999
2020
|
function commandStubRel(name) {
|
|
2000
2021
|
return `.claude/commands/${name}.md`;
|
|
2001
2022
|
}
|
|
2002
|
-
var
|
|
2023
|
+
var RETIRED_MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
2024
|
+
var RETIRED_WRITE_SET_FILES = [RETIRED_MCP_HEADERS_HELPER_REL];
|
|
2003
2025
|
var RETIRED_DENY_PATTERNS = [];
|
|
2004
2026
|
function closedWriteSet() {
|
|
2005
2027
|
const files = /* @__PURE__ */ new Set([
|
|
@@ -2014,7 +2036,7 @@ function closedWriteSet() {
|
|
|
2014
2036
|
INSTALL_MANIFEST_REL,
|
|
2015
2037
|
...RETIRED_WRITE_SET_FILES
|
|
2016
2038
|
]);
|
|
2017
|
-
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, GITIGNORE_REL]);
|
|
2039
|
+
const merged = /* @__PURE__ */ new Set([SETTINGS_REL, MCP_REGISTRATION_REL, GITIGNORE_REL, GITATTRIBUTES_REL]);
|
|
2018
2040
|
const dirs = /* @__PURE__ */ new Set();
|
|
2019
2041
|
for (const path of [...files, ...merged]) {
|
|
2020
2042
|
for (let dir = posix.dirname(path); dir !== "."; dir = posix.dirname(dir))
|
|
@@ -2028,7 +2050,11 @@ function closedWriteSet() {
|
|
|
2028
2050
|
ownedDirs,
|
|
2029
2051
|
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
2030
2052
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
2031
|
-
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL])
|
|
2053
|
+
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL]),
|
|
2054
|
+
gitattributesLines: /* @__PURE__ */ new Set([
|
|
2055
|
+
GITATTRIBUTES_HEADER,
|
|
2056
|
+
...lfRules([...Object.keys(OWNED_GENERATED_HEADERS), ...RETIRED_WRITE_SET_FILES])
|
|
2057
|
+
])
|
|
2032
2058
|
};
|
|
2033
2059
|
}
|
|
2034
2060
|
function readPreviousManifest(targetRepo) {
|
|
@@ -2092,15 +2118,19 @@ function recordMcpAdoptionOnly(draft, existingText) {
|
|
|
2092
2118
|
}
|
|
2093
2119
|
function recordGitignoreReconcile(draft, before, after) {
|
|
2094
2120
|
const adopted = draft.gitignore === void 0 && before !== null ? adoptOlderGitignoreBlock(before) : "";
|
|
2095
|
-
|
|
2096
|
-
if (after !== null && after !== before) {
|
|
2097
|
-
if (before === null)
|
|
2098
|
-
appended = after;
|
|
2099
|
-
else if (after.startsWith(before))
|
|
2100
|
-
appended = after.slice(before.length);
|
|
2101
|
-
}
|
|
2121
|
+
const appended = appendedText(before, after);
|
|
2102
2122
|
recordGitignore(draft, before === null && after !== null, [adopted, appended]);
|
|
2103
2123
|
}
|
|
2124
|
+
function recordGitattributesReconcile(draft, before, after) {
|
|
2125
|
+
recordGitattributes(draft, before === null && after !== null, [appendedText(before, after)]);
|
|
2126
|
+
}
|
|
2127
|
+
function appendedText(before, after) {
|
|
2128
|
+
if (after === null || after === before)
|
|
2129
|
+
return "";
|
|
2130
|
+
if (before === null)
|
|
2131
|
+
return after;
|
|
2132
|
+
return after.startsWith(before) ? after.slice(before.length) : "";
|
|
2133
|
+
}
|
|
2104
2134
|
function assertManifestInWriteSet(manifest, writeSet) {
|
|
2105
2135
|
const strays = [
|
|
2106
2136
|
...manifest.files.map((f) => f.path).filter((p) => !writeSet.files.has(p)),
|
|
@@ -2115,7 +2145,8 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
|
2115
2145
|
const merged = [
|
|
2116
2146
|
SETTINGS_REL,
|
|
2117
2147
|
...manifest.mcp !== void 0 ? [MCP_REGISTRATION_REL] : [],
|
|
2118
|
-
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : []
|
|
2148
|
+
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : [],
|
|
2149
|
+
...manifest.gitattributes !== void 0 && (manifest.gitattributes.created || manifest.gitattributes.appended.length > 0) ? [GITATTRIBUTES_REL] : []
|
|
2119
2150
|
];
|
|
2120
2151
|
const collided = new Set(collidedPaths);
|
|
2121
2152
|
const dirVerdicts = /* @__PURE__ */ new Map();
|
|
@@ -2484,7 +2515,8 @@ set -e
|
|
|
2484
2515
|
|
|
2485
2516
|
# The project root is resolved from THIS SCRIPT'S OWN LOCATION, never from the
|
|
2486
2517
|
# session's cwd \u2014 a session started in a subdirectory must find the same root
|
|
2487
|
-
# .mcp.json the editor loaded, which is the bug
|
|
2518
|
+
# .mcp.json the editor loaded, which is the bug the editor's headers command
|
|
2519
|
+
# already paid for.
|
|
2488
2520
|
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
2489
2521
|
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
2490
2522
|
HC_REGISTRATION="$HC_PROJECT_ROOT/${MCP_REGISTRATION_REL}"
|
|
@@ -2531,7 +2563,9 @@ function isTrackedAndClean(targetRepo, relPath) {
|
|
|
2531
2563
|
return true;
|
|
2532
2564
|
}
|
|
2533
2565
|
function removeLegacyCiStanza(targetRepo) {
|
|
2534
|
-
|
|
2566
|
+
return removeRetiredGeneratedFile(targetRepo, ".halfcycle/ci-stanza.yml", 0, CI_STANZA_KNOWN_HEADER_HASHES);
|
|
2567
|
+
}
|
|
2568
|
+
function removeRetiredGeneratedFile(targetRepo, rel, headerLineIndex, knownHeaderHashes) {
|
|
2535
2569
|
const path = join5(targetRepo, rel);
|
|
2536
2570
|
if (!existsSync3(path))
|
|
2537
2571
|
return "absent";
|
|
@@ -2541,10 +2575,9 @@ function removeLegacyCiStanza(targetRepo) {
|
|
|
2541
2575
|
} catch {
|
|
2542
2576
|
return "failed";
|
|
2543
2577
|
}
|
|
2544
|
-
const headerLine = raw.split("\n")[
|
|
2578
|
+
const headerLine = raw.split("\n")[headerLineIndex] ?? "";
|
|
2545
2579
|
const digest = createHash2("sha256").update(headerLine, "utf-8").digest("hex");
|
|
2546
|
-
|
|
2547
|
-
if (!ours)
|
|
2580
|
+
if (!knownHeaderHashes.includes(digest))
|
|
2548
2581
|
return "kept-foreign";
|
|
2549
2582
|
if (!isTrackedAndClean(targetRepo, rel))
|
|
2550
2583
|
return "kept-uncommitted";
|
|
@@ -2557,203 +2590,33 @@ function removeLegacyCiStanza(targetRepo) {
|
|
|
2557
2590
|
}
|
|
2558
2591
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
2559
2592
|
var MCP_SERVER_KEY = "halfcycle";
|
|
2560
|
-
var
|
|
2561
|
-
|
|
2562
|
-
|
|
2593
|
+
var MCP_HEADERS_HELPER_COMMAND = `node -e "${[
|
|
2594
|
+
`const p=require('path'),f=require('fs'),c=require('child_process');`,
|
|
2595
|
+
`let d=process.cwd();`,
|
|
2596
|
+
`for(;;){`,
|
|
2597
|
+
`const b=p.join(d,'${VENDORED_BIN_REL}');`,
|
|
2598
|
+
`if(f.existsSync(b)){`,
|
|
2599
|
+
`const r=c.spawnSync(process.execPath,[b,'mcp-headers'],{stdio:'inherit'});`,
|
|
2600
|
+
`process.exit(typeof r.status==='number'?r.status:1)}`,
|
|
2601
|
+
`const u=p.dirname(d);`,
|
|
2602
|
+
`if(u===d)break;`,
|
|
2603
|
+
`d=u}`,
|
|
2604
|
+
`console.error('halfcycle: no ${VENDORED_BIN_REL} at or above '+process.cwd()+'; run npx halfcycle in the project folder');`,
|
|
2605
|
+
`process.exit(1)`
|
|
2606
|
+
].join("")}"`;
|
|
2607
|
+
var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
2608
|
+
"37a733ea588b3361b2bbfd86c44e0c95da30d7af6acffd1243ece83c811f0af2",
|
|
2609
|
+
"6732ec6d2d2bf7cb33017749a3d021b59af653dcd07c3811693b5a242d44b1a1"
|
|
2610
|
+
];
|
|
2611
|
+
function removeRetiredMcpHeadersHelper(targetRepo) {
|
|
2612
|
+
return removeRetiredGeneratedFile(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
|
|
2613
|
+
}
|
|
2563
2614
|
var OWNED_GENERATED_HEADERS = {
|
|
2564
2615
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
2565
2616
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
2566
2617
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
2567
|
-
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
2568
|
-
".halfcycle/mcp-headers.sh": GENERATED_MCP_HEADERS_HEADER
|
|
2618
|
+
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
2569
2619
|
};
|
|
2570
|
-
function generateMcpHeadersHelper() {
|
|
2571
|
-
return `#!/bin/sh
|
|
2572
|
-
${GENERATED_MCP_HEADERS_HEADER} the Halfcycle installer.
|
|
2573
|
-
#
|
|
2574
|
-
# Claude Code runs this at MCP connection time, from the session's cwd, and merges
|
|
2575
|
-
# its stdout into the connection headers for the "halfcycle" server in .mcp.json.
|
|
2576
|
-
#
|
|
2577
|
-
# It exists because \${VAR} expansion in .mcp.json reads the PROCESS environment
|
|
2578
|
-
# only: nothing loads a credential into a session, so a referenced
|
|
2579
|
-
# \${HALFCYCLE_TOKEN} expanded to nothing and the server was silently absent from
|
|
2580
|
-
# the tool list.
|
|
2581
|
-
#
|
|
2582
|
-
# This file is tracked and holds NO secret. NEITHER DOES ANY OTHER FILE IN THIS
|
|
2583
|
-
# REPOSITORY (T-09): the credential lives at machine level, outside every checkout,
|
|
2584
|
-
# and this script READS one line out of it (never sources \u2014 a credential store is
|
|
2585
|
-
# not code).
|
|
2586
|
-
set -eu
|
|
2587
|
-
|
|
2588
|
-
# The project root is resolved from THIS SCRIPT'S OWN LOCATION, never from the
|
|
2589
|
-
# caller's working directory. Claude Code runs the helper from the SESSION's cwd,
|
|
2590
|
-
# and a session started in a subdirectory still finds the root .mcp.json \u2014 so a
|
|
2591
|
-
# relative read here silently produced no credential for anyone who started the
|
|
2592
|
-
# session in any subdirectory of a monorepo.
|
|
2593
|
-
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
2594
|
-
PROJECT_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
|
2595
|
-
|
|
2596
|
-
${engagementResolutionShell()}
|
|
2597
|
-
|
|
2598
|
-
# Read one KEY's value out of the credential store into HC_VALUE, in the CALLER's
|
|
2599
|
-
# shell (call it as a statement, never inside $(...)). Last assignment wins,
|
|
2600
|
-
# matching the shell's own \`.\` semantics. An optional leading \`export \` is
|
|
2601
|
-
# tolerated because a hand-edited store may use one.
|
|
2602
|
-
halfcycle_store_value() {
|
|
2603
|
-
HC_VALUE=""
|
|
2604
|
-
hc_line=$(grep "^[[:space:]]*\\(export[[:space:]][[:space:]]*\\)\\{0,1\\}$2=" "$1" | tail -n 1)
|
|
2605
|
-
hc_v=\${hc_line#*"$2="}
|
|
2606
|
-
hc_v=$(printf '%s' "$hc_v" | tr -d '\\r')
|
|
2607
|
-
|
|
2608
|
-
# Strip one layer of matching quotes. The store is WRITTEN single-quoted (both
|
|
2609
|
-
# writers use the same shq: this installer and Studio's provider), because
|
|
2610
|
-
# guard-runner.sh SOURCES the same file and an unquoted value carrying a space
|
|
2611
|
-
# would execute its own remainder. This reader greps rather than sources, so it
|
|
2612
|
-
# has to undo the quoting itself \u2014 and it must land on the same value the
|
|
2613
|
-
# sourcing consumer gets, or one file has two answers.
|
|
2614
|
-
case $hc_v in
|
|
2615
|
-
'"'*'"') hc_v=\${hc_v#'"'}; hc_v=\${hc_v%'"'} ;;
|
|
2616
|
-
"'"*"'")
|
|
2617
|
-
hc_v=\${hc_v#"'"}; hc_v=\${hc_v%"'"}
|
|
2618
|
-
# \u2026and undo shq's embedded-quote escape, '\\'' -> '. The backslash is matched
|
|
2619
|
-
# through a BRACKET EXPRESSION on purpose. MEASURED, on GNU sed 4.9 (Linux,
|
|
2620
|
-
# the CI platform) and BSD sed (macOS), feeding each the script from a file so
|
|
2621
|
-
# no shell quoting is in the way:
|
|
2622
|
-
#
|
|
2623
|
-
# s/'[\\]''/'/g a'\\''b -> a'b both <- shipped
|
|
2624
|
-
# s/'\\''/'/g a'\\''b -> a'\\''b both <- matches NOTHING, exit 0
|
|
2625
|
-
#
|
|
2626
|
-
# A bare \\' in a BRE is undefined by POSIX, and the obvious pattern therefore
|
|
2627
|
-
# does not fail loudly \u2014 it silently substitutes nothing, on BOTH platforms,
|
|
2628
|
-
# and the token then travels with four stray characters in it. The bracket
|
|
2629
|
-
# expression makes the backslash literal by a construction POSIX does define.
|
|
2630
|
-
hc_v=$(printf '%s' "$hc_v" | sed "s/'[\\]''/'/g") ;;
|
|
2631
|
-
esac
|
|
2632
|
-
HC_VALUE=$hc_v
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
# The scheme and host[:port] of a URL, lowercased \u2014 or NOTHING when the URL holds
|
|
2636
|
-
# a space or a control character anywhere. That refusal is the load-bearing part:
|
|
2637
|
-
# a URL parser drops tabs and newlines before it reads the host, so
|
|
2638
|
-
# "https://ours<newline>@elsewhere/" is a request to "elsewhere", while a
|
|
2639
|
-
# line-by-line reading here would see only "https://ours". The host ends at the
|
|
2640
|
-
# first / ? # or backslash, which is where a URL parser ends it for http(s).
|
|
2641
|
-
halfcycle_origin() {
|
|
2642
|
-
hc_u=$1
|
|
2643
|
-
if [ "$(printf '%s' "$hc_u" | tr -d '[:cntrl:][:space:]')" != "$hc_u" ]; then
|
|
2644
|
-
return 0
|
|
2645
|
-
fi
|
|
2646
|
-
printf '%s\\n' "$hc_u" | sed -n 's|^\\([A-Za-z][A-Za-z0-9+.-]*://[^/?#\\\\]*\\).*$|\\1|p' | tr '[:upper:]' '[:lower:]'
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
# HALFCYCLE_TOKEN in the environment WINS, and it is the CI arm: a job with no
|
|
2650
|
-
# browser and no per-user store exports the credential, and a stale store on a
|
|
2651
|
-
# long-lived runner must not silently win over it. Same order, and the same reason,
|
|
2652
|
-
# as the CLI's own resolver (\`resolve-credential.ts\`). The server it may be sent
|
|
2653
|
-
# to is NOT taken from the environment: it is read from this machine's store in
|
|
2654
|
-
# both arms, so a token supplied this way still goes only where this machine was
|
|
2655
|
-
# set up to send it.
|
|
2656
|
-
# The pin names an id that is not a project id. Re-running the installer against
|
|
2657
|
-
# that pin refuses it too, so the one remedy that works is a fresh pin.
|
|
2658
|
-
halfcycle_bad_id_message() {
|
|
2659
|
-
echo "halfcycle: $PROJECT_ROOT/.halfcycle/bundle.json names an engagement id that is not a Halfcycle project id," >&2
|
|
2660
|
-
echo "halfcycle: so no credential was read. Delete that file and run \\"npx halfcycle\\" again: this repository" >&2
|
|
2661
|
-
echo "halfcycle: is then set up as a new Halfcycle project." >&2
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
TOKEN=\${HALFCYCLE_TOKEN:-}
|
|
2665
|
-
MCP_URL=""
|
|
2666
|
-
|
|
2667
|
-
if [ -n "$TOKEN" ]; then
|
|
2668
|
-
if halfcycle_env_file "$PROJECT_ROOT"; then
|
|
2669
|
-
halfcycle_store_value "$HALFCYCLE_ENV_FILE" HALFCYCLE_MCP_URL
|
|
2670
|
-
MCP_URL=$HC_VALUE
|
|
2671
|
-
fi
|
|
2672
|
-
else
|
|
2673
|
-
if ! halfcycle_env_file "$PROJECT_ROOT"; then
|
|
2674
|
-
case $HALFCYCLE_ENV_PROBLEM in
|
|
2675
|
-
no-pin)
|
|
2676
|
-
echo "halfcycle: no .halfcycle/bundle.json in $PROJECT_ROOT, so this repository has no Halfcycle engagement." >&2
|
|
2677
|
-
echo "halfcycle: run \\"npx halfcycle\\" here to install one." >&2 ;;
|
|
2678
|
-
no-id)
|
|
2679
|
-
echo "halfcycle: $PROJECT_ROOT/.halfcycle/bundle.json names no engagement id." >&2
|
|
2680
|
-
echo "halfcycle: run \\"npx halfcycle\\" here to rewrite it." >&2 ;;
|
|
2681
|
-
bad-id)
|
|
2682
|
-
halfcycle_bad_id_message ;;
|
|
2683
|
-
no-home)
|
|
2684
|
-
echo "halfcycle: HOME is not set, so the credential store cannot be located." >&2 ;;
|
|
2685
|
-
*)
|
|
2686
|
-
# OUTCOME B (T-02b, solo-project-ownership) is a \`no-credential\` PROBLEM
|
|
2687
|
-
# with a REASON attached, checked here rather than as a fifth case value
|
|
2688
|
-
# \u2014 see \`engagementResolutionShell\`'s own docblock for why. Re-running
|
|
2689
|
-
# this installer can never obtain a credential for this reason, so it is
|
|
2690
|
-
# the one sentence in this file that must not say to.
|
|
2691
|
-
if [ "$HALFCYCLE_ENV_NOT_THIS_ACCOUNT" = "1" ]; then
|
|
2692
|
-
echo "halfcycle: this project belongs to another Halfcycle account, so this machine has no" >&2
|
|
2693
|
-
echo "halfcycle: credential for it and the Halfcycle tools are not available here. Nothing is" >&2
|
|
2694
|
-
echo "halfcycle: broken \u2014 ask the project's owner for view access, or sign in as the account" >&2
|
|
2695
|
-
echo "halfcycle: that owns it and run \\"npx halfcycle\\" again." >&2
|
|
2696
|
-
else
|
|
2697
|
-
echo "halfcycle: this machine holds no credential for engagement $HALFCYCLE_ENV_ENGAGEMENT." >&2
|
|
2698
|
-
echo "halfcycle: run \\"npx halfcycle\\" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to $HOME/.halfcycle, outside this repository." >&2
|
|
2699
|
-
fi ;;
|
|
2700
|
-
esac
|
|
2701
|
-
exit 1
|
|
2702
|
-
fi
|
|
2703
|
-
ENV_FILE="$HALFCYCLE_ENV_FILE"
|
|
2704
|
-
|
|
2705
|
-
halfcycle_store_value "$ENV_FILE" HALFCYCLE_TOKEN
|
|
2706
|
-
TOKEN=$HC_VALUE
|
|
2707
|
-
if [ -z "$TOKEN" ]; then
|
|
2708
|
-
echo "halfcycle: HALFCYCLE_TOKEN is absent or empty in $ENV_FILE." >&2
|
|
2709
|
-
echo "halfcycle: run \\"npx halfcycle\\" in this repository to rewrite this machine's credential." >&2
|
|
2710
|
-
exit 1
|
|
2711
|
-
fi
|
|
2712
|
-
halfcycle_store_value "$ENV_FILE" HALFCYCLE_MCP_URL
|
|
2713
|
-
MCP_URL=$HC_VALUE
|
|
2714
|
-
fi
|
|
2715
|
-
|
|
2716
|
-
# THE TOKEN GOES ONLY TO THIS MACHINE'S HALFCYCLE SERVER. .mcp.json is a tracked
|
|
2717
|
-
# file: anyone who can land a commit can change the server's url, and this script
|
|
2718
|
-
# would then hand the credential to whatever it names. Claude Code tells the helper
|
|
2719
|
-
# which url it is about to connect to (CLAUDE_CODE_MCP_SERVER_URL); the server this
|
|
2720
|
-
# machine was set up against is recorded OUTSIDE the repository, beside the
|
|
2721
|
-
# credential. The two must share scheme, host and port, or nothing is printed.
|
|
2722
|
-
# No url at all is a refusal too: sending the credential without knowing where it
|
|
2723
|
-
# is going is the thing this check exists to stop.
|
|
2724
|
-
REQUESTED=\${CLAUDE_CODE_MCP_SERVER_URL:-}
|
|
2725
|
-
if [ -z "$REQUESTED" ]; then
|
|
2726
|
-
echo "halfcycle: Claude Code did not say which server it is connecting to, so the Halfcycle credential was not sent." >&2
|
|
2727
|
-
echo "halfcycle: update Claude Code, then reconnect." >&2
|
|
2728
|
-
exit 1
|
|
2729
|
-
fi
|
|
2730
|
-
if [ -z "$MCP_URL" ] && [ "\${HALFCYCLE_ENV_PROBLEM:-}" = "bad-id" ]; then
|
|
2731
|
-
halfcycle_bad_id_message
|
|
2732
|
-
exit 1
|
|
2733
|
-
fi
|
|
2734
|
-
if [ -z "$MCP_URL" ]; then
|
|
2735
|
-
echo "halfcycle: this machine has no Halfcycle server address recorded for this repository, so the credential was not sent." >&2
|
|
2736
|
-
echo "halfcycle: run \\"npx halfcycle\\" in this repository on this machine to record it." >&2
|
|
2737
|
-
exit 1
|
|
2738
|
-
fi
|
|
2739
|
-
WANT=$(halfcycle_origin "$MCP_URL")
|
|
2740
|
-
GOT=$(halfcycle_origin "$REQUESTED")
|
|
2741
|
-
if [ -z "$WANT" ] || [ "$GOT" != "$WANT" ]; then
|
|
2742
|
-
SHOWN=$(printf '%s' "$REQUESTED" | tr -cd '[:graph:]' | cut -c1-200)
|
|
2743
|
-
echo "halfcycle: .mcp.json asks for this repository's Halfcycle credential to be sent to $SHOWN," >&2
|
|
2744
|
-
echo "halfcycle: which is not this machine's Halfcycle server (\${WANT:-none recorded}). The credential was NOT sent." >&2
|
|
2745
|
-
echo "halfcycle: if nobody meant to change .mcp.json, treat that change as suspect; \\"npx halfcycle\\" restores the entry." >&2
|
|
2746
|
-
exit 1
|
|
2747
|
-
fi
|
|
2748
|
-
|
|
2749
|
-
# JSON-escape: backslash first, then double quote. A token carrying either would
|
|
2750
|
-
# otherwise emit invalid JSON, and invalid JSON is dropped SILENTLY \u2014 the
|
|
2751
|
-
# connection succeeds carrying no Authorization header at all.
|
|
2752
|
-
ESCAPED=$(printf '%s' "$TOKEN" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g')
|
|
2753
|
-
|
|
2754
|
-
printf '{"Authorization":"Bearer %s"}\\n' "$ESCAPED"
|
|
2755
|
-
`;
|
|
2756
|
-
}
|
|
2757
2620
|
function generateMcpRegistration(existing, mcpOrigin) {
|
|
2758
2621
|
let base = { mcpServers: {} };
|
|
2759
2622
|
if (existing !== null) {
|
|
@@ -2820,6 +2683,42 @@ ${header}
|
|
|
2820
2683
|
return "failed";
|
|
2821
2684
|
}
|
|
2822
2685
|
}
|
|
2686
|
+
var GITATTRIBUTES_HEADER = "# Halfcycle \u2014 generated scripts keep LF line endings";
|
|
2687
|
+
function readTextOrNull(path) {
|
|
2688
|
+
try {
|
|
2689
|
+
return readFileSync5(path, "utf-8");
|
|
2690
|
+
} catch {
|
|
2691
|
+
return null;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
function lfRules(paths) {
|
|
2695
|
+
return [...paths].filter((path) => path.endsWith(".sh")).map((path) => `${path} text eol=lf`);
|
|
2696
|
+
}
|
|
2697
|
+
var REQUIRED_GITATTRIBUTES_RULES = lfRules(Object.keys(OWNED_GENERATED_HEADERS));
|
|
2698
|
+
function reconcileGitattributes(targetRepo) {
|
|
2699
|
+
const path = join5(targetRepo, GITATTRIBUTES_REL);
|
|
2700
|
+
const rules = REQUIRED_GITATTRIBUTES_RULES;
|
|
2701
|
+
const routed = [];
|
|
2702
|
+
try {
|
|
2703
|
+
if (lstatOrNull(path)?.isSymbolicLink() === true)
|
|
2704
|
+
return "failed";
|
|
2705
|
+
if (!existsSync3(path)) {
|
|
2706
|
+
writeAllowlisted(path, targetRepo, [GITATTRIBUTES_HEADER, ...rules].join("\n") + "\n", routed);
|
|
2707
|
+
return "written";
|
|
2708
|
+
}
|
|
2709
|
+
const existing = readFileSync5(path, "utf-8");
|
|
2710
|
+
const present = new Set(existing.split("\n").map((l) => l.trim()));
|
|
2711
|
+
const toAdd = rules.filter((rule) => !present.has(rule));
|
|
2712
|
+
if (toAdd.length === 0)
|
|
2713
|
+
return "skipped";
|
|
2714
|
+
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
2715
|
+
const suffix = separator + [GITATTRIBUTES_HEADER, ...toAdd].join("\n") + "\n";
|
|
2716
|
+
writeAllowlisted(path, targetRepo, existing + suffix, routed);
|
|
2717
|
+
return "written";
|
|
2718
|
+
} catch {
|
|
2719
|
+
return "failed";
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2823
2722
|
function previousPinForEngagement(targetRepoRoot, engagementId) {
|
|
2824
2723
|
try {
|
|
2825
2724
|
const existing = readBundlePin(targetRepoRoot);
|
|
@@ -3070,9 +2969,6 @@ async function install(options) {
|
|
|
3070
2969
|
break;
|
|
3071
2970
|
}
|
|
3072
2971
|
if (credential) {
|
|
3073
|
-
const helperPath = join5(targetRepo, MCP_HEADERS_HELPER_REL);
|
|
3074
|
-
const helperOutcome = recordOwnedGenerated(report, helperPath, targetRepo, generateMcpHeadersHelper(), OWNED_GENERATED_HEADERS[MCP_HEADERS_HELPER_REL], MCP_HEADERS_HELPER_REL);
|
|
3075
|
-
noteFile(draft, targetRepo, MCP_HEADERS_HELPER_REL, helperOutcome);
|
|
3076
2972
|
const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
|
|
3077
2973
|
const existingMcp = existsSync3(mcpPath) ? readFileSync5(mcpPath, "utf-8") : null;
|
|
3078
2974
|
const mcpContent = generateMcpRegistration(existingMcp, credential.mcpUrl);
|
|
@@ -3084,6 +2980,24 @@ async function install(options) {
|
|
|
3084
2980
|
} else {
|
|
3085
2981
|
report.skippedPaths.push(MCP_REGISTRATION_REL);
|
|
3086
2982
|
}
|
|
2983
|
+
const helperRel = RETIRED_MCP_HEADERS_HELPER_REL;
|
|
2984
|
+
switch (removeRetiredMcpHeadersHelper(targetRepo)) {
|
|
2985
|
+
case "removed":
|
|
2986
|
+
recordRemovedFile(draft, helperRel);
|
|
2987
|
+
report.writtenPaths.push(`${helperRel} (REMOVED \u2014 Halfcycle no longer uses this file)`);
|
|
2988
|
+
break;
|
|
2989
|
+
case "kept-foreign":
|
|
2990
|
+
report.skippedPaths.push(`${helperRel} (KEPT \u2014 Halfcycle no longer uses this file, and it is not one Halfcycle generated byte for byte, so it was left alone; delete it if nothing of yours needs it)`);
|
|
2991
|
+
break;
|
|
2992
|
+
case "kept-uncommitted":
|
|
2993
|
+
report.skippedPaths.push(`${helperRel} (KEPT \u2014 Halfcycle no longer uses this file, but it is not a clean, committed copy in this repository, so it was left alone rather than risk losing an edit; delete it when you are ready)`);
|
|
2994
|
+
break;
|
|
2995
|
+
case "failed":
|
|
2996
|
+
report.skippedPaths.push(`${helperRel} (could not be removed \u2014 Halfcycle no longer uses this file, DELETE IT BY HAND)`);
|
|
2997
|
+
break;
|
|
2998
|
+
case "absent":
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3087
3001
|
} else {
|
|
3088
3002
|
report.skippedPaths.push(`${MCP_REGISTRATION_REL} (no MCP origin \u2014 no credential supplied)`);
|
|
3089
3003
|
const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
|
|
@@ -3099,6 +3013,15 @@ async function install(options) {
|
|
|
3099
3013
|
} else {
|
|
3100
3014
|
record(report, gitignoreOutcome, ".gitignore");
|
|
3101
3015
|
}
|
|
3016
|
+
const gitattributesPath = join5(targetRepo, GITATTRIBUTES_REL);
|
|
3017
|
+
const gitattributesBefore = readTextOrNull(gitattributesPath);
|
|
3018
|
+
const gitattributesOutcome = reconcileGitattributes(targetRepo);
|
|
3019
|
+
recordGitattributesReconcile(draft, gitattributesBefore, readTextOrNull(gitattributesPath));
|
|
3020
|
+
if (gitattributesOutcome === "failed") {
|
|
3021
|
+
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)`);
|
|
3022
|
+
} else {
|
|
3023
|
+
record(report, gitattributesOutcome, GITATTRIBUTES_REL);
|
|
3024
|
+
}
|
|
3102
3025
|
const identityOutcome = writeProjectIdentity(targetRepo);
|
|
3103
3026
|
record(report, identityOutcome, PROJECT_IDENTITY_REL);
|
|
3104
3027
|
noteFile(draft, targetRepo, PROJECT_IDENTITY_REL, identityOutcome);
|
|
@@ -4214,7 +4137,7 @@ function armScript(credential, act, label, plainLabel, home) {
|
|
|
4214
4137
|
}
|
|
4215
4138
|
function identitySwitchRemedy(home, interactive) {
|
|
4216
4139
|
const setOrRemove = `set HALFCYCLE_TOKEN for that account, or remove ${accountStorePath(home)}`;
|
|
4217
|
-
return interactive ? `To act as a different account: run this again and
|
|
4140
|
+
return interactive ? `To act as a different account: run this again and ${SWITCH_ACCOUNT_ANSWER}, ${setOrRemove} to be asked for a sign-in next time.` : `To act as a different account: ${setOrRemove} and run this again with a terminal to sign in.`;
|
|
4218
4141
|
}
|
|
4219
4142
|
async function confirmActingIdentity(serviceUrl, credential, act, deps = {}) {
|
|
4220
4143
|
const write = deps.write ?? ((text) => process.stdout.write(text));
|
|
@@ -4384,7 +4307,7 @@ async function mintBoardEnterCode(baseUrl, sessionToken) {
|
|
|
4384
4307
|
|
|
4385
4308
|
// dist/probe-mcp.js
|
|
4386
4309
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
4387
|
-
async function probeMcpOrigin(mcpUrl) {
|
|
4310
|
+
async function probeMcpOrigin(mcpUrl, token) {
|
|
4388
4311
|
const url = mcpEndpointUrl(mcpUrl);
|
|
4389
4312
|
const controller = new AbortController();
|
|
4390
4313
|
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
@@ -4392,32 +4315,71 @@ async function probeMcpOrigin(mcpUrl) {
|
|
|
4392
4315
|
try {
|
|
4393
4316
|
res = await fetch(url, {
|
|
4394
4317
|
method: "POST",
|
|
4395
|
-
headers: { "content-type": "application/json" },
|
|
4318
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
4396
4319
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }),
|
|
4320
|
+
redirect: "manual",
|
|
4397
4321
|
signal: controller.signal
|
|
4398
4322
|
});
|
|
4399
4323
|
} catch (err) {
|
|
4400
4324
|
return {
|
|
4401
4325
|
reached: false,
|
|
4326
|
+
credentialRefused: false,
|
|
4402
4327
|
problem: controller.signal.aborted ? `it did not answer within ${PROBE_TIMEOUT_MS / 1e3} seconds` : `it could not be reached (${err instanceof Error ? err.message : String(err)})`
|
|
4403
4328
|
};
|
|
4404
4329
|
} finally {
|
|
4405
4330
|
clearTimeout(timer);
|
|
4406
4331
|
}
|
|
4407
4332
|
if (res.status === 404) {
|
|
4408
|
-
return { reached: false, problem: "that address answered, but it serves no /mcp endpoint" };
|
|
4333
|
+
return { reached: false, credentialRefused: false, problem: "that address answered, but it serves no /mcp endpoint" };
|
|
4334
|
+
}
|
|
4335
|
+
if (res.status === 401) {
|
|
4336
|
+
return {
|
|
4337
|
+
reached: false,
|
|
4338
|
+
credentialRefused: true,
|
|
4339
|
+
problem: "it answered, but refused this machine's Halfcycle credential"
|
|
4340
|
+
};
|
|
4341
|
+
}
|
|
4342
|
+
if (res.status >= 300 && res.status < 400) {
|
|
4343
|
+
return {
|
|
4344
|
+
reached: false,
|
|
4345
|
+
credentialRefused: false,
|
|
4346
|
+
problem: `it redirected elsewhere (${res.status}), which is not followed, so the credential was not sent on`
|
|
4347
|
+
};
|
|
4409
4348
|
}
|
|
4410
4349
|
if (!res.ok) {
|
|
4411
|
-
return { reached: false, problem: `it answered ${res.status}` };
|
|
4350
|
+
return { reached: false, credentialRefused: false, problem: `it answered ${res.status}` };
|
|
4412
4351
|
}
|
|
4413
4352
|
const body = await res.json().catch(() => null);
|
|
4414
4353
|
const name = body?.result?.serverInfo?.name;
|
|
4415
4354
|
if (typeof name !== "string" || name === "") {
|
|
4416
|
-
return { reached: false, problem: "it answered, but not as an MCP server" };
|
|
4355
|
+
return { reached: false, credentialRefused: false, problem: "it answered, but not as an MCP server" };
|
|
4417
4356
|
}
|
|
4418
4357
|
return { reached: true, serverName: name };
|
|
4419
4358
|
}
|
|
4420
4359
|
|
|
4360
|
+
// dist/claude-version-probe.js
|
|
4361
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4362
|
+
function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
4363
|
+
const platform4 = deps.platform ?? process.platform;
|
|
4364
|
+
const exec = deps.exec ?? execFileSync3;
|
|
4365
|
+
let raw;
|
|
4366
|
+
try {
|
|
4367
|
+
raw = platform4 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", '"claude --version"'], {
|
|
4368
|
+
encoding: "utf-8",
|
|
4369
|
+
timeout: timeoutMs,
|
|
4370
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4371
|
+
windowsVerbatimArguments: true
|
|
4372
|
+
}) : exec("claude", ["--version"], {
|
|
4373
|
+
encoding: "utf-8",
|
|
4374
|
+
timeout: timeoutMs,
|
|
4375
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4376
|
+
});
|
|
4377
|
+
} catch {
|
|
4378
|
+
return void 0;
|
|
4379
|
+
}
|
|
4380
|
+
return /(\d+\.\d+\.\d+)/.exec(raw)?.[1];
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4421
4383
|
// dist/build-record/write.js
|
|
4422
4384
|
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeSync } from "node:fs";
|
|
4423
4385
|
import { basename as basename2, join as join6, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
|
|
@@ -4516,7 +4478,7 @@ function renderCouldNotRun(entries, p) {
|
|
|
4516
4478
|
}
|
|
4517
4479
|
return lines.join("\n");
|
|
4518
4480
|
}
|
|
4519
|
-
function headline(observed, attested) {
|
|
4481
|
+
function headline(observed, attested, checks) {
|
|
4520
4482
|
const met = attested.decision === "done-when-met";
|
|
4521
4483
|
const problemsReported = attested.walk !== null && attested.walk.verdict === "defects";
|
|
4522
4484
|
switch (observed.crossed.kind) {
|
|
@@ -4524,7 +4486,10 @@ function headline(observed, attested) {
|
|
|
4524
4486
|
if (!met) {
|
|
4525
4487
|
return "**Closed by override.** Halfcycle saw nothing outstanding, and the person closing the phase chose to override rather than state its finish conditions were met.";
|
|
4526
4488
|
}
|
|
4527
|
-
|
|
4489
|
+
if (problemsReported) {
|
|
4490
|
+
return "**Closed with problems reported.** Halfcycle saw nothing outstanding, and the person closing the phase reported problems found in its acceptance walk.";
|
|
4491
|
+
}
|
|
4492
|
+
return checks === "not-observed" ? "**Closed with nothing outstanding, but no checks ran.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met, but Halfcycle has no record of any check running during this phase, so this is not a clean result." : "**Closed cleanly.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met.";
|
|
4528
4493
|
case "unfinished-steps":
|
|
4529
4494
|
case "clean-over-findings":
|
|
4530
4495
|
return met ? "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase stated its finish conditions were met anyway." : "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase chose to override it.";
|
|
@@ -4612,10 +4577,10 @@ function renderStated(attested, p) {
|
|
|
4612
4577
|
}
|
|
4613
4578
|
return lines.join("\n");
|
|
4614
4579
|
}
|
|
4615
|
-
function renderClose(heading, observed, attested, p) {
|
|
4580
|
+
function renderClose(heading, observed, attested, checks, p) {
|
|
4616
4581
|
return [
|
|
4617
4582
|
heading,
|
|
4618
|
-
headline(observed, attested),
|
|
4583
|
+
headline(observed, attested, checks),
|
|
4619
4584
|
OBSERVED_LABEL,
|
|
4620
4585
|
[
|
|
4621
4586
|
`- Closed at: ${closedAtText(observed.closedAt, p)}`,
|
|
@@ -4650,12 +4615,12 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
4650
4615
|
].join("\n"),
|
|
4651
4616
|
total === 1 ? "This phase was closed once." : `This phase was closed ${String(total)} times. The latest close is described first; each earlier close follows, as it was recorded when it was made.`,
|
|
4652
4617
|
...renderChecks(record2.observed.checks, p),
|
|
4653
|
-
...renderClose(total === 1 ? "## The close" : `## The latest close (close ${String(latest.sequence)} of ${String(total)})`, latest, attestedBySequence.get(latest.sequence), p)
|
|
4618
|
+
...renderClose(total === 1 ? "## The close" : `## The latest close (close ${String(latest.sequence)} of ${String(total)})`, latest, attestedBySequence.get(latest.sequence), record2.observed.checks.state, p)
|
|
4654
4619
|
];
|
|
4655
4620
|
if (total > 1) {
|
|
4656
4621
|
body.push("## Earlier closes");
|
|
4657
4622
|
for (const close of closes.slice(0, -1)) {
|
|
4658
|
-
body.push(...renderClose(`### Close ${String(close.sequence)} of ${String(total)}`, close, attestedBySequence.get(close.sequence), p));
|
|
4623
|
+
body.push(...renderClose(`### Close ${String(close.sequence)} of ${String(total)}`, close, attestedBySequence.get(close.sequence), record2.observed.checks.state, p));
|
|
4659
4624
|
}
|
|
4660
4625
|
}
|
|
4661
4626
|
const header = [`${TITLE_PREFIX}${phase}`, HEADER_WHAT, HEADER_REGISTERS, HEADER_CHECKS, HEADER_KEPT];
|
|
@@ -4990,7 +4955,7 @@ async function openPhase(credential, phase, entry, home) {
|
|
|
4990
4955
|
}
|
|
4991
4956
|
|
|
4992
4957
|
// dist/close-phase.js
|
|
4993
|
-
import { execFileSync as
|
|
4958
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4994
4959
|
var NO_STATEMENTS = { thesis: null, outcome: null, deviations: [] };
|
|
4995
4960
|
function readCloseCommit(repoRoot) {
|
|
4996
4961
|
if (repoRoot === void 0) {
|
|
@@ -5001,7 +4966,7 @@ function readCloseCommit(repoRoot) {
|
|
|
5001
4966
|
}
|
|
5002
4967
|
let out;
|
|
5003
4968
|
try {
|
|
5004
|
-
out =
|
|
4969
|
+
out = execFileSync4("git", ["-C", repoRoot, "rev-parse", "HEAD"], {
|
|
5005
4970
|
encoding: "utf-8",
|
|
5006
4971
|
env: gitEnvForNamedRepo(),
|
|
5007
4972
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5296,7 +5261,7 @@ function validate(options) {
|
|
|
5296
5261
|
if (lexists(pinPath)) {
|
|
5297
5262
|
try {
|
|
5298
5263
|
const pin = JSON.parse(readFileSync8(pinPath, "utf-8"));
|
|
5299
|
-
const id = isPlainObject2(pin) ? pin[
|
|
5264
|
+
const id = isPlainObject2(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
|
|
5300
5265
|
engagementId = typeof id === "string" && id !== "" ? id : void 0;
|
|
5301
5266
|
} catch {
|
|
5302
5267
|
pinReadable = false;
|
|
@@ -5438,17 +5403,33 @@ function unmergeMcp(value, rec) {
|
|
|
5438
5403
|
}
|
|
5439
5404
|
return { changed, keptEntry };
|
|
5440
5405
|
}
|
|
5441
|
-
function
|
|
5406
|
+
function gitignoreRules(targetRepo, writeSet) {
|
|
5407
|
+
return {
|
|
5408
|
+
rel: GITIGNORE_REL,
|
|
5409
|
+
header: GITIGNORE_HEADER,
|
|
5410
|
+
ourLines: writeSet.gitignoreLines,
|
|
5411
|
+
keepLine: (line) => coveredPathExists(targetRepo, line)
|
|
5412
|
+
};
|
|
5413
|
+
}
|
|
5414
|
+
function gitattributesRules(writeSet) {
|
|
5415
|
+
return {
|
|
5416
|
+
rel: GITATTRIBUTES_REL,
|
|
5417
|
+
header: GITATTRIBUTES_HEADER,
|
|
5418
|
+
ourLines: writeSet.gitattributesLines,
|
|
5419
|
+
keepLine: () => false
|
|
5420
|
+
};
|
|
5421
|
+
}
|
|
5422
|
+
function trimAppendedBlocks(text, rec, rules) {
|
|
5442
5423
|
const keptLines = [];
|
|
5443
5424
|
const foreignLines = [];
|
|
5444
5425
|
let current = text;
|
|
5445
5426
|
for (const block of [...rec.appended].reverse()) {
|
|
5446
5427
|
const lines = block.split("\n").filter((line) => line !== "");
|
|
5447
|
-
const foreign = lines.filter((line) => !
|
|
5428
|
+
const foreign = lines.filter((line) => !rules.ourLines.has(line));
|
|
5448
5429
|
foreignLines.push(...foreign);
|
|
5449
|
-
const ours = lines.filter((line) =>
|
|
5450
|
-
const entries = ours.filter((line) => line !==
|
|
5451
|
-
const kept = entries.filter((line) =>
|
|
5430
|
+
const ours = lines.filter((line) => rules.ourLines.has(line));
|
|
5431
|
+
const entries = ours.filter((line) => line !== rules.header);
|
|
5432
|
+
const kept = entries.filter((line) => rules.keepLine(line));
|
|
5452
5433
|
keptLines.push(...kept);
|
|
5453
5434
|
if (kept.length === 0 && foreign.length === 0 && current.includes(block)) {
|
|
5454
5435
|
const at = current.lastIndexOf(block);
|
|
@@ -5456,13 +5437,13 @@ function trimGitignore(targetRepo, text, rec, writeSet) {
|
|
|
5456
5437
|
continue;
|
|
5457
5438
|
}
|
|
5458
5439
|
const separated = block.startsWith("\n") || block.includes("\n\n");
|
|
5459
|
-
const removable = ours.filter((line) => line ===
|
|
5440
|
+
const removable = ours.filter((line) => line === rules.header ? kept.length === 0 && foreign.length === 0 : !kept.includes(line));
|
|
5460
5441
|
for (const line of removable) {
|
|
5461
5442
|
const fileLines = current.split("\n");
|
|
5462
5443
|
const at = fileLines.lastIndexOf(line);
|
|
5463
5444
|
if (at < 0)
|
|
5464
5445
|
continue;
|
|
5465
|
-
const withSeparator = line ===
|
|
5446
|
+
const withSeparator = line === rules.header && separated && at > 0 && fileLines[at - 1] === "";
|
|
5466
5447
|
fileLines.splice(withSeparator ? at - 1 : at, withSeparator ? 2 : 1);
|
|
5467
5448
|
current = fileLines.join("\n");
|
|
5468
5449
|
}
|
|
@@ -5554,7 +5535,13 @@ function uninstall(options) {
|
|
|
5554
5535
|
continue;
|
|
5555
5536
|
removeEmptyCreatedDir(targetRepo, dir, writeSet, result, name);
|
|
5556
5537
|
}
|
|
5557
|
-
|
|
5538
|
+
const keptIgnoreLines = trimAppendedFile(targetRepo, manifest.gitignore, gitignoreRules(targetRepo, writeSet), result, name);
|
|
5539
|
+
for (const line of keptIgnoreLines) {
|
|
5540
|
+
result.keptIgnoreLines.push(`${line.trim()} \u2014 ${line.trim().replace(/^\//, "")} still exists`);
|
|
5541
|
+
}
|
|
5542
|
+
if (manifest.gitattributes !== void 0) {
|
|
5543
|
+
trimAppendedFile(targetRepo, manifest.gitattributes, gitattributesRules(writeSet), result, name);
|
|
5544
|
+
}
|
|
5558
5545
|
const credential = handleCredential(options, checked);
|
|
5559
5546
|
const credentialFailed = credential.kind === "failed";
|
|
5560
5547
|
if (result.failed.length === 0 && !credentialFailed) {
|
|
@@ -5668,40 +5655,39 @@ function removeEmptyCreatedDir(targetRepo, dir, writeSet, lists, name) {
|
|
|
5668
5655
|
name(lists.failed, `${dir}/`, dir);
|
|
5669
5656
|
}
|
|
5670
5657
|
}
|
|
5671
|
-
function
|
|
5658
|
+
function trimAppendedFile(targetRepo, rec, rules, lists, name) {
|
|
5659
|
+
const { rel } = rules;
|
|
5672
5660
|
if (rec.appended.length === 0)
|
|
5673
|
-
return;
|
|
5674
|
-
if (symlinkOnPath(targetRepo,
|
|
5675
|
-
name(lists.leftAlone, `${
|
|
5676
|
-
return;
|
|
5661
|
+
return [];
|
|
5662
|
+
if (symlinkOnPath(targetRepo, rel)) {
|
|
5663
|
+
name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
|
|
5664
|
+
return [];
|
|
5677
5665
|
}
|
|
5678
|
-
const path = join8(targetRepo,
|
|
5666
|
+
const path = join8(targetRepo, rel);
|
|
5679
5667
|
let before;
|
|
5680
5668
|
try {
|
|
5681
5669
|
before = readFileSync8(path, "utf-8");
|
|
5682
5670
|
} catch {
|
|
5683
|
-
return;
|
|
5671
|
+
return [];
|
|
5684
5672
|
}
|
|
5685
|
-
const { text, keptLines, foreignLines } =
|
|
5673
|
+
const { text, keptLines, foreignLines } = trimAppendedBlocks(before, rec, rules);
|
|
5686
5674
|
for (const line of foreignLines) {
|
|
5687
|
-
name(lists.leftAlone, `${
|
|
5688
|
-
}
|
|
5689
|
-
for (const line of keptLines) {
|
|
5690
|
-
lists.keptIgnoreLines.push(`${line.trim()} \u2014 ${line.trim().replace(/^\//, "")} still exists`);
|
|
5675
|
+
name(lists.leftAlone, `${rel}: the line ${JSON.stringify(line)} (Halfcycle never writes it, so it was kept)`);
|
|
5691
5676
|
}
|
|
5692
5677
|
if (text === before)
|
|
5693
|
-
return;
|
|
5678
|
+
return keptLines;
|
|
5694
5679
|
try {
|
|
5695
5680
|
if (text === "" && rec.created) {
|
|
5696
5681
|
rmSync3(path);
|
|
5697
|
-
name(lists.removed,
|
|
5682
|
+
name(lists.removed, rel, rel);
|
|
5698
5683
|
} else {
|
|
5699
5684
|
writeFileSync5(path, text, "utf-8");
|
|
5700
|
-
name(lists.restored,
|
|
5685
|
+
name(lists.restored, rel, rel);
|
|
5701
5686
|
}
|
|
5702
5687
|
} catch {
|
|
5703
|
-
name(lists.failed,
|
|
5688
|
+
name(lists.failed, rel, rel);
|
|
5704
5689
|
}
|
|
5690
|
+
return keptLines;
|
|
5705
5691
|
}
|
|
5706
5692
|
function handleCredential(options, checked) {
|
|
5707
5693
|
const { removeCredential, home } = options;
|
|
@@ -5817,7 +5803,6 @@ function renderUninstallReport(result, home) {
|
|
|
5817
5803
|
}
|
|
5818
5804
|
|
|
5819
5805
|
// dist/banner-facts.js
|
|
5820
|
-
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5821
5806
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
5822
5807
|
import { basename as basename3, join as join9, resolve as resolve3 } from "node:path";
|
|
5823
5808
|
var BRAND_URL = "halfcycle.ai";
|
|
@@ -5838,17 +5823,8 @@ function bundleVersion() {
|
|
|
5838
5823
|
return void 0;
|
|
5839
5824
|
}
|
|
5840
5825
|
}
|
|
5841
|
-
function claudeCodeVersion() {
|
|
5842
|
-
|
|
5843
|
-
const raw = execFileSync4("claude", ["--version"], {
|
|
5844
|
-
encoding: "utf-8",
|
|
5845
|
-
timeout: 3e3,
|
|
5846
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
5847
|
-
});
|
|
5848
|
-
return /(\d+\.\d+\.\d+)/.exec(raw)?.[1];
|
|
5849
|
-
} catch {
|
|
5850
|
-
return void 0;
|
|
5851
|
-
}
|
|
5826
|
+
function claudeCodeVersion(deps = {}) {
|
|
5827
|
+
return probeClaudeCodeVersion(3e3, deps);
|
|
5852
5828
|
}
|
|
5853
5829
|
var REAL_PROBES = {
|
|
5854
5830
|
bundleVersion,
|
|
@@ -5949,21 +5925,10 @@ function compareVersions(a, b) {
|
|
|
5949
5925
|
}
|
|
5950
5926
|
return 0;
|
|
5951
5927
|
}
|
|
5952
|
-
function reportClaudeCodeVersion(verbose2) {
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
raw = execFileSync5("claude", ["--version"], {
|
|
5956
|
-
encoding: "utf-8",
|
|
5957
|
-
timeout: 5e3,
|
|
5958
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
5959
|
-
});
|
|
5960
|
-
} catch {
|
|
5928
|
+
function reportClaudeCodeVersion(verbose2, deps = {}) {
|
|
5929
|
+
const version = probeClaudeCodeVersion(5e3, deps);
|
|
5930
|
+
if (version === void 0)
|
|
5961
5931
|
return;
|
|
5962
|
-
}
|
|
5963
|
-
const found = /(\d+\.\d+\.\d+)/.exec(raw);
|
|
5964
|
-
if (!found)
|
|
5965
|
-
return;
|
|
5966
|
-
const version = found[1];
|
|
5967
5932
|
if (compareVersions(version, MIN_HEADERS_HELPER_VERSION) >= 0) {
|
|
5968
5933
|
if (verbose2) {
|
|
5969
5934
|
process.stdout.write(`[halfcycle] Claude Code ${version} detected \u2014 new enough for the credential helper.
|
|
@@ -6005,6 +5970,15 @@ function isHalfcycleMonorepo(dir) {
|
|
|
6005
5970
|
return false;
|
|
6006
5971
|
}
|
|
6007
5972
|
}
|
|
5973
|
+
function isHomeFolder(dir) {
|
|
5974
|
+
try {
|
|
5975
|
+
const target = realpathSync(dir);
|
|
5976
|
+
const home = realpathSync(homedir3());
|
|
5977
|
+
return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
|
|
5978
|
+
} catch {
|
|
5979
|
+
return false;
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
6008
5982
|
var RETIRED_VERBS = /* @__PURE__ */ new Map([
|
|
6009
5983
|
["ci", '"ci" was removed \u2014 CI setup is part of your project, not Halfcycle']
|
|
6010
5984
|
]);
|
|
@@ -6044,6 +6018,12 @@ ${USAGE}`);
|
|
|
6044
6018
|
product's own source tree \u2014 installing here would write over tracked files.
|
|
6045
6019
|
To see what this CLI does: halfcycle --help
|
|
6046
6020
|
To install into a real project: halfcycle <path-to-that-repo>
|
|
6021
|
+
`);
|
|
6022
|
+
process.exit(1);
|
|
6023
|
+
return;
|
|
6024
|
+
}
|
|
6025
|
+
if (isHomeFolder(targetRepo)) {
|
|
6026
|
+
process.stderr.write(`halfcycle: refusing to set up in your home folder (${targetRepo}). Run "npx halfcycle" inside a project folder.
|
|
6047
6027
|
`);
|
|
6048
6028
|
process.exit(1);
|
|
6049
6029
|
return;
|
|
@@ -6186,12 +6166,16 @@ ${USAGE}`);
|
|
|
6186
6166
|
`);
|
|
6187
6167
|
}
|
|
6188
6168
|
if (credential) {
|
|
6189
|
-
const probe = await probeMcpOrigin(credential.mcpUrl);
|
|
6169
|
+
const probe = await probeMcpOrigin(credential.mcpUrl, credential.token);
|
|
6190
6170
|
if (probe.reached) {
|
|
6191
6171
|
if (verbose) {
|
|
6192
6172
|
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 reachable (${probe.serverName})
|
|
6193
6173
|
`);
|
|
6194
6174
|
}
|
|
6175
|
+
} else if (probe.credentialRefused) {
|
|
6176
|
+
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 ${probe.problem}.
|
|
6177
|
+
[halfcycle] The address is right, but the Halfcycle commands will have no method context until it accepts this machine's credential. Re-run "npx halfcycle" in this folder; if it says this again, the refusal is on Halfcycle's side, not in this project.
|
|
6178
|
+
`);
|
|
6195
6179
|
} else {
|
|
6196
6180
|
process.stdout.write(`[halfcycle] Method delivery (MCP): ${credential.mcpUrl} \u2014 NOT reachable: ${probe.problem}.
|
|
6197
6181
|
[halfcycle] The Halfcycle commands will have no method context until that address answers. This is the SECOND of two origins and the platform supplied it, so it is not the CONTROL origin (${credential.serviceUrl}) that is wrong \u2014 that one just worked. It is recorded as HALFCYCLE_MCP_URL in this engagement's credential store; re-run this installer once the service is up.
|
|
@@ -6201,11 +6185,11 @@ ${USAGE}`);
|
|
|
6201
6185
|
if (credential && verbose) {
|
|
6202
6186
|
process.stdout.write(`[halfcycle] Guard evaluation: ON \u2014 the hook evaluates against ${credential.guardUrl}. Credentials (${GUARD_COVERAGE_REQUIRED_KEYS.join(", ")}) are in ${engagementEnvPath(engagementId)}, outside this repository; the hook loads them itself, so you do not export anything and nothing here can be committed.
|
|
6203
6187
|
`);
|
|
6204
|
-
process.stdout.write(`[halfcycle] Your credential is at ${engagementEnvPath(engagementId)} \u2014 outside this repository, readable only by you. .mcp.json reads it at connection time through
|
|
6188
|
+
process.stdout.write(`[halfcycle] Your credential is at ${engagementEnvPath(engagementId)} \u2014 outside this repository, readable only by you. .mcp.json reads it at connection time through the Halfcycle runner in ${VENDORED_BIN_REL}, so there is no token in this tree to commit and nothing for you to export.
|
|
6205
6189
|
`);
|
|
6206
|
-
process.stdout.write(`[halfcycle] Expect a workspace trust prompt the first time you open this folder \u2014 the
|
|
6190
|
+
process.stdout.write(`[halfcycle] Expect a workspace trust prompt the first time you open this folder \u2014 the connection runs a command from this project, and Claude Code will not run it until you accept. Needs Claude Code ${MIN_HEADERS_HELPER_VERSION} or newer (${MIN_HEADER_ROTATION_VERSION}+ to re-authenticate a rotated token without restarting).
|
|
6207
6191
|
`);
|
|
6208
|
-
process.stdout.write(`[halfcycle] If Halfcycle
|
|
6192
|
+
process.stdout.write(`[halfcycle] If Halfcycle reads as not connected, or every call returns 401, the connection could not read GUARD_SERVICE_TOKEN from ${engagementEnvPath(engagementId)} \u2014 re-run this installer.
|
|
6209
6193
|
`);
|
|
6210
6194
|
}
|
|
6211
6195
|
if (credential === void 0 && notThisAccountMessage !== void 0) {
|