halfcycle 0.3.16 → 0.3.18
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 +203 -29
- package/commands/agent-teams-orchestration.md +3 -3
- package/commands/consistency-check.md +3 -3
- package/commands/context-update.md +3 -3
- package/commands/halfcycle-design.md +3 -3
- package/commands/halfcycle-setup.md +3 -3
- package/commands/stack-assembly-worker.md +3 -3
- package/commands/task-execution.md +3 -3
- package/commands/task-graph.md +3 -3
- package/commands/write-spec.md +3 -3
- package/dist/bin.js +403 -139
- package/dist/bin.js.map +3 -3
- package/dist/build-record/close-record.d.ts +124 -0
- package/dist/build-record/close-record.d.ts.map +1 -0
- package/dist/build-record/index.d.ts +8 -5
- package/dist/build-record/index.d.ts.map +1 -1
- package/dist/build-record/sources.d.ts +49 -18
- package/dist/build-record/sources.d.ts.map +1 -1
- package/dist/build-record/template.d.ts.map +1 -1
- package/dist/build-record/types.d.ts +49 -12
- package/dist/build-record/types.d.ts.map +1 -1
- package/dist/build-record/write.d.ts +60 -3
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/close-phase.d.ts +21 -2
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +58 -2
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.js +339 -120
- package/dist/index.js.map +3 -3
- package/dist/open-phase.d.ts +58 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -54,11 +54,26 @@ var firedGuardSchema = z2.object({
|
|
|
54
54
|
*/
|
|
55
55
|
path: z2.string().optional()
|
|
56
56
|
}).strict();
|
|
57
|
+
var notRunCheckSchema = z2.object({
|
|
58
|
+
/** Which check did not run, by the same reference a fired check carries. */
|
|
59
|
+
patternRef: z2.string(),
|
|
60
|
+
/**
|
|
61
|
+
* Why it could not run, in plain words — what this client did not send, or
|
|
62
|
+
* that the check's kind is not implemented yet. Never the check's own rule.
|
|
63
|
+
*/
|
|
64
|
+
reason: z2.string()
|
|
65
|
+
}).strict();
|
|
57
66
|
var resultEnvelopeSchema = z2.object({
|
|
58
67
|
guardsFired: z2.array(firedGuardSchema),
|
|
59
68
|
severity: severitySchema.nullable(),
|
|
60
69
|
blocking: z2.boolean(),
|
|
61
|
-
explanation: z2.string()
|
|
70
|
+
explanation: z2.string(),
|
|
71
|
+
/**
|
|
72
|
+
* The checks that could not run on this edit, each named with the reason.
|
|
73
|
+
* Absent when every check ran — so an evaluation where nothing was skipped
|
|
74
|
+
* looks exactly as it always has, and an absent list is never an empty one.
|
|
75
|
+
*/
|
|
76
|
+
notRun: z2.array(notRunCheckSchema).optional()
|
|
62
77
|
}).strict();
|
|
63
78
|
var wireErrorSchema = z2.object({
|
|
64
79
|
statusCode: z2.number(),
|
|
@@ -170,6 +185,65 @@ var CREW_ROSTER = [
|
|
|
170
185
|
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
171
186
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
172
187
|
|
|
188
|
+
// ../events/dist/phase-identity.js
|
|
189
|
+
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
190
|
+
function isValidPhaseIdentity(identity) {
|
|
191
|
+
if (typeof identity === "number")
|
|
192
|
+
return Number.isInteger(identity) && identity >= 0;
|
|
193
|
+
if (typeof identity !== "string")
|
|
194
|
+
return false;
|
|
195
|
+
if (identity === "")
|
|
196
|
+
return false;
|
|
197
|
+
if (identity.includes("/"))
|
|
198
|
+
return false;
|
|
199
|
+
if (identity.includes("\\"))
|
|
200
|
+
return false;
|
|
201
|
+
if (identity.includes("\0"))
|
|
202
|
+
return false;
|
|
203
|
+
if (identity.startsWith("."))
|
|
204
|
+
return false;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
var InvalidPhaseIdentityError = class extends Error {
|
|
208
|
+
/** The value that was refused, exactly as supplied. */
|
|
209
|
+
identity;
|
|
210
|
+
constructor(identity, allowedLocation) {
|
|
211
|
+
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
212
|
+
const where = allowedLocation === void 0 ? "" : `
|
|
213
|
+
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
214
|
+
super(`that phase identity cannot name ${subject}.
|
|
215
|
+
identity: ${describeIdentity(identity)}` + where);
|
|
216
|
+
this.name = "InvalidPhaseIdentityError";
|
|
217
|
+
this.identity = identity;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
221
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
222
|
+
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function renderPhaseSegment(identity) {
|
|
226
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
227
|
+
throw new InvalidPhaseIdentityError(identity);
|
|
228
|
+
}
|
|
229
|
+
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
230
|
+
}
|
|
231
|
+
function describeIdentity(identity) {
|
|
232
|
+
if (typeof identity === "string")
|
|
233
|
+
return identity;
|
|
234
|
+
if (identity === null)
|
|
235
|
+
return "null";
|
|
236
|
+
if (identity === void 0)
|
|
237
|
+
return "none supplied";
|
|
238
|
+
if (typeof identity === "number" || typeof identity === "boolean")
|
|
239
|
+
return String(identity);
|
|
240
|
+
if (Array.isArray(identity))
|
|
241
|
+
return "(a list)";
|
|
242
|
+
if (typeof identity === "object")
|
|
243
|
+
return "(an object)";
|
|
244
|
+
return `(a ${typeof identity})`;
|
|
245
|
+
}
|
|
246
|
+
|
|
173
247
|
// dist/engagement-credential.js
|
|
174
248
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
175
249
|
import { platform as platform2 } from "node:os";
|
|
@@ -233,33 +307,50 @@ function parseEnvText(raw) {
|
|
|
233
307
|
return out;
|
|
234
308
|
}
|
|
235
309
|
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
236
|
-
const desired = new Map(keys.map((k) =>
|
|
310
|
+
const desired = new Map(keys.map((k) => {
|
|
311
|
+
const value = values[k];
|
|
312
|
+
return [k, value === void 0 ? "" : value];
|
|
313
|
+
}));
|
|
237
314
|
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
315
|
+
const written = keys.filter((k) => desired.get(k) !== null);
|
|
238
316
|
if (existing === null) {
|
|
317
|
+
if (written.length === 0)
|
|
318
|
+
return "";
|
|
239
319
|
return `${ENV_HEADER}
|
|
240
|
-
` +
|
|
320
|
+
` + written.map(line).join("\n") + "\n";
|
|
241
321
|
}
|
|
242
322
|
const seen = /* @__PURE__ */ new Set();
|
|
243
|
-
const out =
|
|
323
|
+
const out = [];
|
|
324
|
+
for (const existingLine of existing.split("\n")) {
|
|
244
325
|
const eq = existingLine.indexOf("=");
|
|
245
|
-
if (eq === -1)
|
|
246
|
-
|
|
326
|
+
if (eq === -1) {
|
|
327
|
+
out.push(existingLine);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
247
330
|
const lhs = existingLine.slice(0, eq);
|
|
248
331
|
const exported = /^\s*export\s+/.exec(lhs);
|
|
249
332
|
const prefix = exported === null ? "" : exported[0];
|
|
250
333
|
const key = lhs.slice(prefix.length).trim();
|
|
251
|
-
if (desired.has(key)) {
|
|
252
|
-
|
|
253
|
-
|
|
334
|
+
if (!desired.has(key)) {
|
|
335
|
+
out.push(existingLine);
|
|
336
|
+
continue;
|
|
254
337
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
338
|
+
seen.add(key);
|
|
339
|
+
if (desired.get(key) === null)
|
|
340
|
+
continue;
|
|
341
|
+
out.push(`${prefix}${line(key)}`);
|
|
342
|
+
}
|
|
343
|
+
const missing = written.filter((k) => !seen.has(k));
|
|
258
344
|
if (missing.length > 0) {
|
|
345
|
+
const header = existing.includes(ENV_HEADER) ? "" : `${ENV_HEADER}
|
|
346
|
+
`;
|
|
347
|
+
const block = header + missing.map(line).join("\n");
|
|
259
348
|
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
349
|
+
if (trailingBlank)
|
|
350
|
+
out.splice(out.length - 1, 0, block);
|
|
351
|
+
else
|
|
352
|
+
out.push(`
|
|
353
|
+
${block}`);
|
|
263
354
|
}
|
|
264
355
|
let result = out.join("\n");
|
|
265
356
|
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
@@ -273,17 +364,19 @@ function readEngagementEnv(engagementId, home) {
|
|
|
273
364
|
return null;
|
|
274
365
|
}
|
|
275
366
|
}
|
|
276
|
-
function writeEngagementEnv(engagementId, values, home) {
|
|
367
|
+
function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KEYS) {
|
|
277
368
|
const path = engagementEnvPath(engagementId, home);
|
|
278
369
|
const dir = engagementStateDir(engagementId, home);
|
|
279
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
280
370
|
let existing;
|
|
281
371
|
try {
|
|
282
372
|
existing = readFileSync(path, "utf-8");
|
|
283
373
|
} catch {
|
|
284
374
|
existing = null;
|
|
285
375
|
}
|
|
286
|
-
const reconciled = reconcileEnvText(existing, values);
|
|
376
|
+
const reconciled = reconcileEnvText(existing, values, keys);
|
|
377
|
+
if (existing === null && reconciled === "")
|
|
378
|
+
return "skipped";
|
|
379
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
287
380
|
const outcome = existing === reconciled ? "skipped" : "written";
|
|
288
381
|
if (outcome === "written") {
|
|
289
382
|
writeFileSync(path, reconciled, { mode: 384 });
|
|
@@ -1877,8 +1970,203 @@ function projectSeedGuard(fired, includeExplanation) {
|
|
|
1877
1970
|
// dist/build-record/sources.js
|
|
1878
1971
|
import { readFileSync as readFileSync7, existsSync as existsSync6 } from "node:fs";
|
|
1879
1972
|
import { createHash } from "node:crypto";
|
|
1880
|
-
import { join as
|
|
1973
|
+
import { join as join10 } from "node:path";
|
|
1974
|
+
|
|
1975
|
+
// dist/build-record/close-record.js
|
|
1881
1976
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1977
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1978
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
1979
|
+
var CLOSE_RECORD_FORMAT = "halfcycle-phase-close/v1";
|
|
1980
|
+
var BUILD_RECORD_ZONE_B_DIR = join8(ZONE_B_DIR, "build-record");
|
|
1981
|
+
var InvalidCloseRecordError = class extends Error {
|
|
1982
|
+
constructor(path, detail) {
|
|
1983
|
+
super(`[build-record] the phase close record at ${path} cannot be read \u2014 ${detail}`);
|
|
1984
|
+
this.name = "InvalidCloseRecordError";
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
function closeRecordPath(repoRoot, phase) {
|
|
1988
|
+
assertValidPhaseIdentity(phase, BUILD_RECORD_ZONE_B_DIR);
|
|
1989
|
+
return join8(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phase)}.close.json`);
|
|
1990
|
+
}
|
|
1991
|
+
function resolveCloseAtHead(repoRoot) {
|
|
1992
|
+
try {
|
|
1993
|
+
const closeCommit = execFileSync2("git", ["-C", repoRoot, "rev-parse", "--short", "HEAD"], {
|
|
1994
|
+
encoding: "utf-8",
|
|
1995
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1996
|
+
}).trim();
|
|
1997
|
+
const closedDate = execFileSync2("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], {
|
|
1998
|
+
encoding: "utf-8",
|
|
1999
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2000
|
+
}).trim();
|
|
2001
|
+
if (closeCommit === "" || closedDate === "")
|
|
2002
|
+
return null;
|
|
2003
|
+
return { closeCommit, closedDate };
|
|
2004
|
+
} catch {
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function writeCloseRecord(repoRoot, phase) {
|
|
2009
|
+
let path;
|
|
2010
|
+
try {
|
|
2011
|
+
path = closeRecordPath(repoRoot, phase);
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
return { recorded: false, path: null, reason: err instanceof Error ? err.message : String(err) };
|
|
2014
|
+
}
|
|
2015
|
+
const at = resolveCloseAtHead(repoRoot);
|
|
2016
|
+
if (at === null) {
|
|
2017
|
+
return {
|
|
2018
|
+
recorded: false,
|
|
2019
|
+
path,
|
|
2020
|
+
reason: `the commit could not be read from ${repoRoot} \u2014 there is no git available, no repository there, or no commit in it yet`
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
const record2 = {
|
|
2024
|
+
format: CLOSE_RECORD_FORMAT,
|
|
2025
|
+
phase,
|
|
2026
|
+
closeCommit: at.closeCommit,
|
|
2027
|
+
closedDate: at.closedDate
|
|
2028
|
+
};
|
|
2029
|
+
try {
|
|
2030
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
2031
|
+
writeFileSync5(path, JSON.stringify(record2, null, 2) + "\n", "utf-8");
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
return { recorded: false, path, reason: err instanceof Error ? err.message : String(err) };
|
|
2034
|
+
}
|
|
2035
|
+
return { recorded: true, path, closeCommit: at.closeCommit, closedDate: at.closedDate };
|
|
2036
|
+
}
|
|
2037
|
+
function parseCloseRecord(path, parsed) {
|
|
2038
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2039
|
+
throw new InvalidCloseRecordError(path, "it does not hold a record");
|
|
2040
|
+
}
|
|
2041
|
+
const record2 = parsed;
|
|
2042
|
+
if (record2.format !== CLOSE_RECORD_FORMAT) {
|
|
2043
|
+
throw new InvalidCloseRecordError(path, `it is not a ${CLOSE_RECORD_FORMAT} record (it says ${JSON.stringify(record2.format)})`);
|
|
2044
|
+
}
|
|
2045
|
+
const commit = record2.closeCommit;
|
|
2046
|
+
const date = record2.closedDate;
|
|
2047
|
+
const missing = [];
|
|
2048
|
+
if (typeof commit !== "string" || commit.trim() === "")
|
|
2049
|
+
missing.push("closeCommit");
|
|
2050
|
+
if (typeof date !== "string" || date.trim() === "")
|
|
2051
|
+
missing.push("closedDate");
|
|
2052
|
+
if (missing.length > 0) {
|
|
2053
|
+
throw new InvalidCloseRecordError(path, `${missing.join(" and ")} ${missing.length === 1 ? "is" : "are"} not recorded in it. A close records the commit and its date together or records neither, so a file carrying one of them was edited by hand.`);
|
|
2054
|
+
}
|
|
2055
|
+
if (typeof record2.phase !== "string" && typeof record2.phase !== "number") {
|
|
2056
|
+
throw new InvalidCloseRecordError(path, "it does not say which phase closed");
|
|
2057
|
+
}
|
|
2058
|
+
return {
|
|
2059
|
+
format: CLOSE_RECORD_FORMAT,
|
|
2060
|
+
phase: record2.phase,
|
|
2061
|
+
closeCommit: commit,
|
|
2062
|
+
closedDate: date
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// dist/build-record/write.js
|
|
2067
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2068
|
+
import { basename, join as join9, resolve, relative as relative2, isAbsolute } from "node:path";
|
|
2069
|
+
|
|
2070
|
+
// dist/build-record/template.js
|
|
2071
|
+
function severityLabel(seed) {
|
|
2072
|
+
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
2073
|
+
}
|
|
2074
|
+
var NOT_RECORDED = "not recorded";
|
|
2075
|
+
function renderCloseLine(record2) {
|
|
2076
|
+
const status = `**Status:** ${record2.status.toUpperCase()}`;
|
|
2077
|
+
const closed = `**Closed:** ${record2.closedDate ?? NOT_RECORDED}`;
|
|
2078
|
+
const commit = record2.closeCommit === null ? `**Close commit:** ${NOT_RECORDED}` : `**Close commit:** \`${record2.closeCommit}\``;
|
|
2079
|
+
return `${status} \xB7 ${closed} \xB7 ${commit}`;
|
|
2080
|
+
}
|
|
2081
|
+
function renderCloseProvenance(record2) {
|
|
2082
|
+
return record2.closeCommit === null && record2.closedDate === null ? "*The close date and the commit were not recorded when this phase closed, so this record does not name them. Nothing has been substituted for them.*" : "*The close date and the commit are the client's own assertion about their own repository, written down when the phase was closed. Halfcycle did not observe them and does not check them.*";
|
|
2083
|
+
}
|
|
2084
|
+
function renderDoneWhenRows(record2) {
|
|
2085
|
+
const rows = [];
|
|
2086
|
+
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
2087
|
+
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
2088
|
+
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
2089
|
+
}
|
|
2090
|
+
return rows.join("\n");
|
|
2091
|
+
}
|
|
2092
|
+
function renderBuildRecordMarkdown(record2) {
|
|
2093
|
+
const { phase } = record2;
|
|
2094
|
+
const companion = `${renderPhaseSegment(phase.id)}.json`;
|
|
2095
|
+
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
2096
|
+
const lines = [
|
|
2097
|
+
// Fixed prose (vault-canonical template) + projected title slots.
|
|
2098
|
+
`# Build Record \u2014 Phase ${phase.id}: ${phase.name}`,
|
|
2099
|
+
"",
|
|
2100
|
+
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
2101
|
+
"",
|
|
2102
|
+
"> This Build Record was assembled automatically at phase close by the method bundle (`packages/bundle`). Every data field below is projected from the companion JSON \u2014 no record content is hand-authored.",
|
|
2103
|
+
"",
|
|
2104
|
+
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
2105
|
+
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
2106
|
+
renderCloseLine(record2),
|
|
2107
|
+
`**Companion:** [\`${companion}\`](${companion})`,
|
|
2108
|
+
"",
|
|
2109
|
+
renderCloseProvenance(record2),
|
|
2110
|
+
"",
|
|
2111
|
+
"## What this phase proved",
|
|
2112
|
+
"",
|
|
2113
|
+
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
2114
|
+
"",
|
|
2115
|
+
"## Delivered",
|
|
2116
|
+
"",
|
|
2117
|
+
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
2118
|
+
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
2119
|
+
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
2120
|
+
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
2121
|
+
"",
|
|
2122
|
+
guardsList ? `Guards evaluated (INV-007-safe results): ${guardsList}. *(Guard \`channel\`/\`version\` are guard-asset metadata, excluded from the v1 record \u2014 INV-007/INV-001.)*` : "No guards fired during this phase.",
|
|
2123
|
+
"",
|
|
2124
|
+
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
2125
|
+
"",
|
|
2126
|
+
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
2127
|
+
"",
|
|
2128
|
+
"| Done-when | Result | Evidence |",
|
|
2129
|
+
"|---|---|---|",
|
|
2130
|
+
renderDoneWhenRows(record2),
|
|
2131
|
+
""
|
|
2132
|
+
];
|
|
2133
|
+
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
2134
|
+
lines.push(`**INV-002 production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
2135
|
+
}
|
|
2136
|
+
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"}`, "");
|
|
2137
|
+
return lines.join("\n");
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// dist/build-record/write.js
|
|
2141
|
+
var BUILD_RECORD_DIR = join9("docs", "build-records");
|
|
2142
|
+
function serialiseRecordJson(record2) {
|
|
2143
|
+
return JSON.stringify(record2, null, 2) + "\n";
|
|
2144
|
+
}
|
|
2145
|
+
function assertInsideOutDir(outDir, candidate, identity) {
|
|
2146
|
+
const rel = relative2(outDir, resolve(candidate));
|
|
2147
|
+
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute(rel) && rel === basename(rel);
|
|
2148
|
+
if (!isPlainFilename) {
|
|
2149
|
+
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
function writeBuildRecord(repoRoot, record2) {
|
|
2153
|
+
const outDir = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
2154
|
+
const identity = record2.phase.id;
|
|
2155
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
2156
|
+
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
2157
|
+
}
|
|
2158
|
+
const segment = renderPhaseSegment(identity);
|
|
2159
|
+
const jsonPath = join9(outDir, `${segment}.json`);
|
|
2160
|
+
const mdPath = join9(outDir, `${segment}.md`);
|
|
2161
|
+
assertInsideOutDir(outDir, jsonPath, identity);
|
|
2162
|
+
assertInsideOutDir(outDir, mdPath, identity);
|
|
2163
|
+
mkdirSync6(outDir, { recursive: true });
|
|
2164
|
+
writeFileSync6(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
2165
|
+
writeFileSync6(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
2166
|
+
return { jsonPath, mdPath };
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// dist/build-record/sources.js
|
|
1882
2170
|
var MissingSourceError = class extends Error {
|
|
1883
2171
|
constructor(sourceName, detail) {
|
|
1884
2172
|
super(`[build-record] missing required source: ${sourceName} \u2014 ${detail}`);
|
|
@@ -1934,7 +2222,7 @@ function syntheticRunId(record2) {
|
|
|
1934
2222
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
1935
2223
|
}
|
|
1936
2224
|
function readGuardEvalLog(logDir, phaseId) {
|
|
1937
|
-
const logPath =
|
|
2225
|
+
const logPath = join10(logDir, `${renderPhaseSegment(phaseId)}.jsonl`);
|
|
1938
2226
|
if (!existsSync6(logPath)) {
|
|
1939
2227
|
throw new MissingSourceError("guard-eval log", `no runner guard-eval log at ${logPath} for phase ${phaseId}`);
|
|
1940
2228
|
}
|
|
@@ -1950,7 +2238,7 @@ function readGuardEvalLog(logDir, phaseId) {
|
|
|
1950
2238
|
return runs;
|
|
1951
2239
|
}
|
|
1952
2240
|
function readOrchestrationState(phasesDir, phaseId) {
|
|
1953
|
-
const statePath =
|
|
2241
|
+
const statePath = join10(phasesDir, `${renderPhaseSegment(phaseId)}-orchestration-state.json`);
|
|
1954
2242
|
if (!existsSync6(statePath)) {
|
|
1955
2243
|
throw new MissingSourceError("orchestration state", `no orchestration-state file at ${statePath} for phase ${phaseId}`);
|
|
1956
2244
|
}
|
|
@@ -1967,22 +2255,21 @@ function readOrchestrationState(phasesDir, phaseId) {
|
|
|
1967
2255
|
}
|
|
1968
2256
|
return { tasks };
|
|
1969
2257
|
}
|
|
1970
|
-
function
|
|
1971
|
-
|
|
1972
|
-
|
|
2258
|
+
function readPhaseClose(repoRoot, phaseId) {
|
|
2259
|
+
const path = closeRecordPath(repoRoot, phaseId);
|
|
2260
|
+
if (!existsSync6(path))
|
|
2261
|
+
return { closeCommit: null, closedDate: null };
|
|
2262
|
+
let parsed;
|
|
1973
2263
|
try {
|
|
1974
|
-
|
|
1975
|
-
encoding: "utf-8"
|
|
1976
|
-
}).trim();
|
|
1977
|
-
closedDate = execFileSync2("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], { encoding: "utf-8" }).trim();
|
|
2264
|
+
parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
1978
2265
|
} catch (err) {
|
|
1979
|
-
throw new
|
|
2266
|
+
throw new InvalidCloseRecordError(path, `it is not readable JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
2267
|
+
}
|
|
2268
|
+
const record2 = parseCloseRecord(path, parsed);
|
|
2269
|
+
if (renderPhaseSegment(record2.phase) !== renderPhaseSegment(phaseId)) {
|
|
2270
|
+
throw new InvalidCloseRecordError(path, `it records the close of ${String(record2.phase)}, not of ${String(phaseId)}`);
|
|
1980
2271
|
}
|
|
1981
|
-
|
|
1982
|
-
throw new MissingSourceError("git HEAD", `empty commit sha in ${repoRoot}`);
|
|
1983
|
-
if (!closedDate)
|
|
1984
|
-
throw new MissingSourceError("git HEAD", `empty commit date in ${repoRoot}`);
|
|
1985
|
-
return { closeCommit, closedDate };
|
|
2272
|
+
return { closeCommit: record2.closeCommit, closedDate: record2.closedDate };
|
|
1986
2273
|
}
|
|
1987
2274
|
function validateNarrated(narrated) {
|
|
1988
2275
|
const req = (v, name) => {
|
|
@@ -1993,6 +2280,7 @@ function validateNarrated(narrated) {
|
|
|
1993
2280
|
req(narrated.engagement, "engagement");
|
|
1994
2281
|
req(narrated.engagementType, "engagementType");
|
|
1995
2282
|
req(narrated.phase?.name, "phase.name");
|
|
2283
|
+
assertValidPhaseIdentity(narrated.phase?.id, BUILD_RECORD_DIR);
|
|
1996
2284
|
req(narrated.status, "status");
|
|
1997
2285
|
req(narrated.thesis, "thesis");
|
|
1998
2286
|
req(narrated.delivered, "delivered");
|
|
@@ -2008,7 +2296,7 @@ function validateNarrated(narrated) {
|
|
|
2008
2296
|
function loadSources(opts) {
|
|
2009
2297
|
const orchestration = readOrchestrationState(opts.phasesDir, opts.phaseId);
|
|
2010
2298
|
const guardEvalRuns = readGuardEvalLog(opts.guardEvalLogDir, opts.phaseId);
|
|
2011
|
-
const
|
|
2299
|
+
const close = readPhaseClose(opts.repoRoot, opts.phaseId);
|
|
2012
2300
|
const narrated = validateNarrated(opts.narrated);
|
|
2013
2301
|
if (!Array.isArray(opts.touchedInvariants)) {
|
|
2014
2302
|
throw new MissingSourceError("feature-spec Touches", "touchedInvariants must be an array");
|
|
@@ -2016,7 +2304,7 @@ function loadSources(opts) {
|
|
|
2016
2304
|
return {
|
|
2017
2305
|
orchestration,
|
|
2018
2306
|
narrated,
|
|
2019
|
-
|
|
2307
|
+
close,
|
|
2020
2308
|
guardEvalRuns,
|
|
2021
2309
|
touchedInvariants: opts.touchedInvariants
|
|
2022
2310
|
};
|
|
@@ -2046,15 +2334,15 @@ function deriveInvariantsExercised(touched) {
|
|
|
2046
2334
|
return [...new Set(touched)].sort((a, b) => a.localeCompare(b));
|
|
2047
2335
|
}
|
|
2048
2336
|
function assembleBuildRecord(sources) {
|
|
2049
|
-
const { narrated,
|
|
2337
|
+
const { narrated, close, orchestration } = sources;
|
|
2050
2338
|
return {
|
|
2051
2339
|
format: BUILD_RECORD_FORMAT,
|
|
2052
2340
|
engagement: narrated.engagement,
|
|
2053
2341
|
engagementType: narrated.engagementType,
|
|
2054
2342
|
phase: { id: narrated.phase.id, name: narrated.phase.name },
|
|
2055
2343
|
status: narrated.status,
|
|
2056
|
-
closedDate:
|
|
2057
|
-
closeCommit:
|
|
2344
|
+
closedDate: close.closedDate,
|
|
2345
|
+
closeCommit: close.closeCommit,
|
|
2058
2346
|
thesis: narrated.thesis,
|
|
2059
2347
|
thesisHeld: deriveThesisHeld(narrated.acceptance),
|
|
2060
2348
|
delivered: narrated.delivered,
|
|
@@ -2068,85 +2356,6 @@ function assembleBuildRecord(sources) {
|
|
|
2068
2356
|
};
|
|
2069
2357
|
}
|
|
2070
2358
|
|
|
2071
|
-
// dist/build-record/template.js
|
|
2072
|
-
function severityLabel(seed) {
|
|
2073
|
-
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
2074
|
-
}
|
|
2075
|
-
function renderDoneWhenRows(record2) {
|
|
2076
|
-
const rows = [];
|
|
2077
|
-
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
2078
|
-
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
2079
|
-
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
2080
|
-
}
|
|
2081
|
-
return rows.join("\n");
|
|
2082
|
-
}
|
|
2083
|
-
function renderBuildRecordMarkdown(record2) {
|
|
2084
|
-
const { phase } = record2;
|
|
2085
|
-
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
2086
|
-
const lines = [
|
|
2087
|
-
// Fixed prose (vault-canonical template) + projected title slots.
|
|
2088
|
-
`# Build Record \u2014 Phase ${phase.id}: ${phase.name}`,
|
|
2089
|
-
"",
|
|
2090
|
-
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
2091
|
-
"",
|
|
2092
|
-
"> This Build Record was assembled automatically at phase close by the method bundle (`packages/bundle`). Every data field below is projected from the companion JSON \u2014 no record content is hand-authored.",
|
|
2093
|
-
"",
|
|
2094
|
-
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
2095
|
-
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
2096
|
-
`**Status:** ${record2.status.toUpperCase()} \xB7 **Closed:** ${record2.closedDate} \xB7 **Close commit:** \`${record2.closeCommit}\``,
|
|
2097
|
-
`**Companion:** [\`phase-${phase.id}.json\`](phase-${phase.id}.json)`,
|
|
2098
|
-
"",
|
|
2099
|
-
"## What this phase proved",
|
|
2100
|
-
"",
|
|
2101
|
-
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
2102
|
-
"",
|
|
2103
|
-
"## Delivered",
|
|
2104
|
-
"",
|
|
2105
|
-
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
2106
|
-
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
2107
|
-
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
2108
|
-
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
2109
|
-
"",
|
|
2110
|
-
guardsList ? `Guards evaluated (INV-007-safe results): ${guardsList}. *(Guard \`channel\`/\`version\` are guard-asset metadata, excluded from the v1 record \u2014 INV-007/INV-001.)*` : "No guards fired during this phase.",
|
|
2111
|
-
"",
|
|
2112
|
-
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
2113
|
-
"",
|
|
2114
|
-
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
2115
|
-
"",
|
|
2116
|
-
"| Done-when | Result | Evidence |",
|
|
2117
|
-
"|---|---|---|",
|
|
2118
|
-
renderDoneWhenRows(record2),
|
|
2119
|
-
""
|
|
2120
|
-
];
|
|
2121
|
-
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
2122
|
-
lines.push(`**INV-002 production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
2123
|
-
}
|
|
2124
|
-
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"}`, "");
|
|
2125
|
-
return lines.join("\n");
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
// dist/build-record/write.js
|
|
2129
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2130
|
-
import { join as join9, resolve, relative as relative2, isAbsolute } from "node:path";
|
|
2131
|
-
var BUILD_RECORD_DIR = join9("docs", "build-records");
|
|
2132
|
-
function serialiseRecordJson(record2) {
|
|
2133
|
-
return JSON.stringify(record2, null, 2) + "\n";
|
|
2134
|
-
}
|
|
2135
|
-
function writeBuildRecord(repoRoot, record2) {
|
|
2136
|
-
const outDir = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
2137
|
-
const expected = resolve(repoRoot, BUILD_RECORD_DIR);
|
|
2138
|
-
const rel = relative2(expected, outDir);
|
|
2139
|
-
if (rel !== "" || isAbsolute(rel)) {
|
|
2140
|
-
throw new Error(`[build-record] refusing to write outside ${BUILD_RECORD_DIR}`);
|
|
2141
|
-
}
|
|
2142
|
-
mkdirSync5(outDir, { recursive: true });
|
|
2143
|
-
const jsonPath = join9(outDir, `phase-${record2.phase.id}.json`);
|
|
2144
|
-
const mdPath = join9(outDir, `phase-${record2.phase.id}.md`);
|
|
2145
|
-
writeFileSync5(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
2146
|
-
writeFileSync5(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
2147
|
-
return { jsonPath, mdPath };
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
2359
|
// dist/build-record/index.js
|
|
2151
2360
|
function assemblePhaseBuildRecord(opts) {
|
|
2152
2361
|
const sources = loadSources({
|
|
@@ -2163,9 +2372,13 @@ function assemblePhaseBuildRecord(opts) {
|
|
|
2163
2372
|
export {
|
|
2164
2373
|
BUILD_RECORD_DIR,
|
|
2165
2374
|
BUILD_RECORD_FORMAT,
|
|
2375
|
+
BUILD_RECORD_ZONE_B_DIR,
|
|
2376
|
+
CLOSE_RECORD_FORMAT,
|
|
2166
2377
|
CreateEngagementRefused,
|
|
2167
2378
|
FLAT_FILE_TRACKER_HOME,
|
|
2168
2379
|
HALFCYCLE_STATE_FORMAT,
|
|
2380
|
+
InvalidCloseRecordError,
|
|
2381
|
+
InvalidPhaseIdentityError,
|
|
2169
2382
|
MissingSourceError,
|
|
2170
2383
|
SetupManifest,
|
|
2171
2384
|
ZONE_B_DIR,
|
|
@@ -2173,8 +2386,10 @@ export {
|
|
|
2173
2386
|
applySetupWrites,
|
|
2174
2387
|
assembleBuildRecord,
|
|
2175
2388
|
assemblePhaseBuildRecord,
|
|
2389
|
+
assertValidPhaseIdentity,
|
|
2176
2390
|
buildEngagementRecord,
|
|
2177
2391
|
checkDrift,
|
|
2392
|
+
closeRecordPath,
|
|
2178
2393
|
contextIndexRow,
|
|
2179
2394
|
createEngagement,
|
|
2180
2395
|
deriveInvariantsExercised,
|
|
@@ -2185,29 +2400,33 @@ export {
|
|
|
2185
2400
|
engagementRecordRow,
|
|
2186
2401
|
flatFileTrackerDefault,
|
|
2187
2402
|
install,
|
|
2403
|
+
isValidPhaseIdentity,
|
|
2188
2404
|
layerDocumentRow,
|
|
2189
2405
|
loadSources,
|
|
2190
2406
|
mergeSettings,
|
|
2191
2407
|
mergeWithRecords,
|
|
2192
2408
|
mintBoardEnterCode,
|
|
2193
2409
|
mintOrReadIdentity,
|
|
2410
|
+
parseCloseRecord,
|
|
2194
2411
|
projectSeedGuard,
|
|
2195
2412
|
projectSeedGuards,
|
|
2196
2413
|
questionsToPut,
|
|
2197
2414
|
readBundlePin,
|
|
2198
|
-
readGitClose,
|
|
2199
2415
|
readGuardEvalLog,
|
|
2200
2416
|
readOrchestrationState,
|
|
2417
|
+
readPhaseClose,
|
|
2201
2418
|
readRemote,
|
|
2202
2419
|
readRootCommit,
|
|
2203
2420
|
renderAnchor,
|
|
2204
2421
|
renderBuildRecordMarkdown,
|
|
2205
2422
|
renderDiscovery,
|
|
2423
|
+
renderPhaseSegment,
|
|
2206
2424
|
runBootstrapScan,
|
|
2207
2425
|
scanLayers,
|
|
2208
2426
|
serialiseRecordJson,
|
|
2209
2427
|
trackerHomeRow,
|
|
2210
2428
|
validateNarrated,
|
|
2211
|
-
writeBuildRecord
|
|
2429
|
+
writeBuildRecord,
|
|
2430
|
+
writeCloseRecord
|
|
2212
2431
|
};
|
|
2213
2432
|
//# sourceMappingURL=index.js.map
|