halfcycle 0.4.8 → 0.4.9

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 (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/bin.bundle.mjs +2031 -216
  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/build-record/obtain.d.ts +14 -7
  15. package/dist/build-record/obtain.d.ts.map +1 -1
  16. package/dist/build-record/render-v2.d.ts +32 -6
  17. package/dist/build-record/render-v2.d.ts.map +1 -1
  18. package/dist/build-record/write.d.ts +2 -2
  19. package/dist/build-record/write.d.ts.map +1 -1
  20. package/dist/cli-contract.d.ts +2 -9
  21. package/dist/cli-contract.d.ts.map +1 -1
  22. package/dist/cli.js +3189 -1401
  23. package/dist/cli.js.map +3 -3
  24. package/dist/close-phase.d.ts +215 -134
  25. package/dist/close-phase.d.ts.map +1 -1
  26. package/dist/draw/index.d.ts +25 -5
  27. package/dist/draw/index.d.ts.map +1 -1
  28. package/dist/identity.d.ts +78 -9
  29. package/dist/identity.d.ts.map +1 -1
  30. package/dist/index.js +1624 -204
  31. package/dist/index.js.map +3 -3
  32. package/dist/install.d.ts +16 -0
  33. package/dist/install.d.ts.map +1 -1
  34. package/dist/open-phase.d.ts +25 -1
  35. package/dist/open-phase.d.ts.map +1 -1
  36. package/dist/plan.d.ts +59 -18
  37. package/dist/plan.d.ts.map +1 -1
  38. package/dist/pre-create-checks.d.ts.map +1 -1
  39. package/dist/review.d.ts +228 -0
  40. package/dist/review.d.ts.map +1 -0
  41. package/dist/shown-record.d.ts +46 -0
  42. package/dist/shown-record.d.ts.map +1 -0
  43. package/dist/system-report.d.ts +28 -4
  44. package/dist/system-report.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/dist/project-page.d.ts +0 -24
  47. 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,6 +15950,174 @@ var systemDrawRefusedSchema = external_exports.object({
15852
15950
  var systemDrawUnavailableSchema = external_exports.object({
15853
15951
  outcome: external_exports.literal("unavailable")
15854
15952
  }).strict();
15953
+ var systemDrawNotClosedSchema = external_exports.object({
15954
+ outcome: external_exports.literal("not-closed")
15955
+ }).strict();
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();
15855
16121
 
15856
16122
  // ../events/dist/phase-plan.js
15857
16123
  var PHASE_PLAN_FORMAT = 1;
@@ -15904,6 +16170,13 @@ var scopeDecisionPartSchema = external_exports.object({
15904
16170
  note: scopeNoteWireSchema.optional()
15905
16171
  }).strict();
15906
16172
  var planVersionSchema = external_exports.number().int().min(1);
16173
+ var carriedPhaseSchema = external_exports.object({
16174
+ phase: scopePhaseSchema,
16175
+ number: external_exports.number().int().min(1)
16176
+ }).strict();
16177
+ var carriedFromEntrySchema = carriedPhaseSchema.extend({
16178
+ part: systemPartIdSchema
16179
+ }).strict();
15907
16180
  var phasePlanRequestSchema = external_exports.object({
15908
16181
  descriptionVersion: external_exports.number().int().min(1),
15909
16182
  plan: external_exports.unknown()
@@ -15915,7 +16188,8 @@ var phasePlanRecordedSchema = external_exports.object({
15915
16188
  state: plannedScopeStateSchema,
15916
16189
  changedParts: external_exports.array(systemPartIdSchema).max(PHASE_PLAN_CHANGES_MAX),
15917
16190
  typedApprovalOffered: external_exports.boolean(),
15918
- reportedAt: utcIso8601
16191
+ reportedAt: utcIso8601,
16192
+ carriedFrom: external_exports.array(carriedFromEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX).optional()
15919
16193
  }).strict();
15920
16194
  var phasePlanRefusedSchema = external_exports.object({
15921
16195
  outcome: external_exports.literal("refused"),
@@ -15924,10 +16198,23 @@ var phasePlanRefusedSchema = external_exports.object({
15924
16198
  var phasePlanUnavailableSchema = external_exports.object({
15925
16199
  outcome: external_exports.literal("unavailable")
15926
16200
  }).strict();
16201
+ var REMOVAL_OWED_ACTS = ["dropped", "removed"];
16202
+ var removalOwedPartSchema = external_exports.object({
16203
+ part: systemPartIdSchema,
16204
+ name: systemPartSchema.shape.name,
16205
+ phase: scopePhaseSchema,
16206
+ number: external_exports.number().int().min(1),
16207
+ act: external_exports.enum(REMOVAL_OWED_ACTS)
16208
+ }).strict();
16209
+ var phasePlanRemovalOwedSchema = external_exports.object({
16210
+ outcome: external_exports.literal("removal-owed"),
16211
+ parts: external_exports.array(removalOwedPartSchema).min(1).max(SYSTEM_PARTS_MAX)
16212
+ }).strict();
15927
16213
  var phasePlanResultSchema = external_exports.discriminatedUnion("outcome", [
15928
16214
  phasePlanRecordedSchema,
15929
16215
  phasePlanRefusedSchema,
15930
- phasePlanUnavailableSchema
16216
+ phasePlanUnavailableSchema,
16217
+ phasePlanRemovalOwedSchema
15931
16218
  ]);
15932
16219
  var scopeApproveRequestSchema = external_exports.object({
15933
16220
  act: external_exports.literal("approve"),
@@ -15989,7 +16276,8 @@ var scopeReadPartSchema = external_exports.object({
15989
16276
  change: phasePlanChangeSchema,
15990
16277
  summary: phasePlanSummarySchema,
15991
16278
  status: scopePartStatusSchema,
15992
- changed: external_exports.boolean()
16279
+ changed: external_exports.boolean(),
16280
+ carriedFrom: carriedPhaseSchema.optional()
15993
16281
  }).strict();
15994
16282
  var scopeReadDecisionSchema = external_exports.object({
15995
16283
  act: scopeActSchema,
@@ -16016,9 +16304,580 @@ var scopeReadPlannedSchema = external_exports.object({
16016
16304
  }).strict();
16017
16305
  var scopeReadSchema = external_exports.union([scopeReadNotPlannedSchema, scopeReadPlannedSchema]);
16018
16306
 
16307
+ // ../events/dist/code-link.js
16308
+ 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" });
16309
+ var codeLinkFactsSchema = external_exports.object({
16310
+ repositoryAddress: codeLinkAddressSchema.nullable(),
16311
+ headOnGitHub: external_exports.boolean()
16312
+ }).strict().refine((v) => !(v.headOnGitHub && v.repositoryAddress === null), {
16313
+ message: "cannot be true when the code is not on GitHub",
16314
+ path: ["headOnGitHub"]
16315
+ });
16316
+ var PART_CODE_LINK_SCOPES = ["part", "phase"];
16317
+ var PART_CODE_LINK_RANGE_REASONS = ["not-github", "not-pushed"];
16318
+ var PART_CODE_LINK_URL_PREFIX = "https://github.com/";
16319
+ var partCodeLinkGithubSchema = external_exports.object({
16320
+ kind: external_exports.literal("github"),
16321
+ scope: external_exports.enum(PART_CODE_LINK_SCOPES),
16322
+ url: external_exports.string().startsWith(PART_CODE_LINK_URL_PREFIX, { message: "must start with https://github.com/" })
16323
+ }).strict();
16324
+ var partCodeLinkRangeSchema = external_exports.object({
16325
+ kind: external_exports.literal("range"),
16326
+ why: external_exports.enum(PART_CODE_LINK_RANGE_REASONS),
16327
+ from: closeCommitSchema,
16328
+ to: closeCommitSchema
16329
+ }).strict();
16330
+ var partCodeLinkNoneSchema = external_exports.object({
16331
+ kind: external_exports.literal("none")
16332
+ }).strict();
16333
+ var partCodeLinkSchema = external_exports.discriminatedUnion("kind", [
16334
+ partCodeLinkGithubSchema,
16335
+ partCodeLinkRangeSchema,
16336
+ partCodeLinkNoneSchema
16337
+ ]);
16338
+
16339
+ // ../events/dist/observed-changes.js
16340
+ var OBSERVED_PATHS_LISTED_MAX = 50;
16341
+ var OBSERVED_PATH_MAX = 300;
16342
+ var OBSERVED_COUNT_MAX = 1e6;
16343
+ var METHOD_FILE_PATTERNS = [
16344
+ "CLAUDE.md",
16345
+ "AGENTS.md",
16346
+ ".claude/context",
16347
+ ".claude/commands",
16348
+ ".claude/hooks",
16349
+ ".claude/settings.json",
16350
+ ".mcp.json",
16351
+ ".halfcycle",
16352
+ ".workbench",
16353
+ "docs/product-spec.md",
16354
+ "docs/architecture-overview.md",
16355
+ "docs/invariants.md",
16356
+ "docs/project-log.md",
16357
+ "docs/code-map.md",
16358
+ "docs/system.json",
16359
+ "docs/diagrams",
16360
+ "docs/features",
16361
+ "docs/phases",
16362
+ "docs/build-records",
16363
+ "docs/decisions",
16364
+ "docs/components"
16365
+ ];
16366
+ var TEST_FILE_PATTERNS = [
16367
+ "**/*.test.*",
16368
+ "**/*.spec.*",
16369
+ "**/*_test.*",
16370
+ "**/*_spec.*",
16371
+ "**/test_*.*",
16372
+ "**/*Test.java",
16373
+ "**/*Tests.java",
16374
+ "**/*Test.kt",
16375
+ "**/*Tests.cs",
16376
+ "**/*Test.php"
16377
+ ];
16378
+ function isMethodFile(path) {
16379
+ return METHOD_FILE_PATTERNS.some((pattern) => patternOwns(pattern, path));
16380
+ }
16381
+ function isTestFile(path) {
16382
+ return TEST_FILE_PATTERNS.some((pattern) => patternOwns(pattern, path));
16383
+ }
16384
+ 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}` });
16385
+ var observedDescriptionVersionSchema = external_exports.number().int().min(1);
16386
+ 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` });
16387
+ 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" });
16388
+ var observedMethodFilesSchema = external_exports.object({
16389
+ added: observedCountSchema,
16390
+ changed: observedCountSchema,
16391
+ removed: observedCountSchema
16392
+ }).strict();
16393
+ var observedFilesSchema = external_exports.object({
16394
+ added: observedCountSchema,
16395
+ changed: observedCountSchema,
16396
+ removed: observedCountSchema,
16397
+ testFilesAdded: observedCountSchema
16398
+ }).strict().refine((v) => v.testFilesAdded <= v.added, {
16399
+ message: "cannot be more than the files added",
16400
+ path: ["testFilesAdded"]
16401
+ });
16402
+ function filesChanged(files) {
16403
+ return files.added + files.changed + files.removed;
16404
+ }
16405
+ function pathsFitTheCount(files, paths) {
16406
+ return files === null ? paths.length === 0 : paths.length <= filesChanged(files);
16407
+ }
16408
+ function checkPartsUnique(parts, ctx, at) {
16409
+ const seen = /* @__PURE__ */ new Set();
16410
+ parts.forEach((entry, i) => {
16411
+ if (seen.has(entry.part)) {
16412
+ ctx.addIssue({ code: "custom", path: [...at, i, "part"], message: `names the part "${entry.part}" twice` });
16413
+ }
16414
+ seen.add(entry.part);
16415
+ });
16416
+ }
16417
+ var observedBasisCountSchema = external_exports.object({
16418
+ outcome: external_exports.literal("count"),
16419
+ phase: phaseNameSchema,
16420
+ startCommit: closeCommitSchema,
16421
+ descriptionVersion: observedDescriptionVersionSchema,
16422
+ description: systemDescriptionSchema
16423
+ }).strict();
16424
+ var observedBasisCannotCountSchema = external_exports.object({
16425
+ outcome: external_exports.literal("cannot-count"),
16426
+ phase: phaseNameSchema,
16427
+ reason: external_exports.literal("no-start-commit")
16428
+ }).strict();
16429
+ var observedBasisNothingToCountSchema = external_exports.object({
16430
+ outcome: external_exports.literal("nothing-to-count"),
16431
+ reason: external_exports.enum(["no-open-phase", "no-description"])
16432
+ }).strict();
16433
+ var observedBasisSchema = external_exports.discriminatedUnion("outcome", [
16434
+ observedBasisCountSchema,
16435
+ observedBasisCannotCountSchema,
16436
+ observedBasisNothingToCountSchema
16437
+ ]);
16438
+ var observedReportPartSchema = external_exports.object({
16439
+ part: systemPartIdSchema,
16440
+ added: observedCountSchema,
16441
+ changed: observedCountSchema,
16442
+ removed: observedCountSchema,
16443
+ testFilesAdded: observedCountSchema
16444
+ }).strict().refine((v) => v.testFilesAdded <= v.added, {
16445
+ message: "cannot be more than the files added",
16446
+ path: ["testFilesAdded"]
16447
+ }).refine((v) => filesChanged(v) >= 1, {
16448
+ message: "a part is listed only when at least one file changed under it",
16449
+ path: ["added"]
16450
+ });
16451
+ var observedReportOutsideSchema = external_exports.object({
16452
+ added: observedCountSchema,
16453
+ changed: observedCountSchema,
16454
+ removed: observedCountSchema,
16455
+ testFilesAdded: observedCountSchema,
16456
+ paths: observedPathListSchema
16457
+ }).strict().refine((v) => v.testFilesAdded <= v.added, {
16458
+ message: "cannot be more than the files added",
16459
+ path: ["testFilesAdded"]
16460
+ }).refine((v) => pathsFitTheCount(v, v.paths), {
16461
+ message: "lists more paths than files changed",
16462
+ path: ["paths"]
16463
+ });
16464
+ var observedChangesReportSchema = external_exports.object({
16465
+ phase: phaseNameSchema,
16466
+ startCommit: closeCommitSchema,
16467
+ headCommit: closeCommitSchema,
16468
+ headCommittedAt: utcIso8601,
16469
+ commits: observedCountSchema,
16470
+ descriptionVersion: observedDescriptionVersionSchema,
16471
+ parts: external_exports.array(observedReportPartSchema).max(SYSTEM_PARTS_MAX, { message: `can list at most ${SYSTEM_PARTS_MAX} parts` }),
16472
+ methodFiles: observedMethodFilesSchema,
16473
+ outsideEveryPart: observedReportOutsideSchema,
16474
+ codeLink: codeLinkFactsSchema
16475
+ }).strict().superRefine((v, ctx) => checkPartsUnique(v.parts, ctx, ["parts"]));
16476
+ var observedSaidSchema = external_exports.object({
16477
+ title: buildTaskSchema.shape.title,
16478
+ agent: buildTaskSchema.shape.agent,
16479
+ whyOutsidePlan: buildTaskSchema.shape.whyOutsidePlan
16480
+ }).strict();
16481
+ var observedNotInPlanSchema = external_exports.object({
16482
+ part: systemPartIdSchema,
16483
+ name: systemPartSchema.shape.name,
16484
+ said: external_exports.array(observedSaidSchema).max(BUILD_SNAPSHOT_TASK_LIMIT)
16485
+ }).strict();
16486
+ var observedChangesRecordedSchema = external_exports.object({
16487
+ outcome: external_exports.literal("recorded"),
16488
+ notInPlan: external_exports.array(observedNotInPlanSchema).max(SYSTEM_PARTS_MAX).superRefine((parts, ctx) => checkPartsUnique(parts, ctx, [])),
16489
+ outside: external_exports.object({
16490
+ files: observedCountSchema,
16491
+ example: observedPathSchema.nullable()
16492
+ }).strict().refine((v) => v.example === null || v.files >= 1, {
16493
+ message: "names an example when no file changed",
16494
+ path: ["example"]
16495
+ })
16496
+ }).strict();
16497
+ var observedChangesKeptSchema = external_exports.object({
16498
+ outcome: external_exports.literal("kept")
16499
+ }).strict();
16500
+ var observedChangesReportResultSchema = external_exports.discriminatedUnion("outcome", [
16501
+ observedChangesRecordedSchema,
16502
+ observedChangesKeptSchema
16503
+ ]);
16504
+ var OBSERVE_NOT_SENT_REASONS = [
16505
+ "not-configured",
16506
+ "nothing-to-count",
16507
+ "cannot-count",
16508
+ "start-not-here",
16509
+ "start-not-on-branch",
16510
+ "basis-moved",
16511
+ "refused",
16512
+ "unreachable"
16513
+ ];
16514
+ var observeRunRecordedSchema = external_exports.object({ outcome: external_exports.literal("recorded") }).strict();
16515
+ var observeRunKeptSchema = external_exports.object({ outcome: external_exports.literal("kept") }).strict();
16516
+ var observeRunNotSentSchema = external_exports.object({
16517
+ outcome: external_exports.literal("not-sent"),
16518
+ reason: external_exports.enum(OBSERVE_NOT_SENT_REASONS)
16519
+ }).strict();
16520
+ var observeRunResultSchema = external_exports.discriminatedUnion("outcome", [
16521
+ observeRunRecordedSchema,
16522
+ observeRunKeptSchema,
16523
+ observeRunNotSentSchema
16524
+ ]);
16525
+ var observedChecksFiredSchema = external_exports.array(external_exports.string().min(1)).refine((names) => names.every((name, i) => i === 0 || names[i - 1] < name), {
16526
+ message: "must be sorted, each name once"
16527
+ });
16528
+ var observedCountingSchema = external_exports.discriminatedUnion("state", [
16529
+ external_exports.object({
16530
+ state: external_exports.literal("counted"),
16531
+ startCommit: closeCommitSchema,
16532
+ startRecordedAt: utcIso8601,
16533
+ headCommit: closeCommitSchema,
16534
+ headCommittedAt: utcIso8601,
16535
+ commits: observedCountSchema,
16536
+ countedAt: utcIso8601
16537
+ }).strict(),
16538
+ external_exports.object({
16539
+ state: external_exports.literal("not-counted-yet"),
16540
+ startCommit: closeCommitSchema,
16541
+ startRecordedAt: utcIso8601
16542
+ }).strict(),
16543
+ external_exports.object({
16544
+ state: external_exports.literal("cannot-count")
16545
+ }).strict()
16546
+ ]);
16547
+ var observedPartSchema = external_exports.object({
16548
+ part: systemPartIdSchema,
16549
+ files: observedFilesSchema.nullable(),
16550
+ checksFired: observedChecksFiredSchema
16551
+ }).strict().refine((v) => v.files !== null && filesChanged(v.files) >= 1 || v.checksFired.length >= 1, {
16552
+ message: "a part is listed only when files changed under it or a check fired on one",
16553
+ path: ["files"]
16554
+ });
16555
+ var observedOutsideSchema = external_exports.object({
16556
+ files: observedFilesSchema.nullable(),
16557
+ paths: observedPathListSchema,
16558
+ checksFired: observedChecksFiredSchema
16559
+ }).strict().refine((v) => pathsFitTheCount(v.files, v.paths), {
16560
+ message: "lists more paths than files changed",
16561
+ path: ["paths"]
16562
+ });
16563
+ var observedChangesSchema = external_exports.object({
16564
+ phase: phaseNameSchema,
16565
+ counting: observedCountingSchema,
16566
+ descriptionVersion: observedDescriptionVersionSchema.nullable(),
16567
+ parts: external_exports.array(observedPartSchema).max(SYSTEM_PARTS_MAX),
16568
+ methodFiles: observedMethodFilesSchema.nullable(),
16569
+ outsideEveryPart: observedOutsideSchema,
16570
+ codeLink: codeLinkFactsSchema.nullable()
16571
+ }).strict().superRefine((v, ctx) => {
16572
+ checkPartsUnique(v.parts, ctx, ["parts"]);
16573
+ const counted = v.counting.state === "counted";
16574
+ const rule = counted ? "must be present when the phase is counted" : "must be null until the phase is counted";
16575
+ if (v.methodFiles !== null !== counted)
16576
+ ctx.addIssue({ code: "custom", path: ["methodFiles"], message: rule });
16577
+ if (v.outsideEveryPart.files !== null !== counted) {
16578
+ ctx.addIssue({ code: "custom", path: ["outsideEveryPart", "files"], message: rule });
16579
+ }
16580
+ if (v.codeLink !== null !== counted)
16581
+ ctx.addIssue({ code: "custom", path: ["codeLink"], message: rule });
16582
+ v.parts.forEach((entry, i) => {
16583
+ if (entry.files !== null !== counted)
16584
+ ctx.addIssue({ code: "custom", path: ["parts", i, "files"], message: rule });
16585
+ });
16586
+ if (counted && v.descriptionVersion === null) {
16587
+ ctx.addIssue({ code: "custom", path: ["descriptionVersion"], message: "must name the version the counts used" });
16588
+ }
16589
+ });
16590
+ var partOutsideThePlanSchema = external_exports.object({
16591
+ part: systemPartIdSchema,
16592
+ name: systemPartSchema.shape.name,
16593
+ seen: observedFilesSchema.nullable(),
16594
+ tasks: external_exports.array(observedSaidSchema.extend({ id: buildTaskSchema.shape.id }).strict()).max(BUILD_SNAPSHOT_TASK_LIMIT)
16595
+ }).strict();
16596
+ var NO_TASK_SAID_LINE = "No task said it changed this part.";
16597
+ function agentSaid(task) {
16598
+ const { title, agent, whyOutsidePlan } = task;
16599
+ if (whyOutsidePlan === void 0)
16600
+ return `"${title}" said it built this part, and gave no reason.`;
16601
+ if (agent === HUMAN_AGENT)
16602
+ return `The person on "${title}" said: "${whyOutsidePlan}"`;
16603
+ if (agent === void 0)
16604
+ return `The agent on "${title}" said: "${whyOutsidePlan}"`;
16605
+ return `${agent} said, on "${title}": "${whyOutsidePlan}"`;
16606
+ }
16607
+
16608
+ // ../events/dist/phase-review.js
16609
+ var BUILT_PART_ACTS = Object.freeze(["accept", "question", "send-back"]);
16610
+ var NOT_BUILT_PART_ACTS = Object.freeze(["carry", "drop"]);
16611
+ var REVIEW_ACTS = Object.freeze([...BUILT_PART_ACTS, ...NOT_BUILT_PART_ACTS]);
16612
+ var reviewActSchema = external_exports.enum(REVIEW_ACTS);
16613
+ function reviewActsFor(built) {
16614
+ return built ? BUILT_PART_ACTS : NOT_BUILT_PART_ACTS;
16615
+ }
16616
+ var REVIEW_NOTE_MAX = SCOPE_NOTE_MAX;
16617
+ var reviewNoteWireSchema = external_exports.string().max(REVIEW_NOTE_MAX);
16618
+ var reviewNoteSchema = external_exports.string().min(1).max(REVIEW_NOTE_MAX);
16619
+ var REVIEW_ANSWER_MAX = 1e3;
16620
+ var reviewAgentAnswerSchema = external_exports.string().min(1).max(REVIEW_ANSWER_MAX);
16621
+ var REVIEW_PARTS_MAX = PHASE_PLAN_CHANGES_MAX * 2;
16622
+ var REVIEW_PART_CHANGES = Object.freeze([...PHASE_PLAN_CHANGE_KINDS, "not-in-plan"]);
16623
+ var reviewPartChangeSchema = external_exports.enum(REVIEW_PART_CHANGES);
16624
+ var REVIEW_DECISIONS = Object.freeze(["accepted", "carried", "dropped"]);
16625
+ var reviewDecisionSchema = external_exports.enum(REVIEW_DECISIONS);
16626
+ function actOf(decision) {
16627
+ if (decision === "accepted")
16628
+ return "accept";
16629
+ if (decision === "carried")
16630
+ return "carry";
16631
+ return "drop";
16632
+ }
16633
+ var reviewCarriedToSchema = carriedPhaseSchema.nullable();
16634
+ var reviewStepNameSchema = stepNameSchema.refine((v) => !stepIdSchema.safeParse(v.trim()).success, {
16635
+ message: "must be the step's plain name"
16636
+ });
16637
+ var reviewStepSchema = external_exports.object({
16638
+ name: reviewStepNameSchema.nullable(),
16639
+ state: unfinishedStepSchema.shape.state,
16640
+ judged: external_exports.boolean()
16641
+ }).strict();
16642
+ var UNNAMED_STEP = "a step of this phase";
16643
+ function stepCalled(step) {
16644
+ return step.name === null ? UNNAMED_STEP : `"${step.name}"`;
16645
+ }
16646
+ function sentenceStart(text) {
16647
+ return text.charAt(0).toUpperCase() + text.slice(1);
16648
+ }
16649
+ function unfinishedStepLabel(step) {
16650
+ const state = step.state === "parked" ? "parked" : "attempted, and did not finish";
16651
+ return `${stepCalled(step)} (${state})`;
16652
+ }
16653
+ function unfinishedStepLine(step) {
16654
+ if (step.judged) {
16655
+ 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.`;
16656
+ }
16657
+ return `Finish ${stepCalled(step)} in your coding session, or record it as declined.`;
16658
+ }
16659
+ var openReviewRequestSchema = external_exports.object({
16660
+ actor: closeActorSchema,
16661
+ acceptanceOutcome: acceptanceOutcomeSchema,
16662
+ statements: closeStatementsSchema
16663
+ }).strict();
16664
+ var REVIEW_OPEN_TASKS_SHOWN_MAX = 10;
16665
+ var reviewOpenedSchema = external_exports.object({
16666
+ outcome: external_exports.literal("opened"),
16667
+ round: external_exports.number().int().min(1),
16668
+ parts: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
16669
+ notBuilt: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
16670
+ counted: external_exports.boolean(),
16671
+ typedReviewOffered: external_exports.boolean()
16672
+ }).strict().refine((v) => v.notBuilt <= v.parts, { message: "cannot exceed the parts in the round", path: ["notBuilt"] });
16673
+ var reviewTasksOpenSchema = external_exports.object({
16674
+ outcome: external_exports.literal("tasks-open"),
16675
+ open: external_exports.array(buildTaskSchema.shape.title).min(1).max(REVIEW_OPEN_TASKS_SHOWN_MAX),
16676
+ more: external_exports.number().int().min(0).max(BUILD_SNAPSHOT_TASK_LIMIT)
16677
+ }).strict();
16678
+ var reviewOutstandingSchema = external_exports.object({
16679
+ outcome: external_exports.literal("outstanding"),
16680
+ steps: external_exports.array(reviewStepSchema).min(1)
16681
+ }).strict();
16682
+ var reviewCleanOverFindingsSchema = external_exports.object({
16683
+ outcome: external_exports.literal("clean-over-findings"),
16684
+ findings: external_exports.number().int().min(1)
16685
+ }).strict();
16686
+ var reviewRefusedSchema = external_exports.object({
16687
+ outcome: external_exports.literal("refused"),
16688
+ problems: external_exports.array(systemDescriptionProblemSchema).min(1)
16689
+ }).strict();
16690
+ function bareReviewOutcome(outcome) {
16691
+ return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
16692
+ }
16693
+ var openReviewResultSchema = external_exports.discriminatedUnion("outcome", [
16694
+ reviewOpenedSchema,
16695
+ reviewTasksOpenSchema,
16696
+ bareReviewOutcome("no-build"),
16697
+ bareReviewOutcome("review-open"),
16698
+ reviewOutstandingSchema,
16699
+ reviewCleanOverFindingsSchema,
16700
+ reviewRefusedSchema,
16701
+ bareReviewOutcome("unavailable")
16702
+ ]);
16703
+ var reviewPhaseNotOpenSchema = external_exports.object({
16704
+ statusCode: external_exports.literal(409),
16705
+ error: external_exports.literal("PhaseNotOpen"),
16706
+ message: external_exports.string().min(1),
16707
+ openPhase: external_exports.string().nullable()
16708
+ }).strict();
16709
+ var openReviewAnswerSchema = external_exports.union([openReviewResultSchema, reviewPhaseNotOpenSchema]);
16710
+ var reviewDecisionEntrySchema = external_exports.object({
16711
+ part: systemPartIdSchema,
16712
+ act: reviewActSchema,
16713
+ note: reviewNoteWireSchema.optional()
16714
+ }).strict();
16715
+ var reviewDecisionRequestSchema = external_exports.object({
16716
+ reviewVersion: external_exports.number().int().min(0),
16717
+ decisions: external_exports.array(reviewDecisionEntrySchema).max(REVIEW_PARTS_MAX)
16718
+ }).strict();
16719
+ var reviewDecidedSchema = external_exports.object({
16720
+ part: systemPartIdSchema,
16721
+ name: systemPartSchema.shape.name,
16722
+ act: reviewActSchema,
16723
+ carriedTo: reviewCarriedToSchema.optional()
16724
+ }).strict().refine((v) => v.carriedTo !== void 0 === (v.act === "carry"), {
16725
+ message: "is present exactly on a carry",
16726
+ path: ["carriedTo"]
16727
+ });
16728
+ var reviewRecordedSchema = external_exports.object({
16729
+ outcome: external_exports.literal("recorded"),
16730
+ decided: external_exports.array(reviewDecidedSchema).max(REVIEW_PARTS_MAX),
16731
+ waiting: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
16732
+ closed: external_exports.boolean(),
16733
+ reviewVersion: external_exports.number().int().min(1)
16734
+ }).strict().superRefine((v, ctx) => {
16735
+ if (v.closed && v.waiting !== 0) {
16736
+ ctx.addIssue({ code: "custom", path: ["waiting"], message: "must be 0 once the phase is closed" });
16737
+ }
16738
+ if (v.decided.length === 0 && !v.closed) {
16739
+ ctx.addIssue({ code: "custom", path: ["decided"], message: "may be empty only when the phase closed" });
16740
+ }
16741
+ });
16742
+ var REVIEW_NOT_DECIDABLE_WHY = Object.freeze(["settled", "question-waiting", "sent-back"]);
16743
+ var reviewNotDecidablePartSchema = external_exports.object({
16744
+ part: systemPartIdSchema,
16745
+ name: systemPartSchema.shape.name,
16746
+ why: external_exports.enum(REVIEW_NOT_DECIDABLE_WHY)
16747
+ }).strict();
16748
+ var reviewNotDecidableSchema = external_exports.object({
16749
+ outcome: external_exports.literal("not-decidable"),
16750
+ parts: external_exports.array(reviewNotDecidablePartSchema).min(1).max(REVIEW_PARTS_MAX)
16751
+ }).strict();
16752
+ var reviewCloseRefusedSchema = external_exports.object({
16753
+ outcome: external_exports.literal("close-refused"),
16754
+ steps: external_exports.array(reviewStepSchema)
16755
+ }).strict();
16756
+ var reviewWalkChangedSchema = external_exports.object({
16757
+ outcome: external_exports.literal("walk-changed"),
16758
+ findings: external_exports.number().int().min(1)
16759
+ }).strict();
16760
+ var reviewDecisionResultSchema = external_exports.discriminatedUnion("outcome", [
16761
+ reviewRecordedSchema,
16762
+ bareReviewOutcome("review-changed"),
16763
+ bareReviewOutcome("no-review"),
16764
+ reviewNotDecidableSchema,
16765
+ reviewCloseRefusedSchema,
16766
+ reviewWalkChangedSchema,
16767
+ bareReviewOutcome("not-owner"),
16768
+ bareReviewOutcome("no-owner"),
16769
+ reviewRefusedSchema,
16770
+ bareReviewOutcome("unavailable")
16771
+ ]);
16772
+ var reviewSentBackSchema = external_exports.object({
16773
+ round: external_exports.number().int().min(1),
16774
+ note: reviewNoteSchema
16775
+ }).strict();
16776
+ var reviewQuestionSchema = external_exports.object({
16777
+ question: reviewNoteSchema,
16778
+ agentAnswer: reviewAgentAnswerSchema.optional()
16779
+ }).strict();
16780
+ var reviewTaskSchema = external_exports.object({
16781
+ title: buildTaskSchema.shape.title,
16782
+ state: buildTaskStateSchema
16783
+ }).strict();
16784
+ var reviewViewPartSchema = external_exports.object({
16785
+ part: systemPartIdSchema,
16786
+ name: systemPartSchema.shape.name,
16787
+ change: reviewPartChangeSchema,
16788
+ built: external_exports.boolean(),
16789
+ builtWithoutApproval: external_exports.boolean(),
16790
+ decidable: external_exports.boolean(),
16791
+ waiting: external_exports.boolean(),
16792
+ decision: reviewDecisionSchema.optional(),
16793
+ reason: reviewNoteSchema.optional(),
16794
+ carriedTo: reviewCarriedToSchema.optional(),
16795
+ sentBack: external_exports.array(reviewSentBackSchema),
16796
+ questions: external_exports.array(reviewQuestionSchema),
16797
+ tasks: external_exports.array(reviewTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
16798
+ said: external_exports.array(observedSaidSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
16799
+ codeLink: partCodeLinkSchema
16800
+ }).strict().superRefine((v, ctx) => {
16801
+ if (v.waiting !== (v.decision === void 0)) {
16802
+ ctx.addIssue({ code: "custom", path: ["waiting"], message: "must be true exactly when the part has no decision" });
16803
+ }
16804
+ if (v.decidable && !v.waiting) {
16805
+ ctx.addIssue({ code: "custom", path: ["decidable"], message: "cannot be true for a settled part" });
16806
+ }
16807
+ if (v.decision !== void 0 && !reviewActsFor(v.built).includes(actOf(v.decision))) {
16808
+ ctx.addIssue({ code: "custom", path: ["decision"], message: "is not an act this part takes" });
16809
+ }
16810
+ const carriedOrDropped = v.decision === "carried" || v.decision === "dropped";
16811
+ if (v.reason !== void 0 !== carriedOrDropped) {
16812
+ ctx.addIssue({ code: "custom", path: ["reason"], message: "is present exactly on a carry or a drop" });
16813
+ }
16814
+ if (v.carriedTo !== void 0 !== (v.decision === "carried")) {
16815
+ ctx.addIssue({ code: "custom", path: ["carriedTo"], message: "is present exactly on a carry" });
16816
+ }
16817
+ if (v.change === "not-in-plan" && !v.built) {
16818
+ ctx.addIssue({ code: "custom", path: ["built"], message: "is always true for a part outside the plan" });
16819
+ }
16820
+ });
16821
+ var reviewPhaseNumberSchema = external_exports.number().int().min(1).nullable();
16822
+ var phaseReviewNoneSchema = external_exports.object({
16823
+ state: external_exports.literal("none"),
16824
+ phase: scopePhaseSchema,
16825
+ number: reviewPhaseNumberSchema,
16826
+ closedWithoutReview: external_exports.boolean()
16827
+ }).strict();
16828
+ var reviewWalkSchema = external_exports.object({
16829
+ verdict: walkVerdictSchema,
16830
+ findings: external_exports.number().int().min(0)
16831
+ }).strict();
16832
+ function reviewedViewSchema(state) {
16833
+ return external_exports.object({
16834
+ state: external_exports.literal(state),
16835
+ phase: scopePhaseSchema,
16836
+ number: reviewPhaseNumberSchema,
16837
+ cycle: external_exports.number().int().min(0),
16838
+ round: external_exports.number().int().min(1),
16839
+ reviewVersion: external_exports.number().int().min(1),
16840
+ typedReviewOffered: external_exports.boolean(),
16841
+ counted: external_exports.boolean(),
16842
+ walk: reviewWalkSchema,
16843
+ parts: external_exports.array(reviewViewPartSchema).max(REVIEW_PARTS_MAX),
16844
+ observed: observedChangesSchema.nullable(),
16845
+ description: systemDescriptionSchema
16846
+ }).strict().superRefine((v, ctx) => {
16847
+ const seen = /* @__PURE__ */ new Set();
16848
+ v.parts.forEach((p, i) => {
16849
+ if (seen.has(p.part))
16850
+ ctx.addIssue({ code: "custom", path: ["parts", i, "part"], message: "is listed twice" });
16851
+ seen.add(p.part);
16852
+ });
16853
+ const counted = v.observed !== null && v.observed.counting.state === "counted";
16854
+ if (v.counted !== counted) {
16855
+ ctx.addIssue({ code: "custom", path: ["counted"], message: "must be true exactly when the counts are counted" });
16856
+ }
16857
+ if (v.observed !== null && v.observed.phase !== v.phase) {
16858
+ ctx.addIssue({ code: "custom", path: ["observed", "phase"], message: "must be the phase under review" });
16859
+ }
16860
+ if (v.state === "closed") {
16861
+ v.parts.forEach((p, i) => {
16862
+ if (p.decision === void 0) {
16863
+ ctx.addIssue({ code: "custom", path: ["parts", i, "decision"], message: "every part of a closed review is decided" });
16864
+ }
16865
+ });
16866
+ }
16867
+ });
16868
+ }
16869
+ var phaseReviewOpenSchema = reviewedViewSchema("open");
16870
+ var phaseReviewClosedSchema = reviewedViewSchema("closed");
16871
+ var phaseReviewViewSchema = external_exports.discriminatedUnion("state", [
16872
+ phaseReviewNoneSchema,
16873
+ phaseReviewOpenSchema,
16874
+ phaseReviewClosedSchema
16875
+ ]);
16876
+
16019
16877
  // ../events/dist/client-version.js
16020
16878
  var CLIENT_VERSION_HEADER = "x-halfcycle-client";
16021
16879
  var TYPED_APPROVAL_OFF_SENTENCE = "Approving by typing is switched off for now.";
16880
+ var TYPED_REVIEW_OFF_SENTENCE = "Reviewing by typing is switched off for now.";
16022
16881
  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.`;
16023
16882
 
16024
16883
  // ../events/dist/scope-notices.js
@@ -16068,7 +16927,7 @@ function sentBackNotice(entry) {
16068
16927
  return [
16069
16928
  `Before anything else: the project's owner sent the scope of phase ${entry.number}, "${entry.phase}", back with these notes.`,
16070
16929
  ...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)}\`.`
16930
+ `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.`
16072
16931
  ].join("\n");
16073
16932
  }
16074
16933
  function waitingNotice(entry, offerTyping) {
@@ -16086,6 +16945,42 @@ function scopeNoticeText(scope, options) {
16086
16945
  };
16087
16946
  }
16088
16947
 
16948
+ // ../events/dist/review-notices.js
16949
+ var reviewNoticeQuestionSchema = external_exports.object({
16950
+ phase: scopePhaseSchema,
16951
+ number: reviewPhaseNumberSchema,
16952
+ part: systemPartIdSchema,
16953
+ partName: systemPartSchema.shape.name,
16954
+ question: reviewNoteSchema
16955
+ }).strict();
16956
+ var reviewNoticeSentBackSchema = external_exports.object({
16957
+ phase: scopePhaseSchema,
16958
+ number: reviewPhaseNumberSchema,
16959
+ part: systemPartIdSchema,
16960
+ partName: systemPartSchema.shape.name,
16961
+ note: reviewNoteSchema
16962
+ }).strict();
16963
+ var reviewNoticesSchema = external_exports.object({
16964
+ questions: external_exports.array(reviewNoticeQuestionSchema).max(REVIEW_PARTS_MAX),
16965
+ sentBack: external_exports.array(reviewNoticeSentBackSchema).max(REVIEW_PARTS_MAX)
16966
+ }).strict().refine((v) => v.questions.length > 0 || v.sentBack.length > 0, {
16967
+ message: "carried only when a question waits or a part was sent back"
16968
+ });
16969
+ function phaseCalled(phase, number4) {
16970
+ return number4 === null ? `phase "${phase}"` : `phase ${number4}, "${phase}"`;
16971
+ }
16972
+ function questionNotice(entry) {
16973
+ const answer = `${HALFCYCLE_COMMAND} review ${quoteForShell(entry.phase)} --answer ${quoteForShell(entry.part)} "<your answer>"`;
16974
+ return `Before anything else: the project's owner asked about "${entry.partName}" in the review of ${phaseCalled(entry.phase, entry.number)}: ${entry.question} Answer it with ${answer}.`;
16975
+ }
16976
+ function sentBackNotice2(entry) {
16977
+ const where = entry.number === null ? `in phase "${entry.phase}"` : `in phase ${entry.number}, "${entry.phase}",`;
16978
+ return `Before anything else: the project's owner sent "${entry.partName}" ${where} back to building: ${entry.note} Add or reopen the tasks that build it. When they are finished, run the close step again.`;
16979
+ }
16980
+ function reviewNoticeText(review) {
16981
+ return [...review.questions.map(questionNotice), ...review.sentBack.map(sentBackNotice2)];
16982
+ }
16983
+
16089
16984
  // ../events/dist/device-auth.js
16090
16985
  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");
16091
16986
  var DEVICE_AUTH_STATUS = {
@@ -16273,6 +17168,56 @@ var chatScopeResultSchema = external_exports.discriminatedUnion("outcome", [
16273
17168
  bareOutcome("unavailable")
16274
17169
  ]);
16275
17170
 
17171
+ // ../events/dist/review-chat.js
17172
+ var chatReviewAcceptRequestSchema = external_exports.object({
17173
+ act: external_exports.literal("accept"),
17174
+ reference: phaseIdentitySchema,
17175
+ reviewVersion: external_exports.number().int().min(0),
17176
+ parts: external_exports.array(systemPartIdSchema).max(REVIEW_PARTS_MAX)
17177
+ }).strict();
17178
+ function notedRequest(act) {
17179
+ return external_exports.object({
17180
+ act: external_exports.literal(act),
17181
+ reference: phaseIdentitySchema,
17182
+ reviewVersion: external_exports.number().int().min(0),
17183
+ part: systemPartIdSchema,
17184
+ note: reviewNoteWireSchema
17185
+ }).strict();
17186
+ }
17187
+ var chatReviewQuestionRequestSchema = notedRequest("question");
17188
+ var chatReviewSendBackRequestSchema = notedRequest("send-back");
17189
+ var chatReviewCarryRequestSchema = notedRequest("carry");
17190
+ var chatReviewDropRequestSchema = notedRequest("drop");
17191
+ var chatReviewRequestSchema = external_exports.discriminatedUnion("act", [
17192
+ chatReviewAcceptRequestSchema,
17193
+ chatReviewQuestionRequestSchema,
17194
+ chatReviewSendBackRequestSchema,
17195
+ chatReviewCarryRequestSchema,
17196
+ chatReviewDropRequestSchema
17197
+ ]);
17198
+ function bareOutcome2(outcome) {
17199
+ return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
17200
+ }
17201
+ var chatReviewResultSchema = external_exports.discriminatedUnion("outcome", [
17202
+ ...reviewDecisionResultSchema.options,
17203
+ bareOutcome2("show-first"),
17204
+ bareOutcome2("not-offered")
17205
+ ]);
17206
+ var reviewAnswerRequestSchema = external_exports.object({
17207
+ part: systemPartIdSchema,
17208
+ answer: statementText(REVIEW_ANSWER_MAX)
17209
+ }).strict();
17210
+ var reviewAnswerRecordedSchema = external_exports.object({
17211
+ outcome: external_exports.literal("recorded"),
17212
+ name: systemPartSchema.shape.name
17213
+ }).strict();
17214
+ var reviewAnswerResultSchema = external_exports.discriminatedUnion("outcome", [
17215
+ reviewAnswerRecordedSchema,
17216
+ bareOutcome2("no-question"),
17217
+ reviewRefusedSchema,
17218
+ bareOutcome2("unavailable")
17219
+ ]);
17220
+
16276
17221
  // dist/hook-protocol.js
16277
17222
  import { writeSync } from "node:fs";
16278
17223
  function emitBlock(reason) {
@@ -16332,8 +17277,6 @@ function emitNeverConfigured(exitCode, reason, engagementId) {
16332
17277
  writeSync(2, neverConfiguredReport(reason, void 0, engagementId));
16333
17278
  process.exitCode = exitCode;
16334
17279
  }
16335
- var SIGN_IN_COMMAND = '"npx halfcycle"';
16336
- var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
16337
17280
  function namesTheSignInCommand(text) {
16338
17281
  return text.includes(SIGN_IN_COMMAND);
16339
17282
  }
@@ -16660,13 +17603,16 @@ import { spawnSync } from "node:child_process";
16660
17603
  import { existsSync, readFileSync } from "node:fs";
16661
17604
  import { relative } from "node:path";
16662
17605
  var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
16663
- function git(args2, cwd, timeoutMs) {
17606
+ function spawnGit(args2, cwd, timeoutMs) {
16664
17607
  const argv = COLOR_AWARE_SUBCOMMANDS.has(args2[0] ?? "") ? [args2[0], "--no-color", ...args2.slice(1)] : args2;
16665
- const result = spawnSync("git", argv, {
17608
+ return spawnSync("git", argv, {
16666
17609
  cwd,
16667
17610
  encoding: "utf-8",
16668
17611
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
16669
17612
  });
17613
+ }
17614
+ function git(args2, cwd, timeoutMs) {
17615
+ const result = spawnGit(args2, cwd, timeoutMs);
16670
17616
  if (result.error) {
16671
17617
  const missing = result.error.code === "ENOENT" && existsSync(cwd);
16672
17618
  return { ok: false, message: `git error: ${result.error.message}`, ...missing ? { gitMissing: true } : {} };
@@ -16828,6 +17774,124 @@ function getWiringStatus(pathspecs, cwd) {
16828
17774
  ...pathspecs
16829
17775
  ], cwd, WIRING_STATUS_TIMEOUT_MS);
16830
17776
  }
17777
+ var OBSERVE_GIT_TIMEOUT_MS = 5e3;
17778
+ function isFullCommit(value) {
17779
+ return closeCommitSchema.safeParse(value).success;
17780
+ }
17781
+ function notACommit(name) {
17782
+ return { ok: false, message: `${name} is not a full commit id` };
17783
+ }
17784
+ function headCommit(repoRoot) {
17785
+ const result = git(["rev-parse", "--verify", "HEAD^{commit}"], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17786
+ if (!result.ok)
17787
+ return result;
17788
+ const commit = result.value.trim();
17789
+ return isFullCommit(commit) ? { ok: true, value: commit } : notACommit("HEAD");
17790
+ }
17791
+ function headCommittedAt(head, repoRoot) {
17792
+ if (!isFullCommit(head))
17793
+ return notACommit("head");
17794
+ const result = git(["log", "-1", "--format=%cI", head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17795
+ if (!result.ok)
17796
+ return result;
17797
+ const when = new Date(result.value.trim());
17798
+ if (Number.isNaN(when.getTime()))
17799
+ return { ok: false, message: "git gave no readable commit time" };
17800
+ const iso = when.toISOString();
17801
+ return utcIso8601.safeParse(iso).success ? { ok: true, value: iso } : { ok: false, message: "git gave no readable commit time" };
17802
+ }
17803
+ function isAncestor(start, head, repoRoot) {
17804
+ if (!isFullCommit(start))
17805
+ return notACommit("start");
17806
+ if (!isFullCommit(head))
17807
+ return notACommit("head");
17808
+ const result = spawnGit(["merge-base", "--is-ancestor", start, head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17809
+ if (result.error)
17810
+ return { ok: false, message: `git error: ${result.error.message}` };
17811
+ if (result.status === 0)
17812
+ return { ok: true, value: true };
17813
+ if (result.status === 1)
17814
+ return { ok: true, value: false };
17815
+ const stderr = result.stderr?.trim() ?? "";
17816
+ return { ok: false, message: `git merge-base failed: ${stderr || "non-zero exit"}` };
17817
+ }
17818
+ function commitCount(start, head, repoRoot) {
17819
+ if (!isFullCommit(start))
17820
+ return notACommit("start");
17821
+ if (!isFullCommit(head))
17822
+ return notACommit("head");
17823
+ const result = git(["rev-list", "--count", `${start}..${head}`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17824
+ if (!result.ok)
17825
+ return result;
17826
+ const text = result.value.trim();
17827
+ if (!/^\d+$/.test(text))
17828
+ return { ok: false, message: "git gave no readable commit count" };
17829
+ return { ok: true, value: Number(text) };
17830
+ }
17831
+ function rangeNameStatus(start, head, repoRoot) {
17832
+ if (!isFullCommit(start))
17833
+ return notACommit("start");
17834
+ if (!isFullCommit(head))
17835
+ return notACommit("head");
17836
+ const result = git(["diff", "--name-status", "--no-renames", "-z", start, head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17837
+ if (!result.ok)
17838
+ return result;
17839
+ return parseNameStatusZ(result.value);
17840
+ }
17841
+ function parseNameStatusZ(output) {
17842
+ if (output === "")
17843
+ return { ok: true, value: [] };
17844
+ if (!output.endsWith("\0"))
17845
+ return { ok: false, message: "git gave an unreadable list of changed files" };
17846
+ const fields = output.slice(0, -1).split("\0");
17847
+ if (fields.length % 2 !== 0)
17848
+ return { ok: false, message: "git gave an unreadable list of changed files" };
17849
+ const entries = [];
17850
+ for (let i = 0; i < fields.length; i += 2) {
17851
+ const status = fields[i];
17852
+ const path = fields[i + 1];
17853
+ if (!/^[A-Z][0-9]*$/.test(status) || status.startsWith("R") || status.startsWith("C") || path === "") {
17854
+ return { ok: false, message: "git gave an unreadable list of changed files" };
17855
+ }
17856
+ entries.push({ status: status.charAt(0), path });
17857
+ }
17858
+ return { ok: true, value: entries };
17859
+ }
17860
+ function readCodeLinkFacts(repoRoot, head) {
17861
+ const nothing = { repositoryAddress: null, headOnGitHub: false };
17862
+ if (!isFullCommit(head))
17863
+ return nothing;
17864
+ try {
17865
+ const remote = currentRemote(repoRoot);
17866
+ const url2 = git(["remote", "get-url", "--", remote], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17867
+ if (!url2.ok)
17868
+ return nothing;
17869
+ const repositoryAddress = githubRepositoryAddress(url2.value.trim());
17870
+ if (repositoryAddress === null)
17871
+ return nothing;
17872
+ const refs = git(["for-each-ref", "--contains", head, "--format=%(refname)", `refs/remotes/${remote}/`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17873
+ if (!refs.ok)
17874
+ return nothing;
17875
+ const facts = { repositoryAddress, headOnGitHub: refs.value.trim() !== "" };
17876
+ return codeLinkFactsSchema.safeParse(facts).success ? facts : nothing;
17877
+ } catch {
17878
+ return nothing;
17879
+ }
17880
+ }
17881
+ function currentRemote(repoRoot) {
17882
+ const symbolic = git(["symbolic-ref", "--quiet", "HEAD"], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17883
+ if (!symbolic.ok)
17884
+ return "origin";
17885
+ const ref = symbolic.value.trim();
17886
+ if (!ref.startsWith("refs/heads/"))
17887
+ return "origin";
17888
+ const branch = ref.slice("refs/heads/".length);
17889
+ const configured = git(["config", "--get", `branch.${branch}.remote`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
17890
+ if (!configured.ok)
17891
+ return "origin";
17892
+ const remote = configured.value.trim();
17893
+ return remote === "" || remote === "." ? "origin" : remote;
17894
+ }
16831
17895
 
16832
17896
  // dist/screen/secret-shape.js
16833
17897
  function screenExcerpts(excerpts) {
@@ -18495,7 +19559,10 @@ var SESSION_NOTICE_SUFFIX = {
18495
19559
  "no-repository": ".norepo",
18496
19560
  "credential-rejected": ".credential",
18497
19561
  "credential-renewed-for-this-run": ".renewed",
18498
- "credential-missing": ".nocredential"
19562
+ "credential-missing": ".nocredential",
19563
+ "observe-no-start-here": ".observe-nostart",
19564
+ "observe-not-on-branch": ".observe-nobranch",
19565
+ "observe-no-start-recorded": ".observe-norecord"
18499
19566
  };
18500
19567
  function firstNoticeThisSession(notice, folder, sessionId) {
18501
19568
  if (!sessionId)
@@ -18590,6 +19657,14 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
18590
19657
  }
18591
19658
  }
18592
19659
  }
19660
+ function observeThrottlePath(folder) {
19661
+ return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}.observe-throttle`);
19662
+ }
19663
+ function observeNoticesPath(folder, sessionId) {
19664
+ if (!sessionId)
19665
+ return null;
19666
+ return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}.observe-said`);
19667
+ }
18593
19668
 
18594
19669
  // dist/seat-configuration.js
18595
19670
  import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync2 } from "node:fs";
@@ -18923,9 +19998,11 @@ function collectDiff(repoRoot, base) {
18923
19998
  getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
18924
19999
  };
18925
20000
  }
18926
- async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
20001
+ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration, afterPass) {
18927
20002
  const reply = hookReply(STOP_HOOK_EVENT[kind]);
18928
20003
  await evaluateSession(input, kind, reply, readSeat);
20004
+ if (afterPass)
20005
+ await afterPass(reply);
18929
20006
  reply.flush();
18930
20007
  }
18931
20008
  async function evaluateSession(input, kind, reply, readSeat) {
@@ -19106,17 +20183,328 @@ async function runSubagentStop() {
19106
20183
  await runSessionDiff(input, "subagent-stop");
19107
20184
  }
19108
20185
 
20186
+ // dist/observed.js
20187
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
20188
+ function emptyTally() {
20189
+ return { added: 0, changed: 0, removed: 0, testFilesAdded: 0 };
20190
+ }
20191
+ function kindOf(status) {
20192
+ if (status === "A")
20193
+ return "added";
20194
+ if (status === "D")
20195
+ return "removed";
20196
+ return "changed";
20197
+ }
20198
+ function countObservedChanges(entries, description) {
20199
+ const methodFiles = { added: 0, changed: 0, removed: 0 };
20200
+ const byPart = /* @__PURE__ */ new Map();
20201
+ const outside = emptyTally();
20202
+ const paths = [];
20203
+ const listed = /* @__PURE__ */ new Set();
20204
+ for (const entry of entries) {
20205
+ const kind = kindOf(entry.status);
20206
+ if (isMethodFile(entry.path)) {
20207
+ methodFiles[kind] += 1;
20208
+ continue;
20209
+ }
20210
+ const owner = ownerOf(entry.path, description);
20211
+ let tally;
20212
+ if (owner === null) {
20213
+ tally = outside;
20214
+ if (entry.path.length >= 1 && entry.path.length <= OBSERVED_PATH_MAX && paths.length < OBSERVED_PATHS_LISTED_MAX && !listed.has(entry.path)) {
20215
+ paths.push(entry.path);
20216
+ listed.add(entry.path);
20217
+ }
20218
+ } else {
20219
+ tally = byPart.get(owner) ?? emptyTally();
20220
+ byPart.set(owner, tally);
20221
+ }
20222
+ tally[kind] += 1;
20223
+ if (kind === "added" && isTestFile(entry.path))
20224
+ tally.testFilesAdded += 1;
20225
+ }
20226
+ const parts = description.parts.filter((part) => byPart.has(part.id)).map((part) => ({ part: part.id, ...byPart.get(part.id) }));
20227
+ return { parts, methodFiles, outsideEveryPart: { ...outside, paths } };
20228
+ }
20229
+ var OBSERVE_MIN_INTERVAL_MS = 6e4;
20230
+ var OBSERVE_RECHECK_MS = 6e5;
20231
+ var OBSERVE_REQUEST_DEADLINE_MS = 1500;
20232
+ async function requestJson(fetchFn, url2, init) {
20233
+ const controller = new AbortController();
20234
+ const timer = setTimeout(() => controller.abort(), OBSERVE_REQUEST_DEADLINE_MS);
20235
+ try {
20236
+ const response = await fetchFn(url2, { ...init, signal: controller.signal });
20237
+ let body = null;
20238
+ try {
20239
+ body = await response.json();
20240
+ } catch {
20241
+ body = null;
20242
+ }
20243
+ if (controller.signal.aborted)
20244
+ return { kind: "unreachable" };
20245
+ return { kind: "answer", status: response.status, body };
20246
+ } catch {
20247
+ return { kind: "unreachable" };
20248
+ } finally {
20249
+ clearTimeout(timer);
20250
+ }
20251
+ }
20252
+ function refusalReason(status) {
20253
+ return status >= 500 ? "unreachable" : "refused";
20254
+ }
20255
+ async function attemptObservation(opts) {
20256
+ const { repoRoot, head, config: config2, controlOrigin, fetchFn } = opts;
20257
+ const base = `${controlOrigin}/engagements/${encodeURIComponent(config2.guardEngagementId)}/observed`;
20258
+ const authorization = `Bearer ${config2.guardServiceToken}`;
20259
+ const basisAnswer = await requestJson(fetchFn, `${base}/basis`, {
20260
+ method: "GET",
20261
+ headers: { Authorization: authorization }
20262
+ });
20263
+ if (basisAnswer.kind === "unreachable")
20264
+ return { outcome: "not-sent", reason: "unreachable" };
20265
+ if (basisAnswer.status < 200 || basisAnswer.status >= 300) {
20266
+ return { outcome: "not-sent", reason: refusalReason(basisAnswer.status) };
20267
+ }
20268
+ const parsedBasis = observedBasisSchema.safeParse(basisAnswer.body);
20269
+ if (!parsedBasis.success)
20270
+ return { outcome: "not-sent", reason: "refused" };
20271
+ const basis = parsedBasis.data;
20272
+ if (basis.outcome === "nothing-to-count")
20273
+ return { outcome: "not-sent", reason: "nothing-to-count" };
20274
+ if (basis.outcome === "cannot-count")
20275
+ return { outcome: "not-sent", reason: "cannot-count", phase: basis.phase };
20276
+ if (repoRoot === null || !refExists(basis.startCommit, repoRoot)) {
20277
+ return { outcome: "not-sent", reason: "start-not-here" };
20278
+ }
20279
+ if (head === null)
20280
+ return { outcome: "not-sent", reason: "start-not-on-branch" };
20281
+ const ancestor = isAncestor(basis.startCommit, head, repoRoot);
20282
+ if (!ancestor.ok)
20283
+ return { outcome: "not-sent", reason: "unreachable" };
20284
+ if (!ancestor.value)
20285
+ return { outcome: "not-sent", reason: "start-not-on-branch" };
20286
+ const commits = commitCount(basis.startCommit, head, repoRoot);
20287
+ const committedAt = headCommittedAt(head, repoRoot);
20288
+ const entries = rangeNameStatus(basis.startCommit, head, repoRoot);
20289
+ if (!commits.ok || !committedAt.ok || !entries.ok)
20290
+ return { outcome: "not-sent", reason: "unreachable" };
20291
+ const report = {
20292
+ phase: basis.phase,
20293
+ startCommit: basis.startCommit,
20294
+ headCommit: head,
20295
+ headCommittedAt: committedAt.value,
20296
+ commits: commits.value,
20297
+ descriptionVersion: basis.descriptionVersion,
20298
+ ...countObservedChanges(entries.value, basis.description),
20299
+ codeLink: readCodeLinkFacts(repoRoot, head)
20300
+ };
20301
+ if (!observedChangesReportSchema.safeParse(report).success)
20302
+ return { outcome: "not-sent", reason: "refused" };
20303
+ const sent = await requestJson(fetchFn, base, {
20304
+ method: "POST",
20305
+ headers: { Authorization: authorization, "Content-Type": "application/json" },
20306
+ body: JSON.stringify(report)
20307
+ });
20308
+ if (sent.kind === "unreachable")
20309
+ return { outcome: "not-sent", reason: "unreachable" };
20310
+ if (sent.status === 409)
20311
+ return { outcome: "not-sent", reason: "basis-moved" };
20312
+ if (sent.status < 200 || sent.status >= 300)
20313
+ return { outcome: "not-sent", reason: refusalReason(sent.status) };
20314
+ const answer = observedChangesReportResultSchema.safeParse(sent.body);
20315
+ if (!answer.success)
20316
+ return { outcome: "not-sent", reason: "refused" };
20317
+ return answer.data.outcome === "kept" ? { outcome: "kept" } : { outcome: "recorded", answer: answer.data };
20318
+ }
20319
+ function readThrottle(folder) {
20320
+ try {
20321
+ const parsed = JSON.parse(readFileSync9(observeThrottlePath(folder), "utf-8"));
20322
+ if (typeof parsed !== "object" || parsed === null)
20323
+ return null;
20324
+ const { attemptedAt, accepted } = parsed;
20325
+ if (typeof attemptedAt !== "number")
20326
+ return null;
20327
+ const sent = accepted;
20328
+ const usable = sent !== null && sent !== void 0 && typeof sent.head === "string" && typeof sent.at === "number";
20329
+ return { attemptedAt, accepted: usable ? { head: sent.head, at: sent.at } : null };
20330
+ } catch {
20331
+ return null;
20332
+ }
20333
+ }
20334
+ function writeThrottle(folder, state) {
20335
+ try {
20336
+ writeFileSync4(observeThrottlePath(folder), JSON.stringify(state), "utf-8");
20337
+ } catch {
20338
+ }
20339
+ }
20340
+ function throttleAllows(state, head, now) {
20341
+ if (state === null)
20342
+ return true;
20343
+ if (now - state.attemptedAt < OBSERVE_MIN_INTERVAL_MS)
20344
+ return false;
20345
+ if (state.accepted === null)
20346
+ return true;
20347
+ if (head === null || head !== state.accepted.head)
20348
+ return true;
20349
+ return now - state.accepted.at >= OBSERVE_RECHECK_MS;
20350
+ }
20351
+ function locate(cwd) {
20352
+ const root = getRepoRoot(cwd);
20353
+ const repoRoot = root.ok ? root.value : null;
20354
+ const read = repoRoot === null ? null : headCommit(repoRoot);
20355
+ return { repoRoot, folder: repoRoot ?? cwd, head: read !== null && read.ok ? read.value : null };
20356
+ }
20357
+ function recordAttempt(folder, prior, head, at, attempt2) {
20358
+ const accepted = (attempt2.outcome === "recorded" || attempt2.outcome === "kept") && head !== null ? { head, at } : prior?.accepted ?? null;
20359
+ writeThrottle(folder, { attemptedAt: at, accepted });
20360
+ }
20361
+ async function runObserve(deps = {}) {
20362
+ let result;
20363
+ try {
20364
+ const configured = readConfig();
20365
+ if (!configured.config) {
20366
+ result = { outcome: "not-sent", reason: "not-configured" };
20367
+ } else {
20368
+ const now = deps.now ?? Date.now;
20369
+ const where = locate(deps.cwd ?? process.cwd());
20370
+ const prior = readThrottle(where.folder);
20371
+ const attempt2 = await attemptObservation({
20372
+ repoRoot: where.repoRoot,
20373
+ head: where.head,
20374
+ config: configured.config,
20375
+ controlOrigin: readControlOrigin().origin,
20376
+ fetchFn: deps.fetchFn ?? globalThis.fetch
20377
+ });
20378
+ recordAttempt(where.folder, prior, where.head, now(), attempt2);
20379
+ result = attempt2.outcome === "not-sent" ? { outcome: "not-sent", reason: attempt2.reason } : { outcome: attempt2.outcome };
20380
+ }
20381
+ } catch {
20382
+ result = { outcome: "not-sent", reason: "unreachable" };
20383
+ }
20384
+ return observeRunResultSchema.parse(result);
20385
+ }
20386
+ async function observeChanges(sessionId, reply, deps = {}) {
20387
+ try {
20388
+ const code = process.exitCode;
20389
+ if (code !== void 0 && code !== null && Number(code) !== 0)
20390
+ return;
20391
+ const configured = readConfig();
20392
+ if (!configured.config)
20393
+ return;
20394
+ const now = (deps.now ?? Date.now)();
20395
+ const where = locate(deps.cwd ?? process.cwd());
20396
+ const prior = readThrottle(where.folder);
20397
+ if (!throttleAllows(prior, where.head, now))
20398
+ return;
20399
+ writeThrottle(where.folder, { attemptedAt: now, accepted: prior?.accepted ?? null });
20400
+ const attempt2 = await attemptObservation({
20401
+ repoRoot: where.repoRoot,
20402
+ head: where.head,
20403
+ config: configured.config,
20404
+ controlOrigin: readControlOrigin().origin,
20405
+ fetchFn: deps.fetchFn ?? globalThis.fetch
20406
+ });
20407
+ recordAttempt(where.folder, prior, where.head, now, attempt2);
20408
+ for (const line2 of noticesFor(attempt2, where.folder, sessionId))
20409
+ reply.notice(line2);
20410
+ } catch {
20411
+ }
20412
+ }
20413
+ 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.";
20414
+ 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.";
20415
+ function r3NoStartRecorded(phase) {
20416
+ 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>"`;
20417
+ }
20418
+ function r4OutsideThePlan(parts) {
20419
+ 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:";
20420
+ const lines = parts.map((entry) => {
20421
+ const said = entry.said.length === 0 ? NO_TASK_SAID_LINE : entry.said.map((task) => agentSaid(task)).join("; ");
20422
+ return `"${entry.name}": ${said}`;
20423
+ });
20424
+ return [first, ...lines, "The project's owner sees them again at the review."].join("\n");
20425
+ }
20426
+ function r5OutsideEveryPart(files, example) {
20427
+ if (files === 1) {
20428
+ 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.`;
20429
+ }
20430
+ 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.`;
20431
+ }
20432
+ function readSaid(path) {
20433
+ const empty = { parts: {}, outsideFiles: 0 };
20434
+ if (path === null)
20435
+ return empty;
20436
+ try {
20437
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
20438
+ const parts = {};
20439
+ if (typeof parsed.parts === "object" && parsed.parts !== null) {
20440
+ for (const [part, reasons] of Object.entries(parsed.parts)) {
20441
+ if (Array.isArray(reasons))
20442
+ parts[part] = reasons.filter((r) => typeof r === "string");
20443
+ }
20444
+ }
20445
+ return { parts, outsideFiles: typeof parsed.outsideFiles === "number" ? parsed.outsideFiles : 0 };
20446
+ } catch {
20447
+ return empty;
20448
+ }
20449
+ }
20450
+ function writeSaid(path, state) {
20451
+ if (path === null)
20452
+ return;
20453
+ try {
20454
+ writeFileSync4(path, JSON.stringify(state), "utf-8");
20455
+ } catch {
20456
+ }
20457
+ }
20458
+ function reasonsOf(entry) {
20459
+ return entry.said.filter((task) => task.whyOutsidePlan !== void 0).map((task) => `${task.title}\0${task.whyOutsidePlan}`);
20460
+ }
20461
+ function noticesFor(attempt2, folder, sessionId) {
20462
+ if (attempt2.outcome === "not-sent") {
20463
+ if (attempt2.reason === "start-not-here") {
20464
+ return firstNoticeThisSession("observe-no-start-here", folder, sessionId) ? [R1_START_NOT_HERE] : [];
20465
+ }
20466
+ if (attempt2.reason === "start-not-on-branch") {
20467
+ return firstNoticeThisSession("observe-not-on-branch", folder, sessionId) ? [R2_START_NOT_ON_BRANCH] : [];
20468
+ }
20469
+ if (attempt2.reason === "cannot-count" && attempt2.phase !== void 0) {
20470
+ return firstNoticeThisSession("observe-no-start-recorded", folder, sessionId) ? [r3NoStartRecorded(attempt2.phase)] : [];
20471
+ }
20472
+ return [];
20473
+ }
20474
+ if (attempt2.outcome === "kept")
20475
+ return [];
20476
+ const { notInPlan, outside } = attempt2.answer;
20477
+ const path = observeNoticesPath(folder, sessionId);
20478
+ const prior = readSaid(path);
20479
+ const notices = [];
20480
+ const parts = {};
20481
+ let grew = false;
20482
+ for (const entry of notInPlan) {
20483
+ const reasons = reasonsOf(entry);
20484
+ parts[entry.part] = reasons;
20485
+ const before = prior.parts[entry.part];
20486
+ if (before === void 0 || reasons.some((reason) => !before.includes(reason)))
20487
+ grew = true;
20488
+ }
20489
+ if (grew)
20490
+ notices.push(r4OutsideThePlan(notInPlan));
20491
+ let outsideFiles = outside.files;
20492
+ if (outside.files > prior.outsideFiles) {
20493
+ if (outside.example !== null)
20494
+ notices.push(r5OutsideEveryPart(outside.files, outside.example));
20495
+ else
20496
+ outsideFiles = prior.outsideFiles;
20497
+ }
20498
+ writeSaid(path, { parts, outsideFiles });
20499
+ return notices;
20500
+ }
20501
+
19109
20502
  // dist/hooks/stop.js
19110
- async function runStop() {
20503
+ async function runStop(observeDeps = {}) {
19111
20504
  const input = await readHookStdin();
19112
- await runSessionDiff(input, "stop");
20505
+ await runSessionDiff(input, "stop", readSeatConfiguration, (reply) => observeChanges(input.session_id, reply, observeDeps));
19113
20506
  }
19114
20507
 
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
20508
  // dist/hooks/scope-phrase.js
19121
20509
  var APPROVE_PREFIX = /^approve /i;
19122
20510
  var SEND_BACK_PREFIX = /^send back /i;
@@ -19150,6 +20538,128 @@ function matchScopePhraseShape(shape, shown) {
19150
20538
  return { kind: "show-first", act: "send-back" };
19151
20539
  }
19152
20540
 
20541
+ // dist/hooks/review-phrase.js
20542
+ var VERBS = [
20543
+ [/^accept /i, "accept"],
20544
+ [/^question /i, "question"],
20545
+ [/^send back /i, "send-back"],
20546
+ [/^carry /i, "carry"],
20547
+ [/^drop /i, "drop"]
20548
+ ];
20549
+ function reviewPhraseShape(prompt) {
20550
+ if (typeof prompt !== "string")
20551
+ return null;
20552
+ const line2 = prompt.trim();
20553
+ if (LINE_BREAK.test(line2))
20554
+ return null;
20555
+ for (const [prefix, act] of VERBS) {
20556
+ const verb = prefix.exec(line2);
20557
+ if (verb === null)
20558
+ continue;
20559
+ const rest = line2.slice(verb[0].length);
20560
+ if (act === "accept") {
20561
+ if (!rest.includes("/"))
20562
+ return isValidPhaseIdentity(rest) ? { kind: "phase", reference: rest } : null;
20563
+ return partShape(act, rest, null);
20564
+ }
20565
+ const colon = rest.indexOf(":");
20566
+ if (colon === -1)
20567
+ return act === "send-back" ? partShape(act, rest, null) : null;
20568
+ return partShape(act, rest.slice(0, colon), rest.slice(colon + 1).trim());
20569
+ }
20570
+ return null;
20571
+ }
20572
+ function partShape(act, head, words) {
20573
+ const slash = head.indexOf("/");
20574
+ if (slash === -1)
20575
+ return null;
20576
+ const reference = head.slice(0, slash);
20577
+ const part = head.slice(slash + 1);
20578
+ if (!isValidPhaseIdentity(reference) || !SYSTEM_PART_ID_PATTERN.test(part))
20579
+ return null;
20580
+ return { kind: "part", act, reference, part, words };
20581
+ }
20582
+ function matchReviewPhraseShape(shape, reviews) {
20583
+ const entry = resolvePhaseReference(shape.reference, reviews);
20584
+ if (shape.kind === "phase") {
20585
+ if (entry === null)
20586
+ return { kind: "nothing" };
20587
+ return {
20588
+ kind: "accept",
20589
+ entry,
20590
+ reference: shape.reference,
20591
+ parts: entry.parts.filter((p) => p.built && p.decidable)
20592
+ };
20593
+ }
20594
+ if (entry === null)
20595
+ return { kind: "show-first", act: shape.act };
20596
+ const part = entry.parts.find((p) => p.part === shape.part);
20597
+ if (part === void 0)
20598
+ return { kind: "not-listed", act: shape.act, entry, part: shape.part };
20599
+ return { kind: "decide", act: shape.act, entry, reference: shape.reference, part, words: shape.words };
20600
+ }
20601
+
20602
+ // dist/hooks/shown-record.js
20603
+ import { chmodSync as chmodSync2, readFileSync as readFileSync10, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
20604
+ import { homedir as homedir4, platform as platform2 } from "node:os";
20605
+ import { join as join8 } from "node:path";
20606
+ function recordDir(engagementId, home) {
20607
+ if (!isEngagementId(engagementId))
20608
+ return null;
20609
+ return join8(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
20610
+ }
20611
+ function readShownRecord(engagementId, home) {
20612
+ const dir = recordDir(engagementId, home);
20613
+ if (dir === null)
20614
+ return { entries: [], reviews: [] };
20615
+ try {
20616
+ return parseShownRecord(readFileSync10(join8(dir, SHOWN_PLANS_FILENAME), "utf-8"));
20617
+ } catch {
20618
+ return { entries: [], reviews: [] };
20619
+ }
20620
+ }
20621
+ function movedPart(part, act) {
20622
+ if (act === void 0)
20623
+ return part;
20624
+ return {
20625
+ ...part,
20626
+ decidable: false,
20627
+ awaitingAnswer: part.awaitingAnswer || act === "question",
20628
+ sentBack: part.sentBack || act === "send-back"
20629
+ };
20630
+ }
20631
+ function moveListedReviewForward(engagementId, home, listed, reviewVersion, decided) {
20632
+ let temp = null;
20633
+ try {
20634
+ const dir = recordDir(engagementId, home);
20635
+ if (dir === null)
20636
+ return false;
20637
+ const path = join8(dir, SHOWN_PLANS_FILENAME);
20638
+ const current = parseShownRecord(readFileSync10(path, "utf-8"));
20639
+ const at = current.reviews.findIndex((r) => r.phase === listed.phase && r.shownAt === listed.shownAt && r.reviewVersion === listed.reviewVersion);
20640
+ if (at === -1)
20641
+ return false;
20642
+ const acts = new Map(decided.map((d) => [d.part, d.act]));
20643
+ const reviews = current.reviews.map((review, i) => i === at ? { ...review, reviewVersion, parts: review.parts.map((p) => movedPart(p, acts.get(p.part))) } : review);
20644
+ const body = serialiseShownRecord({ entries: current.entries, reviews });
20645
+ temp = join8(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
20646
+ writeFileSync5(temp, body, { mode: 384 });
20647
+ if (platform2() !== "win32")
20648
+ chmodSync2(temp, 384);
20649
+ renameSync3(temp, path);
20650
+ return true;
20651
+ } catch {
20652
+ if (temp !== null) {
20653
+ try {
20654
+ unlinkSync4(temp);
20655
+ } catch {
20656
+ return false;
20657
+ }
20658
+ }
20659
+ return false;
20660
+ }
20661
+ }
20662
+
19153
20663
  // dist/hooks/user-prompt-submit-lines.js
19154
20664
  var TELL_THE_PERSON = "Tell the person.";
19155
20665
  function line(text) {
@@ -19225,14 +20735,210 @@ function lineForAnswer(act, phase, answer) {
19225
20735
  }
19226
20736
  }
19227
20737
  }
20738
+ var NOTHING_RECORDED = "Nothing was recorded";
20739
+ var LIST_AGAIN = 'Say "review what was built" to list it again';
20740
+ function reviewNotAttendedLine() {
20741
+ return line(`${NOTHING_RECORDED}: reviewing by typing counts only when a person types it into an open session.`);
20742
+ }
20743
+ function reviewNotSignedInLine() {
20744
+ return line(`${NOTHING_RECORDED}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
20745
+ }
20746
+ function reviewCannotReachLine() {
20747
+ return line(`${NOTHING_RECORDED}: Halfcycle could not be reached just now. Type the line again in a minute.`);
20748
+ }
20749
+ function reviewSwitchedOffLine() {
20750
+ return line(`${NOTHING_RECORDED}. ${TYPED_REVIEW_OFF_SENTENCE}`);
20751
+ }
20752
+ function reviewNotOwnerLine() {
20753
+ return line(`${NOTHING_RECORDED}: only the project's owner can review this phase.`);
20754
+ }
20755
+ function reviewNoOwnerLine() {
20756
+ return line(`${NOTHING_RECORDED}: this project has no owner, so nobody can review its phases.`);
20757
+ }
20758
+ function reviewWrongActLine(name, built) {
20759
+ 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.`);
20760
+ }
20761
+ function wordsActNoun(act) {
20762
+ switch (act) {
20763
+ case "question":
20764
+ return "a question";
20765
+ case "send-back":
20766
+ return "a send-back";
20767
+ case "carry":
20768
+ return "a carry";
20769
+ case "drop":
20770
+ return "a drop";
20771
+ default: {
20772
+ const unreachable = act;
20773
+ return unreachable;
20774
+ }
20775
+ }
20776
+ }
20777
+ function reviewNoWordsLine(act) {
20778
+ return line(`${NOTHING_RECORDED}: ${wordsActNoun(act)} needs words after the colon.`);
20779
+ }
20780
+ function reviewWordsTooLongLine() {
20781
+ return line(`${NOTHING_RECORDED}: the words after the colon are too long. Shorten them and type the line again.`);
20782
+ }
20783
+ function reviewWordsLookSecretLine() {
20784
+ 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.`);
20785
+ }
20786
+ function reviewShowFirstLine() {
20787
+ return line(`${NOTHING_RECORDED}: that review has not been listed here. Say "review what was built" to list it, then type the line again.`);
20788
+ }
20789
+ function reviewPartNotListedLine(part, phase) {
20790
+ return line(`${NOTHING_RECORDED}: "${part}" is not a part of the review of ${phase} as it was listed here.`);
20791
+ }
20792
+ function reviewNothingToAcceptLine(phase) {
20793
+ return line(`${NOTHING_RECORDED}: no built part of ${phase} waits for your decision.`);
20794
+ }
20795
+ function reviewListedTooLongAgoLine(phase) {
20796
+ return line(`${NOTHING_RECORDED}: ${phase} was listed an hour or more ago. ${LIST_AGAIN}, then type the line again within the hour.`);
20797
+ }
20798
+ function reviewChangedLine(phase) {
20799
+ return line(`${NOTHING_RECORDED}: the review of ${phase} changed after it was listed here. ${LIST_AGAIN}, then type the line again.`);
20800
+ }
20801
+ function reviewAlreadyDecidedLine(name) {
20802
+ return line(`${NOTHING_RECORDED}: "${name}" is already decided in this review.`);
20803
+ }
20804
+ function reviewAwaitingAnswerLine(name) {
20805
+ 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.`);
20806
+ }
20807
+ function reviewSentBackLine(name) {
20808
+ return line(`${NOTHING_RECORDED}: "${name}" was sent back to building in this round, so it waits for the next round.`);
20809
+ }
20810
+ function reviewWalkChangedLine(phase, number4) {
20811
+ const typed = typedPhaseReference(phase, number4);
20812
+ return line(`${NOTHING_RECORDED}: phase ${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.`);
20813
+ }
20814
+ function reviewNoReviewLine(phase) {
20815
+ return line(`${NOTHING_RECORDED}: phase ${phase} has no review open.`);
20816
+ }
20817
+ function reviewCloseRefusedLine(phase, steps) {
20818
+ if (steps.length === 0) {
20819
+ return line(`${NOTHING_RECORDED}: phase ${phase} could not close just now. Type the line again in a minute.`);
20820
+ }
20821
+ const labels = steps.map(unfinishedStepLabel).join(", ");
20822
+ const head = steps.length === 1 ? `${NOTHING_RECORDED}: phase ${phase} cannot close while 1 of its steps is unfinished: ${labels}.` : `${NOTHING_RECORDED}: phase ${phase} cannot close while ${steps.length} of its steps are unfinished: ${labels}.`;
20823
+ return line([head, ...steps.map(unfinishedStepLine), "Then type the line again."].join(" "));
20824
+ }
20825
+ function reviewRefusedLine(act) {
20826
+ 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.`);
20827
+ }
20828
+ function stillWaitingOf(phase, waiting) {
20829
+ if (waiting === 0)
20830
+ return `No part of ${phase} waits for your decision now.`;
20831
+ if (waiting === 1)
20832
+ return `1 part of ${phase} still waits for your decision.`;
20833
+ return `${waiting} parts of ${phase} still wait for your decision.`;
20834
+ }
20835
+ function stillWaiting(waiting) {
20836
+ if (waiting === 0)
20837
+ return "No part waits for your decision now.";
20838
+ if (waiting === 1)
20839
+ return "1 part still waits for your decision.";
20840
+ return `${waiting} parts still wait for your decision.`;
20841
+ }
20842
+ function decidedWords(decided) {
20843
+ switch (decided.act) {
20844
+ case "accept":
20845
+ return "accepted";
20846
+ case "question":
20847
+ return "questioned";
20848
+ case "send-back":
20849
+ return "sent back to building";
20850
+ case "carry":
20851
+ return decided.carriedTo === void 0 || decided.carriedTo === null ? "carried to the next phase planned" : `carried to phase ${decided.carriedTo.number}, "${decided.carriedTo.phase}"`;
20852
+ case "drop":
20853
+ return "dropped";
20854
+ default: {
20855
+ const unreachable = decided.act;
20856
+ return unreachable;
20857
+ }
20858
+ }
20859
+ }
20860
+ function reviewRecordedLine(decided, phase, waiting) {
20861
+ return line(`Recorded in your session: "${decided.name}" ${decidedWords(decided)}. ${stillWaitingOf(phase, waiting)}`);
20862
+ }
20863
+ function reviewBulkRecordedLine(accepted, phase, waiting) {
20864
+ const first = accepted === 1 ? `Recorded in your session: 1 part of ${phase} accepted.` : `Recorded in your session: ${accepted} parts of ${phase} accepted.`;
20865
+ return line(`${first} ${stillWaiting(waiting)}`);
20866
+ }
20867
+ function reviewClosedLine(phase) {
20868
+ return line(`Recorded in your session. That was the last decision: phase ${phase} is closed, and its Build Record is ready.`);
20869
+ }
20870
+ function reviewNoPartsClosedLine(phase) {
20871
+ return line(`Recorded in your session. Phase ${phase} had no parts to review, so it is closed, and its Build Record is ready.`);
20872
+ }
20873
+ function notDecidableLine(answer) {
20874
+ const first = answer.parts[0];
20875
+ switch (first.why) {
20876
+ case "question-waiting":
20877
+ return reviewAwaitingAnswerLine(first.name);
20878
+ case "sent-back":
20879
+ return reviewSentBackLine(first.name);
20880
+ case "settled":
20881
+ return reviewAlreadyDecidedLine(first.name);
20882
+ default: {
20883
+ const unreachable = first.why;
20884
+ return unreachable;
20885
+ }
20886
+ }
20887
+ }
20888
+ function reviewLineForAnswer(context, answer) {
20889
+ const { phase } = context;
20890
+ switch (answer.outcome) {
20891
+ case "recorded": {
20892
+ if (answer.closed) {
20893
+ return answer.decided.length === 0 ? reviewNoPartsClosedLine(phase) : reviewClosedLine(phase);
20894
+ }
20895
+ if (context.bulk)
20896
+ return reviewBulkRecordedLine(answer.decided.length, phase, answer.waiting);
20897
+ return reviewRecordedLine(answer.decided[0], phase, answer.waiting);
20898
+ }
20899
+ case "review-changed":
20900
+ return reviewChangedLine(phase);
20901
+ case "no-review":
20902
+ return reviewNoReviewLine(phase);
20903
+ case "not-decidable":
20904
+ return notDecidableLine(answer);
20905
+ case "close-refused":
20906
+ return reviewCloseRefusedLine(phase, answer.steps);
20907
+ case "walk-changed":
20908
+ return reviewWalkChangedLine(phase, context.number);
20909
+ case "not-owner":
20910
+ return reviewNotOwnerLine();
20911
+ case "no-owner":
20912
+ return reviewNoOwnerLine();
20913
+ case "refused":
20914
+ return reviewRefusedLine(context.act);
20915
+ case "unavailable":
20916
+ return reviewCannotReachLine();
20917
+ case "show-first":
20918
+ return reviewShowFirstLine();
20919
+ case "not-offered":
20920
+ return reviewSwitchedOffLine();
20921
+ default: {
20922
+ const unreachable = answer;
20923
+ return unreachable;
20924
+ }
20925
+ }
20926
+ }
19228
20927
 
19229
20928
  // dist/hooks/user-prompt-submit.js
19230
20929
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
19231
20930
  var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
19232
20931
  async function runUserPromptSubmit(opts) {
19233
20932
  let act = null;
20933
+ const review = { recognised: false };
19234
20934
  try {
19235
20935
  const prompt = promptOf(opts.input);
20936
+ const reviewShape = reviewPhraseShape(prompt);
20937
+ if (reviewShape !== null) {
20938
+ const out = await decideReview(reviewShape, opts, review);
20939
+ return out === "" ? "" : `${out}
20940
+ `;
20941
+ }
19236
20942
  const shape = scopePhraseShape(prompt);
19237
20943
  if (shape === null)
19238
20944
  return "";
@@ -19240,10 +20946,114 @@ async function runUserPromptSubmit(opts) {
19240
20946
  return `${await decide(shape, opts)}
19241
20947
  `;
19242
20948
  } catch {
20949
+ if (review.recognised)
20950
+ return `${reviewCannotReachLine()}
20951
+ `;
19243
20952
  return act === null ? "" : `${cannotReachLine(act)}
19244
20953
  `;
19245
20954
  }
19246
20955
  }
20956
+ async function decideReview(shape, opts, progress) {
20957
+ const env = opts.env ?? process.env;
20958
+ if (shape.kind === "phase") {
20959
+ const read2 = readConfig(env);
20960
+ if (read2.config === null)
20961
+ return "";
20962
+ const match2 = matchReviewPhraseShape(shape, readShownRecord(read2.config.guardEngagementId, opts.home).reviews);
20963
+ if (match2.kind === "nothing")
20964
+ return "";
20965
+ progress.recognised = true;
20966
+ if (!readSessionAttended(env))
20967
+ return reviewNotAttendedLine();
20968
+ return checkAndSendReview(match2, read2.config.guardEngagementId, read2.config.guardServiceToken, env, opts);
20969
+ }
20970
+ progress.recognised = true;
20971
+ if (!readSessionAttended(env))
20972
+ return reviewNotAttendedLine();
20973
+ const read = readConfig(env);
20974
+ if (read.config === null) {
20975
+ return readNeverConfiguredReason(env) === "not-this-account" ? reviewNotOwnerLine() : reviewNotSignedInLine();
20976
+ }
20977
+ const match = matchReviewPhraseShape(shape, readShownRecord(read.config.guardEngagementId, opts.home).reviews);
20978
+ return checkAndSendReview(match, read.config.guardEngagementId, read.config.guardServiceToken, env, opts);
20979
+ }
20980
+ async function checkAndSendReview(match, engagementId, credential, env, opts) {
20981
+ if (match.kind === "nothing" || match.kind === "show-first")
20982
+ return reviewShowFirstLine();
20983
+ if (match.kind === "not-listed")
20984
+ return reviewPartNotListedLine(match.part, match.entry.phase);
20985
+ const { entry } = match;
20986
+ if (!listedRecently(entry.shownAt, (opts.now ?? Date.now)()))
20987
+ return reviewListedTooLongAgoLine(entry.phase);
20988
+ let candidate;
20989
+ if (match.kind === "accept") {
20990
+ if (entry.parts.length > 0 && match.parts.length === 0)
20991
+ return reviewNothingToAcceptLine(entry.phase);
20992
+ candidate = {
20993
+ act: "accept",
20994
+ reference: match.reference,
20995
+ reviewVersion: entry.reviewVersion,
20996
+ parts: match.parts.map((p) => p.part)
20997
+ };
20998
+ } else {
20999
+ const { act, part } = match;
21000
+ if (!reviewActsFor(part.built).includes(act))
21001
+ return reviewWrongActLine(part.name, part.built);
21002
+ let words = "";
21003
+ if (act !== "accept") {
21004
+ words = match.words ?? "";
21005
+ if (words === "")
21006
+ return reviewNoWordsLine(act);
21007
+ if (!reviewNoteWireSchema.safeParse(words).success)
21008
+ return reviewWordsTooLongLine();
21009
+ if (looksLikeSecret(words))
21010
+ return reviewWordsLookSecretLine();
21011
+ }
21012
+ if (part.awaitingAnswer)
21013
+ return reviewAwaitingAnswerLine(part.name);
21014
+ if (part.sentBack)
21015
+ return reviewSentBackLine(part.name);
21016
+ if (!part.decidable)
21017
+ return reviewAlreadyDecidedLine(part.name);
21018
+ 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 };
21019
+ }
21020
+ const request = chatReviewRequestSchema.safeParse(candidate);
21021
+ if (!request.success)
21022
+ return reviewShowFirstLine();
21023
+ return postReview(match.kind === "accept", entry, request.data, engagementId, credential, env, opts);
21024
+ }
21025
+ async function postReview(bulk, entry, request, engagementId, credential, env, opts) {
21026
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
21027
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/phases/${encodeURIComponent(entry.phase)}/review/chat`;
21028
+ const controller = new AbortController();
21029
+ const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
21030
+ try {
21031
+ const response = await fetchFn(url2, {
21032
+ method: "POST",
21033
+ headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json" },
21034
+ body: JSON.stringify(request),
21035
+ signal: controller.signal
21036
+ });
21037
+ if (response.status === 401)
21038
+ return reviewNotSignedInLine();
21039
+ if (response.status === 429 || response.status >= 500)
21040
+ return reviewCannotReachLine();
21041
+ const parsed = chatReviewResultSchema.safeParse(await response.json());
21042
+ if (!parsed.success)
21043
+ return reviewCannotReachLine();
21044
+ const answer = parsed.data;
21045
+ if (answer.outcome === "recorded") {
21046
+ if (response.status !== 200)
21047
+ return reviewCannotReachLine();
21048
+ moveListedReviewForward(engagementId, opts.home, entry, answer.reviewVersion, answer.decided);
21049
+ }
21050
+ return reviewLineForAnswer({ act: request.act, bulk, phase: entry.phase, number: entry.number }, answer);
21051
+ } catch {
21052
+ return reviewCannotReachLine();
21053
+ } finally {
21054
+ clearTimeout(timer);
21055
+ }
21056
+ }
19247
21057
  async function decide(shape, opts) {
19248
21058
  const { act } = shape;
19249
21059
  const env = opts.env ?? process.env;
@@ -19254,12 +21064,12 @@ async function decide(shape, opts) {
19254
21064
  return readNeverConfiguredReason(env) === "not-this-account" ? notOwnerLine(act) : notSignedInLine(act);
19255
21065
  }
19256
21066
  const { guardServiceToken, guardEngagementId } = read.config;
19257
- const shown = readShownPlans(guardEngagementId, opts.home);
21067
+ const shown = readShownRecord(guardEngagementId, opts.home).entries;
19258
21068
  const match = matchScopePhraseShape(shape, shown);
19259
21069
  if (match.kind === "show-first")
19260
21070
  return showFirstLine(act);
19261
21071
  const { entry } = match;
19262
- if (!shownRecently(entry, (opts.now ?? Date.now)()))
21072
+ if (!listedRecently(entry.shownAt, (opts.now ?? Date.now)()))
19263
21073
  return tooOldLine(act, entry.phase);
19264
21074
  let candidate;
19265
21075
  if (match.kind === "send-back") {
@@ -19288,19 +21098,8 @@ function promptOf(input) {
19288
21098
  return void 0;
19289
21099
  }
19290
21100
  }
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);
21101
+ function listedRecently(shownAt, now) {
21102
+ const age = now - Date.parse(shownAt);
19304
21103
  return Number.isFinite(age) && age >= 0 && age < SHOWN_ENTRY_MAX_AGE_MS;
19305
21104
  }
19306
21105
  async function post(act, entry, request, engagementId, credential, env, fetchFn = globalThis.fetch) {
@@ -19332,7 +21131,7 @@ async function post(act, entry, request, engagementId, credential, env, fetchFn
19332
21131
  }
19333
21132
 
19334
21133
  // dist/resolve-engagement.js
19335
- import { existsSync as existsSync6, readFileSync as readFileSync10, realpathSync as realpathSync3 } from "node:fs";
21134
+ import { existsSync as existsSync6, readFileSync as readFileSync11, realpathSync as realpathSync3 } from "node:fs";
19336
21135
  import { join as join9, sep as sep2 } from "node:path";
19337
21136
  import { homedir as homedir5 } from "node:os";
19338
21137
  function defaultRegistryPath() {
@@ -19342,7 +21141,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
19342
21141
  if (!existsSync6(registryPath))
19343
21142
  return {};
19344
21143
  try {
19345
- const parsed = JSON.parse(readFileSync10(registryPath, "utf-8"));
21144
+ const parsed = JSON.parse(readFileSync11(registryPath, "utf-8"));
19346
21145
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
19347
21146
  return {};
19348
21147
  const out = {};
@@ -19390,11 +21189,11 @@ function runResolveEngagement(cwd, registryPath) {
19390
21189
  }
19391
21190
 
19392
21191
  // dist/refresh-credential.js
19393
- import { readFileSync as readFileSync12 } from "node:fs";
21192
+ import { readFileSync as readFileSync13 } from "node:fs";
19394
21193
  import { resolve as resolve4 } from "node:path";
19395
21194
 
19396
21195
  // dist/mcp-headers.js
19397
- import { readFileSync as readFileSync11 } from "node:fs";
21196
+ import { readFileSync as readFileSync12 } from "node:fs";
19398
21197
  import { homedir as homedir6 } from "node:os";
19399
21198
  import { dirname as dirname4, resolve as resolve3 } from "node:path";
19400
21199
  function defaultWriteErr(text) {
@@ -19414,7 +21213,7 @@ function projectRootFromBinary(binaryPath) {
19414
21213
  function readPinnedVersion(root) {
19415
21214
  let raw;
19416
21215
  try {
19417
- raw = readFileSync11(resolve3(root, BUNDLE_PIN_REL), "utf-8");
21216
+ raw = readFileSync12(resolve3(root, BUNDLE_PIN_REL), "utf-8");
19418
21217
  } catch {
19419
21218
  return void 0;
19420
21219
  }
@@ -19640,7 +21439,7 @@ var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initializ
19640
21439
  function registeredMcpUrl(projectRoot) {
19641
21440
  let parsed;
19642
21441
  try {
19643
- parsed = JSON.parse(readFileSync12(resolve4(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
21442
+ parsed = JSON.parse(readFileSync13(resolve4(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
19644
21443
  } catch {
19645
21444
  return void 0;
19646
21445
  }
@@ -19747,7 +21546,7 @@ function positionRequestBody(engagementId) {
19747
21546
  params: { name: "halfcycle_position", arguments: { engagementId } }
19748
21547
  });
19749
21548
  }
19750
- async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
21549
+ async function fetchPositionNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
19751
21550
  const controller = new AbortController();
19752
21551
  const timer = setTimeout(() => controller.abort(), deadlineMs);
19753
21552
  try {
@@ -19794,9 +21593,11 @@ async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs
19794
21593
  } catch {
19795
21594
  return void 0;
19796
21595
  }
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;
21596
+ if (typeof body !== "object" || body === null)
21597
+ return void 0;
21598
+ const scopeParsed = scopeNoticesSchema.safeParse(body["scope"]);
21599
+ const reviewParsed = reviewNoticesSchema.safeParse(body["review"]);
21600
+ return { scope: scopeParsed.success ? scopeParsed.data : void 0, review: reviewParsed.success ? reviewParsed.data : void 0 };
19800
21601
  } finally {
19801
21602
  clearTimeout(timer);
19802
21603
  }
@@ -19833,9 +21634,11 @@ async function runRefreshCredential(opts) {
19833
21634
  const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
19834
21635
  const positionDeadline = Math.min(opts.scopeNoticeDeadlineMs ?? SCOPE_NOTICE_DEADLINE_MS, remainingForMargin);
19835
21636
  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));
21637
+ const notices = await fetchPositionNotices(connection.url, connection.bearer, engagementId, opts.fetchFn ?? globalThis.fetch, positionDeadline);
21638
+ if (notices?.scope !== void 0)
21639
+ parts.push(...scopeNoticeLines(notices.scope));
21640
+ if (notices?.review !== void 0)
21641
+ parts.push(...reviewNoticeText(notices.review));
19839
21642
  }
19840
21643
  }
19841
21644
  } else if (connection.sentence === CANNOT_REACH_SENTENCE || connection.sentence === SERVICE_PROBLEM_SENTENCE) {
@@ -19869,7 +21672,7 @@ async function runRefreshCredential(opts) {
19869
21672
  import { fileURLToPath } from "node:url";
19870
21673
 
19871
21674
  // dist/capture.js
19872
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
21675
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync6, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
19873
21676
  import { dirname as dirname5 } from "node:path";
19874
21677
  function runCapture(provider, endpoint, opts = {}) {
19875
21678
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -19906,7 +21709,7 @@ function runCapture(provider, endpoint, opts = {}) {
19906
21709
  const manifestFile = manifestPath(repoRoot);
19907
21710
  try {
19908
21711
  mkdirSync3(dirname5(fixture), { recursive: true });
19909
- writeFileSync4(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
21712
+ writeFileSync6(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
19910
21713
  } catch (err) {
19911
21714
  const msg = err instanceof Error ? err.message : String(err);
19912
21715
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -19937,7 +21740,7 @@ function readInputFile(fromFile) {
19937
21740
  if (!fromFile)
19938
21741
  return void 0;
19939
21742
  try {
19940
- return readFileSync13(fromFile, "utf-8");
21743
+ return readFileSync14(fromFile, "utf-8");
19941
21744
  } catch {
19942
21745
  return void 0;
19943
21746
  }
@@ -19961,6 +21764,17 @@ async function main() {
19961
21764
  process.stdout.write(reply, () => process.exit(0));
19962
21765
  return;
19963
21766
  }
21767
+ if (cmd === "observe") {
21768
+ let line2;
21769
+ try {
21770
+ line2 = JSON.stringify(await runObserve());
21771
+ } catch {
21772
+ line2 = JSON.stringify({ outcome: "not-sent", reason: "unreachable" });
21773
+ }
21774
+ process.stdout.write(`${line2}
21775
+ `, () => process.exit(0));
21776
+ return;
21777
+ }
19964
21778
  if (cmd === "mcp-headers") {
19965
21779
  const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
19966
21780
  process.stdout.write(line2, () => process.exit(0));
@@ -20043,6 +21857,7 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
20043
21857
  halfcycle-runner hook user-prompt-submit
20044
21858
  halfcycle-runner refresh-credential
20045
21859
  halfcycle-runner mcp-headers
21860
+ halfcycle-runner observe
20046
21861
  halfcycle-runner resolve-engagement <cwd>
20047
21862
  halfcycle-runner capture <provider> <endpoint> [--from <file>]
20048
21863
  `);