halfcycle 0.4.8 → 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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/bin.bundle.mjs +2162 -335
  3. package/commands/agent-teams-orchestration.md +1 -1
  4. package/commands/consistency-check.md +1 -1
  5. package/commands/context-update.md +1 -1
  6. package/commands/halfcycle-design.md +1 -1
  7. package/commands/halfcycle-setup.md +1 -1
  8. package/commands/stack-assembly-worker.md +1 -1
  9. package/commands/task-execution.md +1 -1
  10. package/commands/task-graph.md +1 -1
  11. package/commands/write-spec.md +1 -1
  12. package/dist/bin.d.ts +2 -1
  13. package/dist/bin.d.ts.map +1 -1
  14. package/dist/bin.js +0 -0
  15. package/dist/build-record/obtain.d.ts +14 -7
  16. package/dist/build-record/obtain.d.ts.map +1 -1
  17. package/dist/build-record/render-v2.d.ts +32 -6
  18. package/dist/build-record/render-v2.d.ts.map +1 -1
  19. package/dist/build-record/write.d.ts +2 -2
  20. package/dist/build-record/write.d.ts.map +1 -1
  21. package/dist/cli-contract.d.ts +2 -9
  22. package/dist/cli-contract.d.ts.map +1 -1
  23. package/dist/cli.js +3859 -1946
  24. package/dist/cli.js.map +3 -3
  25. package/dist/close-phase.d.ts +223 -134
  26. package/dist/close-phase.d.ts.map +1 -1
  27. package/dist/draw/index.d.ts +29 -6
  28. package/dist/draw/index.d.ts.map +1 -1
  29. package/dist/identity.d.ts +78 -9
  30. package/dist/identity.d.ts.map +1 -1
  31. package/dist/index.js +1685 -232
  32. package/dist/index.js.map +3 -3
  33. package/dist/install.d.ts +16 -0
  34. package/dist/install.d.ts.map +1 -1
  35. package/dist/open-phase.d.ts +25 -1
  36. package/dist/open-phase.d.ts.map +1 -1
  37. package/dist/plan.d.ts +75 -23
  38. package/dist/plan.d.ts.map +1 -1
  39. package/dist/pre-create-checks.d.ts.map +1 -1
  40. package/dist/review.d.ts +231 -0
  41. package/dist/review.d.ts.map +1 -0
  42. package/dist/shown-record.d.ts +46 -0
  43. package/dist/shown-record.d.ts.map +1 -0
  44. package/dist/system-report.d.ts +28 -4
  45. package/dist/system-report.d.ts.map +1 -1
  46. package/package.json +11 -11
  47. package/dist/project-page.d.ts +0 -24
  48. package/dist/project-page.d.ts.map +0 -1
@@ -14781,32 +14781,68 @@ var HALFCYCLE_COMMAND = "npx halfcycle@latest";
14781
14781
 
14782
14782
  // ../core/dist/shown-plans.js
14783
14783
  var SHOWN_PLANS_FILENAME = "shown-plans.json";
14784
- var SHOWN_PLANS_FORMAT = 1;
14785
- var RECORD_KEYS = ["entries", "format"];
14784
+ var SHOWN_RECORD_FORMAT = 2;
14785
+ var SHOWN_RECORD_FORMAT_1 = 1;
14786
+ var FORMAT_1_KEYS = ["entries", "format"];
14787
+ var RECORD_KEYS = ["entries", "format", "reviews"];
14788
+ var WRITTEN_KEYS = ["entries", "reviews"];
14786
14789
  var ENTRY_KEYS = ["number", "phase", "shownAt", "version"];
14787
- function parseShownPlans(text) {
14790
+ var REVIEW_KEYS = ["number", "parts", "phase", "reviewVersion", "round", "shownAt"];
14791
+ var REVIEW_PART_KEYS = ["awaitingAnswer", "built", "decidable", "name", "part", "sentBack"];
14792
+ function nothingShown() {
14793
+ return { entries: [], reviews: [] };
14794
+ }
14795
+ function parseShownRecord(text) {
14788
14796
  try {
14789
14797
  if (typeof text !== "string")
14790
- return [];
14798
+ return nothingShown();
14791
14799
  const parsed = JSON.parse(text);
14792
- if (!isPlainObject2(parsed) || !hasExactKeys(parsed, RECORD_KEYS))
14793
- return [];
14794
- if (parsed["format"] !== SHOWN_PLANS_FORMAT)
14795
- return [];
14796
- const rawEntries = parsed["entries"];
14797
- if (!Array.isArray(rawEntries))
14798
- return [];
14799
- const entries = [];
14800
- for (const raw of rawEntries) {
14801
- const entry = readEntry(raw);
14802
- if (entry === null)
14803
- return [];
14804
- entries.push(entry);
14805
- }
14806
- return hasDuplicates(entries) ? [] : entries;
14800
+ if (!isPlainObject2(parsed))
14801
+ return nothingShown();
14802
+ let rawReviews;
14803
+ if (parsed["format"] === SHOWN_RECORD_FORMAT && hasExactKeys(parsed, RECORD_KEYS)) {
14804
+ rawReviews = parsed["reviews"];
14805
+ } else if (parsed["format"] === SHOWN_RECORD_FORMAT_1 && hasExactKeys(parsed, FORMAT_1_KEYS)) {
14806
+ rawReviews = [];
14807
+ } else {
14808
+ return nothingShown();
14809
+ }
14810
+ const record2 = readRecord(parsed["entries"], rawReviews);
14811
+ return record2 ?? nothingShown();
14807
14812
  } catch {
14808
- return [];
14813
+ return nothingShown();
14814
+ }
14815
+ }
14816
+ function serialiseShownRecord(record2) {
14817
+ if (!isPlainObject2(record2) || !hasExactKeys(record2, WRITTEN_KEYS))
14818
+ throw new Error(UNRECORDABLE);
14819
+ const copied = readRecord(record2["entries"], record2["reviews"]);
14820
+ if (copied === null)
14821
+ throw new Error(UNRECORDABLE);
14822
+ return `${JSON.stringify({ format: SHOWN_RECORD_FORMAT, entries: copied.entries, reviews: copied.reviews }, null, 2)}
14823
+ `;
14824
+ }
14825
+ var UNRECORDABLE = "the list just shown could not be recorded on this machine.";
14826
+ function readRecord(rawEntries, rawReviews) {
14827
+ if (!Array.isArray(rawEntries) || !Array.isArray(rawReviews))
14828
+ return null;
14829
+ const entries = [];
14830
+ for (const raw of rawEntries) {
14831
+ const entry = readEntry(raw);
14832
+ if (entry === null)
14833
+ return null;
14834
+ entries.push(entry);
14835
+ }
14836
+ const reviews = [];
14837
+ for (const raw of rawReviews) {
14838
+ const review = readReview(raw);
14839
+ if (review === null)
14840
+ return null;
14841
+ reviews.push(review);
14809
14842
  }
14843
+ if (hasDuplicates(entries) || hasDuplicates(reviews))
14844
+ return null;
14845
+ return { entries, reviews };
14810
14846
  }
14811
14847
  function readEntry(raw) {
14812
14848
  if (!isPlainObject2(raw) || !hasExactKeys(raw, ENTRY_KEYS))
@@ -14815,7 +14851,7 @@ function readEntry(raw) {
14815
14851
  const number4 = raw["number"];
14816
14852
  const version2 = raw["version"];
14817
14853
  const shownAt = raw["shownAt"];
14818
- if (typeof phase !== "string" || phase === "")
14854
+ if (!isNonEmptyString(phase))
14819
14855
  return null;
14820
14856
  if (!isPositiveInteger(number4) || !isPositiveInteger(version2))
14821
14857
  return null;
@@ -14823,6 +14859,53 @@ function readEntry(raw) {
14823
14859
  return null;
14824
14860
  return { phase, number: number4, version: version2, shownAt };
14825
14861
  }
14862
+ function readReview(raw) {
14863
+ if (!isPlainObject2(raw) || !hasExactKeys(raw, REVIEW_KEYS))
14864
+ return null;
14865
+ const phase = raw["phase"];
14866
+ const number4 = raw["number"];
14867
+ const round = raw["round"];
14868
+ const reviewVersion = raw["reviewVersion"];
14869
+ const shownAt = raw["shownAt"];
14870
+ const rawParts = raw["parts"];
14871
+ if (!isNonEmptyString(phase))
14872
+ return null;
14873
+ if (number4 !== null && !isPositiveInteger(number4))
14874
+ return null;
14875
+ if (!isPositiveInteger(round) || !isWholeNumber(reviewVersion))
14876
+ return null;
14877
+ if (!isIsoInstant(shownAt))
14878
+ return null;
14879
+ if (!Array.isArray(rawParts))
14880
+ return null;
14881
+ const parts = [];
14882
+ const seen = /* @__PURE__ */ new Set();
14883
+ for (const rawPart of rawParts) {
14884
+ const part = readReviewPart(rawPart);
14885
+ if (part === null || seen.has(part.part))
14886
+ return null;
14887
+ seen.add(part.part);
14888
+ parts.push(part);
14889
+ }
14890
+ return { phase, number: number4, round, reviewVersion, shownAt, parts };
14891
+ }
14892
+ function readReviewPart(raw) {
14893
+ if (!isPlainObject2(raw) || !hasExactKeys(raw, REVIEW_PART_KEYS))
14894
+ return null;
14895
+ const part = raw["part"];
14896
+ const name = raw["name"];
14897
+ const built = raw["built"];
14898
+ const decidable = raw["decidable"];
14899
+ const awaitingAnswer = raw["awaitingAnswer"];
14900
+ const sentBack = raw["sentBack"];
14901
+ if (!isNonEmptyString(part) || !isNonEmptyString(name))
14902
+ return null;
14903
+ if (typeof built !== "boolean" || typeof decidable !== "boolean")
14904
+ return null;
14905
+ if (typeof awaitingAnswer !== "boolean" || typeof sentBack !== "boolean")
14906
+ return null;
14907
+ return { part, name, built, decidable, awaitingAnswer, sentBack };
14908
+ }
14826
14909
  function isPlainObject2(value) {
14827
14910
  if (typeof value !== "object" || value === null || Array.isArray(value))
14828
14911
  return false;
@@ -14833,19 +14916,26 @@ function hasExactKeys(value, keys) {
14833
14916
  const own = Object.keys(value).sort();
14834
14917
  return own.length === keys.length && own.every((key, i) => key === keys[i]);
14835
14918
  }
14919
+ function isNonEmptyString(value) {
14920
+ return typeof value === "string" && value !== "";
14921
+ }
14836
14922
  function isPositiveInteger(value) {
14837
14923
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
14838
14924
  }
14925
+ function isWholeNumber(value) {
14926
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
14927
+ }
14839
14928
  function isIsoInstant(value) {
14840
14929
  if (typeof value !== "string")
14841
14930
  return false;
14842
14931
  const ms = Date.parse(value);
14843
14932
  return Number.isFinite(ms) && new Date(ms).toISOString() === value;
14844
14933
  }
14845
- function hasDuplicates(entries) {
14846
- const phases = new Set(entries.map((entry) => entry.phase));
14847
- const numbers = new Set(entries.map((entry) => entry.number));
14848
- return phases.size !== entries.length || numbers.size !== entries.length;
14934
+ function hasDuplicates(items) {
14935
+ const phases = new Set(items.map((item) => item.phase));
14936
+ const numbered = items.filter((item) => item.number !== null);
14937
+ const numbers = new Set(numbered.map((item) => item.number));
14938
+ return phases.size !== items.length || numbers.size !== numbered.length;
14849
14939
  }
14850
14940
 
14851
14941
  // ../core/dist/phase-reference.js
@@ -14861,10 +14951,54 @@ function resolvePhaseReference(typed, phases) {
14861
14951
  const byNumber = phases.filter((entry) => Number.isInteger(entry.number) && String(entry.number) === typed);
14862
14952
  return byNumber.length === 1 ? byNumber[0] : null;
14863
14953
  }
14954
+ function typedPhaseReference(phase, number4) {
14955
+ return phase.includes(":") && number4 !== null ? String(number4) : phase;
14956
+ }
14864
14957
 
14865
14958
  // ../core/dist/release-version.js
14866
14959
  var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
14867
14960
 
14961
+ // ../core/dist/github-address.js
14962
+ var GITHUB_REPOSITORY_ADDRESS = /^https:\/\/github\.com\/[A-Za-z0-9][A-Za-z0-9-]{0,38}\/(?!\.{1,2}$)[A-Za-z0-9._-]{1,100}$/;
14963
+ var HOST = "[Gg][Ii][Tt][Hh][Uu][Bb]\\.[Cc][Oo][Mm]";
14964
+ var OWNER = "[A-Za-z0-9][A-Za-z0-9-]{0,38}";
14965
+ var RAW_REPOSITORY = "[A-Za-z0-9._-]{1,104}";
14966
+ var TAIL = `(${OWNER})/(${RAW_REPOSITORY})/?$`;
14967
+ var URL_USERINFO = "(?:[^\\s/?#@]+@)?";
14968
+ var SSH_USER = "(?:[^\\s/?#@:]+@)?";
14969
+ var REMOTE_FORMS = [
14970
+ new RegExp(`^https?://${URL_USERINFO}${HOST}/${TAIL}`),
14971
+ new RegExp(`^ssh://${SSH_USER}${HOST}/${TAIL}`),
14972
+ new RegExp(`^git://${HOST}/${TAIL}`),
14973
+ new RegExp(`^${SSH_USER}${HOST}:${TAIL}`)
14974
+ ];
14975
+ var REPOSITORY_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._-]{1,100}$/;
14976
+ function githubRepositoryAddress(remote) {
14977
+ for (const form of REMOTE_FORMS) {
14978
+ const match = form.exec(remote);
14979
+ if (match === null)
14980
+ continue;
14981
+ const owner = match[1];
14982
+ const raw = match[2];
14983
+ if (owner === void 0 || raw === void 0)
14984
+ return null;
14985
+ const repository = raw.endsWith(".git") ? raw.slice(0, -4) : raw;
14986
+ if (!REPOSITORY_NAME.test(repository))
14987
+ return null;
14988
+ const address = `https://github.com/${owner}/${repository}`;
14989
+ if (!GITHUB_REPOSITORY_ADDRESS.test(address))
14990
+ return null;
14991
+ if (looksLikeSecret(address))
14992
+ return null;
14993
+ return address;
14994
+ }
14995
+ return null;
14996
+ }
14997
+
14998
+ // ../core/dist/sign-in.js
14999
+ var SIGN_IN_COMMAND = '"npx halfcycle"';
15000
+ var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
15001
+
14868
15002
  // dist/control-origin.js
14869
15003
  var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
14870
15004
  function resolveControlOrigin(env) {
@@ -15150,6 +15284,11 @@ function hasLoneSurrogate(text) {
15150
15284
  }
15151
15285
  var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
15152
15286
 
15287
+ // ../events/dist/human-agent.js
15288
+ var HUMAN_AGENT = "Human";
15289
+ var AGENT_NAME_MAX = 32;
15290
+ var agentNameSchema = external_exports.string().min(1).max(AGENT_NAME_MAX);
15291
+
15153
15292
  // ../events/dist/state.js
15154
15293
  var utcIso8601 = external_exports.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");
15155
15294
  var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
@@ -15273,6 +15412,7 @@ var coverageSchema = external_exports.object({
15273
15412
  });
15274
15413
  }
15275
15414
  });
15415
+ var projectTierSchema = external_exports.number().int().min(0).max(3);
15276
15416
  var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
15277
15417
  recordKind: external_exports.literal("artefact"),
15278
15418
  artefactRef: artefactRefSchema,
@@ -15285,7 +15425,13 @@ var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
15285
15425
  recordKind: external_exports.literal("step-run"),
15286
15426
  runOutcome: runOutcomeSchema,
15287
15427
  failureReason: external_exports.string().optional(),
15288
- outcomeReason: external_exports.string().max(2e3).optional()
15428
+ outcomeReason: external_exports.string().max(2e3).optional(),
15429
+ // Who did this step, as the session reported it. Absent when not reported.
15430
+ agent: agentNameSchema.optional(),
15431
+ // When the session began this step. Absent when not reported.
15432
+ startedAt: utcIso8601.optional(),
15433
+ // The project's tier, when this step settled it. Absent on every other step.
15434
+ projectTier: projectTierSchema.optional()
15289
15435
  }).strict().superRefine((rec, ctx) => {
15290
15436
  if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
15291
15437
  ctx.addIssue({
@@ -15312,7 +15458,12 @@ var gateRecordSchema = stateRecordEnvelopeSchema.extend({
15312
15458
  verdict: gateVerdictSchema,
15313
15459
  actor: external_exports.string(),
15314
15460
  outcomeReason: external_exports.string().max(2e3).optional(),
15315
- coverage: coverageSchema.optional()
15461
+ coverage: coverageSchema.optional(),
15462
+ // Who did the step this checkpoint is on, as reported. Absent when not reported.
15463
+ agent: agentNameSchema.optional(),
15464
+ // When that step, or the review this checkpoint closes, began. Absent when not
15465
+ // reported.
15466
+ startedAt: utcIso8601.optional()
15316
15467
  }).strict().superRefine((rec, ctx) => {
15317
15468
  const issue2 = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
15318
15469
  if (issue2) {
@@ -15326,7 +15477,9 @@ var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
15326
15477
  severity: severitySchema,
15327
15478
  disposition: dispositionSchema,
15328
15479
  summary: external_exports.string().max(2e3),
15329
- coverage: coverageSchema.optional()
15480
+ coverage: coverageSchema.optional(),
15481
+ // Who caught it, as the session reported. Absent when not reported.
15482
+ agent: agentNameSchema.optional()
15330
15483
  }).strict();
15331
15484
  var stateRecordSchema = external_exports.discriminatedUnion("recordKind", [
15332
15485
  artefactRecordSchema,
@@ -15420,6 +15573,11 @@ var FINDINGS_SHOWN_MAX = 50;
15420
15573
  var PHASE_CLOSE_REASON_MAX = 1900;
15421
15574
  var closeCommitSchema = external_exports.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash: 40 or 64 lowercase hex characters");
15422
15575
  var walkVerdictSchema = external_exports.enum(["clean", "defects"]);
15576
+ var acceptanceOutcomeSchema = external_exports.object({
15577
+ verdict: walkVerdictSchema,
15578
+ findings: external_exports.number().int().nonnegative(),
15579
+ findingsDetail: external_exports.array(external_exports.object({ summary: external_exports.string().refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE }) }).strict())
15580
+ }).strict();
15423
15581
  var closeDecisionSchema = external_exports.enum(["done-when-met", "override"]);
15424
15582
  function plainText(max) {
15425
15583
  const base = max === void 0 ? external_exports.string() : external_exports.string().max(max);
@@ -15543,12 +15701,7 @@ var attestedCloseSchema = external_exports.object({
15543
15701
  var attestedSchema = external_exports.object({
15544
15702
  closes: external_exports.array(attestedCloseSchema)
15545
15703
  }).strict();
15546
- var buildRecordV2Schema = external_exports.object({
15547
- format: external_exports.literal(BUILD_RECORD_V2_FORMAT),
15548
- phase: phaseNameSchema,
15549
- observed: observedSchema,
15550
- attested: attestedSchema
15551
- }).strict().superRefine((record2, ctx) => {
15704
+ function checkRecordHalves(record2, ctx) {
15552
15705
  const observedBySequence = new Map(record2.observed.closes.map((c) => [c.sequence, c]));
15553
15706
  const attestedSequences = record2.attested.closes.map((c) => c.sequence);
15554
15707
  const sameSet = attestedSequences.length === observedBySequence.size && new Set(attestedSequences).size === attestedSequences.length && attestedSequences.every((s) => observedBySequence.has(s));
@@ -15576,139 +15729,13 @@ var buildRecordV2Schema = external_exports.object({
15576
15729
  });
15577
15730
  }
15578
15731
  });
15579
- });
15580
-
15581
- // ../events/dist/human-agent.js
15582
- var HUMAN_AGENT = "Human";
15583
-
15584
- // ../events/dist/build-snapshot.js
15585
- var buildTaskStateSchema = external_exports.enum([
15586
- "pending",
15587
- "in-flight",
15588
- "completed",
15589
- "failed",
15590
- "blocked"
15591
- ]);
15592
- var BUILD_TASK_TITLE_MAX = 240;
15593
- var BUILD_SNAPSHOT_TASK_LIMIT = 500;
15594
- var buildTaskSchema = external_exports.object({
15595
- // The task's own id, as the coordinator's graph names it.
15596
- id: external_exports.string().min(1).max(64),
15597
- // What the task is, in the coordinator's words. A description of the work,
15598
- // bounded and scanned — never an excerpt of anybody's code.
15599
- title: external_exports.string().min(1).max(BUILD_TASK_TITLE_MAX),
15600
- // What the task is doing right now.
15601
- state: buildTaskStateSchema,
15602
- // Who holds it — a bare callsign, or the value naming a person when the work is
15603
- // a person's. Absent when nothing has been dispatched to it yet.
15604
- agent: external_exports.string().min(1).max(32).optional(),
15605
- // The kind of work the holder does. Present exactly when an agent holds it.
15606
- discipline: external_exports.string().min(1).max(64).optional(),
15607
- // How much thinking the holder brings. Present exactly when an agent holds it,
15608
- // and there is no field naming the supplier behind it.
15609
- tier: external_exports.string().min(1).max(32).optional(),
15610
- // The tasks this one cannot start before. Always present; empty means it waits
15611
- // on nothing, which is different from not having been computed.
15612
- dependsOn: external_exports.array(external_exports.string().min(1).max(64)).max(BUILD_SNAPSHOT_TASK_LIMIT),
15613
- // The one task this task checks, when it is a check on other work. Absent on a
15614
- // task that is doing the work rather than checking it.
15615
- reviews: external_exports.string().min(1).max(64).optional(),
15616
- // Which wave of the phase this task belongs to — the wave that writes the
15617
- // specifications, or the wave that builds them. Absent when the graph does not
15618
- // record it, which is different from the task belonging to neither.
15619
- layer: methodLayerSchema.optional(),
15620
- // When it was picked up, and when it settled. A reader computes how long it
15621
- // took; neither this row nor this payload carries a duration.
15622
- startedAt: utcIso8601.optional(),
15623
- settledAt: utcIso8601.optional()
15624
- }).strict().superRefine((task, ctx) => {
15625
- if (task.reviews !== void 0 && task.reviews === task.id) {
15626
- ctx.addIssue({
15627
- code: external_exports.ZodIssueCode.custom,
15628
- path: ["reviews"],
15629
- message: `'${task.id}' reviews itself \u2014 a review names the OTHER task it checks.`
15630
- });
15631
- }
15632
- if (task.layer !== void 0 && !isPhaseScopedLayer(task.layer)) {
15633
- ctx.addIssue({
15634
- code: external_exports.ZodIssueCode.custom,
15635
- path: ["layer"],
15636
- message: `'${task.layer}' is not a wave a phase runs \u2014 expected one of ${PHASE_SCOPED_LAYERS.join(", ")}.`
15637
- });
15638
- }
15639
- const profiled = task.agent !== void 0 && task.agent !== HUMAN_AGENT;
15640
- for (const field of ["discipline", "tier"]) {
15641
- if (profiled && task[field] === void 0) {
15642
- ctx.addIssue({
15643
- code: external_exports.ZodIssueCode.custom,
15644
- path: [field],
15645
- message: `'${field}' is required on a task an agent holds.`
15646
- });
15647
- }
15648
- if (!profiled && task[field] !== void 0) {
15649
- ctx.addIssue({
15650
- code: external_exports.ZodIssueCode.custom,
15651
- path: [field],
15652
- message: task.agent === void 0 ? `'${field}' must be absent on a task nobody holds.` : `'${field}' must be absent on work a person holds.`
15653
- });
15654
- }
15655
- }
15656
- });
15657
- var buildPhaseSchema = external_exports.string().min(1).max(128);
15658
- var BUILD_PHASE_TITLE_MAX = 160;
15659
- var buildPhaseTitleSchema = external_exports.string().min(1).max(BUILD_PHASE_TITLE_MAX);
15660
- var buildTaskListSchema = external_exports.array(buildTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT).superRefine((tasks, ctx) => {
15661
- const seen = /* @__PURE__ */ new Set();
15662
- for (const [index, task] of tasks.entries()) {
15663
- if (seen.has(task.id)) {
15664
- ctx.addIssue({
15665
- code: external_exports.ZodIssueCode.custom,
15666
- path: [index, "id"],
15667
- message: `duplicate task id '${task.id}' \u2014 one graph names each task once.`
15668
- });
15669
- }
15670
- seen.add(task.id);
15671
- }
15672
- });
15673
- var buildSnapshotSchema = external_exports.object({
15674
- // Which phase this graph belongs to.
15675
- phase: buildPhaseSchema,
15676
- // That phase's name in words, when the project gives it one.
15677
- phaseTitle: buildPhaseTitleSchema.optional(),
15678
- // Every task on it. Never a partial graph and never a change since last time.
15679
- tasks: buildTaskListSchema,
15680
- // When this graph was last reported.
15681
- reportedAt: utcIso8601,
15682
- // When work on this phase was first reported with no approval of its scope in force.
15683
- builtWithoutApproval: utcIso8601.optional()
15684
- }).strict();
15685
- var buildPhaseEntrySchema = external_exports.object({
15686
- // Which phase has a stored graph.
15687
- phase: buildPhaseSchema,
15688
- // That phase's name in words, when the project gave it one.
15689
- phaseTitle: buildPhaseTitleSchema.optional(),
15690
- // When that phase's graph was last reported.
15691
- reportedAt: utcIso8601,
15692
- // Present when work on this phase was reported with no approval of its scope in force.
15693
- builtWithoutApproval: external_exports.literal(true).optional()
15694
- }).strict();
15695
- var buildSnapshotReportSchema = external_exports.object({
15696
- // Which phase this graph belongs to.
15697
- phase: buildPhaseSchema,
15698
- // That phase's name in words, when the project gives it one.
15699
- phaseTitle: buildPhaseTitleSchema.optional(),
15700
- // The whole graph, every time. Reporting a change instead of the graph is the
15701
- // one thing this route does not accept.
15702
- tasks: buildTaskListSchema
15703
- }).strict();
15704
- var buildSnapshotReportResultSchema = external_exports.object({
15705
- // What happened. One outcome today; a refusal travels as the shared wire error.
15706
- outcome: external_exports.literal("recorded"),
15707
- // Which phase the graph was recorded against.
15708
- phase: buildPhaseSchema,
15709
- // When it was recorded.
15710
- reportedAt: utcIso8601
15711
- }).strict();
15732
+ }
15733
+ var buildRecordV2Schema = external_exports.object({
15734
+ format: external_exports.literal(BUILD_RECORD_V2_FORMAT),
15735
+ phase: phaseNameSchema,
15736
+ observed: observedSchema,
15737
+ attested: attestedSchema
15738
+ }).strict().superRefine(checkRecordHalves);
15712
15739
 
15713
15740
  // ../events/dist/system-description.js
15714
15741
  var SYSTEM_DESCRIPTION_FORMAT = 1;
@@ -15782,6 +15809,72 @@ var systemDescriptionProblemSchema = external_exports.object({
15782
15809
  path: external_exports.string(),
15783
15810
  message: external_exports.string()
15784
15811
  }).strict();
15812
+ function segmentMatches(pattern, segment) {
15813
+ const literals = pattern.split("*");
15814
+ if (literals.length === 1)
15815
+ return pattern === segment;
15816
+ const first = literals[0];
15817
+ const last = literals[literals.length - 1];
15818
+ if (!segment.startsWith(first))
15819
+ return false;
15820
+ let position = first.length;
15821
+ for (const literal2 of literals.slice(1, -1)) {
15822
+ const found = segment.indexOf(literal2, position + 1);
15823
+ if (found === -1)
15824
+ return false;
15825
+ position = found + literal2.length;
15826
+ }
15827
+ return segment.length - last.length >= position + 1 && segment.endsWith(last);
15828
+ }
15829
+ function patternOwns(pattern, path) {
15830
+ const want = pattern.split("/");
15831
+ const have = path.split("/");
15832
+ const width = have.length + 1;
15833
+ const seen = new Uint8Array((want.length + 1) * width);
15834
+ const from = (i, j) => {
15835
+ if (i === want.length)
15836
+ return true;
15837
+ const cell = i * width + j;
15838
+ if (seen[cell] !== 0)
15839
+ return seen[cell] === 2;
15840
+ const segment = want[i];
15841
+ 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);
15842
+ seen[cell] = result ? 2 : 1;
15843
+ return result;
15844
+ };
15845
+ return from(0, 0);
15846
+ }
15847
+ function specificity(pattern) {
15848
+ const segments = pattern.split("/");
15849
+ const firstWildcard = segments.findIndex((segment) => segment.includes("*"));
15850
+ return {
15851
+ leadingLiterals: firstWildcard === -1 ? segments.length : firstWildcard,
15852
+ wildcards: segments.filter((segment) => segment.includes("*")).length,
15853
+ literalCharacters: pattern.replace(/[*/]/g, "").length
15854
+ };
15855
+ }
15856
+ function outranks(a, b) {
15857
+ if (a.rank.leadingLiterals !== b.rank.leadingLiterals)
15858
+ return b.rank.leadingLiterals - a.rank.leadingLiterals;
15859
+ if (a.rank.wildcards !== b.rank.wildcards)
15860
+ return a.rank.wildcards - b.rank.wildcards;
15861
+ if (a.rank.literalCharacters !== b.rank.literalCharacters)
15862
+ return b.rank.literalCharacters - a.rank.literalCharacters;
15863
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
15864
+ }
15865
+ function ownerOf(path, description) {
15866
+ let best = null;
15867
+ for (const part of description.parts) {
15868
+ for (const pattern of part.owns) {
15869
+ if (!patternOwns(pattern, path))
15870
+ continue;
15871
+ const candidate = { id: part.id, rank: specificity(pattern) };
15872
+ if (best === null || outranks(candidate, best) < 0)
15873
+ best = candidate;
15874
+ }
15875
+ }
15876
+ return best === null ? null : best.id;
15877
+ }
15785
15878
  var systemReportRecordedSchema = external_exports.object({
15786
15879
  outcome: external_exports.literal("recorded"),
15787
15880
  version: external_exports.number().int().min(1),
@@ -15811,9 +15904,14 @@ var systemDrawPhaseRequestSchema = external_exports.object({
15811
15904
  phase: phaseIdentitySchema,
15812
15905
  planVersion: external_exports.number().int().min(1)
15813
15906
  }).strict();
15907
+ var systemDrawClosedPhaseRequestSchema = external_exports.object({
15908
+ picture: external_exports.literal("closed-phase"),
15909
+ phase: phaseIdentitySchema
15910
+ }).strict();
15814
15911
  var systemDrawRequestSchema = external_exports.discriminatedUnion("picture", [
15815
15912
  systemDrawArchitectureRequestSchema,
15816
- systemDrawPhaseRequestSchema
15913
+ systemDrawPhaseRequestSchema,
15914
+ systemDrawClosedPhaseRequestSchema
15817
15915
  ]);
15818
15916
  function utf8ByteLength2(text) {
15819
15917
  let bytes = 0;
@@ -15852,58 +15950,232 @@ var systemDrawRefusedSchema = external_exports.object({
15852
15950
  var systemDrawUnavailableSchema = external_exports.object({
15853
15951
  outcome: external_exports.literal("unavailable")
15854
15952
  }).strict();
15855
-
15856
- // ../events/dist/phase-plan.js
15857
- var PHASE_PLAN_FORMAT = 1;
15858
- var PHASE_PLAN_CHANGES_MAX = 60;
15859
- var PHASE_PLAN_SUMMARY_MAX = 160;
15860
- var SCOPE_NOTE_MAX = 500;
15861
- var PHASE_PLAN_PHASE_MAX = buildPhaseSchema.maxLength ?? 0;
15862
- var PHASE_PLAN_CHANGE_KINDS = ["add", "change", "remove"];
15863
- var phasePlanChangeSchema = external_exports.enum(PHASE_PLAN_CHANGE_KINDS, {
15864
- error: `must be one of ${PHASE_PLAN_CHANGE_KINDS.join(", ")}`
15865
- });
15866
- var phasePlanPhaseSchema = phaseIdentitySchema.refine((v) => !isValidPhaseIdentity(v) || buildPhaseSchema.safeParse(String(v)).success, { message: `must be at most ${PHASE_PLAN_PHASE_MAX} characters` });
15867
- var phasePlanSummarySchema = descriptionText(PHASE_PLAN_SUMMARY_MAX);
15868
- var phasePlanEntrySchema = external_exports.object({
15869
- part: systemPartIdSchema,
15870
- change: phasePlanChangeSchema,
15871
- summary: phasePlanSummarySchema
15872
- }).strict();
15873
- var phasePlanSchema = external_exports.object({
15874
- format: external_exports.literal(PHASE_PLAN_FORMAT, { error: `must be ${PHASE_PLAN_FORMAT}` }),
15875
- phase: phasePlanPhaseSchema,
15876
- changes: external_exports.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` })
15953
+ var systemDrawNotClosedSchema = external_exports.object({
15954
+ outcome: external_exports.literal("not-closed")
15877
15955
  }).strict();
15878
- var scopePhaseSchema = buildPhaseSchema.refine((v) => isValidPhaseIdentity(v), {
15879
- message: "must be a phase name"
15880
- });
15881
- var SCOPE_STATE_NAMES = [
15882
- "not-planned",
15883
- "waiting",
15884
- "sent-back",
15885
- "approved",
15886
- "partly-stale",
15887
- "withdrawn"
15888
- ];
15889
- var scopeStateNameSchema = external_exports.enum(SCOPE_STATE_NAMES);
15890
- var plannedScopeStateSchema = scopeStateNameSchema.exclude(["not-planned"]);
15891
- var SCOPE_PART_STATUSES = ["approved", "waiting"];
15892
- var scopePartStatusSchema = external_exports.enum(SCOPE_PART_STATUSES);
15893
- var SCOPE_ACTS = ["approve", "send-back", "withdraw"];
15894
- var scopeActSchema = external_exports.enum(SCOPE_ACTS);
15895
- var SCOPE_GIVEN_IN = ["console", "chat"];
15896
- var scopeGivenInSchema = external_exports.enum(SCOPE_GIVEN_IN);
15897
- var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
15898
- var scopeNoteWireSchema = external_exports.string().max(SCOPE_NOTE_MAX);
15899
- var SCOPE_PART_VERDICTS = ["ok", "change"];
15900
- var scopePartVerdictSchema = external_exports.enum(SCOPE_PART_VERDICTS);
15901
- var scopeDecisionPartSchema = external_exports.object({
15902
- part: systemPartIdSchema,
15903
- verdict: scopePartVerdictSchema,
15956
+
15957
+ // ../events/dist/build-snapshot.js
15958
+ var buildTaskStateSchema = external_exports.enum([
15959
+ "pending",
15960
+ "in-flight",
15961
+ "completed",
15962
+ "failed",
15963
+ "blocked"
15964
+ ]);
15965
+ var BUILD_TASK_TITLE_MAX = 240;
15966
+ var BUILD_SNAPSHOT_TASK_LIMIT = 500;
15967
+ var BUILD_TASK_WHY_OUTSIDE_PLAN_MAX = 160;
15968
+ var BUILD_TASK_BUDGET_MAX_MINUTES = 10080;
15969
+ var BUILD_TASK_NOTE_MAX = 240;
15970
+ var BUILD_TASK_CLEAR_COMMAND_MAX = 240;
15971
+ var buildTaskKindSchema = external_exports.enum(["build", "review", "spec", "walk"]);
15972
+ var buildTaskIdSchema = external_exports.string().min(1).max(64);
15973
+ var buildTaskSchema = external_exports.object({
15974
+ // The task's own id, as the coordinator's graph names it.
15975
+ id: buildTaskIdSchema,
15976
+ // What the task is, in the coordinator's words. A description of the work,
15977
+ // bounded and scanned — never an excerpt of anybody's code.
15978
+ title: external_exports.string().min(1).max(BUILD_TASK_TITLE_MAX),
15979
+ // What the task is doing right now.
15980
+ state: buildTaskStateSchema,
15981
+ // Who holds it — a bare callsign, or the value naming a person when the work is
15982
+ // a person's. Absent when nothing has been dispatched to it yet.
15983
+ agent: agentNameSchema.optional(),
15984
+ // The kind of work the holder does. Present exactly when an agent holds it.
15985
+ discipline: external_exports.string().min(1).max(64).optional(),
15986
+ // How much thinking the holder brings. Present exactly when an agent holds it,
15987
+ // and there is no field naming the supplier behind it.
15988
+ tier: external_exports.string().min(1).max(32).optional(),
15989
+ // The tasks this one cannot start before. Always present; empty means it waits
15990
+ // on nothing, which is different from not having been computed.
15991
+ dependsOn: external_exports.array(buildTaskIdSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
15992
+ // The one task this task checks, when it is a check on other work. Absent on a
15993
+ // task that is doing the work rather than checking it.
15994
+ reviews: buildTaskIdSchema.optional(),
15995
+ // Which wave of the phase this task belongs to — the wave that writes the
15996
+ // specifications, or the wave that builds them. Absent when the graph does not
15997
+ // record it, which is different from the task belonging to neither.
15998
+ layer: methodLayerSchema.optional(),
15999
+ // When it was picked up, and when it settled. A reader computes how long it
16000
+ // took; neither this row nor this payload carries a duration.
16001
+ startedAt: utcIso8601.optional(),
16002
+ settledAt: utcIso8601.optional(),
16003
+ // The parts of this project's system the task builds, by their ids. Absent when
16004
+ // it builds none; never an empty list.
16005
+ builds: external_exports.array(systemPartIdSchema).min(1).max(SYSTEM_PARTS_MAX).optional(),
16006
+ // In one line, why the task built a part the phase's plan does not include, in
16007
+ // the words of whoever holds it. Absent when it gave no reason.
16008
+ whyOutsidePlan: descriptionText(BUILD_TASK_WHY_OUTSIDE_PLAN_MAX).optional(),
16009
+ // How long the task is expected to take, in whole minutes, from one minute to one
16010
+ // week. A plan, not a measurement; absent when no budget was set.
16011
+ budgetMinutes: external_exports.number().int().min(1).max(BUILD_TASK_BUDGET_MAX_MINUTES).optional(),
16012
+ // What kind of work the task is: building, checking other work, writing what to
16013
+ // build, or a person trying the product. Absent when the graph does not say.
16014
+ kind: buildTaskKindSchema.optional(),
16015
+ // One line from whoever holds the task about where it stands. Absent when there
16016
+ // is nothing to say.
16017
+ note: descriptionText(BUILD_TASK_NOTE_MAX).optional(),
16018
+ // On a task held for a person, the one thing they do or run to clear it, in one
16019
+ // line. Absent when the task names none.
16020
+ clearCommand: descriptionText(BUILD_TASK_CLEAR_COMMAND_MAX).optional(),
16021
+ // On a task held for a person, the crew member ready to take it on once it is
16022
+ // cleared, by callsign. Absent when nobody is standing by.
16023
+ standingBy: agentNameSchema.optional()
16024
+ }).strict().superRefine((task, ctx) => {
16025
+ if (task.reviews !== void 0 && task.reviews === task.id) {
16026
+ ctx.addIssue({
16027
+ code: external_exports.ZodIssueCode.custom,
16028
+ path: ["reviews"],
16029
+ message: `'${task.id}' reviews itself \u2014 a review names the OTHER task it checks.`
16030
+ });
16031
+ }
16032
+ if (task.layer !== void 0 && !isPhaseScopedLayer(task.layer)) {
16033
+ ctx.addIssue({
16034
+ code: external_exports.ZodIssueCode.custom,
16035
+ path: ["layer"],
16036
+ message: `'${task.layer}' is not a wave a phase runs \u2014 expected one of ${PHASE_SCOPED_LAYERS.join(", ")}.`
16037
+ });
16038
+ }
16039
+ const profiled = task.agent !== void 0 && task.agent !== HUMAN_AGENT;
16040
+ for (const field of ["discipline", "tier"]) {
16041
+ if (profiled && task[field] === void 0) {
16042
+ ctx.addIssue({
16043
+ code: external_exports.ZodIssueCode.custom,
16044
+ path: [field],
16045
+ message: `'${field}' is required on a task an agent holds.`
16046
+ });
16047
+ }
16048
+ if (!profiled && task[field] !== void 0) {
16049
+ ctx.addIssue({
16050
+ code: external_exports.ZodIssueCode.custom,
16051
+ path: [field],
16052
+ message: task.agent === void 0 ? `'${field}' must be absent on a task nobody holds.` : `'${field}' must be absent on work a person holds.`
16053
+ });
16054
+ }
16055
+ }
16056
+ });
16057
+ var buildPhaseSchema = external_exports.string().min(1).max(128);
16058
+ var BUILD_PHASE_TITLE_MAX = 160;
16059
+ var buildPhaseTitleSchema = external_exports.string().min(1).max(BUILD_PHASE_TITLE_MAX);
16060
+ var buildTaskListSchema = external_exports.array(buildTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT).superRefine((tasks, ctx) => {
16061
+ const seen = /* @__PURE__ */ new Set();
16062
+ for (const [index, task] of tasks.entries()) {
16063
+ if (seen.has(task.id)) {
16064
+ ctx.addIssue({
16065
+ code: external_exports.ZodIssueCode.custom,
16066
+ path: [index, "id"],
16067
+ message: `duplicate task id '${task.id}' \u2014 one graph names each task once.`
16068
+ });
16069
+ }
16070
+ seen.add(task.id);
16071
+ }
16072
+ });
16073
+ var buildTaskMarkSchema = external_exports.object({
16074
+ // The task that was marked.
16075
+ task: buildTaskIdSchema,
16076
+ // When it was first marked.
16077
+ at: utcIso8601
16078
+ }).strict();
16079
+ var buildSnapshotSchema = external_exports.object({
16080
+ // Which phase this graph belongs to.
16081
+ phase: buildPhaseSchema,
16082
+ // That phase's name in words, when the project gives it one.
16083
+ phaseTitle: buildPhaseTitleSchema.optional(),
16084
+ // Every task on it. Never a partial graph and never a change since last time.
16085
+ tasks: buildTaskListSchema,
16086
+ // When this graph was last reported.
16087
+ reportedAt: utcIso8601,
16088
+ // When work on this phase was first reported with no approval of its scope in force.
16089
+ builtWithoutApproval: utcIso8601.optional(),
16090
+ // Each task that started on a part still waiting for approval, and when it was
16091
+ // first seen doing so. Absent when there is none.
16092
+ tasksBuiltWithoutApproval: external_exports.array(buildTaskMarkSchema).min(1).max(BUILD_SNAPSHOT_TASK_LIMIT).optional()
16093
+ }).strict();
16094
+ var buildPhaseEntrySchema = external_exports.object({
16095
+ // Which phase has a stored graph.
16096
+ phase: buildPhaseSchema,
16097
+ // That phase's name in words, when the project gave it one.
16098
+ phaseTitle: buildPhaseTitleSchema.optional(),
16099
+ // When that phase's graph was last reported.
16100
+ reportedAt: utcIso8601,
16101
+ // Present when work on this phase was reported with no approval of its scope in force.
16102
+ builtWithoutApproval: external_exports.literal(true).optional()
16103
+ }).strict();
16104
+ var buildSnapshotReportSchema = external_exports.object({
16105
+ // Which phase this graph belongs to.
16106
+ phase: buildPhaseSchema,
16107
+ // That phase's name in words, when the project gives it one.
16108
+ phaseTitle: buildPhaseTitleSchema.optional(),
16109
+ // The whole graph, every time. Reporting a change instead of the graph is the
16110
+ // one thing this route does not accept.
16111
+ tasks: buildTaskListSchema
16112
+ }).strict();
16113
+ var buildSnapshotReportResultSchema = external_exports.object({
16114
+ // What happened. One outcome today; a refusal travels as the shared wire error.
16115
+ outcome: external_exports.literal("recorded"),
16116
+ // Which phase the graph was recorded against.
16117
+ phase: buildPhaseSchema,
16118
+ // When it was recorded.
16119
+ reportedAt: utcIso8601
16120
+ }).strict();
16121
+
16122
+ // ../events/dist/phase-plan.js
16123
+ var PHASE_PLAN_FORMAT = 1;
16124
+ var PHASE_PLAN_CHANGES_MAX = 60;
16125
+ var PHASE_PLAN_SUMMARY_MAX = 160;
16126
+ var SCOPE_NOTE_MAX = 500;
16127
+ var PHASE_PLAN_PHASE_MAX = buildPhaseSchema.maxLength ?? 0;
16128
+ var PHASE_PLAN_CHANGE_KINDS = ["add", "change", "remove"];
16129
+ var phasePlanChangeSchema = external_exports.enum(PHASE_PLAN_CHANGE_KINDS, {
16130
+ error: `must be one of ${PHASE_PLAN_CHANGE_KINDS.join(", ")}`
16131
+ });
16132
+ var phasePlanPhaseSchema = phaseIdentitySchema.refine((v) => !isValidPhaseIdentity(v) || buildPhaseSchema.safeParse(String(v)).success, { message: `must be at most ${PHASE_PLAN_PHASE_MAX} characters` });
16133
+ var phasePlanSummarySchema = descriptionText(PHASE_PLAN_SUMMARY_MAX);
16134
+ var phasePlanEntrySchema = external_exports.object({
16135
+ part: systemPartIdSchema,
16136
+ change: phasePlanChangeSchema,
16137
+ summary: phasePlanSummarySchema
16138
+ }).strict();
16139
+ var phasePlanSchema = external_exports.object({
16140
+ format: external_exports.literal(PHASE_PLAN_FORMAT, { error: `must be ${PHASE_PLAN_FORMAT}` }),
16141
+ phase: phasePlanPhaseSchema,
16142
+ changes: external_exports.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` })
16143
+ }).strict();
16144
+ var scopePhaseSchema = buildPhaseSchema.refine((v) => isValidPhaseIdentity(v), {
16145
+ message: "must be a phase name"
16146
+ });
16147
+ var SCOPE_STATE_NAMES = [
16148
+ "not-planned",
16149
+ "waiting",
16150
+ "sent-back",
16151
+ "approved",
16152
+ "partly-stale"
16153
+ ];
16154
+ var scopeStateNameSchema = external_exports.enum(SCOPE_STATE_NAMES);
16155
+ var plannedScopeStateSchema = scopeStateNameSchema.exclude(["not-planned"]);
16156
+ var SCOPE_PART_STATUSES = ["approved", "waiting"];
16157
+ var scopePartStatusSchema = external_exports.enum(SCOPE_PART_STATUSES);
16158
+ var SCOPE_ACTS = ["approve", "send-back"];
16159
+ var scopeActSchema = external_exports.enum(SCOPE_ACTS);
16160
+ var SCOPE_GIVEN_IN = ["console", "chat"];
16161
+ var scopeGivenInSchema = external_exports.enum(SCOPE_GIVEN_IN);
16162
+ var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
16163
+ var scopeNoteWireSchema = external_exports.string().max(SCOPE_NOTE_MAX);
16164
+ var SCOPE_PART_VERDICTS = ["ok", "change"];
16165
+ var scopePartVerdictSchema = external_exports.enum(SCOPE_PART_VERDICTS);
16166
+ var scopeDecisionPartSchema = external_exports.object({
16167
+ part: systemPartIdSchema,
16168
+ verdict: scopePartVerdictSchema,
15904
16169
  note: scopeNoteWireSchema.optional()
15905
16170
  }).strict();
15906
16171
  var planVersionSchema = external_exports.number().int().min(1);
16172
+ var carriedPhaseSchema = external_exports.object({
16173
+ phase: scopePhaseSchema,
16174
+ number: external_exports.number().int().min(1)
16175
+ }).strict();
16176
+ var carriedFromEntrySchema = carriedPhaseSchema.extend({
16177
+ part: systemPartIdSchema
16178
+ }).strict();
15907
16179
  var phasePlanRequestSchema = external_exports.object({
15908
16180
  descriptionVersion: external_exports.number().int().min(1),
15909
16181
  plan: external_exports.unknown()
@@ -15915,7 +16187,8 @@ var phasePlanRecordedSchema = external_exports.object({
15915
16187
  state: plannedScopeStateSchema,
15916
16188
  changedParts: external_exports.array(systemPartIdSchema).max(PHASE_PLAN_CHANGES_MAX),
15917
16189
  typedApprovalOffered: external_exports.boolean(),
15918
- reportedAt: utcIso8601
16190
+ reportedAt: utcIso8601,
16191
+ carriedFrom: external_exports.array(carriedFromEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX).optional()
15919
16192
  }).strict();
15920
16193
  var phasePlanRefusedSchema = external_exports.object({
15921
16194
  outcome: external_exports.literal("refused"),
@@ -15924,10 +16197,23 @@ var phasePlanRefusedSchema = external_exports.object({
15924
16197
  var phasePlanUnavailableSchema = external_exports.object({
15925
16198
  outcome: external_exports.literal("unavailable")
15926
16199
  }).strict();
16200
+ var REMOVAL_OWED_ACTS = ["dropped", "removed"];
16201
+ var removalOwedPartSchema = external_exports.object({
16202
+ part: systemPartIdSchema,
16203
+ name: systemPartSchema.shape.name,
16204
+ phase: scopePhaseSchema,
16205
+ number: external_exports.number().int().min(1),
16206
+ act: external_exports.enum(REMOVAL_OWED_ACTS)
16207
+ }).strict();
16208
+ var phasePlanRemovalOwedSchema = external_exports.object({
16209
+ outcome: external_exports.literal("removal-owed"),
16210
+ parts: external_exports.array(removalOwedPartSchema).min(1).max(SYSTEM_PARTS_MAX)
16211
+ }).strict();
15927
16212
  var phasePlanResultSchema = external_exports.discriminatedUnion("outcome", [
15928
16213
  phasePlanRecordedSchema,
15929
16214
  phasePlanRefusedSchema,
15930
- phasePlanUnavailableSchema
16215
+ phasePlanUnavailableSchema,
16216
+ phasePlanRemovalOwedSchema
15931
16217
  ]);
15932
16218
  var scopeApproveRequestSchema = external_exports.object({
15933
16219
  act: external_exports.literal("approve"),
@@ -15940,14 +16226,9 @@ var scopeSendBackRequestSchema = external_exports.object({
15940
16226
  parts: external_exports.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
15941
16227
  note: scopeNoteWireSchema.optional()
15942
16228
  }).strict();
15943
- var scopeWithdrawRequestSchema = external_exports.object({
15944
- act: external_exports.literal("withdraw"),
15945
- planVersion: planVersionSchema
15946
- }).strict();
15947
16229
  var scopeDecisionRequestSchema = external_exports.discriminatedUnion("act", [
15948
16230
  scopeApproveRequestSchema,
15949
- scopeSendBackRequestSchema,
15950
- scopeWithdrawRequestSchema
16231
+ scopeSendBackRequestSchema
15951
16232
  ]);
15952
16233
  var scopeDecisionRecordedSchema = external_exports.object({
15953
16234
  outcome: external_exports.literal("recorded"),
@@ -15980,7 +16261,6 @@ var scopeDecisionResultSchema = external_exports.discriminatedUnion("outcome", [
15980
16261
  reasonOutcome("not-owner"),
15981
16262
  reasonOutcome("no-owner"),
15982
16263
  reasonOutcome("plan-changed"),
15983
- reasonOutcome("nothing-to-withdraw"),
15984
16264
  reasonOutcome("build-started")
15985
16265
  ]);
15986
16266
  var scopeReadPartSchema = external_exports.object({
@@ -15989,7 +16269,8 @@ var scopeReadPartSchema = external_exports.object({
15989
16269
  change: phasePlanChangeSchema,
15990
16270
  summary: phasePlanSummarySchema,
15991
16271
  status: scopePartStatusSchema,
15992
- changed: external_exports.boolean()
16272
+ changed: external_exports.boolean(),
16273
+ carriedFrom: carriedPhaseSchema.optional()
15993
16274
  }).strict();
15994
16275
  var scopeReadDecisionSchema = external_exports.object({
15995
16276
  act: scopeActSchema,
@@ -16016,75 +16297,581 @@ var scopeReadPlannedSchema = external_exports.object({
16016
16297
  }).strict();
16017
16298
  var scopeReadSchema = external_exports.union([scopeReadNotPlannedSchema, scopeReadPlannedSchema]);
16018
16299
 
16019
- // ../events/dist/client-version.js
16020
- var CLIENT_VERSION_HEADER = "x-halfcycle-client";
16021
- var TYPED_APPROVAL_OFF_SENTENCE = "Approving by typing is switched off for now.";
16022
- 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.`;
16300
+ // ../events/dist/code-link.js
16301
+ var codeLinkAddressSchema = external_exports.string().regex(GITHUB_REPOSITORY_ADDRESS, { message: "must be a GitHub repository address: https://github.com/<owner>/<repo>" }).refine((v) => !looksLikeSecret(v), { message: "looks like a password or key, so it is not sent" });
16302
+ var codeLinkFactsSchema = external_exports.object({
16303
+ repositoryAddress: codeLinkAddressSchema.nullable(),
16304
+ headOnGitHub: external_exports.boolean()
16305
+ }).strict().refine((v) => !(v.headOnGitHub && v.repositoryAddress === null), {
16306
+ message: "cannot be true when the code is not on GitHub",
16307
+ path: ["headOnGitHub"]
16308
+ });
16309
+ var PART_CODE_LINK_SCOPES = ["part", "phase"];
16310
+ var PART_CODE_LINK_RANGE_REASONS = ["not-github", "not-pushed"];
16311
+ var PART_CODE_LINK_URL_PREFIX = "https://github.com/";
16312
+ var partCodeLinkGithubSchema = external_exports.object({
16313
+ kind: external_exports.literal("github"),
16314
+ scope: external_exports.enum(PART_CODE_LINK_SCOPES),
16315
+ url: external_exports.string().startsWith(PART_CODE_LINK_URL_PREFIX, { message: "must start with https://github.com/" })
16316
+ }).strict();
16317
+ var partCodeLinkRangeSchema = external_exports.object({
16318
+ kind: external_exports.literal("range"),
16319
+ why: external_exports.enum(PART_CODE_LINK_RANGE_REASONS),
16320
+ from: closeCommitSchema,
16321
+ to: closeCommitSchema
16322
+ }).strict();
16323
+ var partCodeLinkNoneSchema = external_exports.object({
16324
+ kind: external_exports.literal("none")
16325
+ }).strict();
16326
+ var partCodeLinkSchema = external_exports.discriminatedUnion("kind", [
16327
+ partCodeLinkGithubSchema,
16328
+ partCodeLinkRangeSchema,
16329
+ partCodeLinkNoneSchema
16330
+ ]);
16023
16331
 
16024
- // ../events/dist/scope-notices.js
16025
- var scopePartNameSchema = external_exports.string().min(1).max(SYSTEM_PART_NAME_MAX);
16026
- var scopeNoteEntrySchema = external_exports.object({
16027
- part: scopePartNameSchema.optional(),
16028
- note: external_exports.string().min(1).max(SCOPE_NOTE_MAX)
16332
+ // ../events/dist/observed-changes.js
16333
+ var OBSERVED_PATHS_LISTED_MAX = 50;
16334
+ var OBSERVED_PATH_MAX = 300;
16335
+ var OBSERVED_COUNT_MAX = 1e6;
16336
+ var METHOD_FILE_PATTERNS = [
16337
+ "CLAUDE.md",
16338
+ "AGENTS.md",
16339
+ ".claude/context",
16340
+ ".claude/commands",
16341
+ ".claude/hooks",
16342
+ ".claude/settings.json",
16343
+ ".mcp.json",
16344
+ ".halfcycle",
16345
+ ".workbench",
16346
+ "docs/product-spec.md",
16347
+ "docs/architecture-overview.md",
16348
+ "docs/invariants.md",
16349
+ "docs/project-log.md",
16350
+ "docs/code-map.md",
16351
+ "docs/system.json",
16352
+ "docs/diagrams",
16353
+ "docs/features",
16354
+ "docs/phases",
16355
+ "docs/build-records",
16356
+ "docs/decisions",
16357
+ "docs/components"
16358
+ ];
16359
+ var TEST_FILE_PATTERNS = [
16360
+ "**/*.test.*",
16361
+ "**/*.spec.*",
16362
+ "**/*_test.*",
16363
+ "**/*_spec.*",
16364
+ "**/test_*.*",
16365
+ "**/*Test.java",
16366
+ "**/*Tests.java",
16367
+ "**/*Test.kt",
16368
+ "**/*Tests.cs",
16369
+ "**/*Test.php"
16370
+ ];
16371
+ function isMethodFile(path) {
16372
+ return METHOD_FILE_PATTERNS.some((pattern) => patternOwns(pattern, path));
16373
+ }
16374
+ function isTestFile(path) {
16375
+ return TEST_FILE_PATTERNS.some((pattern) => patternOwns(pattern, path));
16376
+ }
16377
+ var observedCountSchema = external_exports.number({ error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).int({ error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).min(0, { error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).max(OBSERVED_COUNT_MAX, { error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` });
16378
+ var observedDescriptionVersionSchema = external_exports.number().int().min(1);
16379
+ var observedPathSchema = external_exports.string().min(1, { message: "must not be empty" }).max(OBSERVED_PATH_MAX, { message: `must be at most ${OBSERVED_PATH_MAX} characters` });
16380
+ var observedPathListSchema = external_exports.array(observedPathSchema).max(OBSERVED_PATHS_LISTED_MAX, { message: `can list at most ${OBSERVED_PATHS_LISTED_MAX} paths` }).refine((paths) => new Set(paths).size === paths.length, { message: "must name each path once" });
16381
+ var observedMethodFilesSchema = external_exports.object({
16382
+ added: observedCountSchema,
16383
+ changed: observedCountSchema,
16384
+ removed: observedCountSchema
16029
16385
  }).strict();
16030
- var scopeSentBackSchema = external_exports.object({
16031
- phase: scopePhaseSchema,
16032
- number: external_exports.number().int().min(1),
16033
- notes: external_exports.array(scopeNoteEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX + 1)
16034
- }).strict().superRefine((entry, ctx) => {
16035
- entry.notes.forEach((n, i) => {
16036
- if (n.part === void 0 && i !== entry.notes.length - 1) {
16037
- ctx.addIssue({
16038
- code: external_exports.ZodIssueCode.custom,
16039
- path: ["notes", i],
16040
- message: "the note on the whole phase comes after every note on a part"
16041
- });
16386
+ var observedFilesSchema = external_exports.object({
16387
+ added: observedCountSchema,
16388
+ changed: observedCountSchema,
16389
+ removed: observedCountSchema,
16390
+ testFilesAdded: observedCountSchema
16391
+ }).strict().refine((v) => v.testFilesAdded <= v.added, {
16392
+ message: "cannot be more than the files added",
16393
+ path: ["testFilesAdded"]
16394
+ });
16395
+ function filesChanged(files) {
16396
+ return files.added + files.changed + files.removed;
16397
+ }
16398
+ function pathsFitTheCount(files, paths) {
16399
+ return files === null ? paths.length === 0 : paths.length <= filesChanged(files);
16400
+ }
16401
+ function checkPartsUnique(parts, ctx, at) {
16402
+ const seen = /* @__PURE__ */ new Set();
16403
+ parts.forEach((entry, i) => {
16404
+ if (seen.has(entry.part)) {
16405
+ ctx.addIssue({ code: "custom", path: [...at, i, "part"], message: `names the part "${entry.part}" twice` });
16042
16406
  }
16407
+ seen.add(entry.part);
16408
+ });
16409
+ }
16410
+ var observedBasisCountSchema = external_exports.object({
16411
+ outcome: external_exports.literal("count"),
16412
+ phase: phaseNameSchema,
16413
+ startCommit: closeCommitSchema,
16414
+ descriptionVersion: observedDescriptionVersionSchema,
16415
+ description: systemDescriptionSchema
16416
+ }).strict();
16417
+ var observedBasisCannotCountSchema = external_exports.object({
16418
+ outcome: external_exports.literal("cannot-count"),
16419
+ phase: phaseNameSchema,
16420
+ reason: external_exports.literal("no-start-commit")
16421
+ }).strict();
16422
+ var observedBasisNothingToCountSchema = external_exports.object({
16423
+ outcome: external_exports.literal("nothing-to-count"),
16424
+ reason: external_exports.enum(["no-open-phase", "no-description"])
16425
+ }).strict();
16426
+ var observedBasisSchema = external_exports.discriminatedUnion("outcome", [
16427
+ observedBasisCountSchema,
16428
+ observedBasisCannotCountSchema,
16429
+ observedBasisNothingToCountSchema
16430
+ ]);
16431
+ var observedReportPartSchema = external_exports.object({
16432
+ part: systemPartIdSchema,
16433
+ added: observedCountSchema,
16434
+ changed: observedCountSchema,
16435
+ removed: observedCountSchema,
16436
+ testFilesAdded: observedCountSchema
16437
+ }).strict().refine((v) => v.testFilesAdded <= v.added, {
16438
+ message: "cannot be more than the files added",
16439
+ path: ["testFilesAdded"]
16440
+ }).refine((v) => filesChanged(v) >= 1, {
16441
+ message: "a part is listed only when at least one file changed under it",
16442
+ path: ["added"]
16443
+ });
16444
+ var observedReportOutsideSchema = external_exports.object({
16445
+ added: observedCountSchema,
16446
+ changed: observedCountSchema,
16447
+ removed: observedCountSchema,
16448
+ testFilesAdded: observedCountSchema,
16449
+ paths: observedPathListSchema
16450
+ }).strict().refine((v) => v.testFilesAdded <= v.added, {
16451
+ message: "cannot be more than the files added",
16452
+ path: ["testFilesAdded"]
16453
+ }).refine((v) => pathsFitTheCount(v, v.paths), {
16454
+ message: "lists more paths than files changed",
16455
+ path: ["paths"]
16456
+ });
16457
+ var observedChangesReportSchema = external_exports.object({
16458
+ phase: phaseNameSchema,
16459
+ startCommit: closeCommitSchema,
16460
+ headCommit: closeCommitSchema,
16461
+ headCommittedAt: utcIso8601,
16462
+ commits: observedCountSchema,
16463
+ descriptionVersion: observedDescriptionVersionSchema,
16464
+ parts: external_exports.array(observedReportPartSchema).max(SYSTEM_PARTS_MAX, { message: `can list at most ${SYSTEM_PARTS_MAX} parts` }),
16465
+ methodFiles: observedMethodFilesSchema,
16466
+ outsideEveryPart: observedReportOutsideSchema,
16467
+ codeLink: codeLinkFactsSchema
16468
+ }).strict().superRefine((v, ctx) => checkPartsUnique(v.parts, ctx, ["parts"]));
16469
+ var observedSaidSchema = external_exports.object({
16470
+ title: buildTaskSchema.shape.title,
16471
+ agent: buildTaskSchema.shape.agent,
16472
+ whyOutsidePlan: buildTaskSchema.shape.whyOutsidePlan
16473
+ }).strict();
16474
+ var observedNotInPlanSchema = external_exports.object({
16475
+ part: systemPartIdSchema,
16476
+ name: systemPartSchema.shape.name,
16477
+ said: external_exports.array(observedSaidSchema).max(BUILD_SNAPSHOT_TASK_LIMIT)
16478
+ }).strict();
16479
+ var observedChangesRecordedSchema = external_exports.object({
16480
+ outcome: external_exports.literal("recorded"),
16481
+ notInPlan: external_exports.array(observedNotInPlanSchema).max(SYSTEM_PARTS_MAX).superRefine((parts, ctx) => checkPartsUnique(parts, ctx, [])),
16482
+ outside: external_exports.object({
16483
+ files: observedCountSchema,
16484
+ example: observedPathSchema.nullable()
16485
+ }).strict().refine((v) => v.example === null || v.files >= 1, {
16486
+ message: "names an example when no file changed",
16487
+ path: ["example"]
16488
+ })
16489
+ }).strict();
16490
+ var observedChangesKeptSchema = external_exports.object({
16491
+ outcome: external_exports.literal("kept")
16492
+ }).strict();
16493
+ var observedChangesReportResultSchema = external_exports.discriminatedUnion("outcome", [
16494
+ observedChangesRecordedSchema,
16495
+ observedChangesKeptSchema
16496
+ ]);
16497
+ var OBSERVE_NOT_SENT_REASONS = [
16498
+ "not-configured",
16499
+ "nothing-to-count",
16500
+ "cannot-count",
16501
+ "start-not-here",
16502
+ "start-not-on-branch",
16503
+ "basis-moved",
16504
+ "refused",
16505
+ "unreachable"
16506
+ ];
16507
+ var observeRunRecordedSchema = external_exports.object({ outcome: external_exports.literal("recorded") }).strict();
16508
+ var observeRunKeptSchema = external_exports.object({ outcome: external_exports.literal("kept") }).strict();
16509
+ var observeRunNotSentSchema = external_exports.object({
16510
+ outcome: external_exports.literal("not-sent"),
16511
+ reason: external_exports.enum(OBSERVE_NOT_SENT_REASONS)
16512
+ }).strict();
16513
+ var observeRunResultSchema = external_exports.discriminatedUnion("outcome", [
16514
+ observeRunRecordedSchema,
16515
+ observeRunKeptSchema,
16516
+ observeRunNotSentSchema
16517
+ ]);
16518
+ var observedChecksFiredSchema = external_exports.array(external_exports.string().min(1)).refine((names) => names.every((name, i) => i === 0 || names[i - 1] < name), {
16519
+ message: "must be sorted, each name once"
16520
+ });
16521
+ var observedCountingSchema = external_exports.discriminatedUnion("state", [
16522
+ external_exports.object({
16523
+ state: external_exports.literal("counted"),
16524
+ startCommit: closeCommitSchema,
16525
+ startRecordedAt: utcIso8601,
16526
+ headCommit: closeCommitSchema,
16527
+ headCommittedAt: utcIso8601,
16528
+ commits: observedCountSchema,
16529
+ countedAt: utcIso8601
16530
+ }).strict(),
16531
+ external_exports.object({
16532
+ state: external_exports.literal("not-counted-yet"),
16533
+ startCommit: closeCommitSchema,
16534
+ startRecordedAt: utcIso8601
16535
+ }).strict(),
16536
+ external_exports.object({
16537
+ state: external_exports.literal("cannot-count")
16538
+ }).strict()
16539
+ ]);
16540
+ var observedPartSchema = external_exports.object({
16541
+ part: systemPartIdSchema,
16542
+ files: observedFilesSchema.nullable(),
16543
+ checksFired: observedChecksFiredSchema
16544
+ }).strict().refine((v) => v.files !== null && filesChanged(v.files) >= 1 || v.checksFired.length >= 1, {
16545
+ message: "a part is listed only when files changed under it or a check fired on one",
16546
+ path: ["files"]
16547
+ });
16548
+ var observedOutsideSchema = external_exports.object({
16549
+ files: observedFilesSchema.nullable(),
16550
+ paths: observedPathListSchema,
16551
+ checksFired: observedChecksFiredSchema
16552
+ }).strict().refine((v) => pathsFitTheCount(v.files, v.paths), {
16553
+ message: "lists more paths than files changed",
16554
+ path: ["paths"]
16555
+ });
16556
+ var observedChangesSchema = external_exports.object({
16557
+ phase: phaseNameSchema,
16558
+ counting: observedCountingSchema,
16559
+ descriptionVersion: observedDescriptionVersionSchema.nullable(),
16560
+ parts: external_exports.array(observedPartSchema).max(SYSTEM_PARTS_MAX),
16561
+ methodFiles: observedMethodFilesSchema.nullable(),
16562
+ outsideEveryPart: observedOutsideSchema,
16563
+ codeLink: codeLinkFactsSchema.nullable()
16564
+ }).strict().superRefine((v, ctx) => {
16565
+ checkPartsUnique(v.parts, ctx, ["parts"]);
16566
+ const counted = v.counting.state === "counted";
16567
+ const rule = counted ? "must be present when the phase is counted" : "must be null until the phase is counted";
16568
+ if (v.methodFiles !== null !== counted)
16569
+ ctx.addIssue({ code: "custom", path: ["methodFiles"], message: rule });
16570
+ if (v.outsideEveryPart.files !== null !== counted) {
16571
+ ctx.addIssue({ code: "custom", path: ["outsideEveryPart", "files"], message: rule });
16572
+ }
16573
+ if (v.codeLink !== null !== counted)
16574
+ ctx.addIssue({ code: "custom", path: ["codeLink"], message: rule });
16575
+ v.parts.forEach((entry, i) => {
16576
+ if (entry.files !== null !== counted)
16577
+ ctx.addIssue({ code: "custom", path: ["parts", i, "files"], message: rule });
16043
16578
  });
16579
+ if (counted && v.descriptionVersion === null) {
16580
+ ctx.addIssue({ code: "custom", path: ["descriptionVersion"], message: "must name the version the counts used" });
16581
+ }
16044
16582
  });
16045
- var scopeWaitingSchema = external_exports.object({
16046
- phase: scopePhaseSchema,
16047
- number: external_exports.number().int().min(1),
16048
- changedParts: external_exports.array(scopePartNameSchema).max(PHASE_PLAN_CHANGES_MAX)
16583
+ var partOutsideThePlanSchema = external_exports.object({
16584
+ part: systemPartIdSchema,
16585
+ name: systemPartSchema.shape.name,
16586
+ seen: observedFilesSchema.nullable(),
16587
+ tasks: external_exports.array(observedSaidSchema.extend({ id: buildTaskSchema.shape.id }).strict()).max(BUILD_SNAPSHOT_TASK_LIMIT)
16049
16588
  }).strict();
16050
- var scopeNoticesSchema = external_exports.object({
16051
- sentBack: external_exports.array(scopeSentBackSchema),
16052
- waiting: external_exports.array(scopeWaitingSchema),
16053
- typedApprovalOffered: external_exports.boolean()
16054
- }).strict().refine((v) => v.sentBack.length > 0 || v.waiting.length > 0, {
16055
- message: "carried only when a phase was sent back or waits for the owner"
16589
+ var NO_TASK_SAID_LINE = "No task said it changed this part.";
16590
+ function agentSaid(task) {
16591
+ const { title, agent, whyOutsidePlan } = task;
16592
+ if (whyOutsidePlan === void 0)
16593
+ return `"${title}" said it built this part, and gave no reason.`;
16594
+ if (agent === HUMAN_AGENT)
16595
+ return `The person on "${title}" said: "${whyOutsidePlan}"`;
16596
+ if (agent === void 0)
16597
+ return `The agent on "${title}" said: "${whyOutsidePlan}"`;
16598
+ return `${agent} said, on "${title}": "${whyOutsidePlan}"`;
16599
+ }
16600
+
16601
+ // ../events/dist/phase-review.js
16602
+ var BUILT_PART_ACTS = Object.freeze(["accept", "question", "send-back"]);
16603
+ var NOT_BUILT_PART_ACTS = Object.freeze(["carry", "drop"]);
16604
+ var REVIEW_ACTS = Object.freeze([...BUILT_PART_ACTS, ...NOT_BUILT_PART_ACTS]);
16605
+ var reviewActSchema = external_exports.enum(REVIEW_ACTS);
16606
+ function reviewActsFor(built) {
16607
+ return built ? BUILT_PART_ACTS : NOT_BUILT_PART_ACTS;
16608
+ }
16609
+ var REVIEW_NOTE_MAX = SCOPE_NOTE_MAX;
16610
+ var reviewNoteWireSchema = external_exports.string().max(REVIEW_NOTE_MAX);
16611
+ var reviewNoteSchema = external_exports.string().min(1).max(REVIEW_NOTE_MAX);
16612
+ var REVIEW_ANSWER_MAX = 1e3;
16613
+ var reviewAgentAnswerSchema = external_exports.string().min(1).max(REVIEW_ANSWER_MAX);
16614
+ var REVIEW_PARTS_MAX = PHASE_PLAN_CHANGES_MAX * 2;
16615
+ var REVIEW_PART_CHANGES = Object.freeze([...PHASE_PLAN_CHANGE_KINDS, "not-in-plan"]);
16616
+ var reviewPartChangeSchema = external_exports.enum(REVIEW_PART_CHANGES);
16617
+ var REVIEW_DECISIONS = Object.freeze(["accepted", "carried", "dropped"]);
16618
+ var reviewDecisionSchema = external_exports.enum(REVIEW_DECISIONS);
16619
+ function actOf(decision) {
16620
+ if (decision === "accepted")
16621
+ return "accept";
16622
+ if (decision === "carried")
16623
+ return "carry";
16624
+ return "drop";
16625
+ }
16626
+ var reviewCarriedToSchema = carriedPhaseSchema.nullable();
16627
+ var reviewStepNameSchema = stepNameSchema.refine((v) => !stepIdSchema.safeParse(v.trim()).success, {
16628
+ message: "must be the step's plain name"
16056
16629
  });
16057
- function quotedParts(parts) {
16058
- return parts.map((p) => `"${p}"`).join(", ");
16630
+ var reviewStepSchema = external_exports.object({
16631
+ name: reviewStepNameSchema.nullable(),
16632
+ state: unfinishedStepSchema.shape.state,
16633
+ judged: external_exports.boolean()
16634
+ }).strict();
16635
+ var UNNAMED_STEP = "a step of this phase";
16636
+ function stepCalled(step) {
16637
+ return step.name === null ? UNNAMED_STEP : `"${step.name}"`;
16059
16638
  }
16060
- function approvalSentence(phase, offerTyping) {
16061
- if (offerTyping === "off")
16062
- return TYPED_APPROVAL_OFF_SENTENCE;
16063
- if (offerTyping === "update")
16064
- return CLIENT_UPDATE_SENTENCE;
16065
- return `In their own Claude Code session, they ask to see the plan, check each part, and then type: approve ${phase}`;
16639
+ function sentenceStart(text) {
16640
+ return text.charAt(0).toUpperCase() + text.slice(1);
16066
16641
  }
16067
- function sentBackNotice(entry) {
16068
- return [
16069
- `Before anything else: the project's owner sent the scope of phase ${entry.number}, "${entry.phase}", back with these notes.`,
16070
- ...entry.notes.map((n) => n.part === void 0 ? `- ${n.note}` : `- "${n.part}": ${n.note}`),
16071
- `Change the plan to answer every note, then send it again with \`${HALFCYCLE_COMMAND} plan ${quoteForShell(entry.phase)}\`.`
16072
- ].join("\n");
16642
+ function unfinishedStepLabel(step) {
16643
+ const state = step.state === "parked" ? "parked" : "attempted, and did not finish";
16644
+ return `${stepCalled(step)} (${state})`;
16073
16645
  }
16074
- function waitingNotice(entry, offerTyping) {
16075
- return [
16076
- ...entry.changedParts.length > 0 ? [`These parts changed since the owner last decided on this phase, and wait for their approval: ${quotedParts(entry.changedParts)}.`] : [],
16077
- `The scope of phase ${entry.number}, "${entry.phase}", is waiting for the project's owner to approve it.`,
16078
- approvalSentence(entry.phase, offerTyping)
16079
- ].join(" ");
16646
+ function unfinishedStepLine(step) {
16647
+ if (step.judged) {
16648
+ return `${sentenceStart(stepCalled(step))} is judged by Halfcycle and has not passed in this phase: run that review again in your coding session until it passes.`;
16649
+ }
16650
+ return `Finish ${stepCalled(step)} in your coding session, or record it as declined.`;
16080
16651
  }
16081
- function scopeNoticeText(scope, options) {
16082
- const omit2 = options.omitActivePhase === true ? options.activePhase : void 0;
16083
- return {
16084
- sentBack: scope.sentBack.map(sentBackNotice),
16085
- waiting: scope.waiting.filter((w) => omit2 === void 0 || w.phase !== omit2).map((w) => waitingNotice(w, options.offerTyping))
16086
- };
16652
+ var openReviewRequestSchema = external_exports.object({
16653
+ actor: closeActorSchema,
16654
+ acceptanceOutcome: acceptanceOutcomeSchema,
16655
+ statements: closeStatementsSchema
16656
+ }).strict();
16657
+ var REVIEW_OPEN_TASKS_SHOWN_MAX = 10;
16658
+ var reviewOpenedSchema = external_exports.object({
16659
+ outcome: external_exports.literal("opened"),
16660
+ round: external_exports.number().int().min(1),
16661
+ parts: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
16662
+ notBuilt: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
16663
+ counted: external_exports.boolean(),
16664
+ typedReviewOffered: external_exports.boolean()
16665
+ }).strict().refine((v) => v.notBuilt <= v.parts, { message: "cannot exceed the parts in the round", path: ["notBuilt"] });
16666
+ var reviewTasksOpenSchema = external_exports.object({
16667
+ outcome: external_exports.literal("tasks-open"),
16668
+ open: external_exports.array(buildTaskSchema.shape.title).min(1).max(REVIEW_OPEN_TASKS_SHOWN_MAX),
16669
+ more: external_exports.number().int().min(0).max(BUILD_SNAPSHOT_TASK_LIMIT)
16670
+ }).strict();
16671
+ var reviewOutstandingSchema = external_exports.object({
16672
+ outcome: external_exports.literal("outstanding"),
16673
+ steps: external_exports.array(reviewStepSchema).min(1)
16674
+ }).strict();
16675
+ var reviewCleanOverFindingsSchema = external_exports.object({
16676
+ outcome: external_exports.literal("clean-over-findings"),
16677
+ findings: external_exports.number().int().min(1)
16678
+ }).strict();
16679
+ var reviewRefusedSchema = external_exports.object({
16680
+ outcome: external_exports.literal("refused"),
16681
+ problems: external_exports.array(systemDescriptionProblemSchema).min(1)
16682
+ }).strict();
16683
+ function bareReviewOutcome(outcome) {
16684
+ return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
16685
+ }
16686
+ var openReviewResultSchema = external_exports.discriminatedUnion("outcome", [
16687
+ reviewOpenedSchema,
16688
+ reviewTasksOpenSchema,
16689
+ bareReviewOutcome("no-build"),
16690
+ bareReviewOutcome("review-open"),
16691
+ reviewOutstandingSchema,
16692
+ reviewCleanOverFindingsSchema,
16693
+ reviewRefusedSchema,
16694
+ bareReviewOutcome("unavailable")
16695
+ ]);
16696
+ var reviewPhaseNotOpenSchema = external_exports.object({
16697
+ statusCode: external_exports.literal(409),
16698
+ error: external_exports.literal("PhaseNotOpen"),
16699
+ message: external_exports.string().min(1),
16700
+ openPhase: external_exports.string().nullable()
16701
+ }).strict();
16702
+ var openReviewAnswerSchema = external_exports.union([openReviewResultSchema, reviewPhaseNotOpenSchema]);
16703
+ var reviewDecisionEntrySchema = external_exports.object({
16704
+ part: systemPartIdSchema,
16705
+ act: reviewActSchema,
16706
+ note: reviewNoteWireSchema.optional()
16707
+ }).strict();
16708
+ var reviewDecisionRequestSchema = external_exports.object({
16709
+ reviewVersion: external_exports.number().int().min(0),
16710
+ decisions: external_exports.array(reviewDecisionEntrySchema).max(REVIEW_PARTS_MAX)
16711
+ }).strict();
16712
+ var reviewDecidedSchema = external_exports.object({
16713
+ part: systemPartIdSchema,
16714
+ name: systemPartSchema.shape.name,
16715
+ act: reviewActSchema,
16716
+ carriedTo: reviewCarriedToSchema.optional()
16717
+ }).strict().refine((v) => v.carriedTo !== void 0 === (v.act === "carry"), {
16718
+ message: "is present exactly on a carry",
16719
+ path: ["carriedTo"]
16720
+ });
16721
+ var reviewRecordedSchema = external_exports.object({
16722
+ outcome: external_exports.literal("recorded"),
16723
+ decided: external_exports.array(reviewDecidedSchema).max(REVIEW_PARTS_MAX),
16724
+ waiting: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
16725
+ closed: external_exports.boolean(),
16726
+ reviewVersion: external_exports.number().int().min(1)
16727
+ }).strict().superRefine((v, ctx) => {
16728
+ if (v.closed && v.waiting !== 0) {
16729
+ ctx.addIssue({ code: "custom", path: ["waiting"], message: "must be 0 once the phase is closed" });
16730
+ }
16731
+ if (v.decided.length === 0 && !v.closed) {
16732
+ ctx.addIssue({ code: "custom", path: ["decided"], message: "may be empty only when the phase closed" });
16733
+ }
16734
+ });
16735
+ var REVIEW_NOT_DECIDABLE_WHY = Object.freeze(["settled", "question-waiting", "sent-back"]);
16736
+ var reviewNotDecidablePartSchema = external_exports.object({
16737
+ part: systemPartIdSchema,
16738
+ name: systemPartSchema.shape.name,
16739
+ why: external_exports.enum(REVIEW_NOT_DECIDABLE_WHY)
16740
+ }).strict();
16741
+ var reviewNotDecidableSchema = external_exports.object({
16742
+ outcome: external_exports.literal("not-decidable"),
16743
+ parts: external_exports.array(reviewNotDecidablePartSchema).min(1).max(REVIEW_PARTS_MAX)
16744
+ }).strict();
16745
+ var reviewCloseRefusedSchema = external_exports.object({
16746
+ outcome: external_exports.literal("close-refused"),
16747
+ steps: external_exports.array(reviewStepSchema)
16748
+ }).strict();
16749
+ var reviewWalkChangedSchema = external_exports.object({
16750
+ outcome: external_exports.literal("walk-changed"),
16751
+ findings: external_exports.number().int().min(1)
16752
+ }).strict();
16753
+ var reviewDecisionResultSchema = external_exports.discriminatedUnion("outcome", [
16754
+ reviewRecordedSchema,
16755
+ bareReviewOutcome("review-changed"),
16756
+ bareReviewOutcome("no-review"),
16757
+ reviewNotDecidableSchema,
16758
+ reviewCloseRefusedSchema,
16759
+ reviewWalkChangedSchema,
16760
+ bareReviewOutcome("not-owner"),
16761
+ bareReviewOutcome("no-owner"),
16762
+ reviewRefusedSchema,
16763
+ bareReviewOutcome("unavailable")
16764
+ ]);
16765
+ var reviewSentBackSchema = external_exports.object({
16766
+ round: external_exports.number().int().min(1),
16767
+ note: reviewNoteSchema
16768
+ }).strict();
16769
+ var reviewQuestionSchema = external_exports.object({
16770
+ question: reviewNoteSchema,
16771
+ agentAnswer: reviewAgentAnswerSchema.optional()
16772
+ }).strict();
16773
+ var reviewTaskSchema = external_exports.object({
16774
+ title: buildTaskSchema.shape.title,
16775
+ state: buildTaskStateSchema
16776
+ }).strict();
16777
+ var reviewViewPartSchema = external_exports.object({
16778
+ part: systemPartIdSchema,
16779
+ name: systemPartSchema.shape.name,
16780
+ change: reviewPartChangeSchema,
16781
+ built: external_exports.boolean(),
16782
+ builtWithoutApproval: external_exports.boolean(),
16783
+ decidable: external_exports.boolean(),
16784
+ waiting: external_exports.boolean(),
16785
+ decision: reviewDecisionSchema.optional(),
16786
+ reason: reviewNoteSchema.optional(),
16787
+ carriedTo: reviewCarriedToSchema.optional(),
16788
+ sentBack: external_exports.array(reviewSentBackSchema),
16789
+ questions: external_exports.array(reviewQuestionSchema),
16790
+ tasks: external_exports.array(reviewTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
16791
+ said: external_exports.array(observedSaidSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
16792
+ codeLink: partCodeLinkSchema
16793
+ }).strict().superRefine((v, ctx) => {
16794
+ if (v.waiting !== (v.decision === void 0)) {
16795
+ ctx.addIssue({ code: "custom", path: ["waiting"], message: "must be true exactly when the part has no decision" });
16796
+ }
16797
+ if (v.decidable && !v.waiting) {
16798
+ ctx.addIssue({ code: "custom", path: ["decidable"], message: "cannot be true for a settled part" });
16799
+ }
16800
+ if (v.decision !== void 0 && !reviewActsFor(v.built).includes(actOf(v.decision))) {
16801
+ ctx.addIssue({ code: "custom", path: ["decision"], message: "is not an act this part takes" });
16802
+ }
16803
+ const carriedOrDropped = v.decision === "carried" || v.decision === "dropped";
16804
+ if (v.reason !== void 0 !== carriedOrDropped) {
16805
+ ctx.addIssue({ code: "custom", path: ["reason"], message: "is present exactly on a carry or a drop" });
16806
+ }
16807
+ if (v.carriedTo !== void 0 !== (v.decision === "carried")) {
16808
+ ctx.addIssue({ code: "custom", path: ["carriedTo"], message: "is present exactly on a carry" });
16809
+ }
16810
+ if (v.change === "not-in-plan" && !v.built) {
16811
+ ctx.addIssue({ code: "custom", path: ["built"], message: "is always true for a part outside the plan" });
16812
+ }
16813
+ });
16814
+ var reviewPhaseNumberSchema = external_exports.number().int().min(1).nullable();
16815
+ var phaseReviewNoneSchema = external_exports.object({
16816
+ state: external_exports.literal("none"),
16817
+ phase: scopePhaseSchema,
16818
+ number: reviewPhaseNumberSchema,
16819
+ closedWithoutReview: external_exports.boolean()
16820
+ }).strict();
16821
+ var reviewWalkSchema = external_exports.object({
16822
+ verdict: walkVerdictSchema,
16823
+ findings: external_exports.number().int().min(0)
16824
+ }).strict();
16825
+ function reviewedViewSchema(state) {
16826
+ return external_exports.object({
16827
+ state: external_exports.literal(state),
16828
+ phase: scopePhaseSchema,
16829
+ number: reviewPhaseNumberSchema,
16830
+ cycle: external_exports.number().int().min(0),
16831
+ round: external_exports.number().int().min(1),
16832
+ reviewVersion: external_exports.number().int().min(1),
16833
+ typedReviewOffered: external_exports.boolean(),
16834
+ counted: external_exports.boolean(),
16835
+ walk: reviewWalkSchema,
16836
+ parts: external_exports.array(reviewViewPartSchema).max(REVIEW_PARTS_MAX),
16837
+ observed: observedChangesSchema.nullable(),
16838
+ description: systemDescriptionSchema
16839
+ }).strict().superRefine((v, ctx) => {
16840
+ const seen = /* @__PURE__ */ new Set();
16841
+ v.parts.forEach((p, i) => {
16842
+ if (seen.has(p.part))
16843
+ ctx.addIssue({ code: "custom", path: ["parts", i, "part"], message: "is listed twice" });
16844
+ seen.add(p.part);
16845
+ });
16846
+ const counted = v.observed !== null && v.observed.counting.state === "counted";
16847
+ if (v.counted !== counted) {
16848
+ ctx.addIssue({ code: "custom", path: ["counted"], message: "must be true exactly when the counts are counted" });
16849
+ }
16850
+ if (v.observed !== null && v.observed.phase !== v.phase) {
16851
+ ctx.addIssue({ code: "custom", path: ["observed", "phase"], message: "must be the phase under review" });
16852
+ }
16853
+ if (v.state === "closed") {
16854
+ v.parts.forEach((p, i) => {
16855
+ if (p.decision === void 0) {
16856
+ ctx.addIssue({ code: "custom", path: ["parts", i, "decision"], message: "every part of a closed review is decided" });
16857
+ }
16858
+ });
16859
+ }
16860
+ });
16087
16861
  }
16862
+ var phaseReviewOpenSchema = reviewedViewSchema("open");
16863
+ var phaseReviewClosedSchema = reviewedViewSchema("closed");
16864
+ var phaseReviewViewSchema = external_exports.discriminatedUnion("state", [
16865
+ phaseReviewNoneSchema,
16866
+ phaseReviewOpenSchema,
16867
+ phaseReviewClosedSchema
16868
+ ]);
16869
+
16870
+ // ../events/dist/client-version.js
16871
+ var CLIENT_VERSION_HEADER = "x-halfcycle-client";
16872
+ var TYPED_APPROVAL_OFF_SENTENCE = "Approving by typing is switched off for now.";
16873
+ var TYPED_REVIEW_OFF_SENTENCE = "Reviewing by typing is switched off for now.";
16874
+ 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.`;
16088
16875
 
16089
16876
  // ../events/dist/device-auth.js
16090
16877
  var utcIso86012 = external_exports.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");
@@ -16185,11 +16972,126 @@ var DASHBOARD_URL = "https://app.halfcycle.ai";
16185
16972
  function projectPageUrl(engagementId) {
16186
16973
  return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
16187
16974
  }
16975
+ function systemViewUrl(engagementId, phase) {
16976
+ const page = projectPageUrl(engagementId);
16977
+ if (page === void 0)
16978
+ return void 0;
16979
+ return `${page}/console/system-view?${new URLSearchParams({ phase }).toString()}`;
16980
+ }
16188
16981
  function requestAccessSentence(engagementId) {
16189
16982
  const page = projectPageUrl(engagementId) ?? "this project's page on Halfcycle";
16190
16983
  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.`;
16191
16984
  }
16192
16985
 
16986
+ // ../events/dist/scope-notices.js
16987
+ var scopePartNameSchema = external_exports.string().min(1).max(SYSTEM_PART_NAME_MAX);
16988
+ var scopeNoteEntrySchema = external_exports.object({
16989
+ part: scopePartNameSchema.optional(),
16990
+ note: external_exports.string().min(1).max(SCOPE_NOTE_MAX)
16991
+ }).strict();
16992
+ var scopeSentBackSchema = external_exports.object({
16993
+ phase: scopePhaseSchema,
16994
+ number: external_exports.number().int().min(1),
16995
+ notes: external_exports.array(scopeNoteEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX + 1)
16996
+ }).strict().superRefine((entry, ctx) => {
16997
+ entry.notes.forEach((n, i) => {
16998
+ if (n.part === void 0 && i !== entry.notes.length - 1) {
16999
+ ctx.addIssue({
17000
+ code: external_exports.ZodIssueCode.custom,
17001
+ path: ["notes", i],
17002
+ message: "the note on the whole phase comes after every note on a part"
17003
+ });
17004
+ }
17005
+ });
17006
+ });
17007
+ var scopeWaitingSchema = external_exports.object({
17008
+ phase: scopePhaseSchema,
17009
+ number: external_exports.number().int().min(1),
17010
+ changedParts: external_exports.array(scopePartNameSchema).max(PHASE_PLAN_CHANGES_MAX)
17011
+ }).strict();
17012
+ var scopeNoticesSchema = external_exports.object({
17013
+ sentBack: external_exports.array(scopeSentBackSchema),
17014
+ waiting: external_exports.array(scopeWaitingSchema),
17015
+ typedApprovalOffered: external_exports.boolean()
17016
+ }).strict().refine((v) => v.sentBack.length > 0 || v.waiting.length > 0, {
17017
+ message: "carried only when a phase was sent back or waits for the owner"
17018
+ });
17019
+ function quotedParts(parts) {
17020
+ return parts.map((p) => `"${p}"`).join(", ");
17021
+ }
17022
+ function approvalSentence(phase, offerTyping) {
17023
+ if (offerTyping === "off")
17024
+ return TYPED_APPROVAL_OFF_SENTENCE;
17025
+ if (offerTyping === "update")
17026
+ return CLIENT_UPDATE_SENTENCE;
17027
+ return `In their own Claude Code session, they ask to see the plan, check each part, and then type: approve ${phase}`;
17028
+ }
17029
+ function phaseWords(phase) {
17030
+ return `phase "${phase}"`;
17031
+ }
17032
+ function sentBackNotice(entry) {
17033
+ return [
17034
+ `Before anything else: the project's owner sent the scope of ${phaseWords(entry.phase)} back with these notes.`,
17035
+ ...entry.notes.map((n) => n.part === void 0 ? `- ${n.note}` : `- "${n.part}": ${n.note}`),
17036
+ `Change the plan to answer every note, then send it again: say "plan the next phase" to start. It then waits for the owner's approval again.`
17037
+ ].join("\n");
17038
+ }
17039
+ function systemViewSentence(offerTyping, address) {
17040
+ if (address === void 0)
17041
+ return [];
17042
+ return [offerTyping === "yes" ? `Or they approve it in the System view: ${address}` : `They approve it in the System view: ${address}`];
17043
+ }
17044
+ function waitingNotice(entry, offerTyping, engagementId) {
17045
+ return [
17046
+ ...entry.changedParts.length > 0 ? [`These parts changed since the owner last decided on this phase, and wait for their approval: ${quotedParts(entry.changedParts)}.`] : [],
17047
+ `The scope of ${phaseWords(entry.phase)} is waiting for the project's owner to approve it.`,
17048
+ approvalSentence(entry.phase, offerTyping),
17049
+ ...systemViewSentence(offerTyping, systemViewUrl(engagementId, entry.phase))
17050
+ ].join(" ");
17051
+ }
17052
+ function scopeNoticeText(scope, options) {
17053
+ const omit2 = options.omitActivePhase === true ? options.activePhase : void 0;
17054
+ return {
17055
+ sentBack: scope.sentBack.map(sentBackNotice),
17056
+ waiting: scope.waiting.filter((w) => omit2 === void 0 || w.phase !== omit2).map((w) => waitingNotice(w, options.offerTyping, options.engagementId))
17057
+ };
17058
+ }
17059
+
17060
+ // ../events/dist/review-notices.js
17061
+ var reviewNoticeQuestionSchema = external_exports.object({
17062
+ phase: scopePhaseSchema,
17063
+ number: reviewPhaseNumberSchema,
17064
+ part: systemPartIdSchema,
17065
+ partName: systemPartSchema.shape.name,
17066
+ question: reviewNoteSchema
17067
+ }).strict();
17068
+ var reviewNoticeSentBackSchema = external_exports.object({
17069
+ phase: scopePhaseSchema,
17070
+ number: reviewPhaseNumberSchema,
17071
+ part: systemPartIdSchema,
17072
+ partName: systemPartSchema.shape.name,
17073
+ note: reviewNoteSchema
17074
+ }).strict();
17075
+ var reviewNoticesSchema = external_exports.object({
17076
+ questions: external_exports.array(reviewNoticeQuestionSchema).max(REVIEW_PARTS_MAX),
17077
+ sentBack: external_exports.array(reviewNoticeSentBackSchema).max(REVIEW_PARTS_MAX)
17078
+ }).strict().refine((v) => v.questions.length > 0 || v.sentBack.length > 0, {
17079
+ message: "carried only when a question waits or a part was sent back"
17080
+ });
17081
+ function phaseCalled(phase) {
17082
+ return `phase "${phase}"`;
17083
+ }
17084
+ function questionNotice(entry) {
17085
+ const answer = `${HALFCYCLE_COMMAND} review ${quoteForShell(entry.phase)} --answer ${quoteForShell(entry.part)} "<your answer>"`;
17086
+ return `Before anything else: the project's owner asked about "${entry.partName}" in the review of ${phaseCalled(entry.phase)}: ${entry.question} Answer it with ${answer}.`;
17087
+ }
17088
+ function sentBackNotice2(entry) {
17089
+ return `Before anything else: the project's owner sent "${entry.partName}" in ${phaseCalled(entry.phase)} back to building: ${entry.note} Rebuild it: add or reopen the tasks that build it. For each task that rebuilds it, add a task that reviews the rebuild, held by a different agent from the one that built it. When each review has passed, report the build with the time each task finished, then run the close step again.`;
17090
+ }
17091
+ function reviewNoticeText(review) {
17092
+ return [...review.questions.map(questionNotice), ...review.sentBack.map(sentBackNotice2)];
17093
+ }
17094
+
16193
17095
  // ../events/dist/credential-wire.js
16194
17096
  var engagementCredentialResponseSchema = external_exports.object({
16195
17097
  engagementId: external_exports.string(),
@@ -16242,7 +17144,7 @@ var chatScopeRequestSchema = external_exports.discriminatedUnion("act", [
16242
17144
  chatScopeApproveRequestSchema,
16243
17145
  chatScopeSendBackRequestSchema
16244
17146
  ]);
16245
- var CHAT_SCOPE_ACTS = ["approve", "send-back"];
17147
+ var CHAT_SCOPE_ACTS = SCOPE_ACTS;
16246
17148
  var chatScopeRecordedSchema = external_exports.object({
16247
17149
  outcome: external_exports.literal("recorded"),
16248
17150
  act: external_exports.enum(CHAT_SCOPE_ACTS),
@@ -16273,6 +17175,56 @@ var chatScopeResultSchema = external_exports.discriminatedUnion("outcome", [
16273
17175
  bareOutcome("unavailable")
16274
17176
  ]);
16275
17177
 
17178
+ // ../events/dist/review-chat.js
17179
+ var chatReviewAcceptRequestSchema = external_exports.object({
17180
+ act: external_exports.literal("accept"),
17181
+ reference: phaseIdentitySchema,
17182
+ reviewVersion: external_exports.number().int().min(0),
17183
+ parts: external_exports.array(systemPartIdSchema).max(REVIEW_PARTS_MAX)
17184
+ }).strict();
17185
+ function notedRequest(act) {
17186
+ return external_exports.object({
17187
+ act: external_exports.literal(act),
17188
+ reference: phaseIdentitySchema,
17189
+ reviewVersion: external_exports.number().int().min(0),
17190
+ part: systemPartIdSchema,
17191
+ note: reviewNoteWireSchema
17192
+ }).strict();
17193
+ }
17194
+ var chatReviewQuestionRequestSchema = notedRequest("question");
17195
+ var chatReviewSendBackRequestSchema = notedRequest("send-back");
17196
+ var chatReviewCarryRequestSchema = notedRequest("carry");
17197
+ var chatReviewDropRequestSchema = notedRequest("drop");
17198
+ var chatReviewRequestSchema = external_exports.discriminatedUnion("act", [
17199
+ chatReviewAcceptRequestSchema,
17200
+ chatReviewQuestionRequestSchema,
17201
+ chatReviewSendBackRequestSchema,
17202
+ chatReviewCarryRequestSchema,
17203
+ chatReviewDropRequestSchema
17204
+ ]);
17205
+ function bareOutcome2(outcome) {
17206
+ return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
17207
+ }
17208
+ var chatReviewResultSchema = external_exports.discriminatedUnion("outcome", [
17209
+ ...reviewDecisionResultSchema.options,
17210
+ bareOutcome2("show-first"),
17211
+ bareOutcome2("not-offered")
17212
+ ]);
17213
+ var reviewAnswerRequestSchema = external_exports.object({
17214
+ part: systemPartIdSchema,
17215
+ answer: statementText(REVIEW_ANSWER_MAX)
17216
+ }).strict();
17217
+ var reviewAnswerRecordedSchema = external_exports.object({
17218
+ outcome: external_exports.literal("recorded"),
17219
+ name: systemPartSchema.shape.name
17220
+ }).strict();
17221
+ var reviewAnswerResultSchema = external_exports.discriminatedUnion("outcome", [
17222
+ reviewAnswerRecordedSchema,
17223
+ bareOutcome2("no-question"),
17224
+ reviewRefusedSchema,
17225
+ bareOutcome2("unavailable")
17226
+ ]);
17227
+
16276
17228
  // dist/hook-protocol.js
16277
17229
  import { writeSync } from "node:fs";
16278
17230
  function emitBlock(reason) {
@@ -16332,8 +17284,6 @@ function emitNeverConfigured(exitCode, reason, engagementId) {
16332
17284
  writeSync(2, neverConfiguredReport(reason, void 0, engagementId));
16333
17285
  process.exitCode = exitCode;
16334
17286
  }
16335
- var SIGN_IN_COMMAND = '"npx halfcycle"';
16336
- var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
16337
17287
  function namesTheSignInCommand(text) {
16338
17288
  return text.includes(SIGN_IN_COMMAND);
16339
17289
  }
@@ -16660,13 +17610,16 @@ import { spawnSync } from "node:child_process";
16660
17610
  import { existsSync, readFileSync } from "node:fs";
16661
17611
  import { relative } from "node:path";
16662
17612
  var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
16663
- function git(args2, cwd, timeoutMs) {
17613
+ function spawnGit(args2, cwd, timeoutMs) {
16664
17614
  const argv = COLOR_AWARE_SUBCOMMANDS.has(args2[0] ?? "") ? [args2[0], "--no-color", ...args2.slice(1)] : args2;
16665
- const result = spawnSync("git", argv, {
17615
+ return spawnSync("git", argv, {
16666
17616
  cwd,
16667
17617
  encoding: "utf-8",
16668
17618
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
16669
17619
  });
17620
+ }
17621
+ function git(args2, cwd, timeoutMs) {
17622
+ const result = spawnGit(args2, cwd, timeoutMs);
16670
17623
  if (result.error) {
16671
17624
  const missing = result.error.code === "ENOENT" && existsSync(cwd);
16672
17625
  return { ok: false, message: `git error: ${result.error.message}`, ...missing ? { gitMissing: true } : {} };
@@ -16828,6 +17781,124 @@ function getWiringStatus(pathspecs, cwd) {
16828
17781
  ...pathspecs
16829
17782
  ], cwd, WIRING_STATUS_TIMEOUT_MS);
16830
17783
  }
17784
+ var OBSERVE_GIT_TIMEOUT_MS = 5e3;
17785
+ function isFullCommit(value) {
17786
+ return closeCommitSchema.safeParse(value).success;
17787
+ }
17788
+ function notACommit(name) {
17789
+ return { ok: false, message: `${name} is not a full commit id` };
17790
+ }
17791
+ function headCommit(repoRoot) {
17792
+ const result = git(["rev-parse", "--verify", "HEAD^{commit}"], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17793
+ if (!result.ok)
17794
+ return result;
17795
+ const commit = result.value.trim();
17796
+ return isFullCommit(commit) ? { ok: true, value: commit } : notACommit("HEAD");
17797
+ }
17798
+ function headCommittedAt(head, repoRoot) {
17799
+ if (!isFullCommit(head))
17800
+ return notACommit("head");
17801
+ const result = git(["log", "-1", "--format=%cI", head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17802
+ if (!result.ok)
17803
+ return result;
17804
+ const when = new Date(result.value.trim());
17805
+ if (Number.isNaN(when.getTime()))
17806
+ return { ok: false, message: "git gave no readable commit time" };
17807
+ const iso = when.toISOString();
17808
+ return utcIso8601.safeParse(iso).success ? { ok: true, value: iso } : { ok: false, message: "git gave no readable commit time" };
17809
+ }
17810
+ function isAncestor(start, head, repoRoot) {
17811
+ if (!isFullCommit(start))
17812
+ return notACommit("start");
17813
+ if (!isFullCommit(head))
17814
+ return notACommit("head");
17815
+ const result = spawnGit(["merge-base", "--is-ancestor", start, head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17816
+ if (result.error)
17817
+ return { ok: false, message: `git error: ${result.error.message}` };
17818
+ if (result.status === 0)
17819
+ return { ok: true, value: true };
17820
+ if (result.status === 1)
17821
+ return { ok: true, value: false };
17822
+ const stderr = result.stderr?.trim() ?? "";
17823
+ return { ok: false, message: `git merge-base failed: ${stderr || "non-zero exit"}` };
17824
+ }
17825
+ function commitCount(start, head, repoRoot) {
17826
+ if (!isFullCommit(start))
17827
+ return notACommit("start");
17828
+ if (!isFullCommit(head))
17829
+ return notACommit("head");
17830
+ const result = git(["rev-list", "--count", `${start}..${head}`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17831
+ if (!result.ok)
17832
+ return result;
17833
+ const text = result.value.trim();
17834
+ if (!/^\d+$/.test(text))
17835
+ return { ok: false, message: "git gave no readable commit count" };
17836
+ return { ok: true, value: Number(text) };
17837
+ }
17838
+ function rangeNameStatus(start, head, repoRoot) {
17839
+ if (!isFullCommit(start))
17840
+ return notACommit("start");
17841
+ if (!isFullCommit(head))
17842
+ return notACommit("head");
17843
+ const result = git(["diff", "--name-status", "--no-renames", "-z", start, head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17844
+ if (!result.ok)
17845
+ return result;
17846
+ return parseNameStatusZ(result.value);
17847
+ }
17848
+ function parseNameStatusZ(output) {
17849
+ if (output === "")
17850
+ return { ok: true, value: [] };
17851
+ if (!output.endsWith("\0"))
17852
+ return { ok: false, message: "git gave an unreadable list of changed files" };
17853
+ const fields = output.slice(0, -1).split("\0");
17854
+ if (fields.length % 2 !== 0)
17855
+ return { ok: false, message: "git gave an unreadable list of changed files" };
17856
+ const entries = [];
17857
+ for (let i = 0; i < fields.length; i += 2) {
17858
+ const status = fields[i];
17859
+ const path = fields[i + 1];
17860
+ if (!/^[A-Z][0-9]*$/.test(status) || status.startsWith("R") || status.startsWith("C") || path === "") {
17861
+ return { ok: false, message: "git gave an unreadable list of changed files" };
17862
+ }
17863
+ entries.push({ status: status.charAt(0), path });
17864
+ }
17865
+ return { ok: true, value: entries };
17866
+ }
17867
+ function readCodeLinkFacts(repoRoot, head) {
17868
+ const nothing = { repositoryAddress: null, headOnGitHub: false };
17869
+ if (!isFullCommit(head))
17870
+ return nothing;
17871
+ try {
17872
+ const remote = currentRemote(repoRoot);
17873
+ const url2 = git(["remote", "get-url", "--", remote], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17874
+ if (!url2.ok)
17875
+ return nothing;
17876
+ const repositoryAddress = githubRepositoryAddress(url2.value.trim());
17877
+ if (repositoryAddress === null)
17878
+ return nothing;
17879
+ const refs = git(["for-each-ref", "--contains", head, "--format=%(refname)", `refs/remotes/${remote}/`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17880
+ if (!refs.ok)
17881
+ return nothing;
17882
+ const facts = { repositoryAddress, headOnGitHub: refs.value.trim() !== "" };
17883
+ return codeLinkFactsSchema.safeParse(facts).success ? facts : nothing;
17884
+ } catch {
17885
+ return nothing;
17886
+ }
17887
+ }
17888
+ function currentRemote(repoRoot) {
17889
+ const symbolic = git(["symbolic-ref", "--quiet", "HEAD"], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17890
+ if (!symbolic.ok)
17891
+ return "origin";
17892
+ const ref = symbolic.value.trim();
17893
+ if (!ref.startsWith("refs/heads/"))
17894
+ return "origin";
17895
+ const branch = ref.slice("refs/heads/".length);
17896
+ const configured = git(["config", "--get", `branch.${branch}.remote`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17897
+ if (!configured.ok)
17898
+ return "origin";
17899
+ const remote = configured.value.trim();
17900
+ return remote === "" || remote === "." ? "origin" : remote;
17901
+ }
16831
17902
 
16832
17903
  // dist/screen/secret-shape.js
16833
17904
  function screenExcerpts(excerpts) {
@@ -18495,7 +19566,10 @@ var SESSION_NOTICE_SUFFIX = {
18495
19566
  "no-repository": ".norepo",
18496
19567
  "credential-rejected": ".credential",
18497
19568
  "credential-renewed-for-this-run": ".renewed",
18498
- "credential-missing": ".nocredential"
19569
+ "credential-missing": ".nocredential",
19570
+ "observe-no-start-here": ".observe-nostart",
19571
+ "observe-not-on-branch": ".observe-nobranch",
19572
+ "observe-no-start-recorded": ".observe-norecord"
18499
19573
  };
18500
19574
  function firstNoticeThisSession(notice, folder, sessionId) {
18501
19575
  if (!sessionId)
@@ -18590,6 +19664,14 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
18590
19664
  }
18591
19665
  }
18592
19666
  }
19667
+ function observeThrottlePath(folder) {
19668
+ return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}.observe-throttle`);
19669
+ }
19670
+ function observeNoticesPath(folder, sessionId) {
19671
+ if (!sessionId)
19672
+ return null;
19673
+ return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}.observe-said`);
19674
+ }
18593
19675
 
18594
19676
  // dist/seat-configuration.js
18595
19677
  import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync2 } from "node:fs";
@@ -18923,9 +20005,11 @@ function collectDiff(repoRoot, base) {
18923
20005
  getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
18924
20006
  };
18925
20007
  }
18926
- async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
20008
+ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration, afterPass) {
18927
20009
  const reply = hookReply(STOP_HOOK_EVENT[kind]);
18928
20010
  await evaluateSession(input, kind, reply, readSeat);
20011
+ if (afterPass)
20012
+ await afterPass(reply);
18929
20013
  reply.flush();
18930
20014
  }
18931
20015
  async function evaluateSession(input, kind, reply, readSeat) {
@@ -19106,17 +20190,328 @@ async function runSubagentStop() {
19106
20190
  await runSessionDiff(input, "subagent-stop");
19107
20191
  }
19108
20192
 
20193
+ // dist/observed.js
20194
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
20195
+ function emptyTally() {
20196
+ return { added: 0, changed: 0, removed: 0, testFilesAdded: 0 };
20197
+ }
20198
+ function kindOf(status) {
20199
+ if (status === "A")
20200
+ return "added";
20201
+ if (status === "D")
20202
+ return "removed";
20203
+ return "changed";
20204
+ }
20205
+ function countObservedChanges(entries, description) {
20206
+ const methodFiles = { added: 0, changed: 0, removed: 0 };
20207
+ const byPart = /* @__PURE__ */ new Map();
20208
+ const outside = emptyTally();
20209
+ const paths = [];
20210
+ const listed = /* @__PURE__ */ new Set();
20211
+ for (const entry of entries) {
20212
+ const kind = kindOf(entry.status);
20213
+ if (isMethodFile(entry.path)) {
20214
+ methodFiles[kind] += 1;
20215
+ continue;
20216
+ }
20217
+ const owner = ownerOf(entry.path, description);
20218
+ let tally;
20219
+ if (owner === null) {
20220
+ tally = outside;
20221
+ if (entry.path.length >= 1 && entry.path.length <= OBSERVED_PATH_MAX && paths.length < OBSERVED_PATHS_LISTED_MAX && !listed.has(entry.path)) {
20222
+ paths.push(entry.path);
20223
+ listed.add(entry.path);
20224
+ }
20225
+ } else {
20226
+ tally = byPart.get(owner) ?? emptyTally();
20227
+ byPart.set(owner, tally);
20228
+ }
20229
+ tally[kind] += 1;
20230
+ if (kind === "added" && isTestFile(entry.path))
20231
+ tally.testFilesAdded += 1;
20232
+ }
20233
+ const parts = description.parts.filter((part) => byPart.has(part.id)).map((part) => ({ part: part.id, ...byPart.get(part.id) }));
20234
+ return { parts, methodFiles, outsideEveryPart: { ...outside, paths } };
20235
+ }
20236
+ var OBSERVE_MIN_INTERVAL_MS = 6e4;
20237
+ var OBSERVE_RECHECK_MS = 6e5;
20238
+ var OBSERVE_REQUEST_DEADLINE_MS = 1500;
20239
+ async function requestJson(fetchFn, url2, init) {
20240
+ const controller = new AbortController();
20241
+ const timer = setTimeout(() => controller.abort(), OBSERVE_REQUEST_DEADLINE_MS);
20242
+ try {
20243
+ const response = await fetchFn(url2, { ...init, signal: controller.signal });
20244
+ let body = null;
20245
+ try {
20246
+ body = await response.json();
20247
+ } catch {
20248
+ body = null;
20249
+ }
20250
+ if (controller.signal.aborted)
20251
+ return { kind: "unreachable" };
20252
+ return { kind: "answer", status: response.status, body };
20253
+ } catch {
20254
+ return { kind: "unreachable" };
20255
+ } finally {
20256
+ clearTimeout(timer);
20257
+ }
20258
+ }
20259
+ function refusalReason(status) {
20260
+ return status >= 500 ? "unreachable" : "refused";
20261
+ }
20262
+ async function attemptObservation(opts) {
20263
+ const { repoRoot, head, config: config2, controlOrigin, fetchFn } = opts;
20264
+ const base = `${controlOrigin}/engagements/${encodeURIComponent(config2.guardEngagementId)}/observed`;
20265
+ const authorization = `Bearer ${config2.guardServiceToken}`;
20266
+ const basisAnswer = await requestJson(fetchFn, `${base}/basis`, {
20267
+ method: "GET",
20268
+ headers: { Authorization: authorization }
20269
+ });
20270
+ if (basisAnswer.kind === "unreachable")
20271
+ return { outcome: "not-sent", reason: "unreachable" };
20272
+ if (basisAnswer.status < 200 || basisAnswer.status >= 300) {
20273
+ return { outcome: "not-sent", reason: refusalReason(basisAnswer.status) };
20274
+ }
20275
+ const parsedBasis = observedBasisSchema.safeParse(basisAnswer.body);
20276
+ if (!parsedBasis.success)
20277
+ return { outcome: "not-sent", reason: "refused" };
20278
+ const basis = parsedBasis.data;
20279
+ if (basis.outcome === "nothing-to-count")
20280
+ return { outcome: "not-sent", reason: "nothing-to-count" };
20281
+ if (basis.outcome === "cannot-count")
20282
+ return { outcome: "not-sent", reason: "cannot-count", phase: basis.phase };
20283
+ if (repoRoot === null || !refExists(basis.startCommit, repoRoot)) {
20284
+ return { outcome: "not-sent", reason: "start-not-here" };
20285
+ }
20286
+ if (head === null)
20287
+ return { outcome: "not-sent", reason: "start-not-on-branch" };
20288
+ const ancestor = isAncestor(basis.startCommit, head, repoRoot);
20289
+ if (!ancestor.ok)
20290
+ return { outcome: "not-sent", reason: "unreachable" };
20291
+ if (!ancestor.value)
20292
+ return { outcome: "not-sent", reason: "start-not-on-branch" };
20293
+ const commits = commitCount(basis.startCommit, head, repoRoot);
20294
+ const committedAt = headCommittedAt(head, repoRoot);
20295
+ const entries = rangeNameStatus(basis.startCommit, head, repoRoot);
20296
+ if (!commits.ok || !committedAt.ok || !entries.ok)
20297
+ return { outcome: "not-sent", reason: "unreachable" };
20298
+ const report = {
20299
+ phase: basis.phase,
20300
+ startCommit: basis.startCommit,
20301
+ headCommit: head,
20302
+ headCommittedAt: committedAt.value,
20303
+ commits: commits.value,
20304
+ descriptionVersion: basis.descriptionVersion,
20305
+ ...countObservedChanges(entries.value, basis.description),
20306
+ codeLink: readCodeLinkFacts(repoRoot, head)
20307
+ };
20308
+ if (!observedChangesReportSchema.safeParse(report).success)
20309
+ return { outcome: "not-sent", reason: "refused" };
20310
+ const sent = await requestJson(fetchFn, base, {
20311
+ method: "POST",
20312
+ headers: { Authorization: authorization, "Content-Type": "application/json" },
20313
+ body: JSON.stringify(report)
20314
+ });
20315
+ if (sent.kind === "unreachable")
20316
+ return { outcome: "not-sent", reason: "unreachable" };
20317
+ if (sent.status === 409)
20318
+ return { outcome: "not-sent", reason: "basis-moved" };
20319
+ if (sent.status < 200 || sent.status >= 300)
20320
+ return { outcome: "not-sent", reason: refusalReason(sent.status) };
20321
+ const answer = observedChangesReportResultSchema.safeParse(sent.body);
20322
+ if (!answer.success)
20323
+ return { outcome: "not-sent", reason: "refused" };
20324
+ return answer.data.outcome === "kept" ? { outcome: "kept" } : { outcome: "recorded", answer: answer.data };
20325
+ }
20326
+ function readThrottle(folder) {
20327
+ try {
20328
+ const parsed = JSON.parse(readFileSync9(observeThrottlePath(folder), "utf-8"));
20329
+ if (typeof parsed !== "object" || parsed === null)
20330
+ return null;
20331
+ const { attemptedAt, accepted } = parsed;
20332
+ if (typeof attemptedAt !== "number")
20333
+ return null;
20334
+ const sent = accepted;
20335
+ const usable = sent !== null && sent !== void 0 && typeof sent.head === "string" && typeof sent.at === "number";
20336
+ return { attemptedAt, accepted: usable ? { head: sent.head, at: sent.at } : null };
20337
+ } catch {
20338
+ return null;
20339
+ }
20340
+ }
20341
+ function writeThrottle(folder, state) {
20342
+ try {
20343
+ writeFileSync4(observeThrottlePath(folder), JSON.stringify(state), "utf-8");
20344
+ } catch {
20345
+ }
20346
+ }
20347
+ function throttleAllows(state, head, now) {
20348
+ if (state === null)
20349
+ return true;
20350
+ if (now - state.attemptedAt < OBSERVE_MIN_INTERVAL_MS)
20351
+ return false;
20352
+ if (state.accepted === null)
20353
+ return true;
20354
+ if (head === null || head !== state.accepted.head)
20355
+ return true;
20356
+ return now - state.accepted.at >= OBSERVE_RECHECK_MS;
20357
+ }
20358
+ function locate(cwd) {
20359
+ const root = getRepoRoot(cwd);
20360
+ const repoRoot = root.ok ? root.value : null;
20361
+ const read = repoRoot === null ? null : headCommit(repoRoot);
20362
+ return { repoRoot, folder: repoRoot ?? cwd, head: read !== null && read.ok ? read.value : null };
20363
+ }
20364
+ function recordAttempt(folder, prior, head, at, attempt2) {
20365
+ const accepted = (attempt2.outcome === "recorded" || attempt2.outcome === "kept") && head !== null ? { head, at } : prior?.accepted ?? null;
20366
+ writeThrottle(folder, { attemptedAt: at, accepted });
20367
+ }
20368
+ async function runObserve(deps = {}) {
20369
+ let result;
20370
+ try {
20371
+ const configured = readConfig();
20372
+ if (!configured.config) {
20373
+ result = { outcome: "not-sent", reason: "not-configured" };
20374
+ } else {
20375
+ const now = deps.now ?? Date.now;
20376
+ const where = locate(deps.cwd ?? process.cwd());
20377
+ const prior = readThrottle(where.folder);
20378
+ const attempt2 = await attemptObservation({
20379
+ repoRoot: where.repoRoot,
20380
+ head: where.head,
20381
+ config: configured.config,
20382
+ controlOrigin: readControlOrigin().origin,
20383
+ fetchFn: deps.fetchFn ?? globalThis.fetch
20384
+ });
20385
+ recordAttempt(where.folder, prior, where.head, now(), attempt2);
20386
+ result = attempt2.outcome === "not-sent" ? { outcome: "not-sent", reason: attempt2.reason } : { outcome: attempt2.outcome };
20387
+ }
20388
+ } catch {
20389
+ result = { outcome: "not-sent", reason: "unreachable" };
20390
+ }
20391
+ return observeRunResultSchema.parse(result);
20392
+ }
20393
+ async function observeChanges(sessionId, reply, deps = {}) {
20394
+ try {
20395
+ const code = process.exitCode;
20396
+ if (code !== void 0 && code !== null && Number(code) !== 0)
20397
+ return;
20398
+ const configured = readConfig();
20399
+ if (!configured.config)
20400
+ return;
20401
+ const now = (deps.now ?? Date.now)();
20402
+ const where = locate(deps.cwd ?? process.cwd());
20403
+ const prior = readThrottle(where.folder);
20404
+ if (!throttleAllows(prior, where.head, now))
20405
+ return;
20406
+ writeThrottle(where.folder, { attemptedAt: now, accepted: prior?.accepted ?? null });
20407
+ const attempt2 = await attemptObservation({
20408
+ repoRoot: where.repoRoot,
20409
+ head: where.head,
20410
+ config: configured.config,
20411
+ controlOrigin: readControlOrigin().origin,
20412
+ fetchFn: deps.fetchFn ?? globalThis.fetch
20413
+ });
20414
+ recordAttempt(where.folder, prior, where.head, now, attempt2);
20415
+ for (const line2 of noticesFor(attempt2, where.folder, sessionId))
20416
+ reply.notice(line2);
20417
+ } catch {
20418
+ }
20419
+ }
20420
+ var R1_START_NOT_HERE = "Halfcycle cannot count what this phase has changed on this computer yet, because the commit the phase started from is not here. Get the project's latest history, and it counts again at the end of the next reply.";
20421
+ var R2_START_NOT_ON_BRANCH = "Halfcycle cannot count what this phase has changed on this branch, because the branch does not contain the commit the phase started from. Switch to the branch the phase is built on, and it counts again at the end of the next reply.";
20422
+ function r3NoStartRecorded(phase) {
20423
+ return `Halfcycle cannot count what phase "${phase}" changes, because no starting commit was recorded when it opened. To count from where the project is now, run: ${HALFCYCLE_COMMAND} open-phase ${quoteForShell(phase)} --actor "you" --evidence "<what shows this phase is ready to start>"`;
20424
+ }
20425
+ function r4OutsideThePlan(parts) {
20426
+ const first = parts.length === 1 ? "Halfcycle saw changes to a part this phase's plan does not include:" : "Halfcycle saw changes to parts this phase's plan does not include:";
20427
+ const lines = parts.map((entry) => {
20428
+ const said = entry.said.length === 0 ? NO_TASK_SAID_LINE : entry.said.map((task) => agentSaid(task)).join("; ");
20429
+ return `"${entry.name}": ${said}`;
20430
+ });
20431
+ return [first, ...lines, "The project's owner sees them again at the review."].join("\n");
20432
+ }
20433
+ function r5OutsideEveryPart(files, example) {
20434
+ if (files === 1) {
20435
+ return `Halfcycle saw 1 changed file that no part of the system owns: ${example}. If it belongs to a part, add it to that part in the description of the system.`;
20436
+ }
20437
+ return `Halfcycle saw ${files} changed files that no part of the system owns, such as ${example}. If they belong to a part, add them to that part in the description of the system.`;
20438
+ }
20439
+ function readSaid(path) {
20440
+ const empty = { parts: {}, outsideFiles: 0 };
20441
+ if (path === null)
20442
+ return empty;
20443
+ try {
20444
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
20445
+ const parts = {};
20446
+ if (typeof parsed.parts === "object" && parsed.parts !== null) {
20447
+ for (const [part, reasons] of Object.entries(parsed.parts)) {
20448
+ if (Array.isArray(reasons))
20449
+ parts[part] = reasons.filter((r) => typeof r === "string");
20450
+ }
20451
+ }
20452
+ return { parts, outsideFiles: typeof parsed.outsideFiles === "number" ? parsed.outsideFiles : 0 };
20453
+ } catch {
20454
+ return empty;
20455
+ }
20456
+ }
20457
+ function writeSaid(path, state) {
20458
+ if (path === null)
20459
+ return;
20460
+ try {
20461
+ writeFileSync4(path, JSON.stringify(state), "utf-8");
20462
+ } catch {
20463
+ }
20464
+ }
20465
+ function reasonsOf(entry) {
20466
+ return entry.said.filter((task) => task.whyOutsidePlan !== void 0).map((task) => `${task.title}\0${task.whyOutsidePlan}`);
20467
+ }
20468
+ function noticesFor(attempt2, folder, sessionId) {
20469
+ if (attempt2.outcome === "not-sent") {
20470
+ if (attempt2.reason === "start-not-here") {
20471
+ return firstNoticeThisSession("observe-no-start-here", folder, sessionId) ? [R1_START_NOT_HERE] : [];
20472
+ }
20473
+ if (attempt2.reason === "start-not-on-branch") {
20474
+ return firstNoticeThisSession("observe-not-on-branch", folder, sessionId) ? [R2_START_NOT_ON_BRANCH] : [];
20475
+ }
20476
+ if (attempt2.reason === "cannot-count" && attempt2.phase !== void 0) {
20477
+ return firstNoticeThisSession("observe-no-start-recorded", folder, sessionId) ? [r3NoStartRecorded(attempt2.phase)] : [];
20478
+ }
20479
+ return [];
20480
+ }
20481
+ if (attempt2.outcome === "kept")
20482
+ return [];
20483
+ const { notInPlan, outside } = attempt2.answer;
20484
+ const path = observeNoticesPath(folder, sessionId);
20485
+ const prior = readSaid(path);
20486
+ const notices = [];
20487
+ const parts = {};
20488
+ let grew = false;
20489
+ for (const entry of notInPlan) {
20490
+ const reasons = reasonsOf(entry);
20491
+ parts[entry.part] = reasons;
20492
+ const before = prior.parts[entry.part];
20493
+ if (before === void 0 || reasons.some((reason) => !before.includes(reason)))
20494
+ grew = true;
20495
+ }
20496
+ if (grew)
20497
+ notices.push(r4OutsideThePlan(notInPlan));
20498
+ let outsideFiles = outside.files;
20499
+ if (outside.files > prior.outsideFiles) {
20500
+ if (outside.example !== null)
20501
+ notices.push(r5OutsideEveryPart(outside.files, outside.example));
20502
+ else
20503
+ outsideFiles = prior.outsideFiles;
20504
+ }
20505
+ writeSaid(path, { parts, outsideFiles });
20506
+ return notices;
20507
+ }
20508
+
19109
20509
  // dist/hooks/stop.js
19110
- async function runStop() {
20510
+ async function runStop(observeDeps = {}) {
19111
20511
  const input = await readHookStdin();
19112
- await runSessionDiff(input, "stop");
20512
+ await runSessionDiff(input, "stop", readSeatConfiguration, (reply) => observeChanges(input.session_id, reply, observeDeps));
19113
20513
  }
19114
20514
 
19115
- // dist/hooks/user-prompt-submit.js
19116
- import { readFileSync as readFileSync9 } from "node:fs";
19117
- import { homedir as homedir4 } from "node:os";
19118
- import { join as join8 } from "node:path";
19119
-
19120
20515
  // dist/hooks/scope-phrase.js
19121
20516
  var APPROVE_PREFIX = /^approve /i;
19122
20517
  var SEND_BACK_PREFIX = /^send back /i;
@@ -19150,6 +20545,128 @@ function matchScopePhraseShape(shape, shown) {
19150
20545
  return { kind: "show-first", act: "send-back" };
19151
20546
  }
19152
20547
 
20548
+ // dist/hooks/review-phrase.js
20549
+ var VERBS = [
20550
+ [/^accept /i, "accept"],
20551
+ [/^question /i, "question"],
20552
+ [/^send back /i, "send-back"],
20553
+ [/^carry /i, "carry"],
20554
+ [/^drop /i, "drop"]
20555
+ ];
20556
+ function reviewPhraseShape(prompt) {
20557
+ if (typeof prompt !== "string")
20558
+ return null;
20559
+ const line2 = prompt.trim();
20560
+ if (LINE_BREAK.test(line2))
20561
+ return null;
20562
+ for (const [prefix, act] of VERBS) {
20563
+ const verb = prefix.exec(line2);
20564
+ if (verb === null)
20565
+ continue;
20566
+ const rest = line2.slice(verb[0].length);
20567
+ if (act === "accept") {
20568
+ if (!rest.includes("/"))
20569
+ return isValidPhaseIdentity(rest) ? { kind: "phase", reference: rest } : null;
20570
+ return partShape(act, rest, null);
20571
+ }
20572
+ const colon = rest.indexOf(":");
20573
+ if (colon === -1)
20574
+ return act === "send-back" ? partShape(act, rest, null) : null;
20575
+ return partShape(act, rest.slice(0, colon), rest.slice(colon + 1).trim());
20576
+ }
20577
+ return null;
20578
+ }
20579
+ function partShape(act, head, words) {
20580
+ const slash = head.indexOf("/");
20581
+ if (slash === -1)
20582
+ return null;
20583
+ const reference = head.slice(0, slash);
20584
+ const part = head.slice(slash + 1);
20585
+ if (!isValidPhaseIdentity(reference) || !SYSTEM_PART_ID_PATTERN.test(part))
20586
+ return null;
20587
+ return { kind: "part", act, reference, part, words };
20588
+ }
20589
+ function matchReviewPhraseShape(shape, reviews) {
20590
+ const entry = resolvePhaseReference(shape.reference, reviews);
20591
+ if (shape.kind === "phase") {
20592
+ if (entry === null)
20593
+ return { kind: "nothing" };
20594
+ return {
20595
+ kind: "accept",
20596
+ entry,
20597
+ reference: shape.reference,
20598
+ parts: entry.parts.filter((p) => p.built && p.decidable)
20599
+ };
20600
+ }
20601
+ if (entry === null)
20602
+ return { kind: "show-first", act: shape.act };
20603
+ const part = entry.parts.find((p) => p.part === shape.part);
20604
+ if (part === void 0)
20605
+ return { kind: "not-listed", act: shape.act, entry, part: shape.part };
20606
+ return { kind: "decide", act: shape.act, entry, reference: shape.reference, part, words: shape.words };
20607
+ }
20608
+
20609
+ // dist/hooks/shown-record.js
20610
+ import { chmodSync as chmodSync2, readFileSync as readFileSync10, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
20611
+ import { homedir as homedir4, platform as platform2 } from "node:os";
20612
+ import { join as join8 } from "node:path";
20613
+ function recordDir(engagementId, home) {
20614
+ if (!isEngagementId(engagementId))
20615
+ return null;
20616
+ return join8(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
20617
+ }
20618
+ function readShownRecord(engagementId, home) {
20619
+ const dir = recordDir(engagementId, home);
20620
+ if (dir === null)
20621
+ return { entries: [], reviews: [] };
20622
+ try {
20623
+ return parseShownRecord(readFileSync10(join8(dir, SHOWN_PLANS_FILENAME), "utf-8"));
20624
+ } catch {
20625
+ return { entries: [], reviews: [] };
20626
+ }
20627
+ }
20628
+ function movedPart(part, act) {
20629
+ if (act === void 0)
20630
+ return part;
20631
+ return {
20632
+ ...part,
20633
+ decidable: false,
20634
+ awaitingAnswer: part.awaitingAnswer || act === "question",
20635
+ sentBack: part.sentBack || act === "send-back"
20636
+ };
20637
+ }
20638
+ function moveListedReviewForward(engagementId, home, listed, reviewVersion, decided) {
20639
+ let temp = null;
20640
+ try {
20641
+ const dir = recordDir(engagementId, home);
20642
+ if (dir === null)
20643
+ return false;
20644
+ const path = join8(dir, SHOWN_PLANS_FILENAME);
20645
+ const current = parseShownRecord(readFileSync10(path, "utf-8"));
20646
+ const at = current.reviews.findIndex((r) => r.phase === listed.phase && r.shownAt === listed.shownAt && r.reviewVersion === listed.reviewVersion);
20647
+ if (at === -1)
20648
+ return false;
20649
+ const acts = new Map(decided.map((d) => [d.part, d.act]));
20650
+ const reviews = current.reviews.map((review, i) => i === at ? { ...review, reviewVersion, parts: review.parts.map((p) => movedPart(p, acts.get(p.part))) } : review);
20651
+ const body = serialiseShownRecord({ entries: current.entries, reviews });
20652
+ temp = join8(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
20653
+ writeFileSync5(temp, body, { mode: 384 });
20654
+ if (platform2() !== "win32")
20655
+ chmodSync2(temp, 384);
20656
+ renameSync3(temp, path);
20657
+ return true;
20658
+ } catch {
20659
+ if (temp !== null) {
20660
+ try {
20661
+ unlinkSync4(temp);
20662
+ } catch {
20663
+ return false;
20664
+ }
20665
+ }
20666
+ return false;
20667
+ }
20668
+ }
20669
+
19153
20670
  // dist/hooks/user-prompt-submit-lines.js
19154
20671
  var TELL_THE_PERSON = "Tell the person.";
19155
20672
  function line(text) {
@@ -19162,7 +20679,7 @@ function showCommand(phase) {
19162
20679
  return `\`${HALFCYCLE_COMMAND} plan ${quoteForShell(phase)} --show\``;
19163
20680
  }
19164
20681
  function recordedLine(act, phase, planVersion) {
19165
- return act === "approve" ? line(`Approved ${phase}, plan version ${planVersion}, in your session.`) : line(`Sent ${phase} back with your note, plan version ${planVersion}, in your session. The note is given first to the next session that asks what to do.`);
20682
+ return act === "approve" ? line(`Approved "${phase}", plan version ${planVersion}, in your session.`) : line(`Sent "${phase}" back with your note, plan version ${planVersion}, in your session. The note is given first to the next session that asks what to do.`);
19166
20683
  }
19167
20684
  function notAttendedLine(act) {
19168
20685
  return line(`${nothingDone(act)}: approving or sending back by typing counts only when a person types it into an open session.`);
@@ -19177,7 +20694,7 @@ function showFirstLine(act) {
19177
20694
  return line(`${nothingDone(act)}: that phase has not been listed here. List its parts first with \`${HALFCYCLE_COMMAND} plan <phase> --show\`, then type the line again.`);
19178
20695
  }
19179
20696
  function tooOldLine(act, phase) {
19180
- return line(`${nothingDone(act)}: ${phase} was listed an hour or more ago. List it again with ${showCommand(phase)}, then type the line again within the hour.`);
20697
+ return line(`${nothingDone(act)}: "${phase}" was listed an hour or more ago. List it again with ${showCommand(phase)}, then type the line again within the hour.`);
19181
20698
  }
19182
20699
  function noteNeededLine() {
19183
20700
  return line("Nothing was sent back: a send-back needs a note after the colon saying what should change.");
@@ -19204,11 +20721,11 @@ function lineForAnswer(act, phase, answer) {
19204
20721
  case "show-first":
19205
20722
  return showFirstLine(act);
19206
20723
  case "plan-changed":
19207
- return line(`${nothingDone(act)}: the plan for ${phase} changed after it was listed here. List it again with ${showCommand(phase)}, check the parts, then type the line again.`);
20724
+ return line(`${nothingDone(act)}: the plan for "${phase}" changed after it was listed here. List it again with ${showCommand(phase)}, check the parts, then type the line again.`);
19208
20725
  case "already-approved":
19209
- return answer.replanNeeded ? line(`${phase} is already approved, so nothing changed. Some parts wait because the description of the system changed since the plan was written: write the plan for ${phase} again, run ${showCommand(phase)}, then approve the new version.`) : line(`${phase} is already approved, so nothing changed.`);
20726
+ return answer.replanNeeded ? line(`"${phase}" is already approved, so nothing changed. Some parts wait because the description of the system changed since the plan was written: write the plan for "${phase}" again, run ${showCommand(phase)}, then approve the new version.`) : line(`"${phase}" is already approved, so nothing changed.`);
19210
20727
  case "build-started":
19211
- return line(`${nothingDone(act)}: tasks in ${phase} have started, so its scope can no longer be sent back. A built part goes back at the phase's review.`);
20728
+ return line(`${nothingDone(act)}: tasks in "${phase}" have started, so its scope can no longer be sent back. A built part goes back at the phase's review.`);
19212
20729
  case "not-owner":
19213
20730
  return notOwnerLine(act);
19214
20731
  case "no-owner":
@@ -19225,14 +20742,215 @@ function lineForAnswer(act, phase, answer) {
19225
20742
  }
19226
20743
  }
19227
20744
  }
20745
+ var NOTHING_RECORDED = "Nothing was recorded";
20746
+ var LIST_AGAIN = 'Say "review what was built" to list it again';
20747
+ function reviewNotAttendedLine() {
20748
+ return line(`${NOTHING_RECORDED}: reviewing by typing counts only when a person types it into an open session.`);
20749
+ }
20750
+ function reviewNotSignedInLine() {
20751
+ return line(`${NOTHING_RECORDED}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
20752
+ }
20753
+ function reviewCannotReachLine() {
20754
+ return line(`${NOTHING_RECORDED}: Halfcycle could not be reached just now. Type the line again in a minute.`);
20755
+ }
20756
+ function reviewSwitchedOffLine() {
20757
+ return line(`${NOTHING_RECORDED}. ${TYPED_REVIEW_OFF_SENTENCE}`);
20758
+ }
20759
+ function reviewNotOwnerLine() {
20760
+ return line(`${NOTHING_RECORDED}: only the project's owner can review this phase.`);
20761
+ }
20762
+ function reviewNoOwnerLine() {
20763
+ return line(`${NOTHING_RECORDED}: this project has no owner, so nobody can review its phases.`);
20764
+ }
20765
+ function reviewWrongActLine(name, built) {
20766
+ return line(built ? `${NOTHING_RECORDED}: "${name}" was built, so it can be accepted, questioned or sent back.` : `${NOTHING_RECORDED}: "${name}" was not built, so it can only be carried or dropped.`);
20767
+ }
20768
+ function wordsActNoun(act) {
20769
+ switch (act) {
20770
+ case "question":
20771
+ return "a question";
20772
+ case "send-back":
20773
+ return "a send-back";
20774
+ case "carry":
20775
+ return "a carry";
20776
+ case "drop":
20777
+ return "a drop";
20778
+ default: {
20779
+ const unreachable = act;
20780
+ return unreachable;
20781
+ }
20782
+ }
20783
+ }
20784
+ function reviewNoWordsLine(act) {
20785
+ return line(`${NOTHING_RECORDED}: ${wordsActNoun(act)} needs words after the colon.`);
20786
+ }
20787
+ function reviewWordsTooLongLine() {
20788
+ return line(`${NOTHING_RECORDED}: the words after the colon are too long. Shorten them and type the line again.`);
20789
+ }
20790
+ function reviewWordsLookSecretLine() {
20791
+ return line(`${NOTHING_RECORDED}: the words look like they hold a password or key, so they never left this computer. Type the line again without it.`);
20792
+ }
20793
+ function reviewShowFirstLine() {
20794
+ return line(`${NOTHING_RECORDED}: that review has not been listed here. Say "review what was built" to list it, then type the line again.`);
20795
+ }
20796
+ function namedPhase(phase) {
20797
+ return `"${phase}"`;
20798
+ }
20799
+ function reviewPartNotListedLine(part, phase) {
20800
+ return line(`${NOTHING_RECORDED}: "${part}" is not a part of the review of ${namedPhase(phase)} as it was listed here.`);
20801
+ }
20802
+ function reviewNothingToAcceptLine(phase) {
20803
+ return line(`${NOTHING_RECORDED}: no built part of ${namedPhase(phase)} waits for your decision.`);
20804
+ }
20805
+ function reviewListedTooLongAgoLine(phase) {
20806
+ return line(`${NOTHING_RECORDED}: ${namedPhase(phase)} was listed an hour or more ago. ${LIST_AGAIN}, then type the line again within the hour.`);
20807
+ }
20808
+ function reviewChangedLine(phase) {
20809
+ return line(`${NOTHING_RECORDED}: the review of ${namedPhase(phase)} changed after it was listed here. ${LIST_AGAIN}, then type the line again.`);
20810
+ }
20811
+ function reviewAlreadyDecidedLine(name) {
20812
+ return line(`${NOTHING_RECORDED}: "${name}" is already decided in this review.`);
20813
+ }
20814
+ function reviewAwaitingAnswerLine(name) {
20815
+ return line(`${NOTHING_RECORDED}: "${name}" is waiting for the agent's answer to your question. Once it is in, say "review what was built" to list it again and decide the part.`);
20816
+ }
20817
+ function reviewSentBackLine(name) {
20818
+ return line(`${NOTHING_RECORDED}: "${name}" was sent back to building in this round, so it waits for the next round.`);
20819
+ }
20820
+ function reviewWalkChangedLine(phase, number4) {
20821
+ const typed = typedPhaseReference(phase, number4);
20822
+ return line(`${NOTHING_RECORDED}: phase ${namedPhase(phase)} cannot close, because its walk was recorded as clean and a defect is now recorded against it. Send the part with the defect back with send back ${typed}/<part>: <what is wrong>; when its tasks are finished, the close step opens the review again with the walk's defects.`);
20823
+ }
20824
+ function reviewNoReviewLine(phase) {
20825
+ return line(`${NOTHING_RECORDED}: phase ${namedPhase(phase)} has no review open.`);
20826
+ }
20827
+ function reviewCloseRefusedLine(phase, steps) {
20828
+ if (steps.length === 0) {
20829
+ return line(`${NOTHING_RECORDED}: phase ${namedPhase(phase)} could not close just now. Type the line again in a minute.`);
20830
+ }
20831
+ const labels = steps.map(unfinishedStepLabel).join(", ");
20832
+ const head = steps.length === 1 ? `${NOTHING_RECORDED}: phase ${namedPhase(phase)} cannot close while 1 of its steps is unfinished: ${labels}.` : `${NOTHING_RECORDED}: phase ${namedPhase(phase)} cannot close while ${steps.length} of its steps are unfinished: ${labels}.`;
20833
+ return line([head, ...steps.map(unfinishedStepLine), "Then type the line again."].join(" "));
20834
+ }
20835
+ function reviewRefusedLine(act) {
20836
+ return act === "accept" ? line(`${NOTHING_RECORDED}: Halfcycle did not accept the request. Update Halfcycle by running \`${HALFCYCLE_COMMAND}\` in this project's folder, then type the line again.`) : line(`${NOTHING_RECORDED}: the words were not accepted. Reword them in plain words and type the line again.`);
20837
+ }
20838
+ function stillWaitingOf(phase, waiting) {
20839
+ const named = namedPhase(phase);
20840
+ if (waiting === 0)
20841
+ return `No part of ${named} waits for your decision now.`;
20842
+ if (waiting === 1)
20843
+ return `1 part of ${named} still waits for your decision.`;
20844
+ return `${waiting} parts of ${named} still wait for your decision.`;
20845
+ }
20846
+ function stillWaiting(waiting) {
20847
+ if (waiting === 0)
20848
+ return "No part waits for your decision now.";
20849
+ if (waiting === 1)
20850
+ return "1 part still waits for your decision.";
20851
+ return `${waiting} parts still wait for your decision.`;
20852
+ }
20853
+ function decidedWords(decided) {
20854
+ switch (decided.act) {
20855
+ case "accept":
20856
+ return "accepted";
20857
+ case "question":
20858
+ return "questioned";
20859
+ case "send-back":
20860
+ return "sent back to building";
20861
+ case "carry":
20862
+ return decided.carriedTo === void 0 || decided.carriedTo === null ? "carried to the next phase planned" : `carried to phase "${decided.carriedTo.phase}"`;
20863
+ case "drop":
20864
+ return "dropped";
20865
+ default: {
20866
+ const unreachable = decided.act;
20867
+ return unreachable;
20868
+ }
20869
+ }
20870
+ }
20871
+ function reviewRecordedLine(decided, phase, waiting) {
20872
+ return line(`Recorded in your session: "${decided.name}" ${decidedWords(decided)}. ${stillWaitingOf(phase, waiting)}`);
20873
+ }
20874
+ function reviewBulkRecordedLine(accepted, phase, waiting) {
20875
+ const named = namedPhase(phase);
20876
+ const first = accepted === 1 ? `Recorded in your session: 1 part of ${named} accepted.` : `Recorded in your session: ${accepted} parts of ${named} accepted.`;
20877
+ return line(`${first} ${stillWaiting(waiting)}`);
20878
+ }
20879
+ function reviewClosedLine(phase) {
20880
+ return `Recorded in your session. That was the last decision: phase ${namedPhase(phase)} is closed. Run its close step now, then tell the person what it says.`;
20881
+ }
20882
+ function reviewNoPartsClosedLine(phase) {
20883
+ return `Recorded in your session. Phase ${namedPhase(phase)} had no parts to review, so it is closed. Run its close step now, then tell the person what it says.`;
20884
+ }
20885
+ function notDecidableLine(answer) {
20886
+ const first = answer.parts[0];
20887
+ switch (first.why) {
20888
+ case "question-waiting":
20889
+ return reviewAwaitingAnswerLine(first.name);
20890
+ case "sent-back":
20891
+ return reviewSentBackLine(first.name);
20892
+ case "settled":
20893
+ return reviewAlreadyDecidedLine(first.name);
20894
+ default: {
20895
+ const unreachable = first.why;
20896
+ return unreachable;
20897
+ }
20898
+ }
20899
+ }
20900
+ function reviewLineForAnswer(context, answer) {
20901
+ const { phase } = context;
20902
+ switch (answer.outcome) {
20903
+ case "recorded": {
20904
+ if (answer.closed) {
20905
+ return answer.decided.length === 0 ? reviewNoPartsClosedLine(phase) : reviewClosedLine(phase);
20906
+ }
20907
+ if (context.bulk)
20908
+ return reviewBulkRecordedLine(answer.decided.length, phase, answer.waiting);
20909
+ return reviewRecordedLine(answer.decided[0], phase, answer.waiting);
20910
+ }
20911
+ case "review-changed":
20912
+ return reviewChangedLine(phase);
20913
+ case "no-review":
20914
+ return reviewNoReviewLine(phase);
20915
+ case "not-decidable":
20916
+ return notDecidableLine(answer);
20917
+ case "close-refused":
20918
+ return reviewCloseRefusedLine(phase, answer.steps);
20919
+ case "walk-changed":
20920
+ return reviewWalkChangedLine(phase, context.number);
20921
+ case "not-owner":
20922
+ return reviewNotOwnerLine();
20923
+ case "no-owner":
20924
+ return reviewNoOwnerLine();
20925
+ case "refused":
20926
+ return reviewRefusedLine(context.act);
20927
+ case "unavailable":
20928
+ return reviewCannotReachLine();
20929
+ case "show-first":
20930
+ return reviewShowFirstLine();
20931
+ case "not-offered":
20932
+ return reviewSwitchedOffLine();
20933
+ default: {
20934
+ const unreachable = answer;
20935
+ return unreachable;
20936
+ }
20937
+ }
20938
+ }
19228
20939
 
19229
20940
  // dist/hooks/user-prompt-submit.js
19230
20941
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
19231
20942
  var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
19232
20943
  async function runUserPromptSubmit(opts) {
19233
20944
  let act = null;
20945
+ const review = { recognised: false };
19234
20946
  try {
19235
20947
  const prompt = promptOf(opts.input);
20948
+ const reviewShape = reviewPhraseShape(prompt);
20949
+ if (reviewShape !== null) {
20950
+ const out = await decideReview(reviewShape, opts, review);
20951
+ return out === "" ? "" : `${out}
20952
+ `;
20953
+ }
19236
20954
  const shape = scopePhraseShape(prompt);
19237
20955
  if (shape === null)
19238
20956
  return "";
@@ -19240,10 +20958,114 @@ async function runUserPromptSubmit(opts) {
19240
20958
  return `${await decide(shape, opts)}
19241
20959
  `;
19242
20960
  } catch {
20961
+ if (review.recognised)
20962
+ return `${reviewCannotReachLine()}
20963
+ `;
19243
20964
  return act === null ? "" : `${cannotReachLine(act)}
19244
20965
  `;
19245
20966
  }
19246
20967
  }
20968
+ async function decideReview(shape, opts, progress) {
20969
+ const env = opts.env ?? process.env;
20970
+ if (shape.kind === "phase") {
20971
+ const read2 = readConfig(env);
20972
+ if (read2.config === null)
20973
+ return "";
20974
+ const match2 = matchReviewPhraseShape(shape, readShownRecord(read2.config.guardEngagementId, opts.home).reviews);
20975
+ if (match2.kind === "nothing")
20976
+ return "";
20977
+ progress.recognised = true;
20978
+ if (!readSessionAttended(env))
20979
+ return reviewNotAttendedLine();
20980
+ return checkAndSendReview(match2, read2.config.guardEngagementId, read2.config.guardServiceToken, env, opts);
20981
+ }
20982
+ progress.recognised = true;
20983
+ if (!readSessionAttended(env))
20984
+ return reviewNotAttendedLine();
20985
+ const read = readConfig(env);
20986
+ if (read.config === null) {
20987
+ return readNeverConfiguredReason(env) === "not-this-account" ? reviewNotOwnerLine() : reviewNotSignedInLine();
20988
+ }
20989
+ const match = matchReviewPhraseShape(shape, readShownRecord(read.config.guardEngagementId, opts.home).reviews);
20990
+ return checkAndSendReview(match, read.config.guardEngagementId, read.config.guardServiceToken, env, opts);
20991
+ }
20992
+ async function checkAndSendReview(match, engagementId, credential, env, opts) {
20993
+ if (match.kind === "nothing" || match.kind === "show-first")
20994
+ return reviewShowFirstLine();
20995
+ if (match.kind === "not-listed")
20996
+ return reviewPartNotListedLine(match.part, match.entry.phase);
20997
+ const { entry } = match;
20998
+ if (!listedRecently(entry.shownAt, (opts.now ?? Date.now)()))
20999
+ return reviewListedTooLongAgoLine(entry.phase);
21000
+ let candidate;
21001
+ if (match.kind === "accept") {
21002
+ if (entry.parts.length > 0 && match.parts.length === 0)
21003
+ return reviewNothingToAcceptLine(entry.phase);
21004
+ candidate = {
21005
+ act: "accept",
21006
+ reference: match.reference,
21007
+ reviewVersion: entry.reviewVersion,
21008
+ parts: match.parts.map((p) => p.part)
21009
+ };
21010
+ } else {
21011
+ const { act, part } = match;
21012
+ if (!reviewActsFor(part.built).includes(act))
21013
+ return reviewWrongActLine(part.name, part.built);
21014
+ let words = "";
21015
+ if (act !== "accept") {
21016
+ words = match.words ?? "";
21017
+ if (words === "")
21018
+ return reviewNoWordsLine(act);
21019
+ if (!reviewNoteWireSchema.safeParse(words).success)
21020
+ return reviewWordsTooLongLine();
21021
+ if (looksLikeSecret(words))
21022
+ return reviewWordsLookSecretLine();
21023
+ }
21024
+ if (part.awaitingAnswer)
21025
+ return reviewAwaitingAnswerLine(part.name);
21026
+ if (part.sentBack)
21027
+ return reviewSentBackLine(part.name);
21028
+ if (!part.decidable)
21029
+ return reviewAlreadyDecidedLine(part.name);
21030
+ candidate = act === "accept" ? { act, reference: match.reference, reviewVersion: entry.reviewVersion, parts: [part.part] } : { act, reference: match.reference, reviewVersion: entry.reviewVersion, part: part.part, note: words };
21031
+ }
21032
+ const request = chatReviewRequestSchema.safeParse(candidate);
21033
+ if (!request.success)
21034
+ return reviewShowFirstLine();
21035
+ return postReview(match.kind === "accept", entry, request.data, engagementId, credential, env, opts);
21036
+ }
21037
+ async function postReview(bulk, entry, request, engagementId, credential, env, opts) {
21038
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
21039
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/phases/${encodeURIComponent(entry.phase)}/review/chat`;
21040
+ const controller = new AbortController();
21041
+ const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
21042
+ try {
21043
+ const response = await fetchFn(url2, {
21044
+ method: "POST",
21045
+ headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json" },
21046
+ body: JSON.stringify(request),
21047
+ signal: controller.signal
21048
+ });
21049
+ if (response.status === 401)
21050
+ return reviewNotSignedInLine();
21051
+ if (response.status === 429 || response.status >= 500)
21052
+ return reviewCannotReachLine();
21053
+ const parsed = chatReviewResultSchema.safeParse(await response.json());
21054
+ if (!parsed.success)
21055
+ return reviewCannotReachLine();
21056
+ const answer = parsed.data;
21057
+ if (answer.outcome === "recorded") {
21058
+ if (response.status !== 200)
21059
+ return reviewCannotReachLine();
21060
+ moveListedReviewForward(engagementId, opts.home, entry, answer.reviewVersion, answer.decided);
21061
+ }
21062
+ return reviewLineForAnswer({ act: request.act, bulk, phase: entry.phase, number: entry.number }, answer);
21063
+ } catch {
21064
+ return reviewCannotReachLine();
21065
+ } finally {
21066
+ clearTimeout(timer);
21067
+ }
21068
+ }
19247
21069
  async function decide(shape, opts) {
19248
21070
  const { act } = shape;
19249
21071
  const env = opts.env ?? process.env;
@@ -19254,12 +21076,12 @@ async function decide(shape, opts) {
19254
21076
  return readNeverConfiguredReason(env) === "not-this-account" ? notOwnerLine(act) : notSignedInLine(act);
19255
21077
  }
19256
21078
  const { guardServiceToken, guardEngagementId } = read.config;
19257
- const shown = readShownPlans(guardEngagementId, opts.home);
21079
+ const shown = readShownRecord(guardEngagementId, opts.home).entries;
19258
21080
  const match = matchScopePhraseShape(shape, shown);
19259
21081
  if (match.kind === "show-first")
19260
21082
  return showFirstLine(act);
19261
21083
  const { entry } = match;
19262
- if (!shownRecently(entry, (opts.now ?? Date.now)()))
21084
+ if (!listedRecently(entry.shownAt, (opts.now ?? Date.now)()))
19263
21085
  return tooOldLine(act, entry.phase);
19264
21086
  let candidate;
19265
21087
  if (match.kind === "send-back") {
@@ -19288,19 +21110,8 @@ function promptOf(input) {
19288
21110
  return void 0;
19289
21111
  }
19290
21112
  }
19291
- function readShownPlans(engagementId, home) {
19292
- if (!isEngagementId(engagementId))
19293
- return [];
19294
- let text;
19295
- try {
19296
- text = readFileSync9(join8(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SHOWN_PLANS_FILENAME), "utf-8");
19297
- } catch {
19298
- return [];
19299
- }
19300
- return parseShownPlans(text);
19301
- }
19302
- function shownRecently(entry, now) {
19303
- const age = now - Date.parse(entry.shownAt);
21113
+ function listedRecently(shownAt, now) {
21114
+ const age = now - Date.parse(shownAt);
19304
21115
  return Number.isFinite(age) && age >= 0 && age < SHOWN_ENTRY_MAX_AGE_MS;
19305
21116
  }
19306
21117
  async function post(act, entry, request, engagementId, credential, env, fetchFn = globalThis.fetch) {
@@ -19332,7 +21143,7 @@ async function post(act, entry, request, engagementId, credential, env, fetchFn
19332
21143
  }
19333
21144
 
19334
21145
  // dist/resolve-engagement.js
19335
- import { existsSync as existsSync6, readFileSync as readFileSync10, realpathSync as realpathSync3 } from "node:fs";
21146
+ import { existsSync as existsSync6, readFileSync as readFileSync11, realpathSync as realpathSync3 } from "node:fs";
19336
21147
  import { join as join9, sep as sep2 } from "node:path";
19337
21148
  import { homedir as homedir5 } from "node:os";
19338
21149
  function defaultRegistryPath() {
@@ -19342,7 +21153,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
19342
21153
  if (!existsSync6(registryPath))
19343
21154
  return {};
19344
21155
  try {
19345
- const parsed = JSON.parse(readFileSync10(registryPath, "utf-8"));
21156
+ const parsed = JSON.parse(readFileSync11(registryPath, "utf-8"));
19346
21157
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
19347
21158
  return {};
19348
21159
  const out = {};
@@ -19390,11 +21201,11 @@ function runResolveEngagement(cwd, registryPath) {
19390
21201
  }
19391
21202
 
19392
21203
  // dist/refresh-credential.js
19393
- import { readFileSync as readFileSync12 } from "node:fs";
21204
+ import { readFileSync as readFileSync13 } from "node:fs";
19394
21205
  import { resolve as resolve4 } from "node:path";
19395
21206
 
19396
21207
  // dist/mcp-headers.js
19397
- import { readFileSync as readFileSync11 } from "node:fs";
21208
+ import { readFileSync as readFileSync12 } from "node:fs";
19398
21209
  import { homedir as homedir6 } from "node:os";
19399
21210
  import { dirname as dirname4, resolve as resolve3 } from "node:path";
19400
21211
  function defaultWriteErr(text) {
@@ -19414,7 +21225,7 @@ function projectRootFromBinary(binaryPath) {
19414
21225
  function readPinnedVersion(root) {
19415
21226
  let raw;
19416
21227
  try {
19417
- raw = readFileSync11(resolve3(root, BUNDLE_PIN_REL), "utf-8");
21228
+ raw = readFileSync12(resolve3(root, BUNDLE_PIN_REL), "utf-8");
19418
21229
  } catch {
19419
21230
  return void 0;
19420
21231
  }
@@ -19640,7 +21451,7 @@ var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initializ
19640
21451
  function registeredMcpUrl(projectRoot) {
19641
21452
  let parsed;
19642
21453
  try {
19643
- parsed = JSON.parse(readFileSync12(resolve4(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
21454
+ parsed = JSON.parse(readFileSync13(resolve4(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
19644
21455
  } catch {
19645
21456
  return void 0;
19646
21457
  }
@@ -19747,7 +21558,7 @@ function positionRequestBody(engagementId) {
19747
21558
  params: { name: "halfcycle_position", arguments: { engagementId } }
19748
21559
  });
19749
21560
  }
19750
- async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
21561
+ async function fetchPositionNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
19751
21562
  const controller = new AbortController();
19752
21563
  const timer = setTimeout(() => controller.abort(), deadlineMs);
19753
21564
  try {
@@ -19794,15 +21605,17 @@ async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs
19794
21605
  } catch {
19795
21606
  return void 0;
19796
21607
  }
19797
- const scope = typeof body === "object" && body !== null ? body["scope"] : void 0;
19798
- const parsed = scopeNoticesSchema.safeParse(scope);
19799
- return parsed.success ? parsed.data : void 0;
21608
+ if (typeof body !== "object" || body === null)
21609
+ return void 0;
21610
+ const scopeParsed = scopeNoticesSchema.safeParse(body["scope"]);
21611
+ const reviewParsed = reviewNoticesSchema.safeParse(body["review"]);
21612
+ return { scope: scopeParsed.success ? scopeParsed.data : void 0, review: reviewParsed.success ? reviewParsed.data : void 0 };
19800
21613
  } finally {
19801
21614
  clearTimeout(timer);
19802
21615
  }
19803
21616
  }
19804
- function scopeNoticeLines(scope) {
19805
- const text = scopeNoticeText(scope, { offerTyping: scope.typedApprovalOffered ? "yes" : "off" });
21617
+ function scopeNoticeLines(scope, engagementId) {
21618
+ const text = scopeNoticeText(scope, { engagementId, offerTyping: scope.typedApprovalOffered ? "yes" : "off" });
19806
21619
  return [...text.sentBack, ...text.waiting];
19807
21620
  }
19808
21621
  function defaultWriteErr2(text) {
@@ -19833,9 +21646,11 @@ async function runRefreshCredential(opts) {
19833
21646
  const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
19834
21647
  const positionDeadline = Math.min(opts.scopeNoticeDeadlineMs ?? SCOPE_NOTICE_DEADLINE_MS, remainingForMargin);
19835
21648
  if (positionDeadline > 0) {
19836
- const scope = await fetchScopeNotices(connection.url, connection.bearer, engagementId, opts.fetchFn ?? globalThis.fetch, positionDeadline);
19837
- if (scope !== void 0)
19838
- parts.push(...scopeNoticeLines(scope));
21649
+ const notices = await fetchPositionNotices(connection.url, connection.bearer, engagementId, opts.fetchFn ?? globalThis.fetch, positionDeadline);
21650
+ if (notices?.scope !== void 0)
21651
+ parts.push(...scopeNoticeLines(notices.scope, engagementId));
21652
+ if (notices?.review !== void 0)
21653
+ parts.push(...reviewNoticeText(notices.review));
19839
21654
  }
19840
21655
  }
19841
21656
  } else if (connection.sentence === CANNOT_REACH_SENTENCE || connection.sentence === SERVICE_PROBLEM_SENTENCE) {
@@ -19869,7 +21684,7 @@ async function runRefreshCredential(opts) {
19869
21684
  import { fileURLToPath } from "node:url";
19870
21685
 
19871
21686
  // dist/capture.js
19872
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
21687
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync6, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
19873
21688
  import { dirname as dirname5 } from "node:path";
19874
21689
  function runCapture(provider, endpoint, opts = {}) {
19875
21690
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -19906,7 +21721,7 @@ function runCapture(provider, endpoint, opts = {}) {
19906
21721
  const manifestFile = manifestPath(repoRoot);
19907
21722
  try {
19908
21723
  mkdirSync3(dirname5(fixture), { recursive: true });
19909
- writeFileSync4(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
21724
+ writeFileSync6(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
19910
21725
  } catch (err) {
19911
21726
  const msg = err instanceof Error ? err.message : String(err);
19912
21727
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -19937,7 +21752,7 @@ function readInputFile(fromFile) {
19937
21752
  if (!fromFile)
19938
21753
  return void 0;
19939
21754
  try {
19940
- return readFileSync13(fromFile, "utf-8");
21755
+ return readFileSync14(fromFile, "utf-8");
19941
21756
  } catch {
19942
21757
  return void 0;
19943
21758
  }
@@ -19961,6 +21776,17 @@ async function main() {
19961
21776
  process.stdout.write(reply, () => process.exit(0));
19962
21777
  return;
19963
21778
  }
21779
+ if (cmd === "observe") {
21780
+ let line2;
21781
+ try {
21782
+ line2 = JSON.stringify(await runObserve());
21783
+ } catch {
21784
+ line2 = JSON.stringify({ outcome: "not-sent", reason: "unreachable" });
21785
+ }
21786
+ process.stdout.write(`${line2}
21787
+ `, () => process.exit(0));
21788
+ return;
21789
+ }
19964
21790
  if (cmd === "mcp-headers") {
19965
21791
  const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
19966
21792
  process.stdout.write(line2, () => process.exit(0));
@@ -20043,6 +21869,7 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
20043
21869
  halfcycle-runner hook user-prompt-submit
20044
21870
  halfcycle-runner refresh-credential
20045
21871
  halfcycle-runner mcp-headers
21872
+ halfcycle-runner observe
20046
21873
  halfcycle-runner resolve-engagement <cwd>
20047
21874
  halfcycle-runner capture <provider> <endpoint> [--from <file>]
20048
21875
  `);