halfcycle 0.3.24 → 0.3.26

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.24",
3
+ "version": "0.3.26",
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
  {
@@ -15111,6 +15111,128 @@ var ciTokenExchangeResponseSchema = external_exports.object({
15111
15111
  engagementId: external_exports.string()
15112
15112
  }).strict();
15113
15113
 
15114
+ // dist/hook-protocol.js
15115
+ import { writeSync } from "node:fs";
15116
+ function emitBlock(reason) {
15117
+ process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
15118
+ process.exit(0);
15119
+ }
15120
+ function emitWarning(text) {
15121
+ process.stdout.write(text + "\n");
15122
+ }
15123
+ function emitInfraWarning(message) {
15124
+ process.stdout.write(`[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. Session continues (fail-open).
15125
+ `);
15126
+ }
15127
+ function emitLocalFault(message) {
15128
+ process.stdout.write(`[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).
15129
+ `);
15130
+ }
15131
+ function emitContractFault(message) {
15132
+ process.stdout.write(`[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
15133
+ 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).
15134
+ `);
15135
+ }
15136
+ var UNCONFIGURED_EXIT_TOOL_USE = 2;
15137
+ var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
15138
+ function emitNeverConfigured(missing, exitCode, reason) {
15139
+ writeSync(2, neverConfiguredReport(missing, reason));
15140
+ process.exitCode = exitCode;
15141
+ }
15142
+ 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.`;
15143
+ var PERMISSIONS_BLOCK = "\n permissions:\n contents: read\n id-token: write\n\n";
15144
+ var BOTH_LINES_SENTENCE = "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.";
15145
+ var CI_CANNOT_PROVE_PROJECT = "has no way to prove which project it belongs to";
15146
+ var CI_REMEDY_LEAD = "Add this to the workflow \u2014 at the top of the file, or on this job:\n" + PERMISSIONS_BLOCK + `${BOTH_LINES_SENTENCE} Then, once, on your own machine in this project, if you have not already: \`npx halfcycle ci bind <owner>/<repo>\`. That is the whole configuration \u2014 there is no secret to store.`;
15147
+ var CI_OLDER_SETUP = "The older setup, GUARD_SERVICE_TOKEN and GUARD_ENGAGEMENT_ID supplied as repository secrets, still works unchanged, and a job that has both needs neither line.";
15148
+ var CI_REMEDY = `This job cannot vouch for anything, because it ${CI_CANNOT_PROVE_PROJECT}. ${CI_REMEDY_LEAD} ${CI_OLDER_SETUP}`;
15149
+ var NEVER_CONFIGURED_HEADLINE = "GUARD NEVER CONFIGURED";
15150
+ var NOT_A_SERVICE_OUTAGE = "This is not a service outage.";
15151
+ function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
15152
+ return `${label} ${NEVER_CONFIGURED_HEADLINE} \u2014 this repository has no guard coverage.
15153
+ No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
15154
+ ${NOT_A_SERVICE_OUTAGE} It means the guard has never run here \u2014 not on this change, and not on any change before it.
15155
+ ${remedy}
15156
+ `;
15157
+ }
15158
+ function ciNeverConfiguredMessage() {
15159
+ return `[Halfcycle CI] ${NEVER_CONFIGURED_HEADLINE} \u2014 this job ${CI_CANNOT_PROVE_PROJECT}, so it checked nothing. ${CI_REMEDY_LEAD}
15160
+ ${NOT_A_SERVICE_OUTAGE} Nothing was sent: this job holds no credential and was not permitted to ask GitHub for a signed identity token. It speaks for this job on this change only \u2014 other jobs and earlier changes it did not see.
15161
+ ${CI_OLDER_SETUP}
15162
+ `;
15163
+ }
15164
+ function notThisAccountReport(label) {
15165
+ return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. Ask the project's owner for view access, or \u2014 if this account is wrong \u2014 sign in as the one that owns the project and run "npx halfcycle" again.
15166
+ `;
15167
+ }
15168
+ function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
15169
+ switch (reason) {
15170
+ case "no-credential-yet":
15171
+ return neverConfiguredMessage(missing, HOOK_REMEDY, label);
15172
+ case "not-this-account":
15173
+ return notThisAccountReport(label);
15174
+ }
15175
+ }
15176
+ var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
15177
+ function classifyFailure(failure) {
15178
+ if (failure.kind === "contract-error") {
15179
+ return {
15180
+ kind: "contract",
15181
+ message: failure.message,
15182
+ label: "contract error",
15183
+ reason: `contract error: ${failure.message}`
15184
+ };
15185
+ }
15186
+ if (failure.kind === "wire-error" && CREDENTIAL_REJECTED_STATUSES.has(failure.statusCode)) {
15187
+ const label = `credential rejected (${failure.statusCode})`;
15188
+ return {
15189
+ kind: "credential",
15190
+ statusCode: failure.statusCode,
15191
+ message: failure.message,
15192
+ label,
15193
+ reason: `${label}: ${failure.message}`
15194
+ };
15195
+ }
15196
+ return {
15197
+ kind: "service",
15198
+ message: failure.message,
15199
+ // Unchanged wording for this class ON PURPOSE — a genuine outage still says
15200
+ // exactly what it said before, and still fails open locally. This task moved
15201
+ // ONE condition out of this branch; it did not redefine the branch.
15202
+ label: "infrastructure failure",
15203
+ reason: `infrastructure failure: ${failure.message}`
15204
+ };
15205
+ }
15206
+ var CREDENTIAL_REJECTED_EXIT_TOOL_USE = UNCONFIGURED_EXIT_TOOL_USE;
15207
+ var CREDENTIAL_REJECTED_EXIT_STOP_FAMILY = UNCONFIGURED_EXIT_STOP_FAMILY;
15208
+ function credentialRejectedMessage(statusCode, serviceMessage, remedy, label = "[Halfcycle]") {
15209
+ const forbidden = statusCode === 403;
15210
+ const statusName = forbidden ? "Forbidden" : "Unauthorized";
15211
+ const cause = forbidden ? `this credential is not authorised for the engagement this repository is sending` : `the credential this repository is sending is not one it accepts`;
15212
+ return `${label} GUARD CREDENTIAL REJECTED \u2014 the guard service answered and refused this credential.
15213
+ No evaluation was performed: the service returned ${statusCode} ${statusName} \u2014 ${serviceMessage}
15214
+ This is not a service outage. The guard service is up and replying; ${cause}, so this change was not checked.
15215
+ ${remedy}
15216
+ `;
15217
+ }
15218
+ function emitCredentialRejected(statusCode, serviceMessage, exitCode, remedy = HOOK_REMEDY) {
15219
+ writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, remedy));
15220
+ process.exitCode = exitCode;
15221
+ }
15222
+ function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
15223
+ return `${label} This project's stored credential has expired and could not be renewed automatically, so changes in this session will not be checked.
15224
+ ${remedy}
15225
+ `;
15226
+ }
15227
+ function notRunReport(notRun, label = "[Halfcycle]") {
15228
+ if (!notRun || notRun.length === 0)
15229
+ return "";
15230
+ const count = notRun.length;
15231
+ const noun = count === 1 ? "check" : "checks";
15232
+ const lines = notRun.map((entry) => ` [NOT RUN] ${entry.patternRef}: ${entry.reason}`);
15233
+ return [`${label} ${count} ${noun} did not run on this change:`, ...lines].join("\n");
15234
+ }
15235
+
15114
15236
  // dist/ci-oidc.js
15115
15237
  var CI_EXCHANGE_PATH = "/auth/ci/exchange";
15116
15238
  var REQUEST_TIMEOUT_MS = 1e4;
@@ -15222,12 +15344,10 @@ function credentialExchangeFailureMessage(failure, label = "[Halfcycle CI]") {
15222
15344
  case "provider":
15223
15345
  return `${label} No CI identity token \u2014 ${failure.message}.
15224
15346
  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.
15347
+ ` + // The block and the both-lines sentence come from the runner's one wording
15348
+ // home (T-15): this text and the never-configured remedy must never
15349
+ // disagree on what the block is.
15350
+ PERMISSIONS_BLOCK + `${BOTH_LINES_SENTENCE} 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
15351
  `;
15232
15352
  case "refused":
15233
15353
  return `${label} CI authentication refused (${failure.statusCode}): ${failure.message}
@@ -15758,7 +15878,7 @@ function parseRawSql(line) {
15758
15878
  }
15759
15879
  function parseQueryBuilder(rawLine) {
15760
15880
  const line = stripDiffPrefix(rawLine);
15761
- const m = line.match(/['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
15881
+ const m = line.match(/(?<!\bcreate(?:Hash|Hmac|Sign|Verify|Cipheriv|Decipheriv)\s*\([^)]*)['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
15762
15882
  if (m) {
15763
15883
  const kind = m[2].toLowerCase();
15764
15884
  return { kind, table: m[1].toLowerCase(), whereColumns: [] };
@@ -16324,6 +16444,22 @@ function isCodeFile(filePath) {
16324
16444
  }
16325
16445
  return true;
16326
16446
  }
16447
+ var INSTALLER_OUTPUT_DIR = ".halfcycle/";
16448
+ function isInstallerOutput(relPath) {
16449
+ const normalised = relPath.replace(/\\/g, "/");
16450
+ return normalised === INSTALLER_OUTPUT_DIR.slice(0, -1) || normalised.startsWith(INSTALLER_OUTPUT_DIR);
16451
+ }
16452
+ function partitionInstallerOutput(files) {
16453
+ const evaluated = [];
16454
+ const installerOutput = [];
16455
+ for (const file2 of files) {
16456
+ if (isInstallerOutput(file2))
16457
+ installerOutput.push(file2);
16458
+ else
16459
+ evaluated.push(file2);
16460
+ }
16461
+ return { evaluated, installerOutput };
16462
+ }
16327
16463
  function buildChangeSet(files, context = {}) {
16328
16464
  const excerptConfig = context.excerpts;
16329
16465
  const excerptsEnabled = excerptConfig?.enabled === true;
@@ -16613,120 +16749,13 @@ async function emitRunRecord(opts) {
16613
16749
  });
16614
16750
  await emitTelemetry(run, {
16615
16751
  controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
16616
- controlTelemetryToken: opts.credential ?? telemetryConfig.controlTelemetryToken,
16752
+ // No bearer at all on a refused credential: `emitTelemetry` skips the POST
16753
+ // when it has nothing to authenticate with, which is the true state here.
16754
+ controlTelemetryToken: opts.credentialRefused ? void 0 : opts.credential ?? telemetryConfig.controlTelemetryToken,
16617
16755
  guardEvalLogDir: telemetryConfig.guardEvalLogDir
16618
16756
  }, { warn: (msg) => process.stderr.write(msg + "\n") });
16619
16757
  }
16620
16758
 
16621
- // dist/hook-protocol.js
16622
- import { writeSync } from "node:fs";
16623
- function emitBlock(reason) {
16624
- process.stdout.write(JSON.stringify({ decision: "block", reason }) + "\n");
16625
- process.exit(0);
16626
- }
16627
- function emitWarning(text) {
16628
- process.stdout.write(text + "\n");
16629
- }
16630
- function emitInfraWarning(message) {
16631
- process.stdout.write(`[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. Session continues (fail-open).
16632
- `);
16633
- }
16634
- function emitLocalFault(message) {
16635
- process.stdout.write(`[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).
16636
- `);
16637
- }
16638
- function emitContractFault(message) {
16639
- process.stdout.write(`[Halfcycle] WARNING: Guard response could not be understood \u2014 ${message}
16640
- 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).
16641
- `);
16642
- }
16643
- var UNCONFIGURED_EXIT_TOOL_USE = 2;
16644
- var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
16645
- function emitNeverConfigured(missing, exitCode, reason) {
16646
- writeSync(2, neverConfiguredReport(missing, reason));
16647
- process.exitCode = exitCode;
16648
- }
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.`;
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.";
16651
- function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
16652
- return `${label} GUARD NEVER CONFIGURED \u2014 this repository has no guard coverage.
16653
- No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
16654
- This is not a service outage. It means the guard has never run here \u2014 not on this change, and not on any change before it.
16655
- ${remedy}
16656
- `;
16657
- }
16658
- function notThisAccountReport(label) {
16659
- return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. Ask the project's owner for view access, or \u2014 if this account is wrong \u2014 sign in as the one that owns the project and run "npx halfcycle" again.
16660
- `;
16661
- }
16662
- function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
16663
- switch (reason) {
16664
- case "no-credential-yet":
16665
- return neverConfiguredMessage(missing, HOOK_REMEDY, label);
16666
- case "not-this-account":
16667
- return notThisAccountReport(label);
16668
- }
16669
- }
16670
- var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
16671
- function classifyFailure(failure) {
16672
- if (failure.kind === "contract-error") {
16673
- return {
16674
- kind: "contract",
16675
- message: failure.message,
16676
- label: "contract error",
16677
- reason: `contract error: ${failure.message}`
16678
- };
16679
- }
16680
- if (failure.kind === "wire-error" && CREDENTIAL_REJECTED_STATUSES.has(failure.statusCode)) {
16681
- const label = `credential rejected (${failure.statusCode})`;
16682
- return {
16683
- kind: "credential",
16684
- statusCode: failure.statusCode,
16685
- message: failure.message,
16686
- label,
16687
- reason: `${label}: ${failure.message}`
16688
- };
16689
- }
16690
- return {
16691
- kind: "service",
16692
- message: failure.message,
16693
- // Unchanged wording for this class ON PURPOSE — a genuine outage still says
16694
- // exactly what it said before, and still fails open locally. This task moved
16695
- // ONE condition out of this branch; it did not redefine the branch.
16696
- label: "infrastructure failure",
16697
- reason: `infrastructure failure: ${failure.message}`
16698
- };
16699
- }
16700
- var CREDENTIAL_REJECTED_EXIT_TOOL_USE = UNCONFIGURED_EXIT_TOOL_USE;
16701
- var CREDENTIAL_REJECTED_EXIT_STOP_FAMILY = UNCONFIGURED_EXIT_STOP_FAMILY;
16702
- function credentialRejectedMessage(statusCode, serviceMessage, remedy, label = "[Halfcycle]") {
16703
- const forbidden = statusCode === 403;
16704
- const statusName = forbidden ? "Forbidden" : "Unauthorized";
16705
- const cause = forbidden ? `this credential is not authorised for the engagement this repository is sending` : `the credential this repository is sending is not one it accepts`;
16706
- return `${label} GUARD CREDENTIAL REJECTED \u2014 the guard service answered and refused this credential.
16707
- No evaluation was performed: the service returned ${statusCode} ${statusName} \u2014 ${serviceMessage}
16708
- This is not a service outage. The guard service is up and replying; ${cause}, so this change was not checked.
16709
- ${remedy}
16710
- `;
16711
- }
16712
- function emitCredentialRejected(statusCode, serviceMessage, exitCode, remedy = HOOK_REMEDY) {
16713
- writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, remedy));
16714
- process.exitCode = exitCode;
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
- }
16721
- function notRunReport(notRun, label = "[Halfcycle]") {
16722
- if (!notRun || notRun.length === 0)
16723
- return "";
16724
- const count = notRun.length;
16725
- const noun = count === 1 ? "check" : "checks";
16726
- const lines = notRun.map((entry) => ` [NOT RUN] ${entry.patternRef}: ${entry.reason}`);
16727
- return [`${label} ${count} ${noun} did not run on this change:`, ...lines].join("\n");
16728
- }
16729
-
16730
16759
  // dist/ci.js
16731
16760
  async function runCi() {
16732
16761
  const repoRootResult = getRepoRoot(process.cwd());
@@ -16750,11 +16779,13 @@ async function runCi() {
16750
16779
  `);
16751
16780
  return 1;
16752
16781
  }
16753
- const changedFiles = changedFilesResult.value;
16754
- process.stdout.write(`[Halfcycle CI] Diff base ${mergeBase.slice(0, 12)} \u2014 ${describeDiffBase(diffBase)}. ${changedFiles.length} file(s) to evaluate.
16782
+ const { evaluated: changedFiles, installerOutput } = partitionInstallerOutput(changedFilesResult.value);
16783
+ const setAside = installerOutput.length === 1 ? ` 1 more under ${INSTALLER_OUTPUT_DIR} is Halfcycle's own file and is not evaluated.` : installerOutput.length > 1 ? ` ${installerOutput.length} more under ${INSTALLER_OUTPUT_DIR} are Halfcycle's own files and are not evaluated.` : "";
16784
+ process.stdout.write(`[Halfcycle CI] Diff base ${mergeBase.slice(0, 12)} \u2014 ${describeDiffBase(diffBase)}. ${changedFiles.length} file(s) to evaluate.${setAside}
16755
16785
  `);
16756
16786
  if (changedFiles.length === 0) {
16757
- process.stdout.write(`[Halfcycle CI] No changed files against the diff base \u2014 nothing to evaluate.
16787
+ process.stdout.write(installerOutput.length > 0 ? `[Halfcycle CI] Nothing to evaluate: every changed file is under ${INSTALLER_OUTPUT_DIR}, which Halfcycle writes itself and does not check.
16788
+ ` : `[Halfcycle CI] No changed files against the diff base \u2014 nothing to evaluate.
16758
16789
  `);
16759
16790
  return 0;
16760
16791
  }
@@ -16792,7 +16823,7 @@ async function runCi() {
16792
16823
  }
16793
16824
  if (!configResult.config) {
16794
16825
  await emitRunRecord({ runType: "ci", outcome: "unconfigured", changeSet });
16795
- process.stderr.write(neverConfiguredMessage(configResult.missing, CI_REMEDY, "[Halfcycle CI]"));
16826
+ process.stderr.write(ciNeverConfiguredMessage());
16796
16827
  return 1;
16797
16828
  }
16798
16829
  return await evaluateAndAct({ ...configResult.config, changeSet });
@@ -16811,7 +16842,8 @@ async function evaluateAndAct(args2) {
16811
16842
  runType: "ci",
16812
16843
  outcome: outcomeForClientFailure(clientResult.kind),
16813
16844
  failureReason: failure.reason,
16814
- changeSet
16845
+ changeSet,
16846
+ credentialRefused: failure.kind === "credential"
16815
16847
  });
16816
16848
  if (failure.kind === "credential") {
16817
16849
  process.stderr.write(credentialRejectedMessage(failure.statusCode, failure.message, CI_REMEDY, "[Halfcycle CI]"));
@@ -16863,13 +16895,17 @@ async function evaluateAndAct(args2) {
16863
16895
  }, {
16864
16896
  warn: (msg) => process.stderr.write(msg + "\n")
16865
16897
  });
16866
- return actOnEnvelope(clientResult.envelope);
16898
+ return actOnEnvelope(clientResult.envelope, {
16899
+ files: changeSet.files.length,
16900
+ signals: changeSetDescriptorCount(changeSet)
16901
+ });
16867
16902
  }
16868
- function actOnEnvelope(envelope) {
16903
+ function actOnEnvelope(envelope, scope) {
16869
16904
  const notRun = notRunReport(envelope.notRun, "[Halfcycle CI]");
16870
16905
  if (notRun)
16871
16906
  process.stdout.write(notRun + "\n");
16872
16907
  if (envelope.guardsFired.length === 0) {
16908
+ process.stdout.write(cleanRunSummary(envelope.explanation, scope) + "\n");
16873
16909
  return 0;
16874
16910
  }
16875
16911
  if (envelope.blocking) {
@@ -16901,6 +16937,13 @@ function actOnEnvelope(envelope) {
16901
16937
  return 0;
16902
16938
  }
16903
16939
  }
16940
+ function cleanRunSummary(verdict, scope) {
16941
+ const files = `${scope.files} file(s) evaluated`;
16942
+ if (scope.signals === 0) {
16943
+ return `[Halfcycle CI] ${verdict} ${files}, 0 signals found to check \u2014 nothing in this change is of a kind the guards look at (env references, SQL writes, schema constraints, wire-bound numeric columns, marked third-party endpoints).`;
16944
+ }
16945
+ return `[Halfcycle CI] ${verdict} ${files}, ${scope.signals} signal(s) checked.`;
16946
+ }
16904
16947
 
16905
16948
  // dist/credential-store.js
16906
16949
  import { chmodSync, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
@@ -17118,6 +17161,8 @@ async function runPostToolUse() {
17118
17161
  const diffContent = isNotebook ? "" : getSingleFileDiff(absFilePath, repoRoot);
17119
17162
  const { relative: pathRelative } = await import("node:path");
17120
17163
  const relPath = pathRelative(repoRoot, absFilePath);
17164
+ if (isInstallerOutput(relPath))
17165
+ return;
17121
17166
  const context = loadChangeSetContext(repoRoot);
17122
17167
  const changeSet = buildChangeSet([{ path: relPath, diffContent }], {
17123
17168
  ...context,
@@ -17149,7 +17194,8 @@ async function runPostToolUse() {
17149
17194
  outcome: outcomeForClientFailure(clientResult.kind),
17150
17195
  failureReason: failure.reason,
17151
17196
  changeSet,
17152
- credential: evaluation.token
17197
+ credential: evaluation.token,
17198
+ credentialRefused: failure.kind === "credential"
17153
17199
  });
17154
17200
  if (failure.kind === "credential") {
17155
17201
  emitCredentialRejected(
@@ -17394,7 +17440,8 @@ async function runSessionDiff(input, kind) {
17394
17440
  emitLocalFault(`Could not get session diff: ${collected.message}`);
17395
17441
  return;
17396
17442
  }
17397
- const { files: changedFiles, getDiff } = collected;
17443
+ const { getDiff } = collected;
17444
+ const { evaluated: changedFiles } = partitionInstallerOutput(collected.files);
17398
17445
  if (changedFiles.length === 0) {
17399
17446
  return;
17400
17447
  }
@@ -17437,7 +17484,8 @@ async function runSessionDiff(input, kind) {
17437
17484
  failureReason: failure.reason,
17438
17485
  changeSet,
17439
17486
  diffBase,
17440
- credential: evaluation.token
17487
+ credential: evaluation.token,
17488
+ credentialRefused: failure.kind === "credential"
17441
17489
  });
17442
17490
  if (failure.kind === "credential") {
17443
17491
  emitCredentialRejected(
package/dist/bin.js CHANGED
@@ -1761,10 +1761,45 @@ exit 0
1761
1761
  }
1762
1762
  function generateCiStanza() {
1763
1763
  return `# Halfcycle guard CI job \u2014 generated by the Halfcycle installer.
1764
- # Paste this job into your CI workflow. It assumes no package manager and no
1765
- # monorepo tooling: it runs the self-contained guard binary vendored at
1766
- # .halfcycle/bin/, so it works in a Python or Go repository as well as a Node
1767
- # one \u2014 the only prerequisite is Node 20 to run the bundled binary.
1764
+ # Below is a complete GitHub Actions workflow: a name, the events it runs on, and
1765
+ # one job. It assumes no package manager and no monorepo tooling: it runs the
1766
+ # self-contained guard binary vendored at .halfcycle/bin/, so it works in a Python
1767
+ # or Go repository as well as a Node one \u2014 the only prerequisite is Node 20 to run
1768
+ # the bundled binary.
1769
+ #
1770
+ # NO WORKFLOW YET? Copy everything from the \`name:\` line to the end of the job
1771
+ # into a new file, .github/workflows/halfcycle-guard.yml, without the leading
1772
+ # "# " on each line. That file is complete as it stands: commit it, and the check
1773
+ # runs on the next push and on every pull request.
1774
+ #
1775
+ # ALREADY HAVE A WORKFLOW? Copy only the job \u2014 from \`halfcycle-guard-ci:\` down to
1776
+ # its \`run:\` line \u2014 under the \`jobs:\` key of the workflow you have, keeping the
1777
+ # indentation. The job carries its own \`permissions\` block, so it needs nothing
1778
+ # from the rest of that file.
1779
+ #
1780
+ # THERE IS NOTHING TO STORE. No token in your repository's secrets, no project id
1781
+ # to look up, and no address to set. The \`permissions\` block below is the whole
1782
+ # configuration: it lets the job ask GitHub for a short-lived signed token naming
1783
+ # the repository it is running in, and Halfcycle trusts the name GitHub signs
1784
+ # rather than anything the job says about itself. That token is traded for a
1785
+ # credential that lives for minutes, and nothing is kept at either end.
1786
+ #
1787
+ # BOTH PERMISSION LINES, NOT JUST THE SECOND. Declaring any permission
1788
+ # replaces the defaults rather than adding to them, so a block naming only
1789
+ # \`id-token\` takes read access away from the checkout step and a private
1790
+ # repository stops checking out before the guard is reached. If your workflow
1791
+ # already has a \`permissions\` block, add \`id-token: write\` to it and leave the
1792
+ # rest alone.
1793
+ #
1794
+ # ONE THING TO DO FIRST, ONCE, ON YOUR OWN MACHINE. In this project, run
1795
+ #
1796
+ # npx halfcycle ci bind <owner>/<repo>
1797
+ #
1798
+ # naming this repository as GitHub spells it (for example acme/widgets). That
1799
+ # tells Halfcycle this project's CI runs from that repository, and it is the only
1800
+ # thing that makes a run mean anything: without it, Halfcycle has a signed
1801
+ # statement of which repository the job is in and no idea whose project that is.
1802
+ # The check says exactly that, and names the command, if you skip it.
1768
1803
  #
1769
1804
  # THERE IS NO ADDRESS TO CONFIGURE ANYWHERE IN HALFCYCLE. Every service this
1770
1805
  # product talks to has one address, the same for every user, and it ships in the
@@ -1772,53 +1807,52 @@ function generateCiStanza() {
1772
1807
  # runs each know where to go. If something tells you to set a Halfcycle URL, it is
1773
1808
  # out of date.
1774
1809
  #
1775
- # AND THERE IS NOTHING TO SET ON YOUR OWN MACHINE EITHER. \`npx halfcycle\` wrote
1776
- # this engagement's credential to $HOME/.halfcycle, owner-only, outside every
1777
- # checkout, and the guard hook reads it from there. A CI job is the one place that
1778
- # store cannot be reached \u2014 no browser, no per-user home \u2014 which is why the two
1779
- # names below exist at all, and why they are the only two: a credential, and the id
1780
- # of your own engagement.
1781
- #
1782
1810
  # WHICH BRANCH MODEL THIS ASSUMES: none. It works on pull-request branches AND on
1783
1811
  # commits pushed straight to the default branch, which is the shape most
1784
- # Halfcycle engagements settle on. \`fetch-depth\` is what makes that true: the
1785
- # check needs the commit BEFORE the one it is evaluating, and the default
1812
+ # Halfcycle engagements settle on. The \`on:\` block is what runs it in both
1813
+ # places: every push, and every pull request. Narrow it if you want (for example
1814
+ # \`branches: [main]\` under \`push:\`), but keep \`pull_request\`, or a change is
1815
+ # first checked after it has merged. \`fetch-depth\` is what makes the check work
1816
+ # there: it needs the commit BEFORE the one it is evaluating, and the default
1786
1817
  # shallow checkout does not have it. With \`fetch-depth: 0\` the check fails loudly
1787
1818
  # if it cannot work out what to evaluate \u2014 it will not pass quietly having
1788
1819
  # evaluated nothing.
1789
1820
  #
1790
- # halfcycle-guard-ci:
1791
- # runs-on: ubuntu-latest
1792
- # steps:
1793
- # - uses: actions/checkout@v4
1794
- # with:
1795
- # # REQUIRED. 0 = full history. The check diffs against the commit before
1796
- # # HEAD (or the fork point on a branch); the default depth of 1 has
1797
- # # neither. Do not lower this.
1798
- # fetch-depth: 0
1799
- # - uses: actions/setup-node@v4
1800
- # with:
1801
- # node-version: '20'
1802
- # - name: Halfcycle guard CI check
1803
- # env:
1804
- # # TWO SECRETS, AND THEY ARE THE TWO THAT ARE YOURS: the engagement's
1805
- # # token and its id. Both were printed by the installer that generated
1806
- # # this file. There is no address to configure \u2014 the guard service this
1807
- # # job talks to ships inside the binary below.
1808
- # GUARD_SERVICE_TOKEN: \${{ secrets.GUARD_SERVICE_TOKEN }}
1809
- # GUARD_ENGAGEMENT_ID: \${{ secrets.GUARD_ENGAGEMENT_ID }}
1810
- # run: node ./.halfcycle/bin/bin.bundle.mjs ci
1821
+ # name: Halfcycle guard
1822
+ # on:
1823
+ # push:
1824
+ # pull_request:
1825
+ # jobs:
1826
+ # halfcycle-guard-ci:
1827
+ # runs-on: ubuntu-latest
1828
+ # permissions:
1829
+ # contents: read
1830
+ # id-token: write
1831
+ # steps:
1832
+ # - uses: actions/checkout@v4
1833
+ # with:
1834
+ # # REQUIRED. 0 = full history. The check diffs against the commit before
1835
+ # # HEAD (or the fork point on a branch); the default depth of 1 has
1836
+ # # neither. Do not lower this.
1837
+ # fetch-depth: 0
1838
+ # - uses: actions/setup-node@v4
1839
+ # with:
1840
+ # node-version: '20'
1841
+ # - name: Halfcycle guard CI check
1842
+ # # No env block, on purpose: this step holds no secret. The permissions
1843
+ # # above are what authenticate it.
1844
+ # run: node ./.halfcycle/bin/bin.bundle.mjs ci
1811
1845
  #
1812
1846
  # The job prints the diff base it used and how many files it evaluated, on every
1813
1847
  # run. If that line says 0 files on a commit that changed something, the base is
1814
- # wrong \u2014 set HALFCYCLE_DIFF_BASE in the env block OF YOUR COPY, beside the two
1815
- # secrets, to name it explicitly (on a GitHub push event, \${{ github.event.before }}
1816
- # is the right value).
1848
+ # wrong \u2014 set HALFCYCLE_DIFF_BASE in an env block OF YOUR COPY of the check step,
1849
+ # to name it explicitly (on a GitHub push event, \${{ github.event.before }} is the
1850
+ # right value).
1817
1851
  #
1818
1852
  # EDIT YOUR COPY, NOT THIS FILE. This one is regenerated by the installer and your
1819
1853
  # changes to it would be replaced the next time you run \`npx halfcycle\`. It is
1820
- # also inert where it sits: no CI system reads this path. Copy the job above into
1821
- # your own workflow and change it there.
1854
+ # also inert where it sits: no CI system reads this path. Copy the workflow above
1855
+ # \u2014 or just its job \u2014 into .github/workflows/ and change it there.
1822
1856
  `;
1823
1857
  }
1824
1858
  var MCP_REGISTRATION_REL = ".mcp.json";
@@ -1990,24 +2024,29 @@ ${header}
1990
2024
  return "failed";
1991
2025
  }
1992
2026
  }
1993
- function previousAccountId(targetRepoRoot, engagementId) {
2027
+ function previousPinForEngagement(targetRepoRoot, engagementId) {
1994
2028
  try {
1995
2029
  const existing = readBundlePin(targetRepoRoot);
1996
2030
  if (existing === null || existing.engagementId !== engagementId)
1997
2031
  return void 0;
1998
- return existing.accountId;
2032
+ return existing;
1999
2033
  } catch {
2000
2034
  return void 0;
2001
2035
  }
2002
2036
  }
2037
+ function nonEmpty(value) {
2038
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
2039
+ }
2003
2040
  function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, accountId, writtenPaths) {
2004
- const carried = accountId ?? previousAccountId(targetRepoRoot, engagementId);
2041
+ const previous = previousPinForEngagement(targetRepoRoot, engagementId);
2042
+ const carried = nonEmpty(accountId) ?? nonEmpty(previous?.accountId);
2043
+ const installedAt = nonEmpty(previous?.installedAt) ?? (/* @__PURE__ */ new Date()).toISOString();
2005
2044
  const pin = {
2006
2045
  version,
2007
2046
  engagementId,
2008
2047
  engagementType,
2009
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2010
- ...carried !== void 0 && carried.trim() !== "" ? { accountId: carried } : {}
2048
+ installedAt,
2049
+ ...carried !== void 0 ? { accountId: carried } : {}
2011
2050
  };
2012
2051
  const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
2013
2052
  writeAllowlisted(pinPath, targetRepoRoot, JSON.stringify(pin, null, 2) + "\n", writtenPaths);
@@ -4677,6 +4716,8 @@ ${USAGE}`);
4677
4716
  process.stdout.write(`[halfcycle] Next: open this folder in Claude Code \u2014 accept the workspace-trust prompt, it is expected \u2014 then run /halfcycle-setup and answer what it asks about your project
4678
4717
  `);
4679
4718
  process.stdout.write(`[halfcycle] Claude Code will also ask permission the first time Halfcycle needs to look up your next step. Choose allow \u2014 without it nothing can run.
4719
+ `);
4720
+ process.stdout.write(`[halfcycle] To check changes in CI too: run \`npx halfcycle ci bind <owner>/<repo>\` in this folder once, then copy the workflow in .halfcycle/ci-stanza.yml into .github/workflows/ (or just its job into a workflow you have) \u2014 there is no secret to store.
4680
4721
  `);
4681
4722
  }
4682
4723
  if (credential) {