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/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 readFileSync11, realpathSync as realpathSync3, statSync, writeSync as writeSync2 } from "node:fs";
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 join12 } from "node:path";
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(/^\d+\.\d+\.\d+$/);
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: `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`
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/device-auth.js
1246
+ // ../events/dist/build-snapshot.js
1094
1247
  import { z as z7 } from "zod";
1095
- var utcIso86013 = z7.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
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 = z7.object({
1137
- deviceCode: z7.string().min(1),
1138
- userCode: z7.string().min(1),
1139
- verificationUrl: z7.string().min(1),
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: z7.number().int().positive()
1985
+ pollIntervalMs: z10.number().int().positive()
1142
1986
  }).strict();
1143
- var devicePollPendingSchema = z7.object({
1144
- status: z7.literal(DEVICE_AUTH_STATUS.PENDING),
1145
- pollIntervalMs: z7.number().int().positive()
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 = z7.object({
1148
- status: z7.literal(DEVICE_AUTH_STATUS.APPROVED),
1149
- accountId: z7.string().min(1),
1150
- credential: z7.string().min(1),
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 = z7.object({
1154
- status: z7.enum([
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: z7.string().min(1)
2009
+ message: z10.string().min(1)
1166
2010
  }).strict();
1167
- var devicePollResponseSchema = z7.union([
2011
+ var devicePollResponseSchema = z10.union([
1168
2012
  devicePollPendingSchema,
1169
2013
  devicePollApprovedSchema,
1170
2014
  deviceAuthRefusalSchema
1171
2015
  ]);
1172
- var deviceConfirmRequestSchema = z7.object({
1173
- userCode: z7.string().min(1),
1174
- decision: z7.enum(["approve", "decline"]),
1175
- externalAuthId: z7.string().min(1).optional(),
1176
- email: z7.string().optional(),
1177
- acceptTerms: z7.boolean().optional()
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 = z7.object({
1180
- status: z7.literal(DEVICE_AUTH_STATUS.RECORDED),
1181
- accountId: z7.string().min(1).optional()
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 = z7.union([
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 z8 } from "zod";
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 = z8.object({
1216
- statusCode: z8.literal(JOIN_REFUSAL_STATUS),
1217
- error: z8.string(),
1218
- message: z8.string(),
1219
- reason: z8.literal(NOT_THIS_ACCOUNT_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 = z8.union([notThisAccountRefusalSchema, wireErrorSchema]);
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 z9 } from "zod";
1228
- var accountIdentitySchema = z9.object({
1229
- accountId: z9.string().min(1),
1230
- email: z9.string().min(1).optional()
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 z10 } from "zod";
1238
- var engagementCredentialResponseSchema = z10.object({
1239
- engagementId: z10.string(),
1240
- sessionToken: z10.string(),
1241
- mcpUrl: z10.string(),
1242
- guardUrl: z10.string(),
1243
- controlTelemetryUrl: z10.string(),
1244
- methodVersion: z10.string().optional(),
1245
- consoleUrl: z10.string().optional()
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 = z10.object({
1248
- token: z10.string(),
2091
+ var ciTokenExchangeResponseSchema = z13.object({
2092
+ token: z13.string(),
1249
2093
  expiresAt: utcIso8601,
1250
- engagementId: z10.string()
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 z11 } from "zod";
1268
- var projectIdSchema = z11.string().uuid();
1269
- var projectEngagementRequestSchema = z11.object({
1270
- derivedName: z11.string()
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 z12 } from "zod";
1275
- var phaseRecordRefusalReasonSchema = z12.enum([
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 = z12.object({
1283
- outcome: z12.literal("record"),
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 = z12.object({
1291
- outcome: z12.literal("withheld"),
2134
+ var phaseRecordWithheldSchema = z15.object({
2135
+ outcome: z15.literal("withheld"),
1292
2136
  phase: phaseNameSchema,
1293
- sequence: z12.number().int().min(1),
2137
+ sequence: z15.number().int().min(1),
1294
2138
  closedAt: utcIso8601,
1295
- message: z12.string()
2139
+ message: z15.string()
1296
2140
  }).strict();
1297
2141
  var phaseRecordRefusedSchema = wireErrorSchema.extend({
1298
- outcome: z12.literal("refused"),
2142
+ outcome: z15.literal("refused"),
1299
2143
  phase: phaseNameSchema,
1300
- statusCode: z12.literal(409),
1301
- error: z12.literal("PhaseRecordRefused"),
2144
+ statusCode: z15.literal(409),
2145
+ error: z15.literal("PhaseRecordRefused"),
1302
2146
  reason: phaseRecordRefusalReasonSchema
1303
2147
  }).strict();
1304
- var phaseRecordResponseSchema = z12.discriminatedUnion("outcome", [
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 shown = JSON.stringify(engagementId.length > 80 ? `${engagementId.slice(0, 80)}\u2026` : engagementId);
1379
- super(`${shown} 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.`);
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 canonicalJson(value) {
2509
+ function canonicalJson2(value) {
1666
2510
  if (Array.isArray(value))
1667
- return value.map(canonicalJson);
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, canonicalJson(v)]);
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(canonicalJson(value)));
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(canonicalJson(ordered), null, 2) + "\n";
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 isRecord(value) {
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 (!isRecord(value))
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 (!isRecord(raw))
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 (!isRecord(entry) || typeof entry["path"] !== "string") {
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 (!isRecord(s))
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 (!isRecord(m))
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 = isPlainObject(existing.hooks) ? existing.hooks : void 0;
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 = isPlainObject(before) ? before["mcpServers"] : void 0;
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 && isPlainObject(servers) && MCP_SERVER_KEY in servers,
2221
- mcpServersCreated: !(isPlainObject(before) && "mcpServers" in before),
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 = isPlainObject(before) ? before["mcpServers"] : void 0;
2235
- if (!isPlainObject(servers) || !(MCP_SERVER_KEY in servers))
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 isPlainObject(value) {
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((resolve4) => {
4242
+ const bindProblem = await new Promise((resolve5) => {
3328
4243
  const onError = (err) => {
3329
- resolve4(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
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
- resolve4(null);
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((resolve4) => {
4265
+ waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve5) => {
3351
4266
  if (pending !== null) {
3352
- resolve4(pending);
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
- resolve4(value);
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((resolve4) => {
4292
+ return new Promise((resolve5) => {
3378
4293
  server.closeAllConnections();
3379
- server.close(() => resolve4());
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((resolve4) => setTimeout(resolve4, ms));
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((resolve4) => {
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
- resolve4({ opened: false, reason: `${command} could not be run` });
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
- resolve4(attempt);
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((resolve4) => {
3609
- const timer = setTimeout(resolve4, started.pollIntervalMs);
4523
+ await new Promise((resolve5) => {
4524
+ const timer = setTimeout(resolve5, started.pollIntervalMs);
3610
4525
  interrupt = () => {
3611
4526
  clearTimeout(timer);
3612
- resolve4();
4527
+ resolve5();
3613
4528
  };
3614
4529
  });
3615
4530
  } else {
3616
- await new Promise((resolve4) => {
3617
- interrupt = resolve4;
3618
- void sleep(started.pollIntervalMs).then(resolve4);
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((resolve4) => {
4241
- rl.once("close", () => resolve4(""));
4242
- rl.question(question2).then(resolve4, () => resolve4(""));
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, platform4 = process.platform) {
5424
+ function refuseSystemFolder(targetRepo, platform5 = process.platform) {
4510
5425
  const key = (folder) => {
4511
5426
  const real = realOrLiteral(folder);
4512
- return platform4 === "win32" ? real.toLowerCase() : real;
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 platform4 = deps.platform ?? process.platform;
5560
+ const platform5 = deps.platform ?? process.platform;
4646
5561
  const exec = deps.exec ?? execFileSync3;
4647
5562
  let raw;
4648
5563
  try {
4649
- raw = platform4 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", '"claude --version"'], {
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 platform4 = deps.platform ?? process.platform;
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 = platform4 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
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 project was run under; it does not change during a project.`,
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, text) {
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
- writeSync(fd, text);
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 { engagementId: answer.engagementId, status: answer.status, stampFailure, closeCommit };
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
+ ![${markdownAltText(target.alt)}](${embedImageLink(target.base, target.doc)})
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 lstatSync2, readFileSync as readFileSync9, readdirSync, rmSync as rmSync4, rmdirSync, writeFileSync as writeFileSync6 } from "node:fs";
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 dirname5, join as join10, relative as relative4, sep } from "node:path";
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
- lstatSync2(path);
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 = join10(current, segment);
7756
+ current = join13(current, segment);
5706
7757
  try {
5707
- if (lstatSync2(current).isSymbolicLink())
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 isPlainObject2(value) {
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 = join10(targetRepo, rel);
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 = readFileSync9(path, "utf-8");
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 (isPlainObject2(value))
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 = join10(targetRepo, INSTALL_MANIFEST_REL);
5751
- const pinPath = join10(targetRepo, BUNDLE_PIN_REL);
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(readFileSync9(manifestPath, "utf-8"));
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(readFileSync9(pinPath, "utf-8"));
5769
- const id = isPlainObject2(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
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 = join10(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
5793
- if (dirname5(engagementStateDir(engagementId, home)) !== expectedParent) {
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 = join10(targetRepo, rel);
7867
+ const path = join13(targetRepo, rel);
5817
7868
  try {
5818
7869
  if (next === null) {
5819
- rmSync4(path);
7870
+ rmSync5(path);
5820
7871
  name(lists.removed, rel, rel);
5821
7872
  } else {
5822
- writeFileSync6(path, next, "utf-8");
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) ? "" : readFileSync9(join10(targetRepo, rel), "utf-8");
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 (!isPlainObject2(hooks))
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 (!isPlainObject2(entry) || !Array.isArray(entry["hooks"]))
7921
+ if (!isPlainObject3(entry) || !Array.isArray(entry["hooks"]))
5871
7922
  continue;
5872
- const objects = entry["hooks"].filter(isPlainObject2);
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 isPlainObject2(servers) && MCP_SERVER_KEY in servers ? servers : null;
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 (isPlainObject2(hooks)) {
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 (isPlainObject2(permissions) && Array.isArray(permissions["deny"])) {
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 (!isPlainObject2(servers))
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(join10(targetRepo, path));
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(join10(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
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
- rmSync4(join10(targetRepo, INSTALL_MANIFEST_REL), { force: true });
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(join10(targetRepo, rel))) {
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 = join10(targetRepo, path);
8247
+ const abs = join13(targetRepo, path);
6197
8248
  let isFile;
6198
8249
  try {
6199
- isFile = lstatSync2(abs).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 = readFileSync9(abs);
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
- rmSync4(abs);
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 = join10(targetRepo, dir);
8281
+ const abs = join13(targetRepo, dir);
6231
8282
  try {
6232
- if (!lstatSync2(abs).isDirectory() || readdirSync(abs).length > 0)
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 = join10(targetRepo, rel);
8300
+ const path = join13(targetRepo, rel);
6250
8301
  let before;
6251
8302
  try {
6252
- before = readFileSync9(path, "utf-8");
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 (lstatSync2(dir).isSymbolicLink())
8331
+ if (lstatSync3(dir).isSymbolicLink())
6281
8332
  return { kind: "failed", dir, reason: "it is a symbolic link" };
6282
- rmSync4(engagementEnvPath(engagementId, home), { force: true });
6283
- rmSync4(notThisAccountMarkerPath(engagementId, home), { force: true });
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 (!lstatSync2(join10(root, dirRel)).isDirectory())
8347
+ if (!lstatSync3(join13(root, dirRel)).isDirectory())
6297
8348
  return;
6298
- names = readdirSync(join10(root, dirRel));
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 (lstatSync2(join10(root, childRel)).isDirectory())
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 = relative4(base, path);
6320
- const under = rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep);
6321
- return `${under ? `~/${rel.split(sep).join("/")}` : path}/`;
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 readFileSync10 } from "node:fs";
6378
- import { basename as basename3, join as join11, resolve as resolve3 } from "node:path";
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(readFileSync10(join11(BUNDLE_ROOT, "package.json"), "utf-8"));
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(resolve3(dir)).trim();
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(readFileSync11(join12(dir, "package.json"), "utf-8"));
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 = realpathSync3(dir);
6552
- const home = realpathSync3(homedir3());
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}