halfcycle 0.4.7 → 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 (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/bin.bundle.mjs +2217 -280
  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 +3194 -1402
  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 +1629 -209
  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/loopback-signin.d.ts.map +1 -1
  35. package/dist/open-phase.d.ts +25 -1
  36. package/dist/open-phase.d.ts.map +1 -1
  37. package/dist/plan.d.ts +59 -18
  38. package/dist/plan.d.ts.map +1 -1
  39. package/dist/pre-create-checks.d.ts.map +1 -1
  40. package/dist/review.d.ts +228 -0
  41. package/dist/review.d.ts.map +1 -0
  42. package/dist/shown-record.d.ts +46 -0
  43. package/dist/shown-record.d.ts.map +1 -0
  44. package/dist/system-report.d.ts +28 -4
  45. package/dist/system-report.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/project-page.d.ts +0 -24
  48. package/dist/project-page.d.ts.map +0 -1
@@ -14781,33 +14781,69 @@ 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();
14809
14814
  }
14810
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);
14842
+ }
14843
+ if (hasDuplicates(entries) || hasDuplicates(reviews))
14844
+ return null;
14845
+ return { entries, reviews };
14846
+ }
14811
14847
  function readEntry(raw) {
14812
14848
  if (!isPlainObject2(raw) || !hasExactKeys(raw, ENTRY_KEYS))
14813
14849
  return null;
@@ -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) {
@@ -14899,8 +15033,8 @@ function readConfig(env = process.env) {
14899
15033
  }
14900
15034
  };
14901
15035
  }
14902
- function readNeverConfiguredReason() {
14903
- return process.env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
15036
+ function readNeverConfiguredReason(env = process.env) {
15037
+ return env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
14904
15038
  }
14905
15039
  function isHalfcycleOwnCi() {
14906
15040
  return process.env["HALFCYCLE_INTERNAL_CI"] === "1";
@@ -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;
@@ -15845,12 +15943,180 @@ var systemPictureSchema = external_exports.object({
15845
15943
  svg: external_exports.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_SVG_MAX_BYTES, { message: "the picture is larger than 2 MB" }),
15846
15944
  png: external_exports.string().min(1).refine((v) => v.length % 4 === 0 && BASE64.test(v), { message: "the image is not valid base64" }).refine((v) => base64DecodedLength(v) <= SYSTEM_DRAW_PNG_MAX_BYTES, { message: "the image is larger than 8 MB" })
15847
15945
  }).strict();
15848
- var systemDrawRefusedSchema = external_exports.object({
15849
- outcome: external_exports.literal("refused"),
15850
- reason: external_exports.string().min(1)
15946
+ var systemDrawRefusedSchema = external_exports.object({
15947
+ outcome: external_exports.literal("refused"),
15948
+ reason: external_exports.string().min(1)
15949
+ }).strict();
15950
+ var systemDrawUnavailableSchema = external_exports.object({
15951
+ outcome: external_exports.literal("unavailable")
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()
15851
16093
  }).strict();
15852
- var systemDrawUnavailableSchema = external_exports.object({
15853
- outcome: external_exports.literal("unavailable")
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
15854
16120
  }).strict();
15855
16121
 
15856
16122
  // ../events/dist/phase-plan.js
@@ -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,146 @@ 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
+
16984
+ // ../events/dist/device-auth.js
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");
16986
+ var DEVICE_AUTH_STATUS = {
16987
+ /** The poll: minted, nobody has acted yet. Keep polling. 200. */
16988
+ PENDING: "pending",
16989
+ /** The poll: approved, and the credential is in this body. Once only. 200. */
16990
+ APPROVED: "approved",
16991
+ /** Confirm: the decision was recorded (approve or decline). 200. */
16992
+ RECORDED: "recorded",
16993
+ /** The human refused at the confirmation page. 400 on the poll. */
16994
+ DECLINED: "declined",
16995
+ /** The code's absolute expiry has passed. 400. */
16996
+ EXPIRED: "expired",
16997
+ /** Never minted, or already spent. 400. */
16998
+ UNKNOWN: "unknown",
16999
+ /** Confirm: neither `approve` nor `decline` was named. Nothing was bound. 400. */
17000
+ INVALID_DECISION: "invalid-decision",
17001
+ /** Confirm: an approval arrived with no signed-in identity. 400. */
17002
+ IDENTITY_REQUIRED: "identity-required",
17003
+ /** This deployment has no `SIGNIN_BASE_URL` / `DEVICE_AUTH_CONFIRM_SECRET`. 503. */
17004
+ NOT_CONFIGURED: "not-configured",
17005
+ /** Confirm: the shared confirmation secret was missing or wrong. 401. */
17006
+ UNAUTHORIZED: "unauthorized",
17007
+ /** Confirm: the code was already approved, declined or claimed. 409. */
17008
+ ALREADY_DECIDED: "already-decided",
17009
+ /**
17010
+ * Confirm: an approval named a signed-in identity whose account has not accepted
17011
+ * the terms of service and privacy policy currently in force. 400.
17012
+ *
17013
+ * NOT BOUND TO THE DEVICE CODE. Unlike every other refusal in this const, this one
17014
+ * says nothing about the code itself — the code is still `pending` after this
17015
+ * response, exactly as it was before the request, so the SAME userCode can be
17016
+ * confirmed again once the caller has recorded acceptance. `deviceConfirmRequestSchema`'s
17017
+ * `acceptTerms` is how a second confirm says it did.
17018
+ */
17019
+ TERMS_REQUIRED: "terms-required"
17020
+ };
17021
+ var DEVICE_POLL_REFUSALS = [
17022
+ DEVICE_AUTH_STATUS.DECLINED,
17023
+ DEVICE_AUTH_STATUS.EXPIRED,
17024
+ DEVICE_AUTH_STATUS.UNKNOWN
17025
+ ];
17026
+ var deviceAuthStartResponseSchema = external_exports.object({
17027
+ deviceCode: external_exports.string().min(1),
17028
+ userCode: external_exports.string().min(1),
17029
+ verificationUrl: external_exports.string().min(1),
17030
+ expiresAt: utcIso86012,
17031
+ pollIntervalMs: external_exports.number().int().positive()
17032
+ }).strict();
17033
+ var devicePollPendingSchema = external_exports.object({
17034
+ status: external_exports.literal(DEVICE_AUTH_STATUS.PENDING),
17035
+ pollIntervalMs: external_exports.number().int().positive()
17036
+ }).strict();
17037
+ var devicePollApprovedSchema = external_exports.object({
17038
+ status: external_exports.literal(DEVICE_AUTH_STATUS.APPROVED),
17039
+ accountId: external_exports.string().min(1),
17040
+ credential: external_exports.string().min(1),
17041
+ expiresAt: utcIso86012
17042
+ }).strict();
17043
+ var deviceAuthRefusalSchema = external_exports.object({
17044
+ status: external_exports.enum([
17045
+ DEVICE_AUTH_STATUS.DECLINED,
17046
+ DEVICE_AUTH_STATUS.EXPIRED,
17047
+ DEVICE_AUTH_STATUS.UNKNOWN,
17048
+ DEVICE_AUTH_STATUS.INVALID_DECISION,
17049
+ DEVICE_AUTH_STATUS.IDENTITY_REQUIRED,
17050
+ DEVICE_AUTH_STATUS.NOT_CONFIGURED,
17051
+ DEVICE_AUTH_STATUS.UNAUTHORIZED,
17052
+ DEVICE_AUTH_STATUS.ALREADY_DECIDED,
17053
+ DEVICE_AUTH_STATUS.TERMS_REQUIRED
17054
+ ]),
17055
+ message: external_exports.string().min(1)
17056
+ }).strict();
17057
+ var devicePollResponseSchema = external_exports.union([
17058
+ devicePollPendingSchema,
17059
+ devicePollApprovedSchema,
17060
+ deviceAuthRefusalSchema
17061
+ ]);
17062
+ var deviceConfirmRequestSchema = external_exports.object({
17063
+ userCode: external_exports.string().min(1),
17064
+ decision: external_exports.enum(["approve", "decline"]),
17065
+ externalAuthId: external_exports.string().min(1).optional(),
17066
+ email: external_exports.string().optional(),
17067
+ acceptTerms: external_exports.boolean().optional()
17068
+ }).strict();
17069
+ var deviceConfirmRecordedSchema = external_exports.object({
17070
+ status: external_exports.literal(DEVICE_AUTH_STATUS.RECORDED),
17071
+ accountId: external_exports.string().min(1).optional()
17072
+ }).strict();
17073
+ var deviceConfirmResponseSchema = external_exports.union([
17074
+ deviceConfirmRecordedSchema,
17075
+ deviceAuthRefusalSchema
17076
+ ]);
17077
+ var DASHBOARD_URL = "https://app.halfcycle.ai";
17078
+
17079
+ // ../events/dist/project-page.js
17080
+ function projectPageUrl(engagementId) {
17081
+ return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
17082
+ }
17083
+ function requestAccessSentence(engagementId) {
17084
+ const page = projectPageUrl(engagementId) ?? "this project's page on Halfcycle";
17085
+ return `To ask for access, open ${page} signed in as this account and choose "Request access". Then let the project's owner know, so they can approve it.`;
17086
+ }
17087
+
16089
17088
  // ../events/dist/credential-wire.js
16090
17089
  var engagementCredentialResponseSchema = external_exports.object({
16091
17090
  engagementId: external_exports.string(),
@@ -16169,6 +17168,56 @@ var chatScopeResultSchema = external_exports.discriminatedUnion("outcome", [
16169
17168
  bareOutcome("unavailable")
16170
17169
  ]);
16171
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
+
16172
17221
  // dist/hook-protocol.js
16173
17222
  import { writeSync } from "node:fs";
16174
17223
  function emitBlock(reason) {
@@ -16224,12 +17273,10 @@ This is not a service outage: the guard service answered. The suspect is this re
16224
17273
  }
16225
17274
  var UNCONFIGURED_EXIT_TOOL_USE = 2;
16226
17275
  var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
16227
- function emitNeverConfigured(exitCode, reason) {
16228
- writeSync(2, neverConfiguredReport(reason));
17276
+ function emitNeverConfigured(exitCode, reason, engagementId) {
17277
+ writeSync(2, neverConfiguredReport(reason, void 0, engagementId));
16229
17278
  process.exitCode = exitCode;
16230
17279
  }
16231
- var SIGN_IN_COMMAND = '"npx halfcycle"';
16232
- var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
16233
17280
  function namesTheSignInCommand(text) {
16234
17281
  return text.includes(SIGN_IN_COMMAND);
16235
17282
  }
@@ -16255,16 +17302,16 @@ ${NOT_A_SERVICE_OUTAGE} Nothing was sent: this job holds no credential and was n
16255
17302
  ${CI_OLDER_SETUP}
16256
17303
  `;
16257
17304
  }
16258
- function notThisAccountReport(label) {
16259
- return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. Ask the project's owner for view access, or \u2014 if this account is wrong \u2014 sign in as the one that owns the project and run "npx halfcycle" again.
17305
+ function notThisAccountReport(label, engagementId) {
17306
+ return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. ${requestAccessSentence(engagementId)} If this account is wrong, sign in as the one that owns the project and run "npx halfcycle" again.
16260
17307
  `;
16261
17308
  }
16262
- function neverConfiguredReport(reason, label = "[Halfcycle]") {
17309
+ function neverConfiguredReport(reason, label = "[Halfcycle]", engagementId) {
16263
17310
  switch (reason) {
16264
17311
  case "no-credential-yet":
16265
17312
  return neverConfiguredMessage(HOOK_REMEDY, label);
16266
17313
  case "not-this-account":
16267
- return notThisAccountReport(label);
17314
+ return notThisAccountReport(label, engagementId);
16268
17315
  }
16269
17316
  }
16270
17317
  var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
@@ -16315,14 +17362,14 @@ function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
16315
17362
  ${remedy}
16316
17363
  `;
16317
17364
  }
16318
- function noCoverageStatement(reason, remedySaid = false) {
17365
+ function noCoverageStatement(reason, remedySaid = false, engagementId) {
16319
17366
  if (reason === "not-this-account") {
16320
17367
  return [
16321
17368
  "[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked.",
16322
17369
  "[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is",
16323
17370
  "[halfcycle] evaluated, and nothing you do is reported as having passed.",
16324
17371
  "[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming.",
16325
- "[halfcycle] Ask the project's owner for view access.",
17372
+ `[halfcycle] ${requestAccessSentence(engagementId)}`,
16326
17373
  "[halfcycle] If this account is wrong, sign in as the one that owns the project and run",
16327
17374
  '[halfcycle] "npx halfcycle" again.'
16328
17375
  ].join("\n");
@@ -16556,13 +17603,16 @@ import { spawnSync } from "node:child_process";
16556
17603
  import { existsSync, readFileSync } from "node:fs";
16557
17604
  import { relative } from "node:path";
16558
17605
  var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
16559
- function git(args2, cwd, timeoutMs) {
17606
+ function spawnGit(args2, cwd, timeoutMs) {
16560
17607
  const argv = COLOR_AWARE_SUBCOMMANDS.has(args2[0] ?? "") ? [args2[0], "--no-color", ...args2.slice(1)] : args2;
16561
- const result = spawnSync("git", argv, {
17608
+ return spawnSync("git", argv, {
16562
17609
  cwd,
16563
17610
  encoding: "utf-8",
16564
17611
  ...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
16565
17612
  });
17613
+ }
17614
+ function git(args2, cwd, timeoutMs) {
17615
+ const result = spawnGit(args2, cwd, timeoutMs);
16566
17616
  if (result.error) {
16567
17617
  const missing = result.error.code === "ENOENT" && existsSync(cwd);
16568
17618
  return { ok: false, message: `git error: ${result.error.message}`, ...missing ? { gitMissing: true } : {} };
@@ -16724,6 +17774,124 @@ function getWiringStatus(pathspecs, cwd) {
16724
17774
  ...pathspecs
16725
17775
  ], cwd, WIRING_STATUS_TIMEOUT_MS);
16726
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
+ }
16727
17895
 
16728
17896
  // dist/screen/secret-shape.js
16729
17897
  function screenExcerpts(excerpts) {
@@ -18108,8 +19276,36 @@ function cleanRunSummary(verdict, scope) {
18108
19276
  return `[Halfcycle CI] ${verdict} ${files}, ${scope.signals} signal(s) checked.`;
18109
19277
  }
18110
19278
 
19279
+ // dist/pinned-engagement.js
19280
+ import { readFileSync as readFileSync5 } from "node:fs";
19281
+ import { resolve } from "node:path";
19282
+ function readPinnedId(root) {
19283
+ let raw;
19284
+ try {
19285
+ raw = readFileSync5(resolve(root, BUNDLE_PIN_REL), "utf-8");
19286
+ } catch {
19287
+ return { problem: "no-pin" };
19288
+ }
19289
+ let pin;
19290
+ try {
19291
+ pin = JSON.parse(raw);
19292
+ } catch {
19293
+ return { problem: "no-id" };
19294
+ }
19295
+ const id = typeof pin === "object" && pin !== null ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
19296
+ return typeof id === "string" && id !== "" ? { id } : { problem: "no-id" };
19297
+ }
19298
+ function pinnedEngagementId(root) {
19299
+ const pinned = readPinnedId(root);
19300
+ return "id" in pinned && isEngagementId(pinned.id) ? pinned.id : void 0;
19301
+ }
19302
+ function projectFolder(env = process.env) {
19303
+ const dir = env["CLAUDE_PROJECT_DIR"];
19304
+ return dir !== void 0 && dir !== "" ? dir : process.cwd();
19305
+ }
19306
+
18111
19307
  // dist/credential-store.js
18112
- import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
19308
+ import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync6, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
18113
19309
  import { homedir as homedir2, platform } from "node:os";
18114
19310
  import { dirname as dirname3, join as join5 } from "node:path";
18115
19311
  var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
@@ -18123,7 +19319,7 @@ function hasNotThisAccountMarker(engagementId, home) {
18123
19319
  function readAccountCredential(controlOrigin, home) {
18124
19320
  let raw;
18125
19321
  try {
18126
- raw = readFileSync5(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ACCOUNT_STORE_FILENAME), "utf-8");
19322
+ raw = readFileSync6(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ACCOUNT_STORE_FILENAME), "utf-8");
18127
19323
  } catch {
18128
19324
  return void 0;
18129
19325
  }
@@ -18140,7 +19336,7 @@ function readStoredEngagementToken(envFilePath) {
18140
19336
  function readEngagementStore(envFilePath) {
18141
19337
  let raw;
18142
19338
  try {
18143
- raw = readFileSync5(envFilePath, "utf-8");
19339
+ raw = readFileSync6(envFilePath, "utf-8");
18144
19340
  } catch {
18145
19341
  return void 0;
18146
19342
  }
@@ -18170,7 +19366,7 @@ function writeRefreshedToken(opts) {
18170
19366
  }
18171
19367
  let existing;
18172
19368
  try {
18173
- existing = readFileSync5(declaredReal, "utf-8");
19369
+ existing = readFileSync6(declaredReal, "utf-8");
18174
19370
  } catch {
18175
19371
  existing = null;
18176
19372
  }
@@ -18313,7 +19509,7 @@ async function evaluateWithRefresh(opts) {
18313
19509
 
18314
19510
  // dist/hooks/marker.js
18315
19511
  import { createHash } from "node:crypto";
18316
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
19512
+ import { existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
18317
19513
  import { tmpdir } from "node:os";
18318
19514
  import { join as join6 } from "node:path";
18319
19515
  var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -18345,7 +19541,7 @@ function readMarker(repoRoot, sessionId) {
18345
19541
  if (!existsSync4(path))
18346
19542
  return null;
18347
19543
  try {
18348
- return readFileSync6(path, "utf-8").trim() || null;
19544
+ return readFileSync7(path, "utf-8").trim() || null;
18349
19545
  } catch {
18350
19546
  return null;
18351
19547
  }
@@ -18363,7 +19559,10 @@ var SESSION_NOTICE_SUFFIX = {
18363
19559
  "no-repository": ".norepo",
18364
19560
  "credential-rejected": ".credential",
18365
19561
  "credential-renewed-for-this-run": ".renewed",
18366
- "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"
18367
19566
  };
18368
19567
  function firstNoticeThisSession(notice, folder, sessionId) {
18369
19568
  if (!sessionId)
@@ -18391,7 +19590,7 @@ function readDedupe(repoRoot, sessionId) {
18391
19590
  if (!existsSync4(path))
18392
19591
  return null;
18393
19592
  try {
18394
- const parsed = JSON.parse(readFileSync6(path, "utf-8"));
19593
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
18395
19594
  if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
18396
19595
  return parsed;
18397
19596
  }
@@ -18436,7 +19635,7 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
18436
19635
  const full = join6(dir, name);
18437
19636
  let unusable = false;
18438
19637
  try {
18439
- const ref = readFileSync6(full, "utf-8").trim();
19638
+ const ref = readFileSync7(full, "utf-8").trim();
18440
19639
  if (!ref || !refKnown(ref))
18441
19640
  unusable = true;
18442
19641
  } catch {
@@ -18458,11 +19657,19 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
18458
19657
  }
18459
19658
  }
18460
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
+ }
18461
19668
 
18462
19669
  // dist/seat-configuration.js
18463
- import { existsSync as existsSync5, readFileSync as readFileSync7, realpathSync as realpathSync2 } from "node:fs";
19670
+ import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync2 } from "node:fs";
18464
19671
  import { homedir as homedir3 } from "node:os";
18465
- import { join as join7, relative as relative3, resolve } from "node:path";
19672
+ import { join as join7, relative as relative3, resolve as resolve2 } from "node:path";
18466
19673
  function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
18467
19674
  try {
18468
19675
  const facts = read(repoRoot);
@@ -18475,7 +19682,7 @@ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
18475
19682
  }
18476
19683
  }
18477
19684
  function readSeatConfiguration(repoRoot) {
18478
- const given = resolve(process.env["CLAUDE_PROJECT_DIR"] || process.cwd());
19685
+ const given = resolve2(process.env["CLAUDE_PROJECT_DIR"] || process.cwd());
18479
19686
  const projectDir = realpathSync2.native(given);
18480
19687
  const home = homedir3();
18481
19688
  let realHome = home;
@@ -18497,7 +19704,7 @@ function isPlainObject3(value) {
18497
19704
  function readSettingsFile(projectDir, rel) {
18498
19705
  let raw;
18499
19706
  try {
18500
- raw = readFileSync7(join7(projectDir, rel), "utf-8");
19707
+ raw = readFileSync8(join7(projectDir, rel), "utf-8");
18501
19708
  } catch (err) {
18502
19709
  const code = err.code;
18503
19710
  const state = code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
@@ -18655,7 +19862,7 @@ async function evaluateEdit(reply, readSeat) {
18655
19862
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
18656
19863
  if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
18657
19864
  return;
18658
- emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
19865
+ emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
18659
19866
  return;
18660
19867
  }
18661
19868
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
@@ -18748,10 +19955,10 @@ ${guardLines}${notRunBlock}`);
18748
19955
  }
18749
19956
  }
18750
19957
  function readStdin() {
18751
- return new Promise((resolve4, reject) => {
19958
+ return new Promise((resolve5, reject) => {
18752
19959
  const chunks = [];
18753
19960
  process.stdin.on("data", (chunk) => chunks.push(chunk));
18754
- process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
19961
+ process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8")));
18755
19962
  process.stdin.on("error", reject);
18756
19963
  });
18757
19964
  }
@@ -18791,9 +19998,11 @@ function collectDiff(repoRoot, base) {
18791
19998
  getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
18792
19999
  };
18793
20000
  }
18794
- async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
20001
+ async function runSessionDiff(input, kind, readSeat = readSeatConfiguration, afterPass) {
18795
20002
  const reply = hookReply(STOP_HOOK_EVENT[kind]);
18796
20003
  await evaluateSession(input, kind, reply, readSeat);
20004
+ if (afterPass)
20005
+ await afterPass(reply);
18797
20006
  reply.flush();
18798
20007
  }
18799
20008
  async function evaluateSession(input, kind, reply, readSeat) {
@@ -18836,7 +20045,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
18836
20045
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
18837
20046
  if (!firstNoticeThisSession("credential-missing", repoRoot, sessionId))
18838
20047
  return;
18839
- emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
20048
+ emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
18840
20049
  return;
18841
20050
  }
18842
20051
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
@@ -18947,9 +20156,9 @@ async function readHookStdin() {
18947
20156
  }
18948
20157
  }
18949
20158
  function readStdin2() {
18950
- return new Promise((resolve4) => {
20159
+ return new Promise((resolve5) => {
18951
20160
  if (process.stdin.isTTY) {
18952
- resolve4("");
20161
+ resolve5("");
18953
20162
  return;
18954
20163
  }
18955
20164
  const chunks = [];
@@ -18959,7 +20168,7 @@ function readStdin2() {
18959
20168
  return;
18960
20169
  settled = true;
18961
20170
  clearTimeout(timer);
18962
- resolve4(Buffer.concat(chunks).toString("utf-8"));
20171
+ resolve5(Buffer.concat(chunks).toString("utf-8"));
18963
20172
  };
18964
20173
  const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
18965
20174
  process.stdin.on("data", (chunk) => chunks.push(chunk));
@@ -18974,17 +20183,328 @@ async function runSubagentStop() {
18974
20183
  await runSessionDiff(input, "subagent-stop");
18975
20184
  }
18976
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
+
18977
20502
  // dist/hooks/stop.js
18978
- async function runStop() {
20503
+ async function runStop(observeDeps = {}) {
18979
20504
  const input = await readHookStdin();
18980
- await runSessionDiff(input, "stop");
20505
+ await runSessionDiff(input, "stop", readSeatConfiguration, (reply) => observeChanges(input.session_id, reply, observeDeps));
18981
20506
  }
18982
20507
 
18983
- // dist/hooks/user-prompt-submit.js
18984
- import { readFileSync as readFileSync8 } from "node:fs";
18985
- import { homedir as homedir4 } from "node:os";
18986
- import { join as join8 } from "node:path";
18987
-
18988
20508
  // dist/hooks/scope-phrase.js
18989
20509
  var APPROVE_PREFIX = /^approve /i;
18990
20510
  var SEND_BACK_PREFIX = /^send back /i;
@@ -19018,6 +20538,128 @@ function matchScopePhraseShape(shape, shown) {
19018
20538
  return { kind: "show-first", act: "send-back" };
19019
20539
  }
19020
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
+
19021
20663
  // dist/hooks/user-prompt-submit-lines.js
19022
20664
  var TELL_THE_PERSON = "Tell the person.";
19023
20665
  function line(text) {
@@ -19038,6 +20680,9 @@ function notAttendedLine(act) {
19038
20680
  function notSignedInLine(act) {
19039
20681
  return line(`${nothingDone(act)}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
19040
20682
  }
20683
+ function notOwnerLine(act) {
20684
+ return line(`${nothingDone(act)}: only the project's owner can approve or send back this scope.`);
20685
+ }
19041
20686
  function showFirstLine(act) {
19042
20687
  return line(`${nothingDone(act)}: that phase has not been listed here. List its parts first with \`${HALFCYCLE_COMMAND} plan <phase> --show\`, then type the line again.`);
19043
20688
  }
@@ -19075,7 +20720,7 @@ function lineForAnswer(act, phase, answer) {
19075
20720
  case "build-started":
19076
20721
  return line(`${nothingDone(act)}: tasks in ${phase} have started, so its scope can no longer be sent back. A built part goes back at the phase's review.`);
19077
20722
  case "not-owner":
19078
- return line(`${nothingDone(act)}: only the project's owner can approve or send back this scope.`);
20723
+ return notOwnerLine(act);
19079
20724
  case "no-owner":
19080
20725
  return line(`${nothingDone(act)}: this project has no owner, so nobody can approve or send back its scope.`);
19081
20726
  case "not-offered":
@@ -19090,14 +20735,210 @@ function lineForAnswer(act, phase, answer) {
19090
20735
  }
19091
20736
  }
19092
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
+ }
19093
20927
 
19094
20928
  // dist/hooks/user-prompt-submit.js
19095
20929
  var CHAT_REQUEST_TIMEOUT_MS = 6e3;
19096
20930
  var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
19097
20931
  async function runUserPromptSubmit(opts) {
19098
20932
  let act = null;
20933
+ const review = { recognised: false };
19099
20934
  try {
19100
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
+ }
19101
20942
  const shape = scopePhraseShape(prompt);
19102
20943
  if (shape === null)
19103
20944
  return "";
@@ -19105,25 +20946,130 @@ async function runUserPromptSubmit(opts) {
19105
20946
  return `${await decide(shape, opts)}
19106
20947
  `;
19107
20948
  } catch {
20949
+ if (review.recognised)
20950
+ return `${reviewCannotReachLine()}
20951
+ `;
19108
20952
  return act === null ? "" : `${cannotReachLine(act)}
19109
20953
  `;
19110
20954
  }
19111
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
+ }
19112
21057
  async function decide(shape, opts) {
19113
21058
  const { act } = shape;
19114
21059
  const env = opts.env ?? process.env;
19115
21060
  if (!readSessionAttended(env))
19116
21061
  return notAttendedLine(act);
19117
21062
  const read = readConfig(env);
19118
- if (read.config === null)
19119
- return notSignedInLine(act);
21063
+ if (read.config === null) {
21064
+ return readNeverConfiguredReason(env) === "not-this-account" ? notOwnerLine(act) : notSignedInLine(act);
21065
+ }
19120
21066
  const { guardServiceToken, guardEngagementId } = read.config;
19121
- const shown = readShownPlans(guardEngagementId, opts.home);
21067
+ const shown = readShownRecord(guardEngagementId, opts.home).entries;
19122
21068
  const match = matchScopePhraseShape(shape, shown);
19123
21069
  if (match.kind === "show-first")
19124
21070
  return showFirstLine(act);
19125
21071
  const { entry } = match;
19126
- if (!shownRecently(entry, (opts.now ?? Date.now)()))
21072
+ if (!listedRecently(entry.shownAt, (opts.now ?? Date.now)()))
19127
21073
  return tooOldLine(act, entry.phase);
19128
21074
  let candidate;
19129
21075
  if (match.kind === "send-back") {
@@ -19152,19 +21098,8 @@ function promptOf(input) {
19152
21098
  return void 0;
19153
21099
  }
19154
21100
  }
19155
- function readShownPlans(engagementId, home) {
19156
- if (!isEngagementId(engagementId))
19157
- return [];
19158
- let text;
19159
- try {
19160
- text = readFileSync8(join8(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SHOWN_PLANS_FILENAME), "utf-8");
19161
- } catch {
19162
- return [];
19163
- }
19164
- return parseShownPlans(text);
19165
- }
19166
- function shownRecently(entry, now) {
19167
- const age = now - Date.parse(entry.shownAt);
21101
+ function listedRecently(shownAt, now) {
21102
+ const age = now - Date.parse(shownAt);
19168
21103
  return Number.isFinite(age) && age >= 0 && age < SHOWN_ENTRY_MAX_AGE_MS;
19169
21104
  }
19170
21105
  async function post(act, entry, request, engagementId, credential, env, fetchFn = globalThis.fetch) {
@@ -19196,7 +21131,7 @@ async function post(act, entry, request, engagementId, credential, env, fetchFn
19196
21131
  }
19197
21132
 
19198
21133
  // dist/resolve-engagement.js
19199
- import { existsSync as existsSync6, readFileSync as readFileSync9, realpathSync as realpathSync3 } from "node:fs";
21134
+ import { existsSync as existsSync6, readFileSync as readFileSync11, realpathSync as realpathSync3 } from "node:fs";
19200
21135
  import { join as join9, sep as sep2 } from "node:path";
19201
21136
  import { homedir as homedir5 } from "node:os";
19202
21137
  function defaultRegistryPath() {
@@ -19206,7 +21141,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
19206
21141
  if (!existsSync6(registryPath))
19207
21142
  return {};
19208
21143
  try {
19209
- const parsed = JSON.parse(readFileSync9(registryPath, "utf-8"));
21144
+ const parsed = JSON.parse(readFileSync11(registryPath, "utf-8"));
19210
21145
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
19211
21146
  return {};
19212
21147
  const out = {};
@@ -19254,13 +21189,13 @@ function runResolveEngagement(cwd, registryPath) {
19254
21189
  }
19255
21190
 
19256
21191
  // dist/refresh-credential.js
19257
- import { readFileSync as readFileSync11 } from "node:fs";
19258
- import { resolve as resolve3 } from "node:path";
21192
+ import { readFileSync as readFileSync13 } from "node:fs";
21193
+ import { resolve as resolve4 } from "node:path";
19259
21194
 
19260
21195
  // dist/mcp-headers.js
19261
- import { readFileSync as readFileSync10 } from "node:fs";
21196
+ import { readFileSync as readFileSync12 } from "node:fs";
19262
21197
  import { homedir as homedir6 } from "node:os";
19263
- import { dirname as dirname4, resolve as resolve2 } from "node:path";
21198
+ import { dirname as dirname4, resolve as resolve3 } from "node:path";
19264
21199
  function defaultWriteErr(text) {
19265
21200
  process.stderr.write(text);
19266
21201
  }
@@ -19273,28 +21208,12 @@ function headerLine(bearer, clientVersion) {
19273
21208
  }
19274
21209
  function projectRootFromBinary(binaryPath) {
19275
21210
  const depth = VENDORED_BIN_REL.split("/").length - 1;
19276
- return resolve2(dirname4(binaryPath), ...Array(depth).fill(".."));
19277
- }
19278
- function readPinnedId(root) {
19279
- let raw;
19280
- try {
19281
- raw = readFileSync10(resolve2(root, BUNDLE_PIN_REL), "utf-8");
19282
- } catch {
19283
- return { problem: "no-pin" };
19284
- }
19285
- let pin;
19286
- try {
19287
- pin = JSON.parse(raw);
19288
- } catch {
19289
- return { problem: "no-id" };
19290
- }
19291
- const id = typeof pin === "object" && pin !== null ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
19292
- return typeof id === "string" && id !== "" ? { id } : { problem: "no-id" };
21211
+ return resolve3(dirname4(binaryPath), ...Array(depth).fill(".."));
19293
21212
  }
19294
21213
  function readPinnedVersion(root) {
19295
21214
  let raw;
19296
21215
  try {
19297
- raw = readFileSync10(resolve2(root, BUNDLE_PIN_REL), "utf-8");
21216
+ raw = readFileSync12(resolve3(root, BUNDLE_PIN_REL), "utf-8");
19298
21217
  } catch {
19299
21218
  return void 0;
19300
21219
  }
@@ -19336,7 +21255,7 @@ function sentences(lines) {
19336
21255
  }
19337
21256
  function badIdSentence(root) {
19338
21257
  return sentences([
19339
- `${resolve2(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
21258
+ `${resolve3(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
19340
21259
  'so no credential was read. Delete that file and run "npx halfcycle" again: this repository',
19341
21260
  "is then set up as a new Halfcycle project."
19342
21261
  ]);
@@ -19350,7 +21269,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
19350
21269
  ]);
19351
21270
  case "no-id":
19352
21271
  return sentences([
19353
- `${resolve2(root, BUNDLE_PIN_REL)} names no engagement id.`,
21272
+ `${resolve3(root, BUNDLE_PIN_REL)} names no engagement id.`,
19354
21273
  'run "npx halfcycle" here to rewrite it.'
19355
21274
  ]);
19356
21275
  case "bad-id":
@@ -19359,13 +21278,15 @@ function storeProblemSentence(problem, root, home, engagementId) {
19359
21278
  return sentences([
19360
21279
  "this project belongs to another Halfcycle account, so this machine has no",
19361
21280
  "credential for it and the Halfcycle tools are not available here. Nothing is",
19362
- "broken \u2014 ask the project's owner for view access, or sign in as the account",
19363
- 'that owns it and run "npx halfcycle" again.'
21281
+ "broken.",
21282
+ requestAccessSentence(engagementId),
21283
+ "If this account is wrong, sign in as the account that owns it and run",
21284
+ '"npx halfcycle" again.'
19364
21285
  ]);
19365
21286
  case "no-credential":
19366
21287
  return sentences([
19367
21288
  `this machine holds no credential for engagement ${engagementId ?? ""}.`,
19368
- `run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve2(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
21289
+ `run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve3(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
19369
21290
  ]);
19370
21291
  }
19371
21292
  }
@@ -19518,7 +21439,7 @@ var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initializ
19518
21439
  function registeredMcpUrl(projectRoot) {
19519
21440
  let parsed;
19520
21441
  try {
19521
- parsed = JSON.parse(readFileSync11(resolve3(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
21442
+ parsed = JSON.parse(readFileSync13(resolve4(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
19522
21443
  } catch {
19523
21444
  return void 0;
19524
21445
  }
@@ -19625,7 +21546,7 @@ function positionRequestBody(engagementId) {
19625
21546
  params: { name: "halfcycle_position", arguments: { engagementId } }
19626
21547
  });
19627
21548
  }
19628
- async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
21549
+ async function fetchPositionNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
19629
21550
  const controller = new AbortController();
19630
21551
  const timer = setTimeout(() => controller.abort(), deadlineMs);
19631
21552
  try {
@@ -19672,9 +21593,11 @@ async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs
19672
21593
  } catch {
19673
21594
  return void 0;
19674
21595
  }
19675
- const scope = typeof body === "object" && body !== null ? body["scope"] : void 0;
19676
- const parsed = scopeNoticesSchema.safeParse(scope);
19677
- 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 };
19678
21601
  } finally {
19679
21602
  clearTimeout(timer);
19680
21603
  }
@@ -19711,9 +21634,11 @@ async function runRefreshCredential(opts) {
19711
21634
  const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
19712
21635
  const positionDeadline = Math.min(opts.scopeNoticeDeadlineMs ?? SCOPE_NOTICE_DEADLINE_MS, remainingForMargin);
19713
21636
  if (positionDeadline > 0) {
19714
- const scope = await fetchScopeNotices(connection.url, connection.bearer, engagementId, opts.fetchFn ?? globalThis.fetch, positionDeadline);
19715
- if (scope !== void 0)
19716
- 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));
19717
21642
  }
19718
21643
  }
19719
21644
  } else if (connection.sentence === CANNOT_REACH_SENTENCE || connection.sentence === SERVICE_PROBLEM_SENTENCE) {
@@ -19723,7 +21648,7 @@ async function runRefreshCredential(opts) {
19723
21648
  }
19724
21649
  if (readConfig().config === null) {
19725
21650
  const remedySaid = parts.some(namesTheSignInCommand);
19726
- parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid));
21651
+ parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid, pinnedEngagementId(projectRootFromBinary(opts.binaryPath))));
19727
21652
  }
19728
21653
  for (const said of renewalSaid) {
19729
21654
  if (namesTheSignInCommand(said) && parts.some(namesTheSignInCommand))
@@ -19747,7 +21672,7 @@ async function runRefreshCredential(opts) {
19747
21672
  import { fileURLToPath } from "node:url";
19748
21673
 
19749
21674
  // dist/capture.js
19750
- import { readFileSync as readFileSync12, 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";
19751
21676
  import { dirname as dirname5 } from "node:path";
19752
21677
  function runCapture(provider, endpoint, opts = {}) {
19753
21678
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -19784,7 +21709,7 @@ function runCapture(provider, endpoint, opts = {}) {
19784
21709
  const manifestFile = manifestPath(repoRoot);
19785
21710
  try {
19786
21711
  mkdirSync3(dirname5(fixture), { recursive: true });
19787
- writeFileSync4(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
21712
+ writeFileSync6(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
19788
21713
  } catch (err) {
19789
21714
  const msg = err instanceof Error ? err.message : String(err);
19790
21715
  return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
@@ -19815,7 +21740,7 @@ function readInputFile(fromFile) {
19815
21740
  if (!fromFile)
19816
21741
  return void 0;
19817
21742
  try {
19818
- return readFileSync12(fromFile, "utf-8");
21743
+ return readFileSync14(fromFile, "utf-8");
19819
21744
  } catch {
19820
21745
  return void 0;
19821
21746
  }
@@ -19839,6 +21764,17 @@ async function main() {
19839
21764
  process.stdout.write(reply, () => process.exit(0));
19840
21765
  return;
19841
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
+ }
19842
21778
  if (cmd === "mcp-headers") {
19843
21779
  const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
19844
21780
  process.stdout.write(line2, () => process.exit(0));
@@ -19921,16 +21857,17 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
19921
21857
  halfcycle-runner hook user-prompt-submit
19922
21858
  halfcycle-runner refresh-credential
19923
21859
  halfcycle-runner mcp-headers
21860
+ halfcycle-runner observe
19924
21861
  halfcycle-runner resolve-engagement <cwd>
19925
21862
  halfcycle-runner capture <provider> <endpoint> [--from <file>]
19926
21863
  `);
19927
21864
  process.exit(1);
19928
21865
  }
19929
21866
  function readStdin3() {
19930
- return new Promise((resolve4, reject) => {
21867
+ return new Promise((resolve5, reject) => {
19931
21868
  const chunks = [];
19932
21869
  process.stdin.on("data", (chunk) => chunks.push(chunk));
19933
- process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
21870
+ process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8")));
19934
21871
  process.stdin.on("error", reject);
19935
21872
  });
19936
21873
  }