halfcycle 0.3.31 → 0.3.32
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/bin/bin.bundle.mjs +310 -40
- package/dist/bin.js +212 -108
- package/dist/bin.js.map +3 -3
- package/dist/identity.d.ts +39 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +196 -102
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +60 -40
- package/dist/install.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/identity.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface ProjectIdentity {
|
|
|
65
65
|
*/
|
|
66
66
|
note: string;
|
|
67
67
|
}
|
|
68
|
+
/** This process's environment without the repository-locating git variables. */
|
|
69
|
+
export declare function gitEnvForNamedRepo(): NodeJS.ProcessEnv;
|
|
68
70
|
/** Read the repository's root commit hash (the first commit, reachable from HEAD). */
|
|
69
71
|
export declare function readRootCommit(targetRepoRoot: string): string | null;
|
|
70
72
|
/** Read the `origin` remote URL. */
|
|
@@ -79,6 +81,43 @@ export declare function readRemote(targetRepoRoot: string): string | null;
|
|
|
79
81
|
* the banner drops the whole clause rather than printing a zero it did not measure.
|
|
80
82
|
*/
|
|
81
83
|
export declare function readCommitCount(targetRepoRoot: string): number | null;
|
|
84
|
+
/** One path git ignores, with the rule that ignores it. */
|
|
85
|
+
export interface IgnoredPath {
|
|
86
|
+
/** The path as it was asked about, relative to the directory the install ran in. */
|
|
87
|
+
path: string;
|
|
88
|
+
/** The pattern that matched, as written in its source. */
|
|
89
|
+
pattern: string;
|
|
90
|
+
/**
|
|
91
|
+
* Where the pattern lives, as git names it: relative to the repository root for a
|
|
92
|
+
* file inside it, else absolute. Made absolute when the install ran below the root.
|
|
93
|
+
*/
|
|
94
|
+
source: string;
|
|
95
|
+
/** The pattern's line number in `source`. */
|
|
96
|
+
line: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Which of `paths` git would leave out of a commit, and the rule that says so, or `null`
|
|
100
|
+
* when git cannot answer (no repository, no `git`) or nothing is ignored. An empty
|
|
101
|
+
* `paths` asks git nothing.
|
|
102
|
+
*
|
|
103
|
+
* ONE `git check-ignore -v -z --stdin`: the paths go NUL-separated on stdin and every
|
|
104
|
+
* answer comes back as four NUL-separated fields — source, line, pattern, path. `-z`
|
|
105
|
+
* because an absolute excludes-file path may contain `:`, which the default output
|
|
106
|
+
* uses as its separator; `--stdin` because `-z` requires it. The command is index
|
|
107
|
+
* aware, so a tracked file is never reported however its rules read.
|
|
108
|
+
*
|
|
109
|
+
* A `!` pattern is a negation: `-v` prints the one that matched with exit 0 although
|
|
110
|
+
* the path is NOT ignored, so it is dropped here rather than reported. Git refuses the
|
|
111
|
+
* whole batch (exit 128) over one path below a symbolic link or inside a submodule;
|
|
112
|
+
* the caller drops those paths before asking, and a refusal here reads as "cannot
|
|
113
|
+
* answer", with no retry.
|
|
114
|
+
*
|
|
115
|
+
* Git reports a source inside the repository relative to the repository ROOT, not to
|
|
116
|
+
* the directory it ran in, so when the install ran below the root such a source is
|
|
117
|
+
* joined to `--show-toplevel` (which resolves symbolic links) and returned absolute.
|
|
118
|
+
* Those two calls are made only when there is something to report.
|
|
119
|
+
*/
|
|
120
|
+
export declare function readIgnoredPaths(targetRepoRoot: string, paths: readonly string[]): IgnoredPath[] | null;
|
|
82
121
|
/**
|
|
83
122
|
* Return the existing committed identity if `.halfcycle/project.json` is present,
|
|
84
123
|
* otherwise mint a fresh one. Minting never overwrites the MINTED fields
|
package/dist/identity.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,qBAAqB,QAGF,CAAC;AAEjC,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,qBAAqB,QAGF,CAAC;AAEjC,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AA6BD,gFAAgF;AAChF,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,UAAU,CAItD;AAqBD,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpE;AAED,oCAAoC;AACpC,wBAAgB,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKrE;AAED,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,EAAE,GAAG,IAAI,CAyBvG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAmBzG"}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
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, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5
5
|
import { dirname as dirname2, join as join5, posix, relative } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
// ../events/dist/result.js
|
|
9
|
-
import { z as z2 } from "zod";
|
|
10
|
-
|
|
11
8
|
// ../core/dist/guard-record.js
|
|
12
9
|
import { z } from "zod";
|
|
13
10
|
var severitySchema = z.enum(["info", "warn", "block"]);
|
|
@@ -113,7 +110,76 @@ ${block}`);
|
|
|
113
110
|
return result;
|
|
114
111
|
}
|
|
115
112
|
|
|
113
|
+
// ../core/dist/hook-wiring.js
|
|
114
|
+
var SETTINGS_REL = ".claude/settings.json";
|
|
115
|
+
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
116
|
+
var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
|
|
117
|
+
var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
118
|
+
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
119
|
+
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
120
|
+
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
121
|
+
var HALFCYCLE_HOOK_WIRING = [
|
|
122
|
+
{
|
|
123
|
+
event: "SessionStart",
|
|
124
|
+
matcher: null,
|
|
125
|
+
timeout: 5,
|
|
126
|
+
command: `bash ./${SESSION_START_MARKER_REL}`,
|
|
127
|
+
script: SESSION_START_MARKER_REL
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
event: "SessionStart",
|
|
131
|
+
matcher: null,
|
|
132
|
+
timeout: 10,
|
|
133
|
+
command: `bash ./${GUARD_RUNNER_REL} refresh-credential`,
|
|
134
|
+
script: GUARD_RUNNER_REL
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
event: "UserPromptSubmit",
|
|
138
|
+
matcher: null,
|
|
139
|
+
timeout: 5,
|
|
140
|
+
command: `bash ./${USER_PROMPT_REMINDER_REL}`,
|
|
141
|
+
script: USER_PROMPT_REMINDER_REL
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
event: "PostToolUse",
|
|
145
|
+
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
146
|
+
timeout: 60,
|
|
147
|
+
command: `bash ./${GUARD_RUNNER_REL} hook post-tool-use`,
|
|
148
|
+
script: GUARD_RUNNER_REL
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
event: "Stop",
|
|
152
|
+
matcher: null,
|
|
153
|
+
timeout: 60,
|
|
154
|
+
command: `bash ./${GUARD_RUNNER_REL} hook stop`,
|
|
155
|
+
script: GUARD_RUNNER_REL
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
event: "SubagentStop",
|
|
159
|
+
matcher: null,
|
|
160
|
+
timeout: 60,
|
|
161
|
+
command: `bash ./${GUARD_RUNNER_REL} hook subagent-stop`,
|
|
162
|
+
script: GUARD_RUNNER_REL
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
event: "SessionEnd",
|
|
166
|
+
matcher: null,
|
|
167
|
+
timeout: 5,
|
|
168
|
+
command: `bash ./${SESSION_END_MARKER_REL}`,
|
|
169
|
+
script: SESSION_END_MARKER_REL
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
var RETIRED_HOOK_COMMANDS = [];
|
|
173
|
+
var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
|
|
174
|
+
...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
|
|
175
|
+
...RETIRED_HOOK_COMMANDS
|
|
176
|
+
]);
|
|
177
|
+
var HALFCYCLE_HOOK_WIRING_PATHS = [
|
|
178
|
+
.../* @__PURE__ */ new Set([SETTINGS_REL, ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])
|
|
179
|
+
];
|
|
180
|
+
|
|
116
181
|
// ../events/dist/result.js
|
|
182
|
+
import { z as z2 } from "zod";
|
|
117
183
|
var firedGuardSchema = z2.object({
|
|
118
184
|
guardId: z2.string(),
|
|
119
185
|
patternRef: z2.string(),
|
|
@@ -925,14 +991,36 @@ halfcycle_env_file() {
|
|
|
925
991
|
// dist/identity.js
|
|
926
992
|
import { execFileSync } from "node:child_process";
|
|
927
993
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
928
|
-
import { join as join3 } from "node:path";
|
|
994
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
929
995
|
import { randomUUID } from "node:crypto";
|
|
930
996
|
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.';
|
|
931
|
-
|
|
997
|
+
var REPOSITORY_LOCATING_GIT_VARS = [
|
|
998
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
999
|
+
"GIT_COMMON_DIR",
|
|
1000
|
+
"GIT_DIR",
|
|
1001
|
+
"GIT_GRAFT_FILE",
|
|
1002
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
1003
|
+
"GIT_INDEX_FILE",
|
|
1004
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
1005
|
+
"GIT_OBJECT_DIRECTORY",
|
|
1006
|
+
"GIT_PREFIX",
|
|
1007
|
+
"GIT_REPLACE_REF_BASE",
|
|
1008
|
+
"GIT_SHALLOW_FILE",
|
|
1009
|
+
"GIT_WORK_TREE"
|
|
1010
|
+
];
|
|
1011
|
+
function gitEnvForNamedRepo() {
|
|
1012
|
+
const env = { ...process.env };
|
|
1013
|
+
for (const name of REPOSITORY_LOCATING_GIT_VARS)
|
|
1014
|
+
delete env[name];
|
|
1015
|
+
return env;
|
|
1016
|
+
}
|
|
1017
|
+
function git(targetRepoRoot, args, stdin) {
|
|
932
1018
|
try {
|
|
933
1019
|
const out = execFileSync("git", ["-C", targetRepoRoot, ...args], {
|
|
934
1020
|
encoding: "utf-8",
|
|
935
|
-
|
|
1021
|
+
env: gitEnvForNamedRepo(),
|
|
1022
|
+
...stdin === void 0 ? {} : { input: stdin },
|
|
1023
|
+
stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "ignore"]
|
|
936
1024
|
});
|
|
937
1025
|
const trimmed = out.trim();
|
|
938
1026
|
return trimmed.length > 0 ? trimmed : null;
|
|
@@ -950,6 +1038,38 @@ function readRootCommit(targetRepoRoot) {
|
|
|
950
1038
|
function readRemote(targetRepoRoot) {
|
|
951
1039
|
return git(targetRepoRoot, ["remote", "get-url", "origin"]);
|
|
952
1040
|
}
|
|
1041
|
+
function readIgnoredPaths(targetRepoRoot, paths) {
|
|
1042
|
+
if (paths.length === 0)
|
|
1043
|
+
return null;
|
|
1044
|
+
const out = git(targetRepoRoot, ["check-ignore", "-v", "-z", "--stdin"], paths.map((p) => `${p}\0`).join(""));
|
|
1045
|
+
if (out === null)
|
|
1046
|
+
return null;
|
|
1047
|
+
const fields = out.split("\0");
|
|
1048
|
+
if (fields[fields.length - 1] === "")
|
|
1049
|
+
fields.pop();
|
|
1050
|
+
if (fields.length % 4 !== 0)
|
|
1051
|
+
return null;
|
|
1052
|
+
const ignored = [];
|
|
1053
|
+
for (let i = 0; i < fields.length; i += 4) {
|
|
1054
|
+
const [source, line, pattern, path] = fields.slice(i, i + 4);
|
|
1055
|
+
if (pattern.startsWith("!"))
|
|
1056
|
+
continue;
|
|
1057
|
+
const lineNumber = Number.parseInt(line, 10);
|
|
1058
|
+
if (!Number.isFinite(lineNumber))
|
|
1059
|
+
return null;
|
|
1060
|
+
ignored.push({ path, pattern, source, line: lineNumber });
|
|
1061
|
+
}
|
|
1062
|
+
if (ignored.length === 0)
|
|
1063
|
+
return null;
|
|
1064
|
+
if (ignored.every((entry) => isAbsolute(entry.source)))
|
|
1065
|
+
return ignored;
|
|
1066
|
+
if (git(targetRepoRoot, ["rev-parse", "--show-prefix"]) === null)
|
|
1067
|
+
return ignored;
|
|
1068
|
+
const toplevel = git(targetRepoRoot, ["rev-parse", "--show-toplevel"]);
|
|
1069
|
+
if (toplevel === null)
|
|
1070
|
+
return null;
|
|
1071
|
+
return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source: join3(toplevel, entry.source) });
|
|
1072
|
+
}
|
|
953
1073
|
function mintOrReadIdentity(targetRepoRoot) {
|
|
954
1074
|
const path = join3(targetRepoRoot, ".halfcycle", "project.json");
|
|
955
1075
|
if (existsSync(path)) {
|
|
@@ -1399,18 +1519,12 @@ var BUNDLE_PIN_REL = ".halfcycle/bundle.json";
|
|
|
1399
1519
|
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
1400
1520
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
1401
1521
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
1402
|
-
var SETTINGS_REL = ".claude/settings.json";
|
|
1403
1522
|
var GITIGNORE_REL = ".gitignore";
|
|
1404
1523
|
function commandStubRel(name) {
|
|
1405
1524
|
return `.claude/commands/${name}.md`;
|
|
1406
1525
|
}
|
|
1407
1526
|
var RETIRED_WRITE_SET_FILES = [];
|
|
1408
|
-
var RETIRED_HOOK_COMMANDS = [];
|
|
1409
1527
|
var RETIRED_DENY_PATTERNS = [];
|
|
1410
|
-
function installerHookCommands() {
|
|
1411
|
-
const settings = JSON.parse(generateSettingsJson());
|
|
1412
|
-
return Object.values(settings.hooks ?? {}).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
|
|
1413
|
-
}
|
|
1414
1528
|
function closedWriteSet() {
|
|
1415
1529
|
const files = /* @__PURE__ */ new Set([
|
|
1416
1530
|
...(readPluginManifest().commands ?? []).map((cmd) => commandStubRel(cmd.name)),
|
|
@@ -1436,7 +1550,7 @@ function closedWriteSet() {
|
|
|
1436
1550
|
merged,
|
|
1437
1551
|
dirs,
|
|
1438
1552
|
ownedDirs,
|
|
1439
|
-
hookCommands:
|
|
1553
|
+
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
1440
1554
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
1441
1555
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL])
|
|
1442
1556
|
};
|
|
@@ -1521,6 +1635,54 @@ function assertManifestInWriteSet(manifest, writeSet) {
|
|
|
1521
1635
|
throw new Error(`[bundle install] the install record names paths outside the write set: ${strays.join(", ")}`);
|
|
1522
1636
|
}
|
|
1523
1637
|
}
|
|
1638
|
+
function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
1639
|
+
const merged = [
|
|
1640
|
+
SETTINGS_REL,
|
|
1641
|
+
...manifest.mcp !== void 0 ? [MCP_REGISTRATION_REL] : [],
|
|
1642
|
+
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : []
|
|
1643
|
+
];
|
|
1644
|
+
const collided = new Set(collidedPaths);
|
|
1645
|
+
const dirVerdicts = /* @__PURE__ */ new Map();
|
|
1646
|
+
const travelsThroughPlainDirs = (rel) => {
|
|
1647
|
+
for (let dir = posix.dirname(rel); dir !== "."; dir = posix.dirname(dir)) {
|
|
1648
|
+
let plain = dirVerdicts.get(dir);
|
|
1649
|
+
if (plain === void 0) {
|
|
1650
|
+
const abs = join5(targetRepo, dir);
|
|
1651
|
+
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join5(abs, ".git")) === null;
|
|
1652
|
+
dirVerdicts.set(dir, plain);
|
|
1653
|
+
}
|
|
1654
|
+
if (!plain)
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
return true;
|
|
1658
|
+
};
|
|
1659
|
+
const paths = [
|
|
1660
|
+
.../* @__PURE__ */ new Set([
|
|
1661
|
+
...manifest.files.filter((f) => !("perMachine" in f)).map((f) => f.path),
|
|
1662
|
+
...merged,
|
|
1663
|
+
INSTALL_MANIFEST_REL
|
|
1664
|
+
])
|
|
1665
|
+
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join5(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
|
|
1666
|
+
const ignored = readIgnoredPaths(targetRepo, paths);
|
|
1667
|
+
if (ignored === null)
|
|
1668
|
+
return null;
|
|
1669
|
+
const sorted = [...ignored].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1670
|
+
const named = sorted.find((entry) => entry.path === SETTINGS_REL) ?? sorted[0];
|
|
1671
|
+
return {
|
|
1672
|
+
path: named.path,
|
|
1673
|
+
pattern: named.pattern,
|
|
1674
|
+
source: named.source,
|
|
1675
|
+
line: named.line,
|
|
1676
|
+
others: sorted.length - 1
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function lstatOrNull(path) {
|
|
1680
|
+
try {
|
|
1681
|
+
return lstatSync(path);
|
|
1682
|
+
} catch {
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1524
1686
|
function isPlainObject(value) {
|
|
1525
1687
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1526
1688
|
}
|
|
@@ -1635,9 +1797,15 @@ var DENY_PATTERNS = [
|
|
|
1635
1797
|
"Bash(git push --no-verify:*)",
|
|
1636
1798
|
"Bash(git commit --no-verify:*)"
|
|
1637
1799
|
];
|
|
1638
|
-
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
1639
1800
|
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.";
|
|
1640
1801
|
function generateSettingsJson() {
|
|
1802
|
+
const hooks = {};
|
|
1803
|
+
for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
|
|
1804
|
+
(hooks[event] ??= []).push({
|
|
1805
|
+
...matcher === null ? {} : { matcher },
|
|
1806
|
+
hooks: [{ type: "command", command, timeout }]
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1641
1809
|
const settings = {
|
|
1642
1810
|
$schema: "https://json.schemastore.org/claude-code-settings.json",
|
|
1643
1811
|
permissions: {
|
|
@@ -1645,88 +1813,10 @@ function generateSettingsJson() {
|
|
|
1645
1813
|
// and is the machine owner's grant to make, not ours.
|
|
1646
1814
|
deny: [...DENY_PATTERNS]
|
|
1647
1815
|
},
|
|
1648
|
-
hooks
|
|
1649
|
-
SessionStart: [
|
|
1650
|
-
{
|
|
1651
|
-
hooks: [
|
|
1652
|
-
{
|
|
1653
|
-
type: "command",
|
|
1654
|
-
command: "bash ./.claude/hooks/session-start-marker.sh",
|
|
1655
|
-
timeout: 5
|
|
1656
|
-
}
|
|
1657
|
-
]
|
|
1658
|
-
},
|
|
1659
|
-
{
|
|
1660
|
-
hooks: [
|
|
1661
|
-
{
|
|
1662
|
-
type: "command",
|
|
1663
|
-
command: "bash ./.claude/hooks/guard-runner.sh refresh-credential",
|
|
1664
|
-
timeout: 10
|
|
1665
|
-
}
|
|
1666
|
-
]
|
|
1667
|
-
}
|
|
1668
|
-
],
|
|
1669
|
-
UserPromptSubmit: [
|
|
1670
|
-
{
|
|
1671
|
-
hooks: [
|
|
1672
|
-
{
|
|
1673
|
-
type: "command",
|
|
1674
|
-
command: "bash ./.claude/hooks/user-prompt-reminder.sh",
|
|
1675
|
-
timeout: 5
|
|
1676
|
-
}
|
|
1677
|
-
]
|
|
1678
|
-
}
|
|
1679
|
-
],
|
|
1680
|
-
PostToolUse: [
|
|
1681
|
-
{
|
|
1682
|
-
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
1683
|
-
hooks: [
|
|
1684
|
-
{
|
|
1685
|
-
type: "command",
|
|
1686
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
|
|
1687
|
-
timeout: 60
|
|
1688
|
-
}
|
|
1689
|
-
]
|
|
1690
|
-
}
|
|
1691
|
-
],
|
|
1692
|
-
Stop: [
|
|
1693
|
-
{
|
|
1694
|
-
hooks: [
|
|
1695
|
-
{
|
|
1696
|
-
type: "command",
|
|
1697
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
1698
|
-
timeout: 60
|
|
1699
|
-
}
|
|
1700
|
-
]
|
|
1701
|
-
}
|
|
1702
|
-
],
|
|
1703
|
-
SubagentStop: [
|
|
1704
|
-
{
|
|
1705
|
-
hooks: [
|
|
1706
|
-
{
|
|
1707
|
-
type: "command",
|
|
1708
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
|
|
1709
|
-
timeout: 60
|
|
1710
|
-
}
|
|
1711
|
-
]
|
|
1712
|
-
}
|
|
1713
|
-
],
|
|
1714
|
-
SessionEnd: [
|
|
1715
|
-
{
|
|
1716
|
-
hooks: [
|
|
1717
|
-
{
|
|
1718
|
-
type: "command",
|
|
1719
|
-
command: "bash ./.claude/hooks/session-end-marker.sh",
|
|
1720
|
-
timeout: 5
|
|
1721
|
-
}
|
|
1722
|
-
]
|
|
1723
|
-
}
|
|
1724
|
-
]
|
|
1725
|
-
}
|
|
1816
|
+
hooks
|
|
1726
1817
|
};
|
|
1727
1818
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
1728
1819
|
}
|
|
1729
|
-
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
1730
1820
|
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
1731
1821
|
function generateGuardRunnerWrapper() {
|
|
1732
1822
|
return `#!/usr/bin/env bash
|
|
@@ -1948,14 +2038,16 @@ var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
|
1948
2038
|
function isTrackedAndClean(targetRepo, relPath) {
|
|
1949
2039
|
try {
|
|
1950
2040
|
execFileSync2("git", ["-C", targetRepo, "ls-files", "--error-unmatch", "--", relPath], {
|
|
1951
|
-
stdio: "ignore"
|
|
2041
|
+
stdio: "ignore",
|
|
2042
|
+
env: gitEnvForNamedRepo()
|
|
1952
2043
|
});
|
|
1953
2044
|
} catch {
|
|
1954
2045
|
return false;
|
|
1955
2046
|
}
|
|
1956
2047
|
try {
|
|
1957
2048
|
execFileSync2("git", ["-C", targetRepo, "diff", "--quiet", "HEAD", "--", relPath], {
|
|
1958
|
-
stdio: "ignore"
|
|
2049
|
+
stdio: "ignore",
|
|
2050
|
+
env: gitEnvForNamedRepo()
|
|
1959
2051
|
});
|
|
1960
2052
|
} catch {
|
|
1961
2053
|
return false;
|
|
@@ -2533,6 +2625,7 @@ async function install(options) {
|
|
|
2533
2625
|
const installManifest = finishManifest(draft);
|
|
2534
2626
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
2535
2627
|
recordOwned(report, join5(targetRepo, INSTALL_MANIFEST_REL), targetRepo, serializeManifest(installManifest), INSTALL_MANIFEST_REL);
|
|
2628
|
+
const notShared = findNotShared(targetRepo, installManifest, report.collidedPaths);
|
|
2536
2629
|
return {
|
|
2537
2630
|
version: manifest.version,
|
|
2538
2631
|
writtenPaths: report.writtenPaths,
|
|
@@ -2541,7 +2634,8 @@ async function install(options) {
|
|
|
2541
2634
|
collidedPaths: report.collidedPaths,
|
|
2542
2635
|
replacedPaths: report.replacedPaths,
|
|
2543
2636
|
bootstrapScanRan: scanResult.ran,
|
|
2544
|
-
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null
|
|
2637
|
+
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
2638
|
+
notShared
|
|
2545
2639
|
};
|
|
2546
2640
|
}
|
|
2547
2641
|
|
|
@@ -2722,7 +2816,7 @@ async function mintBoardEnterCode(baseUrl, sessionToken) {
|
|
|
2722
2816
|
}
|
|
2723
2817
|
|
|
2724
2818
|
// dist/setup/manifest.js
|
|
2725
|
-
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2819
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2726
2820
|
import { dirname as dirname3, join as join6, posix as posix2 } from "node:path";
|
|
2727
2821
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
2728
2822
|
var DECLARED_DIR_PREFIX = "docs";
|
|
@@ -2810,7 +2904,7 @@ function firstSymlinkOnPath(targetRepo, path) {
|
|
|
2810
2904
|
for (let i = 0; i < segments.length; i++) {
|
|
2811
2905
|
walked = join6(walked, segments[i]);
|
|
2812
2906
|
try {
|
|
2813
|
-
if (
|
|
2907
|
+
if (lstatSync2(walked).isSymbolicLink())
|
|
2814
2908
|
return segments.slice(0, i + 1).join("/");
|
|
2815
2909
|
} catch {
|
|
2816
2910
|
return null;
|
|
@@ -3280,7 +3374,7 @@ function parseCloseRecord(path, parsed) {
|
|
|
3280
3374
|
|
|
3281
3375
|
// dist/build-record/write.js
|
|
3282
3376
|
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3283
|
-
import { basename, join as join9, resolve, relative as relative2, isAbsolute } from "node:path";
|
|
3377
|
+
import { basename, join as join9, resolve, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
|
|
3284
3378
|
|
|
3285
3379
|
// dist/build-record/template.js
|
|
3286
3380
|
function severityLabel(seed) {
|
|
@@ -3334,7 +3428,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
3334
3428
|
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
3335
3429
|
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
3336
3430
|
"",
|
|
3337
|
-
guardsList ? `Guards evaluated (results only): ${guardsList}. *(What a guard reported about this phase is recorded; the guard's own definition and its history are not.)*` : "No
|
|
3431
|
+
guardsList ? `Guards evaluated (results only): ${guardsList}. *(What a guard reported about this phase is recorded; the guard's own definition and its history are not.)*` : "No guard firing was recorded on this machine during this phase.",
|
|
3338
3432
|
"",
|
|
3339
3433
|
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
3340
3434
|
"",
|
|
@@ -3359,7 +3453,7 @@ function serialiseRecordJson(record2) {
|
|
|
3359
3453
|
}
|
|
3360
3454
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
3361
3455
|
const rel = relative2(outDir, resolve(candidate));
|
|
3362
|
-
const isPlainFilename = rel !== "" && rel !== ".." && !
|
|
3456
|
+
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute2(rel) && rel === basename(rel);
|
|
3363
3457
|
if (!isPlainFilename) {
|
|
3364
3458
|
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
3365
3459
|
}
|