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/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
- { callsign: "ADA", group: "builder", discipline: "implementation", tier: "deep" },
161
- { callsign: "OTTO", group: "builder", discipline: "implementation", tier: "deep" },
162
- { callsign: "MILO", group: "builder", discipline: "implementation", tier: "standard" },
163
- { callsign: "NOVA", group: "builder", discipline: "implementation", tier: "standard" },
164
- { callsign: "KAI", group: "builder", discipline: "implementation", tier: "standard" },
165
- { callsign: "HUGO", group: "builder", discipline: "infrastructure", tier: "standard" },
166
- { callsign: "WREN", group: "builder", discipline: "refactor", tier: "fast" },
167
- { callsign: "PIP", group: "builder", discipline: "refactor", tier: "fast" },
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
- { callsign: "IRIS", group: "builder", discipline: "feature spec", tier: "deep" },
170
- { callsign: "JUNO", group: "builder", discipline: "feature spec", tier: "standard" },
171
- { callsign: "LEX", group: "builder", discipline: "context & docs", tier: "fast" },
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
- { callsign: "VERA", group: "reviewer", discipline: "spec consistency", tier: "deep" },
174
- { callsign: "ODIN", group: "reviewer", discipline: "cross-spec audit", tier: "deep" },
175
- { callsign: "CASS", group: "reviewer", discipline: "code review", tier: "deep" },
176
- { callsign: "THEO", group: "reviewer", discipline: "code review", tier: "standard" },
177
- { callsign: "ECHO", group: "reviewer", discipline: "test quality", tier: "standard" },
178
- { callsign: "MAVIS", group: "reviewer", discipline: "suites & coverage", tier: "standard" },
179
- { callsign: "ZARA", group: "reviewer", discipline: "smoke & walk aid", tier: "standard" },
180
- { callsign: "NORA", group: "reviewer", discipline: "evidence & provenance", tier: "fast" },
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
- { callsign: "ARGUS", group: "always-on", discipline: "guard \xB7 every diff", tier: "standard" },
183
- { callsign: "ATLAS", group: "always-on", discipline: "coordinator", tier: "deep" }
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
- # Governance hook wrapper \u2014 generated by the Halfcycle installer.
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
- # Session-start marker \u2014 generated by the Halfcycle installer.
831
- # Writes the current git HEAD SHA to a temp file so SubagentStop can
832
- # diff against session-start rather than just HEAD at stop time.
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="$(echo -n "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
836
- MARKER_FILE="\${TMPDIR%/}/halfcycle-session-\${HASH}.ref"
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
- # UserPromptSubmit house-rule reminder \u2014 generated by the Halfcycle installer.
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 above to name it explicitly
1000
- # (on a GitHub push event, \${{ github.event.before }} is the right value).
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
- # Halfcycle MCP connection headers \u2014 generated by the Halfcycle installer.
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
- record(report, writeOwned(crewPath, targetRepoRoot, rendered), ".halfcycle/crew.json");
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
- record(report, writeOwned(vendoredBinDest, targetRepo, readFileSync4(vendoredBinSrc, "utf-8")), ".halfcycle/bin/bin.bundle.mjs");
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-start-marker.sh", generateSessionStartMarker()],
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
- record(report, writeCollisionSafe(hookPath, targetRepo, content), `.claude/hooks/${name}`);
1605
+ recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
1338
1606
  }
1339
1607
  const ciStanzaPath = join5(targetRepo, ".halfcycle", "ci-stanza.yml");
1340
- record(report, writeCollisionSafe(ciStanzaPath, targetRepo, generateCiStanza()), ".halfcycle/ci-stanza.yml");
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
- record(report, writeCollisionSafe(helperPath, targetRepo, generateMcpHeadersHelper()), MCP_HEADERS_HELPER_REL);
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
  };