halfcycle 0.3.21 → 0.3.23
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/dist/bin.js +317 -42
- package/dist/bin.js.map +1 -1
- package/dist/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +310 -41
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +98 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +6 -3
- package/dist/merge-settings.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -142,6 +142,23 @@ import { z as z4 } from "zod";
|
|
|
142
142
|
var crewGroupSchema = z4.enum(["builder", "reviewer", "always-on"]);
|
|
143
143
|
var crewTierSchema = z4.enum(["deep", "standard", "fast"]);
|
|
144
144
|
var CREW_DISCIPLINE_MAX = 64;
|
|
145
|
+
var crewFaceHeadSchema = z4.enum(["sq", "sqr", "rnd", "dome", "hex"]);
|
|
146
|
+
var crewFaceAntennaSchema = z4.enum(["none", "stalk", "twin", "dish"]);
|
|
147
|
+
var crewFaceEyesSchema = z4.enum(["dots", "bars", "visor", "cyclops"]);
|
|
148
|
+
var crewFaceMouthSchema = z4.enum(["line", "grid", "dots", "wave"]);
|
|
149
|
+
var crewFaceSchema = z4.object({
|
|
150
|
+
// The outline of the head. Members who do the same kind of work share one, so a
|
|
151
|
+
// team is recognisable before any name is read.
|
|
152
|
+
head: crewFaceHeadSchema,
|
|
153
|
+
// What sits on top of the head, if anything.
|
|
154
|
+
antenna: crewFaceAntennaSchema,
|
|
155
|
+
// The eyes.
|
|
156
|
+
eyes: crewFaceEyesSchema,
|
|
157
|
+
// The mouth.
|
|
158
|
+
mouth: crewFaceMouthSchema,
|
|
159
|
+
// Whether this member is drawn with ears.
|
|
160
|
+
ears: z4.boolean()
|
|
161
|
+
}).strict();
|
|
145
162
|
var crewMemberSchema = z4.object({
|
|
146
163
|
// The agent's name. The same twenty-one on every project, so a name is a
|
|
147
164
|
// vocabulary a returning client already knows.
|
|
@@ -153,34 +170,163 @@ var crewMemberSchema = z4.object({
|
|
|
153
170
|
discipline: z4.string().min(1).max(CREW_DISCIPLINE_MAX),
|
|
154
171
|
// How much thinking this member brings. There is no field naming the supplier
|
|
155
172
|
// behind it, on this shape or on any other.
|
|
156
|
-
tier: crewTierSchema
|
|
173
|
+
tier: crewTierSchema,
|
|
174
|
+
// How this member's face is drawn. The same face on every project, so a
|
|
175
|
+
// returning client recognises a member before reading the name.
|
|
176
|
+
face: crewFaceSchema
|
|
157
177
|
}).strict();
|
|
158
178
|
var CREW_ROSTER = [
|
|
159
179
|
// ── builders: code ──────────────────────────────────────────────────────────
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
180
|
+
{
|
|
181
|
+
callsign: "ADA",
|
|
182
|
+
group: "builder",
|
|
183
|
+
discipline: "implementation",
|
|
184
|
+
tier: "deep",
|
|
185
|
+
face: { head: "sq", antenna: "stalk", eyes: "dots", mouth: "line", ears: false }
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
callsign: "OTTO",
|
|
189
|
+
group: "builder",
|
|
190
|
+
discipline: "implementation",
|
|
191
|
+
tier: "deep",
|
|
192
|
+
face: { head: "sq", antenna: "twin", eyes: "bars", mouth: "grid", ears: true }
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
callsign: "MILO",
|
|
196
|
+
group: "builder",
|
|
197
|
+
discipline: "implementation",
|
|
198
|
+
tier: "standard",
|
|
199
|
+
face: { head: "sqr", antenna: "stalk", eyes: "dots", mouth: "dots", ears: false }
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
callsign: "NOVA",
|
|
203
|
+
group: "builder",
|
|
204
|
+
discipline: "implementation",
|
|
205
|
+
tier: "standard",
|
|
206
|
+
face: { head: "sq", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
callsign: "KAI",
|
|
210
|
+
group: "builder",
|
|
211
|
+
discipline: "implementation",
|
|
212
|
+
tier: "standard",
|
|
213
|
+
face: { head: "sqr", antenna: "none", eyes: "bars", mouth: "line", ears: true }
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
callsign: "HUGO",
|
|
217
|
+
group: "builder",
|
|
218
|
+
discipline: "infrastructure",
|
|
219
|
+
tier: "standard",
|
|
220
|
+
face: { head: "sq", antenna: "twin", eyes: "dots", mouth: "grid", ears: true }
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
callsign: "WREN",
|
|
224
|
+
group: "builder",
|
|
225
|
+
discipline: "refactor",
|
|
226
|
+
tier: "fast",
|
|
227
|
+
face: { head: "sqr", antenna: "stalk", eyes: "bars", mouth: "wave", ears: false }
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
callsign: "PIP",
|
|
231
|
+
group: "builder",
|
|
232
|
+
discipline: "refactor",
|
|
233
|
+
tier: "fast",
|
|
234
|
+
face: { head: "sqr", antenna: "none", eyes: "dots", mouth: "dots", ears: false }
|
|
235
|
+
},
|
|
168
236
|
// ── builders: specs and docs ────────────────────────────────────────────────
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
237
|
+
{
|
|
238
|
+
callsign: "IRIS",
|
|
239
|
+
group: "builder",
|
|
240
|
+
discipline: "feature spec",
|
|
241
|
+
tier: "deep",
|
|
242
|
+
face: { head: "dome", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
callsign: "JUNO",
|
|
246
|
+
group: "builder",
|
|
247
|
+
discipline: "feature spec",
|
|
248
|
+
tier: "standard",
|
|
249
|
+
face: { head: "dome", antenna: "none", eyes: "dots", mouth: "wave", ears: false }
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
callsign: "LEX",
|
|
253
|
+
group: "builder",
|
|
254
|
+
discipline: "context & docs",
|
|
255
|
+
tier: "fast",
|
|
256
|
+
face: { head: "dome", antenna: "twin", eyes: "bars", mouth: "dots", ears: false }
|
|
257
|
+
},
|
|
172
258
|
// ── reviewers ───────────────────────────────────────────────────────────────
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{
|
|
259
|
+
{
|
|
260
|
+
callsign: "VERA",
|
|
261
|
+
group: "reviewer",
|
|
262
|
+
discipline: "spec consistency",
|
|
263
|
+
tier: "deep",
|
|
264
|
+
face: { head: "rnd", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
callsign: "ODIN",
|
|
268
|
+
group: "reviewer",
|
|
269
|
+
discipline: "cross-spec audit",
|
|
270
|
+
tier: "deep",
|
|
271
|
+
face: { head: "rnd", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
callsign: "CASS",
|
|
275
|
+
group: "reviewer",
|
|
276
|
+
discipline: "code review",
|
|
277
|
+
tier: "deep",
|
|
278
|
+
face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "grid", ears: true }
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
callsign: "THEO",
|
|
282
|
+
group: "reviewer",
|
|
283
|
+
discipline: "code review",
|
|
284
|
+
tier: "standard",
|
|
285
|
+
face: { head: "rnd", antenna: "twin", eyes: "bars", mouth: "line", ears: false }
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
callsign: "ECHO",
|
|
289
|
+
group: "reviewer",
|
|
290
|
+
discipline: "test quality",
|
|
291
|
+
tier: "standard",
|
|
292
|
+
face: { head: "rnd", antenna: "stalk", eyes: "dots", mouth: "wave", ears: false }
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
callsign: "MAVIS",
|
|
296
|
+
group: "reviewer",
|
|
297
|
+
discipline: "suites & coverage",
|
|
298
|
+
tier: "standard",
|
|
299
|
+
face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "dots", ears: false }
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
callsign: "ZARA",
|
|
303
|
+
group: "reviewer",
|
|
304
|
+
discipline: "smoke & walk aid",
|
|
305
|
+
tier: "standard",
|
|
306
|
+
face: { head: "rnd", antenna: "dish", eyes: "bars", mouth: "wave", ears: false }
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
callsign: "NORA",
|
|
310
|
+
group: "reviewer",
|
|
311
|
+
discipline: "evidence & provenance",
|
|
312
|
+
tier: "fast",
|
|
313
|
+
face: { head: "rnd", antenna: "twin", eyes: "dots", mouth: "line", ears: false }
|
|
314
|
+
},
|
|
181
315
|
// ── always on: continuous, takes no task ────────────────────────────────────
|
|
182
|
-
{
|
|
183
|
-
|
|
316
|
+
{
|
|
317
|
+
callsign: "ARGUS",
|
|
318
|
+
group: "always-on",
|
|
319
|
+
discipline: "guard \xB7 every diff",
|
|
320
|
+
tier: "standard",
|
|
321
|
+
face: { head: "hex", antenna: "dish", eyes: "visor", mouth: "grid", ears: true }
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
callsign: "ATLAS",
|
|
325
|
+
group: "always-on",
|
|
326
|
+
discipline: "coordinator",
|
|
327
|
+
tier: "deep",
|
|
328
|
+
face: { head: "hex", antenna: "twin", eyes: "cyclops", mouth: "line", ears: true }
|
|
329
|
+
}
|
|
184
330
|
];
|
|
185
331
|
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
186
332
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
@@ -696,11 +842,15 @@ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
|
|
|
696
842
|
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
697
843
|
return "written";
|
|
698
844
|
}
|
|
845
|
+
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
699
846
|
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
700
847
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
701
848
|
if (!isAllowlisted(rel)) {
|
|
702
849
|
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
703
850
|
}
|
|
851
|
+
if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
|
|
852
|
+
throw new Error(`[bundle install] 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.`);
|
|
853
|
+
}
|
|
704
854
|
if (existsSync3(targetAbsPath) && readFileSync4(targetAbsPath, "utf-8") === content) {
|
|
705
855
|
return "skipped";
|
|
706
856
|
}
|
|
@@ -708,6 +858,39 @@ function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
|
708
858
|
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
709
859
|
return "written";
|
|
710
860
|
}
|
|
861
|
+
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
862
|
+
function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
863
|
+
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
864
|
+
if (!isAllowlisted(rel)) {
|
|
865
|
+
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
866
|
+
}
|
|
867
|
+
let replacedExisting = false;
|
|
868
|
+
if (existsSync3(targetAbsPath)) {
|
|
869
|
+
const current = readFileSync4(targetAbsPath, "utf-8");
|
|
870
|
+
if (current === content)
|
|
871
|
+
return { outcome: "skipped", replacedExisting: false };
|
|
872
|
+
const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
|
|
873
|
+
if (!headerLine.startsWith(generatedMarker))
|
|
874
|
+
return { outcome: "collided", replacedExisting: false };
|
|
875
|
+
replacedExisting = true;
|
|
876
|
+
}
|
|
877
|
+
mkdirSync3(dirname2(targetAbsPath), { recursive: true });
|
|
878
|
+
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
879
|
+
return { outcome: "written", replacedExisting };
|
|
880
|
+
}
|
|
881
|
+
function recordOwnedGenerated(report, targetAbsPath, targetRepoRoot, content, generatedMarker, rel) {
|
|
882
|
+
const { outcome, replacedExisting } = writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker);
|
|
883
|
+
record(report, outcome, rel);
|
|
884
|
+
if (replacedExisting)
|
|
885
|
+
report.replacedPaths.push(rel);
|
|
886
|
+
}
|
|
887
|
+
function recordOwned(report, targetAbsPath, targetRepoRoot, content, rel) {
|
|
888
|
+
const existedBefore = existsSync3(targetAbsPath);
|
|
889
|
+
const outcome = writeOwned(targetAbsPath, targetRepoRoot, content);
|
|
890
|
+
record(report, outcome, rel);
|
|
891
|
+
if (outcome === "written" && existedBefore)
|
|
892
|
+
report.replacedPaths.push(rel);
|
|
893
|
+
}
|
|
711
894
|
function record(report, outcome, rel) {
|
|
712
895
|
const bucket = {
|
|
713
896
|
written: report.writtenPaths,
|
|
@@ -772,6 +955,17 @@ function generateSettingsJson() {
|
|
|
772
955
|
]
|
|
773
956
|
}
|
|
774
957
|
],
|
|
958
|
+
Stop: [
|
|
959
|
+
{
|
|
960
|
+
hooks: [
|
|
961
|
+
{
|
|
962
|
+
type: "command",
|
|
963
|
+
command: "bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
964
|
+
timeout: 60
|
|
965
|
+
}
|
|
966
|
+
]
|
|
967
|
+
}
|
|
968
|
+
],
|
|
775
969
|
SubagentStop: [
|
|
776
970
|
{
|
|
777
971
|
hooks: [
|
|
@@ -782,18 +976,30 @@ function generateSettingsJson() {
|
|
|
782
976
|
}
|
|
783
977
|
]
|
|
784
978
|
}
|
|
979
|
+
],
|
|
980
|
+
SessionEnd: [
|
|
981
|
+
{
|
|
982
|
+
hooks: [
|
|
983
|
+
{
|
|
984
|
+
type: "command",
|
|
985
|
+
command: "bash ./.claude/hooks/session-end-marker.sh",
|
|
986
|
+
timeout: 5
|
|
987
|
+
}
|
|
988
|
+
]
|
|
989
|
+
}
|
|
785
990
|
]
|
|
786
991
|
}
|
|
787
992
|
};
|
|
788
993
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
789
994
|
}
|
|
790
995
|
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
996
|
+
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
791
997
|
function generateGuardRunnerWrapper() {
|
|
792
998
|
return `#!/usr/bin/env bash
|
|
793
|
-
|
|
999
|
+
${GENERATED_GUARD_RUNNER_HEADER} the Halfcycle installer.
|
|
794
1000
|
#
|
|
795
1001
|
# Loads this engagement's credentials so the guard runner has its GUARD_SERVICE_*
|
|
796
|
-
# values when Claude Code fires PostToolUse / SubagentStop, then exec's the
|
|
1002
|
+
# values when Claude Code fires PostToolUse / Stop / SubagentStop, then exec's the
|
|
797
1003
|
# SELF-CONTAINED binary vendored at ${VENDORED_BIN_REL} \u2014 a path inside this
|
|
798
1004
|
# project, so the hook resolves in any checkout of it.
|
|
799
1005
|
#
|
|
@@ -825,15 +1031,28 @@ fi
|
|
|
825
1031
|
exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
|
|
826
1032
|
`;
|
|
827
1033
|
}
|
|
1034
|
+
var GENERATED_SESSION_START_HEADER = "# Session-start marker \u2014 generated by";
|
|
1035
|
+
var SESSION_ID_FROM_HOOK_INPUT_SH = `SESSION_ID="$(printf '%s' "$INPUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);process.stdout.write(typeof j.session_id==="string"?j.session_id:"")}catch{process.stdout.write("")}})' 2>/dev/null || true)"`;
|
|
828
1036
|
function generateSessionStartMarker() {
|
|
829
1037
|
return `#!/usr/bin/env bash
|
|
830
|
-
|
|
831
|
-
#
|
|
832
|
-
#
|
|
1038
|
+
${GENERATED_SESSION_START_HEADER} the Halfcycle installer.
|
|
1039
|
+
# Records the git HEAD sha this session starts from, in a file named after this
|
|
1040
|
+
# session, so the stop-time check can compare what the session did against where
|
|
1041
|
+
# it began instead of only against the latest commit. The session id comes in on
|
|
1042
|
+
# stdin with the hook input; without one, the older session-less name is written
|
|
1043
|
+
# and the stop-time check compares against the latest commit as it used to.
|
|
1044
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
833
1045
|
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
834
1046
|
if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
835
|
-
HASH="$(
|
|
836
|
-
|
|
1047
|
+
HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
1048
|
+
${SESSION_ID_FROM_HOOK_INPUT_SH}
|
|
1049
|
+
MARKER_DIR="\${TMPDIR:-/tmp}"
|
|
1050
|
+
MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
1051
|
+
if [ -n "$SESSION_ID" ]; then
|
|
1052
|
+
MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref"
|
|
1053
|
+
else
|
|
1054
|
+
MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}.ref"
|
|
1055
|
+
fi
|
|
837
1056
|
git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
|
|
838
1057
|
|
|
839
1058
|
# ---------------------------------------------------------------------------
|
|
@@ -896,9 +1115,41 @@ ${engagementResolutionShell()}
|
|
|
896
1115
|
exit 0
|
|
897
1116
|
`;
|
|
898
1117
|
}
|
|
1118
|
+
var GENERATED_SESSION_END_HEADER = "# Session-end marker cleanup \u2014 generated by";
|
|
1119
|
+
function generateSessionEndMarker() {
|
|
1120
|
+
return `#!/usr/bin/env bash
|
|
1121
|
+
${GENERATED_SESSION_END_HEADER} the Halfcycle installer.
|
|
1122
|
+
# Removes this session's marker file (and the evaluation-state file beside it)
|
|
1123
|
+
# written at session start. Deletion happens HERE and not at stop time: the stop
|
|
1124
|
+
# hook runs at the end of every turn, and deleting the marker there would throw
|
|
1125
|
+
# away the session's starting point for every later turn.
|
|
1126
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
1127
|
+
|
|
1128
|
+
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
1129
|
+
if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
1130
|
+
HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
1131
|
+
|
|
1132
|
+
# The session id arrives on stdin as part of the hook payload. Empty when absent.
|
|
1133
|
+
${SESSION_ID_FROM_HOOK_INPUT_SH}
|
|
1134
|
+
|
|
1135
|
+
MARKER_DIR="\${TMPDIR:-/tmp}"
|
|
1136
|
+
MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
1137
|
+
|
|
1138
|
+
# Exact names only \u2014 never a wildcard. A pattern would match another session
|
|
1139
|
+
# running in this same repository and delete the point it started from.
|
|
1140
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.ref" 2>/dev/null || true
|
|
1141
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.eval" 2>/dev/null || true
|
|
1142
|
+
if [ -n "$SESSION_ID" ]; then
|
|
1143
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref" 2>/dev/null || true
|
|
1144
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.eval" 2>/dev/null || true
|
|
1145
|
+
fi
|
|
1146
|
+
exit 0
|
|
1147
|
+
`;
|
|
1148
|
+
}
|
|
1149
|
+
var GENERATED_USER_PROMPT_REMINDER_HEADER = "# UserPromptSubmit house-rule reminder \u2014 generated by";
|
|
899
1150
|
function generateUserPromptReminderHook() {
|
|
900
1151
|
return `#!/usr/bin/env bash
|
|
901
|
-
|
|
1152
|
+
${GENERATED_USER_PROMPT_REMINDER_HEADER} the Halfcycle installer.
|
|
902
1153
|
#
|
|
903
1154
|
# Injects ONE plain-text sentence into context on every prompt (F-2(c),
|
|
904
1155
|
# W8-T-05b) \u2014 the per-turn twin of the house rule W8-T-05a put in the client's
|
|
@@ -996,17 +1247,31 @@ function generateCiStanza() {
|
|
|
996
1247
|
#
|
|
997
1248
|
# The job prints the diff base it used and how many files it evaluated, on every
|
|
998
1249
|
# run. If that line says 0 files on a commit that changed something, the base is
|
|
999
|
-
# wrong \u2014 set HALFCYCLE_DIFF_BASE in the env block
|
|
1000
|
-
# (on a GitHub push event, \${{ github.event.before }}
|
|
1250
|
+
# wrong \u2014 set HALFCYCLE_DIFF_BASE in the env block OF YOUR COPY, beside the two
|
|
1251
|
+
# secrets, to name it explicitly (on a GitHub push event, \${{ github.event.before }}
|
|
1252
|
+
# is the right value).
|
|
1253
|
+
#
|
|
1254
|
+
# EDIT YOUR COPY, NOT THIS FILE. This one is regenerated by the installer and your
|
|
1255
|
+
# changes to it would be replaced the next time you run \`npx halfcycle\`. It is
|
|
1256
|
+
# also inert where it sits: no CI system reads this path. Copy the job above into
|
|
1257
|
+
# your own workflow and change it there.
|
|
1001
1258
|
`;
|
|
1002
1259
|
}
|
|
1003
1260
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
1004
1261
|
var MCP_SERVER_KEY = "halfcycle";
|
|
1005
1262
|
var MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
1006
1263
|
var MCP_HEADERS_HELPER_COMMAND = `/bin/sh -c 'd=$(pwd); while [ ! -f "$d/${MCP_HEADERS_HELPER_REL}" ] && [ "$d" != / ]; do d=$(dirname "$d"); done; [ -f "$d/${MCP_HEADERS_HELPER_REL}" ] || { echo "halfcycle: ${MCP_HEADERS_HELPER_REL} not found at or above $(pwd); re-run npx halfcycle" >&2; exit 1; }; exec /bin/sh "$d/${MCP_HEADERS_HELPER_REL}"'`;
|
|
1264
|
+
var GENERATED_MCP_HEADERS_HEADER = "# Halfcycle MCP connection headers \u2014 generated by";
|
|
1265
|
+
var OWNED_GENERATED_HEADERS = {
|
|
1266
|
+
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
1267
|
+
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
1268
|
+
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
1269
|
+
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER,
|
|
1270
|
+
".halfcycle/mcp-headers.sh": GENERATED_MCP_HEADERS_HEADER
|
|
1271
|
+
};
|
|
1007
1272
|
function generateMcpHeadersHelper() {
|
|
1008
1273
|
return `#!/bin/sh
|
|
1009
|
-
|
|
1274
|
+
${GENERATED_MCP_HEADERS_HEADER} the Halfcycle installer.
|
|
1010
1275
|
#
|
|
1011
1276
|
# Claude Code runs this at MCP connection time, from the session's cwd, and merges
|
|
1012
1277
|
# its stdout into the connection headers for the "halfcycle" server in .mcp.json.
|
|
@@ -1191,7 +1456,7 @@ function writeCrewRoster(targetRepoRoot, report) {
|
|
|
1191
1456
|
const rendered = `${JSON.stringify(doc, null, 2)}
|
|
1192
1457
|
`;
|
|
1193
1458
|
const crewPath = join5(targetRepoRoot, ".halfcycle", "crew.json");
|
|
1194
|
-
|
|
1459
|
+
recordOwned(report, crewPath, targetRepoRoot, rendered, ".halfcycle/crew.json");
|
|
1195
1460
|
}
|
|
1196
1461
|
function readBundlePin(targetRepoRoot) {
|
|
1197
1462
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
@@ -1305,7 +1570,8 @@ async function install(options) {
|
|
|
1305
1570
|
writtenPaths: [],
|
|
1306
1571
|
skippedPaths: [],
|
|
1307
1572
|
mergedPaths: [],
|
|
1308
|
-
collidedPaths: []
|
|
1573
|
+
collidedPaths: [],
|
|
1574
|
+
replacedPaths: []
|
|
1309
1575
|
};
|
|
1310
1576
|
copyManifestCommands(manifest, targetRepo, report);
|
|
1311
1577
|
const capturedDir = join5(targetRepo, "test", "fixtures", "captured");
|
|
@@ -1318,7 +1584,7 @@ async function install(options) {
|
|
|
1318
1584
|
}
|
|
1319
1585
|
const vendoredBinSrc = resolveVendoredBinary();
|
|
1320
1586
|
const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
|
|
1321
|
-
|
|
1587
|
+
recordOwned(report, vendoredBinDest, targetRepo, readFileSync4(vendoredBinSrc, "utf-8"), ".halfcycle/bin/bin.bundle.mjs");
|
|
1322
1588
|
const settingsPath = join5(targetRepo, ".claude", "settings.json");
|
|
1323
1589
|
const settingsPreexisted = existsSync3(settingsPath);
|
|
1324
1590
|
const generatedSettings = JSON.parse(generateSettingsJson());
|
|
@@ -1330,17 +1596,19 @@ async function install(options) {
|
|
|
1330
1596
|
(settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
1331
1597
|
for (const [name, content] of [
|
|
1332
1598
|
["guard-runner.sh", generateGuardRunnerWrapper()],
|
|
1333
|
-
["session-
|
|
1334
|
-
["user-prompt-reminder.sh", generateUserPromptReminderHook()]
|
|
1599
|
+
["session-end-marker.sh", generateSessionEndMarker()],
|
|
1600
|
+
["user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
1601
|
+
["session-start-marker.sh", generateSessionStartMarker()]
|
|
1335
1602
|
]) {
|
|
1603
|
+
const rel = `.claude/hooks/${name}`;
|
|
1336
1604
|
const hookPath = join5(targetRepo, ".claude", "hooks", name);
|
|
1337
|
-
|
|
1605
|
+
recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
|
|
1338
1606
|
}
|
|
1339
1607
|
const ciStanzaPath = join5(targetRepo, ".halfcycle", "ci-stanza.yml");
|
|
1340
|
-
|
|
1608
|
+
recordOwned(report, ciStanzaPath, targetRepo, generateCiStanza(), ".halfcycle/ci-stanza.yml");
|
|
1341
1609
|
if (credential) {
|
|
1342
1610
|
const helperPath = join5(targetRepo, MCP_HEADERS_HELPER_REL);
|
|
1343
|
-
|
|
1611
|
+
recordOwnedGenerated(report, helperPath, targetRepo, generateMcpHeadersHelper(), OWNED_GENERATED_HEADERS[MCP_HEADERS_HELPER_REL], MCP_HEADERS_HELPER_REL);
|
|
1344
1612
|
const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
|
|
1345
1613
|
const existingMcp = existsSync3(mcpPath) ? readFileSync4(mcpPath, "utf-8") : null;
|
|
1346
1614
|
const mcpContent = generateMcpRegistration(existingMcp, credential.mcpUrl);
|
|
@@ -1394,6 +1662,7 @@ async function install(options) {
|
|
|
1394
1662
|
skippedPaths: report.skippedPaths,
|
|
1395
1663
|
mergedPaths: report.mergedPaths,
|
|
1396
1664
|
collidedPaths: report.collidedPaths,
|
|
1665
|
+
replacedPaths: report.replacedPaths,
|
|
1397
1666
|
bootstrapScanRan: scanResult.ran,
|
|
1398
1667
|
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null
|
|
1399
1668
|
};
|