halfcycle 0.4.9 → 0.4.10

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
@@ -420,18 +420,6 @@ function hasDuplicates(items) {
420
420
  }
421
421
 
422
422
  // ../core/dist/phase-reference.js
423
- var TYPED_NUMBER = /^(?:0|[1-9][0-9]*)$/;
424
- function resolvePhaseReference(typed, phases) {
425
- if (typeof typed !== "string" || typed === "")
426
- return null;
427
- const byIdentity = phases.filter((entry) => entry.phase === typed);
428
- if (byIdentity.length > 0)
429
- return byIdentity.length === 1 ? byIdentity[0] : null;
430
- if (!TYPED_NUMBER.test(typed))
431
- return null;
432
- const byNumber = phases.filter((entry) => Number.isInteger(entry.number) && String(entry.number) === typed);
433
- return byNumber.length === 1 ? byNumber[0] : null;
434
- }
435
423
  function typedPhaseReference(phase, number) {
436
424
  return phase.includes(":") && number !== null ? String(number) : phase;
437
425
  }
@@ -1162,10 +1150,10 @@ var openPhaseResponseSchema = z6.object({
1162
1150
 
1163
1151
  // ../events/dist/crew.js
1164
1152
  import { z as z7 } from "zod";
1165
- var crewGroupSchema = z7.enum(["builder", "reviewer", "always-on"]);
1153
+ var crewGroupSchema = z7.enum(["analyst-architect", "builder", "reviewer", "lead"]);
1166
1154
  var crewTierSchema = z7.enum(["deep", "standard", "fast"]);
1167
1155
  var CREW_DISCIPLINE_MAX = 64;
1168
- var crewFaceHeadSchema = z7.enum(["sq", "sqr", "rnd", "dome", "hex"]);
1156
+ var crewFaceHeadSchema = z7.enum(["sq", "sqr", "rnd", "dome", "hex", "gable"]);
1169
1157
  var crewFaceAntennaSchema = z7.enum(["none", "stalk", "twin", "dish"]);
1170
1158
  var crewFaceEyesSchema = z7.enum(["dots", "bars", "visor", "cyclops"]);
1171
1159
  var crewFaceMouthSchema = z7.enum(["line", "grid", "dots", "wave"]);
@@ -1182,12 +1170,25 @@ var crewFaceSchema = z7.object({
1182
1170
  // Whether this member is drawn with ears.
1183
1171
  ears: z7.boolean()
1184
1172
  }).strict();
1173
+ var crewRoleSchema = z7.enum([
1174
+ "business-analyst",
1175
+ "architect",
1176
+ "spec-writer",
1177
+ "builder",
1178
+ "context-docs",
1179
+ "spec-reviewer",
1180
+ "code-test-reviewer",
1181
+ "delivery-lead",
1182
+ "quality-lead"
1183
+ ]);
1185
1184
  var crewMemberSchema = z7.object({
1186
- // The agent's name. The same twenty-one on every project, so a name is a
1185
+ // The agent's name. The same names on every project, so a name is a
1187
1186
  // vocabulary a returning client already knows.
1188
1187
  callsign: z7.string().min(1).max(32),
1189
- // Which of the three headings this member reads under.
1188
+ // Which of the four teams this member belongs to.
1190
1189
  group: crewGroupSchema,
1190
+ // The part this member plays, which decides the stages it leads and helps with.
1191
+ role: crewRoleSchema,
1191
1192
  // The kind of work this member takes. A task names a discipline, and only
1192
1193
  // agents holding it can be dispatched to it.
1193
1194
  discipline: z7.string().min(1).max(CREW_DISCIPLINE_MAX),
@@ -1199,10 +1200,44 @@ var crewMemberSchema = z7.object({
1199
1200
  face: crewFaceSchema
1200
1201
  }).strict();
1201
1202
  var CREW_ROSTER = [
1203
+ // ── analysts and architects ─────────────────────────────────────────────────
1204
+ {
1205
+ callsign: "SOFIA",
1206
+ group: "analyst-architect",
1207
+ role: "business-analyst",
1208
+ discipline: "business analyst",
1209
+ tier: "deep",
1210
+ face: { head: "gable", antenna: "stalk", eyes: "dots", mouth: "wave", ears: false }
1211
+ },
1212
+ {
1213
+ callsign: "BEA",
1214
+ group: "analyst-architect",
1215
+ role: "business-analyst",
1216
+ discipline: "business analyst",
1217
+ tier: "standard",
1218
+ face: { head: "gable", antenna: "none", eyes: "visor", mouth: "line", ears: false }
1219
+ },
1220
+ {
1221
+ callsign: "WREN",
1222
+ group: "analyst-architect",
1223
+ role: "architect",
1224
+ discipline: "solution architect",
1225
+ tier: "deep",
1226
+ face: { head: "gable", antenna: "dish", eyes: "bars", mouth: "line", ears: true }
1227
+ },
1228
+ {
1229
+ callsign: "PEI",
1230
+ group: "analyst-architect",
1231
+ role: "architect",
1232
+ discipline: "technical architect",
1233
+ tier: "standard",
1234
+ face: { head: "gable", antenna: "twin", eyes: "cyclops", mouth: "dots", ears: false }
1235
+ },
1202
1236
  // ── builders: code ──────────────────────────────────────────────────────────
1203
1237
  {
1204
1238
  callsign: "ADA",
1205
1239
  group: "builder",
1240
+ role: "builder",
1206
1241
  discipline: "implementation",
1207
1242
  tier: "deep",
1208
1243
  face: { head: "sq", antenna: "stalk", eyes: "dots", mouth: "line", ears: false }
@@ -1210,6 +1245,7 @@ var CREW_ROSTER = [
1210
1245
  {
1211
1246
  callsign: "OTTO",
1212
1247
  group: "builder",
1248
+ role: "builder",
1213
1249
  discipline: "implementation",
1214
1250
  tier: "deep",
1215
1251
  face: { head: "sq", antenna: "twin", eyes: "bars", mouth: "grid", ears: true }
@@ -1217,6 +1253,7 @@ var CREW_ROSTER = [
1217
1253
  {
1218
1254
  callsign: "MILO",
1219
1255
  group: "builder",
1256
+ role: "builder",
1220
1257
  discipline: "implementation",
1221
1258
  tier: "standard",
1222
1259
  face: { head: "sqr", antenna: "stalk", eyes: "dots", mouth: "dots", ears: false }
@@ -1224,42 +1261,24 @@ var CREW_ROSTER = [
1224
1261
  {
1225
1262
  callsign: "NOVA",
1226
1263
  group: "builder",
1264
+ role: "builder",
1227
1265
  discipline: "implementation",
1228
1266
  tier: "standard",
1229
1267
  face: { head: "sq", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
1230
1268
  },
1231
- {
1232
- callsign: "KAI",
1233
- group: "builder",
1234
- discipline: "implementation",
1235
- tier: "standard",
1236
- face: { head: "sqr", antenna: "none", eyes: "bars", mouth: "line", ears: true }
1237
- },
1238
1269
  {
1239
1270
  callsign: "HUGO",
1240
1271
  group: "builder",
1272
+ role: "builder",
1241
1273
  discipline: "infrastructure",
1242
1274
  tier: "standard",
1243
1275
  face: { head: "sq", antenna: "twin", eyes: "dots", mouth: "grid", ears: true }
1244
1276
  },
1245
- {
1246
- callsign: "WREN",
1247
- group: "builder",
1248
- discipline: "refactor",
1249
- tier: "fast",
1250
- face: { head: "sqr", antenna: "stalk", eyes: "bars", mouth: "wave", ears: false }
1251
- },
1252
- {
1253
- callsign: "PIP",
1254
- group: "builder",
1255
- discipline: "refactor",
1256
- tier: "fast",
1257
- face: { head: "sqr", antenna: "none", eyes: "dots", mouth: "dots", ears: false }
1258
- },
1259
1277
  // ── builders: specs and docs ────────────────────────────────────────────────
1260
1278
  {
1261
1279
  callsign: "IRIS",
1262
1280
  group: "builder",
1281
+ role: "spec-writer",
1263
1282
  discipline: "feature spec",
1264
1283
  tier: "deep",
1265
1284
  face: { head: "dome", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
@@ -1267,6 +1286,7 @@ var CREW_ROSTER = [
1267
1286
  {
1268
1287
  callsign: "JUNO",
1269
1288
  group: "builder",
1289
+ role: "spec-writer",
1270
1290
  discipline: "feature spec",
1271
1291
  tier: "standard",
1272
1292
  face: { head: "dome", antenna: "none", eyes: "dots", mouth: "wave", ears: false }
@@ -1274,6 +1294,7 @@ var CREW_ROSTER = [
1274
1294
  {
1275
1295
  callsign: "LEX",
1276
1296
  group: "builder",
1297
+ role: "context-docs",
1277
1298
  discipline: "context & docs",
1278
1299
  tier: "fast",
1279
1300
  face: { head: "dome", antenna: "twin", eyes: "bars", mouth: "dots", ears: false }
@@ -1282,6 +1303,7 @@ var CREW_ROSTER = [
1282
1303
  {
1283
1304
  callsign: "VERA",
1284
1305
  group: "reviewer",
1306
+ role: "spec-reviewer",
1285
1307
  discipline: "spec consistency",
1286
1308
  tier: "deep",
1287
1309
  face: { head: "rnd", antenna: "stalk", eyes: "visor", mouth: "line", ears: false }
@@ -1289,6 +1311,7 @@ var CREW_ROSTER = [
1289
1311
  {
1290
1312
  callsign: "ODIN",
1291
1313
  group: "reviewer",
1314
+ role: "spec-reviewer",
1292
1315
  discipline: "cross-spec audit",
1293
1316
  tier: "deep",
1294
1317
  face: { head: "rnd", antenna: "dish", eyes: "cyclops", mouth: "line", ears: false }
@@ -1296,6 +1319,7 @@ var CREW_ROSTER = [
1296
1319
  {
1297
1320
  callsign: "CASS",
1298
1321
  group: "reviewer",
1322
+ role: "code-test-reviewer",
1299
1323
  discipline: "code review",
1300
1324
  tier: "deep",
1301
1325
  face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "grid", ears: true }
@@ -1303,13 +1327,23 @@ var CREW_ROSTER = [
1303
1327
  {
1304
1328
  callsign: "THEO",
1305
1329
  group: "reviewer",
1330
+ role: "code-test-reviewer",
1306
1331
  discipline: "code review",
1307
1332
  tier: "standard",
1308
1333
  face: { head: "rnd", antenna: "twin", eyes: "bars", mouth: "line", ears: false }
1309
1334
  },
1335
+ {
1336
+ callsign: "KAI",
1337
+ group: "reviewer",
1338
+ role: "code-test-reviewer",
1339
+ discipline: "code review",
1340
+ tier: "standard",
1341
+ face: { head: "rnd", antenna: "none", eyes: "bars", mouth: "line", ears: true }
1342
+ },
1310
1343
  {
1311
1344
  callsign: "ECHO",
1312
1345
  group: "reviewer",
1346
+ role: "code-test-reviewer",
1313
1347
  discipline: "test quality",
1314
1348
  tier: "standard",
1315
1349
  face: { head: "rnd", antenna: "stalk", eyes: "dots", mouth: "wave", ears: false }
@@ -1317,6 +1351,7 @@ var CREW_ROSTER = [
1317
1351
  {
1318
1352
  callsign: "MAVIS",
1319
1353
  group: "reviewer",
1354
+ role: "code-test-reviewer",
1320
1355
  discipline: "suites & coverage",
1321
1356
  tier: "standard",
1322
1357
  face: { head: "rnd", antenna: "none", eyes: "visor", mouth: "dots", ears: false }
@@ -1324,6 +1359,7 @@ var CREW_ROSTER = [
1324
1359
  {
1325
1360
  callsign: "ZARA",
1326
1361
  group: "reviewer",
1362
+ role: "code-test-reviewer",
1327
1363
  discipline: "smoke & walk aid",
1328
1364
  tier: "standard",
1329
1365
  face: { head: "rnd", antenna: "dish", eyes: "bars", mouth: "wave", ears: false }
@@ -1331,39 +1367,31 @@ var CREW_ROSTER = [
1331
1367
  {
1332
1368
  callsign: "NORA",
1333
1369
  group: "reviewer",
1370
+ role: "code-test-reviewer",
1334
1371
  discipline: "evidence & provenance",
1335
1372
  tier: "fast",
1336
1373
  face: { head: "rnd", antenna: "twin", eyes: "dots", mouth: "line", ears: false }
1337
1374
  },
1338
- // ── always on: continuous, takes no task ────────────────────────────────────
1375
+ // ── leads ───────────────────────────────────────────────────────────────────
1339
1376
  {
1340
1377
  callsign: "ARGUS",
1341
- group: "always-on",
1342
- discipline: "guard \xB7 every diff",
1378
+ group: "lead",
1379
+ role: "quality-lead",
1380
+ discipline: "quality lead \xB7 every diff",
1343
1381
  tier: "standard",
1344
1382
  face: { head: "hex", antenna: "dish", eyes: "visor", mouth: "grid", ears: true }
1345
1383
  },
1346
1384
  {
1347
1385
  callsign: "ATLAS",
1348
- group: "always-on",
1349
- discipline: "coordinator",
1386
+ group: "lead",
1387
+ role: "delivery-lead",
1388
+ discipline: "delivery lead",
1350
1389
  tier: "deep",
1351
1390
  face: { head: "hex", antenna: "twin", eyes: "cyclops", mouth: "line", ears: true }
1352
1391
  }
1353
1392
  ];
1354
1393
  var CREW_ROSTER_SIZE = CREW_ROSTER.length;
1355
1394
  var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
1356
- var crewRoleSchema = z7.enum([
1357
- "business-analyst",
1358
- "architect",
1359
- "spec-writer",
1360
- "builder",
1361
- "context-docs",
1362
- "spec-reviewer",
1363
- "code-test-reviewer",
1364
- "delivery-lead",
1365
- "quality-lead"
1366
- ]);
1367
1395
  var crewLayerSchema = methodLayerSchema.extract(["l0", "l1", "l2", "l3", "l4", "l5"]);
1368
1396
  var crewRoleLayersSchema = z7.object({
1369
1397
  leads: z7.array(crewLayerSchema),
@@ -1722,7 +1750,8 @@ var systemDrawDrawnSchema = /* @__PURE__ */ z8.object({
1722
1750
  mermaid: z8.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_MERMAID_MAX_BYTES, {
1723
1751
  message: "the diagram text is larger than 256 KB"
1724
1752
  }),
1725
- pictures: systemPicturesSchema
1753
+ pictures: systemPicturesSchema,
1754
+ closedAt: utcIso8601.optional()
1726
1755
  }).strict();
1727
1756
  var systemDrawRefusedSchema = z8.object({
1728
1757
  outcome: z8.literal("refused"),
@@ -1967,14 +1996,13 @@ var SCOPE_STATE_NAMES = [
1967
1996
  "waiting",
1968
1997
  "sent-back",
1969
1998
  "approved",
1970
- "partly-stale",
1971
- "withdrawn"
1999
+ "partly-stale"
1972
2000
  ];
1973
2001
  var scopeStateNameSchema = z10.enum(SCOPE_STATE_NAMES);
1974
2002
  var plannedScopeStateSchema = scopeStateNameSchema.exclude(["not-planned"]);
1975
2003
  var SCOPE_PART_STATUSES = ["approved", "waiting"];
1976
2004
  var scopePartStatusSchema = z10.enum(SCOPE_PART_STATUSES);
1977
- var SCOPE_ACTS = ["approve", "send-back", "withdraw"];
2005
+ var SCOPE_ACTS = ["approve", "send-back"];
1978
2006
  var scopeActSchema = z10.enum(SCOPE_ACTS);
1979
2007
  var SCOPE_GIVEN_IN = ["console", "chat"];
1980
2008
  var scopeGivenInSchema = z10.enum(SCOPE_GIVEN_IN);
@@ -2045,14 +2073,9 @@ var scopeSendBackRequestSchema = z10.object({
2045
2073
  parts: z10.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
2046
2074
  note: scopeNoteWireSchema.optional()
2047
2075
  }).strict();
2048
- var scopeWithdrawRequestSchema = z10.object({
2049
- act: z10.literal("withdraw"),
2050
- planVersion: planVersionSchema
2051
- }).strict();
2052
2076
  var scopeDecisionRequestSchema = z10.discriminatedUnion("act", [
2053
2077
  scopeApproveRequestSchema,
2054
- scopeSendBackRequestSchema,
2055
- scopeWithdrawRequestSchema
2078
+ scopeSendBackRequestSchema
2056
2079
  ]);
2057
2080
  var scopeDecisionRecordedSchema = z10.object({
2058
2081
  outcome: z10.literal("recorded"),
@@ -2085,7 +2108,6 @@ var scopeDecisionResultSchema = z10.discriminatedUnion("outcome", [
2085
2108
  reasonOutcome("not-owner"),
2086
2109
  reasonOutcome("no-owner"),
2087
2110
  reasonOutcome("plan-changed"),
2088
- reasonOutcome("nothing-to-withdraw"),
2089
2111
  reasonOutcome("build-started")
2090
2112
  ]);
2091
2113
  var scopeReadPartSchema = z10.object({
@@ -2387,6 +2409,15 @@ var observedChangesSchema = z13.object({
2387
2409
  ctx.addIssue({ code: "custom", path: ["descriptionVersion"], message: "must name the version the counts used" });
2388
2410
  }
2389
2411
  });
2412
+ function partChangedUnderPaths(observed, partId, description) {
2413
+ if (observed === null || observed.counting.state !== "counted")
2414
+ return "unknown";
2415
+ const part = description.parts.find((candidate) => candidate.id === partId);
2416
+ if (part === void 0 || part.outside)
2417
+ return "unknown";
2418
+ const entry = observed.parts.find((candidate) => candidate.part === partId);
2419
+ return entry !== void 0 && entry.files !== null && filesChanged(entry.files) >= 1 ? "changed" : "unchanged";
2420
+ }
2390
2421
  var partOutsideThePlanSchema = z13.object({
2391
2422
  part: systemPartIdSchema,
2392
2423
  name: systemPartSchema.shape.name,
@@ -2404,14 +2435,78 @@ function agentSaid(task) {
2404
2435
  return `The agent on "${title}" said: "${whyOutsidePlan}"`;
2405
2436
  return `${agent} said, on "${title}": "${whyOutsidePlan}"`;
2406
2437
  }
2438
+ var NOTHING_COUNTED_DESPITE_FINISHED_LINE = "Halfcycle only counts committed changes. Work not yet committed, saved in a folder this project ignores, or committed outside this part's own folders, shows as nothing here even when every task that builds this part has finished.";
2439
+ function hasWildcard(segment) {
2440
+ return segment.includes("*");
2441
+ }
2442
+ function leadingLiteralPrefix(segments) {
2443
+ const firstWildcard = segments.findIndex(hasWildcard);
2444
+ return segments.slice(0, firstWildcard === -1 ? segments.length : firstWildcard).join("/");
2445
+ }
2446
+ function isWholeWildcard(segment) {
2447
+ return segment === "*" || segment === "**";
2448
+ }
2449
+ function ownsEverythingUnderPrefix(segments) {
2450
+ const firstWildcard = segments.findIndex(hasWildcard);
2451
+ if (firstWildcard === -1)
2452
+ return false;
2453
+ const tail = segments.slice(firstWildcard);
2454
+ if (!tail.every(isWholeWildcard))
2455
+ return false;
2456
+ return tail.filter((segment) => segment === "*").length <= 1;
2457
+ }
2458
+ var EXTENSION_GLOB_SEGMENT = /^\*\.([^*]+)$/;
2459
+ var EXTENSIONLESS_FILE_NAMES = /* @__PURE__ */ new Set([
2460
+ "Makefile",
2461
+ "Dockerfile",
2462
+ "Gemfile",
2463
+ "Rakefile",
2464
+ "Procfile",
2465
+ "Vagrantfile",
2466
+ "LICENSE",
2467
+ "CHANGELOG",
2468
+ "README",
2469
+ "NOTICE",
2470
+ "Jenkinsfile",
2471
+ "Justfile",
2472
+ "CODEOWNERS"
2473
+ ]);
2474
+ var DOT_PREFIXED_FOLDER_NAMES = /* @__PURE__ */ new Set([".github", ".vscode", ".circleci", ".husky", ".changeset", ".devcontainer", ".idea", ".claude", ".config"]);
2475
+ function isBareFileName(name) {
2476
+ if (EXTENSIONLESS_FILE_NAMES.has(name))
2477
+ return true;
2478
+ if (DOT_PREFIXED_FOLDER_NAMES.has(name))
2479
+ return false;
2480
+ return name.includes(".");
2481
+ }
2407
2482
  function whereOf(owns) {
2408
2483
  if (owns.length !== 1)
2409
- return "its paths";
2484
+ return "under its paths";
2410
2485
  const pattern = owns[0];
2411
- if (pattern.endsWith("/**"))
2412
- return pattern.slice(0, -2);
2413
- const last = pattern.slice(pattern.lastIndexOf("/") + 1);
2414
- return last.includes(".") ? pattern : `${pattern}/`;
2486
+ const segments = pattern.split("/");
2487
+ const lastSegment = segments[segments.length - 1];
2488
+ const extensionGlob = EXTENSION_GLOB_SEGMENT.exec(lastSegment);
2489
+ if (extensionGlob !== null) {
2490
+ const ext = extensionGlob[1];
2491
+ const before = segments.slice(0, -1);
2492
+ if (before.length === 0 || before.some(hasWildcard)) {
2493
+ const prefix2 = leadingLiteralPrefix(before);
2494
+ return prefix2 === "" ? `in its .${ext} files` : `in .${ext} files under ${prefix2}`;
2495
+ }
2496
+ return `in the .${ext} files directly in ${before.join("/")}`;
2497
+ }
2498
+ if (!hasWildcard(pattern)) {
2499
+ return isBareFileName(lastSegment) ? `in ${pattern}` : `under ${pattern}/`;
2500
+ }
2501
+ if (lastSegment === "*") {
2502
+ const folder = segments.slice(0, -1).join("/");
2503
+ if (folder !== "" && !hasWildcard(folder))
2504
+ return `in the ${folder} folder`;
2505
+ }
2506
+ const prefix = leadingLiteralPrefix(segments);
2507
+ if (ownsEverythingUnderPrefix(segments))
2508
+ return prefix === "" ? "anywhere in the project" : `under ${prefix}/`;
2509
+ return prefix === "" ? "in some paths in the project" : `in some paths under ${prefix}/`;
2415
2510
  }
2416
2511
  function howMany(n, one, many) {
2417
2512
  return `${n} ${n === 1 ? one : many}`;
@@ -2440,7 +2535,7 @@ function observedLines(observed, part, description) {
2440
2535
  const t = files === null ? 0 : files.testFilesAdded;
2441
2536
  const where = whereOf(held.owns);
2442
2537
  return [
2443
- n === 0 ? `Nothing changed under ${where}` : `${howMany(n, "file", "files")} changed under ${where}`,
2538
+ n === 0 ? `Nothing changed ${where}` : `${howMany(n, "file", "files")} changed ${where}`,
2444
2539
  t === 0 ? "No test files added" : `${howMany(t, "test file", "test files")} added`,
2445
2540
  checks
2446
2541
  ];
@@ -3021,6 +3116,21 @@ function loopbackVerificationUrl(verificationUrl, target) {
3021
3116
  return url.toString();
3022
3117
  }
3023
3118
 
3119
+ // ../events/dist/project-page.js
3120
+ function projectPageUrl(engagementId) {
3121
+ return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
3122
+ }
3123
+ function systemViewUrl(engagementId, phase) {
3124
+ const page = projectPageUrl(engagementId);
3125
+ if (page === void 0)
3126
+ return void 0;
3127
+ return `${page}/console/system-view?${new URLSearchParams({ phase }).toString()}`;
3128
+ }
3129
+ function requestAccessSentence(engagementId) {
3130
+ const page = projectPageUrl(engagementId) ?? "this project's page on Halfcycle";
3131
+ return `To ask for access, open ${page} signed in as this account and choose "Request access". Then let the project's owner know, so they can approve it.`;
3132
+ }
3133
+
3024
3134
  // ../events/dist/join-refusal.js
3025
3135
  import { z as z17 } from "zod";
3026
3136
  var NOT_THIS_ACCOUNT_REASON = "not-this-account";
@@ -3036,15 +3146,6 @@ function isNotThisAccountRefusal(body) {
3036
3146
  return typeof body === "object" && body !== null && body.reason === NOT_THIS_ACCOUNT_REASON;
3037
3147
  }
3038
3148
 
3039
- // ../events/dist/project-page.js
3040
- function projectPageUrl(engagementId) {
3041
- return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
3042
- }
3043
- function requestAccessSentence(engagementId) {
3044
- const page = projectPageUrl(engagementId) ?? "this project's page on Halfcycle";
3045
- return `To ask for access, open ${page} signed in as this account and choose "Request access". Then let the project's owner know, so they can approve it.`;
3046
- }
3047
-
3048
3149
  // ../events/dist/account-identity.js
3049
3150
  import { z as z18 } from "zod";
3050
3151
  var accountIdentitySchema = z18.object({
@@ -7110,7 +7211,7 @@ function decisionText(part, p) {
7110
7211
  case "accepted":
7111
7212
  return "Accepted";
7112
7213
  case "carried":
7113
- return part.carriedTo === null || part.carriedTo === void 0 ? `Carried to the next phase planned: ${p.text(part.reason ?? "")}` : `Carried to phase ${String(part.carriedTo.number)}: ${p.text(part.reason ?? "")}`;
7214
+ return part.carriedTo === null || part.carriedTo === void 0 ? `Carried to the next phase planned: ${p.text(part.reason ?? "")}` : `Carried to phase "${part.carriedTo.phase}": ${p.text(part.reason ?? "")}`;
7114
7215
  case "dropped":
7115
7216
  return `Dropped: ${p.text(part.reason ?? "")}`;
7116
7217
  }
@@ -7516,6 +7617,12 @@ var COULD_NOT_COUNT_LINE = "[halfcycle] Halfcycle could not count what this phas
7516
7617
  function typedReviewHintLine(offered) {
7517
7618
  return offered ? '[halfcycle] The owner reviews each part in their own Claude Code session: they say "review what was built" to see the list, then type one decision per line.' : `[halfcycle] ${TYPED_REVIEW_OFF_SENTENCE}`;
7518
7619
  }
7620
+ function systemViewReviewLine(offered, engagementId, phase) {
7621
+ const address = systemViewUrl(engagementId, phase);
7622
+ if (address === void 0)
7623
+ return void 0;
7624
+ return offered ? `[halfcycle] Or the owner reviews it in the System view: ${address}` : `[halfcycle] The owner reviews it in the System view: ${address}`;
7625
+ }
7519
7626
  function tasksOpenLine(phase, open, more) {
7520
7627
  const total = open.length + more;
7521
7628
  const shown2 = open.slice(0, TASK_TITLES_SHOWN);
@@ -7540,7 +7647,7 @@ function unfinishedStepsLines(phase, steps) {
7540
7647
  function cleanOverFindingsLine(phase, findings) {
7541
7648
  return findings === 1 ? `${REFUSED} the walk's verdict is clean, but 1 defect is recorded against phase "${phase}". Run this again with --verdict defects and a --finding for it. Nothing was sent to the owner.` : `${REFUSED} the walk's verdict is clean, but ${String(findings)} defects are recorded against phase "${phase}". Run this again with --verdict defects and a --finding for each. Nothing was sent to the owner.`;
7542
7649
  }
7543
- function closeStepReport(phase, attempt) {
7650
+ function closeStepReport(phase, attempt, engagementId) {
7544
7651
  const stdout = [];
7545
7652
  const stderr = [];
7546
7653
  let exitCode = 1;
@@ -7568,6 +7675,11 @@ function closeStepReport(phase, attempt) {
7568
7675
  if (!answer.counted)
7569
7676
  stdout.push(COULD_NOT_COUNT_LINE);
7570
7677
  stdout.push(typedReviewHintLine(answer.typedReviewOffered));
7678
+ {
7679
+ const viewLine = systemViewReviewLine(answer.typedReviewOffered, engagementId, phase);
7680
+ if (viewLine !== void 0)
7681
+ stdout.push(viewLine);
7682
+ }
7571
7683
  if (!attempt.closeCommit.read) {
7572
7684
  stderr.push(`${REFUSED} the commit this review starts from could not be read (${attempt.closeCommit.reason}), so the phase's Build Record will show the commit as not stated.`);
7573
7685
  }
@@ -8663,7 +8775,7 @@ function notAPhaseNameLine(verb, phase) {
8663
8775
  `;
8664
8776
  }
8665
8777
  function removalOwedLines(verb, parts) {
8666
- return parts.map((part) => `halfcycle ${verb}: docs/system.json still holds "${part.name}", which phase ${part.number}'s review ${part.act}. Take it out, run ${HALFCYCLE_COMMAND} system, then run this again.
8778
+ return parts.map((part) => `halfcycle ${verb}: docs/system.json still holds "${part.name}", which phase "${part.phase}"'s review ${part.act}. Take it out, run ${HALFCYCLE_COMMAND} system, then run this again.
8667
8779
  `).join("");
8668
8780
  }
8669
8781
  function redrawLine(repoRoot, phase) {
@@ -8688,28 +8800,27 @@ function planStateSentence(state) {
8688
8800
  return "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
8689
8801
  case "sent-back":
8690
8802
  return "The owner sent this plan back. Change it to answer the owner's notes, then run this again.";
8691
- case "withdrawn":
8692
- return "The owner withdrew their approval of this plan. It waits for the owner's approval again.";
8693
8803
  }
8694
8804
  }
8695
- function approvalLine2(phase, number, typedApprovalOffered, pinnedVersion, shown2) {
8696
- if (!typedApprovalOffered)
8697
- return TYPED_APPROVAL_OFF_SENTENCE;
8698
- if (!clientMeetsChatApprovalFloor(pinnedVersion))
8699
- return CLIENT_UPDATE_SENTENCE;
8700
- const byNumber = String(number);
8701
- const numberReachesThis = byNumber !== phase && resolvePhaseReference(byNumber, shown2)?.phase === phase;
8702
- const typed = numberReachesThis ? `\`approve ${phase}\` (or \`approve ${byNumber}\`)` : `\`approve ${phase}\``;
8703
- return `To approve it, the project's owner types ${typed} in their own Claude Code session. Only the owner can.`;
8805
+ function approvalLine2(phase, number, typedApprovalOffered, pinnedVersion, shown2, engagementId) {
8806
+ const address = systemViewUrl(engagementId, phase);
8807
+ const then = (sentence, viewSentence) => address === void 0 ? sentence : `${sentence} ${viewSentence} ${address}`;
8808
+ if (!typedApprovalOffered) {
8809
+ return then(TYPED_APPROVAL_OFF_SENTENCE, "The project's owner approves it in the System view:");
8810
+ }
8811
+ if (!clientMeetsChatApprovalFloor(pinnedVersion)) {
8812
+ return then(CLIENT_UPDATE_SENTENCE, "Or the project's owner approves it in the System view now:");
8813
+ }
8814
+ return then(`To approve it, the project's owner types \`approve ${phase}\` in their own Claude Code session. Only the owner can.`, "They can also approve it in the System view:");
8704
8815
  }
8705
8816
  function changeLines(plan, description, changed, carriedFrom = []) {
8706
8817
  const names = new Map(description.parts.map((part) => [part.id, part.name]));
8707
8818
  const marked = new Set(changed);
8708
- const carried = new Map(carriedFrom.map((entry) => [entry.part, entry.number]));
8819
+ const carried = new Map(carriedFrom.map((entry) => [entry.part, entry.phase]));
8709
8820
  return plan.changes.map((c) => {
8710
8821
  const mark = marked.has(c.part) ? " (changed since the owner last decided)" : "";
8711
- const carriedNumber = carried.get(c.part);
8712
- const carriedMark = carriedNumber === void 0 ? "" : ` (carried over from phase ${carriedNumber})`;
8822
+ const carriedPhase = carried.get(c.part);
8823
+ const carriedMark = carriedPhase === void 0 ? "" : ` (carried over from phase "${carriedPhase}")`;
8713
8824
  return ` - ${names.get(c.part) ?? c.part}${carriedMark} \xB7 ${PLAN_CHANGE_LABELS[c.change]} \xB7 ${c.summary}${mark}
8714
8825
  `;
8715
8826
  }).join("");
@@ -8756,7 +8867,7 @@ async function runPlan(repoRoot, phase, show, deps) {
8756
8867
  break;
8757
8868
  }
8758
8869
  const shownPhase = String(plan.phase);
8759
- const recorded = `[halfcycle] Plan for ${shownPhase} recorded: plan version ${outcome.version} (phase ${outcome.phaseNumber}).`;
8870
+ const recorded = `[halfcycle] Plan for "${shownPhase}" recorded: plan version ${outcome.version}.`;
8760
8871
  const stateSentence = planStateSentence(outcome.state);
8761
8872
  const redraw = redrawLine(repoRoot, phase) ?? "";
8762
8873
  if (!show)
@@ -8780,7 +8891,7 @@ ${redraw}`;
8780
8891
  exitCode: 1
8781
8892
  };
8782
8893
  }
8783
- const stdout = listed + `${approvalLine2(shownPhase, outcome.phaseNumber, outcome.typedApprovalOffered, deps.pinnedClientVersion(repoRoot), written.entries)}
8894
+ const stdout = listed + `${approvalLine2(shownPhase, outcome.phaseNumber, outcome.typedApprovalOffered, deps.pinnedClientVersion(repoRoot), written.entries, stage.credential.engagementId)}
8784
8895
  `;
8785
8896
  return { stdout, stderr: warnings, exitCode: 0 };
8786
8897
  }
@@ -8888,9 +8999,16 @@ var CANNOT_DRAW_PHASE_YET = `${P} this Halfcycle service cannot draw phase pictu
8888
8999
  function markdownAltText(text) {
8889
9000
  return text.replace(/[[\]\\]/g, (ch) => `\\${ch}`);
8890
9001
  }
9002
+ function closedAtWords(closedAt) {
9003
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.\d+)?Z$/.exec(closedAt);
9004
+ return m === null ? closedAt : `${m[1]} ${m[2]} UTC`;
9005
+ }
9006
+ function closedPhraseFor(phase, closedAt) {
9007
+ return `phase "${phase}", as it closed on ${closedAtWords(closedAt)}`;
9008
+ }
8891
9009
  var ARCHITECTURE_PASTE = { doc: "docs", alt: "Architecture", base: ARCHITECTURE_BASE };
8892
9010
  function phasePaste(phase) {
8893
- return { doc: "docs/phases", alt: `Phase ${phase}`, base: phaseDiagramBase(phase) };
9011
+ return { doc: "docs/phases", alt: `Phase "${phase}"`, base: phaseDiagramBase(phase) };
8894
9012
  }
8895
9013
  function pasteLines(mermaid, target) {
8896
9014
  const block = mermaid.endsWith("\n") ? mermaid : `${mermaid}
@@ -8945,7 +9063,7 @@ async function runDraw(repoRoot, picture, deps) {
8945
9063
  return { stdout: "", stderr: credentialRead.line, exitCode: 1 };
8946
9064
  const closed = await deps.draw(credentialRead.credential, { picture: "closed-phase", phase });
8947
9065
  if (closed.kind === "drawn" || closed.kind === "refused") {
8948
- return writeDrawOutcome(repoRoot, deps, closed, phasePaste(phase), "", CANNOT_DRAW_PHASE_YET);
9066
+ return writeDrawOutcome(repoRoot, deps, closed, phasePaste(phase), "", CANNOT_DRAW_PHASE_YET, phase);
8949
9067
  }
8950
9068
  let checkedPlan = null;
8951
9069
  const beforeSend = (description) => {
@@ -8983,14 +9101,14 @@ async function runDraw(repoRoot, picture, deps) {
8983
9101
  case "recorded":
8984
9102
  break;
8985
9103
  }
8986
- return drawAndWrite(repoRoot, deps, stage.credential, { picture: "phase", phase: plan.phase, planVersion: reported.version }, phasePaste(phase), warnings, CANNOT_DRAW_PHASE_YET);
9104
+ return drawAndWrite(repoRoot, deps, stage.credential, { picture: "phase", phase: plan.phase, planVersion: reported.version }, phasePaste(phase), warnings, CANNOT_DRAW_PHASE_YET, phase);
8987
9105
  }
8988
- async function drawAndWrite(repoRoot, deps, credential, request, target, warnings, cannotDrawYet) {
9106
+ async function drawAndWrite(repoRoot, deps, credential, request, target, warnings, cannotDrawYet, phaseName) {
8989
9107
  const outcome = await deps.draw(credential, request);
8990
9108
  const usable = outcome.kind === "not-closed" ? { kind: "unavailable", why: "Halfcycle sent an answer this command could not use. Nothing was written; try again later." } : outcome;
8991
- return writeDrawOutcome(repoRoot, deps, usable, target, warnings, cannotDrawYet);
9109
+ return writeDrawOutcome(repoRoot, deps, usable, target, warnings, cannotDrawYet, phaseName);
8992
9110
  }
8993
- async function writeDrawOutcome(repoRoot, deps, outcome, target, warnings, cannotDrawYet) {
9111
+ async function writeDrawOutcome(repoRoot, deps, outcome, target, warnings, cannotDrawYet, phaseName) {
8994
9112
  const base = target.base;
8995
9113
  if (outcome.kind === "no-route")
8996
9114
  return { stdout: "", stderr: warnings + cannotDrawYet, exitCode: 1 };
@@ -9014,20 +9132,25 @@ async function writeDrawOutcome(repoRoot, deps, outcome, target, warnings, canno
9014
9132
  exitCode: 1
9015
9133
  };
9016
9134
  }
9135
+ const closedAt = outcome.drawn.closedAt;
9136
+ const closedLead = closedAt !== void 0 && phaseName !== void 0 ? `This is ${closedPhraseFor(phaseName, closedAt)}.
9137
+
9138
+ ` : "";
9139
+ const pasteTarget = closedAt !== void 0 && phaseName !== void 0 ? { ...target, alt: closedPhraseFor(phaseName, closedAt) } : target;
9017
9140
  const rel = (which) => diagramRel(base, which);
9018
9141
  const result = deps.write(repoRoot, base, checked.set);
9019
9142
  switch (result.kind) {
9020
9143
  case "up-to-date":
9021
9144
  return {
9022
- stdout: `[halfcycle] The pictures in ${DIAGRAM_DIR_REL}/ are up to date.
9023
- ` + pasteLines(outcome.drawn.mermaid, target),
9145
+ stdout: closedLead + `[halfcycle] The pictures in ${DIAGRAM_DIR_REL}/ are up to date.
9146
+ ` + pasteLines(outcome.drawn.mermaid, pasteTarget),
9024
9147
  stderr: warnings,
9025
9148
  exitCode: 0
9026
9149
  };
9027
9150
  case "written":
9028
9151
  return {
9029
- stdout: `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(rel))}.
9030
- ` + pasteLines(outcome.drawn.mermaid, target),
9152
+ stdout: closedLead + `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(rel))}.
9153
+ ` + pasteLines(outcome.drawn.mermaid, pasteTarget),
9031
9154
  stderr: warnings,
9032
9155
  exitCode: 0
9033
9156
  };
@@ -9094,8 +9217,7 @@ function parseReviewArgs(argv) {
9094
9217
  }
9095
9218
  function rv1Line(view) {
9096
9219
  const waiting = view.parts.filter((p) => p.decidable).length;
9097
- const lead = view.number === null ? `Phase "${view.phase}"` : `Phase ${view.number}, "${view.phase}"`;
9098
- return `${lead}, review round ${view.round}. Parts waiting for your decision: ${waiting} of ${view.parts.length}.`;
9220
+ return `Phase "${view.phase}", review round ${view.round}. Parts waiting for your decision: ${waiting} of ${view.parts.length}.`;
9099
9221
  }
9100
9222
  var RV1A_LINE = "Halfcycle could not count what this phase changed, so judge each part on what the agents reported alone.";
9101
9223
  var CHANGE_WORDS = {
@@ -9115,7 +9237,7 @@ function partStatus(part, round) {
9115
9237
  case "accepted":
9116
9238
  return "Accepted";
9117
9239
  case "carried":
9118
- return part.carriedTo === void 0 || part.carriedTo === null ? "Carried to the next phase planned" : `Carried to phase ${part.carriedTo.number}`;
9240
+ return part.carriedTo === void 0 || part.carriedTo === null ? "Carried to the next phase planned" : `Carried to phase "${part.carriedTo.phase}"`;
9119
9241
  case "dropped":
9120
9242
  return "Dropped";
9121
9243
  case void 0:
@@ -9162,6 +9284,9 @@ function partLines(part, view) {
9162
9284
  }
9163
9285
  const observed = view.observed ?? nothingCounted(view.phase);
9164
9286
  lines.push(` What Halfcycle saw: ${observedLines(observed, part.part, view.description).join("; ")}`);
9287
+ if (!part.built && part.tasks.length > 0 && part.tasks.every((t) => t.state === "completed") && partChangedUnderPaths(observed, part.part, view.description) === "unchanged") {
9288
+ lines.push(` ${NOTHING_COUNTED_DESPITE_FINISHED_LINE}`);
9289
+ }
9165
9290
  const code = codeLinkLine(part.codeLink);
9166
9291
  if (code !== void 0)
9167
9292
  lines.push(` ${code}`);
@@ -11102,7 +11227,7 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
11102
11227
  acceptanceOutcome: { verdict, findings: findingsDetail.length, findingsDetail },
11103
11228
  stated
11104
11229
  }, () => runObserveChild(runner, credential, repoRoot), repoRoot);
11105
- const report = closeStepReport(phaseArg, attempt);
11230
+ const report = closeStepReport(phaseArg, attempt, credential.engagementId);
11106
11231
  for (const line of report.stdout)
11107
11232
  process.stdout.write(`${line}
11108
11233
  `);