halfcycle 0.3.22 → 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 +316 -41
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
|
|
5
5
|
"commands": [
|
|
6
6
|
{
|
package/dist/bin.js
CHANGED
|
@@ -280,6 +280,23 @@ import { z as z5 } from "zod";
|
|
|
280
280
|
var crewGroupSchema = z5.enum(["builder", "reviewer", "always-on"]);
|
|
281
281
|
var crewTierSchema = z5.enum(["deep", "standard", "fast"]);
|
|
282
282
|
var CREW_DISCIPLINE_MAX = 64;
|
|
283
|
+
var crewFaceHeadSchema = z5.enum(["sq", "sqr", "rnd", "dome", "hex"]);
|
|
284
|
+
var crewFaceAntennaSchema = z5.enum(["none", "stalk", "twin", "dish"]);
|
|
285
|
+
var crewFaceEyesSchema = z5.enum(["dots", "bars", "visor", "cyclops"]);
|
|
286
|
+
var crewFaceMouthSchema = z5.enum(["line", "grid", "dots", "wave"]);
|
|
287
|
+
var crewFaceSchema = z5.object({
|
|
288
|
+
// The outline of the head. Members who do the same kind of work share one, so a
|
|
289
|
+
// team is recognisable before any name is read.
|
|
290
|
+
head: crewFaceHeadSchema,
|
|
291
|
+
// What sits on top of the head, if anything.
|
|
292
|
+
antenna: crewFaceAntennaSchema,
|
|
293
|
+
// The eyes.
|
|
294
|
+
eyes: crewFaceEyesSchema,
|
|
295
|
+
// The mouth.
|
|
296
|
+
mouth: crewFaceMouthSchema,
|
|
297
|
+
// Whether this member is drawn with ears.
|
|
298
|
+
ears: z5.boolean()
|
|
299
|
+
}).strict();
|
|
283
300
|
var crewMemberSchema = z5.object({
|
|
284
301
|
// The agent's name. The same twenty-one on every project, so a name is a
|
|
285
302
|
// vocabulary a returning client already knows.
|
|
@@ -291,34 +308,163 @@ var crewMemberSchema = z5.object({
|
|
|
291
308
|
discipline: z5.string().min(1).max(CREW_DISCIPLINE_MAX),
|
|
292
309
|
// How much thinking this member brings. There is no field naming the supplier
|
|
293
310
|
// behind it, on this shape or on any other.
|
|
294
|
-
tier: crewTierSchema
|
|
311
|
+
tier: crewTierSchema,
|
|
312
|
+
// How this member's face is drawn. The same face on every project, so a
|
|
313
|
+
// returning client recognises a member before reading the name.
|
|
314
|
+
face: crewFaceSchema
|
|
295
315
|
}).strict();
|
|
296
316
|
var CREW_ROSTER = [
|
|
297
317
|
// ── builders: code ──────────────────────────────────────────────────────────
|
|
298
|
-
{
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
{
|
|
318
|
+
{
|
|
319
|
+
callsign: "ADA",
|
|
320
|
+
group: "builder",
|
|
321
|
+
discipline: "implementation",
|
|
322
|
+
tier: "deep",
|
|
323
|
+
face: { head: "sq", antenna: "stalk", eyes: "dots", mouth: "line", ears: false }
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
callsign: "OTTO",
|
|
327
|
+
group: "builder",
|
|
328
|
+
discipline: "implementation",
|
|
329
|
+
tier: "deep",
|
|
330
|
+
face: { head: "sq", antenna: "twin", eyes: "bars", mouth: "grid", ears: true }
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
callsign: "MILO",
|
|
334
|
+
group: "builder",
|
|
335
|
+
discipline: "implementation",
|
|
336
|
+
tier: "standard",
|
|
337
|
+
face: { head: "sqr", antenna: "stalk", eyes: "dots", mouth: "dots", ears: false }
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
callsign: "NOVA",
|
|
341
|
+
group: "builder",
|
|
342
|
+
discipline: "implementation",
|
|
343
|
+
tier: "standard",
|
|
344
|
+
face: { head: "sq", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
callsign: "KAI",
|
|
348
|
+
group: "builder",
|
|
349
|
+
discipline: "implementation",
|
|
350
|
+
tier: "standard",
|
|
351
|
+
face: { head: "sqr", antenna: "none", eyes: "bars", mouth: "line", ears: true }
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
callsign: "HUGO",
|
|
355
|
+
group: "builder",
|
|
356
|
+
discipline: "infrastructure",
|
|
357
|
+
tier: "standard",
|
|
358
|
+
face: { head: "sq", antenna: "twin", eyes: "dots", mouth: "grid", ears: true }
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
callsign: "WREN",
|
|
362
|
+
group: "builder",
|
|
363
|
+
discipline: "refactor",
|
|
364
|
+
tier: "fast",
|
|
365
|
+
face: { head: "sqr", antenna: "stalk", eyes: "bars", mouth: "wave", ears: false }
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
callsign: "PIP",
|
|
369
|
+
group: "builder",
|
|
370
|
+
discipline: "refactor",
|
|
371
|
+
tier: "fast",
|
|
372
|
+
face: { head: "sqr", antenna: "none", eyes: "dots", mouth: "dots", ears: false }
|
|
373
|
+
},
|
|
306
374
|
// ── builders: specs and docs ────────────────────────────────────────────────
|
|
307
|
-
{
|
|
308
|
-
|
|
309
|
-
|
|
375
|
+
{
|
|
376
|
+
callsign: "IRIS",
|
|
377
|
+
group: "builder",
|
|
378
|
+
discipline: "feature spec",
|
|
379
|
+
tier: "deep",
|
|
380
|
+
face: { head: "dome", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
callsign: "JUNO",
|
|
384
|
+
group: "builder",
|
|
385
|
+
discipline: "feature spec",
|
|
386
|
+
tier: "standard",
|
|
387
|
+
face: { head: "dome", antenna: "none", eyes: "dots", mouth: "wave", ears: false }
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
callsign: "LEX",
|
|
391
|
+
group: "builder",
|
|
392
|
+
discipline: "context & docs",
|
|
393
|
+
tier: "fast",
|
|
394
|
+
face: { head: "dome", antenna: "twin", eyes: "bars", mouth: "dots", ears: false }
|
|
395
|
+
},
|
|
310
396
|
// ── reviewers ───────────────────────────────────────────────────────────────
|
|
311
|
-
{
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
{
|
|
397
|
+
{
|
|
398
|
+
callsign: "VERA",
|
|
399
|
+
group: "reviewer",
|
|
400
|
+
discipline: "spec consistency",
|
|
401
|
+
tier: "deep",
|
|
402
|
+
face: { head: "rnd", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
callsign: "ODIN",
|
|
406
|
+
group: "reviewer",
|
|
407
|
+
discipline: "cross-spec audit",
|
|
408
|
+
tier: "deep",
|
|
409
|
+
face: { head: "rnd", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
callsign: "CASS",
|
|
413
|
+
group: "reviewer",
|
|
414
|
+
discipline: "code review",
|
|
415
|
+
tier: "deep",
|
|
416
|
+
face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "grid", ears: true }
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
callsign: "THEO",
|
|
420
|
+
group: "reviewer",
|
|
421
|
+
discipline: "code review",
|
|
422
|
+
tier: "standard",
|
|
423
|
+
face: { head: "rnd", antenna: "twin", eyes: "bars", mouth: "line", ears: false }
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
callsign: "ECHO",
|
|
427
|
+
group: "reviewer",
|
|
428
|
+
discipline: "test quality",
|
|
429
|
+
tier: "standard",
|
|
430
|
+
face: { head: "rnd", antenna: "stalk", eyes: "dots", mouth: "wave", ears: false }
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
callsign: "MAVIS",
|
|
434
|
+
group: "reviewer",
|
|
435
|
+
discipline: "suites & coverage",
|
|
436
|
+
tier: "standard",
|
|
437
|
+
face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "dots", ears: false }
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
callsign: "ZARA",
|
|
441
|
+
group: "reviewer",
|
|
442
|
+
discipline: "smoke & walk aid",
|
|
443
|
+
tier: "standard",
|
|
444
|
+
face: { head: "rnd", antenna: "dish", eyes: "bars", mouth: "wave", ears: false }
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
callsign: "NORA",
|
|
448
|
+
group: "reviewer",
|
|
449
|
+
discipline: "evidence & provenance",
|
|
450
|
+
tier: "fast",
|
|
451
|
+
face: { head: "rnd", antenna: "twin", eyes: "dots", mouth: "line", ears: false }
|
|
452
|
+
},
|
|
319
453
|
// ── always on: continuous, takes no task ────────────────────────────────────
|
|
320
|
-
{
|
|
321
|
-
|
|
454
|
+
{
|
|
455
|
+
callsign: "ARGUS",
|
|
456
|
+
group: "always-on",
|
|
457
|
+
discipline: "guard \xB7 every diff",
|
|
458
|
+
tier: "standard",
|
|
459
|
+
face: { head: "hex", antenna: "dish", eyes: "visor", mouth: "grid", ears: true }
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
callsign: "ATLAS",
|
|
463
|
+
group: "always-on",
|
|
464
|
+
discipline: "coordinator",
|
|
465
|
+
tier: "deep",
|
|
466
|
+
face: { head: "hex", antenna: "twin", eyes: "cyclops", mouth: "line", ears: true }
|
|
467
|
+
}
|
|
322
468
|
];
|
|
323
469
|
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
324
470
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
@@ -1044,11 +1190,15 @@ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
|
|
|
1044
1190
|
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
1045
1191
|
return "written";
|
|
1046
1192
|
}
|
|
1193
|
+
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
1047
1194
|
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
1048
1195
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
1049
1196
|
if (!isAllowlisted(rel)) {
|
|
1050
1197
|
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
1051
1198
|
}
|
|
1199
|
+
if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
|
|
1200
|
+
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.`);
|
|
1201
|
+
}
|
|
1052
1202
|
if (existsSync3(targetAbsPath) && readFileSync5(targetAbsPath, "utf-8") === content) {
|
|
1053
1203
|
return "skipped";
|
|
1054
1204
|
}
|
|
@@ -1056,6 +1206,39 @@ function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
|
1056
1206
|
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
1057
1207
|
return "written";
|
|
1058
1208
|
}
|
|
1209
|
+
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
1210
|
+
function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
1211
|
+
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
1212
|
+
if (!isAllowlisted(rel)) {
|
|
1213
|
+
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
1214
|
+
}
|
|
1215
|
+
let replacedExisting = false;
|
|
1216
|
+
if (existsSync3(targetAbsPath)) {
|
|
1217
|
+
const current = readFileSync5(targetAbsPath, "utf-8");
|
|
1218
|
+
if (current === content)
|
|
1219
|
+
return { outcome: "skipped", replacedExisting: false };
|
|
1220
|
+
const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
|
|
1221
|
+
if (!headerLine.startsWith(generatedMarker))
|
|
1222
|
+
return { outcome: "collided", replacedExisting: false };
|
|
1223
|
+
replacedExisting = true;
|
|
1224
|
+
}
|
|
1225
|
+
mkdirSync4(dirname2(targetAbsPath), { recursive: true });
|
|
1226
|
+
writeFileSync4(targetAbsPath, content, "utf-8");
|
|
1227
|
+
return { outcome: "written", replacedExisting };
|
|
1228
|
+
}
|
|
1229
|
+
function recordOwnedGenerated(report, targetAbsPath, targetRepoRoot, content, generatedMarker, rel) {
|
|
1230
|
+
const { outcome, replacedExisting } = writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker);
|
|
1231
|
+
record(report, outcome, rel);
|
|
1232
|
+
if (replacedExisting)
|
|
1233
|
+
report.replacedPaths.push(rel);
|
|
1234
|
+
}
|
|
1235
|
+
function recordOwned(report, targetAbsPath, targetRepoRoot, content, rel) {
|
|
1236
|
+
const existedBefore = existsSync3(targetAbsPath);
|
|
1237
|
+
const outcome = writeOwned(targetAbsPath, targetRepoRoot, content);
|
|
1238
|
+
record(report, outcome, rel);
|
|
1239
|
+
if (outcome === "written" && existedBefore)
|
|
1240
|
+
report.replacedPaths.push(rel);
|
|
1241
|
+
}
|
|
1059
1242
|
function record(report, outcome, rel) {
|
|
1060
1243
|
const bucket = {
|
|
1061
1244
|
written: report.writtenPaths,
|
|
@@ -1120,6 +1303,17 @@ function generateSettingsJson() {
|
|
|
1120
1303
|
]
|
|
1121
1304
|
}
|
|
1122
1305
|
],
|
|
1306
|
+
Stop: [
|
|
1307
|
+
{
|
|
1308
|
+
hooks: [
|
|
1309
|
+
{
|
|
1310
|
+
type: "command",
|
|
1311
|
+
command: "bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
1312
|
+
timeout: 60
|
|
1313
|
+
}
|
|
1314
|
+
]
|
|
1315
|
+
}
|
|
1316
|
+
],
|
|
1123
1317
|
SubagentStop: [
|
|
1124
1318
|
{
|
|
1125
1319
|
hooks: [
|
|
@@ -1130,18 +1324,30 @@ function generateSettingsJson() {
|
|
|
1130
1324
|
}
|
|
1131
1325
|
]
|
|
1132
1326
|
}
|
|
1327
|
+
],
|
|
1328
|
+
SessionEnd: [
|
|
1329
|
+
{
|
|
1330
|
+
hooks: [
|
|
1331
|
+
{
|
|
1332
|
+
type: "command",
|
|
1333
|
+
command: "bash ./.claude/hooks/session-end-marker.sh",
|
|
1334
|
+
timeout: 5
|
|
1335
|
+
}
|
|
1336
|
+
]
|
|
1337
|
+
}
|
|
1133
1338
|
]
|
|
1134
1339
|
}
|
|
1135
1340
|
};
|
|
1136
1341
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
1137
1342
|
}
|
|
1138
1343
|
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
1344
|
+
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
1139
1345
|
function generateGuardRunnerWrapper() {
|
|
1140
1346
|
return `#!/usr/bin/env bash
|
|
1141
|
-
|
|
1347
|
+
${GENERATED_GUARD_RUNNER_HEADER} the Halfcycle installer.
|
|
1142
1348
|
#
|
|
1143
1349
|
# Loads this engagement's credentials so the guard runner has its GUARD_SERVICE_*
|
|
1144
|
-
# values when Claude Code fires PostToolUse / SubagentStop, then exec's the
|
|
1350
|
+
# values when Claude Code fires PostToolUse / Stop / SubagentStop, then exec's the
|
|
1145
1351
|
# SELF-CONTAINED binary vendored at ${VENDORED_BIN_REL} \u2014 a path inside this
|
|
1146
1352
|
# project, so the hook resolves in any checkout of it.
|
|
1147
1353
|
#
|
|
@@ -1173,15 +1379,28 @@ fi
|
|
|
1173
1379
|
exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
|
|
1174
1380
|
`;
|
|
1175
1381
|
}
|
|
1382
|
+
var GENERATED_SESSION_START_HEADER = "# Session-start marker \u2014 generated by";
|
|
1383
|
+
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)"`;
|
|
1176
1384
|
function generateSessionStartMarker() {
|
|
1177
1385
|
return `#!/usr/bin/env bash
|
|
1178
|
-
|
|
1179
|
-
#
|
|
1180
|
-
#
|
|
1386
|
+
${GENERATED_SESSION_START_HEADER} the Halfcycle installer.
|
|
1387
|
+
# Records the git HEAD sha this session starts from, in a file named after this
|
|
1388
|
+
# session, so the stop-time check can compare what the session did against where
|
|
1389
|
+
# it began instead of only against the latest commit. The session id comes in on
|
|
1390
|
+
# stdin with the hook input; without one, the older session-less name is written
|
|
1391
|
+
# and the stop-time check compares against the latest commit as it used to.
|
|
1392
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
1181
1393
|
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
1182
1394
|
if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
1183
|
-
HASH="$(
|
|
1184
|
-
|
|
1395
|
+
HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
1396
|
+
${SESSION_ID_FROM_HOOK_INPUT_SH}
|
|
1397
|
+
MARKER_DIR="\${TMPDIR:-/tmp}"
|
|
1398
|
+
MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
1399
|
+
if [ -n "$SESSION_ID" ]; then
|
|
1400
|
+
MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref"
|
|
1401
|
+
else
|
|
1402
|
+
MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}.ref"
|
|
1403
|
+
fi
|
|
1185
1404
|
git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
|
|
1186
1405
|
|
|
1187
1406
|
# ---------------------------------------------------------------------------
|
|
@@ -1244,9 +1463,41 @@ ${engagementResolutionShell()}
|
|
|
1244
1463
|
exit 0
|
|
1245
1464
|
`;
|
|
1246
1465
|
}
|
|
1466
|
+
var GENERATED_SESSION_END_HEADER = "# Session-end marker cleanup \u2014 generated by";
|
|
1467
|
+
function generateSessionEndMarker() {
|
|
1468
|
+
return `#!/usr/bin/env bash
|
|
1469
|
+
${GENERATED_SESSION_END_HEADER} the Halfcycle installer.
|
|
1470
|
+
# Removes this session's marker file (and the evaluation-state file beside it)
|
|
1471
|
+
# written at session start. Deletion happens HERE and not at stop time: the stop
|
|
1472
|
+
# hook runs at the end of every turn, and deleting the marker there would throw
|
|
1473
|
+
# away the session's starting point for every later turn.
|
|
1474
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
1475
|
+
|
|
1476
|
+
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
1477
|
+
if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
1478
|
+
HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
1479
|
+
|
|
1480
|
+
# The session id arrives on stdin as part of the hook payload. Empty when absent.
|
|
1481
|
+
${SESSION_ID_FROM_HOOK_INPUT_SH}
|
|
1482
|
+
|
|
1483
|
+
MARKER_DIR="\${TMPDIR:-/tmp}"
|
|
1484
|
+
MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
1485
|
+
|
|
1486
|
+
# Exact names only \u2014 never a wildcard. A pattern would match another session
|
|
1487
|
+
# running in this same repository and delete the point it started from.
|
|
1488
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.ref" 2>/dev/null || true
|
|
1489
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.eval" 2>/dev/null || true
|
|
1490
|
+
if [ -n "$SESSION_ID" ]; then
|
|
1491
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref" 2>/dev/null || true
|
|
1492
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.eval" 2>/dev/null || true
|
|
1493
|
+
fi
|
|
1494
|
+
exit 0
|
|
1495
|
+
`;
|
|
1496
|
+
}
|
|
1497
|
+
var GENERATED_USER_PROMPT_REMINDER_HEADER = "# UserPromptSubmit house-rule reminder \u2014 generated by";
|
|
1247
1498
|
function generateUserPromptReminderHook() {
|
|
1248
1499
|
return `#!/usr/bin/env bash
|
|
1249
|
-
|
|
1500
|
+
${GENERATED_USER_PROMPT_REMINDER_HEADER} the Halfcycle installer.
|
|
1250
1501
|
#
|
|
1251
1502
|
# Injects ONE plain-text sentence into context on every prompt (F-2(c),
|
|
1252
1503
|
# W8-T-05b) \u2014 the per-turn twin of the house rule W8-T-05a put in the client's
|
|
@@ -1344,17 +1595,31 @@ function generateCiStanza() {
|
|
|
1344
1595
|
#
|
|
1345
1596
|
# The job prints the diff base it used and how many files it evaluated, on every
|
|
1346
1597
|
# run. If that line says 0 files on a commit that changed something, the base is
|
|
1347
|
-
# wrong \u2014 set HALFCYCLE_DIFF_BASE in the env block
|
|
1348
|
-
# (on a GitHub push event, \${{ github.event.before }}
|
|
1598
|
+
# wrong \u2014 set HALFCYCLE_DIFF_BASE in the env block OF YOUR COPY, beside the two
|
|
1599
|
+
# secrets, to name it explicitly (on a GitHub push event, \${{ github.event.before }}
|
|
1600
|
+
# is the right value).
|
|
1601
|
+
#
|
|
1602
|
+
# EDIT YOUR COPY, NOT THIS FILE. This one is regenerated by the installer and your
|
|
1603
|
+
# changes to it would be replaced the next time you run \`npx halfcycle\`. It is
|
|
1604
|
+
# also inert where it sits: no CI system reads this path. Copy the job above into
|
|
1605
|
+
# your own workflow and change it there.
|
|
1349
1606
|
`;
|
|
1350
1607
|
}
|
|
1351
1608
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
1352
1609
|
var MCP_SERVER_KEY = "halfcycle";
|
|
1353
1610
|
var MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
1354
1611
|
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}"'`;
|
|
1612
|
+
var GENERATED_MCP_HEADERS_HEADER = "# Halfcycle MCP connection headers \u2014 generated by";
|
|
1613
|
+
var OWNED_GENERATED_HEADERS = {
|
|
1614
|
+
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
1615
|
+
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
1616
|
+
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
1617
|
+
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER,
|
|
1618
|
+
".halfcycle/mcp-headers.sh": GENERATED_MCP_HEADERS_HEADER
|
|
1619
|
+
};
|
|
1355
1620
|
function generateMcpHeadersHelper() {
|
|
1356
1621
|
return `#!/bin/sh
|
|
1357
|
-
|
|
1622
|
+
${GENERATED_MCP_HEADERS_HEADER} the Halfcycle installer.
|
|
1358
1623
|
#
|
|
1359
1624
|
# Claude Code runs this at MCP connection time, from the session's cwd, and merges
|
|
1360
1625
|
# its stdout into the connection headers for the "halfcycle" server in .mcp.json.
|
|
@@ -1539,7 +1804,7 @@ function writeCrewRoster(targetRepoRoot, report) {
|
|
|
1539
1804
|
const rendered = `${JSON.stringify(doc, null, 2)}
|
|
1540
1805
|
`;
|
|
1541
1806
|
const crewPath = join5(targetRepoRoot, ".halfcycle", "crew.json");
|
|
1542
|
-
|
|
1807
|
+
recordOwned(report, crewPath, targetRepoRoot, rendered, ".halfcycle/crew.json");
|
|
1543
1808
|
}
|
|
1544
1809
|
function readBundlePin(targetRepoRoot) {
|
|
1545
1810
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
@@ -1690,7 +1955,8 @@ async function install(options) {
|
|
|
1690
1955
|
writtenPaths: [],
|
|
1691
1956
|
skippedPaths: [],
|
|
1692
1957
|
mergedPaths: [],
|
|
1693
|
-
collidedPaths: []
|
|
1958
|
+
collidedPaths: [],
|
|
1959
|
+
replacedPaths: []
|
|
1694
1960
|
};
|
|
1695
1961
|
copyManifestCommands(manifest, targetRepo, report);
|
|
1696
1962
|
const capturedDir = join5(targetRepo, "test", "fixtures", "captured");
|
|
@@ -1703,7 +1969,7 @@ async function install(options) {
|
|
|
1703
1969
|
}
|
|
1704
1970
|
const vendoredBinSrc = resolveVendoredBinary();
|
|
1705
1971
|
const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
|
|
1706
|
-
|
|
1972
|
+
recordOwned(report, vendoredBinDest, targetRepo, readFileSync5(vendoredBinSrc, "utf-8"), ".halfcycle/bin/bin.bundle.mjs");
|
|
1707
1973
|
const settingsPath = join5(targetRepo, ".claude", "settings.json");
|
|
1708
1974
|
const settingsPreexisted = existsSync3(settingsPath);
|
|
1709
1975
|
const generatedSettings = JSON.parse(generateSettingsJson());
|
|
@@ -1715,17 +1981,19 @@ async function install(options) {
|
|
|
1715
1981
|
(settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
1716
1982
|
for (const [name, content] of [
|
|
1717
1983
|
["guard-runner.sh", generateGuardRunnerWrapper()],
|
|
1718
|
-
["session-
|
|
1719
|
-
["user-prompt-reminder.sh", generateUserPromptReminderHook()]
|
|
1984
|
+
["session-end-marker.sh", generateSessionEndMarker()],
|
|
1985
|
+
["user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
1986
|
+
["session-start-marker.sh", generateSessionStartMarker()]
|
|
1720
1987
|
]) {
|
|
1988
|
+
const rel = `.claude/hooks/${name}`;
|
|
1721
1989
|
const hookPath = join5(targetRepo, ".claude", "hooks", name);
|
|
1722
|
-
|
|
1990
|
+
recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
|
|
1723
1991
|
}
|
|
1724
1992
|
const ciStanzaPath = join5(targetRepo, ".halfcycle", "ci-stanza.yml");
|
|
1725
|
-
|
|
1993
|
+
recordOwned(report, ciStanzaPath, targetRepo, generateCiStanza(), ".halfcycle/ci-stanza.yml");
|
|
1726
1994
|
if (credential) {
|
|
1727
1995
|
const helperPath = join5(targetRepo, MCP_HEADERS_HELPER_REL);
|
|
1728
|
-
|
|
1996
|
+
recordOwnedGenerated(report, helperPath, targetRepo, generateMcpHeadersHelper(), OWNED_GENERATED_HEADERS[MCP_HEADERS_HELPER_REL], MCP_HEADERS_HELPER_REL);
|
|
1729
1997
|
const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
|
|
1730
1998
|
const existingMcp = existsSync3(mcpPath) ? readFileSync5(mcpPath, "utf-8") : null;
|
|
1731
1999
|
const mcpContent = generateMcpRegistration(existingMcp, credential.mcpUrl);
|
|
@@ -1779,6 +2047,7 @@ async function install(options) {
|
|
|
1779
2047
|
skippedPaths: report.skippedPaths,
|
|
1780
2048
|
mergedPaths: report.mergedPaths,
|
|
1781
2049
|
collidedPaths: report.collidedPaths,
|
|
2050
|
+
replacedPaths: report.replacedPaths,
|
|
1782
2051
|
bootstrapScanRan: scanResult.ran,
|
|
1783
2052
|
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null
|
|
1784
2053
|
};
|
|
@@ -4007,6 +4276,12 @@ ${USAGE}`);
|
|
|
4007
4276
|
}
|
|
4008
4277
|
if (result.collidedPaths.length > 0) {
|
|
4009
4278
|
process.stdout.write(`[halfcycle] Collided (a name you already use \u2014 NOT overwritten): ${result.collidedPaths.join(", ")}
|
|
4279
|
+
`);
|
|
4280
|
+
}
|
|
4281
|
+
if (result.replacedPaths.length > 0) {
|
|
4282
|
+
process.stdout.write(`[halfcycle] Updated (files Halfcycle wrote earlier, now replaced): ${result.replacedPaths.join(", ")}
|
|
4283
|
+
`);
|
|
4284
|
+
process.stdout.write(`[halfcycle] If you had changed one of those: a change you had COMMITTED is still in your git history ("git log -p -- <path>"). A change you had not committed is gone \u2014 this command overwrote the file.
|
|
4010
4285
|
`);
|
|
4011
4286
|
}
|
|
4012
4287
|
if (result.legacyCredentialKept !== null) {
|