halfcycle 0.3.25 → 0.3.27
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/README.md +4 -6
- package/bin/bin.bundle.mjs +167 -66
- package/dist/bin.d.ts +11 -5
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +1359 -333
- package/dist/bin.js.map +3 -3
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +5 -5
- package/dist/device-signin.d.ts +7 -4
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +50 -4
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +683 -149
- package/dist/index.js.map +3 -3
- package/dist/install-manifest.d.ts +169 -0
- package/dist/install-manifest.d.ts.map +1 -0
- package/dist/install.d.ts +152 -7
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +12 -0
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/scan.d.ts +7 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/uninstall.d.ts +96 -0
- package/dist/uninstall.d.ts.map +1 -0
- package/package.json +4 -3
- package/dist/ci-bind.d.ts +0 -116
- package/dist/ci-bind.d.ts.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
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/README.md
CHANGED
|
@@ -44,15 +44,13 @@ Credit where it is due, and it is due. GStack, Matt Pocock's skills, GitHub Spec
|
|
|
44
44
|
|
|
45
45
|
The objection this usually meets is that it must be too much for a small project. It would be, if there were one setting. A folder you download has exactly one, so whoever wrote it had to choose who to disappoint, and it is usually the small project that gets buried and quietly drops the process in week three. This reads the work instead and sizes itself to it, which is the section further down.
|
|
46
46
|
|
|
47
|
-
Writing the process down was never the hard part. What a folder you download structurally cannot have is these
|
|
47
|
+
Writing the process down was never the hard part. What a folder you download structurally cannot have is these three things.
|
|
48
48
|
|
|
49
49
|
**1. Something other than you checks the work.** A document cannot audit itself, and the session that wrote the draft is the worst available reviewer of it. So Halfcycle's consistency check runs in a session that has never seen your draft, and the method makes that non-negotiable rather than advisory: after every round of fixes it runs again in another new session, never the one that asked for those fixes, because the reviewer that asked for a fix is the one who cannot see what the fix stranded. The guard checks are the harder edge — they run off your machine, on ours, on every real change, and one that fires either warns you or stops the work outright, depending on how serious the rule it broke is.
|
|
50
50
|
|
|
51
|
-
**2.
|
|
51
|
+
**2. The same gates on every contributor.** The requirement lives in the repository, so everybody inherits identical checks with nothing to set up, and a new hire is compliant in one command. No framework is single-player by accident; they are single-player by construction, and several people changing the same codebase faster than they can synchronise is the one problem a single-session rulebook cannot see.
|
|
52
52
|
|
|
53
|
-
**3.
|
|
54
|
-
|
|
55
|
-
**4. A record of how builds break that keeps growing.** This is the part that compounds, and it is why a fork is a snapshot that starts depreciating the day you take it. See below.
|
|
53
|
+
**3. A record of how builds break that keeps growing.** This is the part that compounds, and it is why a fork is a snapshot that starts depreciating the day you take it. See below.
|
|
56
54
|
|
|
57
55
|
---
|
|
58
56
|
|
|
@@ -158,7 +156,7 @@ This matters enough to be specific about.
|
|
|
158
156
|
|
|
159
157
|
## What you keep
|
|
160
158
|
|
|
161
|
-
Everything the method produces is yours, under your own `docs/`, in your git history, in plain markdown: the product spec, the architecture, the phase plans, the feature specs
|
|
159
|
+
Everything the method produces is yours, under your own `docs/`, in your git history, in plain markdown: the product spec, the architecture, the phase plans, the feature specs and the ordered work. It reads as documentation your team wrote, because it is.
|
|
162
160
|
|
|
163
161
|
Nothing you already had is changed. Existing settings are preserved, and a name collision is reported rather than resolved on your behalf.
|
|
164
162
|
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -14549,6 +14549,11 @@ var HALFCYCLE_DIR_NAME = ".halfcycle";
|
|
|
14549
14549
|
var ENGAGEMENTS_DIR_NAME = "engagements";
|
|
14550
14550
|
var ENGAGEMENT_ENV_FILENAME = "env";
|
|
14551
14551
|
var ACCOUNT_STORE_FILENAME = "account.json";
|
|
14552
|
+
var ENGAGEMENT_ID_PATTERN = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
|
|
14553
|
+
var ENGAGEMENT_ID_SHAPE = new RegExp(ENGAGEMENT_ID_PATTERN);
|
|
14554
|
+
function isEngagementId(value) {
|
|
14555
|
+
return typeof value === "string" && ENGAGEMENT_ID_SHAPE.test(value);
|
|
14556
|
+
}
|
|
14552
14557
|
var ENGAGEMENT_ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
|
|
14553
14558
|
function shq(value) {
|
|
14554
14559
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -14684,6 +14689,9 @@ function readConfig() {
|
|
|
14684
14689
|
function readNeverConfiguredReason() {
|
|
14685
14690
|
return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
|
|
14686
14691
|
}
|
|
14692
|
+
function isHalfcycleOwnCi() {
|
|
14693
|
+
return process.env["HALFCYCLE_INTERNAL_CI"] === "1";
|
|
14694
|
+
}
|
|
14687
14695
|
function readGuardOrigin() {
|
|
14688
14696
|
return resolveGuardOrigin(process.env);
|
|
14689
14697
|
}
|
|
@@ -14813,7 +14821,15 @@ var evaluationRequestSchema = external_exports.object({
|
|
|
14813
14821
|
* Omitting the field means "unknown", never "everything" — a client that
|
|
14814
14822
|
* says nothing is treated as an older one, not a capable one.
|
|
14815
14823
|
*/
|
|
14816
|
-
clientCapabilities: external_exports.array(external_exports.string()).optional()
|
|
14824
|
+
clientCapabilities: external_exports.array(external_exports.string()).optional(),
|
|
14825
|
+
/**
|
|
14826
|
+
* Whether this repository has any deploy file — a Dockerfile, a fly.toml or
|
|
14827
|
+
* a .env.example — anywhere in its tree. A check that compares code against
|
|
14828
|
+
* those files reports itself as not run when this is false.
|
|
14829
|
+
*
|
|
14830
|
+
* Omitting the field means "unknown": every check runs as it always has.
|
|
14831
|
+
*/
|
|
14832
|
+
repoHasDeployFiles: external_exports.boolean().optional()
|
|
14817
14833
|
}).strict();
|
|
14818
14834
|
|
|
14819
14835
|
// ../events/dist/result.js
|
|
@@ -15142,14 +15158,26 @@ function emitNeverConfigured(missing, exitCode, reason) {
|
|
|
15142
15158
|
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
15159
|
var PERMISSIONS_BLOCK = "\n permissions:\n contents: read\n id-token: write\n\n";
|
|
15144
15160
|
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
|
|
15161
|
+
var CI_CANNOT_PROVE_PROJECT = "has no way to prove which project it belongs to";
|
|
15162
|
+
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} Binding a repository so a run means something is internal to Halfcycle \u2014 contact hello@halfcycle.ai to have this one bound. There is no secret to store.`;
|
|
15163
|
+
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.";
|
|
15164
|
+
var CI_REMEDY = `This job cannot vouch for anything, because it ${CI_CANNOT_PROVE_PROJECT}. ${CI_REMEDY_LEAD} ${CI_OLDER_SETUP}`;
|
|
15165
|
+
var CI_NOT_SUPPORTED_MESSAGE = "[Halfcycle CI] This is Halfcycle-internal and is not supported in your CI.\n";
|
|
15166
|
+
var NEVER_CONFIGURED_HEADLINE = "GUARD NEVER CONFIGURED";
|
|
15167
|
+
var NOT_A_SERVICE_OUTAGE = "This is not a service outage.";
|
|
15146
15168
|
function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
|
|
15147
|
-
return `${label}
|
|
15169
|
+
return `${label} ${NEVER_CONFIGURED_HEADLINE} \u2014 this repository has no guard coverage.
|
|
15148
15170
|
No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
|
|
15149
|
-
|
|
15171
|
+
${NOT_A_SERVICE_OUTAGE} It means the guard has never run here \u2014 not on this change, and not on any change before it.
|
|
15150
15172
|
${remedy}
|
|
15151
15173
|
`;
|
|
15152
15174
|
}
|
|
15175
|
+
function ciNeverConfiguredMessage() {
|
|
15176
|
+
return `[Halfcycle CI] ${NEVER_CONFIGURED_HEADLINE} \u2014 this job ${CI_CANNOT_PROVE_PROJECT}, so it checked nothing. ${CI_REMEDY_LEAD}
|
|
15177
|
+
${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.
|
|
15178
|
+
${CI_OLDER_SETUP}
|
|
15179
|
+
`;
|
|
15180
|
+
}
|
|
15153
15181
|
function notThisAccountReport(label) {
|
|
15154
15182
|
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.
|
|
15155
15183
|
`;
|
|
@@ -15613,8 +15641,8 @@ function screenExcerpts(excerpts) {
|
|
|
15613
15641
|
}
|
|
15614
15642
|
|
|
15615
15643
|
// dist/extract/env-refs.js
|
|
15616
|
-
function extractEnvRefs(diffContent, context = {}) {
|
|
15617
|
-
const names = detectEnvReads(diffContent);
|
|
15644
|
+
function extractEnvRefs(diffContent, context = {}, filePath) {
|
|
15645
|
+
const names = detectEnvReads(diffContent, filePath !== void 0 && usesJsComments(filePath));
|
|
15618
15646
|
if (names.size === 0)
|
|
15619
15647
|
return [];
|
|
15620
15648
|
const result = [];
|
|
@@ -15623,19 +15651,31 @@ function extractEnvRefs(diffContent, context = {}) {
|
|
|
15623
15651
|
}
|
|
15624
15652
|
return result;
|
|
15625
15653
|
}
|
|
15626
|
-
|
|
15654
|
+
var JS_COMMENT_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i;
|
|
15655
|
+
function usesJsComments(filePath) {
|
|
15656
|
+
return JS_COMMENT_EXTENSIONS.test(filePath);
|
|
15657
|
+
}
|
|
15658
|
+
function detectEnvReads(diffContent, jsComments) {
|
|
15627
15659
|
const found = /* @__PURE__ */ new Set();
|
|
15628
15660
|
const dotPattern = /(?:process\.env|import\.meta\.env)\.([A-Z_][A-Z0-9_]*)/g;
|
|
15629
15661
|
const bracketPattern = /(?:process\.env|import\.meta\.env)\s*\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\]/g;
|
|
15630
|
-
const destructurePattern = /\{([^}]*)\}\s*=\s*(?:process\.env|import\.meta\.env)\b/g;
|
|
15631
|
-
for (const
|
|
15632
|
-
if (
|
|
15662
|
+
const destructurePattern = /\{([^{}]*)\}\s*=\s*(?:process\.env|import\.meta\.env)\b/g;
|
|
15663
|
+
for (const raw of diffContent.split("\n")) {
|
|
15664
|
+
if (raw.startsWith("-") && !raw.startsWith("---"))
|
|
15633
15665
|
continue;
|
|
15666
|
+
const isSource = raw.startsWith("+") && !raw.startsWith("+++") || raw.startsWith(" ");
|
|
15667
|
+
const line = jsComments && isSource ? raw.slice(1) : raw;
|
|
15668
|
+
const comment = jsComments && isSource ? commentedOut(line) : void 0;
|
|
15669
|
+
const live = (match) => comment === void 0 || !comment(match.index, match.index + match[0].length);
|
|
15634
15670
|
for (const match of line.matchAll(dotPattern))
|
|
15635
|
-
|
|
15671
|
+
if (live(match))
|
|
15672
|
+
found.add(match[1]);
|
|
15636
15673
|
for (const match of line.matchAll(bracketPattern))
|
|
15637
|
-
|
|
15674
|
+
if (live(match))
|
|
15675
|
+
found.add(match[1]);
|
|
15638
15676
|
for (const destructure of line.matchAll(destructurePattern)) {
|
|
15677
|
+
if (!live(destructure))
|
|
15678
|
+
continue;
|
|
15639
15679
|
for (const part of destructure[1].split(",")) {
|
|
15640
15680
|
const nameMatch = part.trim().match(/^([A-Z_][A-Z0-9_]*)\s*(?:[:=]|$)/);
|
|
15641
15681
|
if (nameMatch)
|
|
@@ -15645,6 +15685,28 @@ function detectEnvReads(diffContent) {
|
|
|
15645
15685
|
}
|
|
15646
15686
|
return found;
|
|
15647
15687
|
}
|
|
15688
|
+
var LINE_TERMINATOR = /[\r\u2028\u2029]/g;
|
|
15689
|
+
function commentedOut(line) {
|
|
15690
|
+
const lead = line.length - line.trimStart().length;
|
|
15691
|
+
let body;
|
|
15692
|
+
let close;
|
|
15693
|
+
let expression;
|
|
15694
|
+
if (line.startsWith("//", lead)) {
|
|
15695
|
+
body = lead + 2;
|
|
15696
|
+
LINE_TERMINATOR.lastIndex = body;
|
|
15697
|
+
close = LINE_TERMINATOR.exec(line)?.index ?? line.length;
|
|
15698
|
+
expression = "{";
|
|
15699
|
+
} else if (line.startsWith("/*", lead) || line.startsWith("{/*", lead)) {
|
|
15700
|
+
body = lead + (line[lead] === "{" ? 3 : 2);
|
|
15701
|
+
const found = line.indexOf("*/", body);
|
|
15702
|
+
close = found === -1 ? line.length : found;
|
|
15703
|
+
expression = line[lead] === "{" ? "${" : "{";
|
|
15704
|
+
} else {
|
|
15705
|
+
return void 0;
|
|
15706
|
+
}
|
|
15707
|
+
const opened = line.indexOf(expression, body);
|
|
15708
|
+
return (start, end) => start >= body && end <= close && (opened === -1 || opened >= start || opened >= close);
|
|
15709
|
+
}
|
|
15648
15710
|
function collectDeclarationFiles(context) {
|
|
15649
15711
|
const files = [];
|
|
15650
15712
|
if (context.dockerfileContent) {
|
|
@@ -15656,10 +15718,16 @@ function collectDeclarationFiles(context) {
|
|
|
15656
15718
|
if (context.envExampleContent) {
|
|
15657
15719
|
files.push({ kind: "env-example", path: ".env.example", content: context.envExampleContent });
|
|
15658
15720
|
}
|
|
15659
|
-
|
|
15660
|
-
files.push(
|
|
15721
|
+
for (const file2 of context.declarationFiles ?? [])
|
|
15722
|
+
files.push(file2);
|
|
15661
15723
|
return files;
|
|
15662
15724
|
}
|
|
15725
|
+
function repoHasDeployFiles(context) {
|
|
15726
|
+
const looked = context.declarationFiles !== void 0 || context.dockerfileContent !== void 0 || context.flyTomlContent !== void 0 || context.envExampleContent !== void 0;
|
|
15727
|
+
if (!looked)
|
|
15728
|
+
return void 0;
|
|
15729
|
+
return collectDeclarationFiles(context).length > 0;
|
|
15730
|
+
}
|
|
15663
15731
|
function resolveArgType(name, context) {
|
|
15664
15732
|
const files = collectDeclarationFiles(context);
|
|
15665
15733
|
const build = isBuildTimeSite(name, files);
|
|
@@ -15734,10 +15802,18 @@ function envExampleHas(name, content, argType) {
|
|
|
15734
15802
|
|
|
15735
15803
|
// dist/extract/strip-comments.js
|
|
15736
15804
|
function stripComments(code) {
|
|
15737
|
-
let out =
|
|
15738
|
-
|
|
15739
|
-
|
|
15740
|
-
|
|
15805
|
+
let out = "";
|
|
15806
|
+
let from = 0;
|
|
15807
|
+
for (; ; ) {
|
|
15808
|
+
const open = code.indexOf("/*", from);
|
|
15809
|
+
const close = open === -1 ? -1 : code.indexOf("*/", open + 2);
|
|
15810
|
+
if (close === -1) {
|
|
15811
|
+
out += code.slice(from, open === -1 ? code.length : open);
|
|
15812
|
+
break;
|
|
15813
|
+
}
|
|
15814
|
+
out += code.slice(from, open) + " ";
|
|
15815
|
+
from = close + 2;
|
|
15816
|
+
}
|
|
15741
15817
|
const lineComment = out.match(/(^|[^:])\/\//);
|
|
15742
15818
|
if (lineComment && lineComment.index !== void 0) {
|
|
15743
15819
|
const cut = lineComment.index + lineComment[1].length;
|
|
@@ -15867,7 +15943,7 @@ function parseRawSql(line) {
|
|
|
15867
15943
|
}
|
|
15868
15944
|
function parseQueryBuilder(rawLine) {
|
|
15869
15945
|
const line = stripDiffPrefix(rawLine);
|
|
15870
|
-
const m = line.match(/['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
|
|
15946
|
+
const m = line.match(/(?<!\bcreate(?:Hash|Hmac|Sign|Verify|Cipheriv|Decipheriv)\s*\([^)]*)['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
|
|
15871
15947
|
if (m) {
|
|
15872
15948
|
const kind = m[2].toLowerCase();
|
|
15873
15949
|
return { kind, table: m[1].toLowerCase(), whereColumns: [] };
|
|
@@ -16411,28 +16487,34 @@ function stripDiffPrefix4(line) {
|
|
|
16411
16487
|
|
|
16412
16488
|
// dist/changeset.js
|
|
16413
16489
|
var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".rst", ".adoc"]);
|
|
16414
|
-
var
|
|
16415
|
-
"docs/",
|
|
16416
|
-
"/docs/",
|
|
16417
|
-
"skills/",
|
|
16418
|
-
"/skills/",
|
|
16419
|
-
"runbooks/",
|
|
16420
|
-
"/runbooks/",
|
|
16421
|
-
"method/",
|
|
16422
|
-
"/method/"
|
|
16423
|
-
];
|
|
16490
|
+
var DOC_DIR_NAMES = /* @__PURE__ */ new Set(["docs", "skills", "runbooks", "method"]);
|
|
16424
16491
|
function isCodeFile(filePath) {
|
|
16425
16492
|
const lower = filePath.toLowerCase();
|
|
16426
16493
|
for (const ext of DOC_EXTENSIONS) {
|
|
16427
16494
|
if (lower.endsWith(ext))
|
|
16428
16495
|
return false;
|
|
16429
16496
|
}
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
}
|
|
16497
|
+
const dirs = lower.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
16498
|
+
if (dirs.some((dir) => DOC_DIR_NAMES.has(dir)))
|
|
16499
|
+
return false;
|
|
16434
16500
|
return true;
|
|
16435
16501
|
}
|
|
16502
|
+
var INSTALLER_OUTPUT_DIR = ".halfcycle/";
|
|
16503
|
+
function isInstallerOutput(relPath) {
|
|
16504
|
+
const normalised = relPath.replace(/\\/g, "/");
|
|
16505
|
+
return normalised === INSTALLER_OUTPUT_DIR.slice(0, -1) || normalised.startsWith(INSTALLER_OUTPUT_DIR);
|
|
16506
|
+
}
|
|
16507
|
+
function partitionInstallerOutput(files) {
|
|
16508
|
+
const evaluated = [];
|
|
16509
|
+
const installerOutput = [];
|
|
16510
|
+
for (const file2 of files) {
|
|
16511
|
+
if (isInstallerOutput(file2))
|
|
16512
|
+
installerOutput.push(file2);
|
|
16513
|
+
else
|
|
16514
|
+
evaluated.push(file2);
|
|
16515
|
+
}
|
|
16516
|
+
return { evaluated, installerOutput };
|
|
16517
|
+
}
|
|
16436
16518
|
function buildChangeSet(files, context = {}) {
|
|
16437
16519
|
const excerptConfig = context.excerpts;
|
|
16438
16520
|
const excerptsEnabled = excerptConfig?.enabled === true;
|
|
@@ -16452,7 +16534,7 @@ function buildChangeSet(files, context = {}) {
|
|
|
16452
16534
|
};
|
|
16453
16535
|
}
|
|
16454
16536
|
const astShapes = toAstShapes(detectBlastRadiusMarkers(filePath, diffContent));
|
|
16455
|
-
const envRefs = extractEnvRefs(diffContent, context);
|
|
16537
|
+
const envRefs = extractEnvRefs(diffContent, context, filePath);
|
|
16456
16538
|
const sqlWrites = extractSqlWrites(diffContent);
|
|
16457
16539
|
const thirdPartyEndpoints = extractThirdPartyEndpoints(diffContent, {
|
|
16458
16540
|
repoRoot: context.repoRoot
|
|
@@ -16490,6 +16572,15 @@ function deriveClientCapabilities(changeSet) {
|
|
|
16490
16572
|
}
|
|
16491
16573
|
return [...seen];
|
|
16492
16574
|
}
|
|
16575
|
+
function buildEvaluationRequest(engagementId, changeSet, context) {
|
|
16576
|
+
const hasDeployFiles = repoHasDeployFiles(context);
|
|
16577
|
+
return {
|
|
16578
|
+
engagementId,
|
|
16579
|
+
changeSet,
|
|
16580
|
+
clientCapabilities: deriveClientCapabilities(changeSet),
|
|
16581
|
+
...hasDeployFiles === void 0 ? {} : { repoHasDeployFiles: hasDeployFiles }
|
|
16582
|
+
};
|
|
16583
|
+
}
|
|
16493
16584
|
function produceExcerpts(diffContent, descriptors, caps) {
|
|
16494
16585
|
const excerpts = [];
|
|
16495
16586
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -16722,7 +16813,9 @@ async function emitRunRecord(opts) {
|
|
|
16722
16813
|
});
|
|
16723
16814
|
await emitTelemetry(run, {
|
|
16724
16815
|
controlTelemetryUrl: telemetryConfig.controlTelemetryUrl,
|
|
16725
|
-
|
|
16816
|
+
// No bearer at all on a refused credential: `emitTelemetry` skips the POST
|
|
16817
|
+
// when it has nothing to authenticate with, which is the true state here.
|
|
16818
|
+
controlTelemetryToken: opts.credentialRefused ? void 0 : opts.credential ?? telemetryConfig.controlTelemetryToken,
|
|
16726
16819
|
guardEvalLogDir: telemetryConfig.guardEvalLogDir
|
|
16727
16820
|
}, { warn: (msg) => process.stderr.write(msg + "\n") });
|
|
16728
16821
|
}
|
|
@@ -16750,11 +16843,13 @@ async function runCi() {
|
|
|
16750
16843
|
`);
|
|
16751
16844
|
return 1;
|
|
16752
16845
|
}
|
|
16753
|
-
const changedFiles = changedFilesResult.value;
|
|
16754
|
-
|
|
16846
|
+
const { evaluated: changedFiles, installerOutput } = partitionInstallerOutput(changedFilesResult.value);
|
|
16847
|
+
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.` : "";
|
|
16848
|
+
process.stdout.write(`[Halfcycle CI] Diff base ${mergeBase.slice(0, 12)} \u2014 ${describeDiffBase(diffBase)}. ${changedFiles.length} file(s) to evaluate.${setAside}
|
|
16755
16849
|
`);
|
|
16756
16850
|
if (changedFiles.length === 0) {
|
|
16757
|
-
process.stdout.write(`[Halfcycle CI]
|
|
16851
|
+
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.
|
|
16852
|
+
` : `[Halfcycle CI] No changed files against the diff base \u2014 nothing to evaluate.
|
|
16758
16853
|
`);
|
|
16759
16854
|
return 0;
|
|
16760
16855
|
}
|
|
@@ -16786,24 +16881,21 @@ async function runCi() {
|
|
|
16786
16881
|
guardServiceUrl: readGuardOrigin().origin,
|
|
16787
16882
|
guardServiceToken: acquired.credential.guardServiceToken,
|
|
16788
16883
|
guardEngagementId: acquired.credential.guardEngagementId,
|
|
16789
|
-
changeSet
|
|
16884
|
+
changeSet,
|
|
16885
|
+
context
|
|
16790
16886
|
});
|
|
16791
16887
|
}
|
|
16792
16888
|
}
|
|
16793
16889
|
if (!configResult.config) {
|
|
16794
16890
|
await emitRunRecord({ runType: "ci", outcome: "unconfigured", changeSet });
|
|
16795
|
-
process.stderr.write(
|
|
16891
|
+
process.stderr.write(isHalfcycleOwnCi() ? ciNeverConfiguredMessage() : CI_NOT_SUPPORTED_MESSAGE);
|
|
16796
16892
|
return 1;
|
|
16797
16893
|
}
|
|
16798
|
-
return await evaluateAndAct({ ...configResult.config, changeSet });
|
|
16894
|
+
return await evaluateAndAct({ ...configResult.config, changeSet, context });
|
|
16799
16895
|
}
|
|
16800
16896
|
async function evaluateAndAct(args2) {
|
|
16801
|
-
const { guardServiceUrl, guardServiceToken, guardEngagementId, changeSet } = args2;
|
|
16802
|
-
const clientResult = await evaluate(guardServiceUrl, guardServiceToken,
|
|
16803
|
-
engagementId: guardEngagementId,
|
|
16804
|
-
changeSet,
|
|
16805
|
-
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
16806
|
-
});
|
|
16897
|
+
const { guardServiceUrl, guardServiceToken, guardEngagementId, changeSet, context } = args2;
|
|
16898
|
+
const clientResult = await evaluate(guardServiceUrl, guardServiceToken, buildEvaluationRequest(guardEngagementId, changeSet, context));
|
|
16807
16899
|
if (!clientResult.ok) {
|
|
16808
16900
|
const failure = classifyFailure(clientResult);
|
|
16809
16901
|
await emitRunRecord({
|
|
@@ -16811,10 +16903,11 @@ async function evaluateAndAct(args2) {
|
|
|
16811
16903
|
runType: "ci",
|
|
16812
16904
|
outcome: outcomeForClientFailure(clientResult.kind),
|
|
16813
16905
|
failureReason: failure.reason,
|
|
16814
|
-
changeSet
|
|
16906
|
+
changeSet,
|
|
16907
|
+
credentialRefused: failure.kind === "credential"
|
|
16815
16908
|
});
|
|
16816
16909
|
if (failure.kind === "credential") {
|
|
16817
|
-
process.stderr.write(credentialRejectedMessage(failure.statusCode, failure.message, CI_REMEDY, "[Halfcycle CI]"));
|
|
16910
|
+
process.stderr.write(isHalfcycleOwnCi() ? credentialRejectedMessage(failure.statusCode, failure.message, CI_REMEDY, "[Halfcycle CI]") : CI_NOT_SUPPORTED_MESSAGE);
|
|
16818
16911
|
return 1;
|
|
16819
16912
|
}
|
|
16820
16913
|
if (failure.kind === "service") {
|
|
@@ -16863,13 +16956,17 @@ async function evaluateAndAct(args2) {
|
|
|
16863
16956
|
}, {
|
|
16864
16957
|
warn: (msg) => process.stderr.write(msg + "\n")
|
|
16865
16958
|
});
|
|
16866
|
-
return actOnEnvelope(clientResult.envelope
|
|
16959
|
+
return actOnEnvelope(clientResult.envelope, {
|
|
16960
|
+
files: changeSet.files.length,
|
|
16961
|
+
signals: changeSetDescriptorCount(changeSet)
|
|
16962
|
+
});
|
|
16867
16963
|
}
|
|
16868
|
-
function actOnEnvelope(envelope) {
|
|
16964
|
+
function actOnEnvelope(envelope, scope) {
|
|
16869
16965
|
const notRun = notRunReport(envelope.notRun, "[Halfcycle CI]");
|
|
16870
16966
|
if (notRun)
|
|
16871
16967
|
process.stdout.write(notRun + "\n");
|
|
16872
16968
|
if (envelope.guardsFired.length === 0) {
|
|
16969
|
+
process.stdout.write(cleanRunSummary(envelope.explanation, scope) + "\n");
|
|
16873
16970
|
return 0;
|
|
16874
16971
|
}
|
|
16875
16972
|
if (envelope.blocking) {
|
|
@@ -16901,6 +16998,13 @@ function actOnEnvelope(envelope) {
|
|
|
16901
16998
|
return 0;
|
|
16902
16999
|
}
|
|
16903
17000
|
}
|
|
17001
|
+
function cleanRunSummary(verdict, scope) {
|
|
17002
|
+
const files = `${scope.files} file(s) evaluated`;
|
|
17003
|
+
if (scope.signals === 0) {
|
|
17004
|
+
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).`;
|
|
17005
|
+
}
|
|
17006
|
+
return `[Halfcycle CI] ${verdict} ${files}, ${scope.signals} signal(s) checked.`;
|
|
17007
|
+
}
|
|
16904
17008
|
|
|
16905
17009
|
// dist/credential-store.js
|
|
16906
17010
|
import { chmodSync, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
|
|
@@ -16908,7 +17012,6 @@ import { homedir as homedir2, platform } from "node:os";
|
|
|
16908
17012
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
16909
17013
|
var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
|
|
16910
17014
|
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
17015
|
function composeEngagementEnvPath(engagementId, home) {
|
|
16913
17016
|
return join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, ENGAGEMENT_ENV_FILENAME);
|
|
16914
17017
|
}
|
|
@@ -16936,7 +17039,7 @@ function readStoredEngagementToken(envFilePath) {
|
|
|
16936
17039
|
return value && value.trim() !== "" ? value : void 0;
|
|
16937
17040
|
}
|
|
16938
17041
|
function writeRefreshedToken(opts) {
|
|
16939
|
-
if (!
|
|
17042
|
+
if (!isEngagementId(opts.engagementId)) {
|
|
16940
17043
|
return { written: false, reason: "the engagement this session names is not one this store can be keyed by" };
|
|
16941
17044
|
}
|
|
16942
17045
|
let declaredReal;
|
|
@@ -17118,6 +17221,8 @@ async function runPostToolUse() {
|
|
|
17118
17221
|
const diffContent = isNotebook ? "" : getSingleFileDiff(absFilePath, repoRoot);
|
|
17119
17222
|
const { relative: pathRelative } = await import("node:path");
|
|
17120
17223
|
const relPath = pathRelative(repoRoot, absFilePath);
|
|
17224
|
+
if (isInstallerOutput(relPath))
|
|
17225
|
+
return;
|
|
17121
17226
|
const context = loadChangeSetContext(repoRoot);
|
|
17122
17227
|
const changeSet = buildChangeSet([{ path: relPath, diffContent }], {
|
|
17123
17228
|
...context,
|
|
@@ -17134,11 +17239,7 @@ async function runPostToolUse() {
|
|
|
17134
17239
|
guardServiceUrl,
|
|
17135
17240
|
token: guardServiceToken,
|
|
17136
17241
|
engagementId: guardEngagementId,
|
|
17137
|
-
request:
|
|
17138
|
-
engagementId: guardEngagementId,
|
|
17139
|
-
changeSet,
|
|
17140
|
-
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
17141
|
-
}
|
|
17242
|
+
request: buildEvaluationRequest(guardEngagementId, changeSet, context)
|
|
17142
17243
|
});
|
|
17143
17244
|
const clientResult = evaluation.result;
|
|
17144
17245
|
if (!clientResult.ok) {
|
|
@@ -17149,7 +17250,8 @@ async function runPostToolUse() {
|
|
|
17149
17250
|
outcome: outcomeForClientFailure(clientResult.kind),
|
|
17150
17251
|
failureReason: failure.reason,
|
|
17151
17252
|
changeSet,
|
|
17152
|
-
credential: evaluation.token
|
|
17253
|
+
credential: evaluation.token,
|
|
17254
|
+
credentialRefused: failure.kind === "credential"
|
|
17153
17255
|
});
|
|
17154
17256
|
if (failure.kind === "credential") {
|
|
17155
17257
|
emitCredentialRejected(
|
|
@@ -17394,7 +17496,8 @@ async function runSessionDiff(input, kind) {
|
|
|
17394
17496
|
emitLocalFault(`Could not get session diff: ${collected.message}`);
|
|
17395
17497
|
return;
|
|
17396
17498
|
}
|
|
17397
|
-
const {
|
|
17499
|
+
const { getDiff } = collected;
|
|
17500
|
+
const { evaluated: changedFiles } = partitionInstallerOutput(collected.files);
|
|
17398
17501
|
if (changedFiles.length === 0) {
|
|
17399
17502
|
return;
|
|
17400
17503
|
}
|
|
@@ -17421,11 +17524,7 @@ async function runSessionDiff(input, kind) {
|
|
|
17421
17524
|
guardServiceUrl,
|
|
17422
17525
|
token: guardServiceToken,
|
|
17423
17526
|
engagementId: guardEngagementId,
|
|
17424
|
-
request:
|
|
17425
|
-
engagementId: guardEngagementId,
|
|
17426
|
-
changeSet,
|
|
17427
|
-
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
17428
|
-
}
|
|
17527
|
+
request: buildEvaluationRequest(guardEngagementId, changeSet, context)
|
|
17429
17528
|
});
|
|
17430
17529
|
const clientResult = evaluation.result;
|
|
17431
17530
|
if (!clientResult.ok) {
|
|
@@ -17437,7 +17536,8 @@ async function runSessionDiff(input, kind) {
|
|
|
17437
17536
|
failureReason: failure.reason,
|
|
17438
17537
|
changeSet,
|
|
17439
17538
|
diffBase,
|
|
17440
|
-
credential: evaluation.token
|
|
17539
|
+
credential: evaluation.token,
|
|
17540
|
+
credentialRefused: failure.kind === "credential"
|
|
17441
17541
|
});
|
|
17442
17542
|
if (failure.kind === "credential") {
|
|
17443
17543
|
emitCredentialRejected(
|
|
@@ -17818,6 +17918,7 @@ function readStdin3() {
|
|
|
17818
17918
|
}
|
|
17819
17919
|
main().catch((err) => {
|
|
17820
17920
|
process.stderr.write(`halfcycle-runner: unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
17921
|
+
Contact hello@halfcycle.ai if this keeps happening.
|
|
17821
17922
|
`);
|
|
17822
17923
|
process.exit(1);
|
|
17823
17924
|
});
|
package/dist/bin.d.ts
CHANGED
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
* halfcycle build-record <phase> — assemble the phase Build Record (D1)
|
|
12
12
|
* halfcycle open-phase <phase> — open a phase, with its entry decision
|
|
13
13
|
* halfcycle close-phase <phase> — record a phase's acceptance and close it
|
|
14
|
-
* halfcycle
|
|
15
|
-
* halfcycle ci unbind <owner>/<repo> — withdraw that trust
|
|
14
|
+
* halfcycle uninstall [--remove-credential] — take back what the installer wrote here
|
|
16
15
|
*
|
|
17
|
-
*
|
|
16
|
+
* NO `ci` VERB (T-25, operator decision 2026-09-22(c)) — `ci bind`/`ci unbind`
|
|
17
|
+
* generalised a fix for Halfcycle's own dogfood CI into a client-facing feature;
|
|
18
|
+
* setting up a project's CI is project scope, not something this product does for
|
|
19
|
+
* a client. See `ci-oidc-token-exchange.md`.
|
|
20
|
+
*
|
|
21
|
+
* `bareTarget` BELOW IS DERIVED FROM `CLI_VERBS` (T-21). The bare form
|
|
18
22
|
* (`npx halfcycle` / `npx halfcycle ./repo`) treats the first positional as an
|
|
19
|
-
* install target, so a
|
|
20
|
-
* it
|
|
23
|
+
* install target, so a verb it did not recognise would not be "unknown command" —
|
|
24
|
+
* it would silently be an install into a directory named after the verb. It was a
|
|
25
|
+
* second, hand-typed list of verbs until `uninstall` joined, which is the verb whose
|
|
26
|
+
* misreading would be worst: `halfcycle uninstall` INSTALLING. One list now.
|
|
21
27
|
*
|
|
22
28
|
* TWO GUARDS ON THAT SHAPE (W3-F-4, walk 4). The bare-form-installs behaviour is
|
|
23
29
|
* deliberate and stays — it is the primary distribution path — but walk 4 ran the
|
package/dist/bin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG"}
|