halfcycle 0.3.22 → 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 +894 -214
- 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/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +641 -143
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +112 -11
- package/dist/install.d.ts.map +1 -1
- package/dist/merge-settings.d.ts +27 -9
- 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,50 +220,364 @@ 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
|
|
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({
|
|
401
|
+
// The outline of the head. Members who do the same kind of work share one, so a
|
|
402
|
+
// team is recognisable before any name is read.
|
|
403
|
+
head: crewFaceHeadSchema,
|
|
404
|
+
// What sits on top of the head, if anything.
|
|
405
|
+
antenna: crewFaceAntennaSchema,
|
|
406
|
+
// The eyes.
|
|
407
|
+
eyes: crewFaceEyesSchema,
|
|
408
|
+
// The mouth.
|
|
409
|
+
mouth: crewFaceMouthSchema,
|
|
410
|
+
// Whether this member is drawn with ears.
|
|
411
|
+
ears: z5.boolean()
|
|
412
|
+
}).strict();
|
|
413
|
+
var crewMemberSchema = z5.object({
|
|
146
414
|
// The agent's name. The same twenty-one on every project, so a name is a
|
|
147
415
|
// vocabulary a returning client already knows.
|
|
148
|
-
callsign:
|
|
416
|
+
callsign: z5.string().min(1).max(32),
|
|
149
417
|
// Which of the three headings this member reads under.
|
|
150
418
|
group: crewGroupSchema,
|
|
151
419
|
// The kind of work this member takes. A task names a discipline, and only
|
|
152
420
|
// agents holding it can be dispatched to it.
|
|
153
|
-
discipline:
|
|
421
|
+
discipline: z5.string().min(1).max(CREW_DISCIPLINE_MAX),
|
|
154
422
|
// How much thinking this member brings. There is no field naming the supplier
|
|
155
423
|
// behind it, on this shape or on any other.
|
|
156
|
-
tier: crewTierSchema
|
|
424
|
+
tier: crewTierSchema,
|
|
425
|
+
// How this member's face is drawn. The same face on every project, so a
|
|
426
|
+
// returning client recognises a member before reading the name.
|
|
427
|
+
face: crewFaceSchema
|
|
157
428
|
}).strict();
|
|
158
429
|
var CREW_ROSTER = [
|
|
159
430
|
// ── builders: code ──────────────────────────────────────────────────────────
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
431
|
+
{
|
|
432
|
+
callsign: "ADA",
|
|
433
|
+
group: "builder",
|
|
434
|
+
discipline: "implementation",
|
|
435
|
+
tier: "deep",
|
|
436
|
+
face: { head: "sq", antenna: "stalk", eyes: "dots", mouth: "line", ears: false }
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
callsign: "OTTO",
|
|
440
|
+
group: "builder",
|
|
441
|
+
discipline: "implementation",
|
|
442
|
+
tier: "deep",
|
|
443
|
+
face: { head: "sq", antenna: "twin", eyes: "bars", mouth: "grid", ears: true }
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
callsign: "MILO",
|
|
447
|
+
group: "builder",
|
|
448
|
+
discipline: "implementation",
|
|
449
|
+
tier: "standard",
|
|
450
|
+
face: { head: "sqr", antenna: "stalk", eyes: "dots", mouth: "dots", ears: false }
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
callsign: "NOVA",
|
|
454
|
+
group: "builder",
|
|
455
|
+
discipline: "implementation",
|
|
456
|
+
tier: "standard",
|
|
457
|
+
face: { head: "sq", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
callsign: "KAI",
|
|
461
|
+
group: "builder",
|
|
462
|
+
discipline: "implementation",
|
|
463
|
+
tier: "standard",
|
|
464
|
+
face: { head: "sqr", antenna: "none", eyes: "bars", mouth: "line", ears: true }
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
callsign: "HUGO",
|
|
468
|
+
group: "builder",
|
|
469
|
+
discipline: "infrastructure",
|
|
470
|
+
tier: "standard",
|
|
471
|
+
face: { head: "sq", antenna: "twin", eyes: "dots", mouth: "grid", ears: true }
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
callsign: "WREN",
|
|
475
|
+
group: "builder",
|
|
476
|
+
discipline: "refactor",
|
|
477
|
+
tier: "fast",
|
|
478
|
+
face: { head: "sqr", antenna: "stalk", eyes: "bars", mouth: "wave", ears: false }
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
callsign: "PIP",
|
|
482
|
+
group: "builder",
|
|
483
|
+
discipline: "refactor",
|
|
484
|
+
tier: "fast",
|
|
485
|
+
face: { head: "sqr", antenna: "none", eyes: "dots", mouth: "dots", ears: false }
|
|
486
|
+
},
|
|
168
487
|
// ── builders: specs and docs ────────────────────────────────────────────────
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
488
|
+
{
|
|
489
|
+
callsign: "IRIS",
|
|
490
|
+
group: "builder",
|
|
491
|
+
discipline: "feature spec",
|
|
492
|
+
tier: "deep",
|
|
493
|
+
face: { head: "dome", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
callsign: "JUNO",
|
|
497
|
+
group: "builder",
|
|
498
|
+
discipline: "feature spec",
|
|
499
|
+
tier: "standard",
|
|
500
|
+
face: { head: "dome", antenna: "none", eyes: "dots", mouth: "wave", ears: false }
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
callsign: "LEX",
|
|
504
|
+
group: "builder",
|
|
505
|
+
discipline: "context & docs",
|
|
506
|
+
tier: "fast",
|
|
507
|
+
face: { head: "dome", antenna: "twin", eyes: "bars", mouth: "dots", ears: false }
|
|
508
|
+
},
|
|
172
509
|
// ── reviewers ───────────────────────────────────────────────────────────────
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{
|
|
510
|
+
{
|
|
511
|
+
callsign: "VERA",
|
|
512
|
+
group: "reviewer",
|
|
513
|
+
discipline: "spec consistency",
|
|
514
|
+
tier: "deep",
|
|
515
|
+
face: { head: "rnd", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
callsign: "ODIN",
|
|
519
|
+
group: "reviewer",
|
|
520
|
+
discipline: "cross-spec audit",
|
|
521
|
+
tier: "deep",
|
|
522
|
+
face: { head: "rnd", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
callsign: "CASS",
|
|
526
|
+
group: "reviewer",
|
|
527
|
+
discipline: "code review",
|
|
528
|
+
tier: "deep",
|
|
529
|
+
face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "grid", ears: true }
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
callsign: "THEO",
|
|
533
|
+
group: "reviewer",
|
|
534
|
+
discipline: "code review",
|
|
535
|
+
tier: "standard",
|
|
536
|
+
face: { head: "rnd", antenna: "twin", eyes: "bars", mouth: "line", ears: false }
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
callsign: "ECHO",
|
|
540
|
+
group: "reviewer",
|
|
541
|
+
discipline: "test quality",
|
|
542
|
+
tier: "standard",
|
|
543
|
+
face: { head: "rnd", antenna: "stalk", eyes: "dots", mouth: "wave", ears: false }
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
callsign: "MAVIS",
|
|
547
|
+
group: "reviewer",
|
|
548
|
+
discipline: "suites & coverage",
|
|
549
|
+
tier: "standard",
|
|
550
|
+
face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "dots", ears: false }
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
callsign: "ZARA",
|
|
554
|
+
group: "reviewer",
|
|
555
|
+
discipline: "smoke & walk aid",
|
|
556
|
+
tier: "standard",
|
|
557
|
+
face: { head: "rnd", antenna: "dish", eyes: "bars", mouth: "wave", ears: false }
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
callsign: "NORA",
|
|
561
|
+
group: "reviewer",
|
|
562
|
+
discipline: "evidence & provenance",
|
|
563
|
+
tier: "fast",
|
|
564
|
+
face: { head: "rnd", antenna: "twin", eyes: "dots", mouth: "line", ears: false }
|
|
565
|
+
},
|
|
181
566
|
// ── always on: continuous, takes no task ────────────────────────────────────
|
|
182
|
-
{
|
|
183
|
-
|
|
567
|
+
{
|
|
568
|
+
callsign: "ARGUS",
|
|
569
|
+
group: "always-on",
|
|
570
|
+
discipline: "guard \xB7 every diff",
|
|
571
|
+
tier: "standard",
|
|
572
|
+
face: { head: "hex", antenna: "dish", eyes: "visor", mouth: "grid", ears: true }
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
callsign: "ATLAS",
|
|
576
|
+
group: "always-on",
|
|
577
|
+
discipline: "coordinator",
|
|
578
|
+
tier: "deep",
|
|
579
|
+
face: { head: "hex", antenna: "twin", eyes: "cyclops", mouth: "line", ears: true }
|
|
580
|
+
}
|
|
184
581
|
];
|
|
185
582
|
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
186
583
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
@@ -246,6 +643,21 @@ function describeIdentity(identity) {
|
|
|
246
643
|
return `(a ${typeof identity})`;
|
|
247
644
|
}
|
|
248
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
|
+
|
|
249
661
|
// dist/engagement-credential.js
|
|
250
662
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
251
663
|
import { platform as platform2 } from "node:os";
|
|
@@ -255,12 +667,13 @@ import { join as join2 } from "node:path";
|
|
|
255
667
|
import { homedir, platform } from "node:os";
|
|
256
668
|
import { dirname, join } from "node:path";
|
|
257
669
|
function halfcycleHome(home) {
|
|
258
|
-
return join(home ?? homedir(),
|
|
670
|
+
return join(home ?? homedir(), HALFCYCLE_DIR_NAME);
|
|
259
671
|
}
|
|
260
672
|
|
|
261
673
|
// dist/engagement-credential.js
|
|
262
|
-
var ENGAGEMENTS_DIR =
|
|
263
|
-
var ENV_FILENAME =
|
|
674
|
+
var ENGAGEMENTS_DIR = ENGAGEMENTS_DIR_NAME;
|
|
675
|
+
var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
|
|
676
|
+
var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
|
|
264
677
|
var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
|
|
265
678
|
function engagementStateDir(engagementId, home) {
|
|
266
679
|
return join2(halfcycleHome(home), ENGAGEMENTS_DIR, engagementId);
|
|
@@ -268,7 +681,6 @@ function engagementStateDir(engagementId, home) {
|
|
|
268
681
|
function engagementEnvPath(engagementId, home) {
|
|
269
682
|
return join2(engagementStateDir(engagementId, home), ENV_FILENAME);
|
|
270
683
|
}
|
|
271
|
-
var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
|
|
272
684
|
var GUARD_ENV_KEYS = [
|
|
273
685
|
"GUARD_SERVICE_URL",
|
|
274
686
|
"GUARD_SERVICE_TOKEN",
|
|
@@ -283,82 +695,8 @@ var ENGAGEMENT_ENV_KEYS = [
|
|
|
283
695
|
...GUARD_ENV_KEYS,
|
|
284
696
|
"CONTROL_TELEMETRY_URL"
|
|
285
697
|
];
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
289
|
-
}
|
|
290
|
-
function unquote(value) {
|
|
291
|
-
const v = value.trim();
|
|
292
|
-
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
293
|
-
return v.slice(1, -1).split(`'\\''`).join(`'`);
|
|
294
|
-
}
|
|
295
|
-
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
|
|
296
|
-
return v.slice(1, -1);
|
|
297
|
-
return v;
|
|
298
|
-
}
|
|
299
|
-
function parseEnvText(raw) {
|
|
300
|
-
const out = {};
|
|
301
|
-
for (const line of raw.split("\n")) {
|
|
302
|
-
const eq = line.indexOf("=");
|
|
303
|
-
if (eq === -1)
|
|
304
|
-
continue;
|
|
305
|
-
const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
|
|
306
|
-
if (key === "" || key.startsWith("#"))
|
|
307
|
-
continue;
|
|
308
|
-
out[key] = unquote(line.slice(eq + 1));
|
|
309
|
-
}
|
|
310
|
-
return out;
|
|
311
|
-
}
|
|
312
|
-
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
313
|
-
const desired = new Map(keys.map((k) => {
|
|
314
|
-
const value = values[k];
|
|
315
|
-
return [k, value === void 0 ? "" : value];
|
|
316
|
-
}));
|
|
317
|
-
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
318
|
-
const written = keys.filter((k) => desired.get(k) !== null);
|
|
319
|
-
if (existing === null) {
|
|
320
|
-
if (written.length === 0)
|
|
321
|
-
return "";
|
|
322
|
-
return `${ENV_HEADER}
|
|
323
|
-
` + written.map(line).join("\n") + "\n";
|
|
324
|
-
}
|
|
325
|
-
const seen = /* @__PURE__ */ new Set();
|
|
326
|
-
const out = [];
|
|
327
|
-
for (const existingLine of existing.split("\n")) {
|
|
328
|
-
const eq = existingLine.indexOf("=");
|
|
329
|
-
if (eq === -1) {
|
|
330
|
-
out.push(existingLine);
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
const lhs = existingLine.slice(0, eq);
|
|
334
|
-
const exported = /^\s*export\s+/.exec(lhs);
|
|
335
|
-
const prefix = exported === null ? "" : exported[0];
|
|
336
|
-
const key = lhs.slice(prefix.length).trim();
|
|
337
|
-
if (!desired.has(key)) {
|
|
338
|
-
out.push(existingLine);
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
seen.add(key);
|
|
342
|
-
if (desired.get(key) === null)
|
|
343
|
-
continue;
|
|
344
|
-
out.push(`${prefix}${line(key)}`);
|
|
345
|
-
}
|
|
346
|
-
const missing = written.filter((k) => !seen.has(k));
|
|
347
|
-
if (missing.length > 0) {
|
|
348
|
-
const header = existing.includes(ENV_HEADER) ? "" : `${ENV_HEADER}
|
|
349
|
-
`;
|
|
350
|
-
const block = header + missing.map(line).join("\n");
|
|
351
|
-
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
352
|
-
if (trailingBlank)
|
|
353
|
-
out.splice(out.length - 1, 0, block);
|
|
354
|
-
else
|
|
355
|
-
out.push(`
|
|
356
|
-
${block}`);
|
|
357
|
-
}
|
|
358
|
-
let result = out.join("\n");
|
|
359
|
-
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
360
|
-
result += "\n";
|
|
361
|
-
return result;
|
|
698
|
+
function reconcileEnvText2(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
699
|
+
return reconcileEnvText(existing, values, keys);
|
|
362
700
|
}
|
|
363
701
|
function readEngagementEnv(engagementId, home) {
|
|
364
702
|
try {
|
|
@@ -376,7 +714,7 @@ function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KE
|
|
|
376
714
|
} catch {
|
|
377
715
|
existing = null;
|
|
378
716
|
}
|
|
379
|
-
const reconciled =
|
|
717
|
+
const reconciled = reconcileEnvText2(existing, values, keys);
|
|
380
718
|
if (existing === null && reconciled === "")
|
|
381
719
|
return "skipped";
|
|
382
720
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
@@ -402,7 +740,7 @@ function applyOwnerOnly(path, dir) {
|
|
|
402
740
|
function engagementResolutionShell() {
|
|
403
741
|
return `# --- Halfcycle credential resolution (generated; do not edit) ----------------
|
|
404
742
|
# THE CREDENTIAL IS NOT IN THIS REPOSITORY. It lives at
|
|
405
|
-
# $HOME
|
|
743
|
+
# $HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/<engagement-id>/${ENV_FILENAME}
|
|
406
744
|
# written owner-only by \`npx halfcycle\`. What this repository holds is the
|
|
407
745
|
# engagement id, in the committed .halfcycle/bundle.json. So: read the id out of
|
|
408
746
|
# the pin, then name the file. There is no jq here and node is not dependable on a
|
|
@@ -433,10 +771,10 @@ halfcycle_env_file() {
|
|
|
433
771
|
HALFCYCLE_ENV_PROBLEM="no-home"
|
|
434
772
|
return 1
|
|
435
773
|
fi
|
|
436
|
-
if [ -f "$HOME
|
|
774
|
+
if [ -f "$HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/$hc_id/${NOT_THIS_ACCOUNT_MARKER_FILENAME}" ]; then
|
|
437
775
|
HALFCYCLE_ENV_NOT_THIS_ACCOUNT=1
|
|
438
776
|
fi
|
|
439
|
-
hc_env="$HOME
|
|
777
|
+
hc_env="$HOME/${HALFCYCLE_DIR}/${ENGAGEMENTS_DIR}/$hc_id/${ENV_FILENAME}"
|
|
440
778
|
if [ ! -f "$hc_env" ]; then
|
|
441
779
|
HALFCYCLE_ENV_PROBLEM="no-credential"
|
|
442
780
|
return 1
|
|
@@ -635,7 +973,7 @@ function resolveVendoredBinary() {
|
|
|
635
973
|
if (existsSync3(candidate))
|
|
636
974
|
return candidate;
|
|
637
975
|
}
|
|
638
|
-
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(", ")}.`);
|
|
639
977
|
}
|
|
640
978
|
var WRITE_ALLOWLIST = [
|
|
641
979
|
".claude/commands",
|
|
@@ -696,11 +1034,15 @@ function writeCollisionSafe(targetAbsPath, targetRepoRoot, content) {
|
|
|
696
1034
|
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
697
1035
|
return "written";
|
|
698
1036
|
}
|
|
1037
|
+
var INSTALLER_OWNED_DIR = ".halfcycle/";
|
|
699
1038
|
function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
700
1039
|
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
701
1040
|
if (!isAllowlisted(rel)) {
|
|
702
1041
|
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
703
1042
|
}
|
|
1043
|
+
if (!rel.startsWith(INSTALLER_OWNED_DIR)) {
|
|
1044
|
+
throw new Error(`[bundle install] Path-ownership violation: attempted to claim "${rel}" by path. Only ${INSTALLER_OWNED_DIR} is owned outright by this installer; everywhere else a differing file may be a developer's, so ownership must be proved from the file's own contents (writeOwnedGenerated) rather than assumed from where it sits.`);
|
|
1045
|
+
}
|
|
704
1046
|
if (existsSync3(targetAbsPath) && readFileSync4(targetAbsPath, "utf-8") === content) {
|
|
705
1047
|
return "skipped";
|
|
706
1048
|
}
|
|
@@ -708,6 +1050,39 @@ function writeOwned(targetAbsPath, targetRepoRoot, content) {
|
|
|
708
1050
|
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
709
1051
|
return "written";
|
|
710
1052
|
}
|
|
1053
|
+
var GENERATED_HEADER_LINE_INDEX = 1;
|
|
1054
|
+
function writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker) {
|
|
1055
|
+
const rel = relative(targetRepoRoot, targetAbsPath).replace(/\\/g, "/");
|
|
1056
|
+
if (!isAllowlisted(rel)) {
|
|
1057
|
+
throw new Error(`[bundle install] Write-allowlist violation: attempted to write "${rel}". Only the method surface and scaffolding paths are writable.`);
|
|
1058
|
+
}
|
|
1059
|
+
let replacedExisting = false;
|
|
1060
|
+
if (existsSync3(targetAbsPath)) {
|
|
1061
|
+
const current = readFileSync4(targetAbsPath, "utf-8");
|
|
1062
|
+
if (current === content)
|
|
1063
|
+
return { outcome: "skipped", replacedExisting: false };
|
|
1064
|
+
const headerLine = current.split("\n")[GENERATED_HEADER_LINE_INDEX] ?? "";
|
|
1065
|
+
if (!headerLine.startsWith(generatedMarker))
|
|
1066
|
+
return { outcome: "collided", replacedExisting: false };
|
|
1067
|
+
replacedExisting = true;
|
|
1068
|
+
}
|
|
1069
|
+
mkdirSync3(dirname2(targetAbsPath), { recursive: true });
|
|
1070
|
+
writeFileSync3(targetAbsPath, content, "utf-8");
|
|
1071
|
+
return { outcome: "written", replacedExisting };
|
|
1072
|
+
}
|
|
1073
|
+
function recordOwnedGenerated(report, targetAbsPath, targetRepoRoot, content, generatedMarker, rel) {
|
|
1074
|
+
const { outcome, replacedExisting } = writeOwnedGenerated(targetAbsPath, targetRepoRoot, content, generatedMarker);
|
|
1075
|
+
record(report, outcome, rel);
|
|
1076
|
+
if (replacedExisting)
|
|
1077
|
+
report.replacedPaths.push(rel);
|
|
1078
|
+
}
|
|
1079
|
+
function recordOwned(report, targetAbsPath, targetRepoRoot, content, rel) {
|
|
1080
|
+
const existedBefore = existsSync3(targetAbsPath);
|
|
1081
|
+
const outcome = writeOwned(targetAbsPath, targetRepoRoot, content);
|
|
1082
|
+
record(report, outcome, rel);
|
|
1083
|
+
if (outcome === "written" && existedBefore)
|
|
1084
|
+
report.replacedPaths.push(rel);
|
|
1085
|
+
}
|
|
711
1086
|
function record(report, outcome, rel) {
|
|
712
1087
|
const bucket = {
|
|
713
1088
|
written: report.writtenPaths,
|
|
@@ -747,6 +1122,15 @@ function generateSettingsJson() {
|
|
|
747
1122
|
timeout: 5
|
|
748
1123
|
}
|
|
749
1124
|
]
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
hooks: [
|
|
1128
|
+
{
|
|
1129
|
+
type: "command",
|
|
1130
|
+
command: "bash ./.claude/hooks/guard-runner.sh refresh-credential",
|
|
1131
|
+
timeout: 10
|
|
1132
|
+
}
|
|
1133
|
+
]
|
|
750
1134
|
}
|
|
751
1135
|
],
|
|
752
1136
|
UserPromptSubmit: [
|
|
@@ -772,6 +1156,17 @@ function generateSettingsJson() {
|
|
|
772
1156
|
]
|
|
773
1157
|
}
|
|
774
1158
|
],
|
|
1159
|
+
Stop: [
|
|
1160
|
+
{
|
|
1161
|
+
hooks: [
|
|
1162
|
+
{
|
|
1163
|
+
type: "command",
|
|
1164
|
+
command: "bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
1165
|
+
timeout: 60
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
}
|
|
1169
|
+
],
|
|
775
1170
|
SubagentStop: [
|
|
776
1171
|
{
|
|
777
1172
|
hooks: [
|
|
@@ -782,20 +1177,32 @@ function generateSettingsJson() {
|
|
|
782
1177
|
}
|
|
783
1178
|
]
|
|
784
1179
|
}
|
|
1180
|
+
],
|
|
1181
|
+
SessionEnd: [
|
|
1182
|
+
{
|
|
1183
|
+
hooks: [
|
|
1184
|
+
{
|
|
1185
|
+
type: "command",
|
|
1186
|
+
command: "bash ./.claude/hooks/session-end-marker.sh",
|
|
1187
|
+
timeout: 5
|
|
1188
|
+
}
|
|
1189
|
+
]
|
|
1190
|
+
}
|
|
785
1191
|
]
|
|
786
1192
|
}
|
|
787
1193
|
};
|
|
788
1194
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
789
1195
|
}
|
|
790
1196
|
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
1197
|
+
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
791
1198
|
function generateGuardRunnerWrapper() {
|
|
792
1199
|
return `#!/usr/bin/env bash
|
|
793
|
-
|
|
1200
|
+
${GENERATED_GUARD_RUNNER_HEADER} the Halfcycle installer.
|
|
794
1201
|
#
|
|
795
1202
|
# Loads this engagement's credentials so the guard runner has its GUARD_SERVICE_*
|
|
796
|
-
# values when Claude Code fires PostToolUse / SubagentStop,
|
|
797
|
-
# SELF-CONTAINED binary vendored at ${VENDORED_BIN_REL} \u2014 a path
|
|
798
|
-
# 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.
|
|
799
1206
|
#
|
|
800
1207
|
# The runner itself reads only process.env, so env loading lives here in the hook
|
|
801
1208
|
# wiring rather than inside the binary.
|
|
@@ -810,6 +1217,16 @@ ${engagementResolutionShell()}
|
|
|
810
1217
|
# the runner say what it found \u2014 its never-configured branch is loud and non-zero
|
|
811
1218
|
# on purpose, and a wrapper that exited quietly here would hide it.
|
|
812
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
|
|
813
1230
|
set -a
|
|
814
1231
|
. "$HALFCYCLE_ENV_FILE"
|
|
815
1232
|
set +a
|
|
@@ -825,15 +1242,28 @@ fi
|
|
|
825
1242
|
exec node "$PROJECT_DIR/${VENDORED_BIN_REL}" "$@"
|
|
826
1243
|
`;
|
|
827
1244
|
}
|
|
1245
|
+
var GENERATED_SESSION_START_HEADER = "# Session-start marker \u2014 generated by";
|
|
1246
|
+
var SESSION_ID_FROM_HOOK_INPUT_SH = `SESSION_ID="$(printf '%s' "$INPUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);process.stdout.write(typeof j.session_id==="string"?j.session_id:"")}catch{process.stdout.write("")}})' 2>/dev/null || true)"`;
|
|
828
1247
|
function generateSessionStartMarker() {
|
|
829
1248
|
return `#!/usr/bin/env bash
|
|
830
|
-
|
|
831
|
-
#
|
|
832
|
-
#
|
|
1249
|
+
${GENERATED_SESSION_START_HEADER} the Halfcycle installer.
|
|
1250
|
+
# Records the git HEAD sha this session starts from, in a file named after this
|
|
1251
|
+
# session, so the stop-time check can compare what the session did against where
|
|
1252
|
+
# it began instead of only against the latest commit. The session id comes in on
|
|
1253
|
+
# stdin with the hook input; without one, the older session-less name is written
|
|
1254
|
+
# and the stop-time check compares against the latest commit as it used to.
|
|
1255
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
833
1256
|
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
834
1257
|
if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
835
|
-
HASH="$(
|
|
836
|
-
|
|
1258
|
+
HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
1259
|
+
${SESSION_ID_FROM_HOOK_INPUT_SH}
|
|
1260
|
+
MARKER_DIR="\${TMPDIR:-/tmp}"
|
|
1261
|
+
MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
1262
|
+
if [ -n "$SESSION_ID" ]; then
|
|
1263
|
+
MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref"
|
|
1264
|
+
else
|
|
1265
|
+
MARKER_FILE="\${MARKER_DIR}/halfcycle-session-\${HASH}.ref"
|
|
1266
|
+
fi
|
|
837
1267
|
git -C "$REPO_ROOT" rev-parse HEAD > "$MARKER_FILE" 2>/dev/null || true
|
|
838
1268
|
|
|
839
1269
|
# ---------------------------------------------------------------------------
|
|
@@ -896,9 +1326,41 @@ ${engagementResolutionShell()}
|
|
|
896
1326
|
exit 0
|
|
897
1327
|
`;
|
|
898
1328
|
}
|
|
1329
|
+
var GENERATED_SESSION_END_HEADER = "# Session-end marker cleanup \u2014 generated by";
|
|
1330
|
+
function generateSessionEndMarker() {
|
|
1331
|
+
return `#!/usr/bin/env bash
|
|
1332
|
+
${GENERATED_SESSION_END_HEADER} the Halfcycle installer.
|
|
1333
|
+
# Removes this session's marker file (and the evaluation-state file beside it)
|
|
1334
|
+
# written at session start. Deletion happens HERE and not at stop time: the stop
|
|
1335
|
+
# hook runs at the end of every turn, and deleting the marker there would throw
|
|
1336
|
+
# away the session's starting point for every later turn.
|
|
1337
|
+
INPUT="$(cat 2>/dev/null || true)"
|
|
1338
|
+
|
|
1339
|
+
REPO_ROOT="\${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
|
|
1340
|
+
if [ -z "$REPO_ROOT" ]; then exit 0; fi
|
|
1341
|
+
HASH="$(printf '%s' "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
1342
|
+
|
|
1343
|
+
# The session id arrives on stdin as part of the hook payload. Empty when absent.
|
|
1344
|
+
${SESSION_ID_FROM_HOOK_INPUT_SH}
|
|
1345
|
+
|
|
1346
|
+
MARKER_DIR="\${TMPDIR:-/tmp}"
|
|
1347
|
+
MARKER_DIR="\${MARKER_DIR%/}" # strip trailing slash (macOS TMPDIR ends in /)
|
|
1348
|
+
|
|
1349
|
+
# Exact names only \u2014 never a wildcard. A pattern would match another session
|
|
1350
|
+
# running in this same repository and delete the point it started from.
|
|
1351
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.ref" 2>/dev/null || true
|
|
1352
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}.eval" 2>/dev/null || true
|
|
1353
|
+
if [ -n "$SESSION_ID" ]; then
|
|
1354
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.ref" 2>/dev/null || true
|
|
1355
|
+
rm -f "\${MARKER_DIR}/halfcycle-session-\${HASH}-\${SESSION_ID}.eval" 2>/dev/null || true
|
|
1356
|
+
fi
|
|
1357
|
+
exit 0
|
|
1358
|
+
`;
|
|
1359
|
+
}
|
|
1360
|
+
var GENERATED_USER_PROMPT_REMINDER_HEADER = "# UserPromptSubmit house-rule reminder \u2014 generated by";
|
|
899
1361
|
function generateUserPromptReminderHook() {
|
|
900
1362
|
return `#!/usr/bin/env bash
|
|
901
|
-
|
|
1363
|
+
${GENERATED_USER_PROMPT_REMINDER_HEADER} the Halfcycle installer.
|
|
902
1364
|
#
|
|
903
1365
|
# Injects ONE plain-text sentence into context on every prompt (F-2(c),
|
|
904
1366
|
# W8-T-05b) \u2014 the per-turn twin of the house rule W8-T-05a put in the client's
|
|
@@ -996,17 +1458,31 @@ function generateCiStanza() {
|
|
|
996
1458
|
#
|
|
997
1459
|
# The job prints the diff base it used and how many files it evaluated, on every
|
|
998
1460
|
# run. If that line says 0 files on a commit that changed something, the base is
|
|
999
|
-
# wrong \u2014 set HALFCYCLE_DIFF_BASE in the env block
|
|
1000
|
-
# (on a GitHub push event, \${{ github.event.before }}
|
|
1461
|
+
# wrong \u2014 set HALFCYCLE_DIFF_BASE in the env block OF YOUR COPY, beside the two
|
|
1462
|
+
# secrets, to name it explicitly (on a GitHub push event, \${{ github.event.before }}
|
|
1463
|
+
# is the right value).
|
|
1464
|
+
#
|
|
1465
|
+
# EDIT YOUR COPY, NOT THIS FILE. This one is regenerated by the installer and your
|
|
1466
|
+
# changes to it would be replaced the next time you run \`npx halfcycle\`. It is
|
|
1467
|
+
# also inert where it sits: no CI system reads this path. Copy the job above into
|
|
1468
|
+
# your own workflow and change it there.
|
|
1001
1469
|
`;
|
|
1002
1470
|
}
|
|
1003
1471
|
var MCP_REGISTRATION_REL = ".mcp.json";
|
|
1004
1472
|
var MCP_SERVER_KEY = "halfcycle";
|
|
1005
1473
|
var MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
1006
1474
|
var MCP_HEADERS_HELPER_COMMAND = `/bin/sh -c 'd=$(pwd); while [ ! -f "$d/${MCP_HEADERS_HELPER_REL}" ] && [ "$d" != / ]; do d=$(dirname "$d"); done; [ -f "$d/${MCP_HEADERS_HELPER_REL}" ] || { echo "halfcycle: ${MCP_HEADERS_HELPER_REL} not found at or above $(pwd); re-run npx halfcycle" >&2; exit 1; }; exec /bin/sh "$d/${MCP_HEADERS_HELPER_REL}"'`;
|
|
1475
|
+
var GENERATED_MCP_HEADERS_HEADER = "# Halfcycle MCP connection headers \u2014 generated by";
|
|
1476
|
+
var OWNED_GENERATED_HEADERS = {
|
|
1477
|
+
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
1478
|
+
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
1479
|
+
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
1480
|
+
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER,
|
|
1481
|
+
".halfcycle/mcp-headers.sh": GENERATED_MCP_HEADERS_HEADER
|
|
1482
|
+
};
|
|
1007
1483
|
function generateMcpHeadersHelper() {
|
|
1008
1484
|
return `#!/bin/sh
|
|
1009
|
-
|
|
1485
|
+
${GENERATED_MCP_HEADERS_HEADER} the Halfcycle installer.
|
|
1010
1486
|
#
|
|
1011
1487
|
# Claude Code runs this at MCP connection time, from the session's cwd, and merges
|
|
1012
1488
|
# its stdout into the connection headers for the "halfcycle" server in .mcp.json.
|
|
@@ -1191,7 +1667,7 @@ function writeCrewRoster(targetRepoRoot, report) {
|
|
|
1191
1667
|
const rendered = `${JSON.stringify(doc, null, 2)}
|
|
1192
1668
|
`;
|
|
1193
1669
|
const crewPath = join5(targetRepoRoot, ".halfcycle", "crew.json");
|
|
1194
|
-
|
|
1670
|
+
recordOwned(report, crewPath, targetRepoRoot, rendered, ".halfcycle/crew.json");
|
|
1195
1671
|
}
|
|
1196
1672
|
function readBundlePin(targetRepoRoot) {
|
|
1197
1673
|
const pinPath = join5(targetRepoRoot, ".halfcycle", "bundle.json");
|
|
@@ -1305,7 +1781,8 @@ async function install(options) {
|
|
|
1305
1781
|
writtenPaths: [],
|
|
1306
1782
|
skippedPaths: [],
|
|
1307
1783
|
mergedPaths: [],
|
|
1308
|
-
collidedPaths: []
|
|
1784
|
+
collidedPaths: [],
|
|
1785
|
+
replacedPaths: []
|
|
1309
1786
|
};
|
|
1310
1787
|
copyManifestCommands(manifest, targetRepo, report);
|
|
1311
1788
|
const capturedDir = join5(targetRepo, "test", "fixtures", "captured");
|
|
@@ -1318,7 +1795,7 @@ async function install(options) {
|
|
|
1318
1795
|
}
|
|
1319
1796
|
const vendoredBinSrc = resolveVendoredBinary();
|
|
1320
1797
|
const vendoredBinDest = join5(targetRepo, ".halfcycle", "bin", "bin.bundle.mjs");
|
|
1321
|
-
|
|
1798
|
+
recordOwned(report, vendoredBinDest, targetRepo, readFileSync4(vendoredBinSrc, "utf-8"), ".halfcycle/bin/bin.bundle.mjs");
|
|
1322
1799
|
const settingsPath = join5(targetRepo, ".claude", "settings.json");
|
|
1323
1800
|
const settingsPreexisted = existsSync3(settingsPath);
|
|
1324
1801
|
const generatedSettings = JSON.parse(generateSettingsJson());
|
|
@@ -1330,17 +1807,19 @@ async function install(options) {
|
|
|
1330
1807
|
(settingsPreexisted ? report.mergedPaths : report.writtenPaths).push(".claude/settings.json");
|
|
1331
1808
|
for (const [name, content] of [
|
|
1332
1809
|
["guard-runner.sh", generateGuardRunnerWrapper()],
|
|
1333
|
-
["session-
|
|
1334
|
-
["user-prompt-reminder.sh", generateUserPromptReminderHook()]
|
|
1810
|
+
["session-end-marker.sh", generateSessionEndMarker()],
|
|
1811
|
+
["user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
1812
|
+
["session-start-marker.sh", generateSessionStartMarker()]
|
|
1335
1813
|
]) {
|
|
1814
|
+
const rel = `.claude/hooks/${name}`;
|
|
1336
1815
|
const hookPath = join5(targetRepo, ".claude", "hooks", name);
|
|
1337
|
-
|
|
1816
|
+
recordOwnedGenerated(report, hookPath, targetRepo, content, OWNED_GENERATED_HEADERS[rel], rel);
|
|
1338
1817
|
}
|
|
1339
1818
|
const ciStanzaPath = join5(targetRepo, ".halfcycle", "ci-stanza.yml");
|
|
1340
|
-
|
|
1819
|
+
recordOwned(report, ciStanzaPath, targetRepo, generateCiStanza(), ".halfcycle/ci-stanza.yml");
|
|
1341
1820
|
if (credential) {
|
|
1342
1821
|
const helperPath = join5(targetRepo, MCP_HEADERS_HELPER_REL);
|
|
1343
|
-
|
|
1822
|
+
recordOwnedGenerated(report, helperPath, targetRepo, generateMcpHeadersHelper(), OWNED_GENERATED_HEADERS[MCP_HEADERS_HELPER_REL], MCP_HEADERS_HELPER_REL);
|
|
1344
1823
|
const mcpPath = join5(targetRepo, MCP_REGISTRATION_REL);
|
|
1345
1824
|
const existingMcp = existsSync3(mcpPath) ? readFileSync4(mcpPath, "utf-8") : null;
|
|
1346
1825
|
const mcpContent = generateMcpRegistration(existingMcp, credential.mcpUrl);
|
|
@@ -1394,6 +1873,7 @@ async function install(options) {
|
|
|
1394
1873
|
skippedPaths: report.skippedPaths,
|
|
1395
1874
|
mergedPaths: report.mergedPaths,
|
|
1396
1875
|
collidedPaths: report.collidedPaths,
|
|
1876
|
+
replacedPaths: report.replacedPaths,
|
|
1397
1877
|
bootstrapScanRan: scanResult.ran,
|
|
1398
1878
|
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null
|
|
1399
1879
|
};
|
|
@@ -1474,6 +1954,12 @@ async function createEngagement(baseUrl, name, credential, derivedName) {
|
|
|
1474
1954
|
{ action: "Create-engagement", nothingHappened: "No engagement was created", route: "POST /engagements" }
|
|
1475
1955
|
);
|
|
1476
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
|
+
}
|
|
1477
1963
|
async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
1478
1964
|
const bearer = credential?.trim();
|
|
1479
1965
|
let res;
|
|
@@ -1498,7 +1984,9 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
1498
1984
|
const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
|
|
1499
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);
|
|
1500
1986
|
}
|
|
1501
|
-
const
|
|
1987
|
+
const candidate = withDeclaredTelemetryKey(await res.json().catch(() => null));
|
|
1988
|
+
const declared = engagementCredentialResponseSchema.safeParse(candidate);
|
|
1989
|
+
const body = declared.success ? declared.data : candidate;
|
|
1502
1990
|
if (!body || typeof body.engagementId !== "string" || typeof body.sessionToken !== "string") {
|
|
1503
1991
|
throw new Error(`[bundle install] ${shape.action} response from ${url} did not carry {engagementId, sessionToken}. ${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
|
|
1504
1992
|
}
|
|
@@ -1517,6 +2005,16 @@ async function requestEngagementValues(url, requestBody, credential, shape) {
|
|
|
1517
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.`);
|
|
1518
2006
|
}
|
|
1519
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
|
+
}
|
|
1520
2018
|
return {
|
|
1521
2019
|
engagementId: body.engagementId,
|
|
1522
2020
|
sessionToken: body.sessionToken,
|
|
@@ -1562,7 +2060,7 @@ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync4, readFile
|
|
|
1562
2060
|
import { dirname as dirname3, join as join6, posix } from "node:path";
|
|
1563
2061
|
var DECLARED_ROOT_FILES = ["CLAUDE.md", "AGENTS.md", "ENGAGEMENT.md"];
|
|
1564
2062
|
var DECLARED_DIR_PREFIX = "docs";
|
|
1565
|
-
var
|
|
2063
|
+
var RESERVED_METHOD_PATH = "docs/method";
|
|
1566
2064
|
var STEP_ID_GRAMMAR = /\b(?:l[0-5](?:-l[0-5])?|xl)\.[a-z0-9-]+/i;
|
|
1567
2065
|
var STEP_ANNOTATION = /<!--\s*step:/i;
|
|
1568
2066
|
var ANCHOR_DECLARATION = /<!--\s*anchor:/i;
|
|
@@ -1589,8 +2087,8 @@ function assertWritable(relPath) {
|
|
|
1589
2087
|
if (path.split("/").includes("..")) {
|
|
1590
2088
|
throw new Error(`[halfcycle setup] Refusing to write "${relPath}": a path may not climb out of the repository.`);
|
|
1591
2089
|
}
|
|
1592
|
-
if (isUnderIgnoringCase(foldWin32Canonicalisation(path),
|
|
1593
|
-
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.`);
|
|
1594
2092
|
}
|
|
1595
2093
|
const declared = DECLARED_ROOT_FILES.includes(path) || isUnder(path, DECLARED_DIR_PREFIX);
|
|
1596
2094
|
if (!declared) {
|
|
@@ -1600,7 +2098,7 @@ function assertWritable(relPath) {
|
|
|
1600
2098
|
function assertContentCarriesNoMethodStructure(relPath, content) {
|
|
1601
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";
|
|
1602
2100
|
if (carried) {
|
|
1603
|
-
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.`);
|
|
1604
2102
|
}
|
|
1605
2103
|
}
|
|
1606
2104
|
function assertUsableHeading(relPath, heading) {
|
|
@@ -2150,7 +2648,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
2150
2648
|
"",
|
|
2151
2649
|
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
2152
2650
|
"",
|
|
2153
|
-
"> 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.",
|
|
2154
2652
|
"",
|
|
2155
2653
|
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
2156
2654
|
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
@@ -2170,7 +2668,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
2170
2668
|
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
2171
2669
|
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
2172
2670
|
"",
|
|
2173
|
-
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.",
|
|
2174
2672
|
"",
|
|
2175
2673
|
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
2176
2674
|
"",
|
|
@@ -2182,7 +2680,7 @@ function renderBuildRecordMarkdown(record2) {
|
|
|
2182
2680
|
""
|
|
2183
2681
|
];
|
|
2184
2682
|
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
2185
|
-
lines.push(`**
|
|
2683
|
+
lines.push(`**Production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
2186
2684
|
}
|
|
2187
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"}`, "");
|
|
2188
2686
|
return lines.join("\n");
|