halfcycle 0.3.23 → 0.3.24

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.
@@ -14,75 +14,6 @@ function resolveGuardOrigin(env) {
14
14
  return { origin: DEFAULT_GUARD_ORIGIN, source: "default" };
15
15
  }
16
16
 
17
- // dist/config.js
18
- var EXCERPT_DEFAULTS = {
19
- maxCharsPerExcerpt: 200,
20
- maxExcerptsPerFile: 5,
21
- maxExcerptsPerChangeSet: 20
22
- };
23
- function readConfig() {
24
- const guardServiceUrl = resolveGuardOrigin(process.env).origin;
25
- const guardServiceToken = process.env["GUARD_SERVICE_TOKEN"];
26
- const guardEngagementId = process.env["GUARD_ENGAGEMENT_ID"];
27
- const missing = [];
28
- if (!guardServiceToken)
29
- missing.push("GUARD_SERVICE_TOKEN");
30
- if (!guardEngagementId)
31
- missing.push("GUARD_ENGAGEMENT_ID");
32
- if (missing.length > 0)
33
- return { config: null, missing };
34
- return {
35
- config: {
36
- guardServiceUrl,
37
- guardServiceToken,
38
- guardEngagementId
39
- }
40
- };
41
- }
42
- function readNeverConfiguredReason() {
43
- return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
44
- }
45
- function readExcerptConfig() {
46
- const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
47
- const enabled = (raw ?? "").trim().toLowerCase() === "true";
48
- const readCap = (name, fallback) => {
49
- const v = process.env[name];
50
- if (v === void 0)
51
- return fallback;
52
- const n = Number.parseInt(v, 10);
53
- return Number.isFinite(n) && n > 0 ? n : fallback;
54
- };
55
- return {
56
- enabled,
57
- maxCharsPerExcerpt: readCap("HALFCYCLE_EXCERPT_MAX_CHARS", EXCERPT_DEFAULTS.maxCharsPerExcerpt),
58
- maxExcerptsPerFile: readCap("HALFCYCLE_EXCERPT_MAX_PER_FILE", EXCERPT_DEFAULTS.maxExcerptsPerFile),
59
- maxExcerptsPerChangeSet: readCap("HALFCYCLE_EXCERPT_MAX_PER_CHANGESET", EXCERPT_DEFAULTS.maxExcerptsPerChangeSet)
60
- };
61
- }
62
- function readDiffBaseConfig() {
63
- const raw = process.env["HALFCYCLE_DIFF_BASE"];
64
- const trimmed = (raw ?? "").trim();
65
- if (!trimmed)
66
- return void 0;
67
- if (/^0+$/.test(trimmed))
68
- return void 0;
69
- return trimmed;
70
- }
71
- function readTelemetryConfig() {
72
- return {
73
- controlTelemetryUrl: process.env["CONTROL_TELEMETRY_URL"],
74
- // Reuse GUARD_SERVICE_TOKEN as the per-engagement bearer token for telemetry.
75
- // The control plane validates the same token on POST /telemetry (Phase 1).
76
- controlTelemetryToken: process.env["GUARD_SERVICE_TOKEN"],
77
- // Per-engagement log dir injected by the guard-runner.sh wrapper (E1-T-02).
78
- // Absent → emit.ts defaults to ~/.halfcycle/guard-eval-log/.
79
- guardEvalLogDir: process.env["HALFCYCLE_GUARD_EVAL_LOG_DIR"],
80
- // Phase source injected by the guard-runner.sh wrapper (E1-T-03, #61 seam).
81
- // Absent → the run is unphased.
82
- phase: process.env["HALFCYCLE_PHASE"]
83
- };
84
- }
85
-
86
17
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
87
18
  var external_exports = {};
88
19
  __export(external_exports, {
@@ -14597,6 +14528,220 @@ function date4(params) {
14597
14528
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
14598
14529
  config(en_default());
14599
14530
 
14531
+ // ../core/dist/guard-record.js
14532
+ var severitySchema = external_exports.enum(["info", "warn", "block"]);
14533
+ var channelSchema = external_exports.enum(["stable", "candidate", "rented"]);
14534
+ var matcherKindSchema = external_exports.enum(["grep", "ast", "integrationTest", "llm"]);
14535
+ var matcherConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
14536
+ var grepMatcherSchema = external_exports.object({ kind: external_exports.literal("grep"), config: matcherConfigSchema }).strict();
14537
+ var astMatcherSchema = external_exports.object({ kind: external_exports.literal("ast"), config: matcherConfigSchema }).strict();
14538
+ var integrationTestMatcherSchema = external_exports.object({ kind: external_exports.literal("integrationTest"), config: matcherConfigSchema }).strict();
14539
+ var llmMatcherSchema = external_exports.object({ kind: external_exports.literal("llm"), config: matcherConfigSchema }).strict();
14540
+ var matcherSchema = external_exports.discriminatedUnion("kind", [
14541
+ grepMatcherSchema,
14542
+ astMatcherSchema,
14543
+ integrationTestMatcherSchema,
14544
+ llmMatcherSchema
14545
+ ]);
14546
+
14547
+ // ../core/dist/credential-format.js
14548
+ var HALFCYCLE_DIR_NAME = ".halfcycle";
14549
+ var ENGAGEMENTS_DIR_NAME = "engagements";
14550
+ var ENGAGEMENT_ENV_FILENAME = "env";
14551
+ var ACCOUNT_STORE_FILENAME = "account.json";
14552
+ var ENGAGEMENT_ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
14553
+ function shq(value) {
14554
+ return `'${value.replace(/'/g, `'\\''`)}'`;
14555
+ }
14556
+ function unquote(value) {
14557
+ const v = value.trim();
14558
+ if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
14559
+ return v.slice(1, -1).split(`'\\''`).join(`'`);
14560
+ }
14561
+ if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
14562
+ return v.slice(1, -1);
14563
+ return v;
14564
+ }
14565
+ function parseEnvText(raw) {
14566
+ const out = {};
14567
+ for (const line of raw.split("\n")) {
14568
+ const eq = line.indexOf("=");
14569
+ if (eq === -1)
14570
+ continue;
14571
+ const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
14572
+ if (key === "" || key.startsWith("#"))
14573
+ continue;
14574
+ out[key] = unquote(line.slice(eq + 1));
14575
+ }
14576
+ return out;
14577
+ }
14578
+ function reconcileEnvText(existing, values, keys) {
14579
+ const desired = new Map(keys.map((k) => {
14580
+ const value = values[k];
14581
+ return [k, value === void 0 ? "" : value];
14582
+ }));
14583
+ const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
14584
+ const written = keys.filter((k) => desired.get(k) !== null);
14585
+ if (existing === null) {
14586
+ if (written.length === 0)
14587
+ return "";
14588
+ return `${ENGAGEMENT_ENV_HEADER}
14589
+ ` + written.map(line).join("\n") + "\n";
14590
+ }
14591
+ const seen = /* @__PURE__ */ new Set();
14592
+ const out = [];
14593
+ for (const existingLine of existing.split("\n")) {
14594
+ const eq = existingLine.indexOf("=");
14595
+ if (eq === -1) {
14596
+ out.push(existingLine);
14597
+ continue;
14598
+ }
14599
+ const lhs = existingLine.slice(0, eq);
14600
+ const exported = /^\s*export\s+/.exec(lhs);
14601
+ const prefix = exported === null ? "" : exported[0];
14602
+ const key = lhs.slice(prefix.length).trim();
14603
+ if (!desired.has(key)) {
14604
+ out.push(existingLine);
14605
+ continue;
14606
+ }
14607
+ seen.add(key);
14608
+ if (desired.get(key) === null)
14609
+ continue;
14610
+ out.push(`${prefix}${line(key)}`);
14611
+ }
14612
+ const missing = written.filter((k) => !seen.has(k));
14613
+ if (missing.length > 0) {
14614
+ const header = existing.includes(ENGAGEMENT_ENV_HEADER) ? "" : `${ENGAGEMENT_ENV_HEADER}
14615
+ `;
14616
+ const block = header + missing.map(line).join("\n");
14617
+ const trailingBlank = out.length > 0 && out[out.length - 1] === "";
14618
+ if (trailingBlank)
14619
+ out.splice(out.length - 1, 0, block);
14620
+ else
14621
+ out.push(`
14622
+ ${block}`);
14623
+ }
14624
+ let result = out.join("\n");
14625
+ if (existing.endsWith("\n") && !result.endsWith("\n"))
14626
+ result += "\n";
14627
+ return result;
14628
+ }
14629
+ function normaliseOrigin(serviceUrl) {
14630
+ return serviceUrl.trim().replace(/\/+$/, "").toLowerCase();
14631
+ }
14632
+ function parseAccountStoreText(raw) {
14633
+ try {
14634
+ const parsed = JSON.parse(raw);
14635
+ if (!parsed || typeof parsed !== "object" || typeof parsed.accounts !== "object") {
14636
+ return { version: 1, accounts: {} };
14637
+ }
14638
+ return {
14639
+ version: 1,
14640
+ accounts: parsed.accounts ?? {}
14641
+ };
14642
+ } catch {
14643
+ return { version: 1, accounts: {} };
14644
+ }
14645
+ }
14646
+
14647
+ // ../core/dist/ci-audience.js
14648
+ var HALFCYCLE_OIDC_AUDIENCE = "https://halfcycle.ai";
14649
+
14650
+ // dist/control-origin.js
14651
+ var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
14652
+ function resolveControlOrigin(env) {
14653
+ const configured = env["HALFCYCLE_SERVICE_URL"]?.trim();
14654
+ if (configured)
14655
+ return { origin: normaliseOrigin(configured), source: "environment" };
14656
+ return { origin: DEFAULT_CONTROL_ORIGIN, source: "default" };
14657
+ }
14658
+
14659
+ // dist/config.js
14660
+ var EXCERPT_DEFAULTS = {
14661
+ maxCharsPerExcerpt: 200,
14662
+ maxExcerptsPerFile: 5,
14663
+ maxExcerptsPerChangeSet: 20
14664
+ };
14665
+ function readConfig() {
14666
+ const guardServiceUrl = readGuardOrigin().origin;
14667
+ const guardServiceToken = process.env["GUARD_SERVICE_TOKEN"];
14668
+ const guardEngagementId = process.env["GUARD_ENGAGEMENT_ID"];
14669
+ const missing = [];
14670
+ if (!guardServiceToken)
14671
+ missing.push("GUARD_SERVICE_TOKEN");
14672
+ if (!guardEngagementId)
14673
+ missing.push("GUARD_ENGAGEMENT_ID");
14674
+ if (missing.length > 0)
14675
+ return { config: null, missing };
14676
+ return {
14677
+ config: {
14678
+ guardServiceUrl,
14679
+ guardServiceToken,
14680
+ guardEngagementId
14681
+ }
14682
+ };
14683
+ }
14684
+ function readNeverConfiguredReason() {
14685
+ return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
14686
+ }
14687
+ function readGuardOrigin() {
14688
+ return resolveGuardOrigin(process.env);
14689
+ }
14690
+ function readActionsOidcEnv() {
14691
+ const requestUrl = process.env["ACTIONS_ID_TOKEN_REQUEST_URL"]?.trim();
14692
+ const requestToken = process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]?.trim();
14693
+ if (!requestUrl || !requestToken)
14694
+ return void 0;
14695
+ return { requestUrl, requestToken };
14696
+ }
14697
+ function readControlOrigin() {
14698
+ return resolveControlOrigin(process.env);
14699
+ }
14700
+ function readEnvFilePath() {
14701
+ const raw = process.env["HALFCYCLE_ENV_FILE"]?.trim();
14702
+ return raw ? raw : void 0;
14703
+ }
14704
+ function readExcerptConfig() {
14705
+ const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
14706
+ const enabled = (raw ?? "").trim().toLowerCase() === "true";
14707
+ const readCap = (name, fallback) => {
14708
+ const v = process.env[name];
14709
+ if (v === void 0)
14710
+ return fallback;
14711
+ const n = Number.parseInt(v, 10);
14712
+ return Number.isFinite(n) && n > 0 ? n : fallback;
14713
+ };
14714
+ return {
14715
+ enabled,
14716
+ maxCharsPerExcerpt: readCap("HALFCYCLE_EXCERPT_MAX_CHARS", EXCERPT_DEFAULTS.maxCharsPerExcerpt),
14717
+ maxExcerptsPerFile: readCap("HALFCYCLE_EXCERPT_MAX_PER_FILE", EXCERPT_DEFAULTS.maxExcerptsPerFile),
14718
+ maxExcerptsPerChangeSet: readCap("HALFCYCLE_EXCERPT_MAX_PER_CHANGESET", EXCERPT_DEFAULTS.maxExcerptsPerChangeSet)
14719
+ };
14720
+ }
14721
+ function readDiffBaseConfig() {
14722
+ const raw = process.env["HALFCYCLE_DIFF_BASE"];
14723
+ const trimmed = (raw ?? "").trim();
14724
+ if (!trimmed)
14725
+ return void 0;
14726
+ if (/^0+$/.test(trimmed))
14727
+ return void 0;
14728
+ return trimmed;
14729
+ }
14730
+ function readTelemetryConfig() {
14731
+ return {
14732
+ controlTelemetryUrl: process.env["CONTROL_TELEMETRY_URL"],
14733
+ // Reuse GUARD_SERVICE_TOKEN as the per-engagement bearer token for telemetry.
14734
+ // The control plane validates the same token on POST /telemetry (Phase 1).
14735
+ controlTelemetryToken: process.env["GUARD_SERVICE_TOKEN"],
14736
+ // Per-engagement log dir injected by the guard-runner.sh wrapper (E1-T-02).
14737
+ // Absent → emit.ts defaults to ~/.halfcycle/guard-eval-log/.
14738
+ guardEvalLogDir: process.env["HALFCYCLE_GUARD_EVAL_LOG_DIR"],
14739
+ // Phase source injected by the guard-runner.sh wrapper (E1-T-03, #61 seam).
14740
+ // Absent → the run is unphased.
14741
+ phase: process.env["HALFCYCLE_PHASE"]
14742
+ };
14743
+ }
14744
+
14600
14745
  // ../events/dist/evaluation.js
14601
14746
  var envRefSchema = external_exports.object({
14602
14747
  name: external_exports.string(),
@@ -14671,22 +14816,6 @@ var evaluationRequestSchema = external_exports.object({
14671
14816
  clientCapabilities: external_exports.array(external_exports.string()).optional()
14672
14817
  }).strict();
14673
14818
 
14674
- // ../core/dist/guard-record.js
14675
- var severitySchema = external_exports.enum(["info", "warn", "block"]);
14676
- var channelSchema = external_exports.enum(["stable", "candidate", "rented"]);
14677
- var matcherKindSchema = external_exports.enum(["grep", "ast", "integrationTest", "llm"]);
14678
- var matcherConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
14679
- var grepMatcherSchema = external_exports.object({ kind: external_exports.literal("grep"), config: matcherConfigSchema }).strict();
14680
- var astMatcherSchema = external_exports.object({ kind: external_exports.literal("ast"), config: matcherConfigSchema }).strict();
14681
- var integrationTestMatcherSchema = external_exports.object({ kind: external_exports.literal("integrationTest"), config: matcherConfigSchema }).strict();
14682
- var llmMatcherSchema = external_exports.object({ kind: external_exports.literal("llm"), config: matcherConfigSchema }).strict();
14683
- var matcherSchema = external_exports.discriminatedUnion("kind", [
14684
- grepMatcherSchema,
14685
- astMatcherSchema,
14686
- integrationTestMatcherSchema,
14687
- llmMatcherSchema
14688
- ]);
14689
-
14690
14819
  // ../events/dist/result.js
14691
14820
  var firedGuardSchema = external_exports.object({
14692
14821
  guardId: external_exports.string(),
@@ -14745,6 +14874,173 @@ var wireErrorSchema = external_exports.object({
14745
14874
  message: external_exports.string()
14746
14875
  }).strict();
14747
14876
 
14877
+ // ../events/dist/state.js
14878
+ var utcIso8601 = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
14879
+ var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
14880
+ var stepIdSchema = external_exports.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
14881
+ var methodVersionSchema = external_exports.string().regex(/^\d+\.\d+\.\d+$/);
14882
+ var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
14883
+ var recordKindSchema = external_exports.enum(["artefact", "step-run", "gate", "intervention"]);
14884
+ var methodLayerSchema = external_exports.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
14885
+ var evidenceKindSchema = external_exports.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
14886
+ var declarableEvidenceKindSchema = external_exports.enum(["anchor", "diff", "repro", "question-answer"]);
14887
+ var dispositionSchema = external_exports.enum([
14888
+ "prevented",
14889
+ "corrected-in-spec",
14890
+ "corrected-in-scope",
14891
+ "blocked-in-code",
14892
+ "flagged",
14893
+ "auto-fixed",
14894
+ "surfaced",
14895
+ "filed-forward"
14896
+ ]);
14897
+ var mechanismSchema = external_exports.enum(["guard", "verdict", "served-step", "method-step"]);
14898
+ var artefactStateSchema = external_exports.enum(["created", "updated"]);
14899
+ var QUALIFIED_OUTCOMES = ["declined", "parked"];
14900
+ var qualifiedOutcomeSchema = external_exports.enum(QUALIFIED_OUTCOMES);
14901
+ function isQualifiedOutcome(outcome) {
14902
+ return QUALIFIED_OUTCOMES.includes(outcome);
14903
+ }
14904
+ var runOutcomeSchema = external_exports.enum([
14905
+ "completed",
14906
+ "attempted-failed",
14907
+ ...QUALIFIED_OUTCOMES
14908
+ ]);
14909
+ var gateKindSchema = external_exports.enum(["approval", "mechanical"]);
14910
+ var gateVerdictSchema = external_exports.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
14911
+ function outcomeReasonIssue(outcome, reason) {
14912
+ if (isQualifiedOutcome(outcome) && reason === void 0) {
14913
+ return { message: `outcomeReason is required when the outcome is '${outcome}'` };
14914
+ }
14915
+ if (!isQualifiedOutcome(outcome) && reason !== void 0) {
14916
+ return {
14917
+ message: `outcomeReason is only present on a qualified outcome (${QUALIFIED_OUTCOMES.join(" | ")})`
14918
+ };
14919
+ }
14920
+ return null;
14921
+ }
14922
+ var anchorEvidenceSchema = external_exports.object({
14923
+ kind: external_exports.literal("anchor"),
14924
+ path: external_exports.string().regex(anchorPathRegex),
14925
+ // Optional 1-based line number within the file at `path`. Present when the
14926
+ // writer knows the exact line; omitted — never `0`, never `null` — when it
14927
+ // does not, because a line number is not always obtainable. Absence is a
14928
+ // valid, permanent state here, not a gap meant to be filled in later.
14929
+ line: external_exports.number().optional()
14930
+ }).strict();
14931
+ var diffEvidenceSchema = external_exports.object({
14932
+ kind: external_exports.literal("diff"),
14933
+ ref: external_exports.string()
14934
+ }).strict();
14935
+ var reproEvidenceSchema = external_exports.object({
14936
+ kind: external_exports.literal("repro"),
14937
+ steps: external_exports.string().max(2e3)
14938
+ }).strict();
14939
+ var questionAnswerEvidenceSchema = external_exports.object({
14940
+ kind: external_exports.literal("question-answer"),
14941
+ question: external_exports.string().max(2e3),
14942
+ before: external_exports.string().max(2e3),
14943
+ after: external_exports.string().max(2e3)
14944
+ }).strict();
14945
+ var noOpEvidenceSchema = external_exports.object({
14946
+ kind: external_exports.literal("no-op"),
14947
+ inspected: external_exports.string().max(2e3),
14948
+ unchangedBecause: external_exports.string().max(2e3)
14949
+ }).strict();
14950
+ var evidenceSchema = external_exports.discriminatedUnion("kind", [
14951
+ anchorEvidenceSchema,
14952
+ diffEvidenceSchema,
14953
+ reproEvidenceSchema,
14954
+ questionAnswerEvidenceSchema,
14955
+ noOpEvidenceSchema
14956
+ ]);
14957
+ var stateRecordEnvelopeSchema = external_exports.object({
14958
+ format: external_exports.literal(STATE_RECORD_FORMAT),
14959
+ recordKind: recordKindSchema,
14960
+ recordId: external_exports.string().uuid(),
14961
+ methodVersion: methodVersionSchema,
14962
+ engagementId: external_exports.string(),
14963
+ stepId: stepIdSchema,
14964
+ // Immutable: names when the thing happened, never when a later disposition
14965
+ // moved.
14966
+ occurredAt: utcIso8601,
14967
+ // Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
14968
+ // Carried on the shared envelope so a "when did this last change" query is
14969
+ // kind-agnostic. Kept rather than generalised to `updatedAt` because
14970
+ // `disposition` is the only field any re-emit may change (§5.3).
14971
+ dispositionAt: utcIso8601,
14972
+ evidence: evidenceSchema,
14973
+ // Which phase of the project this record belongs to, if any. Present on
14974
+ // records written during phase-scoped work; absent on records that run once
14975
+ // for the whole project rather than per phase. Absence is meaningful on its
14976
+ // own — it is never replaced with a placeholder value, and it is read as
14977
+ // "satisfies any phase", not as "unknown".
14978
+ phase: external_exports.string().optional()
14979
+ });
14980
+ var artefactRefSchema = external_exports.object({
14981
+ name: external_exports.string(),
14982
+ parent: external_exports.string()
14983
+ }).strict();
14984
+ var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
14985
+ recordKind: external_exports.literal("artefact"),
14986
+ artefactRef: artefactRefSchema,
14987
+ // Some layer wrote it, by construction — an empty array is not a legitimate
14988
+ // state, so the schema says so.
14989
+ writtenByLayers: external_exports.array(methodLayerSchema).min(1),
14990
+ state: artefactStateSchema
14991
+ }).strict();
14992
+ var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
14993
+ recordKind: external_exports.literal("step-run"),
14994
+ runOutcome: runOutcomeSchema,
14995
+ failureReason: external_exports.string().optional(),
14996
+ outcomeReason: external_exports.string().max(2e3).optional()
14997
+ }).strict().superRefine((rec, ctx) => {
14998
+ if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
14999
+ ctx.addIssue({
15000
+ code: external_exports.ZodIssueCode.custom,
15001
+ path: ["failureReason"],
15002
+ message: "failureReason is required when runOutcome is 'attempted-failed'"
15003
+ });
15004
+ }
15005
+ if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
15006
+ ctx.addIssue({
15007
+ code: external_exports.ZodIssueCode.custom,
15008
+ path: ["failureReason"],
15009
+ message: "failureReason is only present when runOutcome is 'attempted-failed'"
15010
+ });
15011
+ }
15012
+ const issue2 = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
15013
+ if (issue2) {
15014
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["outcomeReason"], ...issue2 });
15015
+ }
15016
+ });
15017
+ var gateRecordSchema = stateRecordEnvelopeSchema.extend({
15018
+ recordKind: external_exports.literal("gate"),
15019
+ gateKind: gateKindSchema,
15020
+ verdict: gateVerdictSchema,
15021
+ actor: external_exports.string(),
15022
+ outcomeReason: external_exports.string().max(2e3).optional()
15023
+ }).strict().superRefine((rec, ctx) => {
15024
+ const issue2 = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
15025
+ if (issue2) {
15026
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["outcomeReason"], ...issue2 });
15027
+ }
15028
+ });
15029
+ var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
15030
+ recordKind: external_exports.literal("intervention"),
15031
+ layer: methodLayerSchema,
15032
+ mechanism: mechanismSchema,
15033
+ severity: severitySchema,
15034
+ disposition: dispositionSchema,
15035
+ summary: external_exports.string().max(2e3)
15036
+ }).strict();
15037
+ var stateRecordSchema = external_exports.discriminatedUnion("recordKind", [
15038
+ artefactRecordSchema,
15039
+ stepRunRecordSchema,
15040
+ gateRecordSchema,
15041
+ interventionRecordSchema
15042
+ ]);
15043
+
14748
15044
  // ../events/dist/phase-identity.js
14749
15045
  var PHASE_SEGMENT_PREFIX = "phase-";
14750
15046
  function isValidPhaseIdentity(identity) {
@@ -14801,8 +15097,155 @@ function describeIdentity(identity) {
14801
15097
  return `(a ${typeof identity})`;
14802
15098
  }
14803
15099
 
14804
- // dist/client.js
15100
+ // ../events/dist/credential-wire.js
15101
+ var engagementCredentialResponseSchema = external_exports.object({
15102
+ engagementId: external_exports.string(),
15103
+ sessionToken: external_exports.string(),
15104
+ mcpUrl: external_exports.string(),
15105
+ guardUrl: external_exports.string(),
15106
+ controlTelemetryUrl: external_exports.string()
15107
+ }).strict();
15108
+ var ciTokenExchangeResponseSchema = external_exports.object({
15109
+ token: external_exports.string(),
15110
+ expiresAt: utcIso8601,
15111
+ engagementId: external_exports.string()
15112
+ }).strict();
15113
+
15114
+ // dist/ci-oidc.js
15115
+ var CI_EXCHANGE_PATH = "/auth/ci/exchange";
14805
15116
  var REQUEST_TIMEOUT_MS = 1e4;
15117
+ function idTokenRequestUrl(requestUrl, audience) {
15118
+ const url2 = new URL(requestUrl);
15119
+ url2.searchParams.set("audience", audience);
15120
+ return url2;
15121
+ }
15122
+ async function fetchWithTimeout(fetchFn, input, init) {
15123
+ const controller = new AbortController();
15124
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
15125
+ try {
15126
+ const response = await fetchFn(input, { ...init, signal: controller.signal });
15127
+ return { ok: true, response };
15128
+ } catch (err) {
15129
+ const timedOut = err instanceof Error && err.name === "AbortError";
15130
+ return {
15131
+ ok: false,
15132
+ message: timedOut ? `no answer after ${REQUEST_TIMEOUT_MS}ms` : err instanceof Error ? err.message : String(err)
15133
+ };
15134
+ } finally {
15135
+ clearTimeout(timer);
15136
+ }
15137
+ }
15138
+ async function requestIdentityToken(oidc, fetchFn = globalThis.fetch) {
15139
+ let url2;
15140
+ try {
15141
+ url2 = idTokenRequestUrl(oidc.requestUrl, HALFCYCLE_OIDC_AUDIENCE);
15142
+ } catch {
15143
+ return { ok: false, message: "the token request address this job was given is not a URL" };
15144
+ }
15145
+ const attempt2 = await fetchWithTimeout(fetchFn, url2.toString(), {
15146
+ method: "GET",
15147
+ headers: { Authorization: `Bearer ${oidc.requestToken}` }
15148
+ });
15149
+ if (!attempt2.ok)
15150
+ return { ok: false, message: attempt2.message };
15151
+ const { response } = attempt2;
15152
+ if (!response.ok) {
15153
+ return { ok: false, message: `the token service answered ${response.status}` };
15154
+ }
15155
+ let body;
15156
+ try {
15157
+ body = await response.json();
15158
+ } catch {
15159
+ return { ok: false, message: "the token service did not answer with JSON" };
15160
+ }
15161
+ const value = typeof body === "object" && body !== null ? body.value : void 0;
15162
+ if (typeof value !== "string" || value.trim() === "") {
15163
+ return { ok: false, message: "the token service answered without a token" };
15164
+ }
15165
+ return { ok: true, identityToken: value.trim() };
15166
+ }
15167
+ async function exchangeIdentityToken(controlOrigin, identityToken, fetchFn = globalThis.fetch) {
15168
+ const attempt2 = await fetchWithTimeout(fetchFn, `${controlOrigin}${CI_EXCHANGE_PATH}`, {
15169
+ method: "POST",
15170
+ headers: { Authorization: `Bearer ${identityToken}` }
15171
+ });
15172
+ if (!attempt2.ok)
15173
+ return { ok: false, kind: "service", message: attempt2.message };
15174
+ const { response } = attempt2;
15175
+ let body;
15176
+ try {
15177
+ body = await response.json();
15178
+ } catch {
15179
+ return {
15180
+ ok: false,
15181
+ kind: "contract",
15182
+ message: "the control plane did not answer the exchange with JSON"
15183
+ };
15184
+ }
15185
+ if (!response.ok) {
15186
+ const parsed2 = wireErrorSchema.safeParse(body);
15187
+ if (!parsed2.success) {
15188
+ return {
15189
+ ok: false,
15190
+ kind: "contract",
15191
+ message: `the control plane refused the exchange in an unreadable shape (${response.status})`
15192
+ };
15193
+ }
15194
+ const decided = parsed2.data.statusCode === 401 || parsed2.data.statusCode === 403;
15195
+ return decided ? { ok: false, kind: "refused", statusCode: parsed2.data.statusCode, message: parsed2.data.message } : { ok: false, kind: "service", message: parsed2.data.message };
15196
+ }
15197
+ const parsed = ciTokenExchangeResponseSchema.safeParse(body);
15198
+ if (!parsed.success) {
15199
+ return {
15200
+ ok: false,
15201
+ kind: "contract",
15202
+ message: `the exchange response did not match the declared shape: ${parsed.error.message}`
15203
+ };
15204
+ }
15205
+ return {
15206
+ ok: true,
15207
+ credential: {
15208
+ guardServiceToken: parsed.data.token,
15209
+ guardEngagementId: parsed.data.engagementId,
15210
+ expiresAt: parsed.data.expiresAt
15211
+ }
15212
+ };
15213
+ }
15214
+ async function acquireCredentialByIdentityToken(oidc, controlOrigin, fetchFn = globalThis.fetch) {
15215
+ const minted = await requestIdentityToken(oidc, fetchFn);
15216
+ if (!minted.ok)
15217
+ return { ok: false, kind: "provider", message: minted.message };
15218
+ return exchangeIdentityToken(controlOrigin, minted.identityToken, fetchFn);
15219
+ }
15220
+ function credentialExchangeFailureMessage(failure, label = "[Halfcycle CI]") {
15221
+ switch (failure.kind) {
15222
+ case "provider":
15223
+ return `${label} No CI identity token \u2014 ${failure.message}.
15224
+ No evaluation was attempted. This job authenticates by asking its own runner for a signed identity token, which requires the workflow to grant:
15225
+
15226
+ permissions:
15227
+ contents: read
15228
+ id-token: write
15229
+
15230
+ BOTH LINES. Declaring any permission replaces the defaults rather than adding to them, so a block naming only the identity token takes read access away from the checkout step and a private repository stops checking out before this check is reached. If the workflow already has a permissions block, add \`id-token: write\` to it and leave the rest alone. Or supply the guard secrets instead.
15231
+ `;
15232
+ case "refused":
15233
+ return `${label} CI authentication refused (${failure.statusCode}): ${failure.message}
15234
+ This is NOT a guard violation and NOT an outage \u2014 the identity token was presented and the answer came back. Nothing was evaluated.
15235
+ `;
15236
+ case "service":
15237
+ return `${label} Infrastructure failure: the CI credential could not be obtained \u2014 ${failure.message}.
15238
+ This is NOT a guard violation. Fix the infrastructure issue and re-run.
15239
+ `;
15240
+ case "contract":
15241
+ return `${label} Infrastructure failure (contract error): ${failure.message}.
15242
+ The service answered; the suspect is this runner being older than it.
15243
+ `;
15244
+ }
15245
+ }
15246
+
15247
+ // dist/client.js
15248
+ var REQUEST_TIMEOUT_MS2 = 1e4;
14806
15249
  async function evaluate(url2, token, request, fetchFn = globalThis.fetch) {
14807
15250
  return attempt(
14808
15251
  url2,
@@ -14815,7 +15258,7 @@ async function evaluate(url2, token, request, fetchFn = globalThis.fetch) {
14815
15258
  }
14816
15259
  async function attempt(url2, token, request, fetchFn, retry) {
14817
15260
  const controller = new AbortController();
14818
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
15261
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
14819
15262
  let response;
14820
15263
  try {
14821
15264
  response = await fetchFn(`${url2}/evaluate`, {
@@ -14841,7 +15284,7 @@ async function attempt(url2, token, request, fetchFn, retry) {
14841
15284
  false
14842
15285
  );
14843
15286
  }
14844
- const message = isTimeout ? `Guard service timed out after ${REQUEST_TIMEOUT_MS}ms` : err instanceof Error ? err.message : String(err);
15287
+ const message = isTimeout ? `Guard service timed out after ${REQUEST_TIMEOUT_MS2}ms` : err instanceof Error ? err.message : String(err);
14845
15288
  return { ok: false, kind: "infra-error", message };
14846
15289
  }
14847
15290
  let body;
@@ -16170,7 +16613,7 @@ async function emitRunRecord(opts) {
16170
16613
  });
16171
16614
  await emitTelemetry(run, {
16172
16615
  controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
16173
- controlTelemetryToken: telemetryConfig.controlTelemetryToken,
16616
+ controlTelemetryToken: opts.credential ?? telemetryConfig.controlTelemetryToken,
16174
16617
  guardEvalLogDir: telemetryConfig.guardEvalLogDir
16175
16618
  }, { warn: (msg) => process.stderr.write(msg + "\n") });
16176
16619
  }
@@ -16204,7 +16647,7 @@ function emitNeverConfigured(missing, exitCode, reason) {
16204
16647
  process.exitCode = exitCode;
16205
16648
  }
16206
16649
  var HOOK_REMEDY = `Fix it by running "npx halfcycle" in this repository: it may open your browser to sign in first, then writes this engagement's credentials to ~/.halfcycle \u2014 outside the repository \u2014 and the hook loads them itself.`;
16207
- var CI_REMEDY = "This job cannot vouch for anything; supply the guard secrets to the workflow.";
16650
+ var CI_REMEDY = "This job cannot vouch for anything; supply the guard secrets to the workflow, or let it authenticate with no secret at all by granting the workflow permission to issue an id-token.";
16208
16651
  function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
16209
16652
  return `${label} GUARD NEVER CONFIGURED \u2014 this repository has no guard coverage.
16210
16653
  No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
@@ -16266,10 +16709,15 @@ This is not a service outage. The guard service is up and replying; ${cause}, so
16266
16709
  ${remedy}
16267
16710
  `;
16268
16711
  }
16269
- function emitCredentialRejected(statusCode, serviceMessage, exitCode) {
16270
- writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, HOOK_REMEDY));
16712
+ function emitCredentialRejected(statusCode, serviceMessage, exitCode, remedy = HOOK_REMEDY) {
16713
+ writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, remedy));
16271
16714
  process.exitCode = exitCode;
16272
16715
  }
16716
+ function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
16717
+ return `${label} This project's stored credential has expired and could not be renewed automatically, so changes in this session will not be checked.
16718
+ ${remedy}
16719
+ `;
16720
+ }
16273
16721
  function notRunReport(notRun, label = "[Halfcycle]") {
16274
16722
  if (!notRun || notRun.length === 0)
16275
16723
  return "";
@@ -16320,12 +16768,37 @@ async function runCi() {
16320
16768
  const context = loadChangeSetContext(repoRoot);
16321
16769
  const changeSet = buildChangeSet(fileDiffEntries, { ...context, excerpts: readExcerptConfig() });
16322
16770
  const configResult = readConfig();
16771
+ if (!configResult.config) {
16772
+ const oidc = readActionsOidcEnv();
16773
+ if (oidc) {
16774
+ const acquired = await acquireCredentialByIdentityToken(oidc, readControlOrigin().origin);
16775
+ if (!acquired.ok) {
16776
+ await emitRunRecord({
16777
+ runType: "ci",
16778
+ outcome: acquired.kind === "contract" ? "contract-error" : "infra-error",
16779
+ failureReason: `ci credential: ${acquired.kind}: ${acquired.message}`,
16780
+ changeSet
16781
+ });
16782
+ process.stderr.write(credentialExchangeFailureMessage(acquired));
16783
+ return 1;
16784
+ }
16785
+ return await evaluateAndAct({
16786
+ guardServiceUrl: readGuardOrigin().origin,
16787
+ guardServiceToken: acquired.credential.guardServiceToken,
16788
+ guardEngagementId: acquired.credential.guardEngagementId,
16789
+ changeSet
16790
+ });
16791
+ }
16792
+ }
16323
16793
  if (!configResult.config) {
16324
16794
  await emitRunRecord({ runType: "ci", outcome: "unconfigured", changeSet });
16325
16795
  process.stderr.write(neverConfiguredMessage(configResult.missing, CI_REMEDY, "[Halfcycle CI]"));
16326
16796
  return 1;
16327
16797
  }
16328
- const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
16798
+ return await evaluateAndAct({ ...configResult.config, changeSet });
16799
+ }
16800
+ async function evaluateAndAct(args2) {
16801
+ const { guardServiceUrl, guardServiceToken, guardEngagementId, changeSet } = args2;
16329
16802
  const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
16330
16803
  engagementId: guardEngagementId,
16331
16804
  changeSet,
@@ -16368,7 +16841,23 @@ async function runCi() {
16368
16841
  });
16369
16842
  await emitTelemetry(run, {
16370
16843
  controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
16371
- controlTelemetryToken: telemetryConfig.controlTelemetryToken,
16844
+ // THE CREDENTIAL THIS RUN EVALUATED WITH — the argument, never a second read of
16845
+ // the environment. `readTelemetryConfig().controlTelemetryToken` is the stored
16846
+ // `GUARD_SERVICE_TOKEN`, and on the stored path it is the SAME `process.env` read
16847
+ // this argument came from, so nothing changes there.
16848
+ //
16849
+ // **IT IS NOT `telemetryConfig.controlTelemetryToken ?? guardServiceToken`, AND
16850
+ // THE DIFFERENCE IS A HALF-MIGRATED WORKFLOW** (R-09 blocker). That `??` is
16851
+ // environment-first, so the stored variable wins wherever it is set — including
16852
+ // on the identity-token path, which is reached when `readConfig()` returned
16853
+ // `null`, i.e. when the pair is PARTIAL as well as when it is absent. A workflow
16854
+ // with `GUARD_ENGAGEMENT_ID` removed and `GUARD_SERVICE_TOKEN` still injected —
16855
+ // one of two workflow files edited, an org secret still mapped, one matrix leg
16856
+ // missed — would evaluate correctly against the engagement the plane resolved and
16857
+ // then POST its run record with the stale token, which `bindEngagement` refuses
16858
+ // 403. Every run record from that workflow is lost, and the only trace names a
16859
+ // status rather than the wrong credential.
16860
+ controlTelemetryToken: guardServiceToken,
16372
16861
  // Per-engagement log dir from HALFCYCLE_GUARD_EVAL_LOG_DIR (E1-T-02, #61).
16373
16862
  guardEvalLogDir: telemetryConfig.guardEvalLogDir
16374
16863
  }, {
@@ -16413,6 +16902,196 @@ function actOnEnvelope(envelope) {
16413
16902
  }
16414
16903
  }
16415
16904
 
16905
+ // dist/credential-store.js
16906
+ import { chmodSync, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
16907
+ import { homedir as homedir2, platform } from "node:os";
16908
+ import { dirname as dirname3, join as join5 } from "node:path";
16909
+ var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
16910
+ var TOKEN_READ_KEY = "GUARD_SERVICE_TOKEN";
16911
+ var UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16912
+ function composeEngagementEnvPath(engagementId, home) {
16913
+ return join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, ENGAGEMENT_ENV_FILENAME);
16914
+ }
16915
+ function readAccountCredential(controlOrigin, home) {
16916
+ let raw;
16917
+ try {
16918
+ raw = readFileSync5(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ACCOUNT_STORE_FILENAME), "utf-8");
16919
+ } catch {
16920
+ return void 0;
16921
+ }
16922
+ const entry = parseAccountStoreText(raw).accounts[normaliseOrigin(controlOrigin)];
16923
+ if (!entry || typeof entry.credential !== "string" || entry.credential.trim() === "") {
16924
+ return void 0;
16925
+ }
16926
+ return entry.credential;
16927
+ }
16928
+ function readStoredEngagementToken(envFilePath) {
16929
+ let raw;
16930
+ try {
16931
+ raw = readFileSync5(envFilePath, "utf-8");
16932
+ } catch {
16933
+ return void 0;
16934
+ }
16935
+ const value = parseEnvText(raw)[TOKEN_READ_KEY];
16936
+ return value && value.trim() !== "" ? value : void 0;
16937
+ }
16938
+ function writeRefreshedToken(opts) {
16939
+ if (!UUID_SHAPE.test(opts.engagementId)) {
16940
+ return { written: false, reason: "the engagement this session names is not one this store can be keyed by" };
16941
+ }
16942
+ let declaredReal;
16943
+ let composedReal;
16944
+ try {
16945
+ declaredReal = realpathSync(opts.envFilePath);
16946
+ composedReal = realpathSync(composeEngagementEnvPath(opts.engagementId, opts.home));
16947
+ } catch {
16948
+ return { written: false, reason: "the credential file named for this session could not be resolved on disk" };
16949
+ }
16950
+ if (declaredReal !== composedReal) {
16951
+ return {
16952
+ written: false,
16953
+ reason: "the credential file named for this session is not this engagement's own credential file"
16954
+ };
16955
+ }
16956
+ let existing;
16957
+ try {
16958
+ existing = readFileSync5(declaredReal, "utf-8");
16959
+ } catch {
16960
+ existing = null;
16961
+ }
16962
+ const body = reconcileEnvText(existing, { HALFCYCLE_TOKEN: opts.token, GUARD_SERVICE_TOKEN: opts.token }, REFRESHED_TOKEN_KEYS);
16963
+ const temp = join5(dirname3(declaredReal), `.${ENGAGEMENT_ENV_FILENAME}.${process.pid}.tmp`);
16964
+ try {
16965
+ writeFileSync2(temp, body, { mode: 384 });
16966
+ if (platform() !== "win32")
16967
+ chmodSync(temp, 384);
16968
+ renameSync2(temp, declaredReal);
16969
+ } catch (err) {
16970
+ try {
16971
+ unlinkSync2(temp);
16972
+ } catch {
16973
+ }
16974
+ return {
16975
+ written: false,
16976
+ reason: `the credential file could not be replaced: ${err instanceof Error ? err.message : String(err)}`
16977
+ };
16978
+ }
16979
+ return { written: true };
16980
+ }
16981
+
16982
+ // dist/credential-refresh.js
16983
+ var CONTROL_TIMEOUT_MS = 5e3;
16984
+ var UNAUTHORIZED = 401;
16985
+ async function probeAccount(controlOrigin, credential, fetchFn = globalThis.fetch) {
16986
+ const controller = new AbortController();
16987
+ const timer = setTimeout(() => controller.abort(), CONTROL_TIMEOUT_MS);
16988
+ try {
16989
+ const response = await fetchFn(`${controlOrigin}/account`, {
16990
+ method: "GET",
16991
+ headers: { Authorization: `Bearer ${credential}` },
16992
+ signal: controller.signal
16993
+ });
16994
+ return { reached: true, status: response.status };
16995
+ } catch {
16996
+ return { reached: false };
16997
+ } finally {
16998
+ clearTimeout(timer);
16999
+ }
17000
+ }
17001
+ async function mintThroughJoin(controlOrigin, engagementId, accountCredential, fetchFn) {
17002
+ const controller = new AbortController();
17003
+ const timer = setTimeout(() => controller.abort(), CONTROL_TIMEOUT_MS);
17004
+ let response;
17005
+ try {
17006
+ response = await fetchFn(`${controlOrigin}/engagements/${encodeURIComponent(engagementId)}/join`, {
17007
+ method: "POST",
17008
+ headers: {
17009
+ "Content-Type": "application/json",
17010
+ Authorization: `Bearer ${accountCredential}`
17011
+ },
17012
+ body: JSON.stringify({}),
17013
+ signal: controller.signal
17014
+ });
17015
+ } catch {
17016
+ return { ok: false, remedy: UNREACHABLE_REMEDY };
17017
+ } finally {
17018
+ clearTimeout(timer);
17019
+ }
17020
+ let body;
17021
+ try {
17022
+ body = await response.json();
17023
+ } catch {
17024
+ body = void 0;
17025
+ }
17026
+ if (!response.ok) {
17027
+ if (response.status === UNAUTHORIZED)
17028
+ return { ok: false, remedy: HOOK_REMEDY };
17029
+ return { ok: false, remedy: planeSentence(body) ?? UNREADABLE_REFUSAL_REMEDY };
17030
+ }
17031
+ const parsed = engagementCredentialResponseSchema.safeParse(body);
17032
+ if (!parsed.success)
17033
+ return { ok: false, remedy: UNREADABLE_MINT_REMEDY };
17034
+ return { ok: true, token: parsed.data.sessionToken, source: "mint" };
17035
+ }
17036
+ function planeSentence(body) {
17037
+ if (typeof body !== "object" || body === null)
17038
+ return void 0;
17039
+ const message = body.message;
17040
+ return typeof message === "string" && message.trim() !== "" ? message : void 0;
17041
+ }
17042
+ var UNREACHABLE_REMEDY = "The Halfcycle service could not be reached to renew this credential. Nothing is wrong with this repository; the next change will try again.";
17043
+ var UNREADABLE_REFUSAL_REMEDY = "The Halfcycle service declined to renew this credential and gave no reason this version could read.";
17044
+ var UNREADABLE_MINT_REMEDY = 'The Halfcycle service answered with a credential this version could not read. Update the installed Halfcycle runner: run "npx halfcycle" in this repository.';
17045
+ async function refreshEngagementCredential(opts) {
17046
+ const envFilePath = readEnvFilePath();
17047
+ if (!envFilePath)
17048
+ return { ok: false, remedy: HOOK_REMEDY };
17049
+ const stored = readStoredEngagementToken(envFilePath);
17050
+ if (stored !== void 0 && stored !== opts.refusedToken) {
17051
+ return { ok: true, token: stored, source: "disk" };
17052
+ }
17053
+ const controlOrigin = readControlOrigin().origin;
17054
+ const accountCredential = readAccountCredential(controlOrigin, opts.home);
17055
+ if (!accountCredential)
17056
+ return { ok: false, remedy: HOOK_REMEDY };
17057
+ const minted = await mintThroughJoin(controlOrigin, opts.engagementId, accountCredential, opts.fetchFn ?? globalThis.fetch);
17058
+ if (!minted.ok)
17059
+ return minted;
17060
+ const write = writeRefreshedToken({
17061
+ envFilePath,
17062
+ engagementId: opts.engagementId,
17063
+ token: minted.token,
17064
+ home: opts.home
17065
+ });
17066
+ if (!write.written) {
17067
+ const warn = opts.warn ?? ((message) => process.stderr.write(message));
17068
+ warn(`[Halfcycle] This engagement's credential was renewed for this run only \u2014 ${write.reason}.
17069
+ `);
17070
+ }
17071
+ return minted;
17072
+ }
17073
+ async function evaluateWithRefresh(opts) {
17074
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
17075
+ const first = await evaluate(opts.guardServiceUrl, opts.token, opts.request, fetchFn);
17076
+ if (first.ok)
17077
+ return { result: first, token: opts.token, remedy: HOOK_REMEDY };
17078
+ const failure = classifyFailure(first);
17079
+ if (failure.kind !== "credential" || failure.statusCode !== UNAUTHORIZED) {
17080
+ return { result: first, token: opts.token, remedy: HOOK_REMEDY };
17081
+ }
17082
+ const refreshed = await refreshEngagementCredential({
17083
+ engagementId: opts.engagementId,
17084
+ refusedToken: opts.token,
17085
+ fetchFn,
17086
+ home: opts.home,
17087
+ warn: opts.warn
17088
+ });
17089
+ if (!refreshed.ok)
17090
+ return { result: first, token: opts.token, remedy: refreshed.remedy };
17091
+ const retry = await evaluate(opts.guardServiceUrl, refreshed.token, opts.request, fetchFn);
17092
+ return { result: retry, token: refreshed.token, remedy: HOOK_REMEDY };
17093
+ }
17094
+
16416
17095
  // dist/hooks/post-tool-use.js
16417
17096
  var HANDLED_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
16418
17097
  async function runPostToolUse() {
@@ -16451,11 +17130,17 @@ async function runPostToolUse() {
16451
17130
  return;
16452
17131
  }
16453
17132
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
16454
- const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
17133
+ const evaluation = await evaluateWithRefresh({
17134
+ guardServiceUrl,
17135
+ token: guardServiceToken,
16455
17136
  engagementId: guardEngagementId,
16456
- changeSet,
16457
- clientCapabilities: deriveClientCapabilities(changeSet)
17137
+ request: {
17138
+ engagementId: guardEngagementId,
17139
+ changeSet,
17140
+ clientCapabilities: deriveClientCapabilities(changeSet)
17141
+ }
16458
17142
  });
17143
+ const clientResult = evaluation.result;
16459
17144
  if (!clientResult.ok) {
16460
17145
  const failure = classifyFailure(clientResult);
16461
17146
  await emitRunRecord({
@@ -16463,10 +17148,19 @@ async function runPostToolUse() {
16463
17148
  runType: "hook",
16464
17149
  outcome: outcomeForClientFailure(clientResult.kind),
16465
17150
  failureReason: failure.reason,
16466
- changeSet
17151
+ changeSet,
17152
+ credential: evaluation.token
16467
17153
  });
16468
17154
  if (failure.kind === "credential") {
16469
- emitCredentialRejected(failure.statusCode, failure.message, CREDENTIAL_REJECTED_EXIT_TOOL_USE);
17155
+ emitCredentialRejected(
17156
+ failure.statusCode,
17157
+ failure.message,
17158
+ CREDENTIAL_REJECTED_EXIT_TOOL_USE,
17159
+ // The remedy the renewal attempt earned: the sign-in when the account
17160
+ // credential is the thing that was refused, and otherwise the plane's own
17161
+ // sentence about why it would not mint.
17162
+ evaluation.remedy
17163
+ );
16470
17164
  return;
16471
17165
  }
16472
17166
  if (failure.kind === "contract") {
@@ -16489,7 +17183,13 @@ async function runPostToolUse() {
16489
17183
  });
16490
17184
  await emitTelemetry(run, {
16491
17185
  controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
16492
- controlTelemetryToken: telemetryConfig.controlTelemetryToken,
17186
+ // THE CREDENTIAL IN FORCE, NOT THE ONE IN THE ENVIRONMENT (T-08,
17187
+ // credential-lifecycle). `readTelemetryConfig` reads the bearer off
17188
+ // `process.env` after the evaluation, and a renewal does not write there — so
17189
+ // taking its value on a run that just recovered would present the credential
17190
+ // the plane had already refused and drop the record with a 401, silently,
17191
+ // because telemetry is best-effort.
17192
+ controlTelemetryToken: evaluation.token,
16493
17193
  // Per-engagement log dir from HALFCYCLE_GUARD_EVAL_LOG_DIR (E1-T-02, #61).
16494
17194
  guardEvalLogDir: telemetryConfig.guardEvalLogDir
16495
17195
  }, {
@@ -16528,9 +17228,9 @@ function readStdin() {
16528
17228
 
16529
17229
  // dist/hooks/marker.js
16530
17230
  import { createHash } from "node:crypto";
16531
- import { existsSync as existsSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, readdirSync as readdirSync3, statSync } from "node:fs";
17231
+ import { existsSync as existsSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
16532
17232
  import { tmpdir } from "node:os";
16533
- import { join as join5 } from "node:path";
17233
+ import { join as join6 } from "node:path";
16534
17234
  var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
16535
17235
  var MARKER_PREFIX = "halfcycle-session-";
16536
17236
  var MARKER_SUFFIX = ".ref";
@@ -16548,19 +17248,19 @@ function markerFileName(repoRoot, sessionId) {
16548
17248
  return `${MARKER_PREFIX}${key}${MARKER_SUFFIX}`;
16549
17249
  }
16550
17250
  function markerPath(repoRoot, sessionId) {
16551
- return join5(markerDir(), markerFileName(repoRoot, sessionId));
17251
+ return join6(markerDir(), markerFileName(repoRoot, sessionId));
16552
17252
  }
16553
17253
  function dedupePath(repoRoot, sessionId) {
16554
17254
  const hash2 = repoHash(repoRoot);
16555
17255
  const key = sessionId && sessionId.length > 0 ? `${hash2}-${sessionId}` : hash2;
16556
- return join5(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
17256
+ return join6(markerDir(), `${MARKER_PREFIX}${key}${DEDUPE_SUFFIX}`);
16557
17257
  }
16558
17258
  function readMarker(repoRoot, sessionId) {
16559
17259
  const path = markerPath(repoRoot, sessionId);
16560
17260
  if (!existsSync2(path))
16561
17261
  return null;
16562
17262
  try {
16563
- return readFileSync5(path, "utf-8").trim() || null;
17263
+ return readFileSync6(path, "utf-8").trim() || null;
16564
17264
  } catch {
16565
17265
  return null;
16566
17266
  }
@@ -16569,7 +17269,7 @@ function deleteMarker(repoRoot, sessionId) {
16569
17269
  for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
16570
17270
  try {
16571
17271
  if (existsSync2(path))
16572
- unlinkSync2(path);
17272
+ unlinkSync3(path);
16573
17273
  } catch {
16574
17274
  }
16575
17275
  }
@@ -16579,7 +17279,7 @@ function readDedupe(repoRoot, sessionId) {
16579
17279
  if (!existsSync2(path))
16580
17280
  return null;
16581
17281
  try {
16582
- const parsed = JSON.parse(readFileSync5(path, "utf-8"));
17282
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
16583
17283
  if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
16584
17284
  return parsed;
16585
17285
  }
@@ -16590,7 +17290,7 @@ function readDedupe(repoRoot, sessionId) {
16590
17290
  }
16591
17291
  function writeDedupe(repoRoot, sessionId, state) {
16592
17292
  try {
16593
- writeFileSync2(dedupePath(repoRoot, sessionId), JSON.stringify(state), "utf-8");
17293
+ writeFileSync3(dedupePath(repoRoot, sessionId), JSON.stringify(state), "utf-8");
16594
17294
  } catch {
16595
17295
  }
16596
17296
  }
@@ -16621,10 +17321,10 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
16621
17321
  continue;
16622
17322
  if (name === keepName)
16623
17323
  continue;
16624
- const full = join5(dir, name);
17324
+ const full = join6(dir, name);
16625
17325
  let unusable = false;
16626
17326
  try {
16627
- const ref = readFileSync5(full, "utf-8").trim();
17327
+ const ref = readFileSync6(full, "utf-8").trim();
16628
17328
  if (!ref || !refKnown(ref))
16629
17329
  unusable = true;
16630
17330
  } catch {
@@ -16717,11 +17417,17 @@ async function runSessionDiff(input, kind) {
16717
17417
  return;
16718
17418
  }
16719
17419
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
16720
- const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
17420
+ const evaluation = await evaluateWithRefresh({
17421
+ guardServiceUrl,
17422
+ token: guardServiceToken,
16721
17423
  engagementId: guardEngagementId,
16722
- changeSet,
16723
- clientCapabilities: deriveClientCapabilities(changeSet)
17424
+ request: {
17425
+ engagementId: guardEngagementId,
17426
+ changeSet,
17427
+ clientCapabilities: deriveClientCapabilities(changeSet)
17428
+ }
16724
17429
  });
17430
+ const clientResult = evaluation.result;
16725
17431
  if (!clientResult.ok) {
16726
17432
  const failure = classifyFailure(clientResult);
16727
17433
  await emitRunRecord({
@@ -16730,10 +17436,19 @@ async function runSessionDiff(input, kind) {
16730
17436
  outcome: outcomeForClientFailure(clientResult.kind),
16731
17437
  failureReason: failure.reason,
16732
17438
  changeSet,
16733
- diffBase
17439
+ diffBase,
17440
+ credential: evaluation.token
16734
17441
  });
16735
17442
  if (failure.kind === "credential") {
16736
- emitCredentialRejected(failure.statusCode, failure.message, CREDENTIAL_REJECTED_EXIT_STOP_FAMILY);
17443
+ emitCredentialRejected(
17444
+ failure.statusCode,
17445
+ failure.message,
17446
+ CREDENTIAL_REJECTED_EXIT_STOP_FAMILY,
17447
+ // The remedy the renewal attempt earned: the sign-in when the account
17448
+ // credential is what was refused, and otherwise the plane's own sentence
17449
+ // about why it would not mint.
17450
+ evaluation.remedy
17451
+ );
16737
17452
  return;
16738
17453
  }
16739
17454
  if (failure.kind === "contract") {
@@ -16755,7 +17470,12 @@ async function runSessionDiff(input, kind) {
16755
17470
  });
16756
17471
  await emitTelemetry(run, {
16757
17472
  controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
16758
- controlTelemetryToken: telemetryConfig.controlTelemetryToken,
17473
+ // THE CREDENTIAL IN FORCE, NOT THE ONE IN THE ENVIRONMENT (T-08,
17474
+ // credential-lifecycle). The telemetry bearer is otherwise read off
17475
+ // `process.env` after the evaluation, which a renewal does not write to — so a
17476
+ // run that just recovered would present the refused credential and drop its
17477
+ // record with a 401, silently, because telemetry is best-effort.
17478
+ controlTelemetryToken: evaluation.token,
16759
17479
  guardEvalLogDir: telemetryConfig.guardEvalLogDir
16760
17480
  }, {
16761
17481
  warn: (msg) => process.stderr.write(msg + "\n")
@@ -16836,17 +17556,17 @@ async function runStop() {
16836
17556
  }
16837
17557
 
16838
17558
  // dist/resolve-engagement.js
16839
- import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync } from "node:fs";
16840
- import { join as join6, sep as sep2 } from "node:path";
16841
- import { homedir as homedir2 } from "node:os";
17559
+ import { existsSync as existsSync3, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
17560
+ import { join as join7, sep as sep2 } from "node:path";
17561
+ import { homedir as homedir3 } from "node:os";
16842
17562
  function defaultRegistryPath() {
16843
- return join6(homedir2(), ".halfcycle", "registry.json");
17563
+ return join7(homedir3(), ".halfcycle", "registry.json");
16844
17564
  }
16845
17565
  function readRegistry(registryPath = defaultRegistryPath()) {
16846
17566
  if (!existsSync3(registryPath))
16847
17567
  return {};
16848
17568
  try {
16849
- const parsed = JSON.parse(readFileSync6(registryPath, "utf-8"));
17569
+ const parsed = JSON.parse(readFileSync7(registryPath, "utf-8"));
16850
17570
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
16851
17571
  return {};
16852
17572
  const out = {};
@@ -16861,7 +17581,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
16861
17581
  }
16862
17582
  function canonicalise(p) {
16863
17583
  try {
16864
- return realpathSync(p);
17584
+ return realpathSync2(p);
16865
17585
  } catch {
16866
17586
  return p;
16867
17587
  }
@@ -16893,9 +17613,41 @@ function runResolveEngagement(cwd, registryPath) {
16893
17613
  return 0;
16894
17614
  }
16895
17615
 
17616
+ // dist/refresh-credential.js
17617
+ var UNAUTHORIZED2 = 401;
17618
+ async function runRefreshCredential(opts) {
17619
+ if (readNeverConfiguredReason() === "not-this-account")
17620
+ return 0;
17621
+ if (!readEnvFilePath())
17622
+ return 0;
17623
+ const configResult = readConfig();
17624
+ if (!configResult.config)
17625
+ return 0;
17626
+ const { guardServiceToken, guardEngagementId } = configResult.config;
17627
+ const controlOrigin = readControlOrigin().origin;
17628
+ const fetchFn = opts?.fetchFn ?? globalThis.fetch;
17629
+ const accountCredential = readAccountCredential(controlOrigin, opts?.home);
17630
+ if (accountCredential)
17631
+ await probeAccount(controlOrigin, accountCredential, fetchFn);
17632
+ const probe = await probeAccount(controlOrigin, guardServiceToken, fetchFn);
17633
+ if (!probe.reached || probe.status !== UNAUTHORIZED2)
17634
+ return 0;
17635
+ const refreshed = await refreshEngagementCredential({
17636
+ engagementId: guardEngagementId,
17637
+ refusedToken: guardServiceToken,
17638
+ fetchFn,
17639
+ home: opts?.home
17640
+ });
17641
+ if (!refreshed.ok) {
17642
+ const write = opts?.write ?? ((text) => process.stdout.write(text));
17643
+ write(credentialRenewalFailedMessage(refreshed.remedy));
17644
+ }
17645
+ return 0;
17646
+ }
17647
+
16896
17648
  // dist/capture.js
16897
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "node:fs";
16898
- import { dirname as dirname3 } from "node:path";
17649
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "node:fs";
17650
+ import { dirname as dirname4 } from "node:path";
16899
17651
  function runCapture(provider, endpoint, opts = {}) {
16900
17652
  const repoRoot = opts.repoRoot ?? process.cwd();
16901
17653
  const raw = opts.input ?? readInputFile(opts.fromFile);
@@ -16930,8 +17682,8 @@ function runCapture(provider, endpoint, opts = {}) {
16930
17682
  const fixture = fixturePath(repoRoot, provider, slug);
16931
17683
  const manifestFile = manifestPath(repoRoot);
16932
17684
  try {
16933
- mkdirSync3(dirname3(fixture), { recursive: true });
16934
- writeFileSync3(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
17685
+ mkdirSync3(dirname4(fixture), { recursive: true });
17686
+ writeFileSync4(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
16935
17687
  } catch (err) {
16936
17688
  const msg = err instanceof Error ? err.message : String(err);
16937
17689
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -16962,7 +17714,7 @@ function readInputFile(fromFile) {
16962
17714
  if (!fromFile)
16963
17715
  return void 0;
16964
17716
  try {
16965
- return readFileSync7(fromFile, "utf-8");
17717
+ return readFileSync8(fromFile, "utf-8");
16966
17718
  } catch {
16967
17719
  return void 0;
16968
17720
  }
@@ -16981,6 +17733,11 @@ async function main() {
16981
17733
  process.exit(code);
16982
17734
  return;
16983
17735
  }
17736
+ if (cmd === "refresh-credential") {
17737
+ const code = await runRefreshCredential();
17738
+ process.exit(code);
17739
+ return;
17740
+ }
16984
17741
  if (cmd === "resolve-engagement") {
16985
17742
  const cwd = sub ?? process.cwd();
16986
17743
  const code = runResolveEngagement(cwd);
@@ -17045,6 +17802,7 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
17045
17802
  halfcycle-runner hook post-tool-use
17046
17803
  halfcycle-runner hook stop
17047
17804
  halfcycle-runner hook subagent-stop
17805
+ halfcycle-runner refresh-credential
17048
17806
  halfcycle-runner resolve-engagement <cwd>
17049
17807
  halfcycle-runner capture <provider> <endpoint> [--from <file>]
17050
17808
  `);