halfcycle 0.3.31 → 0.3.33
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 +355 -65
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +1336 -873
- package/dist/bin.js.map +3 -3
- package/dist/build-record/index.d.ts +14 -36
- package/dist/build-record/index.d.ts.map +1 -1
- package/dist/build-record/obtain.d.ts +81 -0
- package/dist/build-record/obtain.d.ts.map +1 -0
- package/dist/build-record/render-v2.d.ts +47 -0
- package/dist/build-record/render-v2.d.ts.map +1 -0
- package/dist/build-record/write.d.ts +77 -60
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/close-phase.d.ts +59 -16
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/identity.d.ts +39 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +993 -671
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +60 -40
- package/dist/install.d.ts.map +1 -1
- package/package.json +3 -1
- package/dist/build-record/assemble.d.ts +0 -37
- package/dist/build-record/assemble.d.ts.map +0 -1
- package/dist/build-record/close-record.d.ts +0 -124
- package/dist/build-record/close-record.d.ts.map +0 -1
- package/dist/build-record/sources.d.ts +0 -111
- package/dist/build-record/sources.d.ts.map +0 -1
- package/dist/build-record/template.d.ts +0 -29
- package/dist/build-record/template.d.ts.map +0 -1
- package/dist/build-record/types.d.ts +0 -200
- package/dist/build-record/types.d.ts.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -3,18 +3,15 @@
|
|
|
3
3
|
// dist/bin.js
|
|
4
4
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
5
5
|
import { existsSync as existsSync5, readFileSync as readFileSync10, statSync } from "node:fs";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join10 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/install.js
|
|
9
9
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10
10
|
import { createHash as createHash2 } from "node:crypto";
|
|
11
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
11
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
12
12
|
import { dirname as dirname2, join as join5, posix, relative } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
|
-
// ../events/dist/result.js
|
|
16
|
-
import { z as z2 } from "zod";
|
|
17
|
-
|
|
18
15
|
// ../core/dist/guard-record.js
|
|
19
16
|
import { z } from "zod";
|
|
20
17
|
var severitySchema = z.enum(["info", "warn", "block"]);
|
|
@@ -138,7 +135,101 @@ function parseAccountStoreText(raw) {
|
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
|
|
138
|
+
// ../core/dist/hook-wiring.js
|
|
139
|
+
var SETTINGS_REL = ".claude/settings.json";
|
|
140
|
+
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
141
|
+
var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
|
|
142
|
+
var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
143
|
+
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
144
|
+
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
145
|
+
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
146
|
+
var HALFCYCLE_HOOK_WIRING = [
|
|
147
|
+
{
|
|
148
|
+
event: "SessionStart",
|
|
149
|
+
matcher: null,
|
|
150
|
+
timeout: 5,
|
|
151
|
+
command: `bash ./${SESSION_START_MARKER_REL}`,
|
|
152
|
+
script: SESSION_START_MARKER_REL
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
event: "SessionStart",
|
|
156
|
+
matcher: null,
|
|
157
|
+
timeout: 10,
|
|
158
|
+
command: `bash ./${GUARD_RUNNER_REL} refresh-credential`,
|
|
159
|
+
script: GUARD_RUNNER_REL
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
event: "UserPromptSubmit",
|
|
163
|
+
matcher: null,
|
|
164
|
+
timeout: 5,
|
|
165
|
+
command: `bash ./${USER_PROMPT_REMINDER_REL}`,
|
|
166
|
+
script: USER_PROMPT_REMINDER_REL
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
event: "PostToolUse",
|
|
170
|
+
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
171
|
+
timeout: 60,
|
|
172
|
+
command: `bash ./${GUARD_RUNNER_REL} hook post-tool-use`,
|
|
173
|
+
script: GUARD_RUNNER_REL
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
event: "Stop",
|
|
177
|
+
matcher: null,
|
|
178
|
+
timeout: 60,
|
|
179
|
+
command: `bash ./${GUARD_RUNNER_REL} hook stop`,
|
|
180
|
+
script: GUARD_RUNNER_REL
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
event: "SubagentStop",
|
|
184
|
+
matcher: null,
|
|
185
|
+
timeout: 60,
|
|
186
|
+
command: `bash ./${GUARD_RUNNER_REL} hook subagent-stop`,
|
|
187
|
+
script: GUARD_RUNNER_REL
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
event: "SessionEnd",
|
|
191
|
+
matcher: null,
|
|
192
|
+
timeout: 5,
|
|
193
|
+
command: `bash ./${SESSION_END_MARKER_REL}`,
|
|
194
|
+
script: SESSION_END_MARKER_REL
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
var RETIRED_HOOK_COMMANDS = [];
|
|
198
|
+
var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
|
|
199
|
+
...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
|
|
200
|
+
...RETIRED_HOOK_COMMANDS
|
|
201
|
+
]);
|
|
202
|
+
var HALFCYCLE_HOOK_WIRING_PATHS = [
|
|
203
|
+
.../* @__PURE__ */ new Set([SETTINGS_REL, ...HALFCYCLE_HOOK_WIRING.map((entry) => entry.script), VENDORED_BIN_REL])
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
// ../core/dist/secret-shape.js
|
|
207
|
+
var SECRET_SHAPE_PATTERNS = [
|
|
208
|
+
// Assignment to a credential-named identifier: `apiKey = "..."`, `secret: '...'`,
|
|
209
|
+
// `password=...`, `token = ...`, `access_key: ...` — the value need not be quoted.
|
|
210
|
+
/\b(?:api[_-]?key|secret|password|passwd|token|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer)\b\s*[:=]\s*\S+/i,
|
|
211
|
+
// Connection strings that embed credentials: `scheme://user:pass@host`.
|
|
212
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]+@/i,
|
|
213
|
+
// Bearer / authorization header values.
|
|
214
|
+
/\bauthorization\b\s*[:=]\s*['"]?\s*bearer\s+\S+/i,
|
|
215
|
+
// PEM / private-key block headers.
|
|
216
|
+
/-----BEGIN\s+(?:RSA\s+|EC\s+|OPENSSH\s+|DSA\s+)?PRIVATE\s+KEY-----/,
|
|
217
|
+
// JWT shape: three base64url segments separated by dots.
|
|
218
|
+
/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/,
|
|
219
|
+
// A long high-entropy hex/base64 run adjacent to a credential-ish word on the
|
|
220
|
+
// same line — a raw key literal. Kept last (broadest, most likely false-drop).
|
|
221
|
+
/\b(?:key|token|secret|credential)\b[\s\S]{0,40}[A-Za-z0-9+/_-]{32,}/i
|
|
222
|
+
];
|
|
223
|
+
function looksLikeSecret(text) {
|
|
224
|
+
for (const pattern of SECRET_SHAPE_PATTERNS) {
|
|
225
|
+
if (pattern.test(text))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
141
231
|
// ../events/dist/result.js
|
|
232
|
+
import { z as z2 } from "zod";
|
|
142
233
|
var firedGuardSchema = z2.object({
|
|
143
234
|
guardId: z2.string(),
|
|
144
235
|
patternRef: z2.string(),
|
|
@@ -196,219 +287,31 @@ var wireErrorSchema = z2.object({
|
|
|
196
287
|
message: z2.string()
|
|
197
288
|
}).strict();
|
|
198
289
|
|
|
199
|
-
// ../events/dist/telemetry.js
|
|
200
|
-
import { z as z3 } from "zod";
|
|
201
|
-
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");
|
|
202
|
-
var guardEvalOutcomeSchema = z3.enum([
|
|
203
|
-
"evaluated",
|
|
204
|
-
"infra-error",
|
|
205
|
-
"contract-error",
|
|
206
|
-
"unconfigured"
|
|
207
|
-
]);
|
|
208
|
-
var guardEvalRunSchema = z3.object({
|
|
209
|
-
// `runId`/`outcome` are the E2 run-observability additions, declared
|
|
210
|
-
// field-level-OPTIONAL as the FIRST (expand) step of an expand-contract
|
|
211
|
-
// migration. Every new record populated by the migrated writer (T-07)
|
|
212
|
-
// carries both; a legacy line predating the amendment carries neither and
|
|
213
|
-
// must still parse (the mixed-era guard-eval log the Build-Record reader
|
|
214
|
-
// assembles from, and the runner emit not yet migrated). So the presence
|
|
215
|
-
// refinement below is skipped for a legacy line (no `outcome`), and the
|
|
216
|
-
// per-writer guarantee lives with the writer + reader-normalisation (T-07),
|
|
217
|
-
// where the schema may then be tightened to required. runId is uuid-checked
|
|
218
|
-
// WHEN present; outcome is enum-checked when present.
|
|
219
|
-
runId: z3.string().uuid().optional(),
|
|
220
|
-
engagementId: z3.string().optional(),
|
|
221
|
-
runType: z3.enum(["hook", "ci"]),
|
|
222
|
-
occurredAt: utcIso8601,
|
|
223
|
-
outcome: guardEvalOutcomeSchema.optional(),
|
|
224
|
-
failureReason: z3.string().optional(),
|
|
225
|
-
phase: z3.string().optional(),
|
|
226
|
-
scope: z3.record(z3.string(), z3.unknown()),
|
|
227
|
-
cost: z3.record(z3.string(), z3.unknown()).optional(),
|
|
228
|
-
result: resultEnvelopeSchema.optional()
|
|
229
|
-
}).strict().superRefine((run, ctx) => {
|
|
230
|
-
if (run.outcome === void 0)
|
|
231
|
-
return;
|
|
232
|
-
if (run.outcome === "evaluated" && run.result === void 0) {
|
|
233
|
-
ctx.addIssue({
|
|
234
|
-
code: z3.ZodIssueCode.custom,
|
|
235
|
-
path: ["result"],
|
|
236
|
-
message: "result is required when outcome is 'evaluated'"
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
if (run.outcome !== "evaluated" && run.result !== void 0) {
|
|
240
|
-
ctx.addIssue({
|
|
241
|
-
code: z3.ZodIssueCode.custom,
|
|
242
|
-
path: ["result"],
|
|
243
|
-
message: "result is only present when outcome is 'evaluated'"
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
if (run.outcome !== "unconfigured" && run.engagementId === void 0) {
|
|
247
|
-
ctx.addIssue({
|
|
248
|
-
code: z3.ZodIssueCode.custom,
|
|
249
|
-
path: ["engagementId"],
|
|
250
|
-
message: "engagementId is required unless outcome is 'unconfigured'"
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
290
|
// ../events/dist/engagement-lifecycle.js
|
|
291
|
+
import { z as z5 } from "zod";
|
|
292
|
+
|
|
293
|
+
// ../events/dist/lone-surrogates.js
|
|
294
|
+
var LONE_SURROGATE = new RegExp("\\p{Cs}", "u");
|
|
295
|
+
function hasLoneSurrogate(text) {
|
|
296
|
+
return LONE_SURROGATE.test(text);
|
|
297
|
+
}
|
|
298
|
+
var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
|
|
299
|
+
|
|
300
|
+
// ../events/dist/build-record.js
|
|
256
301
|
import { z as z4 } from "zod";
|
|
257
|
-
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");
|
|
258
|
-
var engagementTypeSchema = z4.enum(["client"]);
|
|
259
|
-
var lifecycleStatusSchema = z4.enum(["active", "delivering", "closed"]);
|
|
260
|
-
var createEngagementRequestSchema = z4.object({
|
|
261
|
-
name: z4.string(),
|
|
262
|
-
region: z4.string(),
|
|
263
|
-
engagementType: engagementTypeSchema
|
|
264
|
-
}).strict();
|
|
265
|
-
var createEngagementResponseSchema = z4.object({
|
|
266
|
-
engagementId: z4.string(),
|
|
267
|
-
name: z4.string(),
|
|
268
|
-
region: z4.string(),
|
|
269
|
-
status: lifecycleStatusSchema,
|
|
270
|
-
createdAt: utcIso86012,
|
|
271
|
-
sessionToken: z4.string()
|
|
272
|
-
}).strict();
|
|
273
|
-
var engagementStatusResponseSchema = z4.object({
|
|
274
|
-
engagementId: z4.string(),
|
|
275
|
-
name: z4.string(),
|
|
276
|
-
region: z4.string(),
|
|
277
|
-
status: lifecycleStatusSchema,
|
|
278
|
-
installLinked: z4.boolean(),
|
|
279
|
-
createdAt: utcIso86012,
|
|
280
|
-
updatedAt: utcIso86012
|
|
281
|
-
}).strict();
|
|
282
|
-
var linkInstallationRequestSchema = z4.object({
|
|
283
|
-
installationId: z4.string(),
|
|
284
|
-
engagementId: z4.string()
|
|
285
|
-
}).strict();
|
|
286
|
-
var startBuildRequestSchema = z4.object({
|
|
287
|
-
engagementId: z4.string()
|
|
288
|
-
}).strict();
|
|
289
|
-
var startBuildResponseSchema = z4.object({
|
|
290
|
-
engagementId: z4.string(),
|
|
291
|
-
status: lifecycleStatusSchema,
|
|
292
|
-
enqueuedAt: utcIso86012
|
|
293
|
-
}).strict();
|
|
294
|
-
var acceptanceOutcomeSchema = z4.object({
|
|
295
|
-
verdict: z4.enum(["clean", "defects"]),
|
|
296
|
-
findings: z4.number().int().nonnegative(),
|
|
297
|
-
findingsDetail: z4.array(z4.object({ summary: z4.string() }).strict())
|
|
298
|
-
}).strict();
|
|
299
|
-
var PHASE_CLOSE_REASON_MAX = 1900;
|
|
300
|
-
var phaseCloseDecisionSchema = z4.discriminatedUnion("decision", [
|
|
301
|
-
z4.object({
|
|
302
|
-
decision: z4.literal("done-when-met"),
|
|
303
|
-
/** Who is closing the phase. */
|
|
304
|
-
actor: z4.string()
|
|
305
|
-
}).strict(),
|
|
306
|
-
z4.object({
|
|
307
|
-
decision: z4.literal("override"),
|
|
308
|
-
/** Who is closing it anyway. */
|
|
309
|
-
actor: z4.string(),
|
|
310
|
-
/**
|
|
311
|
-
* Why, in their own words. Bounded: the reason is kept on the phase's
|
|
312
|
-
* permanent record of its own close, and that record refuses anything
|
|
313
|
-
* longer — so an over-long reason is refused here, before the close is
|
|
314
|
-
* attempted, rather than failing the close itself.
|
|
315
|
-
*/
|
|
316
|
-
reason: z4.string().max(PHASE_CLOSE_REASON_MAX)
|
|
317
|
-
}).strict()
|
|
318
|
-
]);
|
|
319
|
-
var acceptPhaseRequestSchema = z4.object({
|
|
320
|
-
phase: z4.string(),
|
|
321
|
-
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
322
|
-
close: phaseCloseDecisionSchema.optional()
|
|
323
|
-
}).strict();
|
|
324
|
-
var coeFindingRequestSchema = z4.object({
|
|
325
|
-
phase: z4.string(),
|
|
326
|
-
description: z4.string(),
|
|
327
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
328
|
-
mappedPatternRef: z4.string().nullable()
|
|
329
|
-
}).strict();
|
|
330
|
-
var coeFindingResponseSchema = z4.object({
|
|
331
|
-
findingId: z4.string(),
|
|
332
|
-
engagementId: z4.string(),
|
|
333
|
-
phase: z4.string(),
|
|
334
|
-
description: z4.string(),
|
|
335
|
-
addRateTag: z4.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
336
|
-
mappedPatternRef: z4.string().nullable(),
|
|
337
|
-
createdAt: utcIso86012
|
|
338
|
-
}).strict();
|
|
339
|
-
var operatorFindingResponseSchema = z4.object({
|
|
340
|
-
// The finding just recorded.
|
|
341
|
-
finding: coeFindingResponseSchema,
|
|
342
|
-
// Every finding recorded for that phase, oldest first, including this one.
|
|
343
|
-
phaseFindings: z4.array(coeFindingResponseSchema),
|
|
344
|
-
// Whether the phase's finding rate was recalculated to include this finding.
|
|
345
|
-
figuresRecomputed: z4.boolean()
|
|
346
|
-
}).strict();
|
|
347
|
-
var phaseNotOnRecordRefusalSchema = wireErrorSchema.extend({
|
|
348
|
-
// Always this value for this refusal.
|
|
349
|
-
error: z4.literal("PhaseNotOnRecord"),
|
|
350
|
-
// The phases that can take a finding, sorted.
|
|
351
|
-
phases: z4.array(z4.string())
|
|
352
|
-
}).strict();
|
|
353
|
-
var phaseEntryDecisionSchema = z4.discriminatedUnion("decision", [
|
|
354
|
-
z4.object({
|
|
355
|
-
decision: z4.literal("entry-condition-met"),
|
|
356
|
-
/** Who attests. */
|
|
357
|
-
actor: z4.string(),
|
|
358
|
-
/** What makes the entry condition hold, in the attester's own words. */
|
|
359
|
-
evidence: z4.string()
|
|
360
|
-
}).strict(),
|
|
361
|
-
z4.object({
|
|
362
|
-
decision: z4.literal("override"),
|
|
363
|
-
/** Who is opening it anyway. */
|
|
364
|
-
actor: z4.string(),
|
|
365
|
-
/** Why, in the operator's own words. */
|
|
366
|
-
reason: z4.string()
|
|
367
|
-
}).strict()
|
|
368
|
-
]);
|
|
369
|
-
var PHASE_ENTRY_DECISION_KINDS = [
|
|
370
|
-
"entry-condition-met",
|
|
371
|
-
"override"
|
|
372
|
-
];
|
|
373
|
-
var PHASE_CLOSE_DECISION_KINDS = ["phase-closed", "phase-close-override"];
|
|
374
|
-
var phaseDecisionKindSchema = z4.enum([
|
|
375
|
-
...PHASE_ENTRY_DECISION_KINDS,
|
|
376
|
-
...PHASE_CLOSE_DECISION_KINDS
|
|
377
|
-
]);
|
|
378
|
-
var openPhaseRequestSchema = z4.object({
|
|
379
|
-
phase: z4.string().nullable(),
|
|
380
|
-
entry: phaseEntryDecisionSchema
|
|
381
|
-
}).strict();
|
|
382
|
-
var phaseEntryDecisionRecordSchema = z4.object({
|
|
383
|
-
decisionId: z4.string(),
|
|
384
|
-
decision: z4.enum(PHASE_ENTRY_DECISION_KINDS),
|
|
385
|
-
actor: z4.string(),
|
|
386
|
-
/** The evidence (attestation) or the reason (override) — one basis per row. */
|
|
387
|
-
justification: z4.string(),
|
|
388
|
-
blockingFinding: z4.string().nullable(),
|
|
389
|
-
decidedAt: utcIso86012
|
|
390
|
-
}).strict();
|
|
391
|
-
var openPhaseResponseSchema = z4.object({
|
|
392
|
-
engagement: engagementStatusResponseSchema,
|
|
393
|
-
/** What is open now. `null` is project scope. */
|
|
394
|
-
phase: z4.string().nullable(),
|
|
395
|
-
/** What was open before this decision. `null` is project scope. */
|
|
396
|
-
previousPhase: z4.string().nullable(),
|
|
397
|
-
decision: phaseEntryDecisionRecordSchema
|
|
398
|
-
}).strict();
|
|
399
302
|
|
|
400
303
|
// ../events/dist/state.js
|
|
401
|
-
import { z as
|
|
402
|
-
var
|
|
304
|
+
import { z as z3 } from "zod";
|
|
305
|
+
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");
|
|
403
306
|
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
404
|
-
var stepIdSchema =
|
|
405
|
-
var methodVersionSchema =
|
|
307
|
+
var stepIdSchema = z3.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
308
|
+
var methodVersionSchema = z3.string().regex(/^\d+\.\d+\.\d+$/);
|
|
406
309
|
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
407
|
-
var recordKindSchema =
|
|
408
|
-
var methodLayerSchema =
|
|
409
|
-
var evidenceKindSchema =
|
|
410
|
-
var declarableEvidenceKindSchema =
|
|
411
|
-
var dispositionSchema =
|
|
310
|
+
var recordKindSchema = z3.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
311
|
+
var methodLayerSchema = z3.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
312
|
+
var evidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
313
|
+
var declarableEvidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
314
|
+
var dispositionSchema = z3.enum([
|
|
412
315
|
"prevented",
|
|
413
316
|
"corrected-in-spec",
|
|
414
317
|
"corrected-in-scope",
|
|
@@ -418,20 +321,20 @@ var dispositionSchema = z5.enum([
|
|
|
418
321
|
"surfaced",
|
|
419
322
|
"filed-forward"
|
|
420
323
|
]);
|
|
421
|
-
var mechanismSchema =
|
|
422
|
-
var artefactStateSchema =
|
|
324
|
+
var mechanismSchema = z3.enum(["guard", "verdict", "served-step", "method-step"]);
|
|
325
|
+
var artefactStateSchema = z3.enum(["created", "updated"]);
|
|
423
326
|
var QUALIFIED_OUTCOMES = ["declined", "parked"];
|
|
424
|
-
var qualifiedOutcomeSchema =
|
|
327
|
+
var qualifiedOutcomeSchema = z3.enum(QUALIFIED_OUTCOMES);
|
|
425
328
|
function isQualifiedOutcome(outcome) {
|
|
426
329
|
return QUALIFIED_OUTCOMES.includes(outcome);
|
|
427
330
|
}
|
|
428
|
-
var runOutcomeSchema =
|
|
331
|
+
var runOutcomeSchema = z3.enum([
|
|
429
332
|
"completed",
|
|
430
333
|
"attempted-failed",
|
|
431
334
|
...QUALIFIED_OUTCOMES
|
|
432
335
|
]);
|
|
433
|
-
var gateKindSchema =
|
|
434
|
-
var gateVerdictSchema =
|
|
336
|
+
var gateKindSchema = z3.enum(["approval", "mechanical"]);
|
|
337
|
+
var gateVerdictSchema = z3.enum(["pass", "fail", ...QUALIFIED_OUTCOMES]);
|
|
435
338
|
function outcomeReasonIssue(outcome, reason) {
|
|
436
339
|
if (isQualifiedOutcome(outcome) && reason === void 0) {
|
|
437
340
|
return { message: `outcomeReason is required when the outcome is '${outcome}'` };
|
|
@@ -443,142 +346,527 @@ function outcomeReasonIssue(outcome, reason) {
|
|
|
443
346
|
}
|
|
444
347
|
return null;
|
|
445
348
|
}
|
|
446
|
-
var anchorEvidenceSchema =
|
|
447
|
-
kind:
|
|
448
|
-
path:
|
|
349
|
+
var anchorEvidenceSchema = z3.object({
|
|
350
|
+
kind: z3.literal("anchor"),
|
|
351
|
+
path: z3.string().regex(anchorPathRegex),
|
|
449
352
|
// Optional 1-based line number within the file at `path`. Present when the
|
|
450
353
|
// writer knows the exact line; omitted — never `0`, never `null` — when it
|
|
451
354
|
// does not, because a line number is not always obtainable. Absence is a
|
|
452
355
|
// valid, permanent state here, not a gap meant to be filled in later.
|
|
453
|
-
line:
|
|
356
|
+
line: z3.number().optional()
|
|
454
357
|
}).strict();
|
|
455
|
-
var diffEvidenceSchema =
|
|
456
|
-
kind:
|
|
457
|
-
ref:
|
|
358
|
+
var diffEvidenceSchema = z3.object({
|
|
359
|
+
kind: z3.literal("diff"),
|
|
360
|
+
ref: z3.string()
|
|
458
361
|
}).strict();
|
|
459
|
-
var reproEvidenceSchema =
|
|
460
|
-
kind:
|
|
461
|
-
steps:
|
|
362
|
+
var reproEvidenceSchema = z3.object({
|
|
363
|
+
kind: z3.literal("repro"),
|
|
364
|
+
steps: z3.string().max(2e3)
|
|
462
365
|
}).strict();
|
|
463
|
-
var questionAnswerEvidenceSchema =
|
|
464
|
-
kind:
|
|
465
|
-
question:
|
|
466
|
-
before:
|
|
467
|
-
after:
|
|
366
|
+
var questionAnswerEvidenceSchema = z3.object({
|
|
367
|
+
kind: z3.literal("question-answer"),
|
|
368
|
+
question: z3.string().max(2e3),
|
|
369
|
+
before: z3.string().max(2e3),
|
|
370
|
+
after: z3.string().max(2e3)
|
|
468
371
|
}).strict();
|
|
469
|
-
var noOpEvidenceSchema =
|
|
470
|
-
kind:
|
|
471
|
-
inspected:
|
|
472
|
-
unchangedBecause:
|
|
372
|
+
var noOpEvidenceSchema = z3.object({
|
|
373
|
+
kind: z3.literal("no-op"),
|
|
374
|
+
inspected: z3.string().max(2e3),
|
|
375
|
+
unchangedBecause: z3.string().max(2e3)
|
|
473
376
|
}).strict();
|
|
474
|
-
var evidenceSchema =
|
|
377
|
+
var evidenceSchema = z3.discriminatedUnion("kind", [
|
|
475
378
|
anchorEvidenceSchema,
|
|
476
379
|
diffEvidenceSchema,
|
|
477
380
|
reproEvidenceSchema,
|
|
478
381
|
questionAnswerEvidenceSchema,
|
|
479
382
|
noOpEvidenceSchema
|
|
480
383
|
]);
|
|
481
|
-
var stateRecordEnvelopeSchema =
|
|
482
|
-
format:
|
|
384
|
+
var stateRecordEnvelopeSchema = z3.object({
|
|
385
|
+
format: z3.literal(STATE_RECORD_FORMAT),
|
|
483
386
|
recordKind: recordKindSchema,
|
|
484
|
-
recordId:
|
|
387
|
+
recordId: z3.string().uuid(),
|
|
485
388
|
methodVersion: methodVersionSchema,
|
|
486
|
-
engagementId:
|
|
389
|
+
engagementId: z3.string(),
|
|
487
390
|
stepId: stepIdSchema,
|
|
488
391
|
// Immutable: names when the thing happened, never when a later disposition
|
|
489
392
|
// moved.
|
|
490
|
-
occurredAt:
|
|
393
|
+
occurredAt: utcIso8601,
|
|
491
394
|
// Equal to `occurredAt` on first write; advances on each re-emit OF A FINDING.
|
|
492
395
|
// Carried on the shared envelope so a "when did this last change" query is
|
|
493
396
|
// kind-agnostic. Kept rather than generalised to `updatedAt` because
|
|
494
397
|
// `disposition` is the only field any re-emit may change (§5.3).
|
|
495
|
-
dispositionAt:
|
|
398
|
+
dispositionAt: utcIso8601,
|
|
496
399
|
evidence: evidenceSchema,
|
|
497
400
|
// Which phase of the project this record belongs to, if any. Present on
|
|
498
401
|
// records written during phase-scoped work; absent on records that run once
|
|
499
402
|
// for the whole project rather than per phase. Absence is meaningful on its
|
|
500
403
|
// own — it is never replaced with a placeholder value, and it is read as
|
|
501
404
|
// "satisfies any phase", not as "unknown".
|
|
502
|
-
phase:
|
|
405
|
+
phase: z3.string().optional()
|
|
503
406
|
});
|
|
504
|
-
var artefactRefSchema =
|
|
505
|
-
name:
|
|
506
|
-
parent:
|
|
407
|
+
var artefactRefSchema = z3.object({
|
|
408
|
+
name: z3.string(),
|
|
409
|
+
parent: z3.string()
|
|
507
410
|
}).strict();
|
|
508
|
-
var coverageSchema =
|
|
509
|
-
asked:
|
|
510
|
-
answered:
|
|
411
|
+
var coverageSchema = z3.object({
|
|
412
|
+
asked: z3.number().int().nonnegative(),
|
|
413
|
+
answered: z3.number().int().nonnegative()
|
|
511
414
|
}).strict().superRefine((coverage, ctx) => {
|
|
512
415
|
if (coverage.answered > coverage.asked) {
|
|
513
416
|
ctx.addIssue({
|
|
514
|
-
code:
|
|
417
|
+
code: z3.ZodIssueCode.custom,
|
|
515
418
|
path: ["answered"],
|
|
516
419
|
message: "answered cannot be more than asked"
|
|
517
420
|
});
|
|
518
421
|
}
|
|
519
422
|
});
|
|
520
423
|
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
521
|
-
recordKind:
|
|
424
|
+
recordKind: z3.literal("artefact"),
|
|
522
425
|
artefactRef: artefactRefSchema,
|
|
523
426
|
// Some layer wrote it, by construction — an empty array is not a legitimate
|
|
524
427
|
// state, so the schema says so.
|
|
525
|
-
writtenByLayers:
|
|
428
|
+
writtenByLayers: z3.array(methodLayerSchema).min(1),
|
|
526
429
|
state: artefactStateSchema
|
|
527
430
|
}).strict();
|
|
528
431
|
var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
529
|
-
recordKind:
|
|
432
|
+
recordKind: z3.literal("step-run"),
|
|
530
433
|
runOutcome: runOutcomeSchema,
|
|
531
|
-
failureReason:
|
|
532
|
-
outcomeReason:
|
|
434
|
+
failureReason: z3.string().optional(),
|
|
435
|
+
outcomeReason: z3.string().max(2e3).optional()
|
|
533
436
|
}).strict().superRefine((rec, ctx) => {
|
|
534
437
|
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
535
438
|
ctx.addIssue({
|
|
536
|
-
code:
|
|
439
|
+
code: z3.ZodIssueCode.custom,
|
|
537
440
|
path: ["failureReason"],
|
|
538
441
|
message: "failureReason is required when runOutcome is 'attempted-failed'"
|
|
539
442
|
});
|
|
540
443
|
}
|
|
541
444
|
if (rec.runOutcome !== "attempted-failed" && rec.failureReason !== void 0) {
|
|
542
445
|
ctx.addIssue({
|
|
543
|
-
code:
|
|
446
|
+
code: z3.ZodIssueCode.custom,
|
|
544
447
|
path: ["failureReason"],
|
|
545
448
|
message: "failureReason is only present when runOutcome is 'attempted-failed'"
|
|
546
449
|
});
|
|
547
450
|
}
|
|
548
451
|
const issue = outcomeReasonIssue(rec.runOutcome, rec.outcomeReason);
|
|
549
452
|
if (issue) {
|
|
550
|
-
ctx.addIssue({ code:
|
|
453
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
551
454
|
}
|
|
552
455
|
});
|
|
553
456
|
var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
554
|
-
recordKind:
|
|
457
|
+
recordKind: z3.literal("gate"),
|
|
555
458
|
gateKind: gateKindSchema,
|
|
556
459
|
verdict: gateVerdictSchema,
|
|
557
|
-
actor:
|
|
558
|
-
outcomeReason:
|
|
460
|
+
actor: z3.string(),
|
|
461
|
+
outcomeReason: z3.string().max(2e3).optional(),
|
|
559
462
|
coverage: coverageSchema.optional()
|
|
560
463
|
}).strict().superRefine((rec, ctx) => {
|
|
561
464
|
const issue = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
562
465
|
if (issue) {
|
|
563
|
-
ctx.addIssue({ code:
|
|
466
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["outcomeReason"], ...issue });
|
|
564
467
|
}
|
|
565
468
|
});
|
|
566
469
|
var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
567
|
-
recordKind:
|
|
470
|
+
recordKind: z3.literal("intervention"),
|
|
568
471
|
layer: methodLayerSchema,
|
|
569
472
|
mechanism: mechanismSchema,
|
|
570
473
|
severity: severitySchema,
|
|
571
474
|
disposition: dispositionSchema,
|
|
572
|
-
summary:
|
|
475
|
+
summary: z3.string().max(2e3),
|
|
573
476
|
coverage: coverageSchema.optional()
|
|
574
477
|
}).strict();
|
|
575
|
-
var stateRecordSchema =
|
|
478
|
+
var stateRecordSchema = z3.discriminatedUnion("recordKind", [
|
|
576
479
|
artefactRecordSchema,
|
|
577
480
|
stepRunRecordSchema,
|
|
578
481
|
gateRecordSchema,
|
|
579
482
|
interventionRecordSchema
|
|
580
483
|
]);
|
|
581
484
|
|
|
485
|
+
// ../events/dist/control-characters.js
|
|
486
|
+
var CONTROL_CHARACTER_CLASS = "\\p{Cc}";
|
|
487
|
+
var CONTROL_CHARACTER = new RegExp(CONTROL_CHARACTER_CLASS, "u");
|
|
488
|
+
function hasControlCharacter(text) {
|
|
489
|
+
return CONTROL_CHARACTER.test(text);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ../events/dist/phase-identity.js
|
|
493
|
+
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
494
|
+
var PHASE_NAME_MAX_BYTES = 200;
|
|
495
|
+
function isValidPhaseIdentity(identity) {
|
|
496
|
+
if (typeof identity === "number")
|
|
497
|
+
return Number.isInteger(identity) && identity >= 0;
|
|
498
|
+
if (typeof identity !== "string")
|
|
499
|
+
return false;
|
|
500
|
+
if (identity === "")
|
|
501
|
+
return false;
|
|
502
|
+
if (identity.includes("/"))
|
|
503
|
+
return false;
|
|
504
|
+
if (identity.includes("\\"))
|
|
505
|
+
return false;
|
|
506
|
+
if (identity.startsWith("."))
|
|
507
|
+
return false;
|
|
508
|
+
if (hasControlCharacter(identity))
|
|
509
|
+
return false;
|
|
510
|
+
if (utf8ByteLength(identity) > PHASE_NAME_MAX_BYTES)
|
|
511
|
+
return false;
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
function utf8ByteLength(text) {
|
|
515
|
+
let bytes = 0;
|
|
516
|
+
for (const ch of text) {
|
|
517
|
+
const cp = ch.codePointAt(0);
|
|
518
|
+
bytes += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
519
|
+
}
|
|
520
|
+
return bytes;
|
|
521
|
+
}
|
|
522
|
+
var InvalidPhaseIdentityError = class extends Error {
|
|
523
|
+
/** The value that was refused, exactly as supplied. */
|
|
524
|
+
identity;
|
|
525
|
+
constructor(identity, allowedLocation) {
|
|
526
|
+
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
527
|
+
const where = allowedLocation === void 0 ? "" : `
|
|
528
|
+
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
529
|
+
super(`that phase identity cannot name ${subject}.
|
|
530
|
+
identity: ${describeIdentity(identity)}` + where);
|
|
531
|
+
this.name = "InvalidPhaseIdentityError";
|
|
532
|
+
this.identity = identity;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
536
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
537
|
+
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function renderPhaseSegment(identity) {
|
|
541
|
+
if (!isValidPhaseIdentity(identity)) {
|
|
542
|
+
throw new InvalidPhaseIdentityError(identity);
|
|
543
|
+
}
|
|
544
|
+
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
545
|
+
}
|
|
546
|
+
function describeIdentity(identity) {
|
|
547
|
+
if (identity === "")
|
|
548
|
+
return "(empty)";
|
|
549
|
+
if (typeof identity === "string")
|
|
550
|
+
return identity;
|
|
551
|
+
if (identity === null)
|
|
552
|
+
return "null";
|
|
553
|
+
if (identity === void 0)
|
|
554
|
+
return "none supplied";
|
|
555
|
+
if (typeof identity === "number" || typeof identity === "boolean")
|
|
556
|
+
return String(identity);
|
|
557
|
+
if (Array.isArray(identity))
|
|
558
|
+
return "(a list)";
|
|
559
|
+
if (typeof identity === "object")
|
|
560
|
+
return "(an object)";
|
|
561
|
+
return `(a ${typeof identity})`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ../events/dist/build-record.js
|
|
565
|
+
var BUILD_RECORD_V2_FORMAT = "halfcycle-build-record/v2";
|
|
566
|
+
var STATEMENT_MAX = 2e3;
|
|
567
|
+
var DEVIATION_MAX = 500;
|
|
568
|
+
var DEVIATIONS_MAX = 20;
|
|
569
|
+
var ACTOR_MAX = 200;
|
|
570
|
+
var FINDING_MAX = 500;
|
|
571
|
+
var FINDINGS_SHOWN_MAX = 50;
|
|
572
|
+
var PHASE_CLOSE_REASON_MAX = 1900;
|
|
573
|
+
var closeCommitSchema = z4.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash: 40 or 64 lowercase hex characters");
|
|
574
|
+
var walkVerdictSchema = z4.enum(["clean", "defects"]);
|
|
575
|
+
var closeDecisionSchema = z4.enum(["done-when-met", "override"]);
|
|
576
|
+
function plainText(max) {
|
|
577
|
+
const base = max === void 0 ? z4.string() : z4.string().max(max);
|
|
578
|
+
return base.refine((v) => !hasControlCharacter(v), {
|
|
579
|
+
message: "must be one line with no control characters (no line breaks, tabs or NUL)"
|
|
580
|
+
}).refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE });
|
|
581
|
+
}
|
|
582
|
+
function statementText(max) {
|
|
583
|
+
return plainText(max).refine((v) => !looksLikeSecret(v), {
|
|
584
|
+
message: "looks like a password or key \u2014 rephrase it"
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
var closeActorSchema = plainText(ACTOR_MAX);
|
|
588
|
+
var phaseNameSchema = z4.string().refine((v) => isValidPhaseIdentity(v), {
|
|
589
|
+
message: `must be a phase name: not empty, no slash or backslash, not starting with a dot, no control characters, at most ${PHASE_NAME_MAX_BYTES} bytes`
|
|
590
|
+
});
|
|
591
|
+
var closeStatementsSchema = z4.object({
|
|
592
|
+
thesis: statementText(STATEMENT_MAX).nullable(),
|
|
593
|
+
outcome: statementText(STATEMENT_MAX).nullable(),
|
|
594
|
+
deviations: z4.array(statementText(DEVIATION_MAX)).max(DEVIATIONS_MAX),
|
|
595
|
+
closeCommit: closeCommitSchema.nullable()
|
|
596
|
+
}).strict();
|
|
597
|
+
var closedBySchema = z4.discriminatedUnion("kind", [
|
|
598
|
+
z4.object({ kind: z4.literal("account"), email: plainText().nullable() }).strict(),
|
|
599
|
+
z4.object({ kind: z4.literal("no-account") }).strict(),
|
|
600
|
+
z4.object({ kind: z4.literal("account-removed") }).strict(),
|
|
601
|
+
z4.object({ kind: z4.literal("not-recorded") }).strict()
|
|
602
|
+
]);
|
|
603
|
+
var unfinishedStepSchema = z4.object({
|
|
604
|
+
stepId: plainText(),
|
|
605
|
+
state: z4.enum(["parked", "failed"])
|
|
606
|
+
}).strict();
|
|
607
|
+
var crossedSchema = z4.discriminatedUnion("kind", [
|
|
608
|
+
z4.object({ kind: z4.literal("none") }).strict(),
|
|
609
|
+
z4.object({ kind: z4.literal("unfinished-steps"), steps: z4.array(unfinishedStepSchema).min(1) }).strict(),
|
|
610
|
+
z4.object({ kind: z4.literal("clean-over-findings"), findings: z4.number().int().min(1) }).strict(),
|
|
611
|
+
z4.object({ kind: z4.literal("not-recorded") }).strict()
|
|
612
|
+
]);
|
|
613
|
+
var seatCoverageSchema = z4.object({
|
|
614
|
+
enrolled: z4.number().int().nonnegative(),
|
|
615
|
+
conformant: z4.number().int().nonnegative()
|
|
616
|
+
}).strict().refine((c) => c.conformant <= c.enrolled, {
|
|
617
|
+
message: "conformant cannot exceed enrolled",
|
|
618
|
+
path: ["conformant"]
|
|
619
|
+
});
|
|
620
|
+
var observedCloseSchema = z4.object({
|
|
621
|
+
sequence: z4.number().int().min(1),
|
|
622
|
+
closedAt: utcIso8601,
|
|
623
|
+
closedBy: closedBySchema,
|
|
624
|
+
crossed: crossedSchema,
|
|
625
|
+
seatCoverage: seatCoverageSchema.nullable()
|
|
626
|
+
}).strict();
|
|
627
|
+
var couldNotRunCheckSchema = z4.object({
|
|
628
|
+
check: plainText(),
|
|
629
|
+
description: plainText().nullable(),
|
|
630
|
+
reason: plainText()
|
|
631
|
+
}).strict();
|
|
632
|
+
var firedCheckSchema = z4.object({
|
|
633
|
+
check: plainText(),
|
|
634
|
+
description: plainText().nullable(),
|
|
635
|
+
severity: severitySchema,
|
|
636
|
+
explanation: plainText().nullable(),
|
|
637
|
+
occurrences: z4.number().int().min(1)
|
|
638
|
+
}).strict();
|
|
639
|
+
var phaseChecksSchema = z4.discriminatedUnion("state", [
|
|
640
|
+
z4.object({ state: z4.literal("not-observed") }).strict(),
|
|
641
|
+
z4.object({
|
|
642
|
+
state: z4.literal("nothing-fired"),
|
|
643
|
+
runs: z4.number().int().min(1),
|
|
644
|
+
couldNotRun: z4.array(couldNotRunCheckSchema)
|
|
645
|
+
}).strict(),
|
|
646
|
+
z4.object({
|
|
647
|
+
state: z4.literal("fired"),
|
|
648
|
+
runs: z4.number().int().min(1),
|
|
649
|
+
couldNotRun: z4.array(couldNotRunCheckSchema),
|
|
650
|
+
fired: z4.array(firedCheckSchema).min(1)
|
|
651
|
+
}).strict()
|
|
652
|
+
]);
|
|
653
|
+
var observedSchema = z4.object({
|
|
654
|
+
engagementId: plainText(),
|
|
655
|
+
methodVersion: methodVersionSchema,
|
|
656
|
+
closes: z4.array(observedCloseSchema).min(1),
|
|
657
|
+
checks: phaseChecksSchema
|
|
658
|
+
}).strict().superRefine((o, ctx) => {
|
|
659
|
+
o.closes.forEach((close, i) => {
|
|
660
|
+
if (close.sequence !== i + 1) {
|
|
661
|
+
ctx.addIssue({
|
|
662
|
+
code: "custom",
|
|
663
|
+
message: "closes must be numbered 1, 2, 3 \u2026 in order with no gap",
|
|
664
|
+
path: ["closes", i, "sequence"]
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
var attestedWalkSchema = z4.object({
|
|
670
|
+
verdict: walkVerdictSchema,
|
|
671
|
+
findings: z4.array(plainText(FINDING_MAX)).max(FINDINGS_SHOWN_MAX),
|
|
672
|
+
findingsNotShown: z4.number().int().nonnegative()
|
|
673
|
+
}).strict();
|
|
674
|
+
var attestedStatementsSchema = z4.object({
|
|
675
|
+
thesis: plainText(STATEMENT_MAX).nullable(),
|
|
676
|
+
outcome: plainText(STATEMENT_MAX).nullable(),
|
|
677
|
+
deviations: z4.array(plainText(DEVIATION_MAX)).max(DEVIATIONS_MAX)
|
|
678
|
+
}).strict();
|
|
679
|
+
var attestedCloseSchema = z4.object({
|
|
680
|
+
sequence: z4.number().int().min(1),
|
|
681
|
+
decision: closeDecisionSchema,
|
|
682
|
+
actor: plainText(ACTOR_MAX).nullable(),
|
|
683
|
+
reason: plainText(PHASE_CLOSE_REASON_MAX).nullable(),
|
|
684
|
+
walk: attestedWalkSchema.nullable(),
|
|
685
|
+
closeCommit: closeCommitSchema.nullable(),
|
|
686
|
+
statements: attestedStatementsSchema
|
|
687
|
+
}).strict();
|
|
688
|
+
var attestedSchema = z4.object({
|
|
689
|
+
closes: z4.array(attestedCloseSchema)
|
|
690
|
+
}).strict();
|
|
691
|
+
var buildRecordV2Schema = z4.object({
|
|
692
|
+
format: z4.literal(BUILD_RECORD_V2_FORMAT),
|
|
693
|
+
phase: phaseNameSchema,
|
|
694
|
+
observed: observedSchema,
|
|
695
|
+
attested: attestedSchema
|
|
696
|
+
}).strict().superRefine((record2, ctx) => {
|
|
697
|
+
const observedBySequence = new Map(record2.observed.closes.map((c) => [c.sequence, c]));
|
|
698
|
+
const attestedSequences = record2.attested.closes.map((c) => c.sequence);
|
|
699
|
+
const sameSet = attestedSequences.length === observedBySequence.size && new Set(attestedSequences).size === attestedSequences.length && attestedSequences.every((s) => observedBySequence.has(s));
|
|
700
|
+
if (!sameSet) {
|
|
701
|
+
ctx.addIssue({
|
|
702
|
+
code: "custom",
|
|
703
|
+
message: "attested closes must match the observed closes one for one, by sequence",
|
|
704
|
+
path: ["attested", "closes"]
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
record2.attested.closes.forEach((close, i) => {
|
|
708
|
+
if (close.decision === "override" !== (close.reason !== null)) {
|
|
709
|
+
ctx.addIssue({
|
|
710
|
+
code: "custom",
|
|
711
|
+
message: "reason is given exactly when the decision is override",
|
|
712
|
+
path: ["attested", "closes", i, "reason"]
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
const observed = observedBySequence.get(close.sequence);
|
|
716
|
+
if (observed !== void 0 && observed.closedBy.kind === "account-removed" !== (close.actor === null)) {
|
|
717
|
+
ctx.addIssue({
|
|
718
|
+
code: "custom",
|
|
719
|
+
message: "actor is empty exactly when the closing account was removed",
|
|
720
|
+
path: ["attested", "closes", i, "actor"]
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// ../events/dist/engagement-lifecycle.js
|
|
727
|
+
var utcIso86012 = z5.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");
|
|
728
|
+
var engagementTypeSchema = z5.enum(["client"]);
|
|
729
|
+
var lifecycleStatusSchema = z5.enum(["active", "delivering", "closed"]);
|
|
730
|
+
var createEngagementRequestSchema = z5.object({
|
|
731
|
+
name: z5.string(),
|
|
732
|
+
region: z5.string(),
|
|
733
|
+
engagementType: engagementTypeSchema
|
|
734
|
+
}).strict();
|
|
735
|
+
var createEngagementResponseSchema = z5.object({
|
|
736
|
+
engagementId: z5.string(),
|
|
737
|
+
name: z5.string(),
|
|
738
|
+
region: z5.string(),
|
|
739
|
+
status: lifecycleStatusSchema,
|
|
740
|
+
createdAt: utcIso86012,
|
|
741
|
+
sessionToken: z5.string()
|
|
742
|
+
}).strict();
|
|
743
|
+
var engagementStatusResponseSchema = z5.object({
|
|
744
|
+
engagementId: z5.string(),
|
|
745
|
+
name: z5.string(),
|
|
746
|
+
region: z5.string(),
|
|
747
|
+
status: lifecycleStatusSchema,
|
|
748
|
+
installLinked: z5.boolean(),
|
|
749
|
+
createdAt: utcIso86012,
|
|
750
|
+
updatedAt: utcIso86012
|
|
751
|
+
}).strict();
|
|
752
|
+
var linkInstallationRequestSchema = z5.object({
|
|
753
|
+
installationId: z5.string(),
|
|
754
|
+
engagementId: z5.string()
|
|
755
|
+
}).strict();
|
|
756
|
+
var startBuildRequestSchema = z5.object({
|
|
757
|
+
engagementId: z5.string()
|
|
758
|
+
}).strict();
|
|
759
|
+
var startBuildResponseSchema = z5.object({
|
|
760
|
+
engagementId: z5.string(),
|
|
761
|
+
status: lifecycleStatusSchema,
|
|
762
|
+
enqueuedAt: utcIso86012
|
|
763
|
+
}).strict();
|
|
764
|
+
var acceptanceOutcomeSchema = z5.object({
|
|
765
|
+
verdict: walkVerdictSchema,
|
|
766
|
+
findings: z5.number().int().nonnegative(),
|
|
767
|
+
findingsDetail: z5.array(z5.object({ summary: z5.string().refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE }) }).strict())
|
|
768
|
+
}).strict();
|
|
769
|
+
var phaseCloseDecisionSchema = z5.discriminatedUnion("decision", [
|
|
770
|
+
z5.object({
|
|
771
|
+
decision: z5.literal(closeDecisionSchema.enum["done-when-met"]),
|
|
772
|
+
/** Who is closing the phase. */
|
|
773
|
+
actor: closeActorSchema
|
|
774
|
+
}).strict(),
|
|
775
|
+
z5.object({
|
|
776
|
+
decision: z5.literal(closeDecisionSchema.enum.override),
|
|
777
|
+
/** Who is closing it anyway. */
|
|
778
|
+
actor: closeActorSchema,
|
|
779
|
+
/**
|
|
780
|
+
* Why, in their own words. Bounded: the reason is kept on the phase's
|
|
781
|
+
* permanent record of its own close, and that record refuses anything
|
|
782
|
+
* longer — so an over-long reason is refused here, before the close is
|
|
783
|
+
* attempted, rather than failing the close itself.
|
|
784
|
+
*/
|
|
785
|
+
reason: z5.string().max(PHASE_CLOSE_REASON_MAX).refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE })
|
|
786
|
+
}).strict()
|
|
787
|
+
]);
|
|
788
|
+
var acceptPhaseRequestSchema = z5.object({
|
|
789
|
+
phase: phaseNameSchema,
|
|
790
|
+
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
791
|
+
close: phaseCloseDecisionSchema.optional(),
|
|
792
|
+
statements: closeStatementsSchema.optional()
|
|
793
|
+
}).strict();
|
|
794
|
+
var coeFindingRequestSchema = z5.object({
|
|
795
|
+
phase: z5.string(),
|
|
796
|
+
description: z5.string(),
|
|
797
|
+
addRateTag: z5.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
798
|
+
mappedPatternRef: z5.string().nullable()
|
|
799
|
+
}).strict();
|
|
800
|
+
var coeFindingResponseSchema = z5.object({
|
|
801
|
+
findingId: z5.string(),
|
|
802
|
+
engagementId: z5.string(),
|
|
803
|
+
phase: z5.string(),
|
|
804
|
+
description: z5.string(),
|
|
805
|
+
addRateTag: z5.enum(["seam-new", "seam-repeat", "model-limitation"]),
|
|
806
|
+
mappedPatternRef: z5.string().nullable(),
|
|
807
|
+
createdAt: utcIso86012
|
|
808
|
+
}).strict();
|
|
809
|
+
var operatorFindingResponseSchema = z5.object({
|
|
810
|
+
// The finding just recorded.
|
|
811
|
+
finding: coeFindingResponseSchema,
|
|
812
|
+
// Every finding recorded for that phase, oldest first, including this one.
|
|
813
|
+
phaseFindings: z5.array(coeFindingResponseSchema),
|
|
814
|
+
// Whether the phase's finding rate was recalculated to include this finding.
|
|
815
|
+
figuresRecomputed: z5.boolean()
|
|
816
|
+
}).strict();
|
|
817
|
+
var phaseNotOnRecordRefusalSchema = wireErrorSchema.extend({
|
|
818
|
+
// Always this value for this refusal.
|
|
819
|
+
error: z5.literal("PhaseNotOnRecord"),
|
|
820
|
+
// The phases that can take a finding, sorted.
|
|
821
|
+
phases: z5.array(z5.string())
|
|
822
|
+
}).strict();
|
|
823
|
+
var phaseEntryDecisionSchema = z5.discriminatedUnion("decision", [
|
|
824
|
+
z5.object({
|
|
825
|
+
decision: z5.literal("entry-condition-met"),
|
|
826
|
+
/** Who attests. */
|
|
827
|
+
actor: z5.string(),
|
|
828
|
+
/** What makes the entry condition hold, in the attester's own words. */
|
|
829
|
+
evidence: z5.string()
|
|
830
|
+
}).strict(),
|
|
831
|
+
z5.object({
|
|
832
|
+
decision: z5.literal("override"),
|
|
833
|
+
/** Who is opening it anyway. */
|
|
834
|
+
actor: z5.string(),
|
|
835
|
+
/** Why, in the operator's own words. */
|
|
836
|
+
reason: z5.string()
|
|
837
|
+
}).strict()
|
|
838
|
+
]);
|
|
839
|
+
var PHASE_ENTRY_DECISION_KINDS = [
|
|
840
|
+
"entry-condition-met",
|
|
841
|
+
"override"
|
|
842
|
+
];
|
|
843
|
+
var PHASE_CLOSE_DECISION_KINDS = ["phase-closed", "phase-close-override"];
|
|
844
|
+
var phaseDecisionKindSchema = z5.enum([
|
|
845
|
+
...PHASE_ENTRY_DECISION_KINDS,
|
|
846
|
+
...PHASE_CLOSE_DECISION_KINDS
|
|
847
|
+
]);
|
|
848
|
+
var openPhaseRequestSchema = z5.object({
|
|
849
|
+
phase: phaseNameSchema.nullable(),
|
|
850
|
+
entry: phaseEntryDecisionSchema
|
|
851
|
+
}).strict();
|
|
852
|
+
var phaseEntryDecisionRecordSchema = z5.object({
|
|
853
|
+
decisionId: z5.string(),
|
|
854
|
+
decision: z5.enum(PHASE_ENTRY_DECISION_KINDS),
|
|
855
|
+
actor: z5.string(),
|
|
856
|
+
/** The evidence (attestation) or the reason (override) — one basis per row. */
|
|
857
|
+
justification: z5.string(),
|
|
858
|
+
blockingFinding: z5.string().nullable(),
|
|
859
|
+
decidedAt: utcIso86012
|
|
860
|
+
}).strict();
|
|
861
|
+
var openPhaseResponseSchema = z5.object({
|
|
862
|
+
engagement: engagementStatusResponseSchema,
|
|
863
|
+
/** What is open now. `null` is project scope. */
|
|
864
|
+
phase: z5.string().nullable(),
|
|
865
|
+
/** What was open before this decision. `null` is project scope. */
|
|
866
|
+
previousPhase: z5.string().nullable(),
|
|
867
|
+
decision: phaseEntryDecisionRecordSchema
|
|
868
|
+
}).strict();
|
|
869
|
+
|
|
582
870
|
// ../events/dist/crew.js
|
|
583
871
|
import { z as z6 } from "zod";
|
|
584
872
|
var crewGroupSchema = z6.enum(["builder", "reviewer", "always-on"]);
|
|
@@ -775,7 +1063,7 @@ var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
|
775
1063
|
|
|
776
1064
|
// ../events/dist/device-auth.js
|
|
777
1065
|
import { z as z7 } from "zod";
|
|
778
|
-
var
|
|
1066
|
+
var utcIso86013 = z7.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");
|
|
779
1067
|
var DEVICE_AUTH_STATUS = {
|
|
780
1068
|
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
781
1069
|
PENDING: "pending",
|
|
@@ -820,7 +1108,7 @@ var deviceAuthStartResponseSchema = z7.object({
|
|
|
820
1108
|
deviceCode: z7.string().min(1),
|
|
821
1109
|
userCode: z7.string().min(1),
|
|
822
1110
|
verificationUrl: z7.string().min(1),
|
|
823
|
-
expiresAt:
|
|
1111
|
+
expiresAt: utcIso86013,
|
|
824
1112
|
pollIntervalMs: z7.number().int().positive()
|
|
825
1113
|
}).strict();
|
|
826
1114
|
var devicePollPendingSchema = z7.object({
|
|
@@ -831,7 +1119,7 @@ var devicePollApprovedSchema = z7.object({
|
|
|
831
1119
|
status: z7.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
832
1120
|
accountId: z7.string().min(1),
|
|
833
1121
|
credential: z7.string().min(1),
|
|
834
|
-
expiresAt:
|
|
1122
|
+
expiresAt: utcIso86013
|
|
835
1123
|
}).strict();
|
|
836
1124
|
var deviceAuthRefusalSchema = z7.object({
|
|
837
1125
|
status: z7.enum([
|
|
@@ -916,67 +1204,6 @@ function safeParseAccountIdentity(payload) {
|
|
|
916
1204
|
return accountIdentitySchema.safeParse(payload);
|
|
917
1205
|
}
|
|
918
1206
|
|
|
919
|
-
// ../events/dist/phase-identity.js
|
|
920
|
-
var PHASE_SEGMENT_PREFIX = "phase-";
|
|
921
|
-
function isValidPhaseIdentity(identity) {
|
|
922
|
-
if (typeof identity === "number")
|
|
923
|
-
return Number.isInteger(identity) && identity >= 0;
|
|
924
|
-
if (typeof identity !== "string")
|
|
925
|
-
return false;
|
|
926
|
-
if (identity === "")
|
|
927
|
-
return false;
|
|
928
|
-
if (identity.includes("/"))
|
|
929
|
-
return false;
|
|
930
|
-
if (identity.includes("\\"))
|
|
931
|
-
return false;
|
|
932
|
-
if (identity.includes("\0"))
|
|
933
|
-
return false;
|
|
934
|
-
if (identity.startsWith("."))
|
|
935
|
-
return false;
|
|
936
|
-
return true;
|
|
937
|
-
}
|
|
938
|
-
var InvalidPhaseIdentityError = class extends Error {
|
|
939
|
-
/** The value that was refused, exactly as supplied. */
|
|
940
|
-
identity;
|
|
941
|
-
constructor(identity, allowedLocation) {
|
|
942
|
-
const subject = allowedLocation === void 0 ? "a file" : "a Build Record";
|
|
943
|
-
const where = allowedLocation === void 0 ? "" : `
|
|
944
|
-
Records are written only into ${allowedLocation.replace(/\\/g, "/")}/. Nothing was written.`;
|
|
945
|
-
super(`that phase identity cannot name ${subject}.
|
|
946
|
-
identity: ${describeIdentity(identity)}` + where);
|
|
947
|
-
this.name = "InvalidPhaseIdentityError";
|
|
948
|
-
this.identity = identity;
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
function assertValidPhaseIdentity(identity, allowedLocation) {
|
|
952
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
953
|
-
throw new InvalidPhaseIdentityError(identity, allowedLocation);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
function renderPhaseSegment(identity) {
|
|
957
|
-
if (!isValidPhaseIdentity(identity)) {
|
|
958
|
-
throw new InvalidPhaseIdentityError(identity);
|
|
959
|
-
}
|
|
960
|
-
return `${PHASE_SEGMENT_PREFIX}${identity}`;
|
|
961
|
-
}
|
|
962
|
-
function describeIdentity(identity) {
|
|
963
|
-
if (identity === "")
|
|
964
|
-
return "(empty)";
|
|
965
|
-
if (typeof identity === "string")
|
|
966
|
-
return identity;
|
|
967
|
-
if (identity === null)
|
|
968
|
-
return "null";
|
|
969
|
-
if (identity === void 0)
|
|
970
|
-
return "none supplied";
|
|
971
|
-
if (typeof identity === "number" || typeof identity === "boolean")
|
|
972
|
-
return String(identity);
|
|
973
|
-
if (Array.isArray(identity))
|
|
974
|
-
return "(a list)";
|
|
975
|
-
if (typeof identity === "object")
|
|
976
|
-
return "(an object)";
|
|
977
|
-
return `(a ${typeof identity})`;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
1207
|
// ../events/dist/credential-wire.js
|
|
981
1208
|
import { z as z10 } from "zod";
|
|
982
1209
|
var engagementCredentialResponseSchema = z10.object({
|
|
@@ -988,10 +1215,47 @@ var engagementCredentialResponseSchema = z10.object({
|
|
|
988
1215
|
}).strict();
|
|
989
1216
|
var ciTokenExchangeResponseSchema = z10.object({
|
|
990
1217
|
token: z10.string(),
|
|
991
|
-
expiresAt:
|
|
1218
|
+
expiresAt: utcIso8601,
|
|
992
1219
|
engagementId: z10.string()
|
|
993
1220
|
}).strict();
|
|
994
1221
|
|
|
1222
|
+
// ../events/dist/phase-record.js
|
|
1223
|
+
import { z as z11 } from "zod";
|
|
1224
|
+
var phaseRecordRefusalReasonSchema = z11.enum([
|
|
1225
|
+
"still-open",
|
|
1226
|
+
"reopened",
|
|
1227
|
+
"never-opened",
|
|
1228
|
+
"never-closed",
|
|
1229
|
+
"closed-before-records"
|
|
1230
|
+
]);
|
|
1231
|
+
var phaseRecordRecordSchema = z11.object({
|
|
1232
|
+
outcome: z11.literal("record"),
|
|
1233
|
+
phase: phaseNameSchema,
|
|
1234
|
+
record: buildRecordV2Schema
|
|
1235
|
+
}).strict().refine((body) => body.record.phase === body.phase, {
|
|
1236
|
+
message: "the record is for a different phase than the one asked for",
|
|
1237
|
+
path: ["record", "phase"]
|
|
1238
|
+
});
|
|
1239
|
+
var phaseRecordWithheldSchema = z11.object({
|
|
1240
|
+
outcome: z11.literal("withheld"),
|
|
1241
|
+
phase: phaseNameSchema,
|
|
1242
|
+
sequence: z11.number().int().min(1),
|
|
1243
|
+
closedAt: utcIso8601,
|
|
1244
|
+
message: z11.string()
|
|
1245
|
+
}).strict();
|
|
1246
|
+
var phaseRecordRefusedSchema = wireErrorSchema.extend({
|
|
1247
|
+
outcome: z11.literal("refused"),
|
|
1248
|
+
phase: phaseNameSchema,
|
|
1249
|
+
statusCode: z11.literal(409),
|
|
1250
|
+
error: z11.literal("PhaseRecordRefused"),
|
|
1251
|
+
reason: phaseRecordRefusalReasonSchema
|
|
1252
|
+
}).strict();
|
|
1253
|
+
var phaseRecordResponseSchema = z11.discriminatedUnion("outcome", [
|
|
1254
|
+
phaseRecordRecordSchema,
|
|
1255
|
+
phaseRecordWithheldSchema,
|
|
1256
|
+
phaseRecordRefusedSchema
|
|
1257
|
+
]);
|
|
1258
|
+
|
|
995
1259
|
// dist/engagement-credential.js
|
|
996
1260
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
997
1261
|
import { platform as platform2 } from "node:os";
|
|
@@ -1204,14 +1468,36 @@ halfcycle_env_file() {
|
|
|
1204
1468
|
// dist/identity.js
|
|
1205
1469
|
import { execFileSync } from "node:child_process";
|
|
1206
1470
|
import { existsSync, readFileSync as readFileSync3 } from "node:fs";
|
|
1207
|
-
import { join as join3 } from "node:path";
|
|
1471
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
1208
1472
|
import { randomUUID } from "node:crypto";
|
|
1209
1473
|
var PROJECT_IDENTITY_NOTE = 'projectId is NOT the engagement id \u2014 it identifies this repository/worktree to Halfcycle, not an engagement. The engagement id lives in .halfcycle/bundle.json, in its "engagementId" field.';
|
|
1210
|
-
|
|
1474
|
+
var REPOSITORY_LOCATING_GIT_VARS = [
|
|
1475
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
1476
|
+
"GIT_COMMON_DIR",
|
|
1477
|
+
"GIT_DIR",
|
|
1478
|
+
"GIT_GRAFT_FILE",
|
|
1479
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
1480
|
+
"GIT_INDEX_FILE",
|
|
1481
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
1482
|
+
"GIT_OBJECT_DIRECTORY",
|
|
1483
|
+
"GIT_PREFIX",
|
|
1484
|
+
"GIT_REPLACE_REF_BASE",
|
|
1485
|
+
"GIT_SHALLOW_FILE",
|
|
1486
|
+
"GIT_WORK_TREE"
|
|
1487
|
+
];
|
|
1488
|
+
function gitEnvForNamedRepo() {
|
|
1489
|
+
const env = { ...process.env };
|
|
1490
|
+
for (const name of REPOSITORY_LOCATING_GIT_VARS)
|
|
1491
|
+
delete env[name];
|
|
1492
|
+
return env;
|
|
1493
|
+
}
|
|
1494
|
+
function git(targetRepoRoot, args2, stdin) {
|
|
1211
1495
|
try {
|
|
1212
1496
|
const out = execFileSync("git", ["-C", targetRepoRoot, ...args2], {
|
|
1213
1497
|
encoding: "utf-8",
|
|
1214
|
-
|
|
1498
|
+
env: gitEnvForNamedRepo(),
|
|
1499
|
+
...stdin === void 0 ? {} : { input: stdin },
|
|
1500
|
+
stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "ignore"]
|
|
1215
1501
|
});
|
|
1216
1502
|
const trimmed = out.trim();
|
|
1217
1503
|
return trimmed.length > 0 ? trimmed : null;
|
|
@@ -1236,6 +1522,38 @@ function readCommitCount(targetRepoRoot) {
|
|
|
1236
1522
|
const count = Number.parseInt(out, 10);
|
|
1237
1523
|
return Number.isFinite(count) ? count : null;
|
|
1238
1524
|
}
|
|
1525
|
+
function readIgnoredPaths(targetRepoRoot, paths) {
|
|
1526
|
+
if (paths.length === 0)
|
|
1527
|
+
return null;
|
|
1528
|
+
const out = git(targetRepoRoot, ["check-ignore", "-v", "-z", "--stdin"], paths.map((p) => `${p}\0`).join(""));
|
|
1529
|
+
if (out === null)
|
|
1530
|
+
return null;
|
|
1531
|
+
const fields = out.split("\0");
|
|
1532
|
+
if (fields[fields.length - 1] === "")
|
|
1533
|
+
fields.pop();
|
|
1534
|
+
if (fields.length % 4 !== 0)
|
|
1535
|
+
return null;
|
|
1536
|
+
const ignored = [];
|
|
1537
|
+
for (let i = 0; i < fields.length; i += 4) {
|
|
1538
|
+
const [source, line, pattern, path] = fields.slice(i, i + 4);
|
|
1539
|
+
if (pattern.startsWith("!"))
|
|
1540
|
+
continue;
|
|
1541
|
+
const lineNumber = Number.parseInt(line, 10);
|
|
1542
|
+
if (!Number.isFinite(lineNumber))
|
|
1543
|
+
return null;
|
|
1544
|
+
ignored.push({ path, pattern, source, line: lineNumber });
|
|
1545
|
+
}
|
|
1546
|
+
if (ignored.length === 0)
|
|
1547
|
+
return null;
|
|
1548
|
+
if (ignored.every((entry) => isAbsolute(entry.source)))
|
|
1549
|
+
return ignored;
|
|
1550
|
+
if (git(targetRepoRoot, ["rev-parse", "--show-prefix"]) === null)
|
|
1551
|
+
return ignored;
|
|
1552
|
+
const toplevel = git(targetRepoRoot, ["rev-parse", "--show-toplevel"]);
|
|
1553
|
+
if (toplevel === null)
|
|
1554
|
+
return null;
|
|
1555
|
+
return ignored.map((entry) => isAbsolute(entry.source) ? entry : { ...entry, source: join3(toplevel, entry.source) });
|
|
1556
|
+
}
|
|
1239
1557
|
function mintOrReadIdentity(targetRepoRoot) {
|
|
1240
1558
|
const path = join3(targetRepoRoot, ".halfcycle", "project.json");
|
|
1241
1559
|
if (existsSync(path)) {
|
|
@@ -1619,6 +1937,13 @@ function runBootstrapScan(targetRepo) {
|
|
|
1619
1937
|
}
|
|
1620
1938
|
|
|
1621
1939
|
// dist/install.js
|
|
1940
|
+
function renderNotShared(finding) {
|
|
1941
|
+
const rule = `rule "${finding.pattern}" in ${finding.source}, line ${finding.line}`;
|
|
1942
|
+
if (finding.others === 0) {
|
|
1943
|
+
return `[halfcycle] Not shared: git ignores ${finding.path} (${rule}), which Halfcycle set up here, so it will not reach anyone else who gets a copy of this project.`;
|
|
1944
|
+
}
|
|
1945
|
+
return `[halfcycle] Not shared: git ignores ${finding.path} (${rule}) and ${finding.others} other file${finding.others === 1 ? "" : "s"} Halfcycle set up here, so they will not reach anyone else who gets a copy of this project.`;
|
|
1946
|
+
}
|
|
1622
1947
|
var ZONE_B_DIR = ".workbench";
|
|
1623
1948
|
var __filename = fileURLToPath(import.meta.url);
|
|
1624
1949
|
var __dirname = dirname2(__filename);
|
|
@@ -1670,18 +1995,12 @@ var BUNDLE_PIN_REL = ".halfcycle/bundle.json";
|
|
|
1670
1995
|
var PROJECT_IDENTITY_REL = ".halfcycle/project.json";
|
|
1671
1996
|
var CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
1672
1997
|
var CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
1673
|
-
var SETTINGS_REL = ".claude/settings.json";
|
|
1674
1998
|
var GITIGNORE_REL = ".gitignore";
|
|
1675
1999
|
function commandStubRel(name) {
|
|
1676
2000
|
return `.claude/commands/${name}.md`;
|
|
1677
2001
|
}
|
|
1678
2002
|
var RETIRED_WRITE_SET_FILES = [];
|
|
1679
|
-
var RETIRED_HOOK_COMMANDS = [];
|
|
1680
2003
|
var RETIRED_DENY_PATTERNS = [];
|
|
1681
|
-
function installerHookCommands() {
|
|
1682
|
-
const settings = JSON.parse(generateSettingsJson());
|
|
1683
|
-
return Object.values(settings.hooks ?? {}).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
|
|
1684
|
-
}
|
|
1685
2004
|
function closedWriteSet() {
|
|
1686
2005
|
const files = /* @__PURE__ */ new Set([
|
|
1687
2006
|
...(readPluginManifest().commands ?? []).map((cmd) => commandStubRel(cmd.name)),
|
|
@@ -1707,7 +2026,7 @@ function closedWriteSet() {
|
|
|
1707
2026
|
merged,
|
|
1708
2027
|
dirs,
|
|
1709
2028
|
ownedDirs,
|
|
1710
|
-
hookCommands:
|
|
2029
|
+
hookCommands: new Set(HALFCYCLE_HOOK_COMMANDS),
|
|
1711
2030
|
denyRuleSha256: new Set([...DENY_PATTERNS, ...RETIRED_DENY_PATTERNS].map((rule) => sha256Hex(rule))),
|
|
1712
2031
|
gitignoreLines: /* @__PURE__ */ new Set([GITIGNORE_HEADER, ...REQUIRED_GITIGNORE_ENTRIES, LEGACY_ENV_LOCAL_REL])
|
|
1713
2032
|
};
|
|
@@ -1792,6 +2111,54 @@ function assertManifestInWriteSet(manifest, writeSet) {
|
|
|
1792
2111
|
throw new Error(`[bundle install] the install record names paths outside the write set: ${strays.join(", ")}`);
|
|
1793
2112
|
}
|
|
1794
2113
|
}
|
|
2114
|
+
function findNotShared(targetRepo, manifest, collidedPaths) {
|
|
2115
|
+
const merged = [
|
|
2116
|
+
SETTINGS_REL,
|
|
2117
|
+
...manifest.mcp !== void 0 ? [MCP_REGISTRATION_REL] : [],
|
|
2118
|
+
...manifest.gitignore.created || manifest.gitignore.appended.length > 0 ? [GITIGNORE_REL] : []
|
|
2119
|
+
];
|
|
2120
|
+
const collided = new Set(collidedPaths);
|
|
2121
|
+
const dirVerdicts = /* @__PURE__ */ new Map();
|
|
2122
|
+
const travelsThroughPlainDirs = (rel) => {
|
|
2123
|
+
for (let dir = posix.dirname(rel); dir !== "."; dir = posix.dirname(dir)) {
|
|
2124
|
+
let plain = dirVerdicts.get(dir);
|
|
2125
|
+
if (plain === void 0) {
|
|
2126
|
+
const abs = join5(targetRepo, dir);
|
|
2127
|
+
plain = !lstatOrNull(abs)?.isSymbolicLink() && lstatOrNull(join5(abs, ".git")) === null;
|
|
2128
|
+
dirVerdicts.set(dir, plain);
|
|
2129
|
+
}
|
|
2130
|
+
if (!plain)
|
|
2131
|
+
return false;
|
|
2132
|
+
}
|
|
2133
|
+
return true;
|
|
2134
|
+
};
|
|
2135
|
+
const paths = [
|
|
2136
|
+
.../* @__PURE__ */ new Set([
|
|
2137
|
+
...manifest.files.filter((f) => !("perMachine" in f)).map((f) => f.path),
|
|
2138
|
+
...merged,
|
|
2139
|
+
INSTALL_MANIFEST_REL
|
|
2140
|
+
])
|
|
2141
|
+
].filter((rel) => !collided.has(rel)).filter((rel) => lstatOrNull(join5(targetRepo, rel)) !== null).filter(travelsThroughPlainDirs).sort();
|
|
2142
|
+
const ignored = readIgnoredPaths(targetRepo, paths);
|
|
2143
|
+
if (ignored === null)
|
|
2144
|
+
return null;
|
|
2145
|
+
const sorted = [...ignored].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
2146
|
+
const named = sorted.find((entry) => entry.path === SETTINGS_REL) ?? sorted[0];
|
|
2147
|
+
return {
|
|
2148
|
+
path: named.path,
|
|
2149
|
+
pattern: named.pattern,
|
|
2150
|
+
source: named.source,
|
|
2151
|
+
line: named.line,
|
|
2152
|
+
others: sorted.length - 1
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
function lstatOrNull(path) {
|
|
2156
|
+
try {
|
|
2157
|
+
return lstatSync(path);
|
|
2158
|
+
} catch {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
1795
2162
|
function isPlainObject(value) {
|
|
1796
2163
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1797
2164
|
}
|
|
@@ -1906,9 +2273,15 @@ var DENY_PATTERNS = [
|
|
|
1906
2273
|
"Bash(git push --no-verify:*)",
|
|
1907
2274
|
"Bash(git commit --no-verify:*)"
|
|
1908
2275
|
];
|
|
1909
|
-
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
1910
2276
|
var USER_PROMPT_REMINDER_SENTENCE = "Where this project stands, and what order to work in, is recorded in the Halfcycle service and nowhere in this repository \u2014 call halfcycle_resolve before you answer or plan, because the files here will look complete and current and still be wrong about position.";
|
|
1911
2277
|
function generateSettingsJson() {
|
|
2278
|
+
const hooks = {};
|
|
2279
|
+
for (const { event, matcher, timeout, command } of HALFCYCLE_HOOK_WIRING) {
|
|
2280
|
+
(hooks[event] ??= []).push({
|
|
2281
|
+
...matcher === null ? {} : { matcher },
|
|
2282
|
+
hooks: [{ type: "command", command, timeout }]
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
1912
2285
|
const settings = {
|
|
1913
2286
|
$schema: "https://json.schemastore.org/claude-code-settings.json",
|
|
1914
2287
|
permissions: {
|
|
@@ -1916,88 +2289,10 @@ function generateSettingsJson() {
|
|
|
1916
2289
|
// and is the machine owner's grant to make, not ours.
|
|
1917
2290
|
deny: [...DENY_PATTERNS]
|
|
1918
2291
|
},
|
|
1919
|
-
hooks
|
|
1920
|
-
SessionStart: [
|
|
1921
|
-
{
|
|
1922
|
-
hooks: [
|
|
1923
|
-
{
|
|
1924
|
-
type: "command",
|
|
1925
|
-
command: "bash ./.claude/hooks/session-start-marker.sh",
|
|
1926
|
-
timeout: 5
|
|
1927
|
-
}
|
|
1928
|
-
]
|
|
1929
|
-
},
|
|
1930
|
-
{
|
|
1931
|
-
hooks: [
|
|
1932
|
-
{
|
|
1933
|
-
type: "command",
|
|
1934
|
-
command: "bash ./.claude/hooks/guard-runner.sh refresh-credential",
|
|
1935
|
-
timeout: 10
|
|
1936
|
-
}
|
|
1937
|
-
]
|
|
1938
|
-
}
|
|
1939
|
-
],
|
|
1940
|
-
UserPromptSubmit: [
|
|
1941
|
-
{
|
|
1942
|
-
hooks: [
|
|
1943
|
-
{
|
|
1944
|
-
type: "command",
|
|
1945
|
-
command: "bash ./.claude/hooks/user-prompt-reminder.sh",
|
|
1946
|
-
timeout: 5
|
|
1947
|
-
}
|
|
1948
|
-
]
|
|
1949
|
-
}
|
|
1950
|
-
],
|
|
1951
|
-
PostToolUse: [
|
|
1952
|
-
{
|
|
1953
|
-
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
1954
|
-
hooks: [
|
|
1955
|
-
{
|
|
1956
|
-
type: "command",
|
|
1957
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
|
|
1958
|
-
timeout: 60
|
|
1959
|
-
}
|
|
1960
|
-
]
|
|
1961
|
-
}
|
|
1962
|
-
],
|
|
1963
|
-
Stop: [
|
|
1964
|
-
{
|
|
1965
|
-
hooks: [
|
|
1966
|
-
{
|
|
1967
|
-
type: "command",
|
|
1968
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
1969
|
-
timeout: 60
|
|
1970
|
-
}
|
|
1971
|
-
]
|
|
1972
|
-
}
|
|
1973
|
-
],
|
|
1974
|
-
SubagentStop: [
|
|
1975
|
-
{
|
|
1976
|
-
hooks: [
|
|
1977
|
-
{
|
|
1978
|
-
type: "command",
|
|
1979
|
-
command: "bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
|
|
1980
|
-
timeout: 60
|
|
1981
|
-
}
|
|
1982
|
-
]
|
|
1983
|
-
}
|
|
1984
|
-
],
|
|
1985
|
-
SessionEnd: [
|
|
1986
|
-
{
|
|
1987
|
-
hooks: [
|
|
1988
|
-
{
|
|
1989
|
-
type: "command",
|
|
1990
|
-
command: "bash ./.claude/hooks/session-end-marker.sh",
|
|
1991
|
-
timeout: 5
|
|
1992
|
-
}
|
|
1993
|
-
]
|
|
1994
|
-
}
|
|
1995
|
-
]
|
|
1996
|
-
}
|
|
2292
|
+
hooks
|
|
1997
2293
|
};
|
|
1998
2294
|
return JSON.stringify(settings, null, 2) + "\n";
|
|
1999
2295
|
}
|
|
2000
|
-
var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
2001
2296
|
var GENERATED_GUARD_RUNNER_HEADER = "# Governance hook wrapper \u2014 generated by";
|
|
2002
2297
|
function generateGuardRunnerWrapper() {
|
|
2003
2298
|
return `#!/usr/bin/env bash
|
|
@@ -2219,14 +2514,16 @@ var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
|
2219
2514
|
function isTrackedAndClean(targetRepo, relPath) {
|
|
2220
2515
|
try {
|
|
2221
2516
|
execFileSync2("git", ["-C", targetRepo, "ls-files", "--error-unmatch", "--", relPath], {
|
|
2222
|
-
stdio: "ignore"
|
|
2517
|
+
stdio: "ignore",
|
|
2518
|
+
env: gitEnvForNamedRepo()
|
|
2223
2519
|
});
|
|
2224
2520
|
} catch {
|
|
2225
2521
|
return false;
|
|
2226
2522
|
}
|
|
2227
2523
|
try {
|
|
2228
2524
|
execFileSync2("git", ["-C", targetRepo, "diff", "--quiet", "HEAD", "--", relPath], {
|
|
2229
|
-
stdio: "ignore"
|
|
2525
|
+
stdio: "ignore",
|
|
2526
|
+
env: gitEnvForNamedRepo()
|
|
2230
2527
|
});
|
|
2231
2528
|
} catch {
|
|
2232
2529
|
return false;
|
|
@@ -2842,6 +3139,7 @@ async function install(options) {
|
|
|
2842
3139
|
const installManifest = finishManifest(draft);
|
|
2843
3140
|
assertManifestInWriteSet(installManifest, writeSet);
|
|
2844
3141
|
recordOwned(report, join5(targetRepo, INSTALL_MANIFEST_REL), targetRepo, serializeManifest(installManifest), INSTALL_MANIFEST_REL);
|
|
3142
|
+
const notShared = findNotShared(targetRepo, installManifest, report.collidedPaths);
|
|
2845
3143
|
return {
|
|
2846
3144
|
version: manifest.version,
|
|
2847
3145
|
writtenPaths: report.writtenPaths,
|
|
@@ -2850,7 +3148,8 @@ async function install(options) {
|
|
|
2850
3148
|
collidedPaths: report.collidedPaths,
|
|
2851
3149
|
replacedPaths: report.replacedPaths,
|
|
2852
3150
|
bootstrapScanRan: scanResult.ran,
|
|
2853
|
-
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null
|
|
3151
|
+
legacyCredentialKept: migration.outcome === "kept" ? { keys: migration.unbacked, missing: migration.missingForAdoption } : null,
|
|
3152
|
+
notShared
|
|
2854
3153
|
};
|
|
2855
3154
|
}
|
|
2856
3155
|
|
|
@@ -2888,12 +3187,12 @@ function escapeHtml(value) {
|
|
|
2888
3187
|
}
|
|
2889
3188
|
function completionPage(result, engagementId) {
|
|
2890
3189
|
const approved = result === LOOPBACK_RESULT.APPROVED;
|
|
2891
|
-
const
|
|
3190
|
+
const headline2 = approved ? "Signed in. You can close this tab and return to your terminal." : "Sign-in declined. Nothing was created. You can close this tab.";
|
|
2892
3191
|
const onward = approved ? onwardTarget(engagementId) : null;
|
|
2893
3192
|
const href = onward === null ? "" : escapeHtml(onward.href);
|
|
2894
3193
|
const refresh = onward === null ? "" : `<meta http-equiv="refresh" content="${COMPLETION_REDIRECT_SECONDS};url=${href}">`;
|
|
2895
3194
|
const successLink = onward === null ? "" : `<p><a href="${href}" style="color:#19d3ff">${escapeHtml(onward.label)}</a></p>`;
|
|
2896
|
-
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">' + refresh + `<title>Halfcycle</title></head><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;color:#fafafa;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif"><div style="max-width:26rem;padding:2rem;text-align:center"><p>${
|
|
3195
|
+
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="referrer" content="no-referrer">' + refresh + `<title>Halfcycle</title></head><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;color:#fafafa;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif"><div style="max-width:26rem;padding:2rem;text-align:center"><p>${headline2}</p>${successLink}</div></body></html>`;
|
|
2897
3196
|
}
|
|
2898
3197
|
function isLoopbackPeer(remoteAddress) {
|
|
2899
3198
|
if (remoteAddress === void 0)
|
|
@@ -4119,424 +4418,472 @@ async function probeMcpOrigin(mcpUrl) {
|
|
|
4119
4418
|
return { reached: true, serverName: name };
|
|
4120
4419
|
}
|
|
4121
4420
|
|
|
4122
|
-
// dist/build-record/
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
const seed = {
|
|
4126
|
-
patternRef: fired.patternRef,
|
|
4127
|
-
severity: fired.severity
|
|
4128
|
-
};
|
|
4129
|
-
if (includeExplanation && fired.explanation)
|
|
4130
|
-
seed.explanation = fired.explanation;
|
|
4131
|
-
return seed;
|
|
4132
|
-
}
|
|
4133
|
-
|
|
4134
|
-
// dist/build-record/sources.js
|
|
4135
|
-
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "node:fs";
|
|
4136
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
4137
|
-
import { join as join8 } from "node:path";
|
|
4421
|
+
// dist/build-record/write.js
|
|
4422
|
+
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeSync } from "node:fs";
|
|
4423
|
+
import { basename as basename2, join as join6, resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
|
|
4138
4424
|
|
|
4139
|
-
// dist/build-record/
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
}
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
}
|
|
4155
|
-
function resolveCloseAtHead(repoRoot) {
|
|
4156
|
-
try {
|
|
4157
|
-
const closeCommit = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--short", "HEAD"], {
|
|
4158
|
-
encoding: "utf-8",
|
|
4159
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
4160
|
-
}).trim();
|
|
4161
|
-
const closedDate = execFileSync3("git", ["-C", repoRoot, "show", "-s", "--format=%cs", "HEAD"], {
|
|
4162
|
-
encoding: "utf-8",
|
|
4163
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
4164
|
-
}).trim();
|
|
4165
|
-
if (closeCommit === "" || closedDate === "")
|
|
4166
|
-
return null;
|
|
4167
|
-
return { closeCommit, closedDate };
|
|
4168
|
-
} catch {
|
|
4169
|
-
return null;
|
|
4425
|
+
// dist/build-record/render-v2.js
|
|
4426
|
+
var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
|
|
4427
|
+
var MARKDOWN_SPECIAL = /* @__PURE__ */ new Set(["\\", "`", "*", "_", "[", "]", "<", "|", "~", "#", "&", "$"]);
|
|
4428
|
+
function printRecordText(text) {
|
|
4429
|
+
if (text === "")
|
|
4430
|
+
return "(blank)";
|
|
4431
|
+
let out = "";
|
|
4432
|
+
for (const ch of text) {
|
|
4433
|
+
if (HIDDEN.test(ch)) {
|
|
4434
|
+
out += `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
4435
|
+
} else if (MARKDOWN_SPECIAL.has(ch)) {
|
|
4436
|
+
out += `\\${ch}`;
|
|
4437
|
+
} else {
|
|
4438
|
+
out += ch;
|
|
4439
|
+
}
|
|
4170
4440
|
}
|
|
4441
|
+
return out;
|
|
4171
4442
|
}
|
|
4172
|
-
function
|
|
4173
|
-
let
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
path,
|
|
4184
|
-
reason: `the commit could not be read from ${repoRoot} \u2014 there is no git available, no repository there, or no commit in it yet`
|
|
4185
|
-
};
|
|
4186
|
-
}
|
|
4187
|
-
const record2 = {
|
|
4188
|
-
format: CLOSE_RECORD_FORMAT,
|
|
4189
|
-
phase,
|
|
4190
|
-
closeCommit: at.closeCommit,
|
|
4191
|
-
closedDate: at.closedDate
|
|
4443
|
+
function makePrinter() {
|
|
4444
|
+
let hidden = false;
|
|
4445
|
+
return {
|
|
4446
|
+
text(value) {
|
|
4447
|
+
if (!hidden && HIDDEN.test(value))
|
|
4448
|
+
hidden = true;
|
|
4449
|
+
return printRecordText(value);
|
|
4450
|
+
},
|
|
4451
|
+
sawHidden() {
|
|
4452
|
+
return hidden;
|
|
4453
|
+
}
|
|
4192
4454
|
};
|
|
4193
|
-
try {
|
|
4194
|
-
mkdirSync5(dirname3(path), { recursive: true });
|
|
4195
|
-
writeFileSync5(path, JSON.stringify(record2, null, 2) + "\n", "utf-8");
|
|
4196
|
-
} catch (err) {
|
|
4197
|
-
return { recorded: false, path, reason: err instanceof Error ? err.message : String(err) };
|
|
4198
|
-
}
|
|
4199
|
-
return { recorded: true, path, closeCommit: at.closeCommit, closedDate: at.closedDate };
|
|
4200
4455
|
}
|
|
4201
|
-
function
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
if (
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4456
|
+
function counted(n, one, many) {
|
|
4457
|
+
return `${String(n)} ${n === 1 ? one : many}`;
|
|
4458
|
+
}
|
|
4459
|
+
var TITLE_PREFIX = "# Build Record: ";
|
|
4460
|
+
var HEADER_WHAT = "This is the Build Record for one phase of a software project run with the Halfcycle method. A phase is a stage of the work with an agreed finish line; this record says how this one ended.";
|
|
4461
|
+
var HEADER_REGISTERS = "Two kinds of content are kept apart here. Lines under **Observed by Halfcycle** come from Halfcycle's own records, made as the work happened. Lines under **Stated by the client** are quoted, and are the client's own word: Halfcycle did not check them. Halfcycle witnessed the method being run. The client attests the code.";
|
|
4462
|
+
var HEADER_KEPT = "Halfcycle derived this record and kept it when the phase closed, and changes it afterwards in one way only: if an account that closed this phase is later deleted, its email is removed, and so is the name given at any close Halfcycle recorded as made by that account. The client's Halfcycle command-line tool wrote this file from that kept copy when the phase closed or when it was fetched again, so a later deletion changes the kept copy and reaches this file at the next fetch; the same record, as data, is in the .json file of the same name beside this one. If the phase is closed again, this file is replaced by a new record that describes every close, this one included.";
|
|
4463
|
+
var HEADER_HIDDEN = "Some text in this record contains characters that do not print. Each is shown as U+ and its code of four or more hex digits; the .json file holds the text exactly.";
|
|
4464
|
+
var OBSERVED_LABEL = "**Observed by Halfcycle**";
|
|
4465
|
+
var STATED_LABEL = "**Stated by the client** \u2014 Halfcycle did not check these.";
|
|
4466
|
+
var MISMATCHED_HALVES = "This Build Record could not be written: its two parts describe different closes.";
|
|
4467
|
+
function renderChecks(checks, p) {
|
|
4468
|
+
const blocks = ["## Checks during this phase", OBSERVED_LABEL];
|
|
4469
|
+
if (checks.state === "not-observed") {
|
|
4470
|
+
blocks.push("**Nothing was observed.** Halfcycle has no record of any check running during this phase, so this record cannot say whether the work had problems. This is not a clean result.");
|
|
4471
|
+
return blocks;
|
|
4472
|
+
}
|
|
4473
|
+
const runs = counted(checks.runs, "check run", "check runs");
|
|
4474
|
+
if (checks.state === "nothing-fired") {
|
|
4475
|
+
blocks.push(`**Checks ran and found nothing.** Halfcycle recorded ${runs} during this phase, and no check found a problem.`);
|
|
4476
|
+
} else {
|
|
4477
|
+
blocks.push(`**Checks found problems.** Halfcycle recorded ${runs} during this phase. Each check that found something is listed by what it checks, in plain words, then how serious it is, what it found, and the name your terminal prints for it.`);
|
|
4478
|
+
blocks.push(renderFired(checks.fired, p));
|
|
4218
4479
|
}
|
|
4219
|
-
if (
|
|
4220
|
-
|
|
4480
|
+
if (checks.couldNotRun.length === 0) {
|
|
4481
|
+
blocks.push("No check reported that it could not run.");
|
|
4482
|
+
} else {
|
|
4483
|
+
blocks.push("**Some checks could not run.** They neither passed nor failed during this phase:");
|
|
4484
|
+
blocks.push(renderCouldNotRun(checks.couldNotRun, p));
|
|
4221
4485
|
}
|
|
4222
|
-
return
|
|
4223
|
-
format: CLOSE_RECORD_FORMAT,
|
|
4224
|
-
phase: record2.phase,
|
|
4225
|
-
closeCommit: commit,
|
|
4226
|
-
closedDate: date
|
|
4227
|
-
};
|
|
4486
|
+
return blocks;
|
|
4228
4487
|
}
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4488
|
+
function renderFired(fired, p) {
|
|
4489
|
+
const lines = [];
|
|
4490
|
+
for (const entry of fired) {
|
|
4491
|
+
lines.push(entry.description === null ? "- Check: A check found something here, and no plain-language description of it is on record." : `- Check: ${p.text(entry.description)}`);
|
|
4492
|
+
lines.push(` - Seriousness: ${seriousness(entry.severity)}`);
|
|
4493
|
+
lines.push(` - Found: ${counted(entry.occurrences, "time", "times")} during this phase.`);
|
|
4494
|
+
lines.push(` - What it found: ${entry.explanation === null ? "Not included in this record." : p.text(entry.explanation)}`);
|
|
4495
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
4496
|
+
}
|
|
4497
|
+
return lines.join("\n");
|
|
4237
4498
|
}
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4499
|
+
function seriousness(severity) {
|
|
4500
|
+
switch (severity) {
|
|
4501
|
+
case "block":
|
|
4502
|
+
return "serious, it blocks the work.";
|
|
4503
|
+
case "warn":
|
|
4504
|
+
return "a warning.";
|
|
4505
|
+
case "info":
|
|
4506
|
+
return "for information only.";
|
|
4507
|
+
}
|
|
4244
4508
|
}
|
|
4245
|
-
function
|
|
4246
|
-
|
|
4509
|
+
function renderCouldNotRun(entries, p) {
|
|
4510
|
+
const lines = [];
|
|
4511
|
+
for (const entry of entries) {
|
|
4512
|
+
lines.push(entry.description === null ? "- Check: A check could not run here, and no plain-language description of it is on record." : `- Check: ${p.text(entry.description)}`);
|
|
4513
|
+
lines.push(` - Why it could not run: ${p.text(entry.reason)}`);
|
|
4514
|
+
lines.push(` - The name your terminal prints for this check: ${p.text(entry.check)}`);
|
|
4515
|
+
}
|
|
4516
|
+
return lines.join("\n");
|
|
4247
4517
|
}
|
|
4248
|
-
function
|
|
4249
|
-
const
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4518
|
+
function headline(observed, attested) {
|
|
4519
|
+
const met = attested.decision === "done-when-met";
|
|
4520
|
+
switch (observed.crossed.kind) {
|
|
4521
|
+
case "none":
|
|
4522
|
+
return met ? "**Closed cleanly.** Halfcycle saw nothing outstanding, and the person closing the phase stated its finish conditions were met." : "**Closed by override.** Halfcycle saw nothing outstanding, and the person closing the phase chose to override rather than state its finish conditions were met.";
|
|
4523
|
+
case "unfinished-steps":
|
|
4524
|
+
case "clean-over-findings":
|
|
4525
|
+
return met ? "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase stated its finish conditions were met anyway." : "**Closed over outstanding work.** Halfcycle saw work still outstanding, and the person closing the phase chose to override it.";
|
|
4526
|
+
case "not-recorded":
|
|
4527
|
+
return met ? "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase stated its finish conditions were met." : "**Outstanding work not recorded.** Halfcycle has no record of what was outstanding at this close. The person closing the phase chose to override.";
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
function closedAtText(closedAt, p) {
|
|
4531
|
+
const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?Z$/.exec(closedAt);
|
|
4532
|
+
return m === null ? p.text(closedAt) : `${m[1]} ${m[2]} UTC`;
|
|
4533
|
+
}
|
|
4534
|
+
function signedInAs(closedBy, p) {
|
|
4535
|
+
switch (closedBy.kind) {
|
|
4536
|
+
case "account":
|
|
4537
|
+
return closedBy.email === null ? "an account with no email address on file" : `the account ${p.text(closedBy.email)}`;
|
|
4538
|
+
case "no-account":
|
|
4539
|
+
return "a credential not linked to any person's account";
|
|
4540
|
+
case "account-removed":
|
|
4541
|
+
return "an account that has since been deleted";
|
|
4542
|
+
case "not-recorded":
|
|
4543
|
+
return "not recorded for this close";
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
function outstanding(crossed, p) {
|
|
4547
|
+
switch (crossed.kind) {
|
|
4548
|
+
case "none":
|
|
4549
|
+
return ["- Outstanding work Halfcycle could see: none."];
|
|
4550
|
+
case "not-recorded":
|
|
4551
|
+
return ["- Outstanding work Halfcycle could see: not recorded for this close."];
|
|
4552
|
+
case "clean-over-findings":
|
|
4553
|
+
return [
|
|
4554
|
+
`- Outstanding work Halfcycle could see: ${counted(crossed.findings, "problem", "problems")} recorded against this phase.`
|
|
4555
|
+
];
|
|
4556
|
+
case "unfinished-steps":
|
|
4557
|
+
return [
|
|
4558
|
+
`- Outstanding work Halfcycle could see: ${counted(crossed.steps.length, "step", "steps")} of the method left unfinished. Each is shown by the method's own label for it:`,
|
|
4559
|
+
...crossed.steps.map((step) => ` - Step: ${p.text(step.stepId)} \u2014 ${step.state === "failed" ? "failed" : "set aside unfinished"}`)
|
|
4560
|
+
];
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
function machines(coverage) {
|
|
4564
|
+
const label = "- Machines signed in on this project at the close: ";
|
|
4565
|
+
if (coverage === null)
|
|
4566
|
+
return `${label}not recorded.`;
|
|
4567
|
+
if (coverage.enrolled === 0)
|
|
4568
|
+
return `${label}none.`;
|
|
4569
|
+
return `${label}${String(coverage.enrolled)}, of which ${String(coverage.conformant)} had Halfcycle's checks working as set up.`;
|
|
4570
|
+
}
|
|
4571
|
+
function walkLines(walk, p) {
|
|
4572
|
+
const label = "> - Acceptance walk (the finished work tried as a user would, by someone other than its builder): ";
|
|
4573
|
+
if (walk === null)
|
|
4574
|
+
return [`${label}none reported.`];
|
|
4575
|
+
const lines = [
|
|
4576
|
+
`${label}${walk.verdict === "clean" ? "reported as finding no problems." : "reported as finding problems."}`
|
|
4577
|
+
];
|
|
4578
|
+
walk.findings.forEach((finding, i) => {
|
|
4579
|
+
lines.push(`> - Finding ${String(i + 1)}: ${p.text(finding)}`);
|
|
4580
|
+
});
|
|
4581
|
+
if (walk.findingsNotShown > 0) {
|
|
4582
|
+
lines.push(`> - And ${counted(walk.findingsNotShown, "more finding", "more findings")} not shown here.`);
|
|
4253
4583
|
}
|
|
4254
|
-
return
|
|
4584
|
+
return lines;
|
|
4585
|
+
}
|
|
4586
|
+
function statedOrNot(value, p) {
|
|
4587
|
+
return value === null ? "Not stated by the client." : p.text(value);
|
|
4255
4588
|
}
|
|
4256
|
-
function
|
|
4257
|
-
const { phase } = record2;
|
|
4258
|
-
const companion = `${renderPhaseSegment(phase.id)}.json`;
|
|
4259
|
-
const guardsList = record2.seedGuards.map(severityLabel).join(", ");
|
|
4589
|
+
function renderStated(attested, p) {
|
|
4260
4590
|
const lines = [
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
""
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
`**Engagement:** ${record2.engagement} (${record2.engagementType})`,
|
|
4269
|
-
`**Phase:** ${phase.id} \u2014 ${phase.name}`,
|
|
4270
|
-
renderCloseLine(record2),
|
|
4271
|
-
`**Companion:** [\`${companion}\`](${companion})`,
|
|
4272
|
-
"",
|
|
4273
|
-
renderCloseProvenance(record2),
|
|
4274
|
-
"",
|
|
4275
|
-
"## What this phase proved",
|
|
4276
|
-
"",
|
|
4277
|
-
`${record2.thesis} **Thesis held: ${record2.thesisHeld ? "yes" : "no"}.**`,
|
|
4278
|
-
"",
|
|
4279
|
-
"## Delivered",
|
|
4280
|
-
"",
|
|
4281
|
-
`- **Packages:** ${record2.delivered.packages.join(", ")}`,
|
|
4282
|
-
`- **Services:** ${record2.delivered.services.join(", ")}`,
|
|
4283
|
-
`- **Tools:** ${record2.delivered.tools.join(", ")}`,
|
|
4284
|
-
`- **Dogfood:** ${record2.delivered.dogfood}`,
|
|
4285
|
-
"",
|
|
4286
|
-
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.",
|
|
4287
|
-
"",
|
|
4288
|
-
`## Acceptance (independent walk \u2014 walker ${record2.acceptance.walker})`,
|
|
4289
|
-
"",
|
|
4290
|
-
`**Verdict:** ${record2.acceptance.verdict} \xB7 **Findings:** ${record2.acceptance.findings}`,
|
|
4291
|
-
"",
|
|
4292
|
-
"| Done-when | Result | Evidence |",
|
|
4293
|
-
"|---|---|---|",
|
|
4294
|
-
renderDoneWhenRows(record2),
|
|
4295
|
-
""
|
|
4591
|
+
attested.decision === "done-when-met" ? "> - Decision: the phase's agreed finish conditions were met." : "> - Decision: close the phase by override, without stating its finish conditions were met.",
|
|
4592
|
+
`> - Name given: ${attested.actor === null ? "removed with the account that made this close" : p.text(attested.actor)}`,
|
|
4593
|
+
`> - Reason given: ${attested.reason === null ? "none." : p.text(attested.reason)}`,
|
|
4594
|
+
...walkLines(attested.walk, p),
|
|
4595
|
+
`> - Code closed at (commit): ${statedOrNot(attested.closeCommit, p)}`,
|
|
4596
|
+
`> - What the phase set out to prove: ${statedOrNot(attested.statements.thesis, p)}`,
|
|
4597
|
+
`> - What came out of it: ${statedOrNot(attested.statements.outcome, p)}`
|
|
4296
4598
|
];
|
|
4297
|
-
|
|
4298
|
-
|
|
4599
|
+
const deviations = attested.statements.deviations;
|
|
4600
|
+
if (deviations.length === 0) {
|
|
4601
|
+
lines.push("> - Changes from the plan: None stated by the client.");
|
|
4602
|
+
} else {
|
|
4603
|
+
lines.push("> - Changes from the plan:");
|
|
4604
|
+
deviations.forEach((deviation, i) => {
|
|
4605
|
+
lines.push(`> - Change ${String(i + 1)}: ${p.text(deviation)}`);
|
|
4606
|
+
});
|
|
4299
4607
|
}
|
|
4300
|
-
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"}`, "");
|
|
4301
4608
|
return lines.join("\n");
|
|
4302
4609
|
}
|
|
4610
|
+
function renderClose(heading, observed, attested, p) {
|
|
4611
|
+
return [
|
|
4612
|
+
heading,
|
|
4613
|
+
headline(observed, attested),
|
|
4614
|
+
OBSERVED_LABEL,
|
|
4615
|
+
[
|
|
4616
|
+
`- Closed at: ${closedAtText(observed.closedAt, p)}`,
|
|
4617
|
+
`- Signed in as: ${signedInAs(observed.closedBy, p)}`,
|
|
4618
|
+
...outstanding(observed.crossed, p),
|
|
4619
|
+
machines(observed.seatCoverage)
|
|
4620
|
+
].join("\n"),
|
|
4621
|
+
STATED_LABEL,
|
|
4622
|
+
renderStated(attested, p)
|
|
4623
|
+
];
|
|
4624
|
+
}
|
|
4625
|
+
function renderBuildRecordV2Markdown(record2) {
|
|
4626
|
+
const p = makePrinter();
|
|
4627
|
+
const attestedBySequence = /* @__PURE__ */ new Map();
|
|
4628
|
+
for (const close of record2.attested.closes)
|
|
4629
|
+
attestedBySequence.set(close.sequence, close);
|
|
4630
|
+
const observedSequences = new Set(record2.observed.closes.map((c) => c.sequence));
|
|
4631
|
+
if (attestedBySequence.size !== record2.attested.closes.length || observedSequences.size !== record2.observed.closes.length || attestedBySequence.size !== observedSequences.size || [...observedSequences].some((s) => !attestedBySequence.has(s))) {
|
|
4632
|
+
throw new Error(MISMATCHED_HALVES);
|
|
4633
|
+
}
|
|
4634
|
+
const phase = p.text(record2.phase);
|
|
4635
|
+
const closes = [...record2.observed.closes].sort((a, b) => a.sequence - b.sequence);
|
|
4636
|
+
const total = closes.length;
|
|
4637
|
+
const latest = closes[total - 1];
|
|
4638
|
+
const body = [
|
|
4639
|
+
`Phase: ${phase} (the name the client gave it)`,
|
|
4640
|
+
OBSERVED_LABEL,
|
|
4641
|
+
[
|
|
4642
|
+
`- Method version: ${p.text(record2.observed.methodVersion)}. This is the version of the Halfcycle method the project was run under; it does not change during a project.`,
|
|
4643
|
+
`- Halfcycle's reference for this project: ${p.text(record2.observed.engagementId)}`,
|
|
4644
|
+
`- Record format: ${p.text(record2.format)}`
|
|
4645
|
+
].join("\n"),
|
|
4646
|
+
total === 1 ? "This phase was closed once." : `This phase was closed ${String(total)} times. The latest close is described first; each earlier close follows, as it was recorded when it was made.`,
|
|
4647
|
+
...renderChecks(record2.observed.checks, p),
|
|
4648
|
+
...renderClose(total === 1 ? "## The close" : `## The latest close (close ${String(latest.sequence)} of ${String(total)})`, latest, attestedBySequence.get(latest.sequence), p)
|
|
4649
|
+
];
|
|
4650
|
+
if (total > 1) {
|
|
4651
|
+
body.push("## Earlier closes");
|
|
4652
|
+
for (const close of closes.slice(0, -1)) {
|
|
4653
|
+
body.push(...renderClose(`### Close ${String(close.sequence)} of ${String(total)}`, close, attestedBySequence.get(close.sequence), p));
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
const header = [`${TITLE_PREFIX}${phase}`, HEADER_WHAT, HEADER_REGISTERS, HEADER_KEPT];
|
|
4657
|
+
if (p.sawHidden())
|
|
4658
|
+
header.push(HEADER_HIDDEN);
|
|
4659
|
+
return `${[...header, ...body].join("\n\n")}
|
|
4660
|
+
`;
|
|
4661
|
+
}
|
|
4303
4662
|
|
|
4304
4663
|
// dist/build-record/write.js
|
|
4305
|
-
var BUILD_RECORD_DIR =
|
|
4306
|
-
function serialiseRecordJson(record2) {
|
|
4307
|
-
return JSON.stringify(record2, null, 2) + "\n";
|
|
4308
|
-
}
|
|
4664
|
+
var BUILD_RECORD_DIR = join6("docs", "build-records");
|
|
4309
4665
|
function assertInsideOutDir(outDir, candidate, identity) {
|
|
4310
4666
|
const rel = relative2(outDir, resolve2(candidate));
|
|
4311
|
-
const isPlainFilename = rel !== "" && rel !== ".." && !
|
|
4667
|
+
const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute2(rel) && rel === basename2(rel);
|
|
4312
4668
|
if (!isPlainFilename) {
|
|
4313
4669
|
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
4314
4670
|
}
|
|
4315
4671
|
}
|
|
4316
|
-
function
|
|
4317
|
-
const
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4672
|
+
function createExclusiveOnDisk(path, text) {
|
|
4673
|
+
const fd = openSync(path, "wx");
|
|
4674
|
+
try {
|
|
4675
|
+
writeSync(fd, text);
|
|
4676
|
+
} catch (err) {
|
|
4677
|
+
closeSync(fd);
|
|
4678
|
+
rmSync2(path, { force: true });
|
|
4679
|
+
throw err;
|
|
4321
4680
|
}
|
|
4322
|
-
|
|
4323
|
-
const jsonPath = join7(outDir, `${segment}.json`);
|
|
4324
|
-
const mdPath = join7(outDir, `${segment}.md`);
|
|
4325
|
-
assertInsideOutDir(outDir, jsonPath, identity);
|
|
4326
|
-
assertInsideOutDir(outDir, mdPath, identity);
|
|
4327
|
-
mkdirSync6(outDir, { recursive: true });
|
|
4328
|
-
writeFileSync6(jsonPath, serialiseRecordJson(record2), "utf-8");
|
|
4329
|
-
writeFileSync6(mdPath, renderBuildRecordMarkdown(record2), "utf-8");
|
|
4330
|
-
return { jsonPath, mdPath };
|
|
4681
|
+
closeSync(fd);
|
|
4331
4682
|
}
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
}
|
|
4683
|
+
function renameOnDisk(from, to) {
|
|
4684
|
+
renameSync(from, to);
|
|
4685
|
+
}
|
|
4686
|
+
var NODE_RECORD_PAIR_IO = {
|
|
4687
|
+
createExclusive: createExclusiveOnDisk,
|
|
4688
|
+
rename: renameOnDisk
|
|
4339
4689
|
};
|
|
4340
|
-
function
|
|
4341
|
-
|
|
4342
|
-
for (const run of runs) {
|
|
4343
|
-
if (!run.result)
|
|
4344
|
-
continue;
|
|
4345
|
-
for (const fired of run.result.guardsFired) {
|
|
4346
|
-
const seed = projectSeedGuard(
|
|
4347
|
-
fired,
|
|
4348
|
-
/* includeExplanation */
|
|
4349
|
-
false
|
|
4350
|
-
);
|
|
4351
|
-
const existing = byRef.get(seed.patternRef);
|
|
4352
|
-
if (!existing) {
|
|
4353
|
-
byRef.set(seed.patternRef, seed);
|
|
4354
|
-
} else if (existing.severity !== seed.severity) {
|
|
4355
|
-
byRef.set(seed.patternRef, pickHigherSeverity(existing, seed));
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
}
|
|
4359
|
-
return [...byRef.values()].sort((a, b) => a.patternRef.localeCompare(b.patternRef));
|
|
4690
|
+
function serialiseServedRecord(rawRecord) {
|
|
4691
|
+
return JSON.stringify(rawRecord, null, 2) + "\n";
|
|
4360
4692
|
}
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
return (SEVERITY_ORDINAL[a.severity] ?? -1) >= (SEVERITY_ORDINAL[b.severity] ?? -1) ? a : b;
|
|
4693
|
+
function isEexist(err) {
|
|
4694
|
+
return typeof err === "object" && err !== null && err.code === "EEXIST";
|
|
4364
4695
|
}
|
|
4365
|
-
function
|
|
4366
|
-
|
|
4367
|
-
return
|
|
4696
|
+
function readIfPresent(path) {
|
|
4697
|
+
try {
|
|
4698
|
+
return readFileSync6(path, "utf-8");
|
|
4699
|
+
} catch {
|
|
4700
|
+
return null;
|
|
4368
4701
|
}
|
|
4369
|
-
const record2 = parsed;
|
|
4370
|
-
if (record2["outcome"] !== void 0)
|
|
4371
|
-
return parsed;
|
|
4372
|
-
return {
|
|
4373
|
-
...record2,
|
|
4374
|
-
runId: record2["runId"] ?? syntheticRunId(record2),
|
|
4375
|
-
outcome: "evaluated"
|
|
4376
|
-
};
|
|
4377
4702
|
}
|
|
4378
|
-
function
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
].join("|");
|
|
4385
|
-
const hex = createHash3("sha256").update(`legacy-guard-eval-run:${key}`).digest("hex");
|
|
4386
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
4387
|
-
}
|
|
4388
|
-
function readGuardEvalLog(logDir, phaseId) {
|
|
4389
|
-
const logPath = join8(logDir, `${renderPhaseSegment(phaseId)}.jsonl`);
|
|
4390
|
-
if (!existsSync4(logPath)) {
|
|
4391
|
-
throw new MissingSourceError("guard-eval log", `no runner guard-eval log at ${logPath} for phase ${phaseId}`);
|
|
4392
|
-
}
|
|
4393
|
-
const raw = readFileSync6(logPath, "utf-8");
|
|
4394
|
-
const runs = [];
|
|
4395
|
-
for (const line of raw.split("\n")) {
|
|
4396
|
-
const trimmed = line.trim();
|
|
4397
|
-
if (trimmed === "")
|
|
4398
|
-
continue;
|
|
4399
|
-
const parsed = JSON.parse(trimmed);
|
|
4400
|
-
runs.push(guardEvalRunSchema.parse(normalizeLegacyLine(parsed)));
|
|
4703
|
+
function isOwnEarlierRecord(existingText, served) {
|
|
4704
|
+
let parsedJson;
|
|
4705
|
+
try {
|
|
4706
|
+
parsedJson = JSON.parse(existingText);
|
|
4707
|
+
} catch {
|
|
4708
|
+
return false;
|
|
4401
4709
|
}
|
|
4402
|
-
|
|
4710
|
+
const existing = buildRecordV2Schema.safeParse(parsedJson);
|
|
4711
|
+
if (!existing.success)
|
|
4712
|
+
return false;
|
|
4713
|
+
const old = existing.data;
|
|
4714
|
+
if (old.observed.engagementId !== served.observed.engagementId)
|
|
4715
|
+
return false;
|
|
4716
|
+
if (old.phase !== served.phase)
|
|
4717
|
+
return false;
|
|
4718
|
+
const oldCloses = old.observed.closes.length;
|
|
4719
|
+
const newCloses = served.observed.closes.length;
|
|
4720
|
+
if (oldCloses < newCloses)
|
|
4721
|
+
return true;
|
|
4722
|
+
if (oldCloses > newCloses)
|
|
4723
|
+
return false;
|
|
4724
|
+
const oldSequences = new Set(old.observed.closes.map((c) => c.sequence));
|
|
4725
|
+
return served.observed.closes.every((c) => oldSequences.has(c.sequence));
|
|
4403
4726
|
}
|
|
4404
|
-
function
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4727
|
+
function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_PAIR_IO) {
|
|
4728
|
+
if (!isValidPhaseIdentity(phase)) {
|
|
4729
|
+
throw new InvalidPhaseIdentityError(phase, BUILD_RECORD_DIR);
|
|
4730
|
+
}
|
|
4731
|
+
const outDir = resolve2(repoRoot, BUILD_RECORD_DIR);
|
|
4732
|
+
const segment = renderPhaseSegment(phase);
|
|
4733
|
+
const jsonPath = join6(outDir, `${segment}.json`);
|
|
4734
|
+
const mdPath = join6(outDir, `${segment}.md`);
|
|
4735
|
+
const jsonTmp = `${jsonPath}.tmp`;
|
|
4736
|
+
const mdTmp = `${mdPath}.tmp`;
|
|
4737
|
+
for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
|
|
4738
|
+
assertInsideOutDir(outDir, candidate, phase);
|
|
4739
|
+
}
|
|
4740
|
+
const jsonText = serialiseServedRecord(rawRecord);
|
|
4741
|
+
const mdText = renderBuildRecordV2Markdown(record2);
|
|
4742
|
+
mkdirSync5(outDir, { recursive: true });
|
|
4743
|
+
let createdJson = false;
|
|
4744
|
+
try {
|
|
4745
|
+
io.createExclusive(jsonPath, jsonText);
|
|
4746
|
+
createdJson = true;
|
|
4747
|
+
io.createExclusive(mdPath, mdText);
|
|
4748
|
+
return { kind: "created", jsonPath, mdPath };
|
|
4749
|
+
} catch (err) {
|
|
4750
|
+
if (createdJson)
|
|
4751
|
+
rmSync2(jsonPath, { force: true });
|
|
4752
|
+
if (!isEexist(err))
|
|
4753
|
+
throw err;
|
|
4408
4754
|
}
|
|
4409
|
-
const
|
|
4410
|
-
if (
|
|
4411
|
-
|
|
4755
|
+
const existingJson = readIfPresent(jsonPath);
|
|
4756
|
+
if (existingJson === null) {
|
|
4757
|
+
return { kind: "in-the-way", path: existsSync4(jsonPath) ? jsonPath : mdPath };
|
|
4412
4758
|
}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
throw new MissingSourceError("orchestration state", `task ${id} has no status`);
|
|
4417
|
-
}
|
|
4418
|
-
tasks[id] = { status: t.status };
|
|
4759
|
+
if (existingJson === jsonText) {
|
|
4760
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
4761
|
+
return { kind: "up-to-date", jsonPath, mdPath };
|
|
4419
4762
|
}
|
|
4420
|
-
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
if (!existsSync4(path))
|
|
4425
|
-
return { closeCommit: null, closedDate: null };
|
|
4426
|
-
let parsed;
|
|
4763
|
+
if (!isOwnEarlierRecord(existingJson, record2)) {
|
|
4764
|
+
return { kind: "in-the-way", path: jsonPath };
|
|
4765
|
+
}
|
|
4766
|
+
replaceOne(io, jsonTmp, jsonPath, jsonText);
|
|
4427
4767
|
try {
|
|
4428
|
-
|
|
4768
|
+
replaceOne(io, mdTmp, mdPath, mdText);
|
|
4429
4769
|
} catch (err) {
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4770
|
+
try {
|
|
4771
|
+
replaceOne(io, jsonTmp, jsonPath, existingJson);
|
|
4772
|
+
} catch {
|
|
4773
|
+
rmSync2(jsonPath, { force: true });
|
|
4774
|
+
}
|
|
4775
|
+
throw err;
|
|
4435
4776
|
}
|
|
4436
|
-
return {
|
|
4777
|
+
return { kind: "replaced", jsonPath, mdPath };
|
|
4437
4778
|
}
|
|
4438
|
-
function
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
req(narrated.phase?.name, "phase.name");
|
|
4447
|
-
assertValidPhaseIdentity(narrated.phase?.id, BUILD_RECORD_DIR);
|
|
4448
|
-
req(narrated.status, "status");
|
|
4449
|
-
req(narrated.thesis, "thesis");
|
|
4450
|
-
req(narrated.delivered, "delivered");
|
|
4451
|
-
req(narrated.acceptance, "acceptance");
|
|
4452
|
-
req(narrated.acceptance?.verdict, "acceptance.verdict");
|
|
4453
|
-
req(narrated.gatesEarnedKeep, "gatesEarnedKeep");
|
|
4454
|
-
req(narrated.instruments, "instruments");
|
|
4455
|
-
if (!Array.isArray(narrated.deviations)) {
|
|
4456
|
-
throw new MissingSourceError("narrated artefact", "deviations must be an array");
|
|
4457
|
-
}
|
|
4458
|
-
return narrated;
|
|
4459
|
-
}
|
|
4460
|
-
function loadSources(opts) {
|
|
4461
|
-
const orchestration = readOrchestrationState(opts.phasesDir, opts.phaseId);
|
|
4462
|
-
const guardEvalRuns = readGuardEvalLog(opts.guardEvalLogDir, opts.phaseId);
|
|
4463
|
-
const close = readPhaseClose(opts.repoRoot, opts.phaseId);
|
|
4464
|
-
const narrated = validateNarrated(opts.narrated);
|
|
4465
|
-
if (!Array.isArray(opts.touchedInvariants)) {
|
|
4466
|
-
throw new MissingSourceError("feature-spec Touches", "touchedInvariants must be an array");
|
|
4779
|
+
function replaceOne(io, tmp, finalPath, text) {
|
|
4780
|
+
rmSync2(tmp, { force: true });
|
|
4781
|
+
try {
|
|
4782
|
+
io.createExclusive(tmp, text);
|
|
4783
|
+
io.rename(tmp, finalPath);
|
|
4784
|
+
} catch (err) {
|
|
4785
|
+
rmSync2(tmp, { force: true });
|
|
4786
|
+
throw err;
|
|
4467
4787
|
}
|
|
4468
|
-
return {
|
|
4469
|
-
orchestration,
|
|
4470
|
-
narrated,
|
|
4471
|
-
close,
|
|
4472
|
-
guardEvalRuns,
|
|
4473
|
-
touchedInvariants: opts.touchedInvariants
|
|
4474
|
-
};
|
|
4475
4788
|
}
|
|
4476
4789
|
|
|
4477
|
-
// dist/build-record/
|
|
4478
|
-
|
|
4479
|
-
var
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
for (const dw of Object.values(acceptance.doneWhen)) {
|
|
4490
|
-
if (dw.result === "dropped")
|
|
4491
|
-
continue;
|
|
4492
|
-
if (dw.result !== "pass")
|
|
4493
|
-
return false;
|
|
4494
|
-
}
|
|
4495
|
-
return true;
|
|
4790
|
+
// dist/build-record/obtain.js
|
|
4791
|
+
import { relative as relative3 } from "node:path";
|
|
4792
|
+
var REFUSAL_FOLLOW_UP = {
|
|
4793
|
+
"still-open": "Nothing was written. The record is produced when the phase closes.",
|
|
4794
|
+
"reopened": "Nothing was written. A new record is produced when the phase closes again.",
|
|
4795
|
+
"never-opened": "Nothing was written. Check the name: it must match the phase name exactly as it was opened.",
|
|
4796
|
+
"never-closed": "Nothing was written.",
|
|
4797
|
+
"closed-before-records": "Nothing was written."
|
|
4798
|
+
};
|
|
4799
|
+
var REFUSAL_REASONS = phaseRecordRefusalReasonSchema.options;
|
|
4800
|
+
function buildRecordUrl(credential, phase) {
|
|
4801
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(phase)}/build-record`;
|
|
4496
4802
|
}
|
|
4497
|
-
function
|
|
4498
|
-
|
|
4803
|
+
function isMissingRoute(status, body) {
|
|
4804
|
+
if (status !== 404 || typeof body !== "object" || body === null)
|
|
4805
|
+
return false;
|
|
4806
|
+
const { error, message } = body;
|
|
4807
|
+
return error === "Not Found" && typeof message === "string" && /^Route GET .* not found\.$/s.test(message);
|
|
4499
4808
|
}
|
|
4500
|
-
function
|
|
4501
|
-
const
|
|
4809
|
+
async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_PAIR_IO) {
|
|
4810
|
+
const url = buildRecordUrl(credential, phase);
|
|
4811
|
+
let res;
|
|
4812
|
+
let text;
|
|
4813
|
+
try {
|
|
4814
|
+
res = await fetch(url, {
|
|
4815
|
+
method: "GET",
|
|
4816
|
+
headers: { authorization: `Bearer ${credential.token}` }
|
|
4817
|
+
});
|
|
4818
|
+
text = await res.text();
|
|
4819
|
+
} catch (err) {
|
|
4820
|
+
return {
|
|
4821
|
+
kind: "unavailable",
|
|
4822
|
+
why: `could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}.`
|
|
4823
|
+
};
|
|
4824
|
+
}
|
|
4825
|
+
let raw = null;
|
|
4826
|
+
try {
|
|
4827
|
+
raw = JSON.parse(text);
|
|
4828
|
+
} catch {
|
|
4829
|
+
raw = null;
|
|
4830
|
+
}
|
|
4831
|
+
const parsed = phaseRecordResponseSchema.safeParse(raw);
|
|
4832
|
+
if (parsed.success) {
|
|
4833
|
+
const answer = parsed.data;
|
|
4834
|
+
if (answer.phase !== phase) {
|
|
4835
|
+
return {
|
|
4836
|
+
kind: "unavailable",
|
|
4837
|
+
why: `the service answered for a different phase than the one asked for, so nothing was written.`
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
if (answer.outcome === "withheld")
|
|
4841
|
+
return { kind: "withheld", message: answer.message };
|
|
4842
|
+
if (answer.outcome === "refused") {
|
|
4843
|
+
return { kind: "refused", reason: answer.reason, message: answer.message };
|
|
4844
|
+
}
|
|
4845
|
+
const rawRecord = raw.record;
|
|
4846
|
+
let result;
|
|
4847
|
+
try {
|
|
4848
|
+
result = writeRecordPair(repoRoot, phase, rawRecord, answer.record, io);
|
|
4849
|
+
} catch (err) {
|
|
4850
|
+
return {
|
|
4851
|
+
kind: "unavailable",
|
|
4852
|
+
why: `the record arrived but could not be written: ${err instanceof Error ? err.message : String(err)}. Nothing was left half written.`
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
if (result.kind === "in-the-way") {
|
|
4856
|
+
return { kind: "exists", path: relative3(repoRoot, result.path) };
|
|
4857
|
+
}
|
|
4858
|
+
return {
|
|
4859
|
+
kind: "written",
|
|
4860
|
+
jsonPath: relative3(repoRoot, result.jsonPath),
|
|
4861
|
+
mdPath: relative3(repoRoot, result.mdPath)
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
if (isMissingRoute(res.status, raw)) {
|
|
4865
|
+
return { kind: "unavailable", why: "this Halfcycle service does not provide Build Records yet." };
|
|
4866
|
+
}
|
|
4867
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
4868
|
+
if (res.ok) {
|
|
4869
|
+
return {
|
|
4870
|
+
kind: "unavailable",
|
|
4871
|
+
why: `the service's answer was not a Build Record this version of the CLI can read, so nothing was written.`
|
|
4872
|
+
};
|
|
4873
|
+
}
|
|
4502
4874
|
return {
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
engagementType: narrated.engagementType,
|
|
4506
|
-
phase: { id: narrated.phase.id, name: narrated.phase.name },
|
|
4507
|
-
status: narrated.status,
|
|
4508
|
-
closedDate: close.closedDate,
|
|
4509
|
-
closeCommit: close.closeCommit,
|
|
4510
|
-
thesis: narrated.thesis,
|
|
4511
|
-
thesisHeld: deriveThesisHeld(narrated.acceptance),
|
|
4512
|
-
delivered: narrated.delivered,
|
|
4513
|
-
seedGuards: projectSeedGuards(sources.guardEvalRuns),
|
|
4514
|
-
acceptance: narrated.acceptance,
|
|
4515
|
-
gatesEarnedKeep: narrated.gatesEarnedKeep,
|
|
4516
|
-
instruments: narrated.instruments,
|
|
4517
|
-
invariantsExercised: deriveInvariantsExercised(sources.touchedInvariants),
|
|
4518
|
-
deviations: narrated.deviations,
|
|
4519
|
-
tasks: deriveTasks(orchestration)
|
|
4875
|
+
kind: "unavailable",
|
|
4876
|
+
why: `the service answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
4520
4877
|
};
|
|
4521
4878
|
}
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
const sources = loadSources({
|
|
4526
|
-
repoRoot: opts.repoRoot,
|
|
4527
|
-
phasesDir: opts.phasesDir,
|
|
4528
|
-
guardEvalLogDir: opts.guardEvalLogDir,
|
|
4529
|
-
phaseId: opts.phaseId,
|
|
4530
|
-
narrated: opts.narrated,
|
|
4531
|
-
touchedInvariants: opts.touchedInvariants
|
|
4532
|
-
});
|
|
4533
|
-
const record2 = assembleBuildRecord(sources);
|
|
4534
|
-
return writeBuildRecord(opts.repoRoot, record2);
|
|
4879
|
+
function refetchCommand(phase) {
|
|
4880
|
+
const quoted = /^[A-Za-z0-9._+-]+$/.test(phase) ? phase : `'${phase.replace(/'/g, `'\\''`)}'`;
|
|
4881
|
+
return `npx halfcycle@latest build-record ${quoted}`;
|
|
4535
4882
|
}
|
|
4536
4883
|
|
|
4537
4884
|
// dist/open-phase.js
|
|
4538
4885
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4539
|
-
import { join as
|
|
4886
|
+
import { join as join7 } from "node:path";
|
|
4540
4887
|
var OpenPhaseRefused = class extends Error {
|
|
4541
4888
|
status;
|
|
4542
4889
|
finding;
|
|
@@ -4557,7 +4904,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
|
|
|
4557
4904
|
if (pinned !== null)
|
|
4558
4905
|
assertEngagementId(pinned);
|
|
4559
4906
|
const fromStore = pinned === null ? null : readEngagementEnv(pinned, home);
|
|
4560
|
-
const looked = pinned === null ? `${
|
|
4907
|
+
const looked = pinned === null ? `${join7(repoRoot, ".halfcycle", "bundle.json")} (which names no engagement)` : engagementEnvPath(pinned, home);
|
|
4561
4908
|
const values = {};
|
|
4562
4909
|
for (const key of OPEN_PHASE_ENV_KEYS) {
|
|
4563
4910
|
const value = (env[key] ?? fromStore?.[key] ?? "").trim();
|
|
@@ -4574,7 +4921,7 @@ function readRepoCredential(repoRoot, env = process.env, home) {
|
|
|
4574
4921
|
}
|
|
4575
4922
|
function readPinnedEngagementId(repoRoot) {
|
|
4576
4923
|
try {
|
|
4577
|
-
const pin = JSON.parse(readFileSync7(
|
|
4924
|
+
const pin = JSON.parse(readFileSync7(join7(repoRoot, ".halfcycle", "bundle.json"), "utf-8"));
|
|
4578
4925
|
const id = pin[PIN_ENGAGEMENT_ID_FIELD];
|
|
4579
4926
|
return typeof id === "string" && id !== "" ? id : null;
|
|
4580
4927
|
} catch {
|
|
@@ -4638,6 +4985,35 @@ async function openPhase(credential, phase, entry, home) {
|
|
|
4638
4985
|
}
|
|
4639
4986
|
|
|
4640
4987
|
// dist/close-phase.js
|
|
4988
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4989
|
+
var NO_STATEMENTS = { thesis: null, outcome: null, deviations: [] };
|
|
4990
|
+
function readCloseCommit(repoRoot) {
|
|
4991
|
+
if (repoRoot === void 0) {
|
|
4992
|
+
return {
|
|
4993
|
+
read: false,
|
|
4994
|
+
reason: "no repository was named for this close, so there was nowhere to read a commit from"
|
|
4995
|
+
};
|
|
4996
|
+
}
|
|
4997
|
+
let out;
|
|
4998
|
+
try {
|
|
4999
|
+
out = execFileSync3("git", ["-C", repoRoot, "rev-parse", "HEAD"], {
|
|
5000
|
+
encoding: "utf-8",
|
|
5001
|
+
env: gitEnvForNamedRepo(),
|
|
5002
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5003
|
+
}).trim();
|
|
5004
|
+
} catch (err) {
|
|
5005
|
+
const missingGit = typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
5006
|
+
return {
|
|
5007
|
+
read: false,
|
|
5008
|
+
reason: missingGit ? "git is not available on this machine" : "this folder is not a git repository, or it has no commit yet"
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
5011
|
+
const checked = closeCommitSchema.safeParse(out);
|
|
5012
|
+
if (!checked.success) {
|
|
5013
|
+
return { read: false, reason: "git did not answer with a full commit hash" };
|
|
5014
|
+
}
|
|
5015
|
+
return { read: true, commit: checked.data };
|
|
5016
|
+
}
|
|
4641
5017
|
var ClosePhaseRefused = class extends Error {
|
|
4642
5018
|
status;
|
|
4643
5019
|
finding;
|
|
@@ -4650,14 +5026,28 @@ var ClosePhaseRefused = class extends Error {
|
|
|
4650
5026
|
this.name = "ClosePhaseRefused";
|
|
4651
5027
|
}
|
|
4652
5028
|
};
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
5029
|
+
var CloseRequestInvalid = class extends Error {
|
|
5030
|
+
constructor(message) {
|
|
5031
|
+
super(message);
|
|
5032
|
+
this.name = "CloseRequestInvalid";
|
|
4657
5033
|
}
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
5034
|
+
};
|
|
5035
|
+
async function closePhase(credential, phase, outcome, close, home, repoRoot, stated = NO_STATEMENTS) {
|
|
5036
|
+
const closeCommit = readCloseCommit(repoRoot);
|
|
5037
|
+
const body = {
|
|
5038
|
+
phase,
|
|
5039
|
+
acceptanceOutcome: outcome,
|
|
5040
|
+
close,
|
|
5041
|
+
statements: {
|
|
5042
|
+
thesis: stated.thesis,
|
|
5043
|
+
outcome: stated.outcome,
|
|
5044
|
+
deviations: stated.deviations,
|
|
5045
|
+
closeCommit: closeCommit.read ? closeCommit.commit : null
|
|
5046
|
+
}
|
|
5047
|
+
};
|
|
5048
|
+
const parsed = acceptPhaseRequestSchema.safeParse(body);
|
|
5049
|
+
if (!parsed.success) {
|
|
5050
|
+
throw new CloseRequestInvalid(`that close is not one this platform accepts, so nothing was sent: ${parsed.error.issues.map((i) => `${describeIssuePath(i.path)}: ${i.message}`).join("; ")}.`);
|
|
4661
5051
|
}
|
|
4662
5052
|
const url = `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/accept`;
|
|
4663
5053
|
let res;
|
|
@@ -4668,7 +5058,7 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot) {
|
|
|
4668
5058
|
"content-type": "application/json",
|
|
4669
5059
|
authorization: `Bearer ${credential.token}`
|
|
4670
5060
|
},
|
|
4671
|
-
body: JSON.stringify(
|
|
5061
|
+
body: JSON.stringify(body)
|
|
4672
5062
|
});
|
|
4673
5063
|
} catch (err) {
|
|
4674
5064
|
throw new Error(`[halfcycle] Could not reach the Halfcycle service at ${url}: ${err instanceof Error ? err.message : String(err)}. The phase was not closed.`);
|
|
@@ -4680,17 +5070,24 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot) {
|
|
|
4680
5070
|
const code = typeof body2?.error === "string" ? body2.error : void 0;
|
|
4681
5071
|
throw new ClosePhaseRefused(res.status, message, finding, code);
|
|
4682
5072
|
}
|
|
4683
|
-
const
|
|
4684
|
-
if (!
|
|
5073
|
+
const answer = await res.json().catch(() => null);
|
|
5074
|
+
if (!answer || typeof answer.engagementId !== "string" || typeof answer.status !== "string") {
|
|
4685
5075
|
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.`);
|
|
4686
5076
|
}
|
|
4687
|
-
const closeRecord = repoRoot === void 0 ? {
|
|
4688
|
-
recorded: false,
|
|
4689
|
-
path: null,
|
|
4690
|
-
reason: "no repository was named for this close, so there was nowhere to read a commit from"
|
|
4691
|
-
} : writeCloseRecord(repoRoot, phase);
|
|
4692
5077
|
const stampFailure = writePhaseStamp(credential.engagementId, null, home);
|
|
4693
|
-
return { engagementId:
|
|
5078
|
+
return { engagementId: answer.engagementId, status: answer.status, stampFailure, closeCommit };
|
|
5079
|
+
}
|
|
5080
|
+
function describeIssuePath(path) {
|
|
5081
|
+
const [head, field] = path;
|
|
5082
|
+
if (head === "statements" && field === "thesis")
|
|
5083
|
+
return "--thesis";
|
|
5084
|
+
if (head === "statements" && field === "outcome")
|
|
5085
|
+
return "--outcome";
|
|
5086
|
+
if (head === "statements" && field === "deviations")
|
|
5087
|
+
return "--deviation";
|
|
5088
|
+
if (head === "phase")
|
|
5089
|
+
return "the phase name";
|
|
5090
|
+
return path.map(String).join(".") || "(the close)";
|
|
4694
5091
|
}
|
|
4695
5092
|
|
|
4696
5093
|
// dist/cli-contract.js
|
|
@@ -4807,9 +5204,9 @@ var PLACEHOLDERS = {
|
|
|
4807
5204
|
};
|
|
4808
5205
|
|
|
4809
5206
|
// dist/uninstall.js
|
|
4810
|
-
import { lstatSync, readFileSync as readFileSync8, readdirSync, rmSync as
|
|
5207
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync8, readdirSync, rmSync as rmSync3, rmdirSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4811
5208
|
import { homedir as homedir2 } from "node:os";
|
|
4812
|
-
import { dirname as
|
|
5209
|
+
import { dirname as dirname3, join as join8, relative as relative4, sep } from "node:path";
|
|
4813
5210
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
4814
5211
|
function parseUninstallArgs(args2) {
|
|
4815
5212
|
let removeCredential = false;
|
|
@@ -4826,7 +5223,7 @@ function parseUninstallArgs(args2) {
|
|
|
4826
5223
|
}
|
|
4827
5224
|
function lexists(path) {
|
|
4828
5225
|
try {
|
|
4829
|
-
|
|
5226
|
+
lstatSync2(path);
|
|
4830
5227
|
return true;
|
|
4831
5228
|
} catch {
|
|
4832
5229
|
return false;
|
|
@@ -4835,9 +5232,9 @@ function lexists(path) {
|
|
|
4835
5232
|
function symlinkOnPath(root, rel) {
|
|
4836
5233
|
let current = root;
|
|
4837
5234
|
for (const segment of rel.split("/")) {
|
|
4838
|
-
current =
|
|
5235
|
+
current = join8(current, segment);
|
|
4839
5236
|
try {
|
|
4840
|
-
if (
|
|
5237
|
+
if (lstatSync2(current).isSymbolicLink())
|
|
4841
5238
|
return true;
|
|
4842
5239
|
} catch {
|
|
4843
5240
|
return false;
|
|
@@ -4856,7 +5253,7 @@ var Refusal = class extends Error {
|
|
|
4856
5253
|
function readMergedJson(targetRepo, rel) {
|
|
4857
5254
|
if (symlinkOnPath(targetRepo, rel))
|
|
4858
5255
|
return { state: "symlink" };
|
|
4859
|
-
const path =
|
|
5256
|
+
const path = join8(targetRepo, rel);
|
|
4860
5257
|
if (!lexists(path))
|
|
4861
5258
|
return { state: "absent" };
|
|
4862
5259
|
let value;
|
|
@@ -4872,8 +5269,8 @@ function readMergedJson(targetRepo, rel) {
|
|
|
4872
5269
|
}
|
|
4873
5270
|
function validate(options) {
|
|
4874
5271
|
const { targetRepo, removeCredential, home } = options;
|
|
4875
|
-
const manifestPath =
|
|
4876
|
-
const pinPath =
|
|
5272
|
+
const manifestPath = join8(targetRepo, INSTALL_MANIFEST_REL);
|
|
5273
|
+
const pinPath = join8(targetRepo, BUNDLE_PIN_REL);
|
|
4877
5274
|
if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
|
|
4878
5275
|
throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
|
|
4879
5276
|
}
|
|
@@ -4917,8 +5314,8 @@ function validate(options) {
|
|
|
4917
5314
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} is not a valid Halfcycle project id, so it cannot name a folder on this machine. Nothing was removed, here or under ~/.halfcycle.`);
|
|
4918
5315
|
}
|
|
4919
5316
|
if (engagementId !== void 0) {
|
|
4920
|
-
const expectedParent =
|
|
4921
|
-
if (
|
|
5317
|
+
const expectedParent = join8(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
5318
|
+
if (dirname3(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
4922
5319
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
4923
5320
|
}
|
|
4924
5321
|
}
|
|
@@ -5069,7 +5466,7 @@ function trimGitignore(targetRepo, text, rec, writeSet) {
|
|
|
5069
5466
|
}
|
|
5070
5467
|
function coveredPathExists(targetRepo, line) {
|
|
5071
5468
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
5072
|
-
return path !== "" && lexists(
|
|
5469
|
+
return path !== "" && lexists(join8(targetRepo, path));
|
|
5073
5470
|
}
|
|
5074
5471
|
function uninstall(options) {
|
|
5075
5472
|
const result = {
|
|
@@ -5127,7 +5524,7 @@ function uninstall(options) {
|
|
|
5127
5524
|
removeRecordedFile(targetRepo, entry, writeSet, result, name);
|
|
5128
5525
|
}
|
|
5129
5526
|
for (const path of manifest.leftAlone) {
|
|
5130
|
-
if (lexists(
|
|
5527
|
+
if (lexists(join8(targetRepo, path)))
|
|
5131
5528
|
name(result.leftAlone, path, path);
|
|
5132
5529
|
}
|
|
5133
5530
|
switch (removeLegacyCiStanza(targetRepo)) {
|
|
@@ -5159,10 +5556,10 @@ function uninstall(options) {
|
|
|
5159
5556
|
const pinEntry = manifest.files.find((f) => f.path === BUNDLE_PIN_REL);
|
|
5160
5557
|
if (pinEntry !== void 0)
|
|
5161
5558
|
removeRecordedFile(targetRepo, pinEntry, writeSet, result, name);
|
|
5162
|
-
else if (lexists(
|
|
5559
|
+
else if (lexists(join8(targetRepo, BUNDLE_PIN_REL)))
|
|
5163
5560
|
name(result.leftAlone, BUNDLE_PIN_REL, BUNDLE_PIN_REL);
|
|
5164
5561
|
try {
|
|
5165
|
-
|
|
5562
|
+
rmSync3(join8(targetRepo, INSTALL_MANIFEST_REL), { force: true });
|
|
5166
5563
|
named.add(INSTALL_MANIFEST_REL);
|
|
5167
5564
|
} catch {
|
|
5168
5565
|
name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
|
|
@@ -5192,14 +5589,14 @@ function applyMergedJson(targetRepo, rel, file, lists, name, edit) {
|
|
|
5192
5589
|
const { changed, created, adopted, keptNote } = edit(value);
|
|
5193
5590
|
if (keptNote !== void 0)
|
|
5194
5591
|
name(lists.kept, keptNote, rel);
|
|
5195
|
-
const path =
|
|
5592
|
+
const path = join8(targetRepo, rel);
|
|
5196
5593
|
const empty = Object.keys(value).length === 0;
|
|
5197
5594
|
try {
|
|
5198
5595
|
if (empty && created && !adopted) {
|
|
5199
|
-
|
|
5596
|
+
rmSync3(path);
|
|
5200
5597
|
name(lists.removed, rel, rel);
|
|
5201
5598
|
} else if (changed) {
|
|
5202
|
-
|
|
5599
|
+
writeFileSync5(path, serializeJson(value), "utf-8");
|
|
5203
5600
|
name(lists.restored, rel, rel);
|
|
5204
5601
|
}
|
|
5205
5602
|
if (adopted && lexists(path)) {
|
|
@@ -5219,10 +5616,10 @@ function removeRecordedFile(targetRepo, entry, writeSet, lists, name) {
|
|
|
5219
5616
|
name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
|
|
5220
5617
|
return;
|
|
5221
5618
|
}
|
|
5222
|
-
const abs =
|
|
5619
|
+
const abs = join8(targetRepo, path);
|
|
5223
5620
|
let isFile;
|
|
5224
5621
|
try {
|
|
5225
|
-
isFile =
|
|
5622
|
+
isFile = lstatSync2(abs).isFile();
|
|
5226
5623
|
} catch {
|
|
5227
5624
|
return;
|
|
5228
5625
|
}
|
|
@@ -5244,7 +5641,7 @@ function removeRecordedFile(targetRepo, entry, writeSet, lists, name) {
|
|
|
5244
5641
|
}
|
|
5245
5642
|
}
|
|
5246
5643
|
try {
|
|
5247
|
-
|
|
5644
|
+
rmSync3(abs);
|
|
5248
5645
|
name(lists.removed, path, path);
|
|
5249
5646
|
} catch {
|
|
5250
5647
|
name(lists.failed, path, path);
|
|
@@ -5253,9 +5650,9 @@ function removeRecordedFile(targetRepo, entry, writeSet, lists, name) {
|
|
|
5253
5650
|
function removeEmptyCreatedDir(targetRepo, dir, writeSet, lists, name) {
|
|
5254
5651
|
if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
|
|
5255
5652
|
return;
|
|
5256
|
-
const abs =
|
|
5653
|
+
const abs = join8(targetRepo, dir);
|
|
5257
5654
|
try {
|
|
5258
|
-
if (!
|
|
5655
|
+
if (!lstatSync2(abs).isDirectory() || readdirSync(abs).length > 0)
|
|
5259
5656
|
return;
|
|
5260
5657
|
} catch {
|
|
5261
5658
|
return;
|
|
@@ -5273,7 +5670,7 @@ function trimGitignoreFile(targetRepo, rec, writeSet, lists, name) {
|
|
|
5273
5670
|
name(lists.leftAlone, `${GITIGNORE_REL} (a symbolic link \u2014 not followed)`, GITIGNORE_REL);
|
|
5274
5671
|
return;
|
|
5275
5672
|
}
|
|
5276
|
-
const path =
|
|
5673
|
+
const path = join8(targetRepo, GITIGNORE_REL);
|
|
5277
5674
|
let before;
|
|
5278
5675
|
try {
|
|
5279
5676
|
before = readFileSync8(path, "utf-8");
|
|
@@ -5291,10 +5688,10 @@ function trimGitignoreFile(targetRepo, rec, writeSet, lists, name) {
|
|
|
5291
5688
|
return;
|
|
5292
5689
|
try {
|
|
5293
5690
|
if (text === "" && rec.created) {
|
|
5294
|
-
|
|
5691
|
+
rmSync3(path);
|
|
5295
5692
|
name(lists.removed, GITIGNORE_REL, GITIGNORE_REL);
|
|
5296
5693
|
} else {
|
|
5297
|
-
|
|
5694
|
+
writeFileSync5(path, text, "utf-8");
|
|
5298
5695
|
name(lists.restored, GITIGNORE_REL, GITIGNORE_REL);
|
|
5299
5696
|
}
|
|
5300
5697
|
} catch {
|
|
@@ -5318,10 +5715,10 @@ function handleCredential(options, checked) {
|
|
|
5318
5715
|
if (!lexists(dir))
|
|
5319
5716
|
return { kind: "not-held", dir };
|
|
5320
5717
|
try {
|
|
5321
|
-
if (
|
|
5718
|
+
if (lstatSync2(dir).isSymbolicLink())
|
|
5322
5719
|
return { kind: "failed", dir, reason: "it is a symbolic link" };
|
|
5323
|
-
|
|
5324
|
-
|
|
5720
|
+
rmSync3(engagementEnvPath(engagementId, home), { force: true });
|
|
5721
|
+
rmSync3(notThisAccountMarkerPath(engagementId, home), { force: true });
|
|
5325
5722
|
if (readdirSync(dir).length === 0)
|
|
5326
5723
|
rmdirSync(dir);
|
|
5327
5724
|
} catch (err) {
|
|
@@ -5334,16 +5731,16 @@ function filesUnder(root, rel) {
|
|
|
5334
5731
|
const walk = (dirRel) => {
|
|
5335
5732
|
let names;
|
|
5336
5733
|
try {
|
|
5337
|
-
if (!
|
|
5734
|
+
if (!lstatSync2(join8(root, dirRel)).isDirectory())
|
|
5338
5735
|
return;
|
|
5339
|
-
names = readdirSync(
|
|
5736
|
+
names = readdirSync(join8(root, dirRel));
|
|
5340
5737
|
} catch {
|
|
5341
5738
|
return;
|
|
5342
5739
|
}
|
|
5343
5740
|
for (const entry of names.sort()) {
|
|
5344
5741
|
const childRel = `${dirRel}/${entry}`;
|
|
5345
5742
|
try {
|
|
5346
|
-
if (
|
|
5743
|
+
if (lstatSync2(join8(root, childRel)).isDirectory())
|
|
5347
5744
|
walk(childRel);
|
|
5348
5745
|
else
|
|
5349
5746
|
out.push(childRel);
|
|
@@ -5357,7 +5754,7 @@ function filesUnder(root, rel) {
|
|
|
5357
5754
|
}
|
|
5358
5755
|
function shownPath(path, home) {
|
|
5359
5756
|
const base = home ?? homedir2();
|
|
5360
|
-
const rel =
|
|
5757
|
+
const rel = relative4(base, path);
|
|
5361
5758
|
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep);
|
|
5362
5759
|
return `${under ? `~/${rel.split(sep).join("/")}` : path}/`;
|
|
5363
5760
|
}
|
|
@@ -5417,7 +5814,7 @@ function renderUninstallReport(result, home) {
|
|
|
5417
5814
|
// dist/banner-facts.js
|
|
5418
5815
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5419
5816
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
5420
|
-
import { basename as basename3, join as
|
|
5817
|
+
import { basename as basename3, join as join9, resolve as resolve3 } from "node:path";
|
|
5421
5818
|
var BRAND_URL = "halfcycle.ai";
|
|
5422
5819
|
var TAGLINE = [
|
|
5423
5820
|
"Fewer cycles.",
|
|
@@ -5430,7 +5827,7 @@ var TAGLINE_NARROW = [
|
|
|
5430
5827
|
];
|
|
5431
5828
|
function bundleVersion() {
|
|
5432
5829
|
try {
|
|
5433
|
-
const pkg = JSON.parse(readFileSync9(
|
|
5830
|
+
const pkg = JSON.parse(readFileSync9(join9(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
5434
5831
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
5435
5832
|
} catch {
|
|
5436
5833
|
return void 0;
|
|
@@ -5580,8 +5977,11 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
5580
5977
|
--yes / -y: use this machine's saved sign-in without being asked
|
|
5581
5978
|
halfcycle check-drift <target-repo>
|
|
5582
5979
|
halfcycle build-record <phase> [--repo <root>]
|
|
5980
|
+
fetch a closed phase's Build Record again and write it into docs/build-records/
|
|
5583
5981
|
halfcycle open-phase <phase|--none> --actor "\u2026" (--evidence "\u2026" | --override --reason "\u2026") [--repo <root>]
|
|
5584
5982
|
halfcycle close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
|
|
5983
|
+
[--thesis "\u2026"] [--outcome "\u2026"] [--deviation "\u2026"]\u2026
|
|
5984
|
+
close the phase and write its Build Record into docs/build-records/
|
|
5585
5985
|
halfcycle uninstall [${REMOVE_CREDENTIAL_FLAG}]
|
|
5586
5986
|
take out what the installer added to this project, keeping anything you changed
|
|
5587
5987
|
${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
|
|
@@ -5594,7 +5994,7 @@ var USAGE = ` halfcycle [install] [target-repo] [engagement-id] [self-build|cli
|
|
|
5594
5994
|
`;
|
|
5595
5995
|
function isHalfcycleMonorepo(dir) {
|
|
5596
5996
|
try {
|
|
5597
|
-
const pkg = JSON.parse(readFileSync10(
|
|
5997
|
+
const pkg = JSON.parse(readFileSync10(join10(dir, "package.json"), "utf-8"));
|
|
5598
5998
|
return pkg.name === "halfcycle-monorepo";
|
|
5599
5999
|
} catch {
|
|
5600
6000
|
return false;
|
|
@@ -5816,6 +6216,9 @@ ${USAGE}`);
|
|
|
5816
6216
|
process.stdout.write(`[halfcycle] Found: ${foundLine}
|
|
5817
6217
|
`);
|
|
5818
6218
|
process.stdout.write(`[halfcycle] Set up: ${written} file${written === 1 ? "" : "s"} written${merged > 0 ? ` (${merged} merged)` : ""}, ${credential ? "guards armed on every edit" : "not checked \u2014 this copy is not this account\u2019s project"}
|
|
6219
|
+
`);
|
|
6220
|
+
if (result.notShared)
|
|
6221
|
+
process.stdout.write(`${renderNotShared(result.notShared)}
|
|
5819
6222
|
`);
|
|
5820
6223
|
if (credential) {
|
|
5821
6224
|
process.stdout.write(`[halfcycle] Next: open this folder in Claude Code \u2014 accept the workspace-trust prompt, it is expected \u2014 then run /halfcycle-setup and answer what it asks about your project
|
|
@@ -5898,31 +6301,24 @@ ${USAGE}`);
|
|
|
5898
6301
|
return;
|
|
5899
6302
|
}
|
|
5900
6303
|
if (cmd === "build-record") {
|
|
5901
|
-
const phaseArg = rest
|
|
5902
|
-
const repoRoot = rest
|
|
6304
|
+
const phaseArg = firstPositional(rest);
|
|
6305
|
+
const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
|
|
5903
6306
|
if (phaseArg === void 0) {
|
|
5904
6307
|
process.stderr.write("halfcycle: usage: halfcycle build-record <phase> [--repo <root>]\n");
|
|
5905
6308
|
process.exit(1);
|
|
5906
6309
|
return;
|
|
5907
6310
|
}
|
|
5908
|
-
const phaseId = phaseArg;
|
|
5909
6311
|
try {
|
|
5910
|
-
assertValidPhaseIdentity(
|
|
5911
|
-
const
|
|
5912
|
-
const
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
});
|
|
5921
|
-
process.stdout.write(`[halfcycle] Build Record written:
|
|
5922
|
-
${result.jsonPath}
|
|
5923
|
-
${result.mdPath}
|
|
5924
|
-
`);
|
|
5925
|
-
process.exit(0);
|
|
6312
|
+
assertValidPhaseIdentity(phaseArg, BUILD_RECORD_DIR);
|
|
6313
|
+
const credential = readRepoCredential(repoRoot);
|
|
6314
|
+
const obtained = await obtainBuildRecord(credential, repoRoot, phaseArg);
|
|
6315
|
+
if (obtained.kind === "written") {
|
|
6316
|
+
process.stdout.write(writtenLines(obtained));
|
|
6317
|
+
process.exit(0);
|
|
6318
|
+
return;
|
|
6319
|
+
}
|
|
6320
|
+
process.stderr.write(notWrittenLines("build-record", obtained, phaseArg));
|
|
6321
|
+
process.exit(1);
|
|
5926
6322
|
} catch (err) {
|
|
5927
6323
|
process.stderr.write(`halfcycle build-record failed: ${err instanceof Error ? err.message : String(err)}
|
|
5928
6324
|
`);
|
|
@@ -6055,26 +6451,36 @@ halfcycle open-phase: a phase name has to work as a single folder name on this m
|
|
|
6055
6451
|
process.exit(2);
|
|
6056
6452
|
return;
|
|
6057
6453
|
}
|
|
6454
|
+
const stated = readStatementFlags(rest);
|
|
6455
|
+
if (typeof stated === "string") {
|
|
6456
|
+
process.stderr.write(`halfcycle close-phase: ${stated} needs a value after it.
|
|
6457
|
+
`);
|
|
6458
|
+
process.exit(2);
|
|
6459
|
+
return;
|
|
6460
|
+
}
|
|
6058
6461
|
try {
|
|
6059
6462
|
const credential = readRepoCredential(repoRoot);
|
|
6060
6463
|
const closed = await closePhase(credential, phaseArg, {
|
|
6061
6464
|
verdict,
|
|
6062
6465
|
findings: findingsDetail.length,
|
|
6063
6466
|
findingsDetail
|
|
6064
|
-
}, closeDecision, void 0, repoRoot);
|
|
6467
|
+
}, closeDecision, void 0, repoRoot, stated);
|
|
6065
6468
|
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}.
|
|
6066
6469
|
`);
|
|
6067
6470
|
if (closeDecision.decision === "override") {
|
|
6068
6471
|
process.stdout.write(`[halfcycle] This close was an OVERRIDE: ${closeDecision.reason}
|
|
6069
6472
|
`);
|
|
6070
6473
|
}
|
|
6071
|
-
if (closed.
|
|
6072
|
-
process.
|
|
6474
|
+
if (!closed.closeCommit.read) {
|
|
6475
|
+
process.stderr.write(`halfcycle close-phase: the commit this phase closed at could not be read (${closed.closeCommit.reason}), so its Build Record shows the commit as not stated.
|
|
6073
6476
|
`);
|
|
6477
|
+
}
|
|
6478
|
+
const obtained = await obtainBuildRecord(credential, repoRoot, phaseArg);
|
|
6479
|
+
const recordWritten = obtained.kind === "written";
|
|
6480
|
+
if (obtained.kind === "written") {
|
|
6481
|
+
process.stdout.write(writtenLines(obtained));
|
|
6074
6482
|
} else {
|
|
6075
|
-
process.stderr.write(
|
|
6076
|
-
halfcycle close-phase: this phase's Build Record will say the close commit and the close date were not recorded.
|
|
6077
|
-
`);
|
|
6483
|
+
process.stderr.write(notWrittenLines("close-phase", obtained, phaseArg));
|
|
6078
6484
|
}
|
|
6079
6485
|
process.stdout.write(`[halfcycle] Open the next phase with: halfcycle open-phase <phase> --actor "you" --evidence "\u2026"
|
|
6080
6486
|
`);
|
|
@@ -6082,21 +6488,28 @@ halfcycle close-phase: this phase's Build Record will say the close commit and t
|
|
|
6082
6488
|
process.stderr.write(`halfcycle close-phase: the phase closed, but this machine's record of it could not be cleared: ${closed.stampFailure.reason}
|
|
6083
6489
|
halfcycle close-phase: until ${closed.stampFailure.path} can be written, guard evaluations here are still filed under the phase you just closed.
|
|
6084
6490
|
`);
|
|
6085
|
-
process.exit(1);
|
|
6086
|
-
return;
|
|
6087
6491
|
}
|
|
6088
|
-
process.exit(0);
|
|
6492
|
+
process.exit(recordWritten && !closed.stampFailure ? 0 : 1);
|
|
6089
6493
|
} catch (err) {
|
|
6494
|
+
if (err instanceof CloseRequestInvalid) {
|
|
6495
|
+
process.stderr.write(`halfcycle close-phase: ${err.message}
|
|
6496
|
+
`);
|
|
6497
|
+
process.exit(2);
|
|
6498
|
+
return;
|
|
6499
|
+
}
|
|
6090
6500
|
if (err instanceof ClosePhaseRefused) {
|
|
6091
6501
|
process.stderr.write(`halfcycle close-phase: refused (${err.status}). ${err.message}
|
|
6092
6502
|
`);
|
|
6503
|
+
if (err.status === 400) {
|
|
6504
|
+
process.stderr.write("halfcycle close-phase: Nothing was closed.\n");
|
|
6505
|
+
}
|
|
6093
6506
|
if (err.finding) {
|
|
6094
6507
|
process.stderr.write(`halfcycle close-phase: outstanding work \u2014 ${err.finding}
|
|
6095
6508
|
`);
|
|
6096
6509
|
process.stderr.write(`halfcycle close-phase: close it anyway with \`halfcycle close-phase ${phaseArg} --verdict ${verdict} --actor "${closeActor}" --override --reason "\u2026"\` \u2014 recorded, not waived.
|
|
6097
6510
|
`);
|
|
6098
6511
|
} else if (err.code === "PhaseAlreadyClosed") {
|
|
6099
|
-
process.stderr.write("halfcycle close-phase:
|
|
6512
|
+
process.stderr.write("halfcycle close-phase: to close a phase again, open it again first.\nhalfcycle close-phase: To get a closed phase's Build Record: npx halfcycle@latest build-record <the phase that closed>\n");
|
|
6100
6513
|
}
|
|
6101
6514
|
process.exit(1);
|
|
6102
6515
|
return;
|
|
@@ -6113,6 +6526,56 @@ Usage:
|
|
|
6113
6526
|
${USAGE}`);
|
|
6114
6527
|
process.exit(1);
|
|
6115
6528
|
}
|
|
6529
|
+
function readStatementFlags(argv) {
|
|
6530
|
+
let thesis = null;
|
|
6531
|
+
let outcome = null;
|
|
6532
|
+
const deviations = [];
|
|
6533
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6534
|
+
const flag = argv[i];
|
|
6535
|
+
if (flag !== "--thesis" && flag !== "--outcome" && flag !== "--deviation")
|
|
6536
|
+
continue;
|
|
6537
|
+
const value = argv[i + 1];
|
|
6538
|
+
if (value === void 0 || value.startsWith("--"))
|
|
6539
|
+
return flag;
|
|
6540
|
+
if (flag === "--thesis")
|
|
6541
|
+
thesis = value;
|
|
6542
|
+
else if (flag === "--outcome")
|
|
6543
|
+
outcome = value;
|
|
6544
|
+
else
|
|
6545
|
+
deviations.push(value);
|
|
6546
|
+
i++;
|
|
6547
|
+
}
|
|
6548
|
+
return { thesis, outcome, deviations };
|
|
6549
|
+
}
|
|
6550
|
+
function writtenLines(written) {
|
|
6551
|
+
return `[halfcycle] Build Record written:
|
|
6552
|
+
${written.mdPath}
|
|
6553
|
+
${written.jsonPath}
|
|
6554
|
+
`;
|
|
6555
|
+
}
|
|
6556
|
+
function notWrittenLines(verb, outcome, phase) {
|
|
6557
|
+
const prefix = `halfcycle ${verb}:`;
|
|
6558
|
+
const lead = verb === "close-phase" ? `${prefix} the phase closed, but no Build Record was written. ` : `${prefix} `;
|
|
6559
|
+
switch (outcome.kind) {
|
|
6560
|
+
case "exists":
|
|
6561
|
+
return `${lead}A file is already at ${outcome.path} and it is not this phase's earlier record, so it was left as it is.
|
|
6562
|
+
${prefix} Move it away, then run: ${refetchCommand(phase)}
|
|
6563
|
+
`;
|
|
6564
|
+
case "withheld":
|
|
6565
|
+
return `${lead}${outcome.message}
|
|
6566
|
+
${prefix} Fetch it later with: ${refetchCommand(phase)}
|
|
6567
|
+
`;
|
|
6568
|
+
case "refused":
|
|
6569
|
+
return `${lead}${outcome.message}
|
|
6570
|
+
${prefix} ${REFUSAL_FOLLOW_UP[outcome.reason]}
|
|
6571
|
+
` + (verb === "close-phase" ? `${prefix} Fetch it later with: ${refetchCommand(phase)}
|
|
6572
|
+
` : "");
|
|
6573
|
+
case "unavailable":
|
|
6574
|
+
return `${lead}The Build Record could not be fetched: ${outcome.why}
|
|
6575
|
+
${prefix} Fetch it later with: ${refetchCommand(phase)}
|
|
6576
|
+
`;
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6116
6579
|
function flagValue(argv, name) {
|
|
6117
6580
|
const i = argv.indexOf(name);
|
|
6118
6581
|
if (i < 0 || i + 1 >= argv.length)
|