halfcycle 0.3.31 → 0.3.33

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcycle",
3
- "version": "0.3.31",
3
+ "version": "0.3.33",
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
  {
@@ -14652,6 +14652,100 @@ 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
+
14724
+ // ../core/dist/secret-shape.js
14725
+ var SECRET_SHAPE_PATTERNS = [
14726
+ // Assignment to a credential-named identifier: `apiKey = "..."`, `secret: '...'`,
14727
+ // `password=...`, `token = ...`, `access_key: ...` — the value need not be quoted.
14728
+ /\b(?:api[_-]?key|secret|password|passwd|token|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer)\b\s*[:=]\s*\S+/i,
14729
+ // Connection strings that embed credentials: `scheme://user:pass@host`.
14730
+ /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]+@/i,
14731
+ // Bearer / authorization header values.
14732
+ /\bauthorization\b\s*[:=]\s*['"]?\s*bearer\s+\S+/i,
14733
+ // PEM / private-key block headers.
14734
+ /-----BEGIN\s+(?:RSA\s+|EC\s+|OPENSSH\s+|DSA\s+)?PRIVATE\s+KEY-----/,
14735
+ // JWT shape: three base64url segments separated by dots.
14736
+ /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/,
14737
+ // A long high-entropy hex/base64 run adjacent to a credential-ish word on the
14738
+ // same line — a raw key literal. Kept last (broadest, most likely false-drop).
14739
+ /\b(?:key|token|secret|credential)\b[\s\S]{0,40}[A-Za-z0-9+/_-]{32,}/i
14740
+ ];
14741
+ function looksLikeSecret(text) {
14742
+ for (const pattern of SECRET_SHAPE_PATTERNS) {
14743
+ if (pattern.test(text))
14744
+ return true;
14745
+ }
14746
+ return false;
14747
+ }
14748
+
14655
14749
  // dist/control-origin.js
14656
14750
  var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
14657
14751
  function resolveControlOrigin(env) {
@@ -14809,6 +14903,25 @@ var CLIENT_CAPABILITY_VOCABULARY = Object.keys(diffMetadataSchema.shape).filter(
14809
14903
  function isClientCapability(value) {
14810
14904
  return CLIENT_CAPABILITY_VOCABULARY.includes(value);
14811
14905
  }
14906
+ var seatHookSchema = external_exports.object({
14907
+ event: external_exports.string(),
14908
+ matcher: external_exports.string().nullable(),
14909
+ command: external_exports.string(),
14910
+ timeout: external_exports.number().nullable()
14911
+ }).strict();
14912
+ var seatSettingsFileSchema = external_exports.object({
14913
+ path: external_exports.string(),
14914
+ state: external_exports.enum(["read", "absent", "unreadable"]),
14915
+ hooks: external_exports.array(seatHookSchema)
14916
+ }).strict();
14917
+ var seatPathSchema = external_exports.object({
14918
+ path: external_exports.string(),
14919
+ state: external_exports.enum(["clean", "modified", "added", "deleted", "untracked", "ignored", "absent", "unknown"])
14920
+ }).strict();
14921
+ var seatConfigurationSchema = external_exports.object({
14922
+ settingsFiles: external_exports.array(seatSettingsFileSchema),
14923
+ paths: external_exports.array(seatPathSchema)
14924
+ }).strict();
14812
14925
  var evaluationRequestSchema = external_exports.object({
14813
14926
  engagementId: external_exports.string(),
14814
14927
  changeSet: changeSetSchema,
@@ -14829,7 +14942,17 @@ var evaluationRequestSchema = external_exports.object({
14829
14942
  *
14830
14943
  * Omitting the field means "unknown": every check runs as it always has.
14831
14944
  */
14832
- repoHasDeployFiles: external_exports.boolean().optional()
14945
+ repoHasDeployFiles: external_exports.boolean().optional(),
14946
+ /**
14947
+ * The hooks this tool installed that this project's own settings files
14948
+ * register, each with the event, matcher and timeout it is registered
14949
+ * under, and whether each file those hooks rely on is committed and
14950
+ * unchanged. Sent as found: the service decides what it means, and
14951
+ * nothing about it is sent back.
14952
+ *
14953
+ * Omitting the field means "not observed", never "set up".
14954
+ */
14955
+ seatConfiguration: seatConfigurationSchema.optional()
14833
14956
  }).strict();
14834
14957
 
14835
14958
  // ../events/dist/result.js
@@ -15071,8 +15194,16 @@ var stateRecordSchema = external_exports.discriminatedUnion("recordKind", [
15071
15194
  interventionRecordSchema
15072
15195
  ]);
15073
15196
 
15197
+ // ../events/dist/control-characters.js
15198
+ var CONTROL_CHARACTER_CLASS = "\\p{Cc}";
15199
+ var CONTROL_CHARACTER = new RegExp(CONTROL_CHARACTER_CLASS, "u");
15200
+ function hasControlCharacter(text) {
15201
+ return CONTROL_CHARACTER.test(text);
15202
+ }
15203
+
15074
15204
  // ../events/dist/phase-identity.js
15075
15205
  var PHASE_SEGMENT_PREFIX = "phase-";
15206
+ var PHASE_NAME_MAX_BYTES = 200;
15076
15207
  function isValidPhaseIdentity(identity) {
15077
15208
  if (typeof identity === "number")
15078
15209
  return Number.isInteger(identity) && identity >= 0;
@@ -15084,12 +15215,22 @@ function isValidPhaseIdentity(identity) {
15084
15215
  return false;
15085
15216
  if (identity.includes("\\"))
15086
15217
  return false;
15087
- if (identity.includes("\0"))
15088
- return false;
15089
15218
  if (identity.startsWith("."))
15090
15219
  return false;
15220
+ if (hasControlCharacter(identity))
15221
+ return false;
15222
+ if (utf8ByteLength(identity) > PHASE_NAME_MAX_BYTES)
15223
+ return false;
15091
15224
  return true;
15092
15225
  }
15226
+ function utf8ByteLength(text) {
15227
+ let bytes = 0;
15228
+ for (const ch of text) {
15229
+ const cp = ch.codePointAt(0);
15230
+ bytes += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
15231
+ }
15232
+ return bytes;
15233
+ }
15093
15234
  var InvalidPhaseIdentityError = class extends Error {
15094
15235
  /** The value that was refused, exactly as supplied. */
15095
15236
  identity;
@@ -15508,8 +15649,12 @@ async function attempt(url2, token, request, fetchFn, retry) {
15508
15649
  import { spawnSync } from "node:child_process";
15509
15650
  import { readFileSync } from "node:fs";
15510
15651
  import { relative } from "node:path";
15511
- function git(args2, cwd) {
15512
- const result = spawnSync("git", args2, { cwd, encoding: "utf-8" });
15652
+ function git(args2, cwd, timeoutMs) {
15653
+ const result = spawnSync("git", args2, {
15654
+ cwd,
15655
+ encoding: "utf-8",
15656
+ ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
15657
+ });
15513
15658
  if (result.error) {
15514
15659
  return { ok: false, message: `git error: ${result.error.message}` };
15515
15660
  }
@@ -15653,31 +15798,22 @@ function refExists(ref, repoRoot) {
15653
15798
  const result = git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], repoRoot);
15654
15799
  return result.ok && result.value.trim().length > 0;
15655
15800
  }
15801
+ var WIRING_STATUS_TIMEOUT_MS = 2e3;
15802
+ function getWiringStatus(pathspecs, cwd) {
15803
+ return git([
15804
+ "--no-optional-locks",
15805
+ "status",
15806
+ "--porcelain=v1",
15807
+ "-z",
15808
+ "--no-renames",
15809
+ "--ignored=traditional",
15810
+ "--untracked-files=all",
15811
+ "--",
15812
+ ...pathspecs
15813
+ ], cwd, WIRING_STATUS_TIMEOUT_MS);
15814
+ }
15656
15815
 
15657
15816
  // dist/screen/secret-shape.js
15658
- var SECRET_SHAPE_PATTERNS = [
15659
- // Assignment to a credential-named identifier: `apiKey = "..."`, `secret: '...'`,
15660
- // `password=...`, `token = ...`, `access_key: ...` — the value need not be quoted.
15661
- /\b(?:api[_-]?key|secret|password|passwd|token|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer)\b\s*[:=]\s*\S+/i,
15662
- // Connection strings that embed credentials: `scheme://user:pass@host`.
15663
- /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]+@/i,
15664
- // Bearer / authorization header values.
15665
- /\bauthorization\b\s*[:=]\s*['"]?\s*bearer\s+\S+/i,
15666
- // PEM / private-key block headers.
15667
- /-----BEGIN\s+(?:RSA\s+|EC\s+|OPENSSH\s+|DSA\s+)?PRIVATE\s+KEY-----/,
15668
- // JWT shape: three base64url segments separated by dots.
15669
- /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/,
15670
- // A long high-entropy hex/base64 run adjacent to a credential-ish word on the
15671
- // same line — a raw key literal. Kept last (broadest, most likely false-drop).
15672
- /\b(?:key|token|secret|credential)\b[\s\S]{0,40}[A-Za-z0-9+/_-]{32,}/i
15673
- ];
15674
- function looksLikeSecret(text) {
15675
- for (const pattern of SECRET_SHAPE_PATTERNS) {
15676
- if (pattern.test(text))
15677
- return true;
15678
- }
15679
- return false;
15680
- }
15681
15817
  function screenExcerpts(excerpts) {
15682
15818
  return excerpts.filter((e) => !looksLikeSecret(e.text));
15683
15819
  }
@@ -16614,13 +16750,14 @@ function deriveClientCapabilities(changeSet) {
16614
16750
  }
16615
16751
  return [...seen];
16616
16752
  }
16617
- function buildEvaluationRequest(engagementId, changeSet, context) {
16753
+ function buildEvaluationRequest(engagementId, changeSet, context, seatConfiguration) {
16618
16754
  const hasDeployFiles = repoHasDeployFiles(context);
16619
16755
  return {
16620
16756
  engagementId,
16621
16757
  changeSet,
16622
16758
  clientCapabilities: deriveClientCapabilities(changeSet),
16623
- ...hasDeployFiles === void 0 ? {} : { repoHasDeployFiles: hasDeployFiles }
16759
+ ...hasDeployFiles === void 0 ? {} : { repoHasDeployFiles: hasDeployFiles },
16760
+ ...seatConfiguration === void 0 ? {} : { seatConfiguration }
16624
16761
  };
16625
16762
  }
16626
16763
  function produceExcerpts(diffContent, descriptors, caps) {
@@ -17237,14 +17374,165 @@ async function evaluateWithRefresh(opts) {
17237
17374
  return { result: retry, token: refreshed.token, remedy: HOOK_REMEDY };
17238
17375
  }
17239
17376
 
17377
+ // dist/seat-configuration.js
17378
+ import { existsSync as existsSync2, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
17379
+ import { homedir as homedir3 } from "node:os";
17380
+ import { join as join6, relative as relative3, resolve } from "node:path";
17381
+ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
17382
+ try {
17383
+ const facts = read(repoRoot);
17384
+ if (facts === void 0)
17385
+ return void 0;
17386
+ const parsed = seatConfigurationSchema.safeParse(facts);
17387
+ return parsed.success ? parsed.data : void 0;
17388
+ } catch {
17389
+ return void 0;
17390
+ }
17391
+ }
17392
+ function readSeatConfiguration(repoRoot) {
17393
+ const given = resolve(process.env["CLAUDE_PROJECT_DIR"] || process.cwd());
17394
+ const projectDir = realpathSync2.native(given);
17395
+ const home = homedir3();
17396
+ let realHome = home;
17397
+ try {
17398
+ realHome = realpathSync2.native(home);
17399
+ } catch {
17400
+ realHome = home;
17401
+ }
17402
+ if (projectDir === home || given === home || projectDir === realHome)
17403
+ return void 0;
17404
+ return {
17405
+ settingsFiles: [readSettingsFile(projectDir, SETTINGS_REL), readSettingsFile(projectDir, LOCAL_SETTINGS_REL)],
17406
+ paths: readPathStates(projectDir, repoRoot)
17407
+ };
17408
+ }
17409
+ function isPlainObject2(value) {
17410
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17411
+ }
17412
+ function readSettingsFile(projectDir, rel) {
17413
+ let raw;
17414
+ try {
17415
+ raw = readFileSync6(join6(projectDir, rel), "utf-8");
17416
+ } catch (err) {
17417
+ const code = err.code;
17418
+ const state = code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
17419
+ return { path: rel, state, hooks: [] };
17420
+ }
17421
+ let parsed;
17422
+ try {
17423
+ parsed = JSON.parse(raw);
17424
+ } catch {
17425
+ return { path: rel, state: "unreadable", hooks: [] };
17426
+ }
17427
+ if (!isPlainObject2(parsed))
17428
+ return { path: rel, state: "unreadable", hooks: [] };
17429
+ return { path: rel, state: "read", hooks: ourHooks(parsed["hooks"]) };
17430
+ }
17431
+ function ourHooks(hooks) {
17432
+ if (!isPlainObject2(hooks))
17433
+ return [];
17434
+ const rows = [];
17435
+ const seen = /* @__PURE__ */ new Set();
17436
+ for (const [event, entries] of Object.entries(hooks)) {
17437
+ if (!Array.isArray(entries))
17438
+ continue;
17439
+ for (const entry of entries) {
17440
+ if (!isPlainObject2(entry))
17441
+ continue;
17442
+ let matcher = null;
17443
+ if (Object.hasOwn(entry, "matcher")) {
17444
+ if (typeof entry["matcher"] !== "string")
17445
+ continue;
17446
+ matcher = entry["matcher"];
17447
+ }
17448
+ const commands = entry["hooks"];
17449
+ if (!Array.isArray(commands))
17450
+ continue;
17451
+ for (const hook of commands) {
17452
+ if (!isPlainObject2(hook))
17453
+ continue;
17454
+ const command = hook["command"];
17455
+ if (typeof command !== "string" || !HALFCYCLE_HOOK_COMMANDS.has(command))
17456
+ continue;
17457
+ let timeout = null;
17458
+ if (Object.hasOwn(hook, "timeout")) {
17459
+ const value = hook["timeout"];
17460
+ if (typeof value !== "number" || !Number.isFinite(value))
17461
+ continue;
17462
+ timeout = value;
17463
+ }
17464
+ const key = JSON.stringify([event, matcher, command, timeout]);
17465
+ if (seen.has(key))
17466
+ continue;
17467
+ seen.add(key);
17468
+ rows.push({ event, matcher, command, timeout });
17469
+ }
17470
+ }
17471
+ }
17472
+ return rows;
17473
+ }
17474
+ function readPathStates(projectDir, repoRoot) {
17475
+ const unknown2 = () => HALFCYCLE_HOOK_WIRING_PATHS.map((path) => ({ path, state: "unknown" }));
17476
+ let projectTop = projectDir;
17477
+ if (repoRoot !== projectDir) {
17478
+ const top = getRepoRoot(projectDir);
17479
+ if (!top.ok)
17480
+ return unknown2();
17481
+ projectTop = top.value;
17482
+ }
17483
+ const prefix = relative3(projectTop, projectDir);
17484
+ const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
17485
+ if (!status.ok)
17486
+ return unknown2();
17487
+ const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync2(join6(projectDir, rel)));
17488
+ return mapped ?? unknown2();
17489
+ }
17490
+ function mapWiringStatus(output, prefix, paths, onDisk) {
17491
+ const byGitPath = /* @__PURE__ */ new Map();
17492
+ for (const rel of paths)
17493
+ byGitPath.set(prefix === "" ? rel : `${prefix}/${rel}`, rel);
17494
+ const codes = /* @__PURE__ */ new Map();
17495
+ if (output !== "") {
17496
+ if (!output.endsWith("\0"))
17497
+ return null;
17498
+ for (const record2 of output.slice(0, -1).split("\0")) {
17499
+ if (record2.length < 4 || record2[2] !== " ")
17500
+ return null;
17501
+ const rel = byGitPath.get(record2.slice(3));
17502
+ if (rel === void 0)
17503
+ return null;
17504
+ const xy = record2.slice(0, 2);
17505
+ const existing = codes.get(rel);
17506
+ if (existing)
17507
+ existing.push(xy);
17508
+ else
17509
+ codes.set(rel, [xy]);
17510
+ }
17511
+ }
17512
+ return paths.map((path) => {
17513
+ const xys = codes.get(path);
17514
+ if (!xys)
17515
+ return { path, state: onDisk(path) ? "clean" : "absent" };
17516
+ if (xys.some((xy) => xy.includes("D")))
17517
+ return { path, state: "deleted" };
17518
+ if (xys.includes("??"))
17519
+ return { path, state: "untracked" };
17520
+ if (xys.includes("!!"))
17521
+ return { path, state: "ignored" };
17522
+ if (xys.some((xy) => xy[0] === "A"))
17523
+ return { path, state: "added" };
17524
+ return { path, state: "modified" };
17525
+ });
17526
+ }
17527
+
17240
17528
  // dist/hooks/post-tool-use.js
17241
17529
  var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
17242
- async function runPostToolUse() {
17530
+ async function runPostToolUse(readSeat = readSeatConfiguration) {
17243
17531
  const reply = hookReply("PostToolUse");
17244
- await evaluateEdit(reply);
17532
+ await evaluateEdit(reply, readSeat);
17245
17533
  reply.flush();
17246
17534
  }
17247
- async function evaluateEdit(reply) {
17535
+ async function evaluateEdit(reply, readSeat) {
17248
17536
  let input;
17249
17537
  try {
17250
17538
  const raw = await readStdin();
@@ -17282,11 +17570,12 @@ async function evaluateEdit(reply) {
17282
17570
  return;
17283
17571
  }
17284
17572
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
17573
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
17285
17574
  const evaluation = await evaluateWithRefresh({
17286
17575
  guardServiceUrl,
17287
17576
  token: guardServiceToken,
17288
17577
  engagementId: guardEngagementId,
17289
- request: buildEvaluationRequest(guardEngagementId, changeSet, context)
17578
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
17290
17579
  });
17291
17580
  const clientResult = evaluation.result;
17292
17581
  if (!clientResult.ok) {
@@ -17367,19 +17656,19 @@ ${guardLines}${notRunBlock}`);
17367
17656
  }
17368
17657
  }
17369
17658
  function readStdin() {
17370
- return new Promise((resolve, reject) => {
17659
+ return new Promise((resolve2, reject) => {
17371
17660
  const chunks = [];
17372
17661
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17373
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
17662
+ process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
17374
17663
  process.stdin.on("error", reject);
17375
17664
  });
17376
17665
  }
17377
17666
 
17378
17667
  // dist/hooks/marker.js
17379
17668
  import { createHash } from "node:crypto";
17380
- import { existsSync as existsSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17669
+ import { existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17381
17670
  import { tmpdir } from "node:os";
17382
- import { join as join6 } from "node:path";
17671
+ import { join as join7 } from "node:path";
17383
17672
  var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
17384
17673
  var MARKER_PREFIX = "halfcycle-session-";
17385
17674
  var MARKER_SUFFIX = ".ref";
@@ -17397,19 +17686,19 @@ function markerFileName(repoRoot, sessionId) {
17397
17686
  return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
17398
17687
  }
17399
17688
  function markerPath(repoRoot, sessionId) {
17400
- return join6(markerDir(), markerFileName(repoRoot, sessionId));
17689
+ return join7(markerDir(), markerFileName(repoRoot, sessionId));
17401
17690
  }
17402
17691
  function dedupePath(repoRoot, sessionId) {
17403
17692
  const hash2 = repoHash(repoRoot);
17404
17693
  const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
17405
- return join6(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17694
+ return join7(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17406
17695
  }
17407
17696
  function readMarker(repoRoot, sessionId) {
17408
17697
  const path = markerPath(repoRoot, sessionId);
17409
- if (!existsSync2(path))
17698
+ if (!existsSync3(path))
17410
17699
  return null;
17411
17700
  try {
17412
- return readFileSync6(path, "utf-8").trim() || null;
17701
+ return readFileSync7(path, "utf-8").trim() || null;
17413
17702
  } catch {
17414
17703
  return null;
17415
17704
  }
@@ -17417,7 +17706,7 @@ function readMarker(repoRoot, sessionId) {
17417
17706
  function deleteMarker(repoRoot, sessionId) {
17418
17707
  for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17419
17708
  try {
17420
- if (existsSync2(path))
17709
+ if (existsSync3(path))
17421
17710
  unlinkSync3(path);
17422
17711
  } catch {
17423
17712
  }
@@ -17425,10 +17714,10 @@ function deleteMarker(repoRoot, sessionId) {
17425
17714
  }
17426
17715
  function readDedupe(repoRoot, sessionId) {
17427
17716
  const path = dedupePath(repoRoot, sessionId);
17428
- if (!existsSync2(path))
17717
+ if (!existsSync3(path))
17429
17718
  return null;
17430
17719
  try {
17431
- const parsed = JSON.parse(readFileSync6(path, "utf-8"));
17720
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
17432
17721
  if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
17433
17722
  return parsed;
17434
17723
  }
@@ -17470,10 +17759,10 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
17470
17759
  continue;
17471
17760
  if (name === keepName)
17472
17761
  continue;
17473
- const full = join6(dir, name);
17762
+ const full = join7(dir, name);
17474
17763
  let unusable = false;
17475
17764
  try {
17476
- const ref = readFileSync6(full, "utf-8").trim();
17765
+ const ref = readFileSync7(full, "utf-8").trim();
17477
17766
  if (!ref || !refKnown(ref))
17478
17767
  unusable = true;
17479
17768
  } catch {
@@ -17531,12 +17820,12 @@ function collectDiff(repoRoot, base) {
17531
17820
  getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
17532
17821
  };
17533
17822
  }
17534
- async function runSessionDiff(input, kind) {
17823
+ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
17535
17824
  const reply = hookReply(STOP_HOOK_EVENT[kind]);
17536
- await evaluateSession(input, kind, reply);
17825
+ await evaluateSession(input, kind, reply, readSeat);
17537
17826
  reply.flush();
17538
17827
  }
17539
- async function evaluateSession(input, kind, reply) {
17828
+ async function evaluateSession(input, kind, reply, readSeat) {
17540
17829
  const repoRootResult = getRepoRoot(process.cwd());
17541
17830
  if (!repoRootResult.ok) {
17542
17831
  reply.notice(localFaultText(`Could not find git repo root: ${repoRootResult.message}`));
@@ -17576,11 +17865,12 @@ async function evaluateSession(input, kind, reply) {
17576
17865
  return;
17577
17866
  }
17578
17867
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
17868
+ const seatConfiguration = gatherSeatConfiguration(repoRoot, readSeat);
17579
17869
  const evaluation = await evaluateWithRefresh({
17580
17870
  guardServiceUrl,
17581
17871
  token: guardServiceToken,
17582
17872
  engagementId: guardEngagementId,
17583
- request: buildEvaluationRequest(guardEngagementId, changeSet, context)
17873
+ request: buildEvaluationRequest(guardEngagementId, changeSet, context, seatConfiguration)
17584
17874
  });
17585
17875
  const clientResult = evaluation.result;
17586
17876
  if (!clientResult.ok) {
@@ -17678,9 +17968,9 @@ async function readHookStdin() {
17678
17968
  }
17679
17969
  }
17680
17970
  function readStdin2() {
17681
- return new Promise((resolve) => {
17971
+ return new Promise((resolve2) => {
17682
17972
  if (process.stdin.isTTY) {
17683
- resolve("");
17973
+ resolve2("");
17684
17974
  return;
17685
17975
  }
17686
17976
  const chunks = [];
@@ -17690,7 +17980,7 @@ function readStdin2() {
17690
17980
  return;
17691
17981
  settled = true;
17692
17982
  clearTimeout(timer);
17693
- resolve(Buffer.concat(chunks).toString("utf-8"));
17983
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
17694
17984
  };
17695
17985
  const timer = setTimeout(done, 2e3);
17696
17986
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -17712,17 +18002,17 @@ async function runStop() {
17712
18002
  }
17713
18003
 
17714
18004
  // dist/resolve-engagement.js
17715
- import { existsSync as existsSync3, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
17716
- import { join as join7, sep as sep2 } from "node:path";
17717
- import { homedir as homedir3 } from "node:os";
18005
+ import { existsSync as existsSync4, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18006
+ import { join as join8, sep as sep2 } from "node:path";
18007
+ import { homedir as homedir4 } from "node:os";
17718
18008
  function defaultRegistryPath() {
17719
- return join7(homedir3(), ".halfcycle", "registry.json");
18009
+ return join8(homedir4(), ".halfcycle", "registry.json");
17720
18010
  }
17721
18011
  function readRegistry(registryPath = defaultRegistryPath()) {
17722
- if (!existsSync3(registryPath))
18012
+ if (!existsSync4(registryPath))
17723
18013
  return {};
17724
18014
  try {
17725
- const parsed = JSON.parse(readFileSync7(registryPath, "utf-8"));
18015
+ const parsed = JSON.parse(readFileSync8(registryPath, "utf-8"));
17726
18016
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
17727
18017
  return {};
17728
18018
  const out = {};
@@ -17737,7 +18027,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
17737
18027
  }
17738
18028
  function canonicalise(p) {
17739
18029
  try {
17740
- return realpathSync2(p);
18030
+ return realpathSync3(p);
17741
18031
  } catch {
17742
18032
  return p;
17743
18033
  }
@@ -17802,7 +18092,7 @@ async function runRefreshCredential(opts) {
17802
18092
  }
17803
18093
 
17804
18094
  // dist/capture.js
17805
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "node:fs";
18095
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync5 } from "node:fs";
17806
18096
  import { dirname as dirname4 } from "node:path";
17807
18097
  function runCapture(provider, endpoint, opts = {}) {
17808
18098
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -17870,7 +18160,7 @@ function readInputFile(fromFile) {
17870
18160
  if (!fromFile)
17871
18161
  return void 0;
17872
18162
  try {
17873
- return readFileSync8(fromFile, "utf-8");
18163
+ return readFileSync9(fromFile, "utf-8");
17874
18164
  } catch {
17875
18165
  return void 0;
17876
18166
  }
@@ -17965,10 +18255,10 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
17965
18255
  process.exit(1);
17966
18256
  }
17967
18257
  function readStdin3() {
17968
- return new Promise((resolve, reject) => {
18258
+ return new Promise((resolve2, reject) => {
17969
18259
  const chunks = [];
17970
18260
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17971
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
18261
+ process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
17972
18262
  process.stdin.on("error", reject);
17973
18263
  });
17974
18264
  }
package/dist/bin.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * Subcommands:
9
9
  * halfcycle [install] [target-repo] — install into target (default: cwd)
10
10
  * halfcycle check-drift <target> — report bundle drift for the re-sync gate
11
- * halfcycle build-record <phase> — assemble the phase Build Record (D1)
11
+ * halfcycle build-record <phase> — fetch a closed phase's Build Record and write it
12
12
  * halfcycle open-phase <phase> — open a phase, with its entry decision
13
13
  * halfcycle close-phase <phase> — record a phase's acceptance and close it
14
14
  * halfcycle uninstall [--remove-credential] — take back what the installer wrote here