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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +175 -127
- package/dist/bin.js +85 -44
- package/dist/bin.js.map +1 -1
- package/dist/index.js +83 -44
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +6 -0
- package/dist/install.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
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
|
{
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -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
|
-
|
|
15227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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(
|
|
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 {
|
|
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
|
-
#
|
|
1765
|
-
# monorepo tooling: it runs the
|
|
1766
|
-
# .halfcycle/bin/, so it works in a Python
|
|
1767
|
-
# one \u2014 the only prerequisite is Node 20 to run
|
|
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. \`
|
|
1785
|
-
#
|
|
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
|
-
#
|
|
1791
|
-
#
|
|
1792
|
-
#
|
|
1793
|
-
#
|
|
1794
|
-
#
|
|
1795
|
-
#
|
|
1796
|
-
#
|
|
1797
|
-
#
|
|
1798
|
-
#
|
|
1799
|
-
#
|
|
1800
|
-
#
|
|
1801
|
-
#
|
|
1802
|
-
#
|
|
1803
|
-
#
|
|
1804
|
-
#
|
|
1805
|
-
#
|
|
1806
|
-
#
|
|
1807
|
-
#
|
|
1808
|
-
#
|
|
1809
|
-
#
|
|
1810
|
-
#
|
|
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
|
|
1815
|
-
#
|
|
1816
|
-
#
|
|
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
|
|
1821
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2010
|
-
...carried !== void 0
|
|
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) {
|