halfcycle 0.3.17 → 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/dist/bin.js +401 -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/bin.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// dist/bin.js
|
|
4
4
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5
5
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join11 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/install.js
|
|
9
9
|
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -61,11 +61,26 @@ var firedGuardSchema = z2.object({
|
|
|
61
61
|
*/
|
|
62
62
|
path: z2.string().optional()
|
|
63
63
|
}).strict();
|
|
64
|
+
var notRunCheckSchema = z2.object({
|
|
65
|
+
/** Which check did not run, by the same reference a fired check carries. */
|
|
66
|
+
patternRef: z2.string(),
|
|
67
|
+
/**
|
|
68
|
+
* Why it could not run, in plain words — what this client did not send, or
|
|
69
|
+
* that the check's kind is not implemented yet. Never the check's own rule.
|
|
70
|
+
*/
|
|
71
|
+
reason: z2.string()
|
|
72
|
+
}).strict();
|
|
64
73
|
var resultEnvelopeSchema = z2.object({
|
|
65
74
|
guardsFired: z2.array(firedGuardSchema),
|
|
66
75
|
severity: severitySchema.nullable(),
|
|
67
76
|
blocking: z2.boolean(),
|
|
68
|
-
explanation: z2.string()
|
|
77
|
+
explanation: z2.string(),
|
|
78
|
+
/**
|
|
79
|
+
* The checks that could not run on this edit, each named with the reason.
|
|
80
|
+
* Absent when every check ran — so an evaluation where nothing was skipped
|
|
81
|
+
* looks exactly as it always has, and an absent list is never an empty one.
|
|
82
|
+
*/
|
|
83
|
+
notRun: z2.array(notRunCheckSchema).optional()
|
|
69
84
|
}).strict();
|
|
70
85
|
var wireErrorSchema = z2.object({
|
|
71
86
|
statusCode: z2.number(),
|
|
@@ -436,6 +451,65 @@ function safeParseAccountIdentity(payload) {
|
|
|
436
451
|
return accountIdentitySchema.safeParse(payload);
|
|
437
452
|
}
|
|
438
453
|
|
|
454
|
+
// ../events/dist/phase-identity.js
|
|
455
|
+
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
456
|
+
function isValidPhaseIdentity(identity) {
|
|
457
|
+
if (typeof identity === "number")
|
|
458
|
+
return Number.isInteger(identity) && identity >= 0;
|
|
459
|
+
if (typeof identity !== "string")
|
|
460
|
+
return false;
|
|
461
|
+
if (identity === "")
|
|
462
|
+
return false;
|
|
463
|
+
if (identity.includes("/"))
|
|
464
|
+
return false;
|
|
465
|
+
if (identity.includes("\\"))
|
|
466
|
+
return false;
|
|
467
|
+
if (identity.includes("\0"))
|
|
468
|
+
return false;
|
|
469
|
+
if (identity.startsWith("."))
|
|
470
|
+
return false;
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
var InvalidPhaseIdentityError = class extends Error {
|
|
474
|
+
/** The value that was refused, exactly as supplied. */
|
|
475
|
+
identity;
|
|
476
|
+
constructor(identity, allowedLocation) {
|
|
477
|
+
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
478
|
+
const where = allowedLocation === void 0 ? "" : `
|
|
479
|
+
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
480
|
+
super(`that phase identity cannot name ${subject}.
|
|
481
|
+
identity: ${describeIdentity(identity)}` + where);
|
|
482
|
+
this.name = "InvalidPhaseIdentityError";
|
|
483
|
+
this.identity = identity;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
487
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
488
|
+
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function renderPhaseSegment(identity) {
|
|
492
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
493
|
+
throw new InvalidPhaseIdentityError(identity);
|
|
494
|
+
}
|
|
495
|
+
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
496
|
+
}
|
|
497
|
+
function describeIdentity(identity) {
|
|
498
|
+
if (typeof identity === "string")
|
|
499
|
+
return identity;
|
|
500
|
+
if (identity === null)
|
|
501
|
+
return "null";
|
|
502
|
+
if (identity === void 0)
|
|
503
|
+
return "none supplied";
|
|
504
|
+
if (typeof identity === "number" || typeof identity === "boolean")
|
|
505
|
+
return String(identity);
|
|
506
|
+
if (Array.isArray(identity))
|
|
507
|
+
return "(a list)";
|
|
508
|
+
if (typeof identity === "object")
|
|
509
|
+
return "(an object)";
|
|
510
|
+
return `(a ${typeof identity})`;
|
|
511
|
+
}
|
|
512
|
+
|
|
439
513
|
// dist/engagement-credential.js
|
|
440
514
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
441
515
|
import { platform as platform2 } from "node:os";
|
|
@@ -535,6 +609,7 @@ var ENGAGEMENT_ENV_KEYS = [
|
|
|
535
609
|
...GUARD_ENV_KEYS,
|
|
536
610
|
"CONTROL_TELEMETRY_URL"
|
|
537
611
|
];
|
|
612
|
+
var PHASE_STAMP_ENV_KEY = "HALFCYCLE_PHASE";
|
|
538
613
|
var ENV_HEADER = "# Halfcycle per-engagement credential \u2014 machine level, owner-only, never in a repository.";
|
|
539
614
|
function shq(value) {
|
|
540
615
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -562,33 +637,50 @@ function parseEnvText(raw) {
|
|
|
562
637
|
return out;
|
|
563
638
|
}
|
|
564
639
|
function reconcileEnvText(existing, values, keys = ENGAGEMENT_ENV_KEYS) {
|
|
565
|
-
const desired = new Map(keys.map((k) =>
|
|
640
|
+
const desired = new Map(keys.map((k) => {
|
|
641
|
+
const value = values[k];
|
|
642
|
+
return [k, value === void 0 ? "" : value];
|
|
643
|
+
}));
|
|
566
644
|
const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
|
|
645
|
+
const written = keys.filter((k) => desired.get(k) !== null);
|
|
567
646
|
if (existing === null) {
|
|
647
|
+
if (written.length === 0)
|
|
648
|
+
return "";
|
|
568
649
|
return `${ENV_HEADER}
|
|
569
|
-
` +
|
|
650
|
+
` + written.map(line).join("\n") + "\n";
|
|
570
651
|
}
|
|
571
652
|
const seen = /* @__PURE__ */ new Set();
|
|
572
|
-
const out =
|
|
653
|
+
const out = [];
|
|
654
|
+
for (const existingLine of existing.split("\n")) {
|
|
573
655
|
const eq = existingLine.indexOf("=");
|
|
574
|
-
if (eq === -1)
|
|
575
|
-
|
|
656
|
+
if (eq === -1) {
|
|
657
|
+
out.push(existingLine);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
576
660
|
const lhs = existingLine.slice(0, eq);
|
|
577
661
|
const exported = /^\s*export\s+/.exec(lhs);
|
|
578
662
|
const prefix = exported === null ? "" : exported[0];
|
|
579
663
|
const key = lhs.slice(prefix.length).trim();
|
|
580
|
-
if (desired.has(key)) {
|
|
581
|
-
|
|
582
|
-
|
|
664
|
+
if (!desired.has(key)) {
|
|
665
|
+
out.push(existingLine);
|
|
666
|
+
continue;
|
|
583
667
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
668
|
+
seen.add(key);
|
|
669
|
+
if (desired.get(key) === null)
|
|
670
|
+
continue;
|
|
671
|
+
out.push(`${prefix}${line(key)}`);
|
|
672
|
+
}
|
|
673
|
+
const missing = written.filter((k) => !seen.has(k));
|
|
587
674
|
if (missing.length > 0) {
|
|
675
|
+
const header = existing.includes(ENV_HEADER) ? "" : `${ENV_HEADER}
|
|
676
|
+
`;
|
|
677
|
+
const block = header + missing.map(line).join("\n");
|
|
588
678
|
const trailingBlank = out.length > 0 && out[out.length - 1] === "";
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
679
|
+
if (trailingBlank)
|
|
680
|
+
out.splice(out.length - 1, 0, block);
|
|
681
|
+
else
|
|
682
|
+
out.push(`
|
|
683
|
+
${block}`);
|
|
592
684
|
}
|
|
593
685
|
let result = out.join("\n");
|
|
594
686
|
if (existing.endsWith("\n") && !result.endsWith("\n"))
|
|
@@ -602,17 +694,19 @@ function readEngagementEnv(engagementId, home) {
|
|
|
602
694
|
return null;
|
|
603
695
|
}
|
|
604
696
|
}
|
|
605
|
-
function writeEngagementEnv(engagementId, values, home) {
|
|
697
|
+
function writeEngagementEnv(engagementId, values, home, keys = ENGAGEMENT_ENV_KEYS) {
|
|
606
698
|
const path = engagementEnvPath(engagementId, home);
|
|
607
699
|
const dir = engagementStateDir(engagementId, home);
|
|
608
|
-
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
609
700
|
let existing;
|
|
610
701
|
try {
|
|
611
702
|
existing = readFileSync2(path, "utf-8");
|
|
612
703
|
} catch {
|
|
613
704
|
existing = null;
|
|
614
705
|
}
|
|
615
|
-
const reconciled = reconcileEnvText(existing, values);
|
|
706
|
+
const reconciled = reconcileEnvText(existing, values, keys);
|
|
707
|
+
if (existing === null && reconciled === "")
|
|
708
|
+
return "skipped";
|
|
709
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
616
710
|
const outcome = existing === reconciled ? "skipped" : "written";
|
|
617
711
|
if (outcome === "written") {
|
|
618
712
|
writeFileSync2(path, reconciled, { mode: 384 });
|
|
@@ -2816,8 +2910,203 @@ function projectSeedGuard(fired, includeExplanation) {
|
|
|
2816
2910
|
// dist/build-record/sources.js
|
|
2817
2911
|
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "node:fs";
|
|
2818
2912
|
import { createHash } from "node:crypto";
|
|
2819
|
-
import { join as
|
|
2913
|
+
import { join as join8 } from "node:path";
|
|
2914
|
+
|
|
2915
|
+
// dist/build-record/close-record.js
|
|
2820
2916
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2917
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2918
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
2919
|
+
var CLOSE_RECORD_FORMAT = "halfcycle-phase-close/v1";
|
|
2920
|
+
var BUILD_RECORD_ZONE_B_DIR = join6(ZONE_B_DIR, "build-record");
|
|
2921
|
+
var InvalidCloseRecordError = class extends Error {
|
|
2922
|
+
constructor(path, detail) {
|
|
2923
|
+
super(`[build-record] the phase close record at ${path} cannot be read \u2014 ${detail}`);
|
|
2924
|
+
this.name = "InvalidCloseRecordError";
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
function closeRecordPath(repoRoot, phase) {
|
|
2928
|
+
assertValidPhaseIdentity(phase, BUILD_RECORD_ZONE_B_DIR);
|
|
2929
|
+
return join6(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phase)}.close.json`);
|
|
2930
|
+
}
|
|
2931
|
+
function resolveCloseAtHead(repoRoot) {
|
|
2932
|
+
try {
|
|
2933
|
+
const closeCommit = execFileSync2("git", ["-C", repoRoot, "rev-parse", "--short", "HEAD"], {
|
|
2934
|
+
encoding: "utf-8",
|
|
2935
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2936
|
+
}).trim();
|
|
2937
|
+
const closedDate = execFileSync2("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], {
|
|
2938
|
+
encoding: "utf-8",
|
|
2939
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2940
|
+
}).trim();
|
|
2941
|
+
if (closeCommit === "" || closedDate === "")
|
|
2942
|
+
return null;
|
|
2943
|
+
return { closeCommit, closedDate };
|
|
2944
|
+
} catch {
|
|
2945
|
+
return null;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
function writeCloseRecord(repoRoot, phase) {
|
|
2949
|
+
let path;
|
|
2950
|
+
try {
|
|
2951
|
+
path = closeRecordPath(repoRoot, phase);
|
|
2952
|
+
} catch (err) {
|
|
2953
|
+
return { recorded: false, path: null, reason: err instanceof Error ? err.message : String(err) };
|
|
2954
|
+
}
|
|
2955
|
+
const at = resolveCloseAtHead(repoRoot);
|
|
2956
|
+
if (at === null) {
|
|
2957
|
+
return {
|
|
2958
|
+
recorded: false,
|
|
2959
|
+
path,
|
|
2960
|
+
reason: `the commit could not be read from ${repoRoot} \u2014 there is no git available, no repository there, or no commit in it yet`
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
const record2 = {
|
|
2964
|
+
format: CLOSE_RECORD_FORMAT,
|
|
2965
|
+
phase,
|
|
2966
|
+
closeCommit: at.closeCommit,
|
|
2967
|
+
closedDate: at.closedDate
|
|
2968
|
+
};
|
|
2969
|
+
try {
|
|
2970
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
2971
|
+
writeFileSync5(path, JSON.stringify(record2, null, 2) + "\n", "utf-8");
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
return { recorded: false, path, reason: err instanceof Error ? err.message : String(err) };
|
|
2974
|
+
}
|
|
2975
|
+
return { recorded: true, path, closeCommit: at.closeCommit, closedDate: at.closedDate };
|
|
2976
|
+
}
|
|
2977
|
+
function parseCloseRecord(path, parsed) {
|
|
2978
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2979
|
+
throw new InvalidCloseRecordError(path, "it does not hold a record");
|
|
2980
|
+
}
|
|
2981
|
+
const record2 = parsed;
|
|
2982
|
+
if (record2.format !== CLOSE_RECORD_FORMAT) {
|
|
2983
|
+
throw new InvalidCloseRecordError(path, `it is not a ${CLOSE_RECORD_FORMAT} record (it says ${JSON.stringify(record2.format)})`);
|
|
2984
|
+
}
|
|
2985
|
+
const commit = record2.closeCommit;
|
|
2986
|
+
const date = record2.closedDate;
|
|
2987
|
+
const missing = [];
|
|
2988
|
+
if (typeof commit !== "string" || commit.trim() === "")
|
|
2989
|
+
missing.push("closeCommit");
|
|
2990
|
+
if (typeof date !== "string" || date.trim() === "")
|
|
2991
|
+
missing.push("closedDate");
|
|
2992
|
+
if (missing.length > 0) {
|
|
2993
|
+
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.`);
|
|
2994
|
+
}
|
|
2995
|
+
if (typeof record2.phase !== "string" && typeof record2.phase !== "number") {
|
|
2996
|
+
throw new InvalidCloseRecordError(path, "it does not say which phase closed");
|
|
2997
|
+
}
|
|
2998
|
+
return {
|
|
2999
|
+
format: CLOSE_RECORD_FORMAT,
|
|
3000
|
+
phase: record2.phase,
|
|
3001
|
+
closeCommit: commit,
|
|
3002
|
+
closedDate: date
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// dist/build-record/write.js
|
|
3007
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3008
|
+
import { basename as basename2, join as join7, resolve as resolve2, relative as relative2, isAbsolute } from "node:path";
|
|
3009
|
+
|
|
3010
|
+
// dist/build-record/template.js
|
|
3011
|
+
function severityLabel(seed) {
|
|
3012
|
+
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
3013
|
+
}
|
|
3014
|
+
var NOT_RECORDED = "not recorded";
|
|
3015
|
+
function renderCloseLine(record2) {
|
|
3016
|
+
const status = `**Status:** ${record2.status.toUpperCase()}`;
|
|
3017
|
+
const closed = `**Closed:** ${record2.closedDate ?? NOT_RECORDED}`;
|
|
3018
|
+
const commit = record2.closeCommit === null ? `**Close commit:** ${NOT_RECORDED}` : `**Close commit:** \`${record2.closeCommit}\``;
|
|
3019
|
+
return `${status} \xB7 ${closed} \xB7 ${commit}`;
|
|
3020
|
+
}
|
|
3021
|
+
function renderCloseProvenance(record2) {
|
|
3022
|
+
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.*";
|
|
3023
|
+
}
|
|
3024
|
+
function renderDoneWhenRows(record2) {
|
|
3025
|
+
const rows = [];
|
|
3026
|
+
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
3027
|
+
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
3028
|
+
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
3029
|
+
}
|
|
3030
|
+
return rows.join("\n");
|
|
3031
|
+
}
|
|
3032
|
+
function renderBuildRecordMarkdown(record2) {
|
|
3033
|
+
const { phase } = record2;
|
|
3034
|
+
const companion = `${renderPhaseSegment(phase.id)}.json`;
|
|
3035
|
+
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
3036
|
+
const lines = [
|
|
3037
|
+
// Fixed prose (vault-canonical template) + projected title slots.
|
|
3038
|
+
`# Build Record \u2014 Phase ${phase.id}: ${phase.name}`,
|
|
3039
|
+
"",
|
|
3040
|
+
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
3041
|
+
"",
|
|
3042
|
+
"> 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.",
|
|
3043
|
+
"",
|
|
3044
|
+
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
3045
|
+
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
3046
|
+
renderCloseLine(record2),
|
|
3047
|
+
`**Companion:** [\`${companion}\`](${companion})`,
|
|
3048
|
+
"",
|
|
3049
|
+
renderCloseProvenance(record2),
|
|
3050
|
+
"",
|
|
3051
|
+
"## What this phase proved",
|
|
3052
|
+
"",
|
|
3053
|
+
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
3054
|
+
"",
|
|
3055
|
+
"## Delivered",
|
|
3056
|
+
"",
|
|
3057
|
+
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
3058
|
+
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
3059
|
+
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
3060
|
+
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
3061
|
+
"",
|
|
3062
|
+
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.",
|
|
3063
|
+
"",
|
|
3064
|
+
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
3065
|
+
"",
|
|
3066
|
+
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
3067
|
+
"",
|
|
3068
|
+
"| Done-when | Result | Evidence |",
|
|
3069
|
+
"|---|---|---|",
|
|
3070
|
+
renderDoneWhenRows(record2),
|
|
3071
|
+
""
|
|
3072
|
+
];
|
|
3073
|
+
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
3074
|
+
lines.push(`**INV-002 production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
3075
|
+
}
|
|
3076
|
+
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"}`, "");
|
|
3077
|
+
return lines.join("\n");
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// dist/build-record/write.js
|
|
3081
|
+
var BUILD_RECORD_DIR = join7("docs", "build-records");
|
|
3082
|
+
function serialiseRecordJson(record2) {
|
|
3083
|
+
return JSON.stringify(record2, null, 2) + "\n";
|
|
3084
|
+
}
|
|
3085
|
+
function assertInsideOutDir(outDir, candidate, identity) {
|
|
3086
|
+
const rel = relative2(outDir, resolve2(candidate));
|
|
3087
|
+
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute(rel) && rel === basename2(rel);
|
|
3088
|
+
if (!isPlainFilename) {
|
|
3089
|
+
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
function writeBuildRecord(repoRoot, record2) {
|
|
3093
|
+
const outDir = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
3094
|
+
const identity = record2.phase.id;
|
|
3095
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
3096
|
+
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
3097
|
+
}
|
|
3098
|
+
const segment = renderPhaseSegment(identity);
|
|
3099
|
+
const jsonPath = join7(outDir, `${segment}.json`);
|
|
3100
|
+
const mdPath = join7(outDir, `${segment}.md`);
|
|
3101
|
+
assertInsideOutDir(outDir, jsonPath, identity);
|
|
3102
|
+
assertInsideOutDir(outDir, mdPath, identity);
|
|
3103
|
+
mkdirSync6(outDir, { recursive: true });
|
|
3104
|
+
writeFileSync6(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
3105
|
+
writeFileSync6(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
3106
|
+
return { jsonPath, mdPath };
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// dist/build-record/sources.js
|
|
2821
3110
|
var MissingSourceError = class extends Error {
|
|
2822
3111
|
constructor(sourceName, detail) {
|
|
2823
3112
|
super(`[build-record] missing required source: ${sourceName} \u2014 ${detail}`);
|
|
@@ -2873,7 +3162,7 @@ function syntheticRunId(record2) {
|
|
|
2873
3162
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
2874
3163
|
}
|
|
2875
3164
|
function readGuardEvalLog(logDir, phaseId) {
|
|
2876
|
-
const logPath =
|
|
3165
|
+
const logPath = join8(logDir, `${renderPhaseSegment(phaseId)}.jsonl`);
|
|
2877
3166
|
if (!existsSync4(logPath)) {
|
|
2878
3167
|
throw new MissingSourceError("guard-eval log", `no runner guard-eval log at ${logPath} for phase ${phaseId}`);
|
|
2879
3168
|
}
|
|
@@ -2889,7 +3178,7 @@ function readGuardEvalLog(logDir, phaseId) {
|
|
|
2889
3178
|
return runs;
|
|
2890
3179
|
}
|
|
2891
3180
|
function readOrchestrationState(phasesDir, phaseId) {
|
|
2892
|
-
const statePath =
|
|
3181
|
+
const statePath = join8(phasesDir, `${renderPhaseSegment(phaseId)}-orchestration-state.json`);
|
|
2893
3182
|
if (!existsSync4(statePath)) {
|
|
2894
3183
|
throw new MissingSourceError("orchestration state", `no orchestration-state file at ${statePath} for phase ${phaseId}`);
|
|
2895
3184
|
}
|
|
@@ -2906,22 +3195,21 @@ function readOrchestrationState(phasesDir, phaseId) {
|
|
|
2906
3195
|
}
|
|
2907
3196
|
return { tasks };
|
|
2908
3197
|
}
|
|
2909
|
-
function
|
|
2910
|
-
|
|
2911
|
-
|
|
3198
|
+
function readPhaseClose(repoRoot, phaseId) {
|
|
3199
|
+
const path = closeRecordPath(repoRoot, phaseId);
|
|
3200
|
+
if (!existsSync4(path))
|
|
3201
|
+
return { closeCommit: null, closedDate: null };
|
|
3202
|
+
let parsed;
|
|
2912
3203
|
try {
|
|
2913
|
-
|
|
2914
|
-
encoding: "utf-8"
|
|
2915
|
-
}).trim();
|
|
2916
|
-
closedDate = execFileSync2("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], { encoding: "utf-8" }).trim();
|
|
3204
|
+
parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
2917
3205
|
} catch (err) {
|
|
2918
|
-
throw new
|
|
3206
|
+
throw new InvalidCloseRecordError(path, `it is not readable JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
3207
|
+
}
|
|
3208
|
+
const record2 = parseCloseRecord(path, parsed);
|
|
3209
|
+
if (renderPhaseSegment(record2.phase) !== renderPhaseSegment(phaseId)) {
|
|
3210
|
+
throw new InvalidCloseRecordError(path, `it records the close of ${String(record2.phase)}, not of ${String(phaseId)}`);
|
|
2919
3211
|
}
|
|
2920
|
-
|
|
2921
|
-
throw new MissingSourceError("git HEAD", `empty commit sha in ${repoRoot}`);
|
|
2922
|
-
if (!closedDate)
|
|
2923
|
-
throw new MissingSourceError("git HEAD", `empty commit date in ${repoRoot}`);
|
|
2924
|
-
return { closeCommit, closedDate };
|
|
3212
|
+
return { closeCommit: record2.closeCommit, closedDate: record2.closedDate };
|
|
2925
3213
|
}
|
|
2926
3214
|
function validateNarrated(narrated) {
|
|
2927
3215
|
const req = (v, name) => {
|
|
@@ -2932,6 +3220,7 @@ function validateNarrated(narrated) {
|
|
|
2932
3220
|
req(narrated.engagement, "engagement");
|
|
2933
3221
|
req(narrated.engagementType, "engagementType");
|
|
2934
3222
|
req(narrated.phase?.name, "phase.name");
|
|
3223
|
+
assertValidPhaseIdentity(narrated.phase?.id, BUILD_RECORD_DIR);
|
|
2935
3224
|
req(narrated.status, "status");
|
|
2936
3225
|
req(narrated.thesis, "thesis");
|
|
2937
3226
|
req(narrated.delivered, "delivered");
|
|
@@ -2947,7 +3236,7 @@ function validateNarrated(narrated) {
|
|
|
2947
3236
|
function loadSources(opts) {
|
|
2948
3237
|
const orchestration = readOrchestrationState(opts.phasesDir, opts.phaseId);
|
|
2949
3238
|
const guardEvalRuns = readGuardEvalLog(opts.guardEvalLogDir, opts.phaseId);
|
|
2950
|
-
const
|
|
3239
|
+
const close = readPhaseClose(opts.repoRoot, opts.phaseId);
|
|
2951
3240
|
const narrated = validateNarrated(opts.narrated);
|
|
2952
3241
|
if (!Array.isArray(opts.touchedInvariants)) {
|
|
2953
3242
|
throw new MissingSourceError("feature-spec Touches", "touchedInvariants must be an array");
|
|
@@ -2955,7 +3244,7 @@ function loadSources(opts) {
|
|
|
2955
3244
|
return {
|
|
2956
3245
|
orchestration,
|
|
2957
3246
|
narrated,
|
|
2958
|
-
|
|
3247
|
+
close,
|
|
2959
3248
|
guardEvalRuns,
|
|
2960
3249
|
touchedInvariants: opts.touchedInvariants
|
|
2961
3250
|
};
|
|
@@ -2985,15 +3274,15 @@ function deriveInvariantsExercised(touched) {
|
|
|
2985
3274
|
return [...new Set(touched)].sort((a, b) => a.localeCompare(b));
|
|
2986
3275
|
}
|
|
2987
3276
|
function assembleBuildRecord(sources) {
|
|
2988
|
-
const { narrated,
|
|
3277
|
+
const { narrated, close, orchestration } = sources;
|
|
2989
3278
|
return {
|
|
2990
3279
|
format: BUILD_RECORD_FORMAT,
|
|
2991
3280
|
engagement: narrated.engagement,
|
|
2992
3281
|
engagementType: narrated.engagementType,
|
|
2993
3282
|
phase: { id: narrated.phase.id, name: narrated.phase.name },
|
|
2994
3283
|
status: narrated.status,
|
|
2995
|
-
closedDate:
|
|
2996
|
-
closeCommit:
|
|
3284
|
+
closedDate: close.closedDate,
|
|
3285
|
+
closeCommit: close.closeCommit,
|
|
2997
3286
|
thesis: narrated.thesis,
|
|
2998
3287
|
thesisHeld: deriveThesisHeld(narrated.acceptance),
|
|
2999
3288
|
delivered: narrated.delivered,
|
|
@@ -3007,85 +3296,6 @@ function assembleBuildRecord(sources) {
|
|
|
3007
3296
|
};
|
|
3008
3297
|
}
|
|
3009
3298
|
|
|
3010
|
-
// dist/build-record/template.js
|
|
3011
|
-
function severityLabel(seed) {
|
|
3012
|
-
return `\`${seed.patternRef}\` (severity \`${seed.severity}\`)`;
|
|
3013
|
-
}
|
|
3014
|
-
function renderDoneWhenRows(record2) {
|
|
3015
|
-
const rows = [];
|
|
3016
|
-
for (const [key, dw] of Object.entries(record2.acceptance.doneWhen)) {
|
|
3017
|
-
const mark = dw.result === "pass" ? "\u2705" : dw.result === "dropped" ? "\u2014" : dw.result;
|
|
3018
|
-
rows.push(`| ${key} | ${mark} | ${dw.detail} |`);
|
|
3019
|
-
}
|
|
3020
|
-
return rows.join("\n");
|
|
3021
|
-
}
|
|
3022
|
-
function renderBuildRecordMarkdown(record2) {
|
|
3023
|
-
const { phase } = record2;
|
|
3024
|
-
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
3025
|
-
const lines = [
|
|
3026
|
-
// Fixed prose (vault-canonical template) + projected title slots.
|
|
3027
|
-
`# Build Record \u2014 Phase ${phase.id}: ${phase.name}`,
|
|
3028
|
-
"",
|
|
3029
|
-
`**Format:** \`${record2.format}\` (markdown + JSON pair; portable, readable without Halfcycle systems)`,
|
|
3030
|
-
"",
|
|
3031
|
-
"> 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.",
|
|
3032
|
-
"",
|
|
3033
|
-
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
3034
|
-
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
3035
|
-
`**Status:** ${record2.status.toUpperCase()} \xB7 **Closed:** ${record2.closedDate} \xB7 **Close commit:** \`${record2.closeCommit}\``,
|
|
3036
|
-
`**Companion:** [\`phase-${phase.id}.json\`](phase-${phase.id}.json)`,
|
|
3037
|
-
"",
|
|
3038
|
-
"## What this phase proved",
|
|
3039
|
-
"",
|
|
3040
|
-
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
3041
|
-
"",
|
|
3042
|
-
"## Delivered",
|
|
3043
|
-
"",
|
|
3044
|
-
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
3045
|
-
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
3046
|
-
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
3047
|
-
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
3048
|
-
"",
|
|
3049
|
-
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.",
|
|
3050
|
-
"",
|
|
3051
|
-
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
3052
|
-
"",
|
|
3053
|
-
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
3054
|
-
"",
|
|
3055
|
-
"| Done-when | Result | Evidence |",
|
|
3056
|
-
"|---|---|---|",
|
|
3057
|
-
renderDoneWhenRows(record2),
|
|
3058
|
-
""
|
|
3059
|
-
];
|
|
3060
|
-
if (record2.acceptance.inv002ProdBypassProbe) {
|
|
3061
|
-
lines.push(`**INV-002 production bypass probe:** ${record2.acceptance.inv002ProdBypassProbe.result} \u2014 ${record2.acceptance.inv002ProdBypassProbe.detail}`, "");
|
|
3062
|
-
}
|
|
3063
|
-
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"}`, "");
|
|
3064
|
-
return lines.join("\n");
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
// dist/build-record/write.js
|
|
3068
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3069
|
-
import { join as join7, resolve as resolve2, relative as relative2, isAbsolute } from "node:path";
|
|
3070
|
-
var BUILD_RECORD_DIR = join7("docs", "build-records");
|
|
3071
|
-
function serialiseRecordJson(record2) {
|
|
3072
|
-
return JSON.stringify(record2, null, 2) + "\n";
|
|
3073
|
-
}
|
|
3074
|
-
function writeBuildRecord(repoRoot, record2) {
|
|
3075
|
-
const outDir = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
3076
|
-
const expected = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
3077
|
-
const rel = relative2(expected, outDir);
|
|
3078
|
-
if (rel !== "" || isAbsolute(rel)) {
|
|
3079
|
-
throw new Error(`[build-record] refusing to write outside ${BUILD_RECORD_DIR}`);
|
|
3080
|
-
}
|
|
3081
|
-
mkdirSync5(outDir, { recursive: true });
|
|
3082
|
-
const jsonPath = join7(outDir, `phase-${record2.phase.id}.json`);
|
|
3083
|
-
const mdPath = join7(outDir, `phase-${record2.phase.id}.md`);
|
|
3084
|
-
writeFileSync5(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
3085
|
-
writeFileSync5(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
3086
|
-
return { jsonPath, mdPath };
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
3299
|
// dist/build-record/index.js
|
|
3090
3300
|
function assemblePhaseBuildRecord(opts) {
|
|
3091
3301
|
const sources = loadSources({
|
|
@@ -3102,7 +3312,7 @@ function assemblePhaseBuildRecord(opts) {
|
|
|
3102
3312
|
|
|
3103
3313
|
// dist/open-phase.js
|
|
3104
3314
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
3105
|
-
import { join as
|
|
3315
|
+
import { join as join9 } from "node:path";
|
|
3106
3316
|
var OpenPhaseRefused = class extends Error {
|
|
3107
3317
|
status;
|
|
3108
3318
|
finding;
|
|
@@ -3121,7 +3331,7 @@ var OPEN_PHASE_ENV_KEYS = [
|
|
|
3121
3331
|
function readRepoCredential(repoRoot, env = process.env, home) {
|
|
3122
3332
|
const pinned = readPinnedEngagementId(repoRoot);
|
|
3123
3333
|
const fromStore = pinned === null ? null : readEngagementEnv(pinned, home);
|
|
3124
|
-
const looked = pinned === null ? `${
|
|
3334
|
+
const looked = pinned === null ? `${join9(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
|
|
3125
3335
|
const values = {};
|
|
3126
3336
|
for (const key of OPEN_PHASE_ENV_KEYS) {
|
|
3127
3337
|
const value = (env[key] ?? fromStore?.[key] ?? "").trim();
|
|
@@ -3138,18 +3348,33 @@ function readRepoCredential(repoRoot, env = process.env, home) {
|
|
|
3138
3348
|
}
|
|
3139
3349
|
function readPinnedEngagementId(repoRoot) {
|
|
3140
3350
|
try {
|
|
3141
|
-
const pin = JSON.parse(readFileSync7(
|
|
3351
|
+
const pin = JSON.parse(readFileSync7(join9(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
|
|
3142
3352
|
const id = pin[PIN_ENGAGEMENT_ID_FIELD];
|
|
3143
3353
|
return typeof id === "string" && id !== "" ? id : null;
|
|
3144
3354
|
} catch {
|
|
3145
3355
|
return null;
|
|
3146
3356
|
}
|
|
3147
3357
|
}
|
|
3148
|
-
|
|
3358
|
+
function writePhaseStamp(engagementId, phase, home) {
|
|
3359
|
+
const path = engagementEnvPath(engagementId, home);
|
|
3360
|
+
if (phase !== null && !isValidPhaseIdentity(phase)) {
|
|
3361
|
+
return { path, reason: new InvalidPhaseIdentityError(phase).message };
|
|
3362
|
+
}
|
|
3363
|
+
try {
|
|
3364
|
+
writeEngagementEnv(engagementId, { [PHASE_STAMP_ENV_KEY]: phase }, home, [PHASE_STAMP_ENV_KEY]);
|
|
3365
|
+
return null;
|
|
3366
|
+
} catch (err) {
|
|
3367
|
+
return { path, reason: err instanceof Error ? err.message : String(err) };
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
async function openPhase(credential, phase, entry, home) {
|
|
3149
3371
|
const parsed = phaseEntryDecisionSchema.safeParse(entry);
|
|
3150
3372
|
if (!parsed.success) {
|
|
3151
3373
|
throw new Error(`[halfcycle] The entry decision is not a decision this platform accepts: ${parsed.error.issues.map((i) => i.message).join("; ")}.`);
|
|
3152
3374
|
}
|
|
3375
|
+
if (phase !== null && !isValidPhaseIdentity(phase)) {
|
|
3376
|
+
throw new InvalidPhaseIdentityError(phase);
|
|
3377
|
+
}
|
|
3153
3378
|
const url = `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phase`;
|
|
3154
3379
|
let res;
|
|
3155
3380
|
try {
|
|
@@ -3174,9 +3399,11 @@ async function openPhase(credential, phase, entry) {
|
|
|
3174
3399
|
if (!body || !(typeof body.phase === "string" || body.phase === null) || !(typeof body.previousPhase === "string" || body.previousPhase === null) || typeof body.decision !== "object" || body.decision === null || typeof body.decision.decisionId !== "string") {
|
|
3175
3400
|
throw new Error(`[halfcycle] The response from ${url} did not carry { phase, previousPhase, decision }. The phase may or may not have moved \u2014 read the engagement's decision history before retrying.`);
|
|
3176
3401
|
}
|
|
3402
|
+
const stampFailure = writePhaseStamp(credential.engagementId, body.phase, home);
|
|
3177
3403
|
return {
|
|
3178
3404
|
phase: body.phase,
|
|
3179
3405
|
previousPhase: body.previousPhase,
|
|
3406
|
+
stampFailure,
|
|
3180
3407
|
decision: body.decision
|
|
3181
3408
|
};
|
|
3182
3409
|
}
|
|
@@ -3194,7 +3421,7 @@ var ClosePhaseRefused = class extends Error {
|
|
|
3194
3421
|
this.name = "ClosePhaseRefused";
|
|
3195
3422
|
}
|
|
3196
3423
|
};
|
|
3197
|
-
async function closePhase(credential, phase, outcome, close) {
|
|
3424
|
+
async function closePhase(credential, phase, outcome, close, home, repoRoot) {
|
|
3198
3425
|
const parsed = acceptanceOutcomeSchema.safeParse(outcome);
|
|
3199
3426
|
if (!parsed.success) {
|
|
3200
3427
|
throw new Error(`[halfcycle] That is not an acceptance outcome this platform accepts: ${parsed.error.issues.map((i) => i.message).join("; ")}.`);
|
|
@@ -3228,7 +3455,13 @@ async function closePhase(credential, phase, outcome, close) {
|
|
|
3228
3455
|
if (!body || typeof body.engagementId !== "string" || typeof body.status !== "string") {
|
|
3229
3456
|
throw new Error(`[halfcycle] The response from ${url} did not carry { engagementId, status }. The phase may or may not have closed \u2014 read the engagement's status before retrying.`);
|
|
3230
3457
|
}
|
|
3231
|
-
|
|
3458
|
+
const closeRecord = repoRoot === void 0 ? {
|
|
3459
|
+
recorded: false,
|
|
3460
|
+
path: null,
|
|
3461
|
+
reason: "no repository was named for this close, so there was nowhere to read a commit from"
|
|
3462
|
+
} : writeCloseRecord(repoRoot, phase);
|
|
3463
|
+
const stampFailure = writePhaseStamp(credential.engagementId, null, home);
|
|
3464
|
+
return { engagementId: body.engagementId, status: body.status, stampFailure, closeRecord };
|
|
3232
3465
|
}
|
|
3233
3466
|
|
|
3234
3467
|
// dist/cli-contract.js
|
|
@@ -3349,7 +3582,7 @@ var PLACEHOLDERS = {
|
|
|
3349
3582
|
// dist/banner-facts.js
|
|
3350
3583
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3351
3584
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3352
|
-
import { basename as
|
|
3585
|
+
import { basename as basename3, join as join10, resolve as resolve3 } from "node:path";
|
|
3353
3586
|
var BRAND_URL = "halfcycle.ai";
|
|
3354
3587
|
var TAGLINE = [
|
|
3355
3588
|
"Fewer cycles.",
|
|
@@ -3362,7 +3595,7 @@ var TAGLINE_NARROW = [
|
|
|
3362
3595
|
];
|
|
3363
3596
|
function bundleVersion() {
|
|
3364
3597
|
try {
|
|
3365
|
-
const pkg = JSON.parse(readFileSync8(
|
|
3598
|
+
const pkg = JSON.parse(readFileSync8(join10(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
3366
3599
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
3367
3600
|
} catch {
|
|
3368
3601
|
return void 0;
|
|
@@ -3451,7 +3684,7 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
|
|
|
3451
3684
|
}
|
|
3452
3685
|
function safeBasename(dir) {
|
|
3453
3686
|
try {
|
|
3454
|
-
const name =
|
|
3687
|
+
const name = basename3(resolve3(dir)).trim();
|
|
3455
3688
|
return name === "" ? "project" : name;
|
|
3456
3689
|
} catch {
|
|
3457
3690
|
return "project";
|
|
@@ -3512,7 +3745,7 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
3512
3745
|
install into target (default: the current directory)
|
|
3513
3746
|
--yes / -y: use this machine's saved sign-in without being asked
|
|
3514
3747
|
halfcycle check-drift <target-repo>
|
|
3515
|
-
halfcycle build-record <phase
|
|
3748
|
+
halfcycle build-record <phase> [--repo <root>]
|
|
3516
3749
|
halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
|
|
3517
3750
|
halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
|
|
3518
3751
|
|
|
@@ -3521,7 +3754,7 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
3521
3754
|
`;
|
|
3522
3755
|
function isHalfcycleMonorepo(dir) {
|
|
3523
3756
|
try {
|
|
3524
|
-
const pkg = JSON.parse(readFileSync9(
|
|
3757
|
+
const pkg = JSON.parse(readFileSync9(join11(dir, "package.json"), "utf-8"));
|
|
3525
3758
|
return pkg.name === "halfcycle-monorepo";
|
|
3526
3759
|
} catch {
|
|
3527
3760
|
return false;
|
|
@@ -3755,30 +3988,30 @@ ${USAGE}`);
|
|
|
3755
3988
|
if (cmd === "build-record") {
|
|
3756
3989
|
const phaseArg = rest[0];
|
|
3757
3990
|
const repoRoot = rest.includes("--repo") ? rest[rest.indexOf("--repo") + 1] : process.cwd();
|
|
3758
|
-
if (!phaseArg
|
|
3759
|
-
process.stderr.write("halfcycle
|
|
3991
|
+
if (!phaseArg) {
|
|
3992
|
+
process.stderr.write("halfcycle: usage: halfcycle build-record <phase> [--repo <root>]\n");
|
|
3760
3993
|
process.exit(1);
|
|
3761
3994
|
return;
|
|
3762
3995
|
}
|
|
3763
|
-
const phaseId =
|
|
3996
|
+
const phaseId = phaseArg;
|
|
3764
3997
|
try {
|
|
3765
|
-
const inputPath =
|
|
3998
|
+
const inputPath = join11(repoRoot, BUILD_RECORD_ZONE_B_DIR, `${renderPhaseSegment(phaseId)}.input.json`);
|
|
3766
3999
|
const input = JSON.parse(readFileSync9(inputPath, "utf-8"));
|
|
3767
4000
|
const result = assemblePhaseBuildRecord({
|
|
3768
4001
|
repoRoot,
|
|
3769
|
-
phasesDir:
|
|
3770
|
-
guardEvalLogDir: input.guardEvalLogDir ??
|
|
4002
|
+
phasesDir: join11(repoRoot, "docs", "phases"),
|
|
4003
|
+
guardEvalLogDir: input.guardEvalLogDir ?? join11(repoRoot, ".workbench", "guard-eval-log"),
|
|
3771
4004
|
phaseId,
|
|
3772
4005
|
narrated: input.narrated,
|
|
3773
4006
|
touchedInvariants: input.touchedInvariants
|
|
3774
4007
|
});
|
|
3775
|
-
process.stdout.write(`[halfcycle
|
|
4008
|
+
process.stdout.write(`[halfcycle] Build Record written:
|
|
3776
4009
|
${result.jsonPath}
|
|
3777
4010
|
${result.mdPath}
|
|
3778
4011
|
`);
|
|
3779
4012
|
process.exit(0);
|
|
3780
4013
|
} catch (err) {
|
|
3781
|
-
process.stderr.write(`halfcycle
|
|
4014
|
+
process.stderr.write(`halfcycle build-record failed: ${err instanceof Error ? err.message : String(err)}
|
|
3782
4015
|
`);
|
|
3783
4016
|
process.exit(1);
|
|
3784
4017
|
}
|
|
@@ -3820,8 +4053,22 @@ ${USAGE}`);
|
|
|
3820
4053
|
process.stdout.write(`[halfcycle] This decision CROSSED: ${opened.decision.blockingFinding}
|
|
3821
4054
|
`);
|
|
3822
4055
|
}
|
|
4056
|
+
if (opened.stampFailure) {
|
|
4057
|
+
process.stderr.write(`halfcycle open-phase: the phase moved, but this machine's copy of it could not be written: ${opened.stampFailure.reason}
|
|
4058
|
+
halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard evaluations here are filed under whatever phase that file still names. Fix the file and re-run this command \u2014 re-running it is safe.
|
|
4059
|
+
`);
|
|
4060
|
+
process.exit(1);
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
3823
4063
|
process.exit(0);
|
|
3824
4064
|
} catch (err) {
|
|
4065
|
+
if (err instanceof InvalidPhaseIdentityError) {
|
|
4066
|
+
process.stderr.write(`halfcycle open-phase: ${err.message}
|
|
4067
|
+
halfcycle open-phase: a phase name has to work as a single folder name on this machine \u2014 that is what this machine files its guard evaluations under. Nothing was opened.
|
|
4068
|
+
`);
|
|
4069
|
+
process.exit(2);
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
3825
4072
|
if (err instanceof OpenPhaseRefused) {
|
|
3826
4073
|
process.stderr.write(`halfcycle open-phase: refused (${err.status}). ${err.message}
|
|
3827
4074
|
`);
|
|
@@ -3901,15 +4148,30 @@ ${USAGE}`);
|
|
|
3901
4148
|
verdict,
|
|
3902
4149
|
findings: findingsDetail.length,
|
|
3903
4150
|
findingsDetail
|
|
3904
|
-
}, closeDecision);
|
|
4151
|
+
}, closeDecision, void 0, repoRoot);
|
|
3905
4152
|
process.stdout.write(`[halfcycle] Phase "${phaseArg}" closed as ${verdict}${findingsDetail.length > 0 ? ` with ${findingsDetail.length} finding(s)` : ""} by ${closeDecision.actor}. The engagement is now ${closed.status}.
|
|
3906
4153
|
`);
|
|
3907
4154
|
if (closeDecision.decision === "override") {
|
|
3908
4155
|
process.stdout.write(`[halfcycle] This close was an OVERRIDE: ${closeDecision.reason}
|
|
4156
|
+
`);
|
|
4157
|
+
}
|
|
4158
|
+
if (closed.closeRecord.recorded) {
|
|
4159
|
+
process.stdout.write(`[halfcycle] Closed at commit ${closed.closeRecord.closeCommit} (${closed.closeRecord.closedDate}). The Build Record for this phase will name it.
|
|
4160
|
+
`);
|
|
4161
|
+
} else {
|
|
4162
|
+
process.stderr.write(`halfcycle close-phase: the phase closed, but the commit it closed at was not recorded on this machine: ${closed.closeRecord.reason}
|
|
4163
|
+
halfcycle close-phase: this phase's Build Record will say the close commit and the close date were not recorded.
|
|
3909
4164
|
`);
|
|
3910
4165
|
}
|
|
3911
4166
|
process.stdout.write(`[halfcycle] Open the next phase with: halfcycle open-phase <phase> --actor "you" --evidence "\u2026"
|
|
3912
4167
|
`);
|
|
4168
|
+
if (closed.stampFailure) {
|
|
4169
|
+
process.stderr.write(`halfcycle close-phase: the phase closed, but this machine's record of it could not be cleared: ${closed.stampFailure.reason}
|
|
4170
|
+
halfcycle close-phase: until ${closed.stampFailure.path} can be written, guard evaluations here are still filed under the phase you just closed.
|
|
4171
|
+
`);
|
|
4172
|
+
process.exit(1);
|
|
4173
|
+
return;
|
|
4174
|
+
}
|
|
3913
4175
|
process.exit(0);
|
|
3914
4176
|
} catch (err) {
|
|
3915
4177
|
if (err instanceof ClosePhaseRefused) {
|