halfcycle 0.3.30 → 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.
@@ -14652,6 +14652,75 @@ function parseAccountStoreText(raw) {
14652
14652
  // ../core/dist/ci-audience.js
14653
14653
  var HALFCYCLE_OIDC_AUDIENCE = "https://halfcycle.ai";
14654
14654
 
14655
+ // ../core/dist/hook-wiring.js
14656
+ var SETTINGS_REL = ".claude/settings.json";
14657
+ var LOCAL_SETTINGS_REL = ".claude/settings.local.json";
14658
+ var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
14659
+ var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
14660
+ var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
14661
+ var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
14662
+ var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
14663
+ var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
14664
+ var HALFCYCLE_HOOK_WIRING = [
14665
+ {
14666
+ event: "SessionStart",
14667
+ matcher: null,
14668
+ timeout: 5,
14669
+ command: `bash ./${SESSION_START_MARKER_REL}`,
14670
+ script: SESSION_START_MARKER_REL
14671
+ },
14672
+ {
14673
+ event: "SessionStart",
14674
+ matcher: null,
14675
+ timeout: 10,
14676
+ command: `bash ./${GUARD_RUNNER_REL} refresh-credential`,
14677
+ script: GUARD_RUNNER_REL
14678
+ },
14679
+ {
14680
+ event: "UserPromptSubmit",
14681
+ matcher: null,
14682
+ timeout: 5,
14683
+ command: `bash ./${USER_PROMPT_REMINDER_REL}`,
14684
+ script: USER_PROMPT_REMINDER_REL
14685
+ },
14686
+ {
14687
+ event: "PostToolUse",
14688
+ matcher: WRITE_CAPABLE_TOOLS.join("|"),
14689
+ timeout: 60,
14690
+ command: `bash ./${GUARD_RUNNER_REL} hook post-tool-use`,
14691
+ script: GUARD_RUNNER_REL
14692
+ },
14693
+ {
14694
+ event: "Stop",
14695
+ matcher: null,
14696
+ timeout: 60,
14697
+ command: `bash ./${GUARD_RUNNER_REL} hook stop`,
14698
+ script: GUARD_RUNNER_REL
14699
+ },
14700
+ {
14701
+ event: "SubagentStop",
14702
+ matcher: null,
14703
+ timeout: 60,
14704
+ command: `bash ./${GUARD_RUNNER_REL} hook subagent-stop`,
14705
+ script: GUARD_RUNNER_REL
14706
+ },
14707
+ {
14708
+ event: "SessionEnd",
14709
+ matcher: null,
14710
+ timeout: 5,
14711
+ command: `bash ./${SESSION_END_MARKER_REL}`,
14712
+ script: SESSION_END_MARKER_REL
14713
+ }
14714
+ ];
14715
+ var RETIRED_HOOK_COMMANDS = [];
14716
+ var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
14717
+ ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
14718
+ ...RETIRED_HOOK_COMMANDS
14719
+ ]);
14720
+ var HALFCYCLE_HOOK_WIRING_PATHS = [
14721
+ .../* @__PURE__ */ new Set([SETTINGS_REL, ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])
14722
+ ];
14723
+
14655
14724
  // dist/control-origin.js
14656
14725
  var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
14657
14726
  function resolveControlOrigin(env) {
@@ -14809,6 +14878,25 @@ var CLIENT_CAPABILITY_VOCABULARY = Object.keys(diffMetadataSchema.shape).filter(
14809
14878
  function isClientCapability(value) {
14810
14879
  return CLIENT_CAPABILITY_VOCABULARY.includes(value);
14811
14880
  }
14881
+ var seatHookSchema = external_exports.object({
14882
+ event: external_exports.string(),
14883
+ matcher: external_exports.string().nullable(),
14884
+ command: external_exports.string(),
14885
+ timeout: external_exports.number().nullable()
14886
+ }).strict();
14887
+ var seatSettingsFileSchema = external_exports.object({
14888
+ path: external_exports.string(),
14889
+ state: external_exports.enum(["read", "absent", "unreadable"]),
14890
+ hooks: external_exports.array(seatHookSchema)
14891
+ }).strict();
14892
+ var seatPathSchema = external_exports.object({
14893
+ path: external_exports.string(),
14894
+ state: external_exports.enum(["clean", "modified", "added", "deleted", "untracked", "ignored", "absent", "unknown"])
14895
+ }).strict();
14896
+ var seatConfigurationSchema = external_exports.object({
14897
+ settingsFiles: external_exports.array(seatSettingsFileSchema),
14898
+ paths: external_exports.array(seatPathSchema)
14899
+ }).strict();
14812
14900
  var evaluationRequestSchema = external_exports.object({
14813
14901
  engagementId: external_exports.string(),
14814
14902
  changeSet: changeSetSchema,
@@ -14829,7 +14917,17 @@ var evaluationRequestSchema = external_exports.object({
14829
14917
  *
14830
14918
  * Omitting the field means "unknown": every check runs as it always has.
14831
14919
  */
14832
- repoHasDeployFiles: external_exports.boolean().optional()
14920
+ repoHasDeployFiles: external_exports.boolean().optional(),
14921
+ /**
14922
+ * The hooks this tool installed that this project's own settings files
14923
+ * register, each with the event, matcher and timeout it is registered
14924
+ * under, and whether each file those hooks rely on is committed and
14925
+ * unchanged. Sent as found: the service decides what it means, and
14926
+ * nothing about it is sent back.
14927
+ *
14928
+ * Omitting the field means "not observed", never "set up".
14929
+ */
14930
+ seatConfiguration: seatConfigurationSchema.optional()
14833
14931
  }).strict();
14834
14932
 
14835
14933
  // ../events/dist/result.js
@@ -15147,21 +15245,49 @@ function emitBlock(reason) {
15147
15245
  process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
15148
15246
  process.exit(0);
15149
15247
  }
15150
- function emitWarning(text) {
15151
- process.stdout.write(text + "\n");
15248
+ function nonBlockingReply(event, text) {
15249
+ if (event === "PostToolUse") {
15250
+ return JSON.stringify({
15251
+ systemMessage: text,
15252
+ hookSpecificOutput: { hookEventName: event, additionalContext: text }
15253
+ });
15254
+ }
15255
+ return JSON.stringify({ systemMessage: text });
15152
15256
  }
15153
- function emitInfraWarning(message) {
15154
- process.stdout.write(`[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. Session continues (fail-open).
15257
+ function hookReply(event) {
15258
+ const queued = [];
15259
+ return {
15260
+ notice(text) {
15261
+ queued.push(text);
15262
+ },
15263
+ block(reason) {
15264
+ const carried = queued.splice(0);
15265
+ return emitBlock(carried.length === 0 ? reason : [reason, ...carried].join("\n"));
15266
+ },
15267
+ flush() {
15268
+ if (queued.length === 0)
15269
+ return;
15270
+ const text = queued.splice(0).join("\n");
15271
+ const code = process.exitCode;
15272
+ if (code !== void 0 && code !== null && Number(code) !== 0) {
15273
+ writeSync(2, `${text}
15155
15274
  `);
15156
- }
15157
- function emitLocalFault(message) {
15158
- process.stdout.write(`[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).
15275
+ return;
15276
+ }
15277
+ process.stdout.write(`${nonBlockingReply(event, text)}
15159
15278
  `);
15279
+ }
15280
+ };
15160
15281
  }
15161
- function emitContractFault(message) {
15162
- process.stdout.write(`[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
15163
- This is not a service outage: the guard service answered. The suspect is this repository's own installed halfcycle runner, which is out of date with what the service now sends. Update it: run "npx halfcycle" in this repository, then retry. Session continues (fail-open).
15164
- `);
15282
+ function infraWarningText(message) {
15283
+ return `[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. Session continues (fail-open).`;
15284
+ }
15285
+ function localFaultText(message) {
15286
+ return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
15287
+ }
15288
+ function contractFaultText(message) {
15289
+ return `[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
15290
+ This is not a service outage: the guard service answered. The suspect is this repository's own installed halfcycle runner, which is out of date with what the service now sends. Update it: run "npx halfcycle" in this repository, then retry. Session continues (fail-open).`;
15165
15291
  }
15166
15292
  var UNCONFIGURED_EXIT_TOOL_USE = 2;
15167
15293
  var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
@@ -15480,8 +15606,12 @@ async function attempt(url2, token, request, fetchFn, retry) {
15480
15606
  import { spawnSync } from "node:child_process";
15481
15607
  import { readFileSync } from "node:fs";
15482
15608
  import { relative } from "node:path";
15483
- function git(args2, cwd) {
15484
- const result = spawnSync("git", args2, { cwd, encoding: "utf-8" });
15609
+ function git(args2, cwd, timeoutMs) {
15610
+ const result = spawnSync("git", args2, {
15611
+ cwd,
15612
+ encoding: "utf-8",
15613
+ ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
15614
+ });
15485
15615
  if (result.error) {
15486
15616
  return { ok: false, message: `git error: ${result.error.message}` };
15487
15617
  }
@@ -15625,6 +15755,20 @@ function refExists(ref, repoRoot) {
15625
15755
  const result = git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], repoRoot);
15626
15756
  return result.ok && result.value.trim().length > 0;
15627
15757
  }
15758
+ var WIRING_STATUS_TIMEOUT_MS = 2e3;
15759
+ function getWiringStatus(pathspecs, cwd) {
15760
+ return git([
15761
+ "--no-optional-locks",
15762
+ "status",
15763
+ "--porcelain=v1",
15764
+ "-z",
15765
+ "--no-renames",
15766
+ "--ignored=traditional",
15767
+ "--untracked-files=all",
15768
+ "--",
15769
+ ...pathspecs
15770
+ ], cwd, WIRING_STATUS_TIMEOUT_MS);
15771
+ }
15628
15772
 
15629
15773
  // dist/screen/secret-shape.js
15630
15774
  var SECRET_SHAPE_PATTERNS = [
@@ -16586,13 +16730,14 @@ function deriveClientCapabilities(changeSet) {
16586
16730
  }
16587
16731
  return [...seen];
16588
16732
  }
16589
- function buildEvaluationRequest(engagementId, changeSet, context) {
16733
+ function buildEvaluationRequest(engagementId, changeSet, context, seatConfiguration) {
16590
16734
  const hasDeployFiles = repoHasDeployFiles(context);
16591
16735
  return {
16592
16736
  engagementId,
16593
16737
  changeSet,
16594
16738
  clientCapabilities: deriveClientCapabilities(changeSet),
16595
- ...hasDeployFiles === void 0 ? {} : { repoHasDeployFiles: hasDeployFiles }
16739
+ ...hasDeployFiles === void 0 ? {} : { repoHasDeployFiles: hasDeployFiles },
16740
+ ...seatConfiguration === void 0 ? {} : { seatConfiguration }
16596
16741
  };
16597
16742
  }
16598
16743
  function produceExcerpts(diffContent, descriptors, caps) {
@@ -17209,9 +17354,165 @@ async function evaluateWithRefresh(opts) {
17209
17354
  return { result: retry, token: refreshed.token, remedy: HOOK_REMEDY };
17210
17355
  }
17211
17356
 
17357
+ // dist/seat-configuration.js
17358
+ import { existsSync as existsSync2, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
17359
+ import { homedir as homedir3 } from "node:os";
17360
+ import { join as join6, relative as relative3, resolve } from "node:path";
17361
+ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
17362
+ try {
17363
+ const facts = read(repoRoot);
17364
+ if (facts === void 0)
17365
+ return void 0;
17366
+ const parsed = seatConfigurationSchema.safeParse(facts);
17367
+ return parsed.success ? parsed.data : void 0;
17368
+ } catch {
17369
+ return void 0;
17370
+ }
17371
+ }
17372
+ function readSeatConfiguration(repoRoot) {
17373
+ const given = resolve(process.env["CLAUDE_PROJECT_DIR"] || process.cwd());
17374
+ const projectDir = realpathSync2.native(given);
17375
+ const home = homedir3();
17376
+ let realHome = home;
17377
+ try {
17378
+ realHome = realpathSync2.native(home);
17379
+ } catch {
17380
+ realHome = home;
17381
+ }
17382
+ if (projectDir === home || given === home || projectDir === realHome)
17383
+ return void 0;
17384
+ return {
17385
+ settingsFiles: [readSettingsFile(projectDir, SETTINGS_REL), readSettingsFile(projectDir, LOCAL_SETTINGS_REL)],
17386
+ paths: readPathStates(projectDir, repoRoot)
17387
+ };
17388
+ }
17389
+ function isPlainObject2(value) {
17390
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17391
+ }
17392
+ function readSettingsFile(projectDir, rel) {
17393
+ let raw;
17394
+ try {
17395
+ raw = readFileSync6(join6(projectDir, rel), "utf-8");
17396
+ } catch (err) {
17397
+ const code = err.code;
17398
+ const state = code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
17399
+ return { path: rel, state, hooks: [] };
17400
+ }
17401
+ let parsed;
17402
+ try {
17403
+ parsed = JSON.parse(raw);
17404
+ } catch {
17405
+ return { path: rel, state: "unreadable", hooks: [] };
17406
+ }
17407
+ if (!isPlainObject2(parsed))
17408
+ return { path: rel, state: "unreadable", hooks: [] };
17409
+ return { path: rel, state: "read", hooks: ourHooks(parsed["hooks"]) };
17410
+ }
17411
+ function ourHooks(hooks) {
17412
+ if (!isPlainObject2(hooks))
17413
+ return [];
17414
+ const rows = [];
17415
+ const seen = /* @__PURE__ */ new Set();
17416
+ for (const [event, entries] of Object.entries(hooks)) {
17417
+ if (!Array.isArray(entries))
17418
+ continue;
17419
+ for (const entry of entries) {
17420
+ if (!isPlainObject2(entry))
17421
+ continue;
17422
+ let matcher = null;
17423
+ if (Object.hasOwn(entry, "matcher")) {
17424
+ if (typeof entry["matcher"] !== "string")
17425
+ continue;
17426
+ matcher = entry["matcher"];
17427
+ }
17428
+ const commands = entry["hooks"];
17429
+ if (!Array.isArray(commands))
17430
+ continue;
17431
+ for (const hook of commands) {
17432
+ if (!isPlainObject2(hook))
17433
+ continue;
17434
+ const command = hook["command"];
17435
+ if (typeof command !== "string" || !HALFCYCLE_HOOK_COMMANDS.has(command))
17436
+ continue;
17437
+ let timeout = null;
17438
+ if (Object.hasOwn(hook, "timeout")) {
17439
+ const value = hook["timeout"];
17440
+ if (typeof value !== "number" || !Number.isFinite(value))
17441
+ continue;
17442
+ timeout = value;
17443
+ }
17444
+ const key = JSON.stringify([event, matcher, command, timeout]);
17445
+ if (seen.has(key))
17446
+ continue;
17447
+ seen.add(key);
17448
+ rows.push({ event, matcher, command, timeout });
17449
+ }
17450
+ }
17451
+ }
17452
+ return rows;
17453
+ }
17454
+ function readPathStates(projectDir, repoRoot) {
17455
+ const unknown2 = () => HALFCYCLE_HOOK_WIRING_PATHS.map((path) => ({ path, state: "unknown" }));
17456
+ let projectTop = projectDir;
17457
+ if (repoRoot !== projectDir) {
17458
+ const top = getRepoRoot(projectDir);
17459
+ if (!top.ok)
17460
+ return unknown2();
17461
+ projectTop = top.value;
17462
+ }
17463
+ const prefix = relative3(projectTop, projectDir);
17464
+ const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
17465
+ if (!status.ok)
17466
+ return unknown2();
17467
+ const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync2(join6(projectDir, rel)));
17468
+ return mapped ?? unknown2();
17469
+ }
17470
+ function mapWiringStatus(output, prefix, paths, onDisk) {
17471
+ const byGitPath = /* @__PURE__ */ new Map();
17472
+ for (const rel of paths)
17473
+ byGitPath.set(prefix === "" ? rel : `${prefix}/${rel}`, rel);
17474
+ const codes = /* @__PURE__ */ new Map();
17475
+ if (output !== "") {
17476
+ if (!output.endsWith("\0"))
17477
+ return null;
17478
+ for (const record2 of output.slice(0, -1).split("\0")) {
17479
+ if (record2.length < 4 || record2[2] !== " ")
17480
+ return null;
17481
+ const rel = byGitPath.get(record2.slice(3));
17482
+ if (rel === void 0)
17483
+ return null;
17484
+ const xy = record2.slice(0, 2);
17485
+ const existing = codes.get(rel);
17486
+ if (existing)
17487
+ existing.push(xy);
17488
+ else
17489
+ codes.set(rel, [xy]);
17490
+ }
17491
+ }
17492
+ return paths.map((path) => {
17493
+ const xys = codes.get(path);
17494
+ if (!xys)
17495
+ return { path, state: onDisk(path) ? "clean" : "absent" };
17496
+ if (xys.some((xy) => xy.includes("D")))
17497
+ return { path, state: "deleted" };
17498
+ if (xys.includes("??"))
17499
+ return { path, state: "untracked" };
17500
+ if (xys.includes("!!"))
17501
+ return { path, state: "ignored" };
17502
+ if (xys.some((xy) => xy[0] === "A"))
17503
+ return { path, state: "added" };
17504
+ return { path, state: "modified" };
17505
+ });
17506
+ }
17507
+
17212
17508
  // dist/hooks/post-tool-use.js
17213
17509
  var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
17214
- async function runPostToolUse() {
17510
+ async function runPostToolUse(readSeat = readSeatConfiguration) {
17511
+ const reply = hookReply("PostToolUse");
17512
+ await evaluateEdit(reply, readSeat);
17513
+ reply.flush();
17514
+ }
17515
+ async function evaluateEdit(reply, readSeat) {
17215
17516
  let input;
17216
17517
  try {
17217
17518
  const raw = await readStdin();
@@ -17228,7 +17529,7 @@ async function runPostToolUse() {
17228
17529
  return;
17229
17530
  const repoRootResult = getRepoRoot(process.cwd());
17230
17531
  if (!repoRootResult.ok) {
17231
- emitLocalFault(`Could not find git repo root: ${repoRootResult.message}`);
17532
+ reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17232
17533
  return;
17233
17534
  }
17234
17535
  const repoRoot = repoRootResult.value;
@@ -17249,11 +17550,12 @@ async function runPostToolUse() {
17249
17550
  return;
17250
17551
  }
17251
17552
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
17553
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
17252
17554
  const evaluation = await evaluateWithRefresh({
17253
17555
  guardServiceUrl,
17254
17556
  token: guardServiceToken,
17255
17557
  engagementId: guardEngagementId,
17256
- request: buildEvaluationRequest(guardEngagementId, changeSet, context)
17558
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
17257
17559
  });
17258
17560
  const clientResult = evaluation.result;
17259
17561
  if (!clientResult.ok) {
@@ -17280,10 +17582,10 @@ async function runPostToolUse() {
17280
17582
  return;
17281
17583
  }
17282
17584
  if (failure.kind === "contract") {
17283
- emitContractFault(failure.message);
17585
+ reply.notice(contractFaultText(failure.message));
17284
17586
  return;
17285
17587
  }
17286
- emitInfraWarning(failure.reason);
17588
+ reply.notice(infraWarningText(failure.reason));
17287
17589
  return;
17288
17590
  }
17289
17591
  const telemetryConfig = readTelemetryConfig();
@@ -17311,42 +17613,42 @@ async function runPostToolUse() {
17311
17613
  }, {
17312
17614
  warn: (msg) => process.stderr.write(msg + "\n")
17313
17615
  });
17314
- actOnEnvelope2(clientResult.envelope);
17616
+ actOnEnvelope2(clientResult.envelope, reply);
17315
17617
  }
17316
- function actOnEnvelope2(envelope) {
17618
+ function actOnEnvelope2(envelope, reply) {
17317
17619
  const notRun = notRunReport(envelope.notRun);
17318
17620
  const notRunBlock = notRun ? `
17319
17621
  ${notRun}` : "";
17320
17622
  if (envelope.guardsFired.length === 0) {
17321
17623
  if (notRun)
17322
- emitWarning(notRun);
17624
+ reply.notice(notRun);
17323
17625
  return;
17324
17626
  }
17325
17627
  if (envelope.blocking) {
17326
17628
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
17327
17629
  const reason = `Guard blocked: ${envelope.explanation}
17328
17630
  ${guardLines}${notRunBlock}`;
17329
- emitBlock(reason);
17631
+ reply.block(reason);
17330
17632
  } else {
17331
17633
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
17332
- emitWarning(`[Halfcycle] Guard warning: ${envelope.explanation}
17634
+ reply.notice(`[Halfcycle] Guard warning: ${envelope.explanation}
17333
17635
  ${guardLines}${notRunBlock}`);
17334
17636
  }
17335
17637
  }
17336
17638
  function readStdin() {
17337
- return new Promise((resolve, reject) => {
17639
+ return new Promise((resolve2, reject) => {
17338
17640
  const chunks = [];
17339
17641
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17340
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
17642
+ process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
17341
17643
  process.stdin.on("error", reject);
17342
17644
  });
17343
17645
  }
17344
17646
 
17345
17647
  // dist/hooks/marker.js
17346
17648
  import { createHash } from "node:crypto";
17347
- import { existsSync as existsSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17649
+ import { existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17348
17650
  import { tmpdir } from "node:os";
17349
- import { join as join6 } from "node:path";
17651
+ import { join as join7 } from "node:path";
17350
17652
  var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
17351
17653
  var MARKER_PREFIX = "halfcycle-session-";
17352
17654
  var MARKER_SUFFIX = ".ref";
@@ -17364,19 +17666,19 @@ function markerFileName(repoRoot, sessionId) {
17364
17666
  return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
17365
17667
  }
17366
17668
  function markerPath(repoRoot, sessionId) {
17367
- return join6(markerDir(), markerFileName(repoRoot, sessionId));
17669
+ return join7(markerDir(), markerFileName(repoRoot, sessionId));
17368
17670
  }
17369
17671
  function dedupePath(repoRoot, sessionId) {
17370
17672
  const hash2 = repoHash(repoRoot);
17371
17673
  const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17372
- return join6(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17674
+ return join7(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17373
17675
  }
17374
17676
  function readMarker(repoRoot, sessionId) {
17375
17677
  const path = markerPath(repoRoot, sessionId);
17376
- if (!existsSync2(path))
17678
+ if (!existsSync3(path))
17377
17679
  return null;
17378
17680
  try {
17379
- return readFileSync6(path, "utf-8").trim() || null;
17681
+ return readFileSync7(path, "utf-8").trim() || null;
17380
17682
  } catch {
17381
17683
  return null;
17382
17684
  }
@@ -17384,7 +17686,7 @@ function readMarker(repoRoot, sessionId) {
17384
17686
  function deleteMarker(repoRoot, sessionId) {
17385
17687
  for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17386
17688
  try {
17387
- if (existsSync2(path))
17689
+ if (existsSync3(path))
17388
17690
  unlinkSync3(path);
17389
17691
  } catch {
17390
17692
  }
@@ -17392,10 +17694,10 @@ function deleteMarker(repoRoot, sessionId) {
17392
17694
  }
17393
17695
  function readDedupe(repoRoot, sessionId) {
17394
17696
  const path = dedupePath(repoRoot, sessionId);
17395
- if (!existsSync2(path))
17697
+ if (!existsSync3(path))
17396
17698
  return null;
17397
17699
  try {
17398
- const parsed = JSON.parse(readFileSync6(path, "utf-8"));
17700
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
17399
17701
  if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
17400
17702
  return parsed;
17401
17703
  }
@@ -17437,10 +17739,10 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17437
17739
  continue;
17438
17740
  if (name === keepName)
17439
17741
  continue;
17440
- const full = join6(dir, name);
17742
+ const full = join7(dir, name);
17441
17743
  let unusable = false;
17442
17744
  try {
17443
- const ref = readFileSync6(full, "utf-8").trim();
17745
+ const ref = readFileSync7(full, "utf-8").trim();
17444
17746
  if (!ref || !refKnown(ref))
17445
17747
  unusable = true;
17446
17748
  } catch {
@@ -17464,6 +17766,10 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17464
17766
  }
17465
17767
 
17466
17768
  // dist/hooks/session-diff.js
17769
+ var STOP_HOOK_EVENT = {
17770
+ stop: "Stop",
17771
+ "subagent-stop": "SubagentStop"
17772
+ };
17467
17773
  function resolveDiffBase2(repoRoot, sessionId) {
17468
17774
  const ref = readMarker(repoRoot, sessionId);
17469
17775
  if (ref === null)
@@ -17494,10 +17800,15 @@ function collectDiff(repoRoot, base) {
17494
17800
  getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
17495
17801
  };
17496
17802
  }
17497
- async function runSessionDiff(input, kind) {
17803
+ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
17804
+ const reply = hookReply(STOP_HOOK_EVENT[kind]);
17805
+ await evaluateSession(input, kind, reply, readSeat);
17806
+ reply.flush();
17807
+ }
17808
+ async function evaluateSession(input, kind, reply, readSeat) {
17498
17809
  const repoRootResult = getRepoRoot(process.cwd());
17499
17810
  if (!repoRootResult.ok) {
17500
- emitLocalFault(`Could not find git repo root: ${repoRootResult.message}`);
17811
+ reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
17501
17812
  return;
17502
17813
  }
17503
17814
  const repoRoot = repoRootResult.value;
@@ -17507,7 +17818,7 @@ async function runSessionDiff(input, kind) {
17507
17818
  const diffBase = base.kind;
17508
17819
  const collected = collectDiff(repoRoot, base);
17509
17820
  if (!collected.ok) {
17510
- emitLocalFault(`Could not get session diff: ${collected.message}`);
17821
+ reply.notice(localFaultText(`Could not get session diff: ${collected.message}`));
17511
17822
  return;
17512
17823
  }
17513
17824
  const { getDiff } = collected;
@@ -17534,11 +17845,12 @@ async function runSessionDiff(input, kind) {
17534
17845
  return;
17535
17846
  }
17536
17847
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
17848
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
17537
17849
  const evaluation = await evaluateWithRefresh({
17538
17850
  guardServiceUrl,
17539
17851
  token: guardServiceToken,
17540
17852
  engagementId: guardEngagementId,
17541
- request: buildEvaluationRequest(guardEngagementId, changeSet, context)
17853
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
17542
17854
  });
17543
17855
  const clientResult = evaluation.result;
17544
17856
  if (!clientResult.ok) {
@@ -17566,10 +17878,10 @@ async function runSessionDiff(input, kind) {
17566
17878
  return;
17567
17879
  }
17568
17880
  if (failure.kind === "contract") {
17569
- emitContractFault(failure.message);
17881
+ reply.notice(contractFaultText(failure.message));
17570
17882
  return;
17571
17883
  }
17572
- emitInfraWarning(failure.reason);
17884
+ reply.notice(infraWarningText(failure.reason));
17573
17885
  return;
17574
17886
  }
17575
17887
  const telemetryConfig = readTelemetryConfig();
@@ -17595,28 +17907,28 @@ async function runSessionDiff(input, kind) {
17595
17907
  warn: (msg) => process.stderr.write(msg + "\n")
17596
17908
  });
17597
17909
  writeDedupe(repoRoot, sessionId, { base: baseKey, digest });
17598
- actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true);
17910
+ actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true, reply);
17599
17911
  }
17600
- function actOnEnvelope3(envelope, kind, neverBlock) {
17912
+ function actOnEnvelope3(envelope, kind, neverBlock, reply) {
17601
17913
  const notRun = notRunReport(envelope.notRun);
17602
17914
  const notRunBlock = notRun ? `
17603
17915
  ${notRun}` : "";
17604
17916
  if (envelope.guardsFired.length === 0) {
17605
17917
  if (notRun)
17606
- emitWarning(notRun);
17918
+ reply.notice(notRun);
17607
17919
  return;
17608
17920
  }
17609
17921
  const where = kind === "stop" ? "session stop" : "session stop";
17610
17922
  const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
17611
17923
  if (envelope.blocking && !neverBlock) {
17612
- emitBlock(`Guard blocked at ${where}: ${envelope.explanation}
17924
+ reply.block(`Guard blocked at ${where}: ${envelope.explanation}
17613
17925
  ${guardLines}${notRunBlock}`);
17614
17926
  } else if (envelope.blocking && neverBlock) {
17615
- emitWarning(`[Halfcycle] Guard would block at ${where} but the stop was already continued (stop_hook_active) \u2014 surfacing as a warning:
17927
+ reply.notice(`[Halfcycle] Guard would block at ${where} but the stop was already continued (stop_hook_active) \u2014 surfacing as a warning:
17616
17928
  ${envelope.explanation}
17617
17929
  ${guardLines}${notRunBlock}`);
17618
17930
  } else {
17619
- emitWarning(`[Halfcycle] Guard warning at ${where}: ${envelope.explanation}
17931
+ reply.notice(`[Halfcycle] Guard warning at ${where}: ${envelope.explanation}
17620
17932
  ${guardLines}${notRunBlock}`);
17621
17933
  }
17622
17934
  }
@@ -17636,9 +17948,9 @@ async function readHookStdin() {
17636
17948
  }
17637
17949
  }
17638
17950
  function readStdin2() {
17639
- return new Promise((resolve) => {
17951
+ return new Promise((resolve2) => {
17640
17952
  if (process.stdin.isTTY) {
17641
- resolve("");
17953
+ resolve2("");
17642
17954
  return;
17643
17955
  }
17644
17956
  const chunks = [];
@@ -17648,7 +17960,7 @@ function readStdin2() {
17648
17960
  return;
17649
17961
  settled = true;
17650
17962
  clearTimeout(timer);
17651
- resolve(Buffer.concat(chunks).toString("utf-8"));
17963
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
17652
17964
  };
17653
17965
  const timer = setTimeout(done, 2e3);
17654
17966
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -17670,17 +17982,17 @@ async function runStop() {
17670
17982
  }
17671
17983
 
17672
17984
  // dist/resolve-engagement.js
17673
- import { existsSync as existsSync3, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
17674
- import { join as join7, sep as sep2 } from "node:path";
17675
- import { homedir as homedir3 } from "node:os";
17985
+ import { existsSync as existsSync4, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
17986
+ import { join as join8, sep as sep2 } from "node:path";
17987
+ import { homedir as homedir4 } from "node:os";
17676
17988
  function defaultRegistryPath() {
17677
- return join7(homedir3(), ".halfcycle", "registry.json");
17989
+ return join8(homedir4(), ".halfcycle", "registry.json");
17678
17990
  }
17679
17991
  function readRegistry(registryPath = defaultRegistryPath()) {
17680
- if (!existsSync3(registryPath))
17992
+ if (!existsSync4(registryPath))
17681
17993
  return {};
17682
17994
  try {
17683
- const parsed = JSON.parse(readFileSync7(registryPath, "utf-8"));
17995
+ const parsed = JSON.parse(readFileSync8(registryPath, "utf-8"));
17684
17996
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
17685
17997
  return {};
17686
17998
  const out = {};
@@ -17695,7 +18007,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
17695
18007
  }
17696
18008
  function canonicalise(p) {
17697
18009
  try {
17698
- return realpathSync2(p);
18010
+ return realpathSync3(p);
17699
18011
  } catch {
17700
18012
  return p;
17701
18013
  }
@@ -17760,7 +18072,7 @@ async function runRefreshCredential(opts) {
17760
18072
  }
17761
18073
 
17762
18074
  // dist/capture.js
17763
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "node:fs";
18075
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync5 } from "node:fs";
17764
18076
  import { dirname as dirname4 } from "node:path";
17765
18077
  function runCapture(provider, endpoint, opts = {}) {
17766
18078
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -17828,7 +18140,7 @@ function readInputFile(fromFile) {
17828
18140
  if (!fromFile)
17829
18141
  return void 0;
17830
18142
  try {
17831
- return readFileSync8(fromFile, "utf-8");
18143
+ return readFileSync9(fromFile, "utf-8");
17832
18144
  } catch {
17833
18145
  return void 0;
17834
18146
  }
@@ -17923,10 +18235,10 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
17923
18235
  process.exit(1);
17924
18236
  }
17925
18237
  function readStdin3() {
17926
- return new Promise((resolve, reject) => {
18238
+ return new Promise((resolve2, reject) => {
17927
18239
  const chunks = [];
17928
18240
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17929
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
18241
+ process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
17930
18242
  process.stdin.on("error", reject);
17931
18243
  });
17932
18244
  }