halfcycle 0.3.23 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +885 -127
- package/dist/account-credential.d.ts +12 -12
- package/dist/account-credential.d.ts.map +1 -1
- package/dist/banner-facts.d.ts +9 -0
- package/dist/banner-facts.d.ts.map +1 -1
- package/dist/bin.d.ts +2 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +585 -180
- package/dist/bin.js.map +3 -3
- package/dist/build-record/template.d.ts +11 -0
- package/dist/build-record/template.d.ts.map +1 -1
- package/dist/ci-bind.d.ts +116 -0
- package/dist/ci-bind.d.ts.map +1 -0
- package/dist/cli-contract.d.ts +11 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/confirm-identity.d.ts +50 -0
- package/dist/confirm-identity.d.ts.map +1 -1
- package/dist/create-engagement.d.ts +15 -4
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/device-signin.d.ts +13 -0
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +64 -85
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +338 -109
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +14 -11
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +22 -7
- package/dist/merge-settings.d.ts.map +1 -1
- package/dist/setup/manifest.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,6 +23,89 @@ var matcherSchema = z.discriminatedUnion("kind", [
|
|
|
23
23
|
llmMatcherSchema
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
+
// ../core/dist/credential-format.js
|
|
27
|
+
var HALFCYCLE_DIR_NAME = ".halfcycle";
|
|
28
|
+
var ENGAGEMENTS_DIR_NAME = "engagements";
|
|
29
|
+
var ENGAGEMENT_ENV_FILENAME = "env";
|
|
30
|
+
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
31
|
+
var ENGAGEMENT_ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
|
|
32
|
+
function shq(value) {
|
|
33
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
34
|
+
}
|
|
35
|
+
function unquote(value) {
|
|
36
|
+
const v = value.trim();
|
|
37
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
38
|
+
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
39
|
+
}
|
|
40
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
41
|
+
return v.slice(1, -1);
|
|
42
|
+
return v;
|
|
43
|
+
}
|
|
44
|
+
function parseEnvText(raw) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const line of raw.split("\n")) {
|
|
47
|
+
const eq = line.indexOf("=");
|
|
48
|
+
if (eq === -1)
|
|
49
|
+
continue;
|
|
50
|
+
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
51
|
+
if (key === "" || key.startsWith("#"))
|
|
52
|
+
continue;
|
|
53
|
+
out[key] = unquote(line.slice(eq + 1));
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function reconcileEnvText(existing, values, keys) {
|
|
58
|
+
const desired = new Map(keys.map((k) => {
|
|
59
|
+
const value = values[k];
|
|
60
|
+
return [k, value === void 0 ? "" : value];
|
|
61
|
+
}));
|
|
62
|
+
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
63
|
+
const written = keys.filter((k) => desired.get(k) !== null);
|
|
64
|
+
if (existing === null) {
|
|
65
|
+
if (written.length === 0)
|
|
66
|
+
return "";
|
|
67
|
+
return `${ENGAGEMENT_ENV_HEADER}
|
|
68
|
+
` + written.map(line).join("\n") + "\n";
|
|
69
|
+
}
|
|
70
|
+
const seen = /* @__PURE__ */ new Set();
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const existingLine of existing.split("\n")) {
|
|
73
|
+
const eq = existingLine.indexOf("=");
|
|
74
|
+
if (eq === -1) {
|
|
75
|
+
out.push(existingLine);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const lhs = existingLine.slice(0, eq);
|
|
79
|
+
const exported = /^\s*export\s+/.exec(lhs);
|
|
80
|
+
const prefix = exported === null ? "" : exported[0];
|
|
81
|
+
const key = lhs.slice(prefix.length).trim();
|
|
82
|
+
if (!desired.has(key)) {
|
|
83
|
+
out.push(existingLine);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
seen.add(key);
|
|
87
|
+
if (desired.get(key) === null)
|
|
88
|
+
continue;
|
|
89
|
+
out.push(`${prefix}${line(key)}`);
|
|
90
|
+
}
|
|
91
|
+
const missing = written.filter((k) => !seen.has(k));
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
const header = existing.includes(ENGAGEMENT_ENV_HEADER) ? "" : `${ENGAGEMENT_ENV_HEADER}
|
|
94
|
+
`;
|
|
95
|
+
const block = header + missing.map(line).join("\n");
|
|
96
|
+
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
97
|
+
if (trailingBlank)
|
|
98
|
+
out.splice(out.length - 1, 0, block);
|
|
99
|
+
else
|
|
100
|
+
out.push(`
|
|
101
|
+
${block}`);
|
|
102
|
+
}
|
|
103
|
+
let result = out.join("\n");
|
|
104
|
+
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
105
|
+
result += "\n";
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
26
109
|
// ../events/dist/result.js
|
|
27
110
|
var firedGuardSchema = z2.object({
|
|
28
111
|
guardId: z2.string(),
|
|
@@ -83,7 +166,7 @@ var wireErrorSchema = z2.object({
|
|
|
83
166
|
|
|
84
167
|
// ../events/dist/telemetry.js
|
|
85
168
|
import { z as z3 } from "zod";
|
|
86
|
-
var utcIso8601 = z3.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z
|
|
169
|
+
var utcIso8601 = z3.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
87
170
|
var guardEvalOutcomeSchema = z3.enum([
|
|
88
171
|
"evaluated",
|
|
89
172
|
"infra-error",
|
|
@@ -137,16 +220,184 @@ var guardEvalRunSchema = z3.object({
|
|
|
137
220
|
}
|
|
138
221
|
});
|
|
139
222
|
|
|
140
|
-
// ../events/dist/
|
|
223
|
+
// ../events/dist/state.js
|
|
141
224
|
import { z as z4 } from "zod";
|
|
142
|
-
var
|
|
143
|
-
var
|
|
225
|
+
var utcIso86012 = z4.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
226
|
+
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
227
|
+
var stepIdSchema = z4.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
228
|
+
var methodVersionSchema = z4.string().regex(/^\d+\.\d+\.\d+$/);
|
|
229
|
+
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
230
|
+
var recordKindSchema = z4.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
231
|
+
var methodLayerSchema = z4.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
232
|
+
var evidenceKindSchema = z4.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
233
|
+
var declarableEvidenceKindSchema = z4.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
234
|
+
var dispositionSchema = z4.enum([
|
|
235
|
+
"prevented",
|
|
236
|
+
"corrected-in-spec",
|
|
237
|
+
"corrected-in-scope",
|
|
238
|
+
"blocked-in-code",
|
|
239
|
+
"flagged",
|
|
240
|
+
"auto-fixed",
|
|
241
|
+
"surfaced",
|
|
242
|
+
"filed-forward"
|
|
243
|
+
]);
|
|
244
|
+
var mechanismSchema = z4.enum(["guard", "verdict", "served-step", "method-step"]);
|
|
245
|
+
var artefactStateSchema = z4.enum(["created", "updated"]);
|
|
246
|
+
var QUALIFIED_OUTCOMES = ["declined", "parked"];
|
|
247
|
+
var qualifiedOutcomeSchema = z4.enum(QUALIFIED_OUTCOMES);
|
|
248
|
+
function isQualifiedOutcome(outcome) {
|
|
249
|
+
return QUALIFIED_OUTCOMES.includes(outcome);
|
|
250
|
+
}
|
|
251
|
+
var runOutcomeSchema = z4.enum([
|
|
252
|
+
"completed",
|
|
253
|
+
"attempted-failed",
|
|
254
|
+
...QUALIFIED_OUTCOMES
|
|
255
|
+
]);
|
|
256
|
+
var gateKindSchema = z4.enum(["approval", "mechanical"]);
|
|
257
|
+
var gateVerdictSchema = z4.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
|
|
258
|
+
function outcomeReasonIssue(outcome, reason) {
|
|
259
|
+
if (isQualifiedOutcome(outcome) && reason === void 0) {
|
|
260
|
+
return { message: `outcomeReason is required when the outcome is '${outcome}'` };
|
|
261
|
+
}
|
|
262
|
+
if (!isQualifiedOutcome(outcome) && reason !== void 0) {
|
|
263
|
+
return {
|
|
264
|
+
message: `outcomeReason is only present on a qualified outcome (${QUALIFIED_OUTCOMES.join(" | ")})`
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
var anchorEvidenceSchema = z4.object({
|
|
270
|
+
kind: z4.literal("anchor"),
|
|
271
|
+
path: z4.string().regex(anchorPathRegex),
|
|
272
|
+
// Optional 1-based line number within the file at `path`. Present when the
|
|
273
|
+
// writer knows the exact line; omitted — never `0`, never `null` — when it
|
|
274
|
+
// does not, because a line number is not always obtainable. Absence is a
|
|
275
|
+
// valid, permanent state here, not a gap meant to be filled in later.
|
|
276
|
+
line: z4.number().optional()
|
|
277
|
+
}).strict();
|
|
278
|
+
var diffEvidenceSchema = z4.object({
|
|
279
|
+
kind: z4.literal("diff"),
|
|
280
|
+
ref: z4.string()
|
|
281
|
+
}).strict();
|
|
282
|
+
var reproEvidenceSchema = z4.object({
|
|
283
|
+
kind: z4.literal("repro"),
|
|
284
|
+
steps: z4.string().max(2e3)
|
|
285
|
+
}).strict();
|
|
286
|
+
var questionAnswerEvidenceSchema = z4.object({
|
|
287
|
+
kind: z4.literal("question-answer"),
|
|
288
|
+
question: z4.string().max(2e3),
|
|
289
|
+
before: z4.string().max(2e3),
|
|
290
|
+
after: z4.string().max(2e3)
|
|
291
|
+
}).strict();
|
|
292
|
+
var noOpEvidenceSchema = z4.object({
|
|
293
|
+
kind: z4.literal("no-op"),
|
|
294
|
+
inspected: z4.string().max(2e3),
|
|
295
|
+
unchangedBecause: z4.string().max(2e3)
|
|
296
|
+
}).strict();
|
|
297
|
+
var evidenceSchema = z4.discriminatedUnion("kind", [
|
|
298
|
+
anchorEvidenceSchema,
|
|
299
|
+
diffEvidenceSchema,
|
|
300
|
+
reproEvidenceSchema,
|
|
301
|
+
questionAnswerEvidenceSchema,
|
|
302
|
+
noOpEvidenceSchema
|
|
303
|
+
]);
|
|
304
|
+
var stateRecordEnvelopeSchema = z4.object({
|
|
305
|
+
format: z4.literal(STATE_RECORD_FORMAT),
|
|
306
|
+
recordKind: recordKindSchema,
|
|
307
|
+
recordId: z4.string().uuid(),
|
|
308
|
+
methodVersion: methodVersionSchema,
|
|
309
|
+
engagementId: z4.string(),
|
|
310
|
+
stepId: stepIdSchema,
|
|
311
|
+
// Immutable: names when the thing happened, never when a later disposition
|
|
312
|
+
// moved.
|
|
313
|
+
occurredAt: utcIso86012,
|
|
314
|
+
// Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
|
|
315
|
+
// Carried on the shared envelope so a "when did this last change" query is
|
|
316
|
+
// kind-agnostic. Kept rather than generalised to `updatedAt` because
|
|
317
|
+
// `disposition` is the only field any re-emit may change (§5.3).
|
|
318
|
+
dispositionAt: utcIso86012,
|
|
319
|
+
evidence: evidenceSchema,
|
|
320
|
+
// Which phase of the project this record belongs to, if any. Present on
|
|
321
|
+
// records written during phase-scoped work; absent on records that run once
|
|
322
|
+
// for the whole project rather than per phase. Absence is meaningful on its
|
|
323
|
+
// own — it is never replaced with a placeholder value, and it is read as
|
|
324
|
+
// "satisfies any phase", not as "unknown".
|
|
325
|
+
phase: z4.string().optional()
|
|
326
|
+
});
|
|
327
|
+
var artefactRefSchema = z4.object({
|
|
328
|
+
name: z4.string(),
|
|
329
|
+
parent: z4.string()
|
|
330
|
+
}).strict();
|
|
331
|
+
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
332
|
+
recordKind: z4.literal("artefact"),
|
|
333
|
+
artefactRef: artefactRefSchema,
|
|
334
|
+
// Some layer wrote it, by construction — an empty array is not a legitimate
|
|
335
|
+
// state, so the schema says so.
|
|
336
|
+
writtenByLayers: z4.array(methodLayerSchema).min(1),
|
|
337
|
+
state: artefactStateSchema
|
|
338
|
+
}).strict();
|
|
339
|
+
var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
340
|
+
recordKind: z4.literal("step-run"),
|
|
341
|
+
runOutcome: runOutcomeSchema,
|
|
342
|
+
failureReason: z4.string().optional(),
|
|
343
|
+
outcomeReason: z4.string().max(2e3).optional()
|
|
344
|
+
}).strict().superRefine((rec, ctx) => {
|
|
345
|
+
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
346
|
+
ctx.addIssue({
|
|
347
|
+
code: z4.ZodIssueCode.custom,
|
|
348
|
+
path: ["failureReason"],
|
|
349
|
+
message: "failureReason is required when runOutcome is 'attempted-failed'"
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
|
|
353
|
+
ctx.addIssue({
|
|
354
|
+
code: z4.ZodIssueCode.custom,
|
|
355
|
+
path: ["failureReason"],
|
|
356
|
+
message: "failureReason is only present when runOutcome is 'attempted-failed'"
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const issue = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
|
|
360
|
+
if (issue) {
|
|
361
|
+
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
365
|
+
recordKind: z4.literal("gate"),
|
|
366
|
+
gateKind: gateKindSchema,
|
|
367
|
+
verdict: gateVerdictSchema,
|
|
368
|
+
actor: z4.string(),
|
|
369
|
+
outcomeReason: z4.string().max(2e3).optional()
|
|
370
|
+
}).strict().superRefine((rec, ctx) => {
|
|
371
|
+
const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
372
|
+
if (issue) {
|
|
373
|
+
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
377
|
+
recordKind: z4.literal("intervention"),
|
|
378
|
+
layer: methodLayerSchema,
|
|
379
|
+
mechanism: mechanismSchema,
|
|
380
|
+
severity: severitySchema,
|
|
381
|
+
disposition: dispositionSchema,
|
|
382
|
+
summary: z4.string().max(2e3)
|
|
383
|
+
}).strict();
|
|
384
|
+
var stateRecordSchema = z4.discriminatedUnion("recordKind", [
|
|
385
|
+
artefactRecordSchema,
|
|
386
|
+
stepRunRecordSchema,
|
|
387
|
+
gateRecordSchema,
|
|
388
|
+
interventionRecordSchema
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
// ../events/dist/crew.js
|
|
392
|
+
import { z as z5 } from "zod";
|
|
393
|
+
var crewGroupSchema = z5.enum(["builder", "reviewer", "always-on"]);
|
|
394
|
+
var crewTierSchema = z5.enum(["deep", "standard", "fast"]);
|
|
144
395
|
var CREW_DISCIPLINE_MAX = 64;
|
|
145
|
-
var crewFaceHeadSchema =
|
|
146
|
-
var crewFaceAntennaSchema =
|
|
147
|
-
var crewFaceEyesSchema =
|
|
148
|
-
var crewFaceMouthSchema =
|
|
149
|
-
var crewFaceSchema =
|
|
396
|
+
var crewFaceHeadSchema = z5.enum(["sq", "sqr", "rnd", "dome", "hex"]);
|
|
397
|
+
var crewFaceAntennaSchema = z5.enum(["none", "stalk", "twin", "dish"]);
|
|
398
|
+
var crewFaceEyesSchema = z5.enum(["dots", "bars", "visor", "cyclops"]);
|
|
399
|
+
var crewFaceMouthSchema = z5.enum(["line", "grid", "dots", "wave"]);
|
|
400
|
+
var crewFaceSchema = z5.object({
|
|
150
401
|
// The outline of the head. Members who do the same kind of work share one, so a
|
|
151
402
|
// team is recognisable before any name is read.
|
|
152
403
|
head: crewFaceHeadSchema,
|
|
@@ -157,17 +408,17 @@ var crewFaceSchema = z4.object({
|
|
|
157
408
|
// The mouth.
|
|
158
409
|
mouth: crewFaceMouthSchema,
|
|
159
410
|
// Whether this member is drawn with ears.
|
|
160
|
-
ears:
|
|
411
|
+
ears: z5.boolean()
|
|
161
412
|
}).strict();
|
|
162
|
-
var crewMemberSchema =
|
|
413
|
+
var crewMemberSchema = z5.object({
|
|
163
414
|
// The agent's name. The same twenty-one on every project, so a name is a
|
|
164
415
|
// vocabulary a returning client already knows.
|
|
165
|
-
callsign:
|
|
416
|
+
callsign: z5.string().min(1).max(32),
|
|
166
417
|
// Which of the three headings this member reads under.
|
|
167
418
|
group: crewGroupSchema,
|
|
168
419
|
// The kind of work this member takes. A task names a discipline, and only
|
|
169
420
|
// agents holding it can be dispatched to it.
|
|
170
|
-
discipline:
|
|
421
|
+
discipline: z5.string().min(1).max(CREW_DISCIPLINE_MAX),
|
|
171
422
|
// How much thinking this member brings. There is no field naming the supplier
|
|
172
423
|
// behind it, on this shape or on any other.
|
|
173
424
|
tier: crewTierSchema,
|
|
@@ -392,6 +643,21 @@ function describeIdentity(identity) {
|
|
|
392
643
|
return `(a ${typeof identity})`;
|
|
393
644
|
}
|
|
394
645
|
|
|
646
|
+
// ../events/dist/credential-wire.js
|
|
647
|
+
import { z as z6 } from "zod";
|
|
648
|
+
var engagementCredentialResponseSchema = z6.object({
|
|
649
|
+
engagementId: z6.string(),
|
|
650
|
+
sessionToken: z6.string(),
|
|
651
|
+
mcpUrl: z6.string(),
|
|
652
|
+
guardUrl: z6.string(),
|
|
653
|
+
controlTelemetryUrl: z6.string()
|
|
654
|
+
}).strict();
|
|
655
|
+
var ciTokenExchangeResponseSchema = z6.object({
|
|
656
|
+
token: z6.string(),
|
|
657
|
+
expiresAt: utcIso86012,
|
|
658
|
+
engagementId: z6.string()
|
|
659
|
+
}).strict();
|
|
660
|
+
|
|
395
661
|
// dist/engagement-credential.js
|
|
396
662
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
397
663
|
import { platform as platform2 } from "node:os";
|
|
@@ -401,12 +667,13 @@ import { join as join2 } from "node:path";
|
|
|
401
667
|
import { homedir, platform } from "node:os";
|
|
402
668
|
import { dirname, join } from "node:path";
|
|
403
669
|
function halfcycleHome(home) {
|
|
404
|
-
return join(home ?? homedir(),
|
|
670
|
+
return join(home ?? homedir(), HALFCYCLE_DIR_NAME);
|
|
405
671
|
}
|
|
406
672
|
|
|
407
673
|
// dist/engagement-credential.js
|
|
408
|
-
var ENGAGEMENTS_DIR =
|
|
409
|
-
var ENV_FILENAME =
|
|
674
|
+
var ENGAGEMENTS_DIR = ENGAGEMENTS_DIR_NAME;
|
|
675
|
+
var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
|
|
676
|
+
var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
|
|
410
677
|
var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
411
678
|
function engagementStateDir(engagementId, home) {
|
|
412
679
|
return join2(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
|
|
@@ -414,7 +681,6 @@ function engagementStateDir(engagementId, home) {
|
|
|
414
681
|
function engagementEnvPath(engagementId, home) {
|
|
415
682
|
return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
416
683
|
}
|
|
417
|
-
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
418
684
|
var GUARD_ENV_KEYS = [
|
|
419
685
|
"GUARD_SERVICE_URL",
|
|
420
686
|
"GUARD_SERVICE_TOKEN",
|
|
@@ -429,82 +695,8 @@ var ENGAGEMENT_ENV_KEYS = [
|
|
|
429
695
|
...GUARD_ENV_KEYS,
|
|
430
696
|
"CONTROL_TELEMETRY_URL"
|
|
431
697
|
];
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
435
|
-
}
|
|
436
|
-
function unquote(value) {
|
|
437
|
-
const v = value.trim();
|
|
438
|
-
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
439
|
-
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
440
|
-
}
|
|
441
|
-
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
442
|
-
return v.slice(1, -1);
|
|
443
|
-
return v;
|
|
444
|
-
}
|
|
445
|
-
function parseEnvText(raw) {
|
|
446
|
-
const out = {};
|
|
447
|
-
for (const line of raw.split("\n")) {
|
|
448
|
-
const eq = line.indexOf("=");
|
|
449
|
-
if (eq === -1)
|
|
450
|
-
continue;
|
|
451
|
-
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
452
|
-
if (key === "" || key.startsWith("#"))
|
|
453
|
-
continue;
|
|
454
|
-
out[key] = unquote(line.slice(eq + 1));
|
|
455
|
-
}
|
|
456
|
-
return out;
|
|
457
|
-
}
|
|
458
|
-
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
459
|
-
const desired = new Map(keys.map((k) => {
|
|
460
|
-
const value = values[k];
|
|
461
|
-
return [k, value === void 0 ? "" : value];
|
|
462
|
-
}));
|
|
463
|
-
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
464
|
-
const written = keys.filter((k) => desired.get(k) !== null);
|
|
465
|
-
if (existing === null) {
|
|
466
|
-
if (written.length === 0)
|
|
467
|
-
return "";
|
|
468
|
-
return `${ENV_HEADER}
|
|
469
|
-
` + written.map(line).join("\n") + "\n";
|
|
470
|
-
}
|
|
471
|
-
const seen = /* @__PURE__ */ new Set();
|
|
472
|
-
const out = [];
|
|
473
|
-
for (const existingLine of existing.split("\n")) {
|
|
474
|
-
const eq = existingLine.indexOf("=");
|
|
475
|
-
if (eq === -1) {
|
|
476
|
-
out.push(existingLine);
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
const lhs = existingLine.slice(0, eq);
|
|
480
|
-
const exported = /^\s*export\s+/.exec(lhs);
|
|
481
|
-
const prefix = exported === null ? "" : exported[0];
|
|
482
|
-
const key = lhs.slice(prefix.length).trim();
|
|
483
|
-
if (!desired.has(key)) {
|
|
484
|
-
out.push(existingLine);
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
seen.add(key);
|
|
488
|
-
if (desired.get(key) === null)
|
|
489
|
-
continue;
|
|
490
|
-
out.push(`${prefix}${line(key)}`);
|
|
491
|
-
}
|
|
492
|
-
const missing = written.filter((k) => !seen.has(k));
|
|
493
|
-
if (missing.length > 0) {
|
|
494
|
-
const header = existing.includes(ENV_HEADER) ? "" : `${ENV_HEADER}
|
|
495
|
-
`;
|
|
496
|
-
const block = header + missing.map(line).join("\n");
|
|
497
|
-
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
498
|
-
if (trailingBlank)
|
|
499
|
-
out.splice(out.length - 1, 0, block);
|
|
500
|
-
else
|
|
501
|
-
out.push(`
|
|
502
|
-
${block}`);
|
|
503
|
-
}
|
|
504
|
-
let result = out.join("\n");
|
|
505
|
-
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
506
|
-
result += "\n";
|
|
507
|
-
return result;
|
|
698
|
+
function reconcileEnvText2(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
699
|
+
return reconcileEnvText(existing, values, keys);
|
|
508
700
|
}
|
|
509
701
|
function readEngagementEnv(engagementId, home) {
|
|
510
702
|
try {
|
|
@@ -522,7 +714,7 @@ function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KE
|
|
|
522
714
|
} catch {
|
|
523
715
|
existing = null;
|
|
524
716
|
}
|
|
525
|
-
const reconciled =
|
|
717
|
+
const reconciled = reconcileEnvText2(existing, values, keys);
|
|
526
718
|
if (existing === null && reconciled === "")
|
|
527
719
|
return "skipped";
|
|
528
720
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
@@ -548,7 +740,7 @@ function applyOwnerOnly(path, dir) {
|
|
|
548
740
|
function engagementResolutionShell() {
|
|
549
741
|
return `# --- Halfcycle credential resolution (generated; do not edit) ----------------
|
|
550
742
|
# THE CREDENTIAL IS NOT IN THIS REPOSITORY. It lives at
|
|
551
|
-
# $HOME
|
|
743
|
+
# $HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/<engagement-id>/${ENV_FILENAME}
|
|
552
744
|
# written owner-only by \`npx halfcycle\`. What this repository holds is the
|
|
553
745
|
# engagement id, in the committed .halfcycle/bundle.json. So: read the id out of
|
|
554
746
|
# the pin, then name the file. There is no jq here and node is not dependable on a
|
|
@@ -579,10 +771,10 @@ halfcycle_env_file() {
|
|
|
579
771
|
HALFCYCLE_ENV_PROBLEM="no-home"
|
|
580
772
|
return 1
|
|
581
773
|
fi
|
|
582
|
-
if [ -f "$HOME
|
|
774
|
+
if [ -f "$HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/$hc_id/${NOT_THIS_ACCOUNT_MARKER_FILENAME}" ]; then
|
|
583
775
|
HALFCYCLE_ENV_NOT_THIS_ACCOUNT=1
|
|
584
776
|
fi
|
|
585
|
-
hc_env="$HOME
|
|
777
|
+
hc_env="$HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/$hc_id/${ENV_FILENAME}"
|
|
586
778
|
if [ ! -f "$hc_env" ]; then
|
|
587
779
|
HALFCYCLE_ENV_PROBLEM="no-credential"
|
|
588
780
|
return 1
|
|
@@ -781,7 +973,7 @@ function resolveVendoredBinary() {
|
|
|
781
973
|
if (existsSync3(candidate))
|
|
782
974
|
return candidate;
|
|
783
975
|
}
|
|
784
|
-
throw new Error(`[bundle install] Cannot find the self-contained guard binary bin.bundle.mjs.
|
|
976
|
+
throw new Error(`[bundle install] Cannot find the self-contained guard binary bin.bundle.mjs. This copy of the halfcycle package is incomplete \u2014 reinstall it, or rebuild it if you are working on it. Looked in: ${candidates.join(", ")}.`);
|
|
785
977
|
}
|
|
786
978
|
var WRITE_ALLOWLIST = [
|
|
787
979
|
".claude/commands",
|
|
@@ -930,6 +1122,15 @@ function generateSettingsJson() {
|
|
|
930
1122
|
timeout: 5
|
|
931
1123
|
}
|
|
932
1124
|
]
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
hooks: [
|
|
1128
|
+
{
|
|
1129
|
+
type: "command",
|
|
1130
|
+
command: "bash ./.claude/hooks/guard-runner.sh refresh-credential",
|
|
1131
|
+
timeout: 10
|
|
1132
|
+
}
|
|
1133
|
+
]
|
|
933
1134
|
}
|
|
934
1135
|
],
|
|
935
1136
|
UserPromptSubmit: [
|
|
@@ -999,9 +1200,9 @@ function generateGuardRunnerWrapper() {
|
|
|
999
1200
|
${GENERATED_GUARD_RUNNER_HEADER} the Halfcycle installer.
|
|
1000
1201
|
#
|
|
1001
1202
|
# Loads this engagement's credentials so the guard runner has its GUARD_SERVICE_*
|
|
1002
|
-
# values when Claude Code fires PostToolUse / Stop / SubagentStop,
|
|
1003
|
-
# SELF-CONTAINED binary vendored at ${VENDORED_BIN_REL} \u2014 a path
|
|
1004
|
-
# project, so the hook resolves in any checkout of it.
|
|
1203
|
+
# values when Claude Code fires SessionStart / PostToolUse / Stop / SubagentStop,
|
|
1204
|
+
# then exec's the SELF-CONTAINED binary vendored at ${VENDORED_BIN_REL} \u2014 a path
|
|
1205
|
+
# inside this project, so the hook resolves in any checkout of it.
|
|
1005
1206
|
#
|
|
1006
1207
|
# The runner itself reads only process.env, so env loading lives here in the hook
|
|
1007
1208
|
# wiring rather than inside the binary.
|
|
@@ -1016,6 +1217,16 @@ ${engagementResolutionShell()}
|
|
|
1016
1217
|
# the runner say what it found \u2014 its never-configured branch is loud and non-zero
|
|
1017
1218
|
# on purpose, and a wrapper that exited quietly here would hide it.
|
|
1018
1219
|
if halfcycle_env_file "$PROJECT_DIR"; then
|
|
1220
|
+
# EXPORTED, NOT MERELY SET \u2014 one word, and without it a whole feature is off.
|
|
1221
|
+
# \`set -a\` exports what is assigned AFTER it, and the resolution above assigned
|
|
1222
|
+
# this variable BEFORE it, so the exec'd binary would never see the name of the
|
|
1223
|
+
# file these credentials came from. It needs that name: when the service says
|
|
1224
|
+
# this engagement's credential has timed out, the binary renews it by rewriting
|
|
1225
|
+
# that file. With nothing naming the file it renews nothing \u2014 and it says
|
|
1226
|
+
# nothing and exits 0 while doing so, because there is no session in which that
|
|
1227
|
+
# silence is wrong on its own. So a wrapper missing this line looks perfectly
|
|
1228
|
+
# healthy at every session start and renews nothing, forever.
|
|
1229
|
+
export HALFCYCLE_ENV_FILE
|
|
1019
1230
|
set -a
|
|
1020
1231
|
. "$HALFCYCLE_ENV_FILE"
|
|
1021
1232
|
set +a
|
|
@@ -1743,6 +1954,12 @@ async function createEngagement(baseUrl, name, credential, derivedName) {
|
|
|
1743
1954
|
{ action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" }
|
|
1744
1955
|
);
|
|
1745
1956
|
}
|
|
1957
|
+
function withDeclaredTelemetryKey(raw) {
|
|
1958
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
1959
|
+
return null;
|
|
1960
|
+
const obj = raw;
|
|
1961
|
+
return "controlTelemetryUrl" in obj ? obj : { ...obj, controlTelemetryUrl: "" };
|
|
1962
|
+
}
|
|
1746
1963
|
async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
1747
1964
|
const bearer = credential?.trim();
|
|
1748
1965
|
let res;
|
|
@@ -1767,7 +1984,9 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
1767
1984
|
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
|
|
1768
1985
|
throw new Error(`[bundle install] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
1769
1986
|
}
|
|
1770
|
-
const
|
|
1987
|
+
const candidate = withDeclaredTelemetryKey(await res.json().catch(() => null));
|
|
1988
|
+
const declared = engagementCredentialResponseSchema.safeParse(candidate);
|
|
1989
|
+
const body = declared.success ? declared.data : candidate;
|
|
1771
1990
|
if (!body || typeof body.engagementId !== "string" || typeof body.sessionToken !== "string") {
|
|
1772
1991
|
throw new Error(`[bundle install] ${shape.action} response from ${url} did not carry {engagementId, sessionToken}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
1773
1992
|
}
|
|
@@ -1786,6 +2005,16 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
1786
2005
|
throw new Error(`[bundle install] The Halfcycle service at ${url} created an engagement but did not say where its guard service lives (no guardUrl on the response). Without that address the install would wire a guard hook that evaluates nothing, so nothing was written. Either that service is older than this installer, or ${url} is not a Halfcycle service.`);
|
|
1787
2006
|
}
|
|
1788
2007
|
const controlTelemetryUrl = typeof body.controlTelemetryUrl === "string" && body.controlTelemetryUrl.trim() !== "" ? body.controlTelemetryUrl.trim().replace(/\/+$/, "") : void 0;
|
|
2008
|
+
if (!declared.success) {
|
|
2009
|
+
const fields = declared.error.issues.map((issue) => {
|
|
2010
|
+
const unrecognised = issue.keys;
|
|
2011
|
+
if (unrecognised && unrecognised.length > 0) {
|
|
2012
|
+
return `unexpected: ${unrecognised.map(String).join(", ")}`;
|
|
2013
|
+
}
|
|
2014
|
+
return issue.path.length > 0 ? issue.path.map(String).join(".") : "(the body itself)";
|
|
2015
|
+
}).join(", ");
|
|
2016
|
+
throw new Error(`[bundle install] ${shape.action} response from ${url} is not the credential response this CLI is built to read (${fields}). ${shape.nothingHappened}. Either that service is a different version from this installer, or ${url} is not a Halfcycle service.`);
|
|
2017
|
+
}
|
|
1789
2018
|
return {
|
|
1790
2019
|
engagementId: body.engagementId,
|
|
1791
2020
|
sessionToken: body.sessionToken,
|
|
@@ -1831,7 +2060,7 @@ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync4, readFile
|
|
|
1831
2060
|
import { dirname as dirname3, join as join6, posix } from "node:path";
|
|
1832
2061
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
1833
2062
|
var DECLARED_DIR_PREFIX = "docs";
|
|
1834
|
-
var
|
|
2063
|
+
var RESERVED_METHOD_PATH = "docs/method";
|
|
1835
2064
|
var STEP_ID_GRAMMAR = /\b(?:l[0-5](?:-l[0-5])?|xl)\.[a-z0-9-]+/i;
|
|
1836
2065
|
var STEP_ANNOTATION = /<!--\s*step:/i;
|
|
1837
2066
|
var ANCHOR_DECLARATION = /<!--\s*anchor:/i;
|
|
@@ -1858,8 +2087,8 @@ function assertWritable(relPath) {
|
|
|
1858
2087
|
if (path.split("/").includes("..")) {
|
|
1859
2088
|
throw new Error(`[halfcycle setup] Refusing to write "${relPath}": a path may not climb out of the repository.`);
|
|
1860
2089
|
}
|
|
1861
|
-
if (isUnderIgnoringCase(foldWin32Canonicalisation(path),
|
|
1862
|
-
throw new Error(`[halfcycle setup] Refusing to write "${relPath}": ${
|
|
2090
|
+
if (isUnderIgnoringCase(foldWin32Canonicalisation(path), RESERVED_METHOD_PATH)) {
|
|
2091
|
+
throw new Error(`[halfcycle setup] Refusing to write "${relPath}": ${RESERVED_METHOD_PATH}/ is reserved and is never created in a project repository. The method is served per step and never installed.`);
|
|
1863
2092
|
}
|
|
1864
2093
|
const declared = DECLARED_ROOT_FILES.includes(path) || isUnder(path, DECLARED_DIR_PREFIX);
|
|
1865
2094
|
if (!declared) {
|
|
@@ -1869,7 +2098,7 @@ function assertWritable(relPath) {
|
|
|
1869
2098
|
function assertContentCarriesNoMethodStructure(relPath, content) {
|
|
1870
2099
|
const carried = STEP_ID_GRAMMAR.test(content) && "a method step identifier" || STEP_ANNOTATION.test(content) && "a method step annotation" || ANCHOR_DECLARATION.test(content) && "a method anchor declaration, so this is corpus text";
|
|
1871
2100
|
if (carried) {
|
|
1872
|
-
throw new Error(`[halfcycle setup] Refusing to write "${relPath}": the content carries ${carried}. What a step directs is executed, never transcribed into the repository
|
|
2101
|
+
throw new Error(`[halfcycle setup] Refusing to write "${relPath}": the content carries ${carried}. What a step directs is executed, never transcribed into the repository.`);
|
|
1873
2102
|
}
|
|
1874
2103
|
}
|
|
1875
2104
|
function assertUsableHeading(relPath, heading) {
|
|
@@ -2419,7 +2648,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
2419
2648
|
"",
|
|
2420
2649
|
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
2421
2650
|
"",
|
|
2422
|
-
"> This Build Record was assembled automatically at phase close by
|
|
2651
|
+
"> This Build Record was assembled automatically at phase close by `npx halfcycle close-phase`. Every data field below is projected from the companion JSON \u2014 no record content is hand-authored.",
|
|
2423
2652
|
"",
|
|
2424
2653
|
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
2425
2654
|
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
@@ -2439,7 +2668,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
2439
2668
|
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
2440
2669
|
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
2441
2670
|
"",
|
|
2442
|
-
guardsList ? `Guards evaluated (
|
|
2671
|
+
guardsList ? `Guards evaluated (results only): ${guardsList}. *(What a guard reported about this phase is recorded; the guard's own definition and its history are not.)*` : "No guards fired during this phase.",
|
|
2443
2672
|
"",
|
|
2444
2673
|
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
2445
2674
|
"",
|
|
@@ -2451,7 +2680,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
2451
2680
|
""
|
|
2452
2681
|
];
|
|
2453
2682
|
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
2454
|
-
lines.push(`**
|
|
2683
|
+
lines.push(`**Production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
2455
2684
|
}
|
|
2456
2685
|
lines.push("## Gates that earned their keep", "", `**${record2.gatesEarnedKeep.defectsCaughtPreHuman}** defects were caught by the gates before the human walk; the walk itself found **${record2.gatesEarnedKeep.bugsReachingHumanWalk}**. Named: ${record2.gatesEarnedKeep.named.join("; ")}.`, "", "## Instruments", "", `- **Marginal-cost self-accounting:** ${record2.instruments.marginalCostSelfAccounting}`, `- **COE add-rate:** seam-new ${record2.instruments.coeAddRate.seamNew}, seam-repeat ${record2.instruments.coeAddRate.seamRepeat}, model-limitation ${record2.instruments.coeAddRate.modelLimitation}. ${record2.instruments.coeAddRate.notes}`, "", "## Invariants exercised", "", record2.invariantsExercised.join(", "), "", "## Deviations & decisions recorded", "", ...record2.deviations.map((d) => `- ${d}`), "", "## Tasks", "", `- **Planned:** ${record2.tasks.planned} \xB7 **Landed:** ${record2.tasks.landed} \xB7 **Cancelled:** ${record2.tasks.cancelled.length > 0 ? record2.tasks.cancelled.join(", ") : "none"}`, "");
|
|
2457
2686
|
return lines.join("\n");
|