halfcycle 0.4.5 → 0.4.7
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 +1325 -155
- package/commands/agent-teams-orchestration.md +1 -1
- package/commands/stack-assembly-worker.md +1 -1
- package/commands/task-execution.md +1 -1
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/build-record/obtain.d.ts +3 -1
- package/dist/build-record/obtain.d.ts.map +1 -1
- package/dist/build-record/write.d.ts +5 -1
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +2326 -190
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts +8 -1
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/draw/index.d.ts +135 -0
- package/dist/draw/index.d.ts.map +1 -0
- package/dist/draw/write.d.ts +282 -0
- package/dist/draw/write.d.ts.map +1 -0
- package/dist/index.js +96 -17
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +33 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/missing-route.d.ts +36 -0
- package/dist/missing-route.d.ts.map +1 -0
- package/dist/open-phase.d.ts +8 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/plan.d.ts +200 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/system-report.d.ts +179 -0
- package/dist/system-report.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/bin.js
|
|
4
|
-
import { existsSync as existsSync7, readFileSync as
|
|
4
|
+
import { existsSync as existsSync7, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync, writeSync as writeSync2 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join15 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/install.js
|
|
9
9
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -143,6 +143,7 @@ var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
|
143
143
|
var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
|
|
144
144
|
var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
145
145
|
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
146
|
+
var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
|
|
146
147
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
147
148
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
148
149
|
function hookCommand(script, verb) {
|
|
@@ -171,6 +172,13 @@ var HALFCYCLE_HOOK_WIRING = [
|
|
|
171
172
|
command: hookCommand(USER_PROMPT_REMINDER_REL),
|
|
172
173
|
script: USER_PROMPT_REMINDER_REL
|
|
173
174
|
},
|
|
175
|
+
{
|
|
176
|
+
event: "UserPromptSubmit",
|
|
177
|
+
matcher: null,
|
|
178
|
+
timeout: 10,
|
|
179
|
+
command: hookCommand(USER_PROMPT_SCOPE_REL),
|
|
180
|
+
script: USER_PROMPT_SCOPE_REL
|
|
181
|
+
},
|
|
174
182
|
{
|
|
175
183
|
event: "PostToolUse",
|
|
176
184
|
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
@@ -252,6 +260,131 @@ function quoteForShell(value) {
|
|
|
252
260
|
}
|
|
253
261
|
var HALFCYCLE_COMMAND = "npx halfcycle@latest";
|
|
254
262
|
|
|
263
|
+
// ../core/dist/shown-plans.js
|
|
264
|
+
var SHOWN_PLANS_FILENAME = "shown-plans.json";
|
|
265
|
+
var SHOWN_PLANS_FORMAT = 1;
|
|
266
|
+
var RECORD_KEYS = ["entries", "format"];
|
|
267
|
+
var ENTRY_KEYS = ["number", "phase", "shownAt", "version"];
|
|
268
|
+
function parseShownPlans(text) {
|
|
269
|
+
try {
|
|
270
|
+
if (typeof text !== "string")
|
|
271
|
+
return [];
|
|
272
|
+
const parsed = JSON.parse(text);
|
|
273
|
+
if (!isPlainObject(parsed) || !hasExactKeys(parsed, RECORD_KEYS))
|
|
274
|
+
return [];
|
|
275
|
+
if (parsed["format"] !== SHOWN_PLANS_FORMAT)
|
|
276
|
+
return [];
|
|
277
|
+
const rawEntries = parsed["entries"];
|
|
278
|
+
if (!Array.isArray(rawEntries))
|
|
279
|
+
return [];
|
|
280
|
+
const entries = [];
|
|
281
|
+
for (const raw of rawEntries) {
|
|
282
|
+
const entry = readEntry(raw);
|
|
283
|
+
if (entry === null)
|
|
284
|
+
return [];
|
|
285
|
+
entries.push(entry);
|
|
286
|
+
}
|
|
287
|
+
return hasDuplicates(entries) ? [] : entries;
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function serialiseShownPlans(entries) {
|
|
293
|
+
if (!Array.isArray(entries))
|
|
294
|
+
throw new Error(UNRECORDABLE);
|
|
295
|
+
const copied = [];
|
|
296
|
+
for (const raw of entries) {
|
|
297
|
+
const entry = readEntry(raw);
|
|
298
|
+
if (entry === null)
|
|
299
|
+
throw new Error(UNRECORDABLE);
|
|
300
|
+
copied.push(entry);
|
|
301
|
+
}
|
|
302
|
+
if (hasDuplicates(copied))
|
|
303
|
+
throw new Error(UNRECORDABLE);
|
|
304
|
+
return `${JSON.stringify({ format: SHOWN_PLANS_FORMAT, entries: copied }, null, 2)}
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
var UNRECORDABLE = "the list just shown could not be recorded on this machine.";
|
|
308
|
+
function readEntry(raw) {
|
|
309
|
+
if (!isPlainObject(raw) || !hasExactKeys(raw, ENTRY_KEYS))
|
|
310
|
+
return null;
|
|
311
|
+
const phase = raw["phase"];
|
|
312
|
+
const number = raw["number"];
|
|
313
|
+
const version = raw["version"];
|
|
314
|
+
const shownAt = raw["shownAt"];
|
|
315
|
+
if (typeof phase !== "string" || phase === "")
|
|
316
|
+
return null;
|
|
317
|
+
if (!isPositiveInteger(number) || !isPositiveInteger(version))
|
|
318
|
+
return null;
|
|
319
|
+
if (!isIsoInstant(shownAt))
|
|
320
|
+
return null;
|
|
321
|
+
return { phase, number, version, shownAt };
|
|
322
|
+
}
|
|
323
|
+
function isPlainObject(value) {
|
|
324
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
325
|
+
return false;
|
|
326
|
+
const proto = Object.getPrototypeOf(value);
|
|
327
|
+
return proto === Object.prototype || proto === null;
|
|
328
|
+
}
|
|
329
|
+
function hasExactKeys(value, keys) {
|
|
330
|
+
const own = Object.keys(value).sort();
|
|
331
|
+
return own.length === keys.length && own.every((key, i) => key === keys[i]);
|
|
332
|
+
}
|
|
333
|
+
function isPositiveInteger(value) {
|
|
334
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
335
|
+
}
|
|
336
|
+
function isIsoInstant(value) {
|
|
337
|
+
if (typeof value !== "string")
|
|
338
|
+
return false;
|
|
339
|
+
const ms = Date.parse(value);
|
|
340
|
+
return Number.isFinite(ms) && new Date(ms).toISOString() === value;
|
|
341
|
+
}
|
|
342
|
+
function hasDuplicates(entries) {
|
|
343
|
+
const phases = new Set(entries.map((entry) => entry.phase));
|
|
344
|
+
const numbers = new Set(entries.map((entry) => entry.number));
|
|
345
|
+
return phases.size !== entries.length || numbers.size !== entries.length;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ../core/dist/phase-reference.js
|
|
349
|
+
var TYPED_NUMBER = /^(?:0|[1-9][0-9]*)$/;
|
|
350
|
+
function resolvePhaseReference(typed, phases) {
|
|
351
|
+
if (typeof typed !== "string" || typed === "")
|
|
352
|
+
return null;
|
|
353
|
+
const byIdentity = phases.filter((entry) => entry.phase === typed);
|
|
354
|
+
if (byIdentity.length > 0)
|
|
355
|
+
return byIdentity.length === 1 ? byIdentity[0] : null;
|
|
356
|
+
if (!TYPED_NUMBER.test(typed))
|
|
357
|
+
return null;
|
|
358
|
+
const byNumber = phases.filter((entry) => Number.isInteger(entry.number) && String(entry.number) === typed);
|
|
359
|
+
return byNumber.length === 1 ? byNumber[0] : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ../core/dist/release-version.js
|
|
363
|
+
var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
364
|
+
function isReleaseVersion(value) {
|
|
365
|
+
return typeof value === "string" && RELEASE_VERSION_PATTERN.test(value);
|
|
366
|
+
}
|
|
367
|
+
function significant(part) {
|
|
368
|
+
const trimmed = part.replace(/^0+/, "");
|
|
369
|
+
return trimmed === "" ? "0" : trimmed;
|
|
370
|
+
}
|
|
371
|
+
function compareReleaseVersions(a, b) {
|
|
372
|
+
if (!isReleaseVersion(a) || !isReleaseVersion(b)) {
|
|
373
|
+
throw new TypeError("compareReleaseVersions takes two MAJOR.MINOR.PATCH versions");
|
|
374
|
+
}
|
|
375
|
+
const pa = a.split(".").map(significant);
|
|
376
|
+
const pb = b.split(".").map(significant);
|
|
377
|
+
for (let i = 0; i < 3; i++) {
|
|
378
|
+
const x = pa[i];
|
|
379
|
+
const y = pb[i];
|
|
380
|
+
if (x.length !== y.length)
|
|
381
|
+
return x.length - y.length;
|
|
382
|
+
if (x !== y)
|
|
383
|
+
return x < y ? -1 : 1;
|
|
384
|
+
}
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
255
388
|
// ../events/dist/result.js
|
|
256
389
|
import { z as z2 } from "zod";
|
|
257
390
|
var firedGuardSchema = z2.object({
|
|
@@ -321,18 +454,19 @@ function hasLoneSurrogate(text) {
|
|
|
321
454
|
}
|
|
322
455
|
var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
|
|
323
456
|
|
|
324
|
-
// ../events/dist/build-record.js
|
|
325
|
-
import { z as z4 } from "zod";
|
|
326
|
-
|
|
327
457
|
// ../events/dist/state.js
|
|
328
458
|
import { z as z3 } from "zod";
|
|
329
459
|
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");
|
|
330
460
|
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
331
461
|
var stepIdSchema = z3.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
332
|
-
var methodVersionSchema = z3.string().regex(
|
|
462
|
+
var methodVersionSchema = z3.string().regex(RELEASE_VERSION_PATTERN);
|
|
333
463
|
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
334
464
|
var recordKindSchema = z3.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
335
465
|
var methodLayerSchema = z3.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
466
|
+
var PHASE_SCOPED_LAYERS = ["l4", "l5"];
|
|
467
|
+
function isPhaseScopedLayer(layer) {
|
|
468
|
+
return PHASE_SCOPED_LAYERS.includes(layer);
|
|
469
|
+
}
|
|
336
470
|
var evidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
337
471
|
var declarableEvidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
338
472
|
var dispositionSchema = z3.enum([
|
|
@@ -506,6 +640,9 @@ var stateRecordSchema = z3.discriminatedUnion("recordKind", [
|
|
|
506
640
|
interventionRecordSchema
|
|
507
641
|
]);
|
|
508
642
|
|
|
643
|
+
// ../events/dist/build-record.js
|
|
644
|
+
import { z as z4 } from "zod";
|
|
645
|
+
|
|
509
646
|
// ../events/dist/control-characters.js
|
|
510
647
|
var CONTROL_CHARACTER_CLASS = "\\p{Cc}";
|
|
511
648
|
var CONTROL_CHARACTER = new RegExp(CONTROL_CHARACTER_CLASS, "u");
|
|
@@ -609,9 +746,11 @@ function statementText(max) {
|
|
|
609
746
|
});
|
|
610
747
|
}
|
|
611
748
|
var closeActorSchema = plainText(ACTOR_MAX);
|
|
749
|
+
var PHASE_NAME_RULE = `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`;
|
|
612
750
|
var phaseNameSchema = z4.string().refine((v) => isValidPhaseIdentity(v), {
|
|
613
|
-
message:
|
|
751
|
+
message: PHASE_NAME_RULE
|
|
614
752
|
});
|
|
753
|
+
var phaseIdentitySchema = z4.union([z4.string(), z4.number()], { error: "must be a phase name or a whole number" }).refine((v) => isValidPhaseIdentity(v), { message: PHASE_NAME_RULE });
|
|
615
754
|
var closeStatementsSchema = z4.object({
|
|
616
755
|
thesis: statementText(STATEMENT_MAX).nullable(),
|
|
617
756
|
outcome: statementText(STATEMENT_MAX).nullable(),
|
|
@@ -887,17 +1026,31 @@ var phaseEntryDecisionRecordSchema = z5.object({
|
|
|
887
1026
|
blockingFinding: z5.string().nullable(),
|
|
888
1027
|
decidedAt: utcIso86012
|
|
889
1028
|
}).strict();
|
|
1029
|
+
var methodUpdateSchema = z5.object({
|
|
1030
|
+
from: methodVersionSchema,
|
|
1031
|
+
to: methodVersionSchema
|
|
1032
|
+
}).strict();
|
|
890
1033
|
var openPhaseResponseSchema = z5.object({
|
|
891
1034
|
engagement: engagementStatusResponseSchema,
|
|
892
1035
|
/** What is open now. `null` is project scope. */
|
|
893
1036
|
phase: z5.string().nullable(),
|
|
894
1037
|
/** What was open before this decision. `null` is project scope. */
|
|
895
1038
|
previousPhase: z5.string().nullable(),
|
|
896
|
-
decision: phaseEntryDecisionRecordSchema
|
|
1039
|
+
decision: phaseEntryDecisionRecordSchema,
|
|
1040
|
+
/** The method version this project moved to with this open, or `null` when it did not move. */
|
|
1041
|
+
methodUpdated: methodUpdateSchema.nullable()
|
|
1042
|
+
}).strict();
|
|
1043
|
+
var acceptPhaseResponseSchema = engagementStatusResponseSchema.extend({
|
|
1044
|
+
methodUpdated: methodUpdateSchema.nullable()
|
|
897
1045
|
}).strict();
|
|
898
1046
|
|
|
899
1047
|
// ../events/dist/crew.js
|
|
900
1048
|
import { z as z6 } from "zod";
|
|
1049
|
+
|
|
1050
|
+
// ../events/dist/human-agent.js
|
|
1051
|
+
var HUMAN_AGENT = "Human";
|
|
1052
|
+
|
|
1053
|
+
// ../events/dist/crew.js
|
|
901
1054
|
var crewGroupSchema = z6.enum(["builder", "reviewer", "always-on"]);
|
|
902
1055
|
var crewTierSchema = z6.enum(["deep", "standard", "fast"]);
|
|
903
1056
|
var CREW_DISCIPLINE_MAX = 64;
|
|
@@ -1090,9 +1243,700 @@ var CREW_ROSTER = [
|
|
|
1090
1243
|
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
1091
1244
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
1092
1245
|
|
|
1093
|
-
// ../events/dist/
|
|
1246
|
+
// ../events/dist/build-snapshot.js
|
|
1094
1247
|
import { z as z7 } from "zod";
|
|
1095
|
-
var
|
|
1248
|
+
var buildTaskStateSchema = z7.enum([
|
|
1249
|
+
"pending",
|
|
1250
|
+
"in-flight",
|
|
1251
|
+
"completed",
|
|
1252
|
+
"failed",
|
|
1253
|
+
"blocked"
|
|
1254
|
+
]);
|
|
1255
|
+
var BUILD_TASK_TITLE_MAX = 240;
|
|
1256
|
+
var BUILD_SNAPSHOT_TASK_LIMIT = 500;
|
|
1257
|
+
var buildTaskSchema = z7.object({
|
|
1258
|
+
// The task's own id, as the coordinator's graph names it.
|
|
1259
|
+
id: z7.string().min(1).max(64),
|
|
1260
|
+
// What the task is, in the coordinator's words. A description of the work,
|
|
1261
|
+
// bounded and scanned — never an excerpt of anybody's code.
|
|
1262
|
+
title: z7.string().min(1).max(BUILD_TASK_TITLE_MAX),
|
|
1263
|
+
// What the task is doing right now.
|
|
1264
|
+
state: buildTaskStateSchema,
|
|
1265
|
+
// Who holds it — a bare callsign, or the value naming a person when the work is
|
|
1266
|
+
// a person's. Absent when nothing has been dispatched to it yet.
|
|
1267
|
+
agent: z7.string().min(1).max(32).optional(),
|
|
1268
|
+
// The kind of work the holder does. Present exactly when an agent holds it.
|
|
1269
|
+
discipline: z7.string().min(1).max(64).optional(),
|
|
1270
|
+
// How much thinking the holder brings. Present exactly when an agent holds it,
|
|
1271
|
+
// and there is no field naming the supplier behind it.
|
|
1272
|
+
tier: z7.string().min(1).max(32).optional(),
|
|
1273
|
+
// The tasks this one cannot start before. Always present; empty means it waits
|
|
1274
|
+
// on nothing, which is different from not having been computed.
|
|
1275
|
+
dependsOn: z7.array(z7.string().min(1).max(64)).max(BUILD_SNAPSHOT_TASK_LIMIT),
|
|
1276
|
+
// The one task this task checks, when it is a check on other work. Absent on a
|
|
1277
|
+
// task that is doing the work rather than checking it.
|
|
1278
|
+
reviews: z7.string().min(1).max(64).optional(),
|
|
1279
|
+
// Which wave of the phase this task belongs to — the wave that writes the
|
|
1280
|
+
// specifications, or the wave that builds them. Absent when the graph does not
|
|
1281
|
+
// record it, which is different from the task belonging to neither.
|
|
1282
|
+
layer: methodLayerSchema.optional(),
|
|
1283
|
+
// When it was picked up, and when it settled. A reader computes how long it
|
|
1284
|
+
// took; neither this row nor this payload carries a duration.
|
|
1285
|
+
startedAt: utcIso8601.optional(),
|
|
1286
|
+
settledAt: utcIso8601.optional()
|
|
1287
|
+
}).strict().superRefine((task, ctx) => {
|
|
1288
|
+
if (task.reviews !== void 0 && task.reviews === task.id) {
|
|
1289
|
+
ctx.addIssue({
|
|
1290
|
+
code: z7.ZodIssueCode.custom,
|
|
1291
|
+
path: ["reviews"],
|
|
1292
|
+
message: `'${task.id}' reviews itself \u2014 a review names the OTHER task it checks.`
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
if (task.layer !== void 0 && !isPhaseScopedLayer(task.layer)) {
|
|
1296
|
+
ctx.addIssue({
|
|
1297
|
+
code: z7.ZodIssueCode.custom,
|
|
1298
|
+
path: ["layer"],
|
|
1299
|
+
message: `'${task.layer}' is not a wave a phase runs \u2014 expected one of ${PHASE_SCOPED_LAYERS.join(", ")}.`
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
const profiled = task.agent !== void 0 && task.agent !== HUMAN_AGENT;
|
|
1303
|
+
for (const field of ["discipline", "tier"]) {
|
|
1304
|
+
if (profiled && task[field] === void 0) {
|
|
1305
|
+
ctx.addIssue({
|
|
1306
|
+
code: z7.ZodIssueCode.custom,
|
|
1307
|
+
path: [field],
|
|
1308
|
+
message: `'${field}' is required on a task an agent holds.`
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
if (!profiled && task[field] !== void 0) {
|
|
1312
|
+
ctx.addIssue({
|
|
1313
|
+
code: z7.ZodIssueCode.custom,
|
|
1314
|
+
path: [field],
|
|
1315
|
+
message: task.agent === void 0 ? `'${field}' must be absent on a task nobody holds.` : `'${field}' must be absent on work a person holds.`
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
var buildPhaseSchema = z7.string().min(1).max(128);
|
|
1321
|
+
var BUILD_PHASE_TITLE_MAX = 160;
|
|
1322
|
+
var buildPhaseTitleSchema = z7.string().min(1).max(BUILD_PHASE_TITLE_MAX);
|
|
1323
|
+
var buildTaskListSchema = z7.array(buildTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT).superRefine((tasks, ctx) => {
|
|
1324
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1325
|
+
for (const [index, task] of tasks.entries()) {
|
|
1326
|
+
if (seen.has(task.id)) {
|
|
1327
|
+
ctx.addIssue({
|
|
1328
|
+
code: z7.ZodIssueCode.custom,
|
|
1329
|
+
path: [index, "id"],
|
|
1330
|
+
message: `duplicate task id '${task.id}' \u2014 one graph names each task once.`
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
seen.add(task.id);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
var buildSnapshotSchema = z7.object({
|
|
1337
|
+
// Which phase this graph belongs to.
|
|
1338
|
+
phase: buildPhaseSchema,
|
|
1339
|
+
// That phase's name in words, when the project gives it one.
|
|
1340
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
1341
|
+
// Every task on it. Never a partial graph and never a change since last time.
|
|
1342
|
+
tasks: buildTaskListSchema,
|
|
1343
|
+
// When this graph was last reported.
|
|
1344
|
+
reportedAt: utcIso8601,
|
|
1345
|
+
// When work on this phase was first reported with no approval of its scope in force.
|
|
1346
|
+
builtWithoutApproval: utcIso8601.optional()
|
|
1347
|
+
}).strict();
|
|
1348
|
+
var buildPhaseEntrySchema = z7.object({
|
|
1349
|
+
// Which phase has a stored graph.
|
|
1350
|
+
phase: buildPhaseSchema,
|
|
1351
|
+
// That phase's name in words, when the project gave it one.
|
|
1352
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
1353
|
+
// When that phase's graph was last reported.
|
|
1354
|
+
reportedAt: utcIso8601,
|
|
1355
|
+
// Present when work on this phase was reported with no approval of its scope in force.
|
|
1356
|
+
builtWithoutApproval: z7.literal(true).optional()
|
|
1357
|
+
}).strict();
|
|
1358
|
+
var buildSnapshotReportSchema = z7.object({
|
|
1359
|
+
// Which phase this graph belongs to.
|
|
1360
|
+
phase: buildPhaseSchema,
|
|
1361
|
+
// That phase's name in words, when the project gives it one.
|
|
1362
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
1363
|
+
// The whole graph, every time. Reporting a change instead of the graph is the
|
|
1364
|
+
// one thing this route does not accept.
|
|
1365
|
+
tasks: buildTaskListSchema
|
|
1366
|
+
}).strict();
|
|
1367
|
+
var buildSnapshotReportResultSchema = z7.object({
|
|
1368
|
+
// What happened. One outcome today; a refusal travels as the shared wire error.
|
|
1369
|
+
outcome: z7.literal("recorded"),
|
|
1370
|
+
// Which phase the graph was recorded against.
|
|
1371
|
+
phase: buildPhaseSchema,
|
|
1372
|
+
// When it was recorded.
|
|
1373
|
+
reportedAt: utcIso8601
|
|
1374
|
+
}).strict();
|
|
1375
|
+
|
|
1376
|
+
// ../events/dist/phase-plan.js
|
|
1377
|
+
import { z as z9 } from "zod";
|
|
1378
|
+
|
|
1379
|
+
// ../events/dist/check-problems.js
|
|
1380
|
+
function isRecord(value) {
|
|
1381
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1382
|
+
}
|
|
1383
|
+
function dotted(path) {
|
|
1384
|
+
return path.map((key) => String(key)).join(".");
|
|
1385
|
+
}
|
|
1386
|
+
function isMissingKey(raw, path) {
|
|
1387
|
+
if (path.length === 0)
|
|
1388
|
+
return false;
|
|
1389
|
+
let parent = raw;
|
|
1390
|
+
for (const key of path.slice(0, -1)) {
|
|
1391
|
+
if (!isRecord(parent) && !Array.isArray(parent))
|
|
1392
|
+
return false;
|
|
1393
|
+
parent = parent[key];
|
|
1394
|
+
}
|
|
1395
|
+
return isRecord(parent) && !(String(path[path.length - 1]) in parent);
|
|
1396
|
+
}
|
|
1397
|
+
function shapeProblemsOf(schema, raw, undeclared) {
|
|
1398
|
+
const parsed = schema.safeParse(raw);
|
|
1399
|
+
if (parsed.success)
|
|
1400
|
+
return [];
|
|
1401
|
+
const problems = [];
|
|
1402
|
+
for (const issue of parsed.error.issues) {
|
|
1403
|
+
if (issue.code === "unrecognized_keys") {
|
|
1404
|
+
for (const key of issue.keys) {
|
|
1405
|
+
problems.push({ path: dotted([...issue.path, key]), message: undeclared });
|
|
1406
|
+
}
|
|
1407
|
+
} else if (isMissingKey(raw, issue.path)) {
|
|
1408
|
+
problems.push({ path: dotted(issue.path), message: "is required" });
|
|
1409
|
+
} else {
|
|
1410
|
+
problems.push({ path: dotted(issue.path), message: issue.message });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return problems;
|
|
1414
|
+
}
|
|
1415
|
+
function shown(value) {
|
|
1416
|
+
return looksLikeSecret(value) ? "a value that looks like a credential" : `"${value}"`;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// ../events/dist/system-description.js
|
|
1420
|
+
import { z as z8 } from "zod";
|
|
1421
|
+
var SYSTEM_DESCRIPTION_FORMAT = 1;
|
|
1422
|
+
var SYSTEM_PARTS_MAX = 60;
|
|
1423
|
+
var SYSTEM_CONNECTIONS_MAX = 400;
|
|
1424
|
+
var SYSTEM_SLOT_MAX = 11;
|
|
1425
|
+
var SYSTEM_OWNS_MAX = 20;
|
|
1426
|
+
var SYSTEM_OWNED_PATTERN_MAX = 200;
|
|
1427
|
+
var SYSTEM_PART_NAME_MAX = 60;
|
|
1428
|
+
var SYSTEM_PART_DESCRIPTION_MAX = 160;
|
|
1429
|
+
var SYSTEM_CONNECTION_LABEL_MAX = 60;
|
|
1430
|
+
var SYSTEM_PART_ID_PATTERN = /^[a-z][a-z0-9-]{0,39}$/;
|
|
1431
|
+
var SYSTEM_LANES = ["clients", "edge", "services", "data", "external"];
|
|
1432
|
+
var systemLaneSchema = z8.enum(SYSTEM_LANES, {
|
|
1433
|
+
error: `must be one of ${SYSTEM_LANES.join(", ")}`
|
|
1434
|
+
});
|
|
1435
|
+
var SYSTEM_CONNECTION_KINDS = ["call", "event", "write", "external"];
|
|
1436
|
+
var systemConnectionKindSchema = z8.enum(SYSTEM_CONNECTION_KINDS, {
|
|
1437
|
+
error: `must be one of ${SYSTEM_CONNECTION_KINDS.join(", ")}`
|
|
1438
|
+
});
|
|
1439
|
+
var LINE_SEPARATOR = /[\u2028\u2029]/;
|
|
1440
|
+
function descriptionText(max) {
|
|
1441
|
+
return statementText(max).refine((v) => v.trim().length > 0, { message: "must not be empty or only spaces" }).refine((v) => !LINE_SEPARATOR.test(v), {
|
|
1442
|
+
message: "must be one line: it holds a line or paragraph separator"
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
var systemPartIdSchema = z8.string().regex(SYSTEM_PART_ID_PATTERN, {
|
|
1446
|
+
message: "must be a short id: a lowercase letter, then lowercase letters, digits or hyphens, 40 characters at most"
|
|
1447
|
+
}).refine((v) => !looksLikeSecret(v), { message: "looks like a password or key; choose another id" });
|
|
1448
|
+
function hasEmptySegment(pattern) {
|
|
1449
|
+
return pattern.split("/").some((segment) => segment.length === 0);
|
|
1450
|
+
}
|
|
1451
|
+
function hasDotSegment(pattern) {
|
|
1452
|
+
return pattern.split("/").some((segment) => segment === "." || segment === "..");
|
|
1453
|
+
}
|
|
1454
|
+
function hasPartialDoubleStar(pattern) {
|
|
1455
|
+
return pattern.split("/").some((segment) => segment.includes("**") && segment !== "**");
|
|
1456
|
+
}
|
|
1457
|
+
var SEGMENT_CHARACTERS = /^[A-Za-z0-9._\-*]*$/;
|
|
1458
|
+
function hasForeignCharacter(pattern) {
|
|
1459
|
+
return pattern.split("/").some((segment) => !SEGMENT_CHARACTERS.test(segment));
|
|
1460
|
+
}
|
|
1461
|
+
var systemOwnedPatternSchema = z8.string().max(SYSTEM_OWNED_PATTERN_MAX, { message: `must be at most ${SYSTEM_OWNED_PATTERN_MAX} characters` }).refine((v) => !hasEmptySegment(v), {
|
|
1462
|
+
message: "must not start or end with a slash, or have two slashes together"
|
|
1463
|
+
}).refine((v) => !hasDotSegment(v), { message: 'must not have a "." or ".." folder in it' }).refine((v) => !hasPartialDoubleStar(v), { message: '"**" must stand alone between slashes' }).refine((v) => !hasForeignCharacter(v), {
|
|
1464
|
+
message: 'may use only letters, digits, ".", "_", "-" and "*" between slashes'
|
|
1465
|
+
}).refine((v) => !looksLikeSecret(v), {
|
|
1466
|
+
message: "this path looks like a credential; list a shorter folder above it"
|
|
1467
|
+
});
|
|
1468
|
+
var systemPartSchema = z8.object({
|
|
1469
|
+
id: systemPartIdSchema,
|
|
1470
|
+
name: descriptionText(SYSTEM_PART_NAME_MAX),
|
|
1471
|
+
description: descriptionText(SYSTEM_PART_DESCRIPTION_MAX),
|
|
1472
|
+
lane: systemLaneSchema,
|
|
1473
|
+
slot: z8.number({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).int({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).min(0, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).max(SYSTEM_SLOT_MAX, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }),
|
|
1474
|
+
outside: z8.boolean(),
|
|
1475
|
+
owns: z8.array(systemOwnedPatternSchema).max(SYSTEM_OWNS_MAX, { message: `can list at most ${SYSTEM_OWNS_MAX} paths` })
|
|
1476
|
+
}).strict();
|
|
1477
|
+
var systemConnectionSchema = z8.object({
|
|
1478
|
+
from: systemPartIdSchema,
|
|
1479
|
+
to: systemPartIdSchema,
|
|
1480
|
+
kind: systemConnectionKindSchema,
|
|
1481
|
+
label: descriptionText(SYSTEM_CONNECTION_LABEL_MAX)
|
|
1482
|
+
}).strict();
|
|
1483
|
+
var systemDescriptionSchema = z8.object({
|
|
1484
|
+
format: z8.literal(SYSTEM_DESCRIPTION_FORMAT, { error: `must be ${SYSTEM_DESCRIPTION_FORMAT}` }),
|
|
1485
|
+
parts: z8.array(systemPartSchema).min(1, { message: "must list at least one part" }).max(SYSTEM_PARTS_MAX, { message: `can hold at most ${SYSTEM_PARTS_MAX} parts (five lanes of twelve slots)` }),
|
|
1486
|
+
connections: z8.array(systemConnectionSchema).max(SYSTEM_CONNECTIONS_MAX, { message: `can hold at most ${SYSTEM_CONNECTIONS_MAX} connections` })
|
|
1487
|
+
}).strict();
|
|
1488
|
+
var systemDescriptionProblemSchema = z8.object({
|
|
1489
|
+
path: z8.string(),
|
|
1490
|
+
message: z8.string()
|
|
1491
|
+
}).strict();
|
|
1492
|
+
function shapeProblems(raw) {
|
|
1493
|
+
return shapeProblemsOf(systemDescriptionSchema, raw, "is not a field a description has");
|
|
1494
|
+
}
|
|
1495
|
+
function quoted(value) {
|
|
1496
|
+
return typeof value === "string" ? shown(value) : "another part";
|
|
1497
|
+
}
|
|
1498
|
+
function crossFieldProblems(raw) {
|
|
1499
|
+
const problems = [];
|
|
1500
|
+
if (!isRecord(raw))
|
|
1501
|
+
return problems;
|
|
1502
|
+
const parts = Array.isArray(raw.parts) ? raw.parts : [];
|
|
1503
|
+
const connections = Array.isArray(raw.connections) ? raw.connections : [];
|
|
1504
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1505
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1506
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1507
|
+
parts.forEach((part, i) => {
|
|
1508
|
+
if (!isRecord(part))
|
|
1509
|
+
return;
|
|
1510
|
+
if (typeof part.id === "string") {
|
|
1511
|
+
if (ids.has(part.id)) {
|
|
1512
|
+
problems.push({ path: `parts.${i}.id`, message: `another part already has the id ${shown(part.id)}` });
|
|
1513
|
+
}
|
|
1514
|
+
ids.add(part.id);
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof part.lane === "string" && typeof part.slot === "number") {
|
|
1517
|
+
const place = `${part.lane}\0${part.slot}`;
|
|
1518
|
+
const earlier = placed.get(place);
|
|
1519
|
+
if (earlier !== void 0) {
|
|
1520
|
+
problems.push({
|
|
1521
|
+
path: `parts.${i}.slot`,
|
|
1522
|
+
message: `${quoted(parts[earlier].id)} already sits in lane ${shown(part.lane)} at slot ${part.slot}`
|
|
1523
|
+
});
|
|
1524
|
+
} else {
|
|
1525
|
+
placed.set(place, i);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (typeof part.outside === "boolean" && Array.isArray(part.owns)) {
|
|
1529
|
+
if (part.outside && part.owns.length > 0) {
|
|
1530
|
+
problems.push({ path: `parts.${i}.owns`, message: "must be empty for a part outside the repository" });
|
|
1531
|
+
}
|
|
1532
|
+
if (!part.outside && part.owns.length === 0) {
|
|
1533
|
+
problems.push({ path: `parts.${i}.owns`, message: "must list at least one path this part owns" });
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (Array.isArray(part.owns)) {
|
|
1537
|
+
part.owns.forEach((pattern, k) => {
|
|
1538
|
+
if (typeof pattern !== "string")
|
|
1539
|
+
return;
|
|
1540
|
+
const owner = owners.get(pattern);
|
|
1541
|
+
if (owner === void 0) {
|
|
1542
|
+
owners.set(pattern, i);
|
|
1543
|
+
} else if (owner === i) {
|
|
1544
|
+
problems.push({ path: `parts.${i}.owns.${k}`, message: `${shown(pattern)} is listed twice in this part` });
|
|
1545
|
+
} else {
|
|
1546
|
+
problems.push({
|
|
1547
|
+
path: `parts.${i}.owns.${k}`,
|
|
1548
|
+
message: `${shown(pattern)} is already owned by ${quoted(parts[owner].id)}`
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
1555
|
+
connections.forEach((connection, i) => {
|
|
1556
|
+
if (!isRecord(connection))
|
|
1557
|
+
return;
|
|
1558
|
+
const { from, to } = connection;
|
|
1559
|
+
if (typeof from === "string" && !ids.has(from)) {
|
|
1560
|
+
problems.push({ path: `connections.${i}.from`, message: `names no part: ${shown(from)}` });
|
|
1561
|
+
}
|
|
1562
|
+
if (typeof to === "string" && !ids.has(to)) {
|
|
1563
|
+
problems.push({ path: `connections.${i}.to`, message: `names no part: ${shown(to)}` });
|
|
1564
|
+
}
|
|
1565
|
+
if (typeof from === "string" && typeof to === "string") {
|
|
1566
|
+
if (from === to) {
|
|
1567
|
+
problems.push({ path: `connections.${i}.to`, message: `must name a different part from "from" (${shown(from)})` });
|
|
1568
|
+
}
|
|
1569
|
+
const pair = `${from}\0${to}`;
|
|
1570
|
+
if (pairs.has(pair)) {
|
|
1571
|
+
problems.push({ path: `connections.${i}`, message: `another connection already goes from ${shown(from)} to ${shown(to)}` });
|
|
1572
|
+
}
|
|
1573
|
+
pairs.add(pair);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
return problems;
|
|
1577
|
+
}
|
|
1578
|
+
function checkSystemDescription(value) {
|
|
1579
|
+
const problems = [...shapeProblems(value), ...crossFieldProblems(value)];
|
|
1580
|
+
if (problems.length > 0)
|
|
1581
|
+
return { ok: false, problems };
|
|
1582
|
+
return { ok: true, description: systemDescriptionSchema.parse(value) };
|
|
1583
|
+
}
|
|
1584
|
+
function segmentMatches(pattern, segment) {
|
|
1585
|
+
const literals = pattern.split("*");
|
|
1586
|
+
if (literals.length === 1)
|
|
1587
|
+
return pattern === segment;
|
|
1588
|
+
const first = literals[0];
|
|
1589
|
+
const last = literals[literals.length - 1];
|
|
1590
|
+
if (!segment.startsWith(first))
|
|
1591
|
+
return false;
|
|
1592
|
+
let position = first.length;
|
|
1593
|
+
for (const literal of literals.slice(1, -1)) {
|
|
1594
|
+
const found = segment.indexOf(literal, position + 1);
|
|
1595
|
+
if (found === -1)
|
|
1596
|
+
return false;
|
|
1597
|
+
position = found + literal.length;
|
|
1598
|
+
}
|
|
1599
|
+
return segment.length - last.length >= position + 1 && segment.endsWith(last);
|
|
1600
|
+
}
|
|
1601
|
+
function patternOwns(pattern, path) {
|
|
1602
|
+
const want = pattern.split("/");
|
|
1603
|
+
const have = path.split("/");
|
|
1604
|
+
const width = have.length + 1;
|
|
1605
|
+
const seen = new Uint8Array((want.length + 1) * width);
|
|
1606
|
+
const from = (i, j) => {
|
|
1607
|
+
if (i === want.length)
|
|
1608
|
+
return true;
|
|
1609
|
+
const cell = i * width + j;
|
|
1610
|
+
if (seen[cell] !== 0)
|
|
1611
|
+
return seen[cell] === 2;
|
|
1612
|
+
const segment = want[i];
|
|
1613
|
+
const result = segment === "**" ? from(i + 1, j) || j < have.length && from(i, j + 1) : j < have.length && segmentMatches(segment, have[j]) && from(i + 1, j + 1);
|
|
1614
|
+
seen[cell] = result ? 2 : 1;
|
|
1615
|
+
return result;
|
|
1616
|
+
};
|
|
1617
|
+
return from(0, 0);
|
|
1618
|
+
}
|
|
1619
|
+
function specificity(pattern) {
|
|
1620
|
+
const segments = pattern.split("/");
|
|
1621
|
+
const firstWildcard = segments.findIndex((segment) => segment.includes("*"));
|
|
1622
|
+
return {
|
|
1623
|
+
leadingLiterals: firstWildcard === -1 ? segments.length : firstWildcard,
|
|
1624
|
+
wildcards: segments.filter((segment) => segment.includes("*")).length,
|
|
1625
|
+
literalCharacters: pattern.replace(/[*/]/g, "").length
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function outranks(a, b) {
|
|
1629
|
+
if (a.rank.leadingLiterals !== b.rank.leadingLiterals)
|
|
1630
|
+
return b.rank.leadingLiterals - a.rank.leadingLiterals;
|
|
1631
|
+
if (a.rank.wildcards !== b.rank.wildcards)
|
|
1632
|
+
return a.rank.wildcards - b.rank.wildcards;
|
|
1633
|
+
if (a.rank.literalCharacters !== b.rank.literalCharacters)
|
|
1634
|
+
return b.rank.literalCharacters - a.rank.literalCharacters;
|
|
1635
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1636
|
+
}
|
|
1637
|
+
function ownerOf(path, description) {
|
|
1638
|
+
let best = null;
|
|
1639
|
+
for (const part of description.parts) {
|
|
1640
|
+
for (const pattern of part.owns) {
|
|
1641
|
+
if (!patternOwns(pattern, path))
|
|
1642
|
+
continue;
|
|
1643
|
+
const candidate = { id: part.id, rank: specificity(pattern) };
|
|
1644
|
+
if (best === null || outranks(candidate, best) < 0)
|
|
1645
|
+
best = candidate;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return best === null ? null : best.id;
|
|
1649
|
+
}
|
|
1650
|
+
var systemReportRecordedSchema = z8.object({
|
|
1651
|
+
outcome: z8.literal("recorded"),
|
|
1652
|
+
version: z8.number().int().min(1),
|
|
1653
|
+
reportedAt: utcIso8601
|
|
1654
|
+
}).strict();
|
|
1655
|
+
var systemReportRefusedSchema = z8.object({
|
|
1656
|
+
outcome: z8.literal("refused"),
|
|
1657
|
+
problems: z8.array(systemDescriptionProblemSchema).min(1)
|
|
1658
|
+
}).strict();
|
|
1659
|
+
var systemReportUnavailableSchema = z8.object({
|
|
1660
|
+
outcome: z8.literal("unavailable")
|
|
1661
|
+
}).strict();
|
|
1662
|
+
var systemReportResultSchema = z8.discriminatedUnion("outcome", [
|
|
1663
|
+
systemReportRecordedSchema,
|
|
1664
|
+
systemReportRefusedSchema,
|
|
1665
|
+
systemReportUnavailableSchema
|
|
1666
|
+
]);
|
|
1667
|
+
var SYSTEM_DRAW_SVG_MAX_BYTES = 2 * 1024 * 1024;
|
|
1668
|
+
var SYSTEM_DRAW_MERMAID_MAX_BYTES = 256 * 1024;
|
|
1669
|
+
var SYSTEM_DRAW_PNG_MAX_BYTES = 8 * 1024 * 1024;
|
|
1670
|
+
var SYSTEM_DIAGRAM_MARKER_LINE = "<!-- drawn by Halfcycle from docs/system.json -->";
|
|
1671
|
+
var systemDrawArchitectureRequestSchema = z8.object({
|
|
1672
|
+
picture: z8.literal("architecture"),
|
|
1673
|
+
version: z8.number().int().min(1)
|
|
1674
|
+
}).strict();
|
|
1675
|
+
var systemDrawPhaseRequestSchema = z8.object({
|
|
1676
|
+
picture: z8.literal("phase"),
|
|
1677
|
+
phase: phaseIdentitySchema,
|
|
1678
|
+
planVersion: z8.number().int().min(1)
|
|
1679
|
+
}).strict();
|
|
1680
|
+
var systemDrawRequestSchema = z8.discriminatedUnion("picture", [
|
|
1681
|
+
systemDrawArchitectureRequestSchema,
|
|
1682
|
+
systemDrawPhaseRequestSchema
|
|
1683
|
+
]);
|
|
1684
|
+
function utf8ByteLength2(text) {
|
|
1685
|
+
let bytes = 0;
|
|
1686
|
+
for (let i = 0; i < text.length; i++) {
|
|
1687
|
+
const unit = text.charCodeAt(i);
|
|
1688
|
+
if (unit < 128)
|
|
1689
|
+
bytes += 1;
|
|
1690
|
+
else if (unit < 2048)
|
|
1691
|
+
bytes += 2;
|
|
1692
|
+
else if (unit >= 55296 && unit <= 56319 && i + 1 < text.length) {
|
|
1693
|
+
const next = text.charCodeAt(i + 1);
|
|
1694
|
+
if (next >= 56320 && next <= 57343) {
|
|
1695
|
+
bytes += 4;
|
|
1696
|
+
i++;
|
|
1697
|
+
} else {
|
|
1698
|
+
bytes += 3;
|
|
1699
|
+
}
|
|
1700
|
+
} else
|
|
1701
|
+
bytes += 3;
|
|
1702
|
+
}
|
|
1703
|
+
return bytes;
|
|
1704
|
+
}
|
|
1705
|
+
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
1706
|
+
function base64DecodedLength(text) {
|
|
1707
|
+
const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
|
|
1708
|
+
return text.length / 4 * 3 - padding;
|
|
1709
|
+
}
|
|
1710
|
+
var SYSTEM_PICTURE_THEMES = ["broadsheet", "flightdeck"];
|
|
1711
|
+
var systemPictureSchema = z8.object({
|
|
1712
|
+
svg: z8.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_SVG_MAX_BYTES, { message: "the picture is larger than 2 MB" }),
|
|
1713
|
+
png: z8.string().min(1).refine((v) => v.length % 4 === 0 && BASE64.test(v), { message: "the image is not valid base64" }).refine((v) => base64DecodedLength(v) <= SYSTEM_DRAW_PNG_MAX_BYTES, { message: "the image is larger than 8 MB" })
|
|
1714
|
+
}).strict();
|
|
1715
|
+
var systemPicturesSchema = /* @__PURE__ */ z8.object(Object.fromEntries(SYSTEM_PICTURE_THEMES.map((theme) => [theme, systemPictureSchema]))).strict();
|
|
1716
|
+
var systemDrawDrawnSchema = /* @__PURE__ */ z8.object({
|
|
1717
|
+
outcome: z8.literal("drawn"),
|
|
1718
|
+
mermaid: z8.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_MERMAID_MAX_BYTES, {
|
|
1719
|
+
message: "the diagram text is larger than 256 KB"
|
|
1720
|
+
}),
|
|
1721
|
+
pictures: systemPicturesSchema
|
|
1722
|
+
}).strict();
|
|
1723
|
+
var systemDrawRefusedSchema = z8.object({
|
|
1724
|
+
outcome: z8.literal("refused"),
|
|
1725
|
+
reason: z8.string().min(1)
|
|
1726
|
+
}).strict();
|
|
1727
|
+
var systemDrawUnavailableSchema = z8.object({
|
|
1728
|
+
outcome: z8.literal("unavailable")
|
|
1729
|
+
}).strict();
|
|
1730
|
+
var systemDrawResultSchema = /* @__PURE__ */ z8.discriminatedUnion("outcome", [
|
|
1731
|
+
systemDrawDrawnSchema,
|
|
1732
|
+
systemDrawRefusedSchema,
|
|
1733
|
+
systemDrawUnavailableSchema
|
|
1734
|
+
]);
|
|
1735
|
+
|
|
1736
|
+
// ../events/dist/phase-plan.js
|
|
1737
|
+
var PHASE_PLAN_FORMAT = 1;
|
|
1738
|
+
var PHASE_PLAN_CHANGES_MAX = 60;
|
|
1739
|
+
var PHASE_PLAN_SUMMARY_MAX = 160;
|
|
1740
|
+
var SCOPE_NOTE_MAX = 500;
|
|
1741
|
+
var PHASE_PLAN_PHASE_MAX = buildPhaseSchema.maxLength ?? 0;
|
|
1742
|
+
var PHASE_PLAN_CHANGE_KINDS = ["add", "change", "remove"];
|
|
1743
|
+
var phasePlanChangeSchema = z9.enum(PHASE_PLAN_CHANGE_KINDS, {
|
|
1744
|
+
error: `must be one of ${PHASE_PLAN_CHANGE_KINDS.join(", ")}`
|
|
1745
|
+
});
|
|
1746
|
+
var phasePlanPhaseSchema = phaseIdentitySchema.refine((v) => !isValidPhaseIdentity(v) || buildPhaseSchema.safeParse(String(v)).success, { message: `must be at most ${PHASE_PLAN_PHASE_MAX} characters` });
|
|
1747
|
+
var phasePlanSummarySchema = descriptionText(PHASE_PLAN_SUMMARY_MAX);
|
|
1748
|
+
var phasePlanEntrySchema = z9.object({
|
|
1749
|
+
part: systemPartIdSchema,
|
|
1750
|
+
change: phasePlanChangeSchema,
|
|
1751
|
+
summary: phasePlanSummarySchema
|
|
1752
|
+
}).strict();
|
|
1753
|
+
var phasePlanSchema = z9.object({
|
|
1754
|
+
format: z9.literal(PHASE_PLAN_FORMAT, { error: `must be ${PHASE_PLAN_FORMAT}` }),
|
|
1755
|
+
phase: phasePlanPhaseSchema,
|
|
1756
|
+
changes: z9.array(phasePlanEntrySchema).min(1, { message: "must list at least one part" }).max(PHASE_PLAN_CHANGES_MAX, { message: `can list at most ${PHASE_PLAN_CHANGES_MAX} parts` })
|
|
1757
|
+
}).strict();
|
|
1758
|
+
function planCrossFieldProblems(raw, description) {
|
|
1759
|
+
const problems = [];
|
|
1760
|
+
if (!isRecord(raw) || !Array.isArray(raw.changes))
|
|
1761
|
+
return problems;
|
|
1762
|
+
const known = new Set(description.parts.map((p) => p.id));
|
|
1763
|
+
const named = /* @__PURE__ */ new Map();
|
|
1764
|
+
raw.changes.forEach((change, i) => {
|
|
1765
|
+
if (!isRecord(change) || typeof change.part !== "string")
|
|
1766
|
+
return;
|
|
1767
|
+
if (!known.has(change.part)) {
|
|
1768
|
+
problems.push({ path: `changes.${i}.part`, message: `names no part in the system description: ${shown(change.part)}` });
|
|
1769
|
+
}
|
|
1770
|
+
const earlier = named.get(change.part);
|
|
1771
|
+
if (earlier === void 0) {
|
|
1772
|
+
named.set(change.part, i);
|
|
1773
|
+
} else {
|
|
1774
|
+
problems.push({ path: `changes.${i}.part`, message: `${shown(change.part)} is already listed at changes.${earlier}` });
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
return problems;
|
|
1778
|
+
}
|
|
1779
|
+
function checkPhasePlan(value, description) {
|
|
1780
|
+
const problems = [
|
|
1781
|
+
...shapeProblemsOf(phasePlanSchema, value, "is not a field a plan has"),
|
|
1782
|
+
...planCrossFieldProblems(value, description)
|
|
1783
|
+
];
|
|
1784
|
+
if (problems.length > 0)
|
|
1785
|
+
return { ok: false, problems };
|
|
1786
|
+
return { ok: true, plan: phasePlanSchema.parse(value) };
|
|
1787
|
+
}
|
|
1788
|
+
var scopePhaseSchema = buildPhaseSchema.refine((v) => isValidPhaseIdentity(v), {
|
|
1789
|
+
message: "must be a phase name"
|
|
1790
|
+
});
|
|
1791
|
+
var SCOPE_STATE_NAMES = [
|
|
1792
|
+
"not-planned",
|
|
1793
|
+
"waiting",
|
|
1794
|
+
"sent-back",
|
|
1795
|
+
"approved",
|
|
1796
|
+
"partly-stale",
|
|
1797
|
+
"withdrawn"
|
|
1798
|
+
];
|
|
1799
|
+
var scopeStateNameSchema = z9.enum(SCOPE_STATE_NAMES);
|
|
1800
|
+
var plannedScopeStateSchema = scopeStateNameSchema.exclude(["not-planned"]);
|
|
1801
|
+
var SCOPE_PART_STATUSES = ["approved", "waiting"];
|
|
1802
|
+
var scopePartStatusSchema = z9.enum(SCOPE_PART_STATUSES);
|
|
1803
|
+
var SCOPE_ACTS = ["approve", "send-back", "withdraw"];
|
|
1804
|
+
var scopeActSchema = z9.enum(SCOPE_ACTS);
|
|
1805
|
+
var SCOPE_GIVEN_IN = ["console", "chat"];
|
|
1806
|
+
var scopeGivenInSchema = z9.enum(SCOPE_GIVEN_IN);
|
|
1807
|
+
var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
|
|
1808
|
+
var scopeNoteWireSchema = z9.string().max(SCOPE_NOTE_MAX);
|
|
1809
|
+
var SCOPE_PART_VERDICTS = ["ok", "change"];
|
|
1810
|
+
var scopePartVerdictSchema = z9.enum(SCOPE_PART_VERDICTS);
|
|
1811
|
+
var scopeDecisionPartSchema = z9.object({
|
|
1812
|
+
part: systemPartIdSchema,
|
|
1813
|
+
verdict: scopePartVerdictSchema,
|
|
1814
|
+
note: scopeNoteWireSchema.optional()
|
|
1815
|
+
}).strict();
|
|
1816
|
+
var planVersionSchema = z9.number().int().min(1);
|
|
1817
|
+
var phasePlanRequestSchema = z9.object({
|
|
1818
|
+
descriptionVersion: z9.number().int().min(1),
|
|
1819
|
+
plan: z9.unknown()
|
|
1820
|
+
}).strict();
|
|
1821
|
+
var phasePlanRecordedSchema = z9.object({
|
|
1822
|
+
outcome: z9.literal("recorded"),
|
|
1823
|
+
version: planVersionSchema,
|
|
1824
|
+
phaseNumber: z9.number().int().min(1),
|
|
1825
|
+
state: plannedScopeStateSchema,
|
|
1826
|
+
changedParts: z9.array(systemPartIdSchema).max(PHASE_PLAN_CHANGES_MAX),
|
|
1827
|
+
typedApprovalOffered: z9.boolean(),
|
|
1828
|
+
reportedAt: utcIso8601
|
|
1829
|
+
}).strict();
|
|
1830
|
+
var phasePlanRefusedSchema = z9.object({
|
|
1831
|
+
outcome: z9.literal("refused"),
|
|
1832
|
+
problems: z9.array(systemDescriptionProblemSchema).min(1)
|
|
1833
|
+
}).strict();
|
|
1834
|
+
var phasePlanUnavailableSchema = z9.object({
|
|
1835
|
+
outcome: z9.literal("unavailable")
|
|
1836
|
+
}).strict();
|
|
1837
|
+
var phasePlanResultSchema = z9.discriminatedUnion("outcome", [
|
|
1838
|
+
phasePlanRecordedSchema,
|
|
1839
|
+
phasePlanRefusedSchema,
|
|
1840
|
+
phasePlanUnavailableSchema
|
|
1841
|
+
]);
|
|
1842
|
+
var scopeApproveRequestSchema = z9.object({
|
|
1843
|
+
act: z9.literal("approve"),
|
|
1844
|
+
planVersion: planVersionSchema,
|
|
1845
|
+
parts: z9.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX)
|
|
1846
|
+
}).strict();
|
|
1847
|
+
var scopeSendBackRequestSchema = z9.object({
|
|
1848
|
+
act: z9.literal("send-back"),
|
|
1849
|
+
planVersion: planVersionSchema,
|
|
1850
|
+
parts: z9.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
|
|
1851
|
+
note: scopeNoteWireSchema.optional()
|
|
1852
|
+
}).strict();
|
|
1853
|
+
var scopeWithdrawRequestSchema = z9.object({
|
|
1854
|
+
act: z9.literal("withdraw"),
|
|
1855
|
+
planVersion: planVersionSchema
|
|
1856
|
+
}).strict();
|
|
1857
|
+
var scopeDecisionRequestSchema = z9.discriminatedUnion("act", [
|
|
1858
|
+
scopeApproveRequestSchema,
|
|
1859
|
+
scopeSendBackRequestSchema,
|
|
1860
|
+
scopeWithdrawRequestSchema
|
|
1861
|
+
]);
|
|
1862
|
+
var scopeDecisionRecordedSchema = z9.object({
|
|
1863
|
+
outcome: z9.literal("recorded"),
|
|
1864
|
+
act: scopeActSchema,
|
|
1865
|
+
phase: scopePhaseSchema,
|
|
1866
|
+
planVersion: planVersionSchema,
|
|
1867
|
+
state: plannedScopeStateSchema,
|
|
1868
|
+
decidedAt: utcIso8601
|
|
1869
|
+
}).strict();
|
|
1870
|
+
var scopeDecisionAlreadyApprovedSchema = z9.object({
|
|
1871
|
+
outcome: z9.literal("already-approved"),
|
|
1872
|
+
replanNeeded: z9.boolean(),
|
|
1873
|
+
reason: z9.string().min(1)
|
|
1874
|
+
}).strict();
|
|
1875
|
+
var scopeDecisionRefusedSchema = z9.object({
|
|
1876
|
+
outcome: z9.literal("refused"),
|
|
1877
|
+
problems: z9.array(systemDescriptionProblemSchema).min(1)
|
|
1878
|
+
}).strict();
|
|
1879
|
+
var scopeDecisionUnavailableSchema = z9.object({
|
|
1880
|
+
outcome: z9.literal("unavailable")
|
|
1881
|
+
}).strict();
|
|
1882
|
+
function reasonOutcome(outcome) {
|
|
1883
|
+
return z9.object({ outcome: z9.literal(outcome), reason: z9.string().min(1) }).strict();
|
|
1884
|
+
}
|
|
1885
|
+
var scopeDecisionResultSchema = z9.discriminatedUnion("outcome", [
|
|
1886
|
+
scopeDecisionRecordedSchema,
|
|
1887
|
+
scopeDecisionAlreadyApprovedSchema,
|
|
1888
|
+
scopeDecisionRefusedSchema,
|
|
1889
|
+
scopeDecisionUnavailableSchema,
|
|
1890
|
+
reasonOutcome("not-owner"),
|
|
1891
|
+
reasonOutcome("no-owner"),
|
|
1892
|
+
reasonOutcome("plan-changed"),
|
|
1893
|
+
reasonOutcome("nothing-to-withdraw"),
|
|
1894
|
+
reasonOutcome("build-started")
|
|
1895
|
+
]);
|
|
1896
|
+
var scopeReadPartSchema = z9.object({
|
|
1897
|
+
part: systemPartIdSchema,
|
|
1898
|
+
name: systemPartSchema.shape.name,
|
|
1899
|
+
change: phasePlanChangeSchema,
|
|
1900
|
+
summary: phasePlanSummarySchema,
|
|
1901
|
+
status: scopePartStatusSchema,
|
|
1902
|
+
changed: z9.boolean()
|
|
1903
|
+
}).strict();
|
|
1904
|
+
var scopeReadDecisionSchema = z9.object({
|
|
1905
|
+
act: scopeActSchema,
|
|
1906
|
+
planVersion: planVersionSchema,
|
|
1907
|
+
givenIn: scopeGivenInSchema,
|
|
1908
|
+
at: utcIso8601,
|
|
1909
|
+
parts: z9.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
|
|
1910
|
+
note: scopeNoteWireSchema.optional()
|
|
1911
|
+
}).strict();
|
|
1912
|
+
var scopeReadNotPlannedSchema = z9.object({
|
|
1913
|
+
phase: scopePhaseSchema,
|
|
1914
|
+
state: z9.literal("not-planned"),
|
|
1915
|
+
canDecide: z9.boolean()
|
|
1916
|
+
}).strict();
|
|
1917
|
+
var scopeReadPlannedSchema = z9.object({
|
|
1918
|
+
phase: scopePhaseSchema,
|
|
1919
|
+
state: plannedScopeStateSchema,
|
|
1920
|
+
phaseNumber: z9.number().int().min(1),
|
|
1921
|
+
planVersion: planVersionSchema,
|
|
1922
|
+
parts: z9.array(scopeReadPartSchema).min(1).max(PHASE_PLAN_CHANGES_MAX),
|
|
1923
|
+
decisions: z9.array(scopeReadDecisionSchema),
|
|
1924
|
+
canDecide: z9.boolean(),
|
|
1925
|
+
replanLine: z9.string().min(1).optional()
|
|
1926
|
+
}).strict();
|
|
1927
|
+
var scopeReadSchema = z9.union([scopeReadNotPlannedSchema, scopeReadPlannedSchema]);
|
|
1928
|
+
|
|
1929
|
+
// ../events/dist/client-version.js
|
|
1930
|
+
var CHAT_APPROVAL_CLIENT_VERSION = "0.4.7";
|
|
1931
|
+
function clientMeetsChatApprovalFloor(version) {
|
|
1932
|
+
return isReleaseVersion(version) && compareReleaseVersions(version, CHAT_APPROVAL_CLIENT_VERSION) >= 0;
|
|
1933
|
+
}
|
|
1934
|
+
var TYPED_APPROVAL_OFF_SENTENCE = "Approving by typing is switched off for now.";
|
|
1935
|
+
var CLIENT_UPDATE_SENTENCE = `To approve by typing in a session, Halfcycle needs updating: run \`${HALFCYCLE_COMMAND}\` in this project's folder, then reopen your session and ask again.`;
|
|
1936
|
+
|
|
1937
|
+
// ../events/dist/device-auth.js
|
|
1938
|
+
import { z as z10 } from "zod";
|
|
1939
|
+
var utcIso86013 = z10.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");
|
|
1096
1940
|
var DEVICE_AUTH_STATUS = {
|
|
1097
1941
|
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
1098
1942
|
PENDING: "pending",
|
|
@@ -1133,25 +1977,25 @@ var DEVICE_POLL_REFUSALS = [
|
|
|
1133
1977
|
DEVICE_AUTH_STATUS.EXPIRED,
|
|
1134
1978
|
DEVICE_AUTH_STATUS.UNKNOWN
|
|
1135
1979
|
];
|
|
1136
|
-
var deviceAuthStartResponseSchema =
|
|
1137
|
-
deviceCode:
|
|
1138
|
-
userCode:
|
|
1139
|
-
verificationUrl:
|
|
1980
|
+
var deviceAuthStartResponseSchema = z10.object({
|
|
1981
|
+
deviceCode: z10.string().min(1),
|
|
1982
|
+
userCode: z10.string().min(1),
|
|
1983
|
+
verificationUrl: z10.string().min(1),
|
|
1140
1984
|
expiresAt: utcIso86013,
|
|
1141
|
-
pollIntervalMs:
|
|
1985
|
+
pollIntervalMs: z10.number().int().positive()
|
|
1142
1986
|
}).strict();
|
|
1143
|
-
var devicePollPendingSchema =
|
|
1144
|
-
status:
|
|
1145
|
-
pollIntervalMs:
|
|
1987
|
+
var devicePollPendingSchema = z10.object({
|
|
1988
|
+
status: z10.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
1989
|
+
pollIntervalMs: z10.number().int().positive()
|
|
1146
1990
|
}).strict();
|
|
1147
|
-
var devicePollApprovedSchema =
|
|
1148
|
-
status:
|
|
1149
|
-
accountId:
|
|
1150
|
-
credential:
|
|
1991
|
+
var devicePollApprovedSchema = z10.object({
|
|
1992
|
+
status: z10.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
1993
|
+
accountId: z10.string().min(1),
|
|
1994
|
+
credential: z10.string().min(1),
|
|
1151
1995
|
expiresAt: utcIso86013
|
|
1152
1996
|
}).strict();
|
|
1153
|
-
var deviceAuthRefusalSchema =
|
|
1154
|
-
status:
|
|
1997
|
+
var deviceAuthRefusalSchema = z10.object({
|
|
1998
|
+
status: z10.enum([
|
|
1155
1999
|
DEVICE_AUTH_STATUS.DECLINED,
|
|
1156
2000
|
DEVICE_AUTH_STATUS.EXPIRED,
|
|
1157
2001
|
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
@@ -1162,25 +2006,25 @@ var deviceAuthRefusalSchema = z7.object({
|
|
|
1162
2006
|
DEVICE_AUTH_STATUS.ALREADY_DECIDED,
|
|
1163
2007
|
DEVICE_AUTH_STATUS.TERMS_REQUIRED
|
|
1164
2008
|
]),
|
|
1165
|
-
message:
|
|
2009
|
+
message: z10.string().min(1)
|
|
1166
2010
|
}).strict();
|
|
1167
|
-
var devicePollResponseSchema =
|
|
2011
|
+
var devicePollResponseSchema = z10.union([
|
|
1168
2012
|
devicePollPendingSchema,
|
|
1169
2013
|
devicePollApprovedSchema,
|
|
1170
2014
|
deviceAuthRefusalSchema
|
|
1171
2015
|
]);
|
|
1172
|
-
var deviceConfirmRequestSchema =
|
|
1173
|
-
userCode:
|
|
1174
|
-
decision:
|
|
1175
|
-
externalAuthId:
|
|
1176
|
-
email:
|
|
1177
|
-
acceptTerms:
|
|
2016
|
+
var deviceConfirmRequestSchema = z10.object({
|
|
2017
|
+
userCode: z10.string().min(1),
|
|
2018
|
+
decision: z10.enum(["approve", "decline"]),
|
|
2019
|
+
externalAuthId: z10.string().min(1).optional(),
|
|
2020
|
+
email: z10.string().optional(),
|
|
2021
|
+
acceptTerms: z10.boolean().optional()
|
|
1178
2022
|
}).strict();
|
|
1179
|
-
var deviceConfirmRecordedSchema =
|
|
1180
|
-
status:
|
|
1181
|
-
accountId:
|
|
2023
|
+
var deviceConfirmRecordedSchema = z10.object({
|
|
2024
|
+
status: z10.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
2025
|
+
accountId: z10.string().min(1).optional()
|
|
1182
2026
|
}).strict();
|
|
1183
|
-
var deviceConfirmResponseSchema =
|
|
2027
|
+
var deviceConfirmResponseSchema = z10.union([
|
|
1184
2028
|
deviceConfirmRecordedSchema,
|
|
1185
2029
|
deviceAuthRefusalSchema
|
|
1186
2030
|
]);
|
|
@@ -1209,45 +2053,45 @@ function loopbackVerificationUrl(verificationUrl, target) {
|
|
|
1209
2053
|
}
|
|
1210
2054
|
|
|
1211
2055
|
// ../events/dist/join-refusal.js
|
|
1212
|
-
import { z as
|
|
2056
|
+
import { z as z11 } from "zod";
|
|
1213
2057
|
var NOT_THIS_ACCOUNT_REASON = "not-this-account";
|
|
1214
2058
|
var JOIN_REFUSAL_STATUS = 403;
|
|
1215
|
-
var notThisAccountRefusalSchema =
|
|
1216
|
-
statusCode:
|
|
1217
|
-
error:
|
|
1218
|
-
message:
|
|
1219
|
-
reason:
|
|
2059
|
+
var notThisAccountRefusalSchema = z11.object({
|
|
2060
|
+
statusCode: z11.literal(JOIN_REFUSAL_STATUS),
|
|
2061
|
+
error: z11.string(),
|
|
2062
|
+
message: z11.string(),
|
|
2063
|
+
reason: z11.literal(NOT_THIS_ACCOUNT_REASON)
|
|
1220
2064
|
}).strict();
|
|
1221
|
-
var joinRefusalSchema =
|
|
2065
|
+
var joinRefusalSchema = z11.union([notThisAccountRefusalSchema, wireErrorSchema]);
|
|
1222
2066
|
function isNotThisAccountRefusal(body) {
|
|
1223
2067
|
return typeof body === "object" && body !== null && body.reason === NOT_THIS_ACCOUNT_REASON;
|
|
1224
2068
|
}
|
|
1225
2069
|
|
|
1226
2070
|
// ../events/dist/account-identity.js
|
|
1227
|
-
import { z as
|
|
1228
|
-
var accountIdentitySchema =
|
|
1229
|
-
accountId:
|
|
1230
|
-
email:
|
|
2071
|
+
import { z as z12 } from "zod";
|
|
2072
|
+
var accountIdentitySchema = z12.object({
|
|
2073
|
+
accountId: z12.string().min(1),
|
|
2074
|
+
email: z12.string().min(1).optional()
|
|
1231
2075
|
}).strict();
|
|
1232
2076
|
function safeParseAccountIdentity(payload) {
|
|
1233
2077
|
return accountIdentitySchema.safeParse(payload);
|
|
1234
2078
|
}
|
|
1235
2079
|
|
|
1236
2080
|
// ../events/dist/credential-wire.js
|
|
1237
|
-
import { z as
|
|
1238
|
-
var engagementCredentialResponseSchema =
|
|
1239
|
-
engagementId:
|
|
1240
|
-
sessionToken:
|
|
1241
|
-
mcpUrl:
|
|
1242
|
-
guardUrl:
|
|
1243
|
-
controlTelemetryUrl:
|
|
1244
|
-
methodVersion:
|
|
1245
|
-
consoleUrl:
|
|
2081
|
+
import { z as z13 } from "zod";
|
|
2082
|
+
var engagementCredentialResponseSchema = z13.object({
|
|
2083
|
+
engagementId: z13.string(),
|
|
2084
|
+
sessionToken: z13.string(),
|
|
2085
|
+
mcpUrl: z13.string(),
|
|
2086
|
+
guardUrl: z13.string(),
|
|
2087
|
+
controlTelemetryUrl: z13.string(),
|
|
2088
|
+
methodVersion: z13.string().optional(),
|
|
2089
|
+
consoleUrl: z13.string().optional()
|
|
1246
2090
|
}).strict();
|
|
1247
|
-
var ciTokenExchangeResponseSchema =
|
|
1248
|
-
token:
|
|
2091
|
+
var ciTokenExchangeResponseSchema = z13.object({
|
|
2092
|
+
token: z13.string(),
|
|
1249
2093
|
expiresAt: utcIso8601,
|
|
1250
|
-
engagementId:
|
|
2094
|
+
engagementId: z13.string()
|
|
1251
2095
|
}).strict();
|
|
1252
2096
|
var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
|
|
1253
2097
|
var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
|
|
@@ -1264,44 +2108,44 @@ var PLACEHOLDER_SENTENCES = Object.freeze({
|
|
|
1264
2108
|
});
|
|
1265
2109
|
|
|
1266
2110
|
// ../events/dist/project-engagement.js
|
|
1267
|
-
import { z as
|
|
1268
|
-
var projectIdSchema =
|
|
1269
|
-
var projectEngagementRequestSchema =
|
|
1270
|
-
derivedName:
|
|
2111
|
+
import { z as z14 } from "zod";
|
|
2112
|
+
var projectIdSchema = z14.string().uuid();
|
|
2113
|
+
var projectEngagementRequestSchema = z14.object({
|
|
2114
|
+
derivedName: z14.string()
|
|
1271
2115
|
}).strict();
|
|
1272
2116
|
|
|
1273
2117
|
// ../events/dist/phase-record.js
|
|
1274
|
-
import { z as
|
|
1275
|
-
var phaseRecordRefusalReasonSchema =
|
|
2118
|
+
import { z as z15 } from "zod";
|
|
2119
|
+
var phaseRecordRefusalReasonSchema = z15.enum([
|
|
1276
2120
|
"still-open",
|
|
1277
2121
|
"reopened",
|
|
1278
2122
|
"never-opened",
|
|
1279
2123
|
"never-closed",
|
|
1280
2124
|
"closed-before-records"
|
|
1281
2125
|
]);
|
|
1282
|
-
var phaseRecordRecordSchema =
|
|
1283
|
-
outcome:
|
|
2126
|
+
var phaseRecordRecordSchema = z15.object({
|
|
2127
|
+
outcome: z15.literal("record"),
|
|
1284
2128
|
phase: phaseNameSchema,
|
|
1285
2129
|
record: buildRecordV2Schema
|
|
1286
2130
|
}).strict().refine((body) => body.record.phase === body.phase, {
|
|
1287
2131
|
message: "the record is for a different phase than the one asked for",
|
|
1288
2132
|
path: ["record", "phase"]
|
|
1289
2133
|
});
|
|
1290
|
-
var phaseRecordWithheldSchema =
|
|
1291
|
-
outcome:
|
|
2134
|
+
var phaseRecordWithheldSchema = z15.object({
|
|
2135
|
+
outcome: z15.literal("withheld"),
|
|
1292
2136
|
phase: phaseNameSchema,
|
|
1293
|
-
sequence:
|
|
2137
|
+
sequence: z15.number().int().min(1),
|
|
1294
2138
|
closedAt: utcIso8601,
|
|
1295
|
-
message:
|
|
2139
|
+
message: z15.string()
|
|
1296
2140
|
}).strict();
|
|
1297
2141
|
var phaseRecordRefusedSchema = wireErrorSchema.extend({
|
|
1298
|
-
outcome:
|
|
2142
|
+
outcome: z15.literal("refused"),
|
|
1299
2143
|
phase: phaseNameSchema,
|
|
1300
|
-
statusCode:
|
|
1301
|
-
error:
|
|
2144
|
+
statusCode: z15.literal(409),
|
|
2145
|
+
error: z15.literal("PhaseRecordRefused"),
|
|
1302
2146
|
reason: phaseRecordRefusalReasonSchema
|
|
1303
2147
|
}).strict();
|
|
1304
|
-
var phaseRecordResponseSchema =
|
|
2148
|
+
var phaseRecordResponseSchema = z15.discriminatedUnion("outcome", [
|
|
1305
2149
|
phaseRecordRecordSchema,
|
|
1306
2150
|
phaseRecordWithheldSchema,
|
|
1307
2151
|
phaseRecordRefusedSchema
|
|
@@ -1375,8 +2219,8 @@ var ENV_FILENAME = ENGAGEMENT_ENV_FILENAME;
|
|
|
1375
2219
|
var HALFCYCLE_DIR = HALFCYCLE_DIR_NAME;
|
|
1376
2220
|
var InvalidEngagementIdError = class extends Error {
|
|
1377
2221
|
constructor(engagementId) {
|
|
1378
|
-
const
|
|
1379
|
-
super(`${
|
|
2222
|
+
const shown2 = JSON.stringify(engagementId.length > 80 ? `${engagementId.slice(0, 80)}\u2026` : engagementId);
|
|
2223
|
+
super(`${shown2} is not a Halfcycle project id, so it cannot name a folder on this machine and nothing was written. A project id looks like 3f2a1c4e-8b7d-4e2f-9a61-0c5d7e8f9b10. If it is the "${PIN_ENGAGEMENT_ID_FIELD}" in .halfcycle/bundle.json, delete that file and run "npx halfcycle" again: this repository is then set up as a new Halfcycle project.`);
|
|
1380
2224
|
this.name = "InvalidEngagementIdError";
|
|
1381
2225
|
}
|
|
1382
2226
|
};
|
|
@@ -1662,17 +2506,17 @@ function fileSha256(bytes) {
|
|
|
1662
2506
|
const text = typeof bytes === "string" ? bytes : Buffer2.from(bytes).toString("utf-8");
|
|
1663
2507
|
return sha256Hex(text.split("\n").map(lineKey).join("\n"));
|
|
1664
2508
|
}
|
|
1665
|
-
function
|
|
2509
|
+
function canonicalJson2(value) {
|
|
1666
2510
|
if (Array.isArray(value))
|
|
1667
|
-
return value.map(
|
|
2511
|
+
return value.map(canonicalJson2);
|
|
1668
2512
|
if (value !== null && typeof value === "object") {
|
|
1669
|
-
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k,
|
|
2513
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k, canonicalJson2(v)]);
|
|
1670
2514
|
return Object.fromEntries(entries);
|
|
1671
2515
|
}
|
|
1672
2516
|
return value;
|
|
1673
2517
|
}
|
|
1674
2518
|
function canonicalSha256(value) {
|
|
1675
|
-
return sha256Hex(JSON.stringify(
|
|
2519
|
+
return sha256Hex(JSON.stringify(canonicalJson2(value)));
|
|
1676
2520
|
}
|
|
1677
2521
|
function byString(a, b) {
|
|
1678
2522
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -1691,7 +2535,7 @@ function serializeManifest(manifest) {
|
|
|
1691
2535
|
leftAlone: [...manifest.leftAlone].sort(byString),
|
|
1692
2536
|
settings
|
|
1693
2537
|
};
|
|
1694
|
-
return JSON.stringify(
|
|
2538
|
+
return JSON.stringify(canonicalJson2(ordered), null, 2) + "\n";
|
|
1695
2539
|
}
|
|
1696
2540
|
var UnreadableManifestError = class extends Error {
|
|
1697
2541
|
constructor(reason) {
|
|
@@ -1700,7 +2544,7 @@ var UnreadableManifestError = class extends Error {
|
|
|
1700
2544
|
}
|
|
1701
2545
|
};
|
|
1702
2546
|
var HEX64 = /^[0-9a-f]{64}$/;
|
|
1703
|
-
function
|
|
2547
|
+
function isRecord2(value) {
|
|
1704
2548
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1705
2549
|
}
|
|
1706
2550
|
function stringArray(value, field) {
|
|
@@ -1721,7 +2565,7 @@ function bool(value, field) {
|
|
|
1721
2565
|
return value;
|
|
1722
2566
|
}
|
|
1723
2567
|
function appendedTextRecord(value, field) {
|
|
1724
|
-
if (!
|
|
2568
|
+
if (!isRecord2(value))
|
|
1725
2569
|
throw new UnreadableManifestError(`"${field}" is not an object`);
|
|
1726
2570
|
onlyKeys(value, ["created", "appended"], `"${field}"`);
|
|
1727
2571
|
return {
|
|
@@ -1741,7 +2585,7 @@ function parseManifest(text) {
|
|
|
1741
2585
|
} catch {
|
|
1742
2586
|
throw new UnreadableManifestError("it is not valid JSON");
|
|
1743
2587
|
}
|
|
1744
|
-
if (!
|
|
2588
|
+
if (!isRecord2(raw))
|
|
1745
2589
|
throw new UnreadableManifestError("it is not a JSON object");
|
|
1746
2590
|
onlyKeys(raw, ["format", "files", "createdDirs", "leftAlone", "settings", "mcp", "gitignore", "gitattributes"], "the manifest");
|
|
1747
2591
|
if (raw["format"] !== INSTALL_MANIFEST_FORMAT) {
|
|
@@ -1750,7 +2594,7 @@ function parseManifest(text) {
|
|
|
1750
2594
|
if (!Array.isArray(raw["files"]))
|
|
1751
2595
|
throw new UnreadableManifestError('"files" is not a list');
|
|
1752
2596
|
const files = raw["files"].map((entry) => {
|
|
1753
|
-
if (!
|
|
2597
|
+
if (!isRecord2(entry) || typeof entry["path"] !== "string") {
|
|
1754
2598
|
throw new UnreadableManifestError('a "files" entry has no path');
|
|
1755
2599
|
}
|
|
1756
2600
|
if (entry["perMachine"] === true) {
|
|
@@ -1764,7 +2608,7 @@ function parseManifest(text) {
|
|
|
1764
2608
|
return { path: entry["path"], sha256: entry["sha256"] };
|
|
1765
2609
|
});
|
|
1766
2610
|
const s = raw["settings"];
|
|
1767
|
-
if (!
|
|
2611
|
+
if (!isRecord2(s))
|
|
1768
2612
|
throw new UnreadableManifestError('"settings" is missing');
|
|
1769
2613
|
onlyKeys(s, [
|
|
1770
2614
|
"created",
|
|
@@ -1791,7 +2635,7 @@ function parseManifest(text) {
|
|
|
1791
2635
|
let mcp;
|
|
1792
2636
|
if (raw["mcp"] !== void 0) {
|
|
1793
2637
|
const m = raw["mcp"];
|
|
1794
|
-
if (!
|
|
2638
|
+
if (!isRecord2(m))
|
|
1795
2639
|
throw new UnreadableManifestError('"mcp" is not an object');
|
|
1796
2640
|
onlyKeys(m, ["created", "adopted", "mcpServersCreated", "entrySha256"], '"mcp"');
|
|
1797
2641
|
if (m["entrySha256"] !== void 0 && (typeof m["entrySha256"] !== "string" || !HEX64.test(m["entrySha256"]))) {
|
|
@@ -2097,6 +2941,8 @@ var WRITE_ALLOWLIST = [
|
|
|
2097
2941
|
BUNDLE_PIN_REL,
|
|
2098
2942
|
".halfcycle/bin",
|
|
2099
2943
|
"docs/build-records",
|
|
2944
|
+
// Architecture pictures the draw command writes from Halfcycle's answer. The installer never writes here; keep the entry so this path stays declared as one Halfcycle writes.
|
|
2945
|
+
"docs/diagrams",
|
|
2100
2946
|
"test/fixtures/captured",
|
|
2101
2947
|
MCP_REGISTRATION_REL,
|
|
2102
2948
|
".gitattributes",
|
|
@@ -2195,7 +3041,7 @@ function recordSettingsMerge(draft, existing, generated, preexisted) {
|
|
|
2195
3041
|
const generatedHooks = generated.hooks ?? {};
|
|
2196
3042
|
const hookCommands = Object.values(generatedHooks).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
|
|
2197
3043
|
const ours = /* @__PURE__ */ new Set([...hookCommands, ...HALFCYCLE_HOOK_COMMANDS]);
|
|
2198
|
-
const existingHooks =
|
|
3044
|
+
const existingHooks = isPlainObject2(existing.hooks) ? existing.hooks : void 0;
|
|
2199
3045
|
const heldOurs = Object.values(existingHooks ?? {}).some((entries) => Array.isArray(entries) && entries.some((entry) => isHalfcycleEntry(entry, ours)));
|
|
2200
3046
|
const existingDeny = Array.isArray(existing.permissions?.deny) ? existing.permissions.deny : [];
|
|
2201
3047
|
const schemaChanged = generated.$schema !== void 0 && existing.$schema !== generated.$schema;
|
|
@@ -2213,12 +3059,12 @@ function recordSettingsMerge(draft, existing, generated, preexisted) {
|
|
|
2213
3059
|
}
|
|
2214
3060
|
function recordMcpMerge(draft, existingText, writtenText) {
|
|
2215
3061
|
const before = existingText === null ? void 0 : JSON.parse(existingText);
|
|
2216
|
-
const servers =
|
|
3062
|
+
const servers = isPlainObject2(before) ? before["mcpServers"] : void 0;
|
|
2217
3063
|
const written = JSON.parse(writtenText);
|
|
2218
3064
|
recordMcp(draft, {
|
|
2219
3065
|
created: existingText === null,
|
|
2220
|
-
adopted: draft.previous === null &&
|
|
2221
|
-
mcpServersCreated: !(
|
|
3066
|
+
adopted: draft.previous === null && isPlainObject2(servers) && MCP_SERVER_KEY in servers,
|
|
3067
|
+
mcpServersCreated: !(isPlainObject2(before) && "mcpServers" in before),
|
|
2222
3068
|
entrySha256: canonicalSha256(written.mcpServers[MCP_SERVER_KEY])
|
|
2223
3069
|
});
|
|
2224
3070
|
}
|
|
@@ -2231,8 +3077,8 @@ function recordMcpAdoptionOnly(draft, existingText) {
|
|
|
2231
3077
|
} catch {
|
|
2232
3078
|
return;
|
|
2233
3079
|
}
|
|
2234
|
-
const servers =
|
|
2235
|
-
if (!
|
|
3080
|
+
const servers = isPlainObject2(before) ? before["mcpServers"] : void 0;
|
|
3081
|
+
if (!isPlainObject2(servers) || !(MCP_SERVER_KEY in servers))
|
|
2236
3082
|
return;
|
|
2237
3083
|
recordMcp(draft, { created: false, adopted: true, mcpServersCreated: false });
|
|
2238
3084
|
}
|
|
@@ -2317,7 +3163,7 @@ function readFileOrNull(path) {
|
|
|
2317
3163
|
return null;
|
|
2318
3164
|
}
|
|
2319
3165
|
}
|
|
2320
|
-
function
|
|
3166
|
+
function isPlainObject2(value) {
|
|
2321
3167
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2322
3168
|
}
|
|
2323
3169
|
function readPluginManifest() {
|
|
@@ -2665,6 +3511,54 @@ printf '%s\\n' '${USER_PROMPT_REMINDER_SENTENCE}'
|
|
|
2665
3511
|
exit 0
|
|
2666
3512
|
`;
|
|
2667
3513
|
}
|
|
3514
|
+
var GENERATED_USER_PROMPT_SCOPE_HEADER = "# Typed scope approval \u2014 generated by";
|
|
3515
|
+
function generateUserPromptScopeHook() {
|
|
3516
|
+
return `#!/usr/bin/env bash
|
|
3517
|
+
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer.
|
|
3518
|
+
#
|
|
3519
|
+
# Lets the owner of this project approve a phase's scope, or send it back, by
|
|
3520
|
+
# typing "approve <phase>" or "send back <phase>: <note>" as the whole prompt in
|
|
3521
|
+
# their own Claude Code session.
|
|
3522
|
+
#
|
|
3523
|
+
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
3524
|
+
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
3525
|
+
# any whitespace (typed, or escaped in the input), with "approve " or
|
|
3526
|
+
# "send back " in any case, it exits 0 with no output, and Node never starts.
|
|
3527
|
+
# A quote inside any other field arrives escaped, so no other field (the folder
|
|
3528
|
+
# the session is in, for one) can read as the start of the prompt.
|
|
3529
|
+
#
|
|
3530
|
+
# Otherwise it finds the project root from its own location, never from the
|
|
3531
|
+
# folder the session has moved to, and hands the same input to the guard
|
|
3532
|
+
# wrapper beside it. That decides whether the line counts and prints at most
|
|
3533
|
+
# one plain line. Nothing here ever blocks a prompt.
|
|
3534
|
+
#
|
|
3535
|
+
# FAILS OPEN up to that hand-over: any error before it exits 0 with no output.
|
|
3536
|
+
# After it, the wrapper's own exit stands.
|
|
3537
|
+
trap 'exit 0' ERR
|
|
3538
|
+
set -e
|
|
3539
|
+
|
|
3540
|
+
# Read with the shell's own builtin, so the check below is the only program an
|
|
3541
|
+
# ordinary prompt starts.
|
|
3542
|
+
HC_INPUT=""
|
|
3543
|
+
IFS= read -r -d '' HC_INPUT || true
|
|
3544
|
+
|
|
3545
|
+
# What may come before the phrase: whitespace as typed, as a short escape, as a
|
|
3546
|
+
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
3547
|
+
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
3548
|
+
HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
|
|
3549
|
+
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back) "; then
|
|
3550
|
+
exit 0
|
|
3551
|
+
fi
|
|
3552
|
+
|
|
3553
|
+
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
3554
|
+
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
3555
|
+
export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
3556
|
+
|
|
3557
|
+
trap - ERR
|
|
3558
|
+
set +e
|
|
3559
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
|
|
3560
|
+
`;
|
|
3561
|
+
}
|
|
2668
3562
|
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
2669
3563
|
"b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
|
|
2670
3564
|
"674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
|
|
@@ -2740,6 +3634,7 @@ function removeRetiredMcpHeadersHelper(targetRepo) {
|
|
|
2740
3634
|
var OWNED_GENERATED_HEADERS = {
|
|
2741
3635
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
2742
3636
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
3637
|
+
".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
|
|
2743
3638
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
2744
3639
|
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
2745
3640
|
};
|
|
@@ -2748,6 +3643,7 @@ function hookScriptsThisVersionWrites() {
|
|
|
2748
3643
|
[".claude/hooks/guard-runner.sh", generateGuardRunnerWrapper()],
|
|
2749
3644
|
[".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
|
|
2750
3645
|
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
3646
|
+
[".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
|
|
2751
3647
|
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
2752
3648
|
];
|
|
2753
3649
|
}
|
|
@@ -2892,6 +3788,25 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, c
|
|
|
2892
3788
|
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(pin, null, 2) + "\n");
|
|
2893
3789
|
writeAllowlisted(pinPath, targetRepoRoot, pinText, writtenPaths);
|
|
2894
3790
|
}
|
|
3791
|
+
function updatePinnedMethodVersion(repoRoot, engagementId, version) {
|
|
3792
|
+
let pin;
|
|
3793
|
+
try {
|
|
3794
|
+
pin = readBundlePin(repoRoot);
|
|
3795
|
+
} catch {
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
if (pin === null || pin.engagementId !== engagementId)
|
|
3799
|
+
return;
|
|
3800
|
+
const pinPath = join5(repoRoot, BUNDLE_PIN_REL);
|
|
3801
|
+
const updated = { ...pin, methodVersion: version };
|
|
3802
|
+
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(updated, null, 2) + "\n");
|
|
3803
|
+
try {
|
|
3804
|
+
writeAllowlisted(pinPath, repoRoot, pinText, []);
|
|
3805
|
+
} catch {
|
|
3806
|
+
process.stderr.write(`[halfcycle] Could not update ${BUNDLE_PIN_REL} with the new method version.
|
|
3807
|
+
`);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
2895
3810
|
function writeCrewRoster(targetRepoRoot, report) {
|
|
2896
3811
|
const doc = {
|
|
2897
3812
|
$comment: "GENERATED by the Halfcycle installer \u2014 do not edit by hand. Re-run the installer to pick up a roster change.",
|
|
@@ -3324,14 +4239,14 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
3324
4239
|
else
|
|
3325
4240
|
pending = callback;
|
|
3326
4241
|
}, engagementId));
|
|
3327
|
-
const bindProblem = await new Promise((
|
|
4242
|
+
const bindProblem = await new Promise((resolve5) => {
|
|
3328
4243
|
const onError = (err) => {
|
|
3329
|
-
|
|
4244
|
+
resolve5(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
|
|
3330
4245
|
};
|
|
3331
4246
|
server.once("error", onError);
|
|
3332
4247
|
server.listen({ host: LOOPBACK_REDIRECT_HOST, port: wanted.port }, () => {
|
|
3333
4248
|
server.removeListener("error", onError);
|
|
3334
|
-
|
|
4249
|
+
resolve5(null);
|
|
3335
4250
|
});
|
|
3336
4251
|
});
|
|
3337
4252
|
if (bindProblem !== null) {
|
|
@@ -3347,9 +4262,9 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
3347
4262
|
bound: true,
|
|
3348
4263
|
target: { port: address.port, state },
|
|
3349
4264
|
boundAddress: address.address,
|
|
3350
|
-
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((
|
|
4265
|
+
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve5) => {
|
|
3351
4266
|
if (pending !== null) {
|
|
3352
|
-
|
|
4267
|
+
resolve5(pending);
|
|
3353
4268
|
return;
|
|
3354
4269
|
}
|
|
3355
4270
|
const startedAt = Date.now();
|
|
@@ -3360,7 +4275,7 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
3360
4275
|
clearTimeout(timer);
|
|
3361
4276
|
deliver = null;
|
|
3362
4277
|
abandon = null;
|
|
3363
|
-
|
|
4278
|
+
resolve5(value);
|
|
3364
4279
|
};
|
|
3365
4280
|
const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
|
|
3366
4281
|
deliver = finish;
|
|
@@ -3374,9 +4289,9 @@ async function bindLoopback(env = process.env, engagementId) {
|
|
|
3374
4289
|
};
|
|
3375
4290
|
}
|
|
3376
4291
|
function closeServer(server) {
|
|
3377
|
-
return new Promise((
|
|
4292
|
+
return new Promise((resolve5) => {
|
|
3378
4293
|
server.closeAllConnections();
|
|
3379
|
-
server.close(() =>
|
|
4294
|
+
server.close(() => resolve5());
|
|
3380
4295
|
});
|
|
3381
4296
|
}
|
|
3382
4297
|
|
|
@@ -3399,7 +4314,7 @@ function defaultWrite(text) {
|
|
|
3399
4314
|
process.stdout.write(text);
|
|
3400
4315
|
}
|
|
3401
4316
|
function defaultSleep(ms) {
|
|
3402
|
-
return new Promise((
|
|
4317
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3403
4318
|
}
|
|
3404
4319
|
function browserOpenCommand(os, url) {
|
|
3405
4320
|
if (os === "darwin")
|
|
@@ -3446,12 +4361,12 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
|
|
|
3446
4361
|
};
|
|
3447
4362
|
}
|
|
3448
4363
|
const { command, args: args2, windowsVerbatimArguments } = browserOpenCommand(os, url);
|
|
3449
|
-
return new Promise((
|
|
4364
|
+
return new Promise((resolve5) => {
|
|
3450
4365
|
let child;
|
|
3451
4366
|
try {
|
|
3452
4367
|
child = spawnImpl(command, args2, { stdio: "ignore", env, windowsVerbatimArguments });
|
|
3453
4368
|
} catch {
|
|
3454
|
-
|
|
4369
|
+
resolve5({ opened: false, reason: `${command} could not be run` });
|
|
3455
4370
|
return;
|
|
3456
4371
|
}
|
|
3457
4372
|
let settled = false;
|
|
@@ -3460,7 +4375,7 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
|
|
|
3460
4375
|
return;
|
|
3461
4376
|
settled = true;
|
|
3462
4377
|
clearTimeout(timer);
|
|
3463
|
-
|
|
4378
|
+
resolve5(attempt);
|
|
3464
4379
|
};
|
|
3465
4380
|
const timer = setTimeout(() => {
|
|
3466
4381
|
child.unref();
|
|
@@ -3605,17 +4520,17 @@ async function pollBesideLoopback(serviceUrl, started, wake, deps) {
|
|
|
3605
4520
|
continue;
|
|
3606
4521
|
}
|
|
3607
4522
|
if (sleep === void 0) {
|
|
3608
|
-
await new Promise((
|
|
3609
|
-
const timer = setTimeout(
|
|
4523
|
+
await new Promise((resolve5) => {
|
|
4524
|
+
const timer = setTimeout(resolve5, started.pollIntervalMs);
|
|
3610
4525
|
interrupt = () => {
|
|
3611
4526
|
clearTimeout(timer);
|
|
3612
|
-
|
|
4527
|
+
resolve5();
|
|
3613
4528
|
};
|
|
3614
4529
|
});
|
|
3615
4530
|
} else {
|
|
3616
|
-
await new Promise((
|
|
3617
|
-
interrupt =
|
|
3618
|
-
void sleep(started.pollIntervalMs).then(
|
|
4531
|
+
await new Promise((resolve5) => {
|
|
4532
|
+
interrupt = resolve5;
|
|
4533
|
+
void sleep(started.pollIntervalMs).then(resolve5);
|
|
3619
4534
|
});
|
|
3620
4535
|
}
|
|
3621
4536
|
interrupt = null;
|
|
@@ -4237,9 +5152,9 @@ function wrongAccountVisibility(act) {
|
|
|
4237
5152
|
async function askLine(question2) {
|
|
4238
5153
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4239
5154
|
try {
|
|
4240
|
-
return await new Promise((
|
|
4241
|
-
rl.once("close", () =>
|
|
4242
|
-
rl.question(question2).then(
|
|
5155
|
+
return await new Promise((resolve5) => {
|
|
5156
|
+
rl.once("close", () => resolve5(""));
|
|
5157
|
+
rl.question(question2).then(resolve5, () => resolve5(""));
|
|
4243
5158
|
});
|
|
4244
5159
|
} finally {
|
|
4245
5160
|
rl.close();
|
|
@@ -4506,10 +5421,10 @@ var SYSTEM_FOLDERS = [
|
|
|
4506
5421
|
"C:\\Program Files",
|
|
4507
5422
|
"C:\\Program Files (x86)"
|
|
4508
5423
|
];
|
|
4509
|
-
function refuseSystemFolder(targetRepo,
|
|
5424
|
+
function refuseSystemFolder(targetRepo, platform5 = process.platform) {
|
|
4510
5425
|
const key = (folder) => {
|
|
4511
5426
|
const real = realOrLiteral(folder);
|
|
4512
|
-
return
|
|
5427
|
+
return platform5 === "win32" ? real.toLowerCase() : real;
|
|
4513
5428
|
};
|
|
4514
5429
|
const target = key(targetRepo);
|
|
4515
5430
|
if (SYSTEM_FOLDERS.some((folder) => key(folder) === target)) {
|
|
@@ -4642,11 +5557,11 @@ import { execFileSync as execFileSync3 } from "node:child_process";
|
|
|
4642
5557
|
import { existsSync as existsSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
4643
5558
|
import { delimiter as delimiter2, join as join7 } from "node:path";
|
|
4644
5559
|
function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
4645
|
-
const
|
|
5560
|
+
const platform5 = deps.platform ?? process.platform;
|
|
4646
5561
|
const exec = deps.exec ?? execFileSync3;
|
|
4647
5562
|
let raw;
|
|
4648
5563
|
try {
|
|
4649
|
-
raw =
|
|
5564
|
+
raw = platform5 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", '"claude --version"'], {
|
|
4650
5565
|
encoding: "utf-8",
|
|
4651
5566
|
timeout: timeoutMs,
|
|
4652
5567
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -4676,7 +5591,7 @@ var INSTALL_METHOD_RULES = [
|
|
|
4676
5591
|
];
|
|
4677
5592
|
function probeClaudeCodeInstallMethod(deps = {}) {
|
|
4678
5593
|
const env = deps.env ?? process.env;
|
|
4679
|
-
const
|
|
5594
|
+
const platform5 = deps.platform ?? process.platform;
|
|
4680
5595
|
const exists = deps.exists ?? existsSync5;
|
|
4681
5596
|
const realpath = deps.realpath ?? ((file) => {
|
|
4682
5597
|
try {
|
|
@@ -4685,7 +5600,7 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
4685
5600
|
return file;
|
|
4686
5601
|
}
|
|
4687
5602
|
});
|
|
4688
|
-
const names =
|
|
5603
|
+
const names = platform5 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
|
|
4689
5604
|
const dirs = (env["PATH"] ?? "").split(delimiter2).filter((dir) => dir !== "");
|
|
4690
5605
|
for (const dir of dirs) {
|
|
4691
5606
|
for (const name of names) {
|
|
@@ -5068,7 +5983,7 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
5068
5983
|
`Phase: ${phase} (the name the client gave it)`,
|
|
5069
5984
|
OBSERVED_LABEL,
|
|
5070
5985
|
[
|
|
5071
|
-
`- Method version: ${p.text(record2.observed.methodVersion)}. The Halfcycle method is updated from time to time, and each update has its own version number. This is the one the
|
|
5986
|
+
`- Method version: ${p.text(record2.observed.methodVersion)}. The Halfcycle method is updated from time to time, and each update has its own version number. This is the one the phase was run under. A project moves to a newer version only between phases.`,
|
|
5072
5987
|
`- Project identifier: ${p.text(record2.observed.engagementId)}. Halfcycle keeps this project's records under this identifier.`,
|
|
5073
5988
|
`- Record format: ${p.text(record2.format)}. This names the layout this file and the .json file beside it follow, for software that reads them.`
|
|
5074
5989
|
].join("\n"),
|
|
@@ -5098,10 +6013,14 @@ function assertInsideOutDir(outDir, candidate, identity) {
|
|
|
5098
6013
|
throw new InvalidPhaseIdentityError(identity, BUILD_RECORD_DIR);
|
|
5099
6014
|
}
|
|
5100
6015
|
}
|
|
5101
|
-
function createExclusiveOnDisk(path,
|
|
6016
|
+
function createExclusiveOnDisk(path, data) {
|
|
6017
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
5102
6018
|
const fd = openSync(path, "wx");
|
|
5103
6019
|
try {
|
|
5104
|
-
|
|
6020
|
+
let written = 0;
|
|
6021
|
+
while (written < bytes.length) {
|
|
6022
|
+
written += writeSync(fd, bytes, written, bytes.length - written);
|
|
6023
|
+
}
|
|
5105
6024
|
} catch (err) {
|
|
5106
6025
|
closeSync(fd);
|
|
5107
6026
|
rmSync3(path, { force: true });
|
|
@@ -5218,6 +6137,22 @@ function replaceOne(io, tmp, finalPath, text) {
|
|
|
5218
6137
|
|
|
5219
6138
|
// dist/build-record/obtain.js
|
|
5220
6139
|
import { relative as relative3 } from "node:path";
|
|
6140
|
+
|
|
6141
|
+
// dist/missing-route.js
|
|
6142
|
+
var MESSAGE = {
|
|
6143
|
+
GET: /^Route GET .* not found\.$/s,
|
|
6144
|
+
POST: /^Route POST .* not found\.$/s
|
|
6145
|
+
};
|
|
6146
|
+
function isMissingRoute(method, status, body) {
|
|
6147
|
+
if (status !== 404 || typeof body !== "object" || body === null)
|
|
6148
|
+
return false;
|
|
6149
|
+
const { error, message } = body;
|
|
6150
|
+
if (error !== "Not Found" || typeof message !== "string")
|
|
6151
|
+
return false;
|
|
6152
|
+
return MESSAGE[method].test(message);
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
// dist/build-record/obtain.js
|
|
5221
6156
|
var REFUSAL_FOLLOW_UP = {
|
|
5222
6157
|
"still-open": "Nothing was written. The record is produced when the phase closes.",
|
|
5223
6158
|
"reopened": "Nothing was written. A new record is produced when the phase closes again.",
|
|
@@ -5229,12 +6164,6 @@ var REFUSAL_REASONS = phaseRecordRefusalReasonSchema.options;
|
|
|
5229
6164
|
function buildRecordUrl(credential, phase) {
|
|
5230
6165
|
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(phase)}/build-record`;
|
|
5231
6166
|
}
|
|
5232
|
-
function isMissingRoute(status, body) {
|
|
5233
|
-
if (status !== 404 || typeof body !== "object" || body === null)
|
|
5234
|
-
return false;
|
|
5235
|
-
const { error, message } = body;
|
|
5236
|
-
return error === "Not Found" && typeof message === "string" && /^Route GET .* not found\.$/s.test(message);
|
|
5237
|
-
}
|
|
5238
6167
|
async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_PAIR_IO) {
|
|
5239
6168
|
const url = buildRecordUrl(credential, phase);
|
|
5240
6169
|
let res;
|
|
@@ -5291,7 +6220,7 @@ async function obtainBuildRecord(credential, repoRoot, phase, io = NODE_RECORD_P
|
|
|
5291
6220
|
headline: latestCloseHeadline(answer.record)
|
|
5292
6221
|
};
|
|
5293
6222
|
}
|
|
5294
|
-
if (isMissingRoute(res.status, raw)) {
|
|
6223
|
+
if (isMissingRoute("GET", res.status, raw)) {
|
|
5295
6224
|
return { kind: "unavailable", why: "this Halfcycle service does not provide Build Records yet." };
|
|
5296
6225
|
}
|
|
5297
6226
|
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
@@ -5313,6 +6242,10 @@ function refetchCommand(phase) {
|
|
|
5313
6242
|
// dist/open-phase.js
|
|
5314
6243
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5315
6244
|
import { join as join9 } from "node:path";
|
|
6245
|
+
function readMethodUpdate(value) {
|
|
6246
|
+
const parsed = methodUpdateSchema.safeParse(value);
|
|
6247
|
+
return parsed.success ? parsed.data : null;
|
|
6248
|
+
}
|
|
5316
6249
|
function describeOpen(opened) {
|
|
5317
6250
|
const { phase, previousPhase } = opened;
|
|
5318
6251
|
if (previousPhase === phase) {
|
|
@@ -5422,13 +6355,18 @@ async function openPhase(credential, phase, entry, home) {
|
|
|
5422
6355
|
phase: body.phase,
|
|
5423
6356
|
previousPhase: body.previousPhase,
|
|
5424
6357
|
stampFailure,
|
|
5425
|
-
decision: body.decision
|
|
6358
|
+
decision: body.decision,
|
|
6359
|
+
methodUpdated: readMethodUpdate(body.methodUpdated)
|
|
5426
6360
|
};
|
|
5427
6361
|
}
|
|
5428
6362
|
|
|
5429
6363
|
// dist/close-phase.js
|
|
5430
6364
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5431
6365
|
var NO_STATEMENTS = { thesis: null, outcome: null, deviations: [] };
|
|
6366
|
+
function readMethodUpdate2(value) {
|
|
6367
|
+
const parsed = methodUpdateSchema.safeParse(value);
|
|
6368
|
+
return parsed.success ? parsed.data : null;
|
|
6369
|
+
}
|
|
5432
6370
|
function readCloseCommit(repoRoot) {
|
|
5433
6371
|
if (repoRoot === void 0) {
|
|
5434
6372
|
return {
|
|
@@ -5517,7 +6455,13 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot, sta
|
|
|
5517
6455
|
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.`);
|
|
5518
6456
|
}
|
|
5519
6457
|
const stampFailure = writePhaseStamp(credential.engagementId, null, home);
|
|
5520
|
-
return {
|
|
6458
|
+
return {
|
|
6459
|
+
engagementId: answer.engagementId,
|
|
6460
|
+
status: answer.status,
|
|
6461
|
+
stampFailure,
|
|
6462
|
+
closeCommit,
|
|
6463
|
+
methodUpdated: readMethodUpdate2(answer.methodUpdated)
|
|
6464
|
+
};
|
|
5521
6465
|
}
|
|
5522
6466
|
function describeIssuePath(path) {
|
|
5523
6467
|
const [head, field] = path;
|
|
@@ -5532,12 +6476,1119 @@ function describeIssuePath(path) {
|
|
|
5532
6476
|
return path.map(String).join(".") || "(the close)";
|
|
5533
6477
|
}
|
|
5534
6478
|
|
|
6479
|
+
// dist/system-report.js
|
|
6480
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
6481
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
6482
|
+
import { join as join10 } from "node:path";
|
|
6483
|
+
var SYSTEM_DESCRIPTION_REL = "docs/system.json";
|
|
6484
|
+
function readDescriptionFile(repoRoot) {
|
|
6485
|
+
let text;
|
|
6486
|
+
try {
|
|
6487
|
+
text = readFileSync9(join10(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
|
|
6488
|
+
} catch (err) {
|
|
6489
|
+
const code = err.code;
|
|
6490
|
+
return code === "ENOENT" || code === "ENOTDIR" ? { kind: "missing" } : { kind: "unreadable" };
|
|
6491
|
+
}
|
|
6492
|
+
try {
|
|
6493
|
+
return { kind: "read", value: JSON.parse(text) };
|
|
6494
|
+
} catch (err) {
|
|
6495
|
+
return { kind: "not-json", error: err instanceof Error ? err.message : String(err) };
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
function problemPlace(raw, path) {
|
|
6499
|
+
if (path === "")
|
|
6500
|
+
return SYSTEM_DESCRIPTION_REL;
|
|
6501
|
+
const [list, index] = path.split(".");
|
|
6502
|
+
const position = Number(index);
|
|
6503
|
+
if (!Number.isInteger(position) || position < 0)
|
|
6504
|
+
return path;
|
|
6505
|
+
const items = typeof raw === "object" && raw !== null ? raw[list ?? ""] : void 0;
|
|
6506
|
+
const item = Array.isArray(items) ? items[position] : void 0;
|
|
6507
|
+
const fields = typeof item === "object" && item !== null ? item : {};
|
|
6508
|
+
if (list === "parts") {
|
|
6509
|
+
const id = fields["id"];
|
|
6510
|
+
return typeof id === "string" && id !== "" ? `${path} (part "${id}")` : `${path} (part ${position + 1})`;
|
|
6511
|
+
}
|
|
6512
|
+
if (list === "connections") {
|
|
6513
|
+
const from = fields["from"];
|
|
6514
|
+
const to = fields["to"];
|
|
6515
|
+
return typeof from === "string" && typeof to === "string" ? `${path} (connection "${from}" to "${to}")` : `${path} (connection ${position + 1})`;
|
|
6516
|
+
}
|
|
6517
|
+
return path;
|
|
6518
|
+
}
|
|
6519
|
+
function problemLines(raw, problems) {
|
|
6520
|
+
return problems.map((p) => ` ${problemPlace(raw, p.path)}: ${p.message}
|
|
6521
|
+
`).join("");
|
|
6522
|
+
}
|
|
6523
|
+
var listTrackedFiles = (repoRoot) => {
|
|
6524
|
+
try {
|
|
6525
|
+
const out = execFileSync5("git", ["-C", repoRoot, "ls-files", "-z"], {
|
|
6526
|
+
encoding: "utf-8",
|
|
6527
|
+
env: gitEnvForNamedRepo(),
|
|
6528
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
6529
|
+
maxBuffer: 256 * 1024 * 1024
|
|
6530
|
+
});
|
|
6531
|
+
return out.split("\0").filter((p) => p !== "");
|
|
6532
|
+
} catch {
|
|
6533
|
+
return null;
|
|
6534
|
+
}
|
|
6535
|
+
};
|
|
6536
|
+
function unownedPatterns(description, tracked) {
|
|
6537
|
+
if (tracked === null)
|
|
6538
|
+
return null;
|
|
6539
|
+
const found = [];
|
|
6540
|
+
for (const part of description.parts) {
|
|
6541
|
+
for (const pattern of part.owns) {
|
|
6542
|
+
const alone = {
|
|
6543
|
+
format: description.format,
|
|
6544
|
+
parts: [{ ...part, owns: [pattern] }],
|
|
6545
|
+
connections: []
|
|
6546
|
+
};
|
|
6547
|
+
if (!tracked.some((path) => ownerOf(path, alone) !== null))
|
|
6548
|
+
found.push({ partId: part.id, pattern });
|
|
6549
|
+
}
|
|
6550
|
+
}
|
|
6551
|
+
return found;
|
|
6552
|
+
}
|
|
6553
|
+
function unownedPatternLines(unowned, verb) {
|
|
6554
|
+
return unowned.map((u) => `halfcycle ${verb}: warning: "${u.pattern}" (part "${u.partId}") matches no file in this repository. Correct the path, or remove it if the code moved.
|
|
6555
|
+
`).join("");
|
|
6556
|
+
}
|
|
6557
|
+
function systemReportUrl(credential) {
|
|
6558
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/system`;
|
|
6559
|
+
}
|
|
6560
|
+
function serviceHost(serviceUrl) {
|
|
6561
|
+
try {
|
|
6562
|
+
return new URL(serviceUrl).host;
|
|
6563
|
+
} catch {
|
|
6564
|
+
return serviceUrl;
|
|
6565
|
+
}
|
|
6566
|
+
}
|
|
6567
|
+
async function reportSystemDescription(credential, description) {
|
|
6568
|
+
let res;
|
|
6569
|
+
let text;
|
|
6570
|
+
try {
|
|
6571
|
+
res = await fetch(systemReportUrl(credential), {
|
|
6572
|
+
method: "POST",
|
|
6573
|
+
headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
|
|
6574
|
+
body: JSON.stringify(description)
|
|
6575
|
+
});
|
|
6576
|
+
text = await res.text();
|
|
6577
|
+
} catch {
|
|
6578
|
+
return {
|
|
6579
|
+
kind: "unavailable",
|
|
6580
|
+
why: `could not reach Halfcycle at ${serviceHost(credential.serviceUrl)}. Check this machine's connection, then run this again.`
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
let raw = null;
|
|
6584
|
+
try {
|
|
6585
|
+
raw = JSON.parse(text);
|
|
6586
|
+
} catch {
|
|
6587
|
+
raw = null;
|
|
6588
|
+
}
|
|
6589
|
+
const parsed = systemReportResultSchema.safeParse(raw);
|
|
6590
|
+
if (parsed.success) {
|
|
6591
|
+
const answer = parsed.data;
|
|
6592
|
+
if (answer.outcome === "recorded") {
|
|
6593
|
+
return { kind: "recorded", version: answer.version, reportedAt: answer.reportedAt };
|
|
6594
|
+
}
|
|
6595
|
+
if (answer.outcome === "refused")
|
|
6596
|
+
return { kind: "refused", problems: answer.problems };
|
|
6597
|
+
return {
|
|
6598
|
+
kind: "unavailable",
|
|
6599
|
+
why: "Halfcycle could not record it just now. Nothing was recorded; run this again in a minute."
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
if (isMissingRoute("POST", res.status, raw))
|
|
6603
|
+
return { kind: "no-route" };
|
|
6604
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
6605
|
+
if (res.ok) {
|
|
6606
|
+
return {
|
|
6607
|
+
kind: "unavailable",
|
|
6608
|
+
why: "Halfcycle sent an answer this command could not read. Nothing was recorded; try again later."
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6611
|
+
return {
|
|
6612
|
+
kind: "unavailable",
|
|
6613
|
+
why: `Halfcycle answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
6614
|
+
};
|
|
6615
|
+
}
|
|
6616
|
+
async function checkAndReportDescription(repoRoot, deps, verb, beforeSend = () => null) {
|
|
6617
|
+
const p = `halfcycle ${verb}:`;
|
|
6618
|
+
const file = readDescriptionFile(repoRoot);
|
|
6619
|
+
if (file.kind === "missing") {
|
|
6620
|
+
return {
|
|
6621
|
+
kind: "stopped",
|
|
6622
|
+
stderr: `${p} there is no ${SYSTEM_DESCRIPTION_REL} in this project yet. Run the Layer 2 architecture step to write it, then run this again.
|
|
6623
|
+
`
|
|
6624
|
+
};
|
|
6625
|
+
}
|
|
6626
|
+
if (file.kind === "unreadable") {
|
|
6627
|
+
return {
|
|
6628
|
+
kind: "stopped",
|
|
6629
|
+
stderr: `${p} ${SYSTEM_DESCRIPTION_REL} could not be read. Check that it is a file you can open, then run this again.
|
|
6630
|
+
`
|
|
6631
|
+
};
|
|
6632
|
+
}
|
|
6633
|
+
if (file.kind === "not-json") {
|
|
6634
|
+
return {
|
|
6635
|
+
kind: "stopped",
|
|
6636
|
+
stderr: `${p} ${SYSTEM_DESCRIPTION_REL} is not valid JSON (${file.error}). Fix it, then run this again. Nothing was sent.
|
|
6637
|
+
`
|
|
6638
|
+
};
|
|
6639
|
+
}
|
|
6640
|
+
const checked = checkSystemDescription(file.value);
|
|
6641
|
+
if (!checked.ok) {
|
|
6642
|
+
const n = checked.problems.length;
|
|
6643
|
+
return {
|
|
6644
|
+
kind: "stopped",
|
|
6645
|
+
stderr: `${p} ${SYSTEM_DESCRIPTION_REL} has ${n} ${n === 1 ? "problem" : "problems"}, so nothing was sent:
|
|
6646
|
+
` + problemLines(file.value, checked.problems)
|
|
6647
|
+
};
|
|
6648
|
+
}
|
|
6649
|
+
const unowned = unownedPatterns(checked.description, deps.listTracked(repoRoot));
|
|
6650
|
+
const warnings = unowned === null ? "" : unownedPatternLines(unowned, verb);
|
|
6651
|
+
const refusal = beforeSend(checked.description);
|
|
6652
|
+
if (refusal !== null)
|
|
6653
|
+
return { kind: "stopped", stderr: warnings + refusal };
|
|
6654
|
+
let credential;
|
|
6655
|
+
try {
|
|
6656
|
+
credential = deps.readCredential(repoRoot);
|
|
6657
|
+
} catch {
|
|
6658
|
+
return {
|
|
6659
|
+
kind: "stopped",
|
|
6660
|
+
stderr: warnings + `${p} this project is not connected to Halfcycle on this machine. Run ${HALFCYCLE_COMMAND} here first, then run this again. Nothing was sent.
|
|
6661
|
+
`
|
|
6662
|
+
};
|
|
6663
|
+
}
|
|
6664
|
+
const outcome = await deps.report(credential, checked.description);
|
|
6665
|
+
switch (outcome.kind) {
|
|
6666
|
+
case "recorded":
|
|
6667
|
+
return { kind: "recorded", version: outcome.version, credential, warnings };
|
|
6668
|
+
case "refused": {
|
|
6669
|
+
const n = outcome.problems.length;
|
|
6670
|
+
return {
|
|
6671
|
+
kind: "stopped",
|
|
6672
|
+
stderr: warnings + `${p} Halfcycle refused ${SYSTEM_DESCRIPTION_REL} (${n} ${n === 1 ? "problem" : "problems"}); nothing was recorded:
|
|
6673
|
+
` + problemLines(file.value, outcome.problems)
|
|
6674
|
+
};
|
|
6675
|
+
}
|
|
6676
|
+
case "no-route":
|
|
6677
|
+
return { kind: "no-route", warnings };
|
|
6678
|
+
case "unavailable":
|
|
6679
|
+
return { kind: "stopped", stderr: warnings + `${p} ${outcome.why}
|
|
6680
|
+
` };
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
async function runSystem(repoRoot, deps) {
|
|
6684
|
+
const stage = await checkAndReportDescription(repoRoot, deps, "system");
|
|
6685
|
+
switch (stage.kind) {
|
|
6686
|
+
case "stopped":
|
|
6687
|
+
return { stdout: "", stderr: stage.stderr, exitCode: 1 };
|
|
6688
|
+
case "no-route":
|
|
6689
|
+
return {
|
|
6690
|
+
stdout: "",
|
|
6691
|
+
stderr: stage.warnings + "halfcycle system: this Halfcycle service cannot take a system description yet. Nothing was recorded.\n",
|
|
6692
|
+
exitCode: 1
|
|
6693
|
+
};
|
|
6694
|
+
case "recorded":
|
|
6695
|
+
return {
|
|
6696
|
+
stdout: `[halfcycle] System description recorded: version ${stage.version}.
|
|
6697
|
+
`,
|
|
6698
|
+
stderr: stage.warnings,
|
|
6699
|
+
exitCode: 0
|
|
6700
|
+
};
|
|
6701
|
+
}
|
|
6702
|
+
}
|
|
6703
|
+
|
|
6704
|
+
// dist/plan.js
|
|
6705
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6706
|
+
import { platform as platform4 } from "node:os";
|
|
6707
|
+
import { join as join11 } from "node:path";
|
|
6708
|
+
var PLAN_USAGE = `${HALFCYCLE_COMMAND} plan <phase> [--show] [--repo <root>]`;
|
|
6709
|
+
var PLAN_DIR_REL = "docs/phases";
|
|
6710
|
+
function planFileRel(phase) {
|
|
6711
|
+
return `${PLAN_DIR_REL}/${renderPhaseSegment(phase)}.plan.json`;
|
|
6712
|
+
}
|
|
6713
|
+
var PLAN_CHANGE_LABELS = {
|
|
6714
|
+
add: "Will be added",
|
|
6715
|
+
change: "Will change",
|
|
6716
|
+
remove: "Will be removed"
|
|
6717
|
+
};
|
|
6718
|
+
function parsePlanArgs(argv) {
|
|
6719
|
+
const positionals = [];
|
|
6720
|
+
let repoRoot;
|
|
6721
|
+
let show = false;
|
|
6722
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6723
|
+
const arg = argv[i];
|
|
6724
|
+
if (arg === "--repo") {
|
|
6725
|
+
const value = argv[i + 1];
|
|
6726
|
+
if (repoRoot !== void 0 || value === void 0 || value === "" || value.startsWith("-"))
|
|
6727
|
+
return { ok: false };
|
|
6728
|
+
repoRoot = value;
|
|
6729
|
+
i++;
|
|
6730
|
+
continue;
|
|
6731
|
+
}
|
|
6732
|
+
if (arg === "--show") {
|
|
6733
|
+
if (show)
|
|
6734
|
+
return { ok: false };
|
|
6735
|
+
show = true;
|
|
6736
|
+
continue;
|
|
6737
|
+
}
|
|
6738
|
+
if (arg.startsWith("-"))
|
|
6739
|
+
return { ok: false };
|
|
6740
|
+
positionals.push(arg);
|
|
6741
|
+
}
|
|
6742
|
+
if (positionals.length !== 1)
|
|
6743
|
+
return { ok: false };
|
|
6744
|
+
return { ok: true, phase: positionals[0], show, repoRoot };
|
|
6745
|
+
}
|
|
6746
|
+
function planProblemPlace(raw, fileRel, path) {
|
|
6747
|
+
if (path === "")
|
|
6748
|
+
return fileRel;
|
|
6749
|
+
const [list, index] = path.split(".");
|
|
6750
|
+
const position = Number(index);
|
|
6751
|
+
if (list !== "changes" || !Number.isInteger(position) || position < 0)
|
|
6752
|
+
return path;
|
|
6753
|
+
const changes = typeof raw === "object" && raw !== null ? raw["changes"] : void 0;
|
|
6754
|
+
const item = Array.isArray(changes) ? changes[position] : void 0;
|
|
6755
|
+
const part = typeof item === "object" && item !== null ? item["part"] : void 0;
|
|
6756
|
+
return typeof part === "string" && part !== "" ? `${path} (part "${part}")` : `${path} (part ${position + 1})`;
|
|
6757
|
+
}
|
|
6758
|
+
function planProblemLines(raw, fileRel, problems) {
|
|
6759
|
+
return problems.map((p) => ` ${planProblemPlace(raw, fileRel, p.path)}: ${p.message}
|
|
6760
|
+
`).join("");
|
|
6761
|
+
}
|
|
6762
|
+
function readAndCheckPlan(repoRoot, phase, description, verb) {
|
|
6763
|
+
const p = `halfcycle ${verb}:`;
|
|
6764
|
+
const fileRel = planFileRel(phase);
|
|
6765
|
+
let text;
|
|
6766
|
+
try {
|
|
6767
|
+
text = readFileSync10(join11(repoRoot, fileRel), "utf-8");
|
|
6768
|
+
} catch (err) {
|
|
6769
|
+
const code = err.code;
|
|
6770
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
6771
|
+
return {
|
|
6772
|
+
ok: false,
|
|
6773
|
+
line: `${p} there is no ${fileRel} in this project yet. Write the plan in the scoping step, then run this again. Nothing was sent.
|
|
6774
|
+
`
|
|
6775
|
+
};
|
|
6776
|
+
}
|
|
6777
|
+
return {
|
|
6778
|
+
ok: false,
|
|
6779
|
+
line: `${p} ${fileRel} could not be read. Check that it is a file you can open, then run this again. Nothing was sent.
|
|
6780
|
+
`
|
|
6781
|
+
};
|
|
6782
|
+
}
|
|
6783
|
+
let raw;
|
|
6784
|
+
try {
|
|
6785
|
+
raw = JSON.parse(text);
|
|
6786
|
+
} catch (err) {
|
|
6787
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
6788
|
+
return { ok: false, line: `${p} ${fileRel} is not valid JSON (${why}). Fix it, then run this again. Nothing was sent.
|
|
6789
|
+
` };
|
|
6790
|
+
}
|
|
6791
|
+
const named = typeof raw === "object" && raw !== null ? raw["phase"] : void 0;
|
|
6792
|
+
if (isValidPhaseIdentity(named) && renderPhaseSegment(named) !== renderPhaseSegment(phase)) {
|
|
6793
|
+
return {
|
|
6794
|
+
ok: false,
|
|
6795
|
+
line: `${p} ${fileRel} is the plan for phase "${String(named)}", not phase "${phase}". Run this for the phase the plan names, or correct the plan's phase. Nothing was sent.
|
|
6796
|
+
`
|
|
6797
|
+
};
|
|
6798
|
+
}
|
|
6799
|
+
const checked = checkPhasePlan(raw, description);
|
|
6800
|
+
if (!checked.ok) {
|
|
6801
|
+
const n = checked.problems.length;
|
|
6802
|
+
return {
|
|
6803
|
+
ok: false,
|
|
6804
|
+
line: `${p} ${fileRel} has ${n} ${n === 1 ? "problem" : "problems"}, so nothing was sent:
|
|
6805
|
+
` + planProblemLines(raw, fileRel, checked.problems)
|
|
6806
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
return { ok: true, plan: checked.plan };
|
|
6809
|
+
}
|
|
6810
|
+
function planReportUrl(credential, phase) {
|
|
6811
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(String(phase))}/plan`;
|
|
6812
|
+
}
|
|
6813
|
+
function serviceHost2(serviceUrl) {
|
|
6814
|
+
try {
|
|
6815
|
+
return new URL(serviceUrl).host;
|
|
6816
|
+
} catch {
|
|
6817
|
+
return serviceUrl;
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
async function reportPhasePlan(credential, plan, descriptionVersion) {
|
|
6821
|
+
const body = { descriptionVersion, plan };
|
|
6822
|
+
let res;
|
|
6823
|
+
let text;
|
|
6824
|
+
try {
|
|
6825
|
+
res = await fetch(planReportUrl(credential, plan.phase), {
|
|
6826
|
+
method: "POST",
|
|
6827
|
+
headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
|
|
6828
|
+
body: JSON.stringify(body)
|
|
6829
|
+
});
|
|
6830
|
+
text = await res.text();
|
|
6831
|
+
} catch {
|
|
6832
|
+
return {
|
|
6833
|
+
kind: "unavailable",
|
|
6834
|
+
why: `could not reach Halfcycle at ${serviceHost2(credential.serviceUrl)}. Check this machine's connection, then run this again.`
|
|
6835
|
+
};
|
|
6836
|
+
}
|
|
6837
|
+
let raw = null;
|
|
6838
|
+
try {
|
|
6839
|
+
raw = JSON.parse(text);
|
|
6840
|
+
} catch {
|
|
6841
|
+
raw = null;
|
|
6842
|
+
}
|
|
6843
|
+
const parsed = phasePlanResultSchema.safeParse(raw);
|
|
6844
|
+
if (parsed.success) {
|
|
6845
|
+
const answer = parsed.data;
|
|
6846
|
+
if (answer.outcome === "recorded") {
|
|
6847
|
+
return {
|
|
6848
|
+
kind: "recorded",
|
|
6849
|
+
version: answer.version,
|
|
6850
|
+
phaseNumber: answer.phaseNumber,
|
|
6851
|
+
state: answer.state,
|
|
6852
|
+
changedParts: answer.changedParts,
|
|
6853
|
+
typedApprovalOffered: answer.typedApprovalOffered
|
|
6854
|
+
};
|
|
6855
|
+
}
|
|
6856
|
+
if (answer.outcome === "refused")
|
|
6857
|
+
return { kind: "refused", problems: answer.problems };
|
|
6858
|
+
return {
|
|
6859
|
+
kind: "unavailable",
|
|
6860
|
+
why: "Halfcycle could not record the plan just now. Nothing was recorded; run this again in a minute."
|
|
6861
|
+
};
|
|
6862
|
+
}
|
|
6863
|
+
if (isMissingRoute("POST", res.status, raw))
|
|
6864
|
+
return { kind: "no-route" };
|
|
6865
|
+
if (res.ok) {
|
|
6866
|
+
return {
|
|
6867
|
+
kind: "unavailable",
|
|
6868
|
+
why: "Halfcycle sent an answer this command could not read. The plan may not be recorded; run this again later."
|
|
6869
|
+
};
|
|
6870
|
+
}
|
|
6871
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
6872
|
+
return {
|
|
6873
|
+
kind: "unavailable",
|
|
6874
|
+
why: `Halfcycle answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
6875
|
+
};
|
|
6876
|
+
}
|
|
6877
|
+
function writeShownPlan(engagementId, entry, home) {
|
|
6878
|
+
let temp = null;
|
|
6879
|
+
try {
|
|
6880
|
+
const dir = engagementStateDir(engagementId, home);
|
|
6881
|
+
const path = join11(dir, SHOWN_PLANS_FILENAME);
|
|
6882
|
+
let existing = [];
|
|
6883
|
+
try {
|
|
6884
|
+
existing = parseShownPlans(readFileSync10(path, "utf-8"));
|
|
6885
|
+
} catch {
|
|
6886
|
+
existing = [];
|
|
6887
|
+
}
|
|
6888
|
+
const kept = existing.filter((e) => e.phase !== entry.phase && e.number !== entry.number);
|
|
6889
|
+
const entries = [...kept, entry];
|
|
6890
|
+
const body = serialiseShownPlans(entries);
|
|
6891
|
+
mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
6892
|
+
temp = join11(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
|
|
6893
|
+
writeFileSync6(temp, body, { mode: 384 });
|
|
6894
|
+
if (platform4() !== "win32")
|
|
6895
|
+
chmodSync3(temp, 384);
|
|
6896
|
+
renameSync2(temp, path);
|
|
6897
|
+
return { ok: true, entries };
|
|
6898
|
+
} catch {
|
|
6899
|
+
if (temp !== null) {
|
|
6900
|
+
try {
|
|
6901
|
+
unlinkSync(temp);
|
|
6902
|
+
} catch {
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
return { ok: false };
|
|
6906
|
+
}
|
|
6907
|
+
}
|
|
6908
|
+
function readPinnedClientVersion(repoRoot) {
|
|
6909
|
+
try {
|
|
6910
|
+
const version = readBundlePin(repoRoot)?.version;
|
|
6911
|
+
return typeof version === "string" ? version : void 0;
|
|
6912
|
+
} catch {
|
|
6913
|
+
return void 0;
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
function planRefusedLines(verb, phase, plan, problems) {
|
|
6917
|
+
const fileRel = planFileRel(phase);
|
|
6918
|
+
const n = problems.length;
|
|
6919
|
+
return `halfcycle ${verb}: Halfcycle refused ${fileRel} (${n} ${n === 1 ? "problem" : "problems"}); the plan was not recorded:
|
|
6920
|
+
` + planProblemLines(plan, fileRel, problems);
|
|
6921
|
+
}
|
|
6922
|
+
function notAPhaseNameLine(verb, phase) {
|
|
6923
|
+
return `halfcycle ${verb}: "${phase}" is not a phase name: a name cannot be empty, start with a dot, or hold a slash, a backslash or a control character. Nothing was sent.
|
|
6924
|
+
`;
|
|
6925
|
+
}
|
|
6926
|
+
function planStateSentence(state) {
|
|
6927
|
+
switch (state) {
|
|
6928
|
+
case "waiting":
|
|
6929
|
+
return "Waiting for the owner's approval.";
|
|
6930
|
+
case "approved":
|
|
6931
|
+
return "The owner has approved this plan.";
|
|
6932
|
+
case "partly-stale":
|
|
6933
|
+
return "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
|
|
6934
|
+
case "sent-back":
|
|
6935
|
+
return "The owner sent this plan back. Change it to answer the owner's notes, then run this again.";
|
|
6936
|
+
case "withdrawn":
|
|
6937
|
+
return "The owner withdrew their approval of this plan. It waits for the owner's approval again.";
|
|
6938
|
+
}
|
|
6939
|
+
}
|
|
6940
|
+
function approvalLine(phase, number, typedApprovalOffered, pinnedVersion, shown2) {
|
|
6941
|
+
if (!typedApprovalOffered)
|
|
6942
|
+
return TYPED_APPROVAL_OFF_SENTENCE;
|
|
6943
|
+
if (!clientMeetsChatApprovalFloor(pinnedVersion))
|
|
6944
|
+
return CLIENT_UPDATE_SENTENCE;
|
|
6945
|
+
const byNumber = String(number);
|
|
6946
|
+
const numberReachesThis = byNumber !== phase && resolvePhaseReference(byNumber, shown2)?.phase === phase;
|
|
6947
|
+
const typed = numberReachesThis ? `\`approve ${phase}\` (or \`approve ${byNumber}\`)` : `\`approve ${phase}\``;
|
|
6948
|
+
return `To approve it, the project's owner types ${typed} in their own Claude Code session. Only the owner can.`;
|
|
6949
|
+
}
|
|
6950
|
+
function changeLines(plan, description, changed) {
|
|
6951
|
+
const names = new Map(description.parts.map((part) => [part.id, part.name]));
|
|
6952
|
+
const marked = new Set(changed);
|
|
6953
|
+
return plan.changes.map((c) => {
|
|
6954
|
+
const mark = marked.has(c.part) ? " (changed since the owner last decided)" : "";
|
|
6955
|
+
return ` - ${names.get(c.part) ?? c.part} \xB7 ${PLAN_CHANGE_LABELS[c.change]} \xB7 ${c.summary}${mark}
|
|
6956
|
+
`;
|
|
6957
|
+
}).join("");
|
|
6958
|
+
}
|
|
6959
|
+
async function runPlan(repoRoot, phase, show, deps) {
|
|
6960
|
+
const p = "halfcycle plan:";
|
|
6961
|
+
if (!isValidPhaseIdentity(phase))
|
|
6962
|
+
return { stdout: "", stderr: notAPhaseNameLine("plan", phase), exitCode: 1 };
|
|
6963
|
+
let checkedPlan = null;
|
|
6964
|
+
let checkedDescription = null;
|
|
6965
|
+
const stage = await checkAndReportDescription(repoRoot, deps, "plan", (description2) => {
|
|
6966
|
+
const checked = readAndCheckPlan(repoRoot, phase, description2, "plan");
|
|
6967
|
+
if (!checked.ok)
|
|
6968
|
+
return checked.line;
|
|
6969
|
+
checkedPlan = checked.plan;
|
|
6970
|
+
checkedDescription = description2;
|
|
6971
|
+
return null;
|
|
6972
|
+
});
|
|
6973
|
+
if (stage.kind === "stopped")
|
|
6974
|
+
return { stdout: "", stderr: stage.stderr, exitCode: 1 };
|
|
6975
|
+
const cannotTakeYet = `${p} this Halfcycle service cannot take a phase plan yet. Nothing was recorded.
|
|
6976
|
+
`;
|
|
6977
|
+
if (stage.kind === "no-route")
|
|
6978
|
+
return { stdout: "", stderr: stage.warnings + cannotTakeYet, exitCode: 1 };
|
|
6979
|
+
const warnings = stage.warnings;
|
|
6980
|
+
const plan = checkedPlan;
|
|
6981
|
+
const description = checkedDescription;
|
|
6982
|
+
if (plan === null || description === null) {
|
|
6983
|
+
return { stdout: "", stderr: warnings + `${p} the plan was not checked, so nothing more was sent.
|
|
6984
|
+
`, exitCode: 1 };
|
|
6985
|
+
}
|
|
6986
|
+
const outcome = await deps.reportPlan(stage.credential, plan, stage.version);
|
|
6987
|
+
switch (outcome.kind) {
|
|
6988
|
+
case "no-route":
|
|
6989
|
+
return { stdout: "", stderr: warnings + cannotTakeYet, exitCode: 1 };
|
|
6990
|
+
case "unavailable":
|
|
6991
|
+
return { stdout: "", stderr: warnings + `${p} ${outcome.why}
|
|
6992
|
+
`, exitCode: 1 };
|
|
6993
|
+
case "refused":
|
|
6994
|
+
return { stdout: "", stderr: warnings + planRefusedLines("plan", phase, plan, outcome.problems), exitCode: 1 };
|
|
6995
|
+
case "recorded":
|
|
6996
|
+
break;
|
|
6997
|
+
}
|
|
6998
|
+
const shownPhase = String(plan.phase);
|
|
6999
|
+
const recorded = `[halfcycle] Plan for ${shownPhase} recorded: plan version ${outcome.version} (phase ${outcome.phaseNumber}).`;
|
|
7000
|
+
const stateSentence = planStateSentence(outcome.state);
|
|
7001
|
+
if (!show)
|
|
7002
|
+
return { stdout: `${recorded} ${stateSentence}
|
|
7003
|
+
`, stderr: warnings, exitCode: 0 };
|
|
7004
|
+
const entry = {
|
|
7005
|
+
phase: shownPhase,
|
|
7006
|
+
number: outcome.phaseNumber,
|
|
7007
|
+
version: outcome.version,
|
|
7008
|
+
shownAt: deps.now().toISOString()
|
|
7009
|
+
};
|
|
7010
|
+
const written = deps.recordShown(stage.credential.engagementId, entry);
|
|
7011
|
+
const listed = `${recorded}
|
|
7012
|
+
` + changeLines(plan, description, outcome.changedParts) + `${stateSentence}
|
|
7013
|
+
`;
|
|
7014
|
+
if (!written.ok) {
|
|
7015
|
+
return {
|
|
7016
|
+
stdout: listed,
|
|
7017
|
+
stderr: warnings + `${p} the list above could not be recorded on this machine, so typing an approval for it would not work. Check that the .halfcycle folder in your home folder can be written to, then run this again.
|
|
7018
|
+
`,
|
|
7019
|
+
exitCode: 1
|
|
7020
|
+
};
|
|
7021
|
+
}
|
|
7022
|
+
const stdout = listed + `${approvalLine(shownPhase, outcome.phaseNumber, outcome.typedApprovalOffered, deps.pinnedClientVersion(repoRoot), written.entries)}
|
|
7023
|
+
`;
|
|
7024
|
+
return { stdout, stderr: warnings, exitCode: 0 };
|
|
7025
|
+
}
|
|
7026
|
+
|
|
7027
|
+
// dist/draw/write.js
|
|
7028
|
+
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync11, readSync, realpathSync as realpathSync3, rmSync as rmSync4 } from "node:fs";
|
|
7029
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join12, relative as relative4, resolve as resolve3, sep } from "node:path";
|
|
7030
|
+
var DOCS_DIR = "docs";
|
|
7031
|
+
var DIAGRAMS_DIR = "diagrams";
|
|
7032
|
+
var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
|
|
7033
|
+
var ARCHITECTURE_NAME = "architecture";
|
|
7034
|
+
var ARCHITECTURE_BASE = ARCHITECTURE_NAME;
|
|
7035
|
+
function phaseDiagramBase(phase) {
|
|
7036
|
+
return renderPhaseSegment(phase);
|
|
7037
|
+
}
|
|
7038
|
+
var FLIGHTDECK_SUFFIX = ".flightdeck";
|
|
7039
|
+
function baseTakesFlightDeckNames(base) {
|
|
7040
|
+
return base.toLowerCase().endsWith(FLIGHTDECK_SUFFIX);
|
|
7041
|
+
}
|
|
7042
|
+
function pictureNames(base) {
|
|
7043
|
+
const names = {
|
|
7044
|
+
broadsheet: base,
|
|
7045
|
+
flightdeck: `${base}${FLIGHTDECK_SUFFIX}`
|
|
7046
|
+
};
|
|
7047
|
+
return names;
|
|
7048
|
+
}
|
|
7049
|
+
var EMBEDDED_LOOK = "broadsheet";
|
|
7050
|
+
var DIAGRAM_MARKER_LINE = SYSTEM_DIAGRAM_MARKER_LINE;
|
|
7051
|
+
var PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
7052
|
+
function svgOf(look) {
|
|
7053
|
+
return `${look}.svg`;
|
|
7054
|
+
}
|
|
7055
|
+
function pngOf(look) {
|
|
7056
|
+
return `${look}.png`;
|
|
7057
|
+
}
|
|
7058
|
+
var DIAGRAM_FILES = [
|
|
7059
|
+
...SYSTEM_PICTURE_THEMES.map(svgOf),
|
|
7060
|
+
...SYSTEM_PICTURE_THEMES.map(pngOf),
|
|
7061
|
+
"mmd"
|
|
7062
|
+
];
|
|
7063
|
+
function fileNames(base) {
|
|
7064
|
+
const looks = pictureNames(base);
|
|
7065
|
+
return Object.fromEntries([
|
|
7066
|
+
...SYSTEM_PICTURE_THEMES.flatMap((look) => [
|
|
7067
|
+
[svgOf(look), `${looks[look]}.svg`],
|
|
7068
|
+
[pngOf(look), `${looks[look]}.png`]
|
|
7069
|
+
]),
|
|
7070
|
+
["mmd", `${base}.mmd`]
|
|
7071
|
+
]);
|
|
7072
|
+
}
|
|
7073
|
+
function embedImageName(base) {
|
|
7074
|
+
return fileNames(base)[pngOf(EMBEDDED_LOOK)];
|
|
7075
|
+
}
|
|
7076
|
+
function markdownLinkName(name) {
|
|
7077
|
+
return encodeURIComponent(name).replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
7078
|
+
}
|
|
7079
|
+
function embedImageLink(base, doc) {
|
|
7080
|
+
const up = doc === "docs" ? "" : "../";
|
|
7081
|
+
return `${up}${DIAGRAMS_DIR}/${markdownLinkName(embedImageName(base))}`;
|
|
7082
|
+
}
|
|
7083
|
+
function diagramPaths(repoRoot, base) {
|
|
7084
|
+
const root = resolve3(repoRoot);
|
|
7085
|
+
const dir = join12(root, DOCS_DIR, DIAGRAMS_DIR);
|
|
7086
|
+
const names = fileNames(base);
|
|
7087
|
+
const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join12(dir, names[which])]));
|
|
7088
|
+
return { repoRoot: root, dir, file };
|
|
7089
|
+
}
|
|
7090
|
+
function diagramRel(base, which) {
|
|
7091
|
+
return `${DIAGRAM_DIR_REL}/${fileNames(base)[which]}`;
|
|
7092
|
+
}
|
|
7093
|
+
var DIAGRAM_FILES_AS_LISTED = [
|
|
7094
|
+
...SYSTEM_PICTURE_THEMES.flatMap((look) => [pngOf(look), svgOf(look)]),
|
|
7095
|
+
"mmd"
|
|
7096
|
+
];
|
|
7097
|
+
function spokenList(items) {
|
|
7098
|
+
if (items.length <= 1)
|
|
7099
|
+
return items.join("");
|
|
7100
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
7101
|
+
}
|
|
7102
|
+
function lstatOrNull2(path) {
|
|
7103
|
+
try {
|
|
7104
|
+
return lstatSync2(path);
|
|
7105
|
+
} catch {
|
|
7106
|
+
return null;
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
function isInside(outer, inner) {
|
|
7110
|
+
const rel = relative4(outer, inner);
|
|
7111
|
+
return rel === "" || !isAbsolute3(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
7112
|
+
}
|
|
7113
|
+
function checkDiagramPlace(paths) {
|
|
7114
|
+
let root;
|
|
7115
|
+
try {
|
|
7116
|
+
root = realpathSync3(paths.repoRoot);
|
|
7117
|
+
} catch {
|
|
7118
|
+
return { ok: false, why: `the folder ${paths.repoRoot} could not be opened. Check the path, then run this again.` };
|
|
7119
|
+
}
|
|
7120
|
+
let ancestor = paths.dir;
|
|
7121
|
+
while (lstatOrNull2(ancestor) === null) {
|
|
7122
|
+
const parent = dirname5(ancestor);
|
|
7123
|
+
if (parent === ancestor)
|
|
7124
|
+
break;
|
|
7125
|
+
ancestor = parent;
|
|
7126
|
+
}
|
|
7127
|
+
let real;
|
|
7128
|
+
try {
|
|
7129
|
+
real = realpathSync3(ancestor);
|
|
7130
|
+
} catch {
|
|
7131
|
+
return {
|
|
7132
|
+
ok: false,
|
|
7133
|
+
why: `${DIAGRAM_DIR_REL} leads to a place that does not exist. Remove the link in the way, then run this again.`
|
|
7134
|
+
};
|
|
7135
|
+
}
|
|
7136
|
+
if (!isInside(root, real)) {
|
|
7137
|
+
return {
|
|
7138
|
+
ok: false,
|
|
7139
|
+
why: `${DIAGRAM_DIR_REL} leads outside this repository, so nothing was written. Remove the link in the way, then run this again.`
|
|
7140
|
+
};
|
|
7141
|
+
}
|
|
7142
|
+
let isFolder = false;
|
|
7143
|
+
try {
|
|
7144
|
+
isFolder = lstatSync2(real).isDirectory();
|
|
7145
|
+
} catch {
|
|
7146
|
+
isFolder = false;
|
|
7147
|
+
}
|
|
7148
|
+
if (!isFolder) {
|
|
7149
|
+
const name = relative4(root, real).split("\\").join("/");
|
|
7150
|
+
return {
|
|
7151
|
+
ok: false,
|
|
7152
|
+
why: `${name === "" ? "this repository" : name} is not a folder, so ${DIAGRAM_DIR_REL} cannot be made. Move it, then run this again.`
|
|
7153
|
+
};
|
|
7154
|
+
}
|
|
7155
|
+
return { ok: true };
|
|
7156
|
+
}
|
|
7157
|
+
function startsWithMarkerLine(bytes) {
|
|
7158
|
+
const head = Buffer.from(bytes.subarray(0, DIAGRAM_MARKER_LINE.length + 2)).toString("utf-8");
|
|
7159
|
+
return head.startsWith(`${DIAGRAM_MARKER_LINE}
|
|
7160
|
+
`) || head.startsWith(`${DIAGRAM_MARKER_LINE}\r
|
|
7161
|
+
`);
|
|
7162
|
+
}
|
|
7163
|
+
function readHead(path) {
|
|
7164
|
+
const fd = openSync2(path, "r");
|
|
7165
|
+
try {
|
|
7166
|
+
const head = Buffer.alloc(DIAGRAM_MARKER_LINE.length + 2);
|
|
7167
|
+
const got = readSync(fd, head, 0, head.length, 0);
|
|
7168
|
+
return head.subarray(0, got);
|
|
7169
|
+
} finally {
|
|
7170
|
+
closeSync2(fd);
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
function readExistingSet(paths) {
|
|
7174
|
+
const found = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, null]));
|
|
7175
|
+
for (const which of DIAGRAM_FILES) {
|
|
7176
|
+
const stat = lstatOrNull2(paths.file[which]);
|
|
7177
|
+
if (stat === null)
|
|
7178
|
+
continue;
|
|
7179
|
+
if (!stat.isFile())
|
|
7180
|
+
return { ok: false, inTheWay: which, temp: false };
|
|
7181
|
+
try {
|
|
7182
|
+
found[which] = readFileSync11(paths.file[which]);
|
|
7183
|
+
} catch {
|
|
7184
|
+
return { ok: false, inTheWay: which, temp: false };
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
for (const look of SYSTEM_PICTURE_THEMES) {
|
|
7188
|
+
const svg = found[svgOf(look)];
|
|
7189
|
+
if (svg !== null && !startsWithMarkerLine(svg))
|
|
7190
|
+
return { ok: false, inTheWay: svgOf(look), temp: false };
|
|
7191
|
+
}
|
|
7192
|
+
for (const look of SYSTEM_PICTURE_THEMES) {
|
|
7193
|
+
if (found[pngOf(look)] !== null && found[svgOf(look)] === null)
|
|
7194
|
+
return { ok: false, inTheWay: pngOf(look), temp: false };
|
|
7195
|
+
}
|
|
7196
|
+
if (found.mmd !== null && found[svgOf(EMBEDDED_LOOK)] === null)
|
|
7197
|
+
return { ok: false, inTheWay: "mmd", temp: false };
|
|
7198
|
+
const temp = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, null]));
|
|
7199
|
+
for (const which of DIAGRAM_FILES) {
|
|
7200
|
+
const stat = lstatOrNull2(tempPath(paths, which));
|
|
7201
|
+
if (stat === null)
|
|
7202
|
+
continue;
|
|
7203
|
+
if (!stat.isFile())
|
|
7204
|
+
return { ok: false, inTheWay: which, temp: true };
|
|
7205
|
+
try {
|
|
7206
|
+
temp[which] = readHead(tempPath(paths, which));
|
|
7207
|
+
} catch {
|
|
7208
|
+
return { ok: false, inTheWay: which, temp: true };
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
for (const look of SYSTEM_PICTURE_THEMES) {
|
|
7212
|
+
const svg = temp[svgOf(look)];
|
|
7213
|
+
if (svg !== null && !startsWithMarkerLine(svg))
|
|
7214
|
+
return { ok: false, inTheWay: svgOf(look), temp: true };
|
|
7215
|
+
}
|
|
7216
|
+
const markedSvgBeside = (look) => found[svgOf(look)] !== null || temp[svgOf(look)] !== null;
|
|
7217
|
+
for (const look of SYSTEM_PICTURE_THEMES) {
|
|
7218
|
+
if (temp[pngOf(look)] !== null && !markedSvgBeside(look))
|
|
7219
|
+
return { ok: false, inTheWay: pngOf(look), temp: true };
|
|
7220
|
+
}
|
|
7221
|
+
if (temp.mmd !== null && !markedSvgBeside(EMBEDDED_LOOK))
|
|
7222
|
+
return { ok: false, inTheWay: "mmd", temp: true };
|
|
7223
|
+
return { ok: true, existing: found, ourTemps: DIAGRAM_FILES.filter((which) => temp[which] !== null) };
|
|
7224
|
+
}
|
|
7225
|
+
function tempPath(paths, which) {
|
|
7226
|
+
return `${paths.file[which]}.tmp`;
|
|
7227
|
+
}
|
|
7228
|
+
function inTheWayLine(base, which, temp = false) {
|
|
7229
|
+
return `${diagramRel(base, which)}${temp ? ".tmp" : ""} is already there and was not drawn by this command. Move it or delete it, then run this again. Nothing was written.`;
|
|
7230
|
+
}
|
|
7231
|
+
function checkPictures(drawn, base) {
|
|
7232
|
+
const set = {};
|
|
7233
|
+
for (const look of SYSTEM_PICTURE_THEMES) {
|
|
7234
|
+
const picture = drawn.pictures[look];
|
|
7235
|
+
const pngFor = diagramRel(base, pngOf(look));
|
|
7236
|
+
const svgFor = diagramRel(base, svgOf(look));
|
|
7237
|
+
const png = Buffer.from(picture.png, "base64");
|
|
7238
|
+
if (png.length > SYSTEM_DRAW_PNG_MAX_BYTES)
|
|
7239
|
+
return { ok: false, why: `the image for ${pngFor} is larger than 8 MB` };
|
|
7240
|
+
if (png.length < PNG_SIGNATURE.length || !PNG_SIGNATURE.every((b, i) => png[i] === b)) {
|
|
7241
|
+
return { ok: false, why: `the image for ${pngFor} is not a PNG` };
|
|
7242
|
+
}
|
|
7243
|
+
const svg = Buffer.from(picture.svg, "utf-8");
|
|
7244
|
+
if (svg.length > SYSTEM_DRAW_SVG_MAX_BYTES)
|
|
7245
|
+
return { ok: false, why: `the picture for ${svgFor} is larger than 2 MB` };
|
|
7246
|
+
const body = picture.svg.endsWith("\n") ? picture.svg.slice(0, -1) : picture.svg;
|
|
7247
|
+
if (!picture.svg.startsWith(`${DIAGRAM_MARKER_LINE}
|
|
7248
|
+
<svg`) || !body.endsWith("</svg>")) {
|
|
7249
|
+
return { ok: false, why: `the picture for ${svgFor} is not a complete SVG drawn by Halfcycle` };
|
|
7250
|
+
}
|
|
7251
|
+
set[pngOf(look)] = png;
|
|
7252
|
+
set[svgOf(look)] = svg;
|
|
7253
|
+
}
|
|
7254
|
+
const mmd = Buffer.from(drawn.mermaid, "utf-8");
|
|
7255
|
+
if (mmd.length > SYSTEM_DRAW_MERMAID_MAX_BYTES)
|
|
7256
|
+
return { ok: false, why: "the diagram text is larger than 256 KB" };
|
|
7257
|
+
if (!drawn.mermaid.startsWith("flowchart"))
|
|
7258
|
+
return { ok: false, why: "the diagram text is not a Mermaid flowchart" };
|
|
7259
|
+
set.mmd = mmd;
|
|
7260
|
+
return { ok: true, set };
|
|
7261
|
+
}
|
|
7262
|
+
function removeQuietly(path) {
|
|
7263
|
+
try {
|
|
7264
|
+
rmSync4(path, { force: true });
|
|
7265
|
+
} catch {
|
|
7266
|
+
return;
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
function sameBytes(a, b) {
|
|
7270
|
+
return a !== null && Buffer.from(a).equals(b);
|
|
7271
|
+
}
|
|
7272
|
+
function writeDiagramSet(repoRoot, base, set, io = NODE_RECORD_PAIR_IO) {
|
|
7273
|
+
const paths = diagramPaths(repoRoot, base);
|
|
7274
|
+
const place = checkDiagramPlace(paths);
|
|
7275
|
+
if (!place.ok)
|
|
7276
|
+
return { kind: "unsafe", why: place.why };
|
|
7277
|
+
const read = readExistingSet(paths);
|
|
7278
|
+
if (!read.ok)
|
|
7279
|
+
return { kind: "in-the-way", file: read.inTheWay, temp: read.temp };
|
|
7280
|
+
const existing = read.existing;
|
|
7281
|
+
const changed = DIAGRAM_FILES.filter((which) => !sameBytes(existing[which], set[which]));
|
|
7282
|
+
if (changed.length === 0)
|
|
7283
|
+
return { kind: "up-to-date" };
|
|
7284
|
+
try {
|
|
7285
|
+
mkdirSync8(paths.dir, { recursive: true });
|
|
7286
|
+
} catch {
|
|
7287
|
+
return { kind: "failed", file: changed[0], removed: [] };
|
|
7288
|
+
}
|
|
7289
|
+
const made = checkDiagramPlace(paths);
|
|
7290
|
+
if (!made.ok)
|
|
7291
|
+
return { kind: "unsafe", why: made.why };
|
|
7292
|
+
const tmp = (which) => tempPath(paths, which);
|
|
7293
|
+
for (const which of read.ourTemps)
|
|
7294
|
+
removeQuietly(tmp(which));
|
|
7295
|
+
const staged = [];
|
|
7296
|
+
const clearStaged = () => {
|
|
7297
|
+
for (const which of staged)
|
|
7298
|
+
removeQuietly(tmp(which));
|
|
7299
|
+
};
|
|
7300
|
+
for (const which of changed) {
|
|
7301
|
+
try {
|
|
7302
|
+
io.createExclusive(tmp(which), set[which]);
|
|
7303
|
+
staged.push(which);
|
|
7304
|
+
} catch {
|
|
7305
|
+
clearStaged();
|
|
7306
|
+
return { kind: "failed", file: which, removed: [] };
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
const renamed = [];
|
|
7310
|
+
for (const which of changed) {
|
|
7311
|
+
try {
|
|
7312
|
+
io.rename(tmp(which), paths.file[which]);
|
|
7313
|
+
renamed.push(which);
|
|
7314
|
+
} catch {
|
|
7315
|
+
clearStaged();
|
|
7316
|
+
const removed = restore(io, paths, renamed, existing);
|
|
7317
|
+
return { kind: "failed", file: which, removed };
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7320
|
+
return { kind: "written", changed };
|
|
7321
|
+
}
|
|
7322
|
+
function restore(io, paths, renamed, existing) {
|
|
7323
|
+
const removed = [];
|
|
7324
|
+
for (const which of [...renamed].reverse()) {
|
|
7325
|
+
const finalPath = paths.file[which];
|
|
7326
|
+
const previous = existing[which];
|
|
7327
|
+
const tmp = tempPath(paths, which);
|
|
7328
|
+
try {
|
|
7329
|
+
if (previous === null) {
|
|
7330
|
+
rmSync4(finalPath, { force: true });
|
|
7331
|
+
continue;
|
|
7332
|
+
}
|
|
7333
|
+
rmSync4(tmp, { force: true });
|
|
7334
|
+
io.createExclusive(tmp, previous);
|
|
7335
|
+
io.rename(tmp, finalPath);
|
|
7336
|
+
} catch {
|
|
7337
|
+
removeQuietly(tmp);
|
|
7338
|
+
removeQuietly(finalPath);
|
|
7339
|
+
removed.push(which);
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
return removed;
|
|
7343
|
+
}
|
|
7344
|
+
|
|
7345
|
+
// dist/draw/index.js
|
|
7346
|
+
var DRAW_USAGE = `${HALFCYCLE_COMMAND} draw (architecture | phase <phase>) [--repo <root>]`;
|
|
7347
|
+
function parseDrawArgs(argv) {
|
|
7348
|
+
const positionals = [];
|
|
7349
|
+
let repoRoot;
|
|
7350
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7351
|
+
const arg = argv[i];
|
|
7352
|
+
if (arg === "--repo") {
|
|
7353
|
+
const value = argv[i + 1];
|
|
7354
|
+
if (repoRoot !== void 0 || value === void 0 || value === "" || value.startsWith("-"))
|
|
7355
|
+
return { ok: false };
|
|
7356
|
+
repoRoot = value;
|
|
7357
|
+
i++;
|
|
7358
|
+
continue;
|
|
7359
|
+
}
|
|
7360
|
+
if (arg.startsWith("-"))
|
|
7361
|
+
return { ok: false };
|
|
7362
|
+
positionals.push(arg);
|
|
7363
|
+
}
|
|
7364
|
+
if (positionals.length === 1 && positionals[0] === "architecture") {
|
|
7365
|
+
return { ok: true, picture: { kind: "architecture" }, repoRoot };
|
|
7366
|
+
}
|
|
7367
|
+
if (positionals.length === 2 && positionals[0] === "phase") {
|
|
7368
|
+
return { ok: true, picture: { kind: "phase", phase: positionals[1] }, repoRoot };
|
|
7369
|
+
}
|
|
7370
|
+
return { ok: false };
|
|
7371
|
+
}
|
|
7372
|
+
function systemDrawUrl(credential) {
|
|
7373
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/system/draw`;
|
|
7374
|
+
}
|
|
7375
|
+
function serviceHost3(serviceUrl) {
|
|
7376
|
+
try {
|
|
7377
|
+
return new URL(serviceUrl).host;
|
|
7378
|
+
} catch {
|
|
7379
|
+
return serviceUrl;
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
async function requestDrawing(credential, request) {
|
|
7383
|
+
let res;
|
|
7384
|
+
let text;
|
|
7385
|
+
try {
|
|
7386
|
+
res = await fetch(systemDrawUrl(credential), {
|
|
7387
|
+
method: "POST",
|
|
7388
|
+
headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
|
|
7389
|
+
body: JSON.stringify(request)
|
|
7390
|
+
});
|
|
7391
|
+
text = await res.text();
|
|
7392
|
+
} catch {
|
|
7393
|
+
return {
|
|
7394
|
+
kind: "unavailable",
|
|
7395
|
+
why: `could not reach Halfcycle at ${serviceHost3(credential.serviceUrl)}. Check this machine's connection, then run this again. Nothing was written.`
|
|
7396
|
+
};
|
|
7397
|
+
}
|
|
7398
|
+
let raw = null;
|
|
7399
|
+
try {
|
|
7400
|
+
raw = JSON.parse(text);
|
|
7401
|
+
} catch {
|
|
7402
|
+
raw = null;
|
|
7403
|
+
}
|
|
7404
|
+
const parsed = systemDrawResultSchema.safeParse(raw);
|
|
7405
|
+
if (parsed.success) {
|
|
7406
|
+
const answer = parsed.data;
|
|
7407
|
+
if (answer.outcome === "drawn")
|
|
7408
|
+
return { kind: "drawn", drawn: answer };
|
|
7409
|
+
if (answer.outcome === "refused")
|
|
7410
|
+
return { kind: "refused", reason: answer.reason };
|
|
7411
|
+
return {
|
|
7412
|
+
kind: "unavailable",
|
|
7413
|
+
why: "Halfcycle could not draw the pictures just now. Nothing was written; run this again in a minute."
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7416
|
+
if (isMissingRoute("POST", res.status, raw))
|
|
7417
|
+
return { kind: "no-route" };
|
|
7418
|
+
if (res.ok) {
|
|
7419
|
+
return {
|
|
7420
|
+
kind: "unavailable",
|
|
7421
|
+
why: "Halfcycle sent an answer this command could not read. Nothing was written; try again later."
|
|
7422
|
+
};
|
|
7423
|
+
}
|
|
7424
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
7425
|
+
return {
|
|
7426
|
+
kind: "unavailable",
|
|
7427
|
+
why: `Halfcycle answered ${res.status}${message === null ? "." : `: ${message}`} Nothing was written.`
|
|
7428
|
+
};
|
|
7429
|
+
}
|
|
7430
|
+
var P = "halfcycle draw:";
|
|
7431
|
+
var CANNOT_DRAW_YET = `${P} this Halfcycle service cannot draw pictures yet. Nothing was written.
|
|
7432
|
+
`;
|
|
7433
|
+
var CANNOT_DRAW_PHASE_YET = `${P} this Halfcycle service cannot draw phase pictures yet. Nothing was written.
|
|
7434
|
+
`;
|
|
7435
|
+
function markdownAltText(text) {
|
|
7436
|
+
return text.replace(/[[\]\\]/g, (ch) => `\\${ch}`);
|
|
7437
|
+
}
|
|
7438
|
+
var ARCHITECTURE_PASTE = { doc: "docs", alt: "Architecture", base: ARCHITECTURE_BASE };
|
|
7439
|
+
function phasePaste(phase) {
|
|
7440
|
+
return { doc: "docs/phases", alt: `Phase ${phase}`, base: phaseDiagramBase(phase) };
|
|
7441
|
+
}
|
|
7442
|
+
function pasteLines(mermaid, target) {
|
|
7443
|
+
const block = mermaid.endsWith("\n") ? mermaid : `${mermaid}
|
|
7444
|
+
`;
|
|
7445
|
+
const where = target.doc === "docs" ? "a document directly under docs/" : "this phase's document in docs/phases/";
|
|
7446
|
+
return `
|
|
7447
|
+
Paste this into ${where}:
|
|
7448
|
+
|
|
7449
|
+
})
|
|
7450
|
+
|
|
7451
|
+
\`\`\`mermaid
|
|
7452
|
+
` + block + "```\n";
|
|
7453
|
+
}
|
|
7454
|
+
function localRefusal(repoRoot, base) {
|
|
7455
|
+
const paths = diagramPaths(repoRoot, base);
|
|
7456
|
+
const place = checkDiagramPlace(paths);
|
|
7457
|
+
if (!place.ok)
|
|
7458
|
+
return `${P} ${place.why}
|
|
7459
|
+
`;
|
|
7460
|
+
const read = readExistingSet(paths);
|
|
7461
|
+
if (!read.ok)
|
|
7462
|
+
return `${P} ${inTheWayLine(base, read.inTheWay, read.temp)}
|
|
7463
|
+
`;
|
|
7464
|
+
return null;
|
|
7465
|
+
}
|
|
7466
|
+
async function runDraw(repoRoot, picture, deps) {
|
|
7467
|
+
if (picture.kind === "architecture") {
|
|
7468
|
+
const stage2 = await checkAndReportDescription(repoRoot, deps, "draw", () => localRefusal(repoRoot, ARCHITECTURE_BASE));
|
|
7469
|
+
if (stage2.kind === "stopped")
|
|
7470
|
+
return { stdout: "", stderr: stage2.stderr, exitCode: 1 };
|
|
7471
|
+
if (stage2.kind === "no-route")
|
|
7472
|
+
return { stdout: "", stderr: stage2.warnings + CANNOT_DRAW_YET, exitCode: 1 };
|
|
7473
|
+
return drawAndWrite(repoRoot, deps, stage2.credential, { version: stage2.version, picture: "architecture" }, ARCHITECTURE_PASTE, stage2.warnings, CANNOT_DRAW_YET);
|
|
7474
|
+
}
|
|
7475
|
+
const phase = picture.phase;
|
|
7476
|
+
if (!isValidPhaseIdentity(phase))
|
|
7477
|
+
return { stdout: "", stderr: notAPhaseNameLine("draw", phase), exitCode: 1 };
|
|
7478
|
+
const base = phaseDiagramBase(phase);
|
|
7479
|
+
if (baseTakesFlightDeckNames(base)) {
|
|
7480
|
+
return {
|
|
7481
|
+
stdout: "",
|
|
7482
|
+
stderr: `${P} the phase "${phase}" cannot be drawn: its pictures would take the names of another phase's Flight Deck pictures. Name the phase so it does not end in ".flightdeck", then run this again. Nothing was sent.
|
|
7483
|
+
`,
|
|
7484
|
+
exitCode: 1
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
7487
|
+
let checkedPlan = null;
|
|
7488
|
+
const beforeSend = (description) => {
|
|
7489
|
+
const checked = readAndCheckPlan(repoRoot, phase, description, "draw");
|
|
7490
|
+
if (!checked.ok)
|
|
7491
|
+
return checked.line;
|
|
7492
|
+
const refusal = localRefusal(repoRoot, base);
|
|
7493
|
+
if (refusal !== null)
|
|
7494
|
+
return refusal;
|
|
7495
|
+
checkedPlan = checked.plan;
|
|
7496
|
+
return null;
|
|
7497
|
+
};
|
|
7498
|
+
const stage = await checkAndReportDescription(repoRoot, deps, "draw", beforeSend);
|
|
7499
|
+
if (stage.kind === "stopped")
|
|
7500
|
+
return { stdout: "", stderr: stage.stderr, exitCode: 1 };
|
|
7501
|
+
if (stage.kind === "no-route")
|
|
7502
|
+
return { stdout: "", stderr: stage.warnings + CANNOT_DRAW_PHASE_YET, exitCode: 1 };
|
|
7503
|
+
const warnings = stage.warnings;
|
|
7504
|
+
const plan = checkedPlan;
|
|
7505
|
+
if (plan === null) {
|
|
7506
|
+
return { stdout: "", stderr: warnings + `${P} the plan was not checked, so nothing more was sent.
|
|
7507
|
+
`, exitCode: 1 };
|
|
7508
|
+
}
|
|
7509
|
+
const reported = await deps.reportPlan(stage.credential, plan, stage.version);
|
|
7510
|
+
switch (reported.kind) {
|
|
7511
|
+
case "no-route":
|
|
7512
|
+
return { stdout: "", stderr: warnings + CANNOT_DRAW_PHASE_YET, exitCode: 1 };
|
|
7513
|
+
case "unavailable":
|
|
7514
|
+
return { stdout: "", stderr: warnings + `${P} ${reported.why}
|
|
7515
|
+
`, exitCode: 1 };
|
|
7516
|
+
case "refused":
|
|
7517
|
+
return { stdout: "", stderr: warnings + planRefusedLines("draw", phase, plan, reported.problems), exitCode: 1 };
|
|
7518
|
+
case "recorded":
|
|
7519
|
+
break;
|
|
7520
|
+
}
|
|
7521
|
+
return drawAndWrite(repoRoot, deps, stage.credential, { picture: "phase", phase: plan.phase, planVersion: reported.version }, phasePaste(phase), warnings, CANNOT_DRAW_PHASE_YET);
|
|
7522
|
+
}
|
|
7523
|
+
async function drawAndWrite(repoRoot, deps, credential, request, target, warnings, cannotDrawYet) {
|
|
7524
|
+
const base = target.base;
|
|
7525
|
+
const outcome = await deps.draw(credential, request);
|
|
7526
|
+
if (outcome.kind === "no-route")
|
|
7527
|
+
return { stdout: "", stderr: warnings + cannotDrawYet, exitCode: 1 };
|
|
7528
|
+
if (outcome.kind === "refused") {
|
|
7529
|
+
return {
|
|
7530
|
+
stdout: "",
|
|
7531
|
+
stderr: warnings + `${P} Halfcycle would not draw the pictures: ${outcome.reason} Nothing was written.
|
|
7532
|
+
`,
|
|
7533
|
+
exitCode: 1
|
|
7534
|
+
};
|
|
7535
|
+
}
|
|
7536
|
+
if (outcome.kind === "unavailable")
|
|
7537
|
+
return { stdout: "", stderr: warnings + `${P} ${outcome.why}
|
|
7538
|
+
`, exitCode: 1 };
|
|
7539
|
+
const checked = checkPictures(outcome.drawn, base);
|
|
7540
|
+
if (!checked.ok) {
|
|
7541
|
+
return {
|
|
7542
|
+
stdout: "",
|
|
7543
|
+
stderr: warnings + `${P} Halfcycle sent pictures this command cannot use (${checked.why}). Nothing was written; try again later.
|
|
7544
|
+
`,
|
|
7545
|
+
exitCode: 1
|
|
7546
|
+
};
|
|
7547
|
+
}
|
|
7548
|
+
const rel = (which) => diagramRel(base, which);
|
|
7549
|
+
const result = deps.write(repoRoot, base, checked.set);
|
|
7550
|
+
switch (result.kind) {
|
|
7551
|
+
case "up-to-date":
|
|
7552
|
+
return {
|
|
7553
|
+
stdout: `[halfcycle] The pictures in ${DIAGRAM_DIR_REL}/ are up to date.
|
|
7554
|
+
` + pasteLines(outcome.drawn.mermaid, target),
|
|
7555
|
+
stderr: warnings,
|
|
7556
|
+
exitCode: 0
|
|
7557
|
+
};
|
|
7558
|
+
case "written":
|
|
7559
|
+
return {
|
|
7560
|
+
stdout: `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(rel))}.
|
|
7561
|
+
` + pasteLines(outcome.drawn.mermaid, target),
|
|
7562
|
+
stderr: warnings,
|
|
7563
|
+
exitCode: 0
|
|
7564
|
+
};
|
|
7565
|
+
case "in-the-way":
|
|
7566
|
+
return { stdout: "", stderr: warnings + `${P} ${inTheWayLine(base, result.file, result.temp)}
|
|
7567
|
+
`, exitCode: 1 };
|
|
7568
|
+
case "unsafe":
|
|
7569
|
+
return { stdout: "", stderr: warnings + `${P} ${result.why}
|
|
7570
|
+
`, exitCode: 1 };
|
|
7571
|
+
case "failed": {
|
|
7572
|
+
const lost = result.removed.length === 0 ? "The pictures already there are as they were." : `${spokenList(result.removed.map(rel))} could not be put back as ${result.removed.length === 1 ? "it was, so it was" : "they were, so they were"} removed; the other pictures are as they were.`;
|
|
7573
|
+
return {
|
|
7574
|
+
stdout: "",
|
|
7575
|
+
stderr: warnings + `${P} ${rel(result.file)} could not be written. ${lost} Check that you can write to ${DIAGRAM_DIR_REL}, then run this again.
|
|
7576
|
+
`,
|
|
7577
|
+
exitCode: 1
|
|
7578
|
+
};
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
|
|
5535
7583
|
// dist/cli-contract.js
|
|
5536
|
-
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "uninstall"];
|
|
7584
|
+
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "uninstall"];
|
|
5537
7585
|
var CONTRACTS = [
|
|
5538
7586
|
{ verb: "install", required: [], conditional: [] },
|
|
5539
7587
|
{ verb: "check-drift", required: [], conditional: [] },
|
|
5540
7588
|
{ verb: "build-record", required: [], conditional: [] },
|
|
7589
|
+
{ verb: "system", required: [], conditional: [] },
|
|
7590
|
+
{ verb: "draw", required: [], conditional: [] },
|
|
7591
|
+
{ verb: "plan", required: [], conditional: [] },
|
|
5541
7592
|
{ verb: "uninstall", required: [], conditional: [] },
|
|
5542
7593
|
{
|
|
5543
7594
|
verb: "open-phase",
|
|
@@ -5674,9 +7725,9 @@ function placeholderRefusal(verb, tokens) {
|
|
|
5674
7725
|
}
|
|
5675
7726
|
|
|
5676
7727
|
// dist/uninstall.js
|
|
5677
|
-
import { lstatSync as
|
|
7728
|
+
import { lstatSync as lstatSync3, readFileSync as readFileSync12, readdirSync, rmSync as rmSync5, rmdirSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5678
7729
|
import { homedir as homedir2 } from "node:os";
|
|
5679
|
-
import { dirname as
|
|
7730
|
+
import { dirname as dirname6, join as join13, relative as relative5, sep as sep2 } from "node:path";
|
|
5680
7731
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
5681
7732
|
function parseUninstallArgs(args2) {
|
|
5682
7733
|
let removeCredential = false;
|
|
@@ -5693,7 +7744,7 @@ function parseUninstallArgs(args2) {
|
|
|
5693
7744
|
}
|
|
5694
7745
|
function lexists(path) {
|
|
5695
7746
|
try {
|
|
5696
|
-
|
|
7747
|
+
lstatSync3(path);
|
|
5697
7748
|
return true;
|
|
5698
7749
|
} catch {
|
|
5699
7750
|
return false;
|
|
@@ -5702,9 +7753,9 @@ function lexists(path) {
|
|
|
5702
7753
|
function symlinkOnPath(root, rel) {
|
|
5703
7754
|
let current = root;
|
|
5704
7755
|
for (const segment of rel.split("/")) {
|
|
5705
|
-
current =
|
|
7756
|
+
current = join13(current, segment);
|
|
5706
7757
|
try {
|
|
5707
|
-
if (
|
|
7758
|
+
if (lstatSync3(current).isSymbolicLink())
|
|
5708
7759
|
return true;
|
|
5709
7760
|
} catch {
|
|
5710
7761
|
return false;
|
|
@@ -5712,7 +7763,7 @@ function symlinkOnPath(root, rel) {
|
|
|
5712
7763
|
}
|
|
5713
7764
|
return false;
|
|
5714
7765
|
}
|
|
5715
|
-
function
|
|
7766
|
+
function isPlainObject3(value) {
|
|
5716
7767
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5717
7768
|
}
|
|
5718
7769
|
function serializeJson(value) {
|
|
@@ -5724,13 +7775,13 @@ function readMergedJson(targetRepo, rule) {
|
|
|
5724
7775
|
const { rel } = rule;
|
|
5725
7776
|
if (symlinkOnPath(targetRepo, rel))
|
|
5726
7777
|
return { state: "symlink" };
|
|
5727
|
-
const path =
|
|
7778
|
+
const path = join13(targetRepo, rel);
|
|
5728
7779
|
if (!lexists(path))
|
|
5729
7780
|
return { state: "absent" };
|
|
5730
7781
|
let text;
|
|
5731
7782
|
let value;
|
|
5732
7783
|
try {
|
|
5733
|
-
text =
|
|
7784
|
+
text = readFileSync12(path, "utf-8");
|
|
5734
7785
|
} catch {
|
|
5735
7786
|
text = "";
|
|
5736
7787
|
}
|
|
@@ -5739,7 +7790,7 @@ function readMergedJson(targetRepo, rule) {
|
|
|
5739
7790
|
} catch {
|
|
5740
7791
|
value = void 0;
|
|
5741
7792
|
}
|
|
5742
|
-
if (
|
|
7793
|
+
if (isPlainObject3(value))
|
|
5743
7794
|
return { state: "object", text, value };
|
|
5744
7795
|
if (!rule.holdsOurs(text))
|
|
5745
7796
|
return { state: "not-ours" };
|
|
@@ -5747,15 +7798,15 @@ function readMergedJson(targetRepo, rule) {
|
|
|
5747
7798
|
}
|
|
5748
7799
|
function validate(options, rules) {
|
|
5749
7800
|
const { targetRepo, removeCredential, home } = options;
|
|
5750
|
-
const manifestPath =
|
|
5751
|
-
const pinPath =
|
|
7801
|
+
const manifestPath = join13(targetRepo, INSTALL_MANIFEST_REL);
|
|
7802
|
+
const pinPath = join13(targetRepo, BUNDLE_PIN_REL);
|
|
5752
7803
|
if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
|
|
5753
7804
|
throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
|
|
5754
7805
|
}
|
|
5755
7806
|
let manifest = null;
|
|
5756
7807
|
if (lexists(manifestPath)) {
|
|
5757
7808
|
try {
|
|
5758
|
-
manifest = parseManifest(
|
|
7809
|
+
manifest = parseManifest(readFileSync12(manifestPath, "utf-8"));
|
|
5759
7810
|
} catch (err) {
|
|
5760
7811
|
const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
|
|
5761
7812
|
throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "${HALFCYCLE_COMMAND}" once to rewrite it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
|
|
@@ -5765,8 +7816,8 @@ function validate(options, rules) {
|
|
|
5765
7816
|
let pinReadable = true;
|
|
5766
7817
|
if (lexists(pinPath)) {
|
|
5767
7818
|
try {
|
|
5768
|
-
const pin = JSON.parse(
|
|
5769
|
-
const id =
|
|
7819
|
+
const pin = JSON.parse(readFileSync12(pinPath, "utf-8"));
|
|
7820
|
+
const id = isPlainObject3(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
|
|
5770
7821
|
engagementId = typeof id === "string" && id !== "" ? id : void 0;
|
|
5771
7822
|
} catch {
|
|
5772
7823
|
pinReadable = false;
|
|
@@ -5789,8 +7840,8 @@ function validate(options, rules) {
|
|
|
5789
7840
|
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.`);
|
|
5790
7841
|
}
|
|
5791
7842
|
if (engagementId !== void 0) {
|
|
5792
|
-
const expectedParent =
|
|
5793
|
-
if (
|
|
7843
|
+
const expectedParent = join13(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
7844
|
+
if (dirname6(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
5794
7845
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
5795
7846
|
}
|
|
5796
7847
|
}
|
|
@@ -5813,13 +7864,13 @@ function mergedFileRules(writeSet) {
|
|
|
5813
7864
|
function writeBack(targetRepo, rel, current, next, lists, name) {
|
|
5814
7865
|
if (next === current)
|
|
5815
7866
|
return;
|
|
5816
|
-
const path =
|
|
7867
|
+
const path = join13(targetRepo, rel);
|
|
5817
7868
|
try {
|
|
5818
7869
|
if (next === null) {
|
|
5819
|
-
|
|
7870
|
+
rmSync5(path);
|
|
5820
7871
|
name(lists.removed, rel, rel);
|
|
5821
7872
|
} else {
|
|
5822
|
-
|
|
7873
|
+
writeFileSync7(path, next, "utf-8");
|
|
5823
7874
|
name(lists.restored, rel, rel);
|
|
5824
7875
|
}
|
|
5825
7876
|
} catch {
|
|
@@ -5831,7 +7882,7 @@ function noRecord(targetRepo, writeSet) {
|
|
|
5831
7882
|
const claim = (rel, header, lines) => {
|
|
5832
7883
|
let text;
|
|
5833
7884
|
try {
|
|
5834
|
-
text = symlinkOnPath(targetRepo, rel) ? "" :
|
|
7885
|
+
text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync12(join13(targetRepo, rel), "utf-8");
|
|
5835
7886
|
} catch {
|
|
5836
7887
|
text = "";
|
|
5837
7888
|
}
|
|
@@ -5860,16 +7911,16 @@ function noRecord(targetRepo, writeSet) {
|
|
|
5860
7911
|
}
|
|
5861
7912
|
function ourHookEntries(value, commands) {
|
|
5862
7913
|
const hooks = value["hooks"];
|
|
5863
|
-
if (!
|
|
7914
|
+
if (!isPlainObject3(hooks))
|
|
5864
7915
|
return [];
|
|
5865
7916
|
const found = [];
|
|
5866
7917
|
for (const [event, entries] of Object.entries(hooks)) {
|
|
5867
7918
|
if (!Array.isArray(entries))
|
|
5868
7919
|
continue;
|
|
5869
7920
|
for (const entry of entries) {
|
|
5870
|
-
if (!
|
|
7921
|
+
if (!isPlainObject3(entry) || !Array.isArray(entry["hooks"]))
|
|
5871
7922
|
continue;
|
|
5872
|
-
const objects = entry["hooks"].filter(
|
|
7923
|
+
const objects = entry["hooks"].filter(isPlainObject3);
|
|
5873
7924
|
if (isHalfcycleEntry({ hooks: objects }, commands)) {
|
|
5874
7925
|
found.push({ event, entry });
|
|
5875
7926
|
}
|
|
@@ -5879,7 +7930,7 @@ function ourHookEntries(value, commands) {
|
|
|
5879
7930
|
}
|
|
5880
7931
|
function ourMcpServers(value) {
|
|
5881
7932
|
const servers = value["mcpServers"];
|
|
5882
|
-
return
|
|
7933
|
+
return isPlainObject3(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
5883
7934
|
}
|
|
5884
7935
|
function unmergeSettings(value, rec, writeSet) {
|
|
5885
7936
|
let changed = false;
|
|
@@ -5890,7 +7941,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5890
7941
|
refused.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
|
|
5891
7942
|
}
|
|
5892
7943
|
const hooks = value["hooks"];
|
|
5893
|
-
if (
|
|
7944
|
+
if (isPlainObject3(hooks)) {
|
|
5894
7945
|
const ours = new Set(ourHookEntries(value, commands).map(({ entry }) => entry));
|
|
5895
7946
|
let emptiedAnEvent = false;
|
|
5896
7947
|
for (const event of Object.keys(hooks)) {
|
|
@@ -5925,7 +7976,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5925
7976
|
}
|
|
5926
7977
|
}
|
|
5927
7978
|
const permissions = value["permissions"];
|
|
5928
|
-
if (
|
|
7979
|
+
if (isPlainObject3(permissions) && Array.isArray(permissions["deny"])) {
|
|
5929
7980
|
const deny = permissions["deny"];
|
|
5930
7981
|
for (const hash of denyHashes) {
|
|
5931
7982
|
for (let i = deny.length - 1; i >= 0; i--) {
|
|
@@ -5957,7 +8008,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
5957
8008
|
}
|
|
5958
8009
|
function unmergeMcp(value, rec) {
|
|
5959
8010
|
const servers = value["mcpServers"];
|
|
5960
|
-
if (!
|
|
8011
|
+
if (!isPlainObject3(servers))
|
|
5961
8012
|
return { changed: false };
|
|
5962
8013
|
let changed = false;
|
|
5963
8014
|
let removedEntry = false;
|
|
@@ -6057,7 +8108,7 @@ function lastOccurrence(text, block) {
|
|
|
6057
8108
|
}
|
|
6058
8109
|
function coveredPathExists(targetRepo, line) {
|
|
6059
8110
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
6060
|
-
return path !== "" && lexists(
|
|
8111
|
+
return path !== "" && lexists(join13(targetRepo, path));
|
|
6061
8112
|
}
|
|
6062
8113
|
function uninstall(options) {
|
|
6063
8114
|
const result = {
|
|
@@ -6137,7 +8188,7 @@ function uninstall(options) {
|
|
|
6137
8188
|
const gitattributes = manifest.gitattributes ?? { created: false, appended: [] };
|
|
6138
8189
|
trimAppendedFile(targetRepo, gitattributes, deleteWhenEmpty(gitattributes.created), gitattributesRules(writeSet), result, name);
|
|
6139
8190
|
const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0 && result.keptIgnoreLines.length === 0;
|
|
6140
|
-
if (checked.manifest === null && !lexists(
|
|
8191
|
+
if (checked.manifest === null && !lexists(join13(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
|
|
6141
8192
|
return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
|
|
6142
8193
|
}
|
|
6143
8194
|
const credential = handleCredential(options, checked);
|
|
@@ -6145,7 +8196,7 @@ function uninstall(options) {
|
|
|
6145
8196
|
if (result.failed.length === 0 && !credentialFailed) {
|
|
6146
8197
|
removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
|
|
6147
8198
|
try {
|
|
6148
|
-
|
|
8199
|
+
rmSync5(join13(targetRepo, INSTALL_MANIFEST_REL), { force: true });
|
|
6149
8200
|
named.add(INSTALL_MANIFEST_REL);
|
|
6150
8201
|
} catch {
|
|
6151
8202
|
name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
|
|
@@ -6180,7 +8231,7 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
|
|
|
6180
8231
|
return;
|
|
6181
8232
|
const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
|
|
6182
8233
|
writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
|
|
6183
|
-
if (adopted && lexists(
|
|
8234
|
+
if (adopted && lexists(join13(targetRepo, rel))) {
|
|
6184
8235
|
name(lists.leftAlone, `${rel} (Halfcycle's hooks were taken out. There is no record of this file from before the install, so a permission rule or "$schema" Halfcycle added may still be there \u2014 it cannot be told apart from your own)`, rel);
|
|
6185
8236
|
}
|
|
6186
8237
|
}
|
|
@@ -6193,10 +8244,10 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
6193
8244
|
name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
|
|
6194
8245
|
return;
|
|
6195
8246
|
}
|
|
6196
|
-
const abs =
|
|
8247
|
+
const abs = join13(targetRepo, path);
|
|
6197
8248
|
let isFile;
|
|
6198
8249
|
try {
|
|
6199
|
-
isFile =
|
|
8250
|
+
isFile = lstatSync3(abs).isFile();
|
|
6200
8251
|
} catch {
|
|
6201
8252
|
return;
|
|
6202
8253
|
}
|
|
@@ -6207,7 +8258,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
6207
8258
|
if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
|
|
6208
8259
|
let bytes;
|
|
6209
8260
|
try {
|
|
6210
|
-
bytes =
|
|
8261
|
+
bytes = readFileSync12(abs);
|
|
6211
8262
|
} catch {
|
|
6212
8263
|
name(lists.failed, path, path);
|
|
6213
8264
|
return;
|
|
@@ -6218,7 +8269,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
6218
8269
|
}
|
|
6219
8270
|
}
|
|
6220
8271
|
try {
|
|
6221
|
-
|
|
8272
|
+
rmSync5(abs);
|
|
6222
8273
|
name(lists.removed, path, path);
|
|
6223
8274
|
} catch {
|
|
6224
8275
|
name(lists.failed, path, path);
|
|
@@ -6227,9 +8278,9 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
6227
8278
|
function removeEmptyDir(targetRepo, dir, writeSet, lists, name) {
|
|
6228
8279
|
if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
|
|
6229
8280
|
return;
|
|
6230
|
-
const abs =
|
|
8281
|
+
const abs = join13(targetRepo, dir);
|
|
6231
8282
|
try {
|
|
6232
|
-
if (!
|
|
8283
|
+
if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
|
|
6233
8284
|
return;
|
|
6234
8285
|
} catch {
|
|
6235
8286
|
return;
|
|
@@ -6246,10 +8297,10 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
|
|
|
6246
8297
|
name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
|
|
6247
8298
|
return [];
|
|
6248
8299
|
}
|
|
6249
|
-
const path =
|
|
8300
|
+
const path = join13(targetRepo, rel);
|
|
6250
8301
|
let before;
|
|
6251
8302
|
try {
|
|
6252
|
-
before =
|
|
8303
|
+
before = readFileSync12(path, "utf-8");
|
|
6253
8304
|
} catch {
|
|
6254
8305
|
return [];
|
|
6255
8306
|
}
|
|
@@ -6277,10 +8328,10 @@ function handleCredential(options, checked) {
|
|
|
6277
8328
|
if (!lexists(dir))
|
|
6278
8329
|
return { kind: "not-held", dir };
|
|
6279
8330
|
try {
|
|
6280
|
-
if (
|
|
8331
|
+
if (lstatSync3(dir).isSymbolicLink())
|
|
6281
8332
|
return { kind: "failed", dir, reason: "it is a symbolic link" };
|
|
6282
|
-
|
|
6283
|
-
|
|
8333
|
+
rmSync5(engagementEnvPath(engagementId, home), { force: true });
|
|
8334
|
+
rmSync5(notThisAccountMarkerPath(engagementId, home), { force: true });
|
|
6284
8335
|
if (readdirSync(dir).length === 0)
|
|
6285
8336
|
rmdirSync(dir);
|
|
6286
8337
|
} catch (err) {
|
|
@@ -6293,16 +8344,16 @@ function filesUnder(root, rel) {
|
|
|
6293
8344
|
const walk = (dirRel) => {
|
|
6294
8345
|
let names;
|
|
6295
8346
|
try {
|
|
6296
|
-
if (!
|
|
8347
|
+
if (!lstatSync3(join13(root, dirRel)).isDirectory())
|
|
6297
8348
|
return;
|
|
6298
|
-
names = readdirSync(
|
|
8349
|
+
names = readdirSync(join13(root, dirRel));
|
|
6299
8350
|
} catch {
|
|
6300
8351
|
return;
|
|
6301
8352
|
}
|
|
6302
8353
|
for (const entry of names.sort()) {
|
|
6303
8354
|
const childRel = `${dirRel}/${entry}`;
|
|
6304
8355
|
try {
|
|
6305
|
-
if (
|
|
8356
|
+
if (lstatSync3(join13(root, childRel)).isDirectory())
|
|
6306
8357
|
walk(childRel);
|
|
6307
8358
|
else
|
|
6308
8359
|
out.push(childRel);
|
|
@@ -6316,9 +8367,9 @@ function filesUnder(root, rel) {
|
|
|
6316
8367
|
}
|
|
6317
8368
|
function shownPath(path, home) {
|
|
6318
8369
|
const base = home ?? homedir2();
|
|
6319
|
-
const rel =
|
|
6320
|
-
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(
|
|
6321
|
-
return `${under ? `~/${rel.split(
|
|
8370
|
+
const rel = relative5(base, path);
|
|
8371
|
+
const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep2);
|
|
8372
|
+
return `${under ? `~/${rel.split(sep2).join("/")}` : path}/`;
|
|
6322
8373
|
}
|
|
6323
8374
|
function renderUninstallReport(result, home) {
|
|
6324
8375
|
if (result.outcome === "refused") {
|
|
@@ -6374,8 +8425,8 @@ function renderUninstallReport(result, home) {
|
|
|
6374
8425
|
}
|
|
6375
8426
|
|
|
6376
8427
|
// dist/banner-facts.js
|
|
6377
|
-
import { readFileSync as
|
|
6378
|
-
import { basename as basename3, join as
|
|
8428
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
8429
|
+
import { basename as basename3, join as join14, resolve as resolve4 } from "node:path";
|
|
6379
8430
|
var BRAND_URL = "halfcycle.ai";
|
|
6380
8431
|
var TAGLINE = [
|
|
6381
8432
|
"Fewer cycles.",
|
|
@@ -6388,7 +8439,7 @@ var TAGLINE_NARROW = [
|
|
|
6388
8439
|
];
|
|
6389
8440
|
function bundleVersion() {
|
|
6390
8441
|
try {
|
|
6391
|
-
const pkg = JSON.parse(
|
|
8442
|
+
const pkg = JSON.parse(readFileSync13(join14(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
6392
8443
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
6393
8444
|
} catch {
|
|
6394
8445
|
return void 0;
|
|
@@ -6467,7 +8518,7 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
|
|
|
6467
8518
|
}
|
|
6468
8519
|
function safeBasename(dir) {
|
|
6469
8520
|
try {
|
|
6470
|
-
const name = basename3(
|
|
8521
|
+
const name = basename3(resolve4(dir)).trim();
|
|
6471
8522
|
return name === "" ? "project" : name;
|
|
6472
8523
|
} catch {
|
|
6473
8524
|
return "project";
|
|
@@ -6528,6 +8579,12 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
6528
8579
|
${HALFCYCLE_COMMAND} close-phase <phase> --verdict <clean|defects> --actor "\u2026" [--finding "\u2026"]\u2026 [--override --reason "\u2026"] [--repo <root>]
|
|
6529
8580
|
[--thesis "\u2026"] [--outcome "\u2026"] [--deviation "\u2026"]\u2026
|
|
6530
8581
|
close the phase and write its Build Record into docs/build-records/
|
|
8582
|
+
${HALFCYCLE_COMMAND} system [--repo <root>]
|
|
8583
|
+
check docs/system.json, the description of your system, and send it to Halfcycle
|
|
8584
|
+
${DRAW_USAGE}
|
|
8585
|
+
draw the architecture picture, or one phase's plan, from docs/system.json into docs/diagrams/
|
|
8586
|
+
${PLAN_USAGE}
|
|
8587
|
+
check a phase's plan in docs/phases/ and send it to Halfcycle; --show lists it for the owner
|
|
6531
8588
|
${HALFCYCLE_COMMAND} uninstall [${REMOVE_CREDENTIAL_FLAG}]
|
|
6532
8589
|
take out what the installer added to this project, keeping anything you changed
|
|
6533
8590
|
${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
|
|
@@ -6540,7 +8597,7 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
6540
8597
|
`;
|
|
6541
8598
|
function isHalfcycleMonorepo(dir) {
|
|
6542
8599
|
try {
|
|
6543
|
-
const pkg = JSON.parse(
|
|
8600
|
+
const pkg = JSON.parse(readFileSync14(join15(dir, "package.json"), "utf-8"));
|
|
6544
8601
|
return pkg.name === "halfcycle-monorepo";
|
|
6545
8602
|
} catch {
|
|
6546
8603
|
return false;
|
|
@@ -6548,8 +8605,8 @@ function isHalfcycleMonorepo(dir) {
|
|
|
6548
8605
|
}
|
|
6549
8606
|
function isHomeFolder(dir) {
|
|
6550
8607
|
try {
|
|
6551
|
-
const target =
|
|
6552
|
-
const home =
|
|
8608
|
+
const target = realpathSync4(dir);
|
|
8609
|
+
const home = realpathSync4(homedir3());
|
|
6553
8610
|
return process.platform === "win32" ? target.toLowerCase() === home.toLowerCase() : target === home;
|
|
6554
8611
|
} catch {
|
|
6555
8612
|
return false;
|
|
@@ -6976,6 +9033,69 @@ ${USAGE}`);
|
|
|
6976
9033
|
}
|
|
6977
9034
|
return;
|
|
6978
9035
|
}
|
|
9036
|
+
if (cmd === "system") {
|
|
9037
|
+
const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
|
|
9038
|
+
const run = await runSystem(repoRoot, {
|
|
9039
|
+
readCredential: (root) => readRepoCredential(root),
|
|
9040
|
+
listTracked: listTrackedFiles,
|
|
9041
|
+
report: reportSystemDescription
|
|
9042
|
+
});
|
|
9043
|
+
if (run.stdout !== "")
|
|
9044
|
+
process.stdout.write(run.stdout);
|
|
9045
|
+
if (run.stderr !== "")
|
|
9046
|
+
process.stderr.write(run.stderr);
|
|
9047
|
+
process.exitCode = run.exitCode;
|
|
9048
|
+
return;
|
|
9049
|
+
}
|
|
9050
|
+
if (cmd === "draw") {
|
|
9051
|
+
const parsed = parseDrawArgs(rest);
|
|
9052
|
+
if (!parsed.ok) {
|
|
9053
|
+
process.stderr.write(`halfcycle draw: usage: ${DRAW_USAGE}
|
|
9054
|
+
`);
|
|
9055
|
+
process.exitCode = 2;
|
|
9056
|
+
return;
|
|
9057
|
+
}
|
|
9058
|
+
const repoRoot = parsed.repoRoot ?? process.cwd();
|
|
9059
|
+
const run = await runDraw(repoRoot, parsed.picture, {
|
|
9060
|
+
readCredential: (root) => readRepoCredential(root),
|
|
9061
|
+
listTracked: listTrackedFiles,
|
|
9062
|
+
report: reportSystemDescription,
|
|
9063
|
+
reportPlan: reportPhasePlan,
|
|
9064
|
+
draw: requestDrawing,
|
|
9065
|
+
write: (root, base, set) => writeDiagramSet(root, base, set)
|
|
9066
|
+
});
|
|
9067
|
+
if (run.stdout !== "")
|
|
9068
|
+
process.stdout.write(run.stdout);
|
|
9069
|
+
if (run.stderr !== "")
|
|
9070
|
+
process.stderr.write(run.stderr);
|
|
9071
|
+
process.exitCode = run.exitCode;
|
|
9072
|
+
return;
|
|
9073
|
+
}
|
|
9074
|
+
if (cmd === "plan") {
|
|
9075
|
+
const parsed = parsePlanArgs(rest);
|
|
9076
|
+
if (!parsed.ok) {
|
|
9077
|
+
process.stderr.write(`halfcycle plan: usage: ${PLAN_USAGE}
|
|
9078
|
+
`);
|
|
9079
|
+
process.exitCode = 2;
|
|
9080
|
+
return;
|
|
9081
|
+
}
|
|
9082
|
+
const repoRoot = parsed.repoRoot ?? process.cwd();
|
|
9083
|
+
const run = await runPlan(repoRoot, parsed.phase, parsed.show, {
|
|
9084
|
+
readCredential: (root) => readRepoCredential(root),
|
|
9085
|
+
listTracked: listTrackedFiles,
|
|
9086
|
+
report: reportSystemDescription,
|
|
9087
|
+
reportPlan: reportPhasePlan,
|
|
9088
|
+
pinnedClientVersion: readPinnedClientVersion,
|
|
9089
|
+
recordShown: (engagementId, entry) => writeShownPlan(engagementId, entry),
|
|
9090
|
+
now: () => /* @__PURE__ */ new Date()
|
|
9091
|
+
});
|
|
9092
|
+
if (run.stdout !== "")
|
|
9093
|
+
process.stdout.write(run.stdout);
|
|
9094
|
+
if (run.stderr !== "")
|
|
9095
|
+
process.stderr.write(run.stderr);
|
|
9096
|
+
process.exitCode = run.exitCode;
|
|
9097
|
+
return;
|
|
9098
|
+
}
|
|
6979
9099
|
if (cmd === "open-phase") {
|
|
6980
9100
|
const repoRoot = flagValue(rest, "--repo") ?? process.cwd();
|
|
6981
9101
|
const toProjectScope = rest.includes("--none");
|
|
@@ -7020,6 +9140,11 @@ ${USAGE}`);
|
|
|
7020
9140
|
process.stdout.write(`[halfcycle] This decision CROSSED: ${opened.decision.blockingFinding}
|
|
7021
9141
|
`);
|
|
7022
9142
|
}
|
|
9143
|
+
if (opened.methodUpdated) {
|
|
9144
|
+
process.stdout.write(`[halfcycle] ${openMethodMovedLine(opened.methodUpdated)}
|
|
9145
|
+
`);
|
|
9146
|
+
updatePinnedMethodVersion(repoRoot, credential.engagementId, opened.methodUpdated.to);
|
|
9147
|
+
}
|
|
7023
9148
|
if (opened.stampFailure) {
|
|
7024
9149
|
process.stderr.write(`halfcycle open-phase: the phase moved, but this machine's copy of it could not be written: ${opened.stampFailure.reason}
|
|
7025
9150
|
halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard evaluations here are filed under whatever phase that file still names. Fix the file and re-run this command \u2014 re-running it is safe.
|
|
@@ -7137,6 +9262,11 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
|
|
|
7137
9262
|
process.stdout.write(`[halfcycle] This close was an OVERRIDE: ${closeDecision.reason}
|
|
7138
9263
|
`);
|
|
7139
9264
|
}
|
|
9265
|
+
if (closed.methodUpdated) {
|
|
9266
|
+
process.stdout.write(`[halfcycle] ${closeMethodMovedLine(closed.methodUpdated)}
|
|
9267
|
+
`);
|
|
9268
|
+
updatePinnedMethodVersion(repoRoot, credential.engagementId, closed.methodUpdated.to);
|
|
9269
|
+
}
|
|
7140
9270
|
if (!closed.closeCommit.read) {
|
|
7141
9271
|
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.
|
|
7142
9272
|
`);
|
|
@@ -7223,6 +9353,12 @@ function readStatementFlags(argv) {
|
|
|
7223
9353
|
function plainOutstandingFinding(finding) {
|
|
7224
9354
|
return finding.replace(/\(attempted-failed\)/g, "(attempted, and did not finish)");
|
|
7225
9355
|
}
|
|
9356
|
+
function closeMethodMovedLine(moved) {
|
|
9357
|
+
return `Halfcycle's method has a newer version, ${moved.to}. Your next phase will follow it. The phase you just closed stays recorded under ${moved.from}, the version it ran under.`;
|
|
9358
|
+
}
|
|
9359
|
+
function openMethodMovedLine(moved) {
|
|
9360
|
+
return `This phase follows Halfcycle's newer method version, ${moved.to}. The project was on ${moved.from}.`;
|
|
9361
|
+
}
|
|
7226
9362
|
function writtenLines(written) {
|
|
7227
9363
|
return `[halfcycle] Build Record written:
|
|
7228
9364
|
${written.mdPath}
|