halfcycle 0.3.17 → 0.3.19
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 +1 -1
- package/bin/bin.bundle.mjs +224 -33
- package/dist/bin.js +571 -198
- package/dist/bin.js.map +3 -3
- package/dist/build-record/close-record.d.ts +124 -0
- package/dist/build-record/close-record.d.ts.map +1 -0
- package/dist/build-record/index.d.ts +8 -5
- package/dist/build-record/index.d.ts.map +1 -1
- package/dist/build-record/sources.d.ts +49 -18
- package/dist/build-record/sources.d.ts.map +1 -1
- package/dist/build-record/template.d.ts.map +1 -1
- package/dist/build-record/types.d.ts +49 -12
- package/dist/build-record/types.d.ts.map +1 -1
- package/dist/build-record/write.d.ts +60 -3
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/close-phase.d.ts +21 -2
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/confirm-identity.d.ts +25 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +29 -1
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +85 -2
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +412 -142
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts.map +1 -1
- package/dist/open-phase.d.ts +58 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/own-engagement.d.ts +50 -10
- package/dist/own-engagement.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
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
|
@@ -42,7 +42,7 @@ A method document cannot fix that, because a document cannot interrupt you. This
|
|
|
42
42
|
|
|
43
43
|
Credit where it is due, and it is due. GStack, Matt Pocock's skills, GitHub Spec Kit, BMAD, OpenSpec and claude-flow are genuinely good, and between them they taught this whole category that the bottleneck is discipline rather than model intelligence. We took two things from them deliberately and say so: the one-question-at-a-time interrogation shape, and an install that is one command and then stays out of your way.
|
|
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
47
|
Writing the process down was never the hard part. What a folder you download structurally cannot have is these four things.
|
|
48
48
|
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -39,6 +39,9 @@ function readConfig() {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
function readNeverConfiguredReason() {
|
|
43
|
+
return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
|
|
44
|
+
}
|
|
42
45
|
function readExcerptConfig() {
|
|
43
46
|
const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
|
|
44
47
|
const enabled = (raw ?? "").trim().toLowerCase() === "true";
|
|
@@ -14594,6 +14597,80 @@ function date4(params) {
|
|
|
14594
14597
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
14595
14598
|
config(en_default());
|
|
14596
14599
|
|
|
14600
|
+
// ../events/dist/evaluation.js
|
|
14601
|
+
var envRefSchema = external_exports.object({
|
|
14602
|
+
name: external_exports.string(),
|
|
14603
|
+
argType: external_exports.enum(["build", "runtime", "both"])
|
|
14604
|
+
}).strict();
|
|
14605
|
+
var sqlWriteSchema = external_exports.object({
|
|
14606
|
+
kind: external_exports.enum(["insert", "update", "upsert", "delete"]),
|
|
14607
|
+
table: external_exports.string(),
|
|
14608
|
+
whereColumns: external_exports.array(external_exports.string())
|
|
14609
|
+
}).strict();
|
|
14610
|
+
var schemaConstraintSchema = external_exports.object({
|
|
14611
|
+
table: external_exports.string(),
|
|
14612
|
+
kind: external_exports.enum(["unique", "check", "foreignKey"]),
|
|
14613
|
+
constraintName: external_exports.string().optional()
|
|
14614
|
+
}).strict();
|
|
14615
|
+
var numericColumnSchema = external_exports.object({
|
|
14616
|
+
table: external_exports.string(),
|
|
14617
|
+
column: external_exports.string(),
|
|
14618
|
+
pgType: external_exports.enum(["numeric", "bigint", "decimal"]),
|
|
14619
|
+
wireField: external_exports.string().optional(),
|
|
14620
|
+
hasRegisteredCoercion: external_exports.boolean().optional()
|
|
14621
|
+
}).strict();
|
|
14622
|
+
var thirdPartyEndpointSchema = external_exports.object({
|
|
14623
|
+
signature: external_exports.string(),
|
|
14624
|
+
provider: external_exports.string().optional(),
|
|
14625
|
+
hasCapturedFixture: external_exports.boolean()
|
|
14626
|
+
}).strict();
|
|
14627
|
+
var sourceExcerptSchema = external_exports.object({
|
|
14628
|
+
/** The declared construct the excerpt was cut around (a derived-fact anchor). */
|
|
14629
|
+
construct: external_exports.enum(["sqlWrite", "thirdPartyEndpoint", "envRef"]),
|
|
14630
|
+
/** 1-based start line in the changed file (best-effort). */
|
|
14631
|
+
startLine: external_exports.number().int().nonnegative(),
|
|
14632
|
+
/** The bounded excerpt text — capped CLIENT-SIDE in the runner before send. */
|
|
14633
|
+
text: external_exports.string()
|
|
14634
|
+
}).strict();
|
|
14635
|
+
var diffMetadataSchema = external_exports.object({
|
|
14636
|
+
path: external_exports.string(),
|
|
14637
|
+
astShapes: external_exports.array(external_exports.string()),
|
|
14638
|
+
envRefs: external_exports.array(envRefSchema),
|
|
14639
|
+
sqlWrites: external_exports.array(sqlWriteSchema),
|
|
14640
|
+
endpointSigs: external_exports.array(external_exports.string()),
|
|
14641
|
+
schemaConstraints: external_exports.array(schemaConstraintSchema),
|
|
14642
|
+
numericColumns: external_exports.array(numericColumnSchema),
|
|
14643
|
+
thirdPartyEndpoints: external_exports.array(thirdPartyEndpointSchema),
|
|
14644
|
+
// Present only when you have turned source excerpts on. Left out
|
|
14645
|
+
// entirely otherwise — never sent as an empty list.
|
|
14646
|
+
sourceExcerpts: external_exports.array(sourceExcerptSchema).optional()
|
|
14647
|
+
}).strict();
|
|
14648
|
+
var changeSetSchema = external_exports.object({
|
|
14649
|
+
files: external_exports.array(diffMetadataSchema)
|
|
14650
|
+
}).strict();
|
|
14651
|
+
var NON_CAPABILITY_DESCRIPTOR_FIELDS = [
|
|
14652
|
+
"path",
|
|
14653
|
+
"sourceExcerpts"
|
|
14654
|
+
];
|
|
14655
|
+
var CLIENT_CAPABILITY_VOCABULARY = Object.keys(diffMetadataSchema.shape).filter((field) => !NON_CAPABILITY_DESCRIPTOR_FIELDS.includes(field));
|
|
14656
|
+
function isClientCapability(value) {
|
|
14657
|
+
return CLIENT_CAPABILITY_VOCABULARY.includes(value);
|
|
14658
|
+
}
|
|
14659
|
+
var evaluationRequestSchema = external_exports.object({
|
|
14660
|
+
engagementId: external_exports.string(),
|
|
14661
|
+
changeSet: changeSetSchema,
|
|
14662
|
+
/**
|
|
14663
|
+
* What this client can supply: the names of the descriptor kinds it fills in
|
|
14664
|
+
* for each changed file. A check that needs a kind this client does not send
|
|
14665
|
+
* is reported by name as not run, with the missing kind named, instead of
|
|
14666
|
+
* quietly reporting nothing.
|
|
14667
|
+
*
|
|
14668
|
+
* Omitting the field means "unknown", never "everything" — a client that
|
|
14669
|
+
* says nothing is treated as an older one, not a capable one.
|
|
14670
|
+
*/
|
|
14671
|
+
clientCapabilities: external_exports.array(external_exports.string()).optional()
|
|
14672
|
+
}).strict();
|
|
14673
|
+
|
|
14597
14674
|
// ../core/dist/guard-record.js
|
|
14598
14675
|
var severitySchema = external_exports.enum(["info", "warn", "block"]);
|
|
14599
14676
|
var channelSchema = external_exports.enum(["stable", "candidate", "rented"]);
|
|
@@ -14641,11 +14718,26 @@ var firedGuardSchema = external_exports.object({
|
|
|
14641
14718
|
*/
|
|
14642
14719
|
path: external_exports.string().optional()
|
|
14643
14720
|
}).strict();
|
|
14721
|
+
var notRunCheckSchema = external_exports.object({
|
|
14722
|
+
/** Which check did not run, by the same reference a fired check carries. */
|
|
14723
|
+
patternRef: external_exports.string(),
|
|
14724
|
+
/**
|
|
14725
|
+
* Why it could not run, in plain words — what this client did not send, or
|
|
14726
|
+
* that the check's kind is not implemented yet. Never the check's own rule.
|
|
14727
|
+
*/
|
|
14728
|
+
reason: external_exports.string()
|
|
14729
|
+
}).strict();
|
|
14644
14730
|
var resultEnvelopeSchema = external_exports.object({
|
|
14645
14731
|
guardsFired: external_exports.array(firedGuardSchema),
|
|
14646
14732
|
severity: severitySchema.nullable(),
|
|
14647
14733
|
blocking: external_exports.boolean(),
|
|
14648
|
-
explanation: external_exports.string()
|
|
14734
|
+
explanation: external_exports.string(),
|
|
14735
|
+
/**
|
|
14736
|
+
* The checks that could not run on this edit, each named with the reason.
|
|
14737
|
+
* Absent when every check ran — so an evaluation where nothing was skipped
|
|
14738
|
+
* looks exactly as it always has, and an absent list is never an empty one.
|
|
14739
|
+
*/
|
|
14740
|
+
notRun: external_exports.array(notRunCheckSchema).optional()
|
|
14649
14741
|
}).strict();
|
|
14650
14742
|
var wireErrorSchema = external_exports.object({
|
|
14651
14743
|
statusCode: external_exports.number(),
|
|
@@ -14653,6 +14745,62 @@ var wireErrorSchema = external_exports.object({
|
|
|
14653
14745
|
message: external_exports.string()
|
|
14654
14746
|
}).strict();
|
|
14655
14747
|
|
|
14748
|
+
// ../events/dist/phase-identity.js
|
|
14749
|
+
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
14750
|
+
function isValidPhaseIdentity(identity) {
|
|
14751
|
+
if (typeof identity === "number")
|
|
14752
|
+
return Number.isInteger(identity) && identity >= 0;
|
|
14753
|
+
if (typeof identity !== "string")
|
|
14754
|
+
return false;
|
|
14755
|
+
if (identity === "")
|
|
14756
|
+
return false;
|
|
14757
|
+
if (identity.includes("/"))
|
|
14758
|
+
return false;
|
|
14759
|
+
if (identity.includes("\\"))
|
|
14760
|
+
return false;
|
|
14761
|
+
if (identity.includes("\0"))
|
|
14762
|
+
return false;
|
|
14763
|
+
if (identity.startsWith("."))
|
|
14764
|
+
return false;
|
|
14765
|
+
return true;
|
|
14766
|
+
}
|
|
14767
|
+
var InvalidPhaseIdentityError = class extends Error {
|
|
14768
|
+
/** The value that was refused, exactly as supplied. */
|
|
14769
|
+
identity;
|
|
14770
|
+
constructor(identity, allowedLocation) {
|
|
14771
|
+
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
14772
|
+
const where = allowedLocation === void 0 ? "" : `
|
|
14773
|
+
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
14774
|
+
super(`that phase identity cannot name ${subject}.
|
|
14775
|
+
identity: ${describeIdentity(identity)}` + where);
|
|
14776
|
+
this.name = "InvalidPhaseIdentityError";
|
|
14777
|
+
this.identity = identity;
|
|
14778
|
+
}
|
|
14779
|
+
};
|
|
14780
|
+
function renderPhaseSegment(identity) {
|
|
14781
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
14782
|
+
throw new InvalidPhaseIdentityError(identity);
|
|
14783
|
+
}
|
|
14784
|
+
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
14785
|
+
}
|
|
14786
|
+
function describeIdentity(identity) {
|
|
14787
|
+
if (identity === "")
|
|
14788
|
+
return "(empty)";
|
|
14789
|
+
if (typeof identity === "string")
|
|
14790
|
+
return identity;
|
|
14791
|
+
if (identity === null)
|
|
14792
|
+
return "null";
|
|
14793
|
+
if (identity === void 0)
|
|
14794
|
+
return "none supplied";
|
|
14795
|
+
if (typeof identity === "number" || typeof identity === "boolean")
|
|
14796
|
+
return String(identity);
|
|
14797
|
+
if (Array.isArray(identity))
|
|
14798
|
+
return "(a list)";
|
|
14799
|
+
if (typeof identity === "object")
|
|
14800
|
+
return "(an object)";
|
|
14801
|
+
return `(a ${typeof identity})`;
|
|
14802
|
+
}
|
|
14803
|
+
|
|
14656
14804
|
// dist/client.js
|
|
14657
14805
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
14658
14806
|
async function evaluate(url2, token, request, fetchFn = globalThis.fetch) {
|
|
@@ -15780,6 +15928,16 @@ function buildChangeSet(files, context = {}) {
|
|
|
15780
15928
|
});
|
|
15781
15929
|
return { files: fileMetadata };
|
|
15782
15930
|
}
|
|
15931
|
+
function deriveClientCapabilities(changeSet) {
|
|
15932
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15933
|
+
for (const file2 of changeSet.files) {
|
|
15934
|
+
for (const key of Object.keys(file2)) {
|
|
15935
|
+
if (isClientCapability(key))
|
|
15936
|
+
seen.add(key);
|
|
15937
|
+
}
|
|
15938
|
+
}
|
|
15939
|
+
return [...seen];
|
|
15940
|
+
}
|
|
15783
15941
|
function produceExcerpts(diffContent, descriptors, caps) {
|
|
15784
15942
|
const excerpts = [];
|
|
15785
15943
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -15911,26 +16069,22 @@ async function emitTelemetry(run, config2, logger, fetchFn = globalThis.fetch) {
|
|
|
15911
16069
|
await postTelemetry(run, config2.controlTelemetryUrl, config2.controlTelemetryToken, logger, fetchFn);
|
|
15912
16070
|
}
|
|
15913
16071
|
}
|
|
15914
|
-
function normalizePhaseBaseName(phase) {
|
|
15915
|
-
if (phase === void 0)
|
|
15916
|
-
return null;
|
|
15917
|
-
const trimmed = phase.trim();
|
|
15918
|
-
if (trimmed === "")
|
|
15919
|
-
return null;
|
|
15920
|
-
const match = /^(?:phase-)?(\d+)$/.exec(trimmed);
|
|
15921
|
-
if (!match)
|
|
15922
|
-
return null;
|
|
15923
|
-
return `phase-${match[1]}`;
|
|
15924
|
-
}
|
|
15925
16072
|
function localLogPath(run, config2) {
|
|
15926
16073
|
const logDir = config2.guardEvalLogDir ?? join4(homedir(), ".halfcycle", "guard-eval-log");
|
|
15927
|
-
const
|
|
15928
|
-
const fileName =
|
|
16074
|
+
const phase = run.phase;
|
|
16075
|
+
const fileName = phase === void 0 || phase === "" ? "unphased.jsonl" : `${renderPhaseSegment(phase)}.jsonl`;
|
|
15929
16076
|
return join4(logDir, fileName);
|
|
15930
16077
|
}
|
|
15931
16078
|
function appendToLocalLog(run, config2, logger) {
|
|
16079
|
+
let logPath;
|
|
16080
|
+
try {
|
|
16081
|
+
logPath = localLogPath(run, config2);
|
|
16082
|
+
} catch (err) {
|
|
16083
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16084
|
+
logger.warn(`[Halfcycle telemetry] Phase name refused, nothing written (dropped): ${msg}`);
|
|
16085
|
+
return;
|
|
16086
|
+
}
|
|
15932
16087
|
try {
|
|
15933
|
-
const logPath = localLogPath(run, config2);
|
|
15934
16088
|
mkdirSync2(dirname2(logPath), { recursive: true });
|
|
15935
16089
|
appendFileSync(logPath, JSON.stringify(run) + "\n", "utf-8");
|
|
15936
16090
|
} catch (err) {
|
|
@@ -16045,8 +16199,8 @@ This is not a service outage: the guard service answered. The suspect is this re
|
|
|
16045
16199
|
}
|
|
16046
16200
|
var UNCONFIGURED_EXIT_TOOL_USE = 2;
|
|
16047
16201
|
var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
|
|
16048
|
-
function emitNeverConfigured(missing, exitCode) {
|
|
16049
|
-
writeSync(2,
|
|
16202
|
+
function emitNeverConfigured(missing, exitCode, reason) {
|
|
16203
|
+
writeSync(2, neverConfiguredReport(missing, reason));
|
|
16050
16204
|
process.exitCode = exitCode;
|
|
16051
16205
|
}
|
|
16052
16206
|
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.`;
|
|
@@ -16058,6 +16212,18 @@ This is not a service outage. It means the guard has never run here \u2014 not o
|
|
|
16058
16212
|
${remedy}
|
|
16059
16213
|
`;
|
|
16060
16214
|
}
|
|
16215
|
+
function notThisAccountReport(label) {
|
|
16216
|
+
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.
|
|
16217
|
+
`;
|
|
16218
|
+
}
|
|
16219
|
+
function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
|
|
16220
|
+
switch (reason) {
|
|
16221
|
+
case "no-credential-yet":
|
|
16222
|
+
return neverConfiguredMessage(missing, HOOK_REMEDY, label);
|
|
16223
|
+
case "not-this-account":
|
|
16224
|
+
return notThisAccountReport(label);
|
|
16225
|
+
}
|
|
16226
|
+
}
|
|
16061
16227
|
var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
16062
16228
|
function classifyFailure(failure) {
|
|
16063
16229
|
if (failure.kind === "contract-error") {
|
|
@@ -16104,6 +16270,14 @@ function emitCredentialRejected(statusCode, serviceMessage, exitCode) {
|
|
|
16104
16270
|
writeSync(2, credentialRejectedMessage(statusCode, serviceMessage, HOOK_REMEDY));
|
|
16105
16271
|
process.exitCode = exitCode;
|
|
16106
16272
|
}
|
|
16273
|
+
function notRunReport(notRun, label = "[Halfcycle]") {
|
|
16274
|
+
if (!notRun || notRun.length === 0)
|
|
16275
|
+
return "";
|
|
16276
|
+
const count = notRun.length;
|
|
16277
|
+
const noun = count === 1 ? "check" : "checks";
|
|
16278
|
+
const lines = notRun.map((entry) => ` [NOT RUN] ${entry.patternRef}: ${entry.reason}`);
|
|
16279
|
+
return [`${label} ${count} ${noun} did not run on this change:`, ...lines].join("\n");
|
|
16280
|
+
}
|
|
16107
16281
|
|
|
16108
16282
|
// dist/ci.js
|
|
16109
16283
|
async function runCi() {
|
|
@@ -16154,7 +16328,8 @@ async function runCi() {
|
|
|
16154
16328
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
16155
16329
|
const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
|
|
16156
16330
|
engagementId: guardEngagementId,
|
|
16157
|
-
changeSet
|
|
16331
|
+
changeSet,
|
|
16332
|
+
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
16158
16333
|
});
|
|
16159
16334
|
if (!clientResult.ok) {
|
|
16160
16335
|
const failure = classifyFailure(clientResult);
|
|
@@ -16183,9 +16358,9 @@ async function runCi() {
|
|
|
16183
16358
|
const run = buildGuardEvalRun({
|
|
16184
16359
|
engagementId: guardEngagementId,
|
|
16185
16360
|
runType: "ci",
|
|
16186
|
-
//
|
|
16187
|
-
//
|
|
16188
|
-
//
|
|
16361
|
+
// The phase this run belongs to, as the client named it — it names the
|
|
16362
|
+
// guard-eval log file the run is appended to. Absent at CI time → the run
|
|
16363
|
+
// is unphased and lands in unphased.jsonl.
|
|
16189
16364
|
phase: telemetryConfig.phase,
|
|
16190
16365
|
outcome: "evaluated",
|
|
16191
16366
|
changeSet,
|
|
@@ -16202,6 +16377,9 @@ async function runCi() {
|
|
|
16202
16377
|
return actOnEnvelope(clientResult.envelope);
|
|
16203
16378
|
}
|
|
16204
16379
|
function actOnEnvelope(envelope) {
|
|
16380
|
+
const notRun = notRunReport(envelope.notRun, "[Halfcycle CI]");
|
|
16381
|
+
if (notRun)
|
|
16382
|
+
process.stdout.write(notRun + "\n");
|
|
16205
16383
|
if (envelope.guardsFired.length === 0) {
|
|
16206
16384
|
return 0;
|
|
16207
16385
|
}
|
|
@@ -16269,13 +16447,14 @@ async function runPostToolUse() {
|
|
|
16269
16447
|
const configResult = readConfig();
|
|
16270
16448
|
if (!configResult.config) {
|
|
16271
16449
|
await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
|
|
16272
|
-
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_TOOL_USE);
|
|
16450
|
+
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
|
|
16273
16451
|
return;
|
|
16274
16452
|
}
|
|
16275
16453
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
16276
16454
|
const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
|
|
16277
16455
|
engagementId: guardEngagementId,
|
|
16278
|
-
changeSet
|
|
16456
|
+
changeSet,
|
|
16457
|
+
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
16279
16458
|
});
|
|
16280
16459
|
if (!clientResult.ok) {
|
|
16281
16460
|
const failure = classifyFailure(clientResult);
|
|
@@ -16301,8 +16480,8 @@ async function runPostToolUse() {
|
|
|
16301
16480
|
const run = buildGuardEvalRun({
|
|
16302
16481
|
engagementId: guardEngagementId,
|
|
16303
16482
|
runType: "hook",
|
|
16304
|
-
//
|
|
16305
|
-
//
|
|
16483
|
+
// The phase this run belongs to, as the client named it — it names the
|
|
16484
|
+
// guard-eval log file the run is appended to. Absent → the run is unphased.
|
|
16306
16485
|
phase: telemetryConfig.phase,
|
|
16307
16486
|
outcome: "evaluated",
|
|
16308
16487
|
changeSet,
|
|
@@ -16319,18 +16498,23 @@ async function runPostToolUse() {
|
|
|
16319
16498
|
actOnEnvelope2(clientResult.envelope);
|
|
16320
16499
|
}
|
|
16321
16500
|
function actOnEnvelope2(envelope) {
|
|
16501
|
+
const notRun = notRunReport(envelope.notRun);
|
|
16502
|
+
const notRunBlock = notRun ? `
|
|
16503
|
+
${notRun}` : "";
|
|
16322
16504
|
if (envelope.guardsFired.length === 0) {
|
|
16505
|
+
if (notRun)
|
|
16506
|
+
emitWarning(notRun);
|
|
16323
16507
|
return;
|
|
16324
16508
|
}
|
|
16325
16509
|
if (envelope.blocking) {
|
|
16326
16510
|
const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
|
|
16327
16511
|
const reason = `Guard blocked: ${envelope.explanation}
|
|
16328
|
-
${guardLines}`;
|
|
16512
|
+
${guardLines}${notRunBlock}`;
|
|
16329
16513
|
emitBlock(reason);
|
|
16330
16514
|
} else {
|
|
16331
16515
|
const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
|
|
16332
16516
|
emitWarning(`[Halfcycle] Guard warning: ${envelope.explanation}
|
|
16333
|
-
${guardLines}`);
|
|
16517
|
+
${guardLines}${notRunBlock}`);
|
|
16334
16518
|
}
|
|
16335
16519
|
}
|
|
16336
16520
|
function readStdin() {
|
|
@@ -16529,13 +16713,14 @@ async function runSessionDiff(input, kind) {
|
|
|
16529
16713
|
const configResult = readConfig();
|
|
16530
16714
|
if (!configResult.config) {
|
|
16531
16715
|
await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
|
|
16532
|
-
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_STOP_FAMILY);
|
|
16716
|
+
emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
|
|
16533
16717
|
return;
|
|
16534
16718
|
}
|
|
16535
16719
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
16536
16720
|
const clientResult = await evaluate(guardServiceUrl, guardServiceToken, {
|
|
16537
16721
|
engagementId: guardEngagementId,
|
|
16538
|
-
changeSet
|
|
16722
|
+
changeSet,
|
|
16723
|
+
clientCapabilities: deriveClientCapabilities(changeSet)
|
|
16539
16724
|
});
|
|
16540
16725
|
if (!clientResult.ok) {
|
|
16541
16726
|
const failure = classifyFailure(clientResult);
|
|
@@ -16579,20 +16764,26 @@ async function runSessionDiff(input, kind) {
|
|
|
16579
16764
|
actOnEnvelope3(clientResult.envelope, kind, input.stop_hook_active === true);
|
|
16580
16765
|
}
|
|
16581
16766
|
function actOnEnvelope3(envelope, kind, neverBlock) {
|
|
16582
|
-
|
|
16767
|
+
const notRun = notRunReport(envelope.notRun);
|
|
16768
|
+
const notRunBlock = notRun ? `
|
|
16769
|
+
${notRun}` : "";
|
|
16770
|
+
if (envelope.guardsFired.length === 0) {
|
|
16771
|
+
if (notRun)
|
|
16772
|
+
emitWarning(notRun);
|
|
16583
16773
|
return;
|
|
16774
|
+
}
|
|
16584
16775
|
const where = kind === "stop" ? "session stop" : "session stop";
|
|
16585
16776
|
const guardLines = envelope.guardsFired.map((g) => ` [${g.severity.toUpperCase()}] ${g.patternRef}: ${g.explanation}`).join("\n");
|
|
16586
16777
|
if (envelope.blocking && !neverBlock) {
|
|
16587
16778
|
emitBlock(`Guard blocked at ${where}: ${envelope.explanation}
|
|
16588
|
-
${guardLines}`);
|
|
16779
|
+
${guardLines}${notRunBlock}`);
|
|
16589
16780
|
} else if (envelope.blocking && neverBlock) {
|
|
16590
16781
|
emitWarning(`[Halfcycle] Guard would block at ${where} but the stop was already continued (stop_hook_active) \u2014 surfacing as a warning:
|
|
16591
16782
|
${envelope.explanation}
|
|
16592
|
-
${guardLines}`);
|
|
16783
|
+
${guardLines}${notRunBlock}`);
|
|
16593
16784
|
} else {
|
|
16594
16785
|
emitWarning(`[Halfcycle] Guard warning at ${where}: ${envelope.explanation}
|
|
16595
|
-
${guardLines}`);
|
|
16786
|
+
${guardLines}${notRunBlock}`);
|
|
16596
16787
|
}
|
|
16597
16788
|
}
|
|
16598
16789
|
|