halfcycle 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2266,8 +2266,8 @@ var Doc = class {
2266
2266
  const lines = content.split("\n").filter((x) => x);
2267
2267
  const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length));
2268
2268
  const dedented = lines.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x);
2269
- for (const line of dedented) {
2270
- this.content.push(line);
2269
+ for (const line2 of dedented) {
2270
+ this.content.push(line2);
2271
2271
  }
2272
2272
  }
2273
2273
  compile() {
@@ -14572,14 +14572,14 @@ function unquote(value) {
14572
14572
  }
14573
14573
  function parseEnvText(raw) {
14574
14574
  const out = {};
14575
- for (const line of raw.split("\n")) {
14576
- const eq = line.indexOf("=");
14575
+ for (const line2 of raw.split("\n")) {
14576
+ const eq = line2.indexOf("=");
14577
14577
  if (eq === -1)
14578
14578
  continue;
14579
- const key = line.slice(0, eq).replace(/^\s*export\s+/, "").trim();
14579
+ const key = line2.slice(0, eq).replace(/^\s*export\s+/, "").trim();
14580
14580
  if (key === "" || key.startsWith("#"))
14581
14581
  continue;
14582
- out[key] = unquote(line.slice(eq + 1));
14582
+ out[key] = unquote(line2.slice(eq + 1));
14583
14583
  }
14584
14584
  return out;
14585
14585
  }
@@ -14588,13 +14588,13 @@ function reconcileEnvText(existing, values, keys) {
14588
14588
  const value = values[k];
14589
14589
  return [k, value === void 0 ? "" : value];
14590
14590
  }));
14591
- const line = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
14591
+ const line2 = (k) => `${k}=${shq(desired.get(k) ?? "")}`;
14592
14592
  const written = keys.filter((k) => desired.get(k) !== null);
14593
14593
  if (existing === null) {
14594
14594
  if (written.length === 0)
14595
14595
  return "";
14596
14596
  return `${ENGAGEMENT_ENV_HEADER}
14597
- ` + written.map(line).join("\n") + "\n";
14597
+ ` + written.map(line2).join("\n") + "\n";
14598
14598
  }
14599
14599
  const seen = /* @__PURE__ */ new Set();
14600
14600
  const out = [];
@@ -14615,13 +14615,13 @@ function reconcileEnvText(existing, values, keys) {
14615
14615
  seen.add(key);
14616
14616
  if (desired.get(key) === null)
14617
14617
  continue;
14618
- out.push(`${prefix}${line(key)}`);
14618
+ out.push(`${prefix}${line2(key)}`);
14619
14619
  }
14620
14620
  const missing = written.filter((k) => !seen.has(k));
14621
14621
  if (missing.length > 0) {
14622
14622
  const header = existing.includes(ENGAGEMENT_ENV_HEADER) ? "" : `${ENGAGEMENT_ENV_HEADER}
14623
14623
  `;
14624
- const block = header + missing.map(line).join("\n");
14624
+ const block = header + missing.map(line2).join("\n");
14625
14625
  const trailingBlank = out.length > 0 && out[out.length - 1] === "";
14626
14626
  if (trailingBlank)
14627
14627
  out.splice(out.length - 1, 0, block);
@@ -14662,6 +14662,7 @@ var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
14662
14662
  var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
14663
14663
  var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
14664
14664
  var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
14665
+ var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
14665
14666
  var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
14666
14667
  var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
14667
14668
  function hookCommand(script, verb) {
@@ -14690,6 +14691,13 @@ var HALFCYCLE_HOOK_WIRING = [
14690
14691
  command: hookCommand(USER_PROMPT_REMINDER_REL),
14691
14692
  script: USER_PROMPT_REMINDER_REL
14692
14693
  },
14694
+ {
14695
+ event: "UserPromptSubmit",
14696
+ matcher: null,
14697
+ timeout: 10,
14698
+ command: hookCommand(USER_PROMPT_SCOPE_REL),
14699
+ script: USER_PROMPT_SCOPE_REL
14700
+ },
14693
14701
  {
14694
14702
  event: "PostToolUse",
14695
14703
  matcher: WRITE_CAPABLE_TOOLS.join("|"),
@@ -14765,6 +14773,98 @@ function looksLikeSecret(text) {
14765
14773
  var MCP_REGISTRATION_REL = ".mcp.json";
14766
14774
  var MCP_SERVER_KEY = "halfcycle";
14767
14775
 
14776
+ // ../core/dist/shell-quote.js
14777
+ function quoteForShell(value) {
14778
+ return `'${value.replace(/'/g, `'\\''`)}'`;
14779
+ }
14780
+ var HALFCYCLE_COMMAND = "npx halfcycle@latest";
14781
+
14782
+ // ../core/dist/shown-plans.js
14783
+ var SHOWN_PLANS_FILENAME = "shown-plans.json";
14784
+ var SHOWN_PLANS_FORMAT = 1;
14785
+ var RECORD_KEYS = ["entries", "format"];
14786
+ var ENTRY_KEYS = ["number", "phase", "shownAt", "version"];
14787
+ function parseShownPlans(text) {
14788
+ try {
14789
+ if (typeof text !== "string")
14790
+ return [];
14791
+ 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;
14807
+ } catch {
14808
+ return [];
14809
+ }
14810
+ }
14811
+ function readEntry(raw) {
14812
+ if (!isPlainObject2(raw) || !hasExactKeys(raw, ENTRY_KEYS))
14813
+ return null;
14814
+ const phase = raw["phase"];
14815
+ const number4 = raw["number"];
14816
+ const version2 = raw["version"];
14817
+ const shownAt = raw["shownAt"];
14818
+ if (typeof phase !== "string" || phase === "")
14819
+ return null;
14820
+ if (!isPositiveInteger(number4) || !isPositiveInteger(version2))
14821
+ return null;
14822
+ if (!isIsoInstant(shownAt))
14823
+ return null;
14824
+ return { phase, number: number4, version: version2, shownAt };
14825
+ }
14826
+ function isPlainObject2(value) {
14827
+ if (typeof value !== "object" || value === null || Array.isArray(value))
14828
+ return false;
14829
+ const proto = Object.getPrototypeOf(value);
14830
+ return proto === Object.prototype || proto === null;
14831
+ }
14832
+ function hasExactKeys(value, keys) {
14833
+ const own = Object.keys(value).sort();
14834
+ return own.length === keys.length && own.every((key, i) => key === keys[i]);
14835
+ }
14836
+ function isPositiveInteger(value) {
14837
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
14838
+ }
14839
+ function isIsoInstant(value) {
14840
+ if (typeof value !== "string")
14841
+ return false;
14842
+ const ms = Date.parse(value);
14843
+ return Number.isFinite(ms) && new Date(ms).toISOString() === value;
14844
+ }
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;
14849
+ }
14850
+
14851
+ // ../core/dist/phase-reference.js
14852
+ var TYPED_NUMBER = /^(?:0|[1-9][0-9]*)$/;
14853
+ function resolvePhaseReference(typed, phases) {
14854
+ if (typeof typed !== "string" || typed === "")
14855
+ return null;
14856
+ const byIdentity = phases.filter((entry) => entry.phase === typed);
14857
+ if (byIdentity.length > 0)
14858
+ return byIdentity.length === 1 ? byIdentity[0] : null;
14859
+ if (!TYPED_NUMBER.test(typed))
14860
+ return null;
14861
+ const byNumber = phases.filter((entry) => Number.isInteger(entry.number) && String(entry.number) === typed);
14862
+ return byNumber.length === 1 ? byNumber[0] : null;
14863
+ }
14864
+
14865
+ // ../core/dist/release-version.js
14866
+ var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
14867
+
14768
14868
  // dist/control-origin.js
14769
14869
  var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
14770
14870
  function resolveControlOrigin(env) {
@@ -14830,6 +14930,9 @@ function readMcpServerUrl() {
14830
14930
  const raw = process.env["CLAUDE_CODE_MCP_SERVER_URL"];
14831
14931
  return raw ? raw : void 0;
14832
14932
  }
14933
+ function readSessionAttended(env = process.env) {
14934
+ return env["CLAUDE_CODE_SESSION_ATTENDED"] === "1";
14935
+ }
14833
14936
  function readExcerptConfig() {
14834
14937
  const raw = process.env["HALFCYCLE_SOURCE_EXCERPTS"];
14835
14938
  const enabled = (raw ?? "").trim().toLowerCase() !== "false";
@@ -15040,14 +15143,25 @@ var wireErrorSchema = external_exports.object({
15040
15143
  message: external_exports.string()
15041
15144
  }).strict();
15042
15145
 
15146
+ // ../events/dist/lone-surrogates.js
15147
+ var LONE_SURROGATE = /\p{Cs}/u;
15148
+ function hasLoneSurrogate(text) {
15149
+ return LONE_SURROGATE.test(text);
15150
+ }
15151
+ var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
15152
+
15043
15153
  // ../events/dist/state.js
15044
15154
  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");
15045
15155
  var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
15046
15156
  var stepIdSchema = external_exports.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
15047
- var methodVersionSchema = external_exports.string().regex(/^\d+\.\d+\.\d+$/);
15157
+ var methodVersionSchema = external_exports.string().regex(RELEASE_VERSION_PATTERN);
15048
15158
  var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
15049
15159
  var recordKindSchema = external_exports.enum(["artefact", "step-run", "gate", "intervention"]);
15050
15160
  var methodLayerSchema = external_exports.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
15161
+ var PHASE_SCOPED_LAYERS = ["l4", "l5"];
15162
+ function isPhaseScopedLayer(layer) {
15163
+ return PHASE_SCOPED_LAYERS.includes(layer);
15164
+ }
15051
15165
  var evidenceKindSchema = external_exports.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
15052
15166
  var declarableEvidenceKindSchema = external_exports.enum(["anchor", "diff", "repro", "question-answer"]);
15053
15167
  var dispositionSchema = external_exports.enum([
@@ -15295,6 +15409,683 @@ function describeIdentity(identity) {
15295
15409
  return `(a ${typeof identity})`;
15296
15410
  }
15297
15411
 
15412
+ // ../events/dist/build-record.js
15413
+ var BUILD_RECORD_V2_FORMAT = "halfcycle-build-record/v2";
15414
+ var STATEMENT_MAX = 2e3;
15415
+ var DEVIATION_MAX = 500;
15416
+ var DEVIATIONS_MAX = 20;
15417
+ var ACTOR_MAX = 200;
15418
+ var FINDING_MAX = 500;
15419
+ var FINDINGS_SHOWN_MAX = 50;
15420
+ var PHASE_CLOSE_REASON_MAX = 1900;
15421
+ 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
+ var walkVerdictSchema = external_exports.enum(["clean", "defects"]);
15423
+ var closeDecisionSchema = external_exports.enum(["done-when-met", "override"]);
15424
+ function plainText(max) {
15425
+ const base = max === void 0 ? external_exports.string() : external_exports.string().max(max);
15426
+ return base.refine((v) => !hasControlCharacter(v), {
15427
+ message: "must be one line with no control characters (no line breaks, tabs or NUL)"
15428
+ }).refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE });
15429
+ }
15430
+ function statementText(max) {
15431
+ return plainText(max).refine((v) => !looksLikeSecret(v), {
15432
+ message: "looks like a password or key \u2014 rephrase it"
15433
+ });
15434
+ }
15435
+ var closeActorSchema = plainText(ACTOR_MAX);
15436
+ var PHASE_NAME_RULE = `must be a phase name: not empty, no slash or backslash, not starting with a dot, no control characters, at most ${PHASE_NAME_MAX_BYTES} bytes`;
15437
+ var phaseNameSchema = external_exports.string().refine((v) => isValidPhaseIdentity(v), {
15438
+ message: PHASE_NAME_RULE
15439
+ });
15440
+ var phaseIdentitySchema = external_exports.union([external_exports.string(), external_exports.number()], { error: "must be a phase name or a whole number" }).refine((v) => isValidPhaseIdentity(v), { message: PHASE_NAME_RULE });
15441
+ var closeStatementsSchema = external_exports.object({
15442
+ thesis: statementText(STATEMENT_MAX).nullable(),
15443
+ outcome: statementText(STATEMENT_MAX).nullable(),
15444
+ deviations: external_exports.array(statementText(DEVIATION_MAX)).max(DEVIATIONS_MAX),
15445
+ closeCommit: closeCommitSchema.nullable()
15446
+ }).strict();
15447
+ var closedBySchema = external_exports.discriminatedUnion("kind", [
15448
+ external_exports.object({ kind: external_exports.literal("account"), email: plainText().nullable() }).strict(),
15449
+ external_exports.object({ kind: external_exports.literal("no-account") }).strict(),
15450
+ external_exports.object({ kind: external_exports.literal("account-removed") }).strict(),
15451
+ external_exports.object({ kind: external_exports.literal("not-recorded") }).strict()
15452
+ ]);
15453
+ var STEP_NAME_MAX = 200;
15454
+ var stepNameSchema = plainText(STEP_NAME_MAX).refine((v) => v.trim().length > 0, {
15455
+ message: "must not be blank"
15456
+ });
15457
+ var unfinishedStepSchema = external_exports.object({
15458
+ stepId: plainText(),
15459
+ state: external_exports.enum(["parked", "failed"]),
15460
+ name: stepNameSchema.optional()
15461
+ }).strict();
15462
+ var crossedSchema = external_exports.discriminatedUnion("kind", [
15463
+ external_exports.object({ kind: external_exports.literal("none") }).strict(),
15464
+ external_exports.object({ kind: external_exports.literal("unfinished-steps"), steps: external_exports.array(unfinishedStepSchema).min(1) }).strict(),
15465
+ external_exports.object({ kind: external_exports.literal("clean-over-findings"), findings: external_exports.number().int().min(1) }).strict(),
15466
+ external_exports.object({ kind: external_exports.literal("not-recorded") }).strict()
15467
+ ]);
15468
+ var seatCoverageSchema = external_exports.object({
15469
+ enrolled: external_exports.number().int().nonnegative(),
15470
+ conformant: external_exports.number().int().nonnegative()
15471
+ }).strict().refine((c) => c.conformant <= c.enrolled, {
15472
+ message: "conformant cannot exceed enrolled",
15473
+ path: ["conformant"]
15474
+ });
15475
+ var observedCloseSchema = external_exports.object({
15476
+ sequence: external_exports.number().int().min(1),
15477
+ closedAt: utcIso8601,
15478
+ closedBy: closedBySchema,
15479
+ crossed: crossedSchema,
15480
+ seatCoverage: seatCoverageSchema.nullable()
15481
+ }).strict();
15482
+ var couldNotRunCheckSchema = external_exports.object({
15483
+ check: plainText(),
15484
+ description: plainText().nullable(),
15485
+ reason: plainText()
15486
+ }).strict();
15487
+ var firedCheckSchema = external_exports.object({
15488
+ check: plainText(),
15489
+ description: plainText().nullable(),
15490
+ severity: severitySchema,
15491
+ explanation: plainText().nullable(),
15492
+ occurrences: external_exports.number().int().min(1)
15493
+ }).strict();
15494
+ var phaseChecksSchema = external_exports.discriminatedUnion("state", [
15495
+ external_exports.object({ state: external_exports.literal("not-observed") }).strict(),
15496
+ external_exports.object({
15497
+ state: external_exports.literal("nothing-fired"),
15498
+ runs: external_exports.number().int().min(1),
15499
+ couldNotRun: external_exports.array(couldNotRunCheckSchema)
15500
+ }).strict(),
15501
+ external_exports.object({
15502
+ state: external_exports.literal("fired"),
15503
+ runs: external_exports.number().int().min(1),
15504
+ couldNotRun: external_exports.array(couldNotRunCheckSchema),
15505
+ fired: external_exports.array(firedCheckSchema).min(1)
15506
+ }).strict()
15507
+ ]);
15508
+ var observedSchema = external_exports.object({
15509
+ engagementId: plainText(),
15510
+ methodVersion: methodVersionSchema,
15511
+ closes: external_exports.array(observedCloseSchema).min(1),
15512
+ checks: phaseChecksSchema
15513
+ }).strict().superRefine((o, ctx) => {
15514
+ o.closes.forEach((close, i) => {
15515
+ if (close.sequence !== i + 1) {
15516
+ ctx.addIssue({
15517
+ code: "custom",
15518
+ message: "closes must be numbered 1, 2, 3 \u2026 in order with no gap",
15519
+ path: ["closes", i, "sequence"]
15520
+ });
15521
+ }
15522
+ });
15523
+ });
15524
+ var attestedWalkSchema = external_exports.object({
15525
+ verdict: walkVerdictSchema,
15526
+ findings: external_exports.array(plainText(FINDING_MAX)).max(FINDINGS_SHOWN_MAX),
15527
+ findingsNotShown: external_exports.number().int().nonnegative()
15528
+ }).strict();
15529
+ var attestedStatementsSchema = external_exports.object({
15530
+ thesis: plainText(STATEMENT_MAX).nullable(),
15531
+ outcome: plainText(STATEMENT_MAX).nullable(),
15532
+ deviations: external_exports.array(plainText(DEVIATION_MAX)).max(DEVIATIONS_MAX)
15533
+ }).strict();
15534
+ var attestedCloseSchema = external_exports.object({
15535
+ sequence: external_exports.number().int().min(1),
15536
+ decision: closeDecisionSchema,
15537
+ actor: plainText(ACTOR_MAX).nullable(),
15538
+ reason: plainText(PHASE_CLOSE_REASON_MAX).nullable(),
15539
+ walk: attestedWalkSchema.nullable(),
15540
+ closeCommit: closeCommitSchema.nullable(),
15541
+ statements: attestedStatementsSchema
15542
+ }).strict();
15543
+ var attestedSchema = external_exports.object({
15544
+ closes: external_exports.array(attestedCloseSchema)
15545
+ }).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) => {
15552
+ const observedBySequence = new Map(record2.observed.closes.map((c) => [c.sequence, c]));
15553
+ const attestedSequences = record2.attested.closes.map((c) => c.sequence);
15554
+ const sameSet = attestedSequences.length === observedBySequence.size && new Set(attestedSequences).size === attestedSequences.length && attestedSequences.every((s) => observedBySequence.has(s));
15555
+ if (!sameSet) {
15556
+ ctx.addIssue({
15557
+ code: "custom",
15558
+ message: "attested closes must match the observed closes one for one, by sequence",
15559
+ path: ["attested", "closes"]
15560
+ });
15561
+ }
15562
+ record2.attested.closes.forEach((close, i) => {
15563
+ if (close.decision === "override" !== (close.reason !== null)) {
15564
+ ctx.addIssue({
15565
+ code: "custom",
15566
+ message: "reason is given exactly when the decision is override",
15567
+ path: ["attested", "closes", i, "reason"]
15568
+ });
15569
+ }
15570
+ const observed = observedBySequence.get(close.sequence);
15571
+ if (observed !== void 0 && observed.closedBy.kind === "account-removed" !== (close.actor === null)) {
15572
+ ctx.addIssue({
15573
+ code: "custom",
15574
+ message: "actor is empty exactly when the closing account was removed",
15575
+ path: ["attested", "closes", i, "actor"]
15576
+ });
15577
+ }
15578
+ });
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();
15712
+
15713
+ // ../events/dist/system-description.js
15714
+ var SYSTEM_DESCRIPTION_FORMAT = 1;
15715
+ var SYSTEM_PARTS_MAX = 60;
15716
+ var SYSTEM_CONNECTIONS_MAX = 400;
15717
+ var SYSTEM_SLOT_MAX = 11;
15718
+ var SYSTEM_OWNS_MAX = 20;
15719
+ var SYSTEM_OWNED_PATTERN_MAX = 200;
15720
+ var SYSTEM_PART_NAME_MAX = 60;
15721
+ var SYSTEM_PART_DESCRIPTION_MAX = 160;
15722
+ var SYSTEM_CONNECTION_LABEL_MAX = 60;
15723
+ var SYSTEM_PART_ID_PATTERN = /^[a-z][a-z0-9-]{0,39}$/;
15724
+ var SYSTEM_LANES = ["clients", "edge", "services", "data", "external"];
15725
+ var systemLaneSchema = external_exports.enum(SYSTEM_LANES, {
15726
+ error: `must be one of ${SYSTEM_LANES.join(", ")}`
15727
+ });
15728
+ var SYSTEM_CONNECTION_KINDS = ["call", "event", "write", "external"];
15729
+ var systemConnectionKindSchema = external_exports.enum(SYSTEM_CONNECTION_KINDS, {
15730
+ error: `must be one of ${SYSTEM_CONNECTION_KINDS.join(", ")}`
15731
+ });
15732
+ var LINE_SEPARATOR = /[\u2028\u2029]/;
15733
+ function descriptionText(max) {
15734
+ return statementText(max).refine((v) => v.trim().length > 0, { message: "must not be empty or only spaces" }).refine((v) => !LINE_SEPARATOR.test(v), {
15735
+ message: "must be one line: it holds a line or paragraph separator"
15736
+ });
15737
+ }
15738
+ var systemPartIdSchema = external_exports.string().regex(SYSTEM_PART_ID_PATTERN, {
15739
+ message: "must be a short id: a lowercase letter, then lowercase letters, digits or hyphens, 40 characters at most"
15740
+ }).refine((v) => !looksLikeSecret(v), { message: "looks like a password or key; choose another id" });
15741
+ function hasEmptySegment(pattern) {
15742
+ return pattern.split("/").some((segment) => segment.length === 0);
15743
+ }
15744
+ function hasDotSegment(pattern) {
15745
+ return pattern.split("/").some((segment) => segment === "." || segment === "..");
15746
+ }
15747
+ function hasPartialDoubleStar(pattern) {
15748
+ return pattern.split("/").some((segment) => segment.includes("**") && segment !== "**");
15749
+ }
15750
+ var SEGMENT_CHARACTERS = /^[A-Za-z0-9._\-*]*$/;
15751
+ function hasForeignCharacter(pattern) {
15752
+ return pattern.split("/").some((segment) => !SEGMENT_CHARACTERS.test(segment));
15753
+ }
15754
+ var systemOwnedPatternSchema = external_exports.string().max(SYSTEM_OWNED_PATTERN_MAX, { message: `must be at most ${SYSTEM_OWNED_PATTERN_MAX} characters` }).refine((v) => !hasEmptySegment(v), {
15755
+ message: "must not start or end with a slash, or have two slashes together"
15756
+ }).refine((v) => !hasDotSegment(v), { message: 'must not have a "." or ".." folder in it' }).refine((v) => !hasPartialDoubleStar(v), { message: '"**" must stand alone between slashes' }).refine((v) => !hasForeignCharacter(v), {
15757
+ message: 'may use only letters, digits, ".", "_", "-" and "*" between slashes'
15758
+ }).refine((v) => !looksLikeSecret(v), {
15759
+ message: "this path looks like a credential; list a shorter folder above it"
15760
+ });
15761
+ var systemPartSchema = external_exports.object({
15762
+ id: systemPartIdSchema,
15763
+ name: descriptionText(SYSTEM_PART_NAME_MAX),
15764
+ description: descriptionText(SYSTEM_PART_DESCRIPTION_MAX),
15765
+ lane: systemLaneSchema,
15766
+ slot: external_exports.number({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).int({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).min(0, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).max(SYSTEM_SLOT_MAX, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }),
15767
+ outside: external_exports.boolean(),
15768
+ owns: external_exports.array(systemOwnedPatternSchema).max(SYSTEM_OWNS_MAX, { message: `can list at most ${SYSTEM_OWNS_MAX} paths` })
15769
+ }).strict();
15770
+ var systemConnectionSchema = external_exports.object({
15771
+ from: systemPartIdSchema,
15772
+ to: systemPartIdSchema,
15773
+ kind: systemConnectionKindSchema,
15774
+ label: descriptionText(SYSTEM_CONNECTION_LABEL_MAX)
15775
+ }).strict();
15776
+ var systemDescriptionSchema = external_exports.object({
15777
+ format: external_exports.literal(SYSTEM_DESCRIPTION_FORMAT, { error: `must be ${SYSTEM_DESCRIPTION_FORMAT}` }),
15778
+ parts: external_exports.array(systemPartSchema).min(1, { message: "must list at least one part" }).max(SYSTEM_PARTS_MAX, { message: `can hold at most ${SYSTEM_PARTS_MAX} parts (five lanes of twelve slots)` }),
15779
+ connections: external_exports.array(systemConnectionSchema).max(SYSTEM_CONNECTIONS_MAX, { message: `can hold at most ${SYSTEM_CONNECTIONS_MAX} connections` })
15780
+ }).strict();
15781
+ var systemDescriptionProblemSchema = external_exports.object({
15782
+ path: external_exports.string(),
15783
+ message: external_exports.string()
15784
+ }).strict();
15785
+ var systemReportRecordedSchema = external_exports.object({
15786
+ outcome: external_exports.literal("recorded"),
15787
+ version: external_exports.number().int().min(1),
15788
+ reportedAt: utcIso8601
15789
+ }).strict();
15790
+ var systemReportRefusedSchema = external_exports.object({
15791
+ outcome: external_exports.literal("refused"),
15792
+ problems: external_exports.array(systemDescriptionProblemSchema).min(1)
15793
+ }).strict();
15794
+ var systemReportUnavailableSchema = external_exports.object({
15795
+ outcome: external_exports.literal("unavailable")
15796
+ }).strict();
15797
+ var systemReportResultSchema = external_exports.discriminatedUnion("outcome", [
15798
+ systemReportRecordedSchema,
15799
+ systemReportRefusedSchema,
15800
+ systemReportUnavailableSchema
15801
+ ]);
15802
+ var SYSTEM_DRAW_SVG_MAX_BYTES = 2 * 1024 * 1024;
15803
+ var SYSTEM_DRAW_MERMAID_MAX_BYTES = 256 * 1024;
15804
+ var SYSTEM_DRAW_PNG_MAX_BYTES = 8 * 1024 * 1024;
15805
+ var systemDrawArchitectureRequestSchema = external_exports.object({
15806
+ picture: external_exports.literal("architecture"),
15807
+ version: external_exports.number().int().min(1)
15808
+ }).strict();
15809
+ var systemDrawPhaseRequestSchema = external_exports.object({
15810
+ picture: external_exports.literal("phase"),
15811
+ phase: phaseIdentitySchema,
15812
+ planVersion: external_exports.number().int().min(1)
15813
+ }).strict();
15814
+ var systemDrawRequestSchema = external_exports.discriminatedUnion("picture", [
15815
+ systemDrawArchitectureRequestSchema,
15816
+ systemDrawPhaseRequestSchema
15817
+ ]);
15818
+ function utf8ByteLength2(text) {
15819
+ let bytes = 0;
15820
+ for (let i = 0; i < text.length; i++) {
15821
+ const unit = text.charCodeAt(i);
15822
+ if (unit < 128)
15823
+ bytes += 1;
15824
+ else if (unit < 2048)
15825
+ bytes += 2;
15826
+ else if (unit >= 55296 && unit <= 56319 && i + 1 < text.length) {
15827
+ const next = text.charCodeAt(i + 1);
15828
+ if (next >= 56320 && next <= 57343) {
15829
+ bytes += 4;
15830
+ i++;
15831
+ } else {
15832
+ bytes += 3;
15833
+ }
15834
+ } else
15835
+ bytes += 3;
15836
+ }
15837
+ return bytes;
15838
+ }
15839
+ var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
15840
+ function base64DecodedLength(text) {
15841
+ const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
15842
+ return text.length / 4 * 3 - padding;
15843
+ }
15844
+ var systemPictureSchema = external_exports.object({
15845
+ svg: external_exports.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_SVG_MAX_BYTES, { message: "the picture is larger than 2 MB" }),
15846
+ 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
+ }).strict();
15848
+ var systemDrawRefusedSchema = external_exports.object({
15849
+ outcome: external_exports.literal("refused"),
15850
+ reason: external_exports.string().min(1)
15851
+ }).strict();
15852
+ var systemDrawUnavailableSchema = external_exports.object({
15853
+ outcome: external_exports.literal("unavailable")
15854
+ }).strict();
15855
+
15856
+ // ../events/dist/phase-plan.js
15857
+ var PHASE_PLAN_FORMAT = 1;
15858
+ var PHASE_PLAN_CHANGES_MAX = 60;
15859
+ var PHASE_PLAN_SUMMARY_MAX = 160;
15860
+ var SCOPE_NOTE_MAX = 500;
15861
+ var PHASE_PLAN_PHASE_MAX = buildPhaseSchema.maxLength ?? 0;
15862
+ var PHASE_PLAN_CHANGE_KINDS = ["add", "change", "remove"];
15863
+ var phasePlanChangeSchema = external_exports.enum(PHASE_PLAN_CHANGE_KINDS, {
15864
+ error: `must be one of ${PHASE_PLAN_CHANGE_KINDS.join(", ")}`
15865
+ });
15866
+ var phasePlanPhaseSchema = phaseIdentitySchema.refine((v) => !isValidPhaseIdentity(v) || buildPhaseSchema.safeParse(String(v)).success, { message: `must be at most ${PHASE_PLAN_PHASE_MAX} characters` });
15867
+ var phasePlanSummarySchema = descriptionText(PHASE_PLAN_SUMMARY_MAX);
15868
+ var phasePlanEntrySchema = external_exports.object({
15869
+ part: systemPartIdSchema,
15870
+ change: phasePlanChangeSchema,
15871
+ summary: phasePlanSummarySchema
15872
+ }).strict();
15873
+ var phasePlanSchema = external_exports.object({
15874
+ format: external_exports.literal(PHASE_PLAN_FORMAT, { error: `must be ${PHASE_PLAN_FORMAT}` }),
15875
+ phase: phasePlanPhaseSchema,
15876
+ changes: external_exports.array(phasePlanEntrySchema).min(1, { message: "must list at least one part" }).max(PHASE_PLAN_CHANGES_MAX, { message: `can list at most ${PHASE_PLAN_CHANGES_MAX} parts` })
15877
+ }).strict();
15878
+ var scopePhaseSchema = buildPhaseSchema.refine((v) => isValidPhaseIdentity(v), {
15879
+ message: "must be a phase name"
15880
+ });
15881
+ var SCOPE_STATE_NAMES = [
15882
+ "not-planned",
15883
+ "waiting",
15884
+ "sent-back",
15885
+ "approved",
15886
+ "partly-stale",
15887
+ "withdrawn"
15888
+ ];
15889
+ var scopeStateNameSchema = external_exports.enum(SCOPE_STATE_NAMES);
15890
+ var plannedScopeStateSchema = scopeStateNameSchema.exclude(["not-planned"]);
15891
+ var SCOPE_PART_STATUSES = ["approved", "waiting"];
15892
+ var scopePartStatusSchema = external_exports.enum(SCOPE_PART_STATUSES);
15893
+ var SCOPE_ACTS = ["approve", "send-back", "withdraw"];
15894
+ var scopeActSchema = external_exports.enum(SCOPE_ACTS);
15895
+ var SCOPE_GIVEN_IN = ["console", "chat"];
15896
+ var scopeGivenInSchema = external_exports.enum(SCOPE_GIVEN_IN);
15897
+ var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
15898
+ var scopeNoteWireSchema = external_exports.string().max(SCOPE_NOTE_MAX);
15899
+ var SCOPE_PART_VERDICTS = ["ok", "change"];
15900
+ var scopePartVerdictSchema = external_exports.enum(SCOPE_PART_VERDICTS);
15901
+ var scopeDecisionPartSchema = external_exports.object({
15902
+ part: systemPartIdSchema,
15903
+ verdict: scopePartVerdictSchema,
15904
+ note: scopeNoteWireSchema.optional()
15905
+ }).strict();
15906
+ var planVersionSchema = external_exports.number().int().min(1);
15907
+ var phasePlanRequestSchema = external_exports.object({
15908
+ descriptionVersion: external_exports.number().int().min(1),
15909
+ plan: external_exports.unknown()
15910
+ }).strict();
15911
+ var phasePlanRecordedSchema = external_exports.object({
15912
+ outcome: external_exports.literal("recorded"),
15913
+ version: planVersionSchema,
15914
+ phaseNumber: external_exports.number().int().min(1),
15915
+ state: plannedScopeStateSchema,
15916
+ changedParts: external_exports.array(systemPartIdSchema).max(PHASE_PLAN_CHANGES_MAX),
15917
+ typedApprovalOffered: external_exports.boolean(),
15918
+ reportedAt: utcIso8601
15919
+ }).strict();
15920
+ var phasePlanRefusedSchema = external_exports.object({
15921
+ outcome: external_exports.literal("refused"),
15922
+ problems: external_exports.array(systemDescriptionProblemSchema).min(1)
15923
+ }).strict();
15924
+ var phasePlanUnavailableSchema = external_exports.object({
15925
+ outcome: external_exports.literal("unavailable")
15926
+ }).strict();
15927
+ var phasePlanResultSchema = external_exports.discriminatedUnion("outcome", [
15928
+ phasePlanRecordedSchema,
15929
+ phasePlanRefusedSchema,
15930
+ phasePlanUnavailableSchema
15931
+ ]);
15932
+ var scopeApproveRequestSchema = external_exports.object({
15933
+ act: external_exports.literal("approve"),
15934
+ planVersion: planVersionSchema,
15935
+ parts: external_exports.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX)
15936
+ }).strict();
15937
+ var scopeSendBackRequestSchema = external_exports.object({
15938
+ act: external_exports.literal("send-back"),
15939
+ planVersion: planVersionSchema,
15940
+ parts: external_exports.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
15941
+ note: scopeNoteWireSchema.optional()
15942
+ }).strict();
15943
+ var scopeWithdrawRequestSchema = external_exports.object({
15944
+ act: external_exports.literal("withdraw"),
15945
+ planVersion: planVersionSchema
15946
+ }).strict();
15947
+ var scopeDecisionRequestSchema = external_exports.discriminatedUnion("act", [
15948
+ scopeApproveRequestSchema,
15949
+ scopeSendBackRequestSchema,
15950
+ scopeWithdrawRequestSchema
15951
+ ]);
15952
+ var scopeDecisionRecordedSchema = external_exports.object({
15953
+ outcome: external_exports.literal("recorded"),
15954
+ act: scopeActSchema,
15955
+ phase: scopePhaseSchema,
15956
+ planVersion: planVersionSchema,
15957
+ state: plannedScopeStateSchema,
15958
+ decidedAt: utcIso8601
15959
+ }).strict();
15960
+ var scopeDecisionAlreadyApprovedSchema = external_exports.object({
15961
+ outcome: external_exports.literal("already-approved"),
15962
+ replanNeeded: external_exports.boolean(),
15963
+ reason: external_exports.string().min(1)
15964
+ }).strict();
15965
+ var scopeDecisionRefusedSchema = external_exports.object({
15966
+ outcome: external_exports.literal("refused"),
15967
+ problems: external_exports.array(systemDescriptionProblemSchema).min(1)
15968
+ }).strict();
15969
+ var scopeDecisionUnavailableSchema = external_exports.object({
15970
+ outcome: external_exports.literal("unavailable")
15971
+ }).strict();
15972
+ function reasonOutcome(outcome) {
15973
+ return external_exports.object({ outcome: external_exports.literal(outcome), reason: external_exports.string().min(1) }).strict();
15974
+ }
15975
+ var scopeDecisionResultSchema = external_exports.discriminatedUnion("outcome", [
15976
+ scopeDecisionRecordedSchema,
15977
+ scopeDecisionAlreadyApprovedSchema,
15978
+ scopeDecisionRefusedSchema,
15979
+ scopeDecisionUnavailableSchema,
15980
+ reasonOutcome("not-owner"),
15981
+ reasonOutcome("no-owner"),
15982
+ reasonOutcome("plan-changed"),
15983
+ reasonOutcome("nothing-to-withdraw"),
15984
+ reasonOutcome("build-started")
15985
+ ]);
15986
+ var scopeReadPartSchema = external_exports.object({
15987
+ part: systemPartIdSchema,
15988
+ name: systemPartSchema.shape.name,
15989
+ change: phasePlanChangeSchema,
15990
+ summary: phasePlanSummarySchema,
15991
+ status: scopePartStatusSchema,
15992
+ changed: external_exports.boolean()
15993
+ }).strict();
15994
+ var scopeReadDecisionSchema = external_exports.object({
15995
+ act: scopeActSchema,
15996
+ planVersion: planVersionSchema,
15997
+ givenIn: scopeGivenInSchema,
15998
+ at: utcIso8601,
15999
+ parts: external_exports.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
16000
+ note: scopeNoteWireSchema.optional()
16001
+ }).strict();
16002
+ var scopeReadNotPlannedSchema = external_exports.object({
16003
+ phase: scopePhaseSchema,
16004
+ state: external_exports.literal("not-planned"),
16005
+ canDecide: external_exports.boolean()
16006
+ }).strict();
16007
+ var scopeReadPlannedSchema = external_exports.object({
16008
+ phase: scopePhaseSchema,
16009
+ state: plannedScopeStateSchema,
16010
+ phaseNumber: external_exports.number().int().min(1),
16011
+ planVersion: planVersionSchema,
16012
+ parts: external_exports.array(scopeReadPartSchema).min(1).max(PHASE_PLAN_CHANGES_MAX),
16013
+ decisions: external_exports.array(scopeReadDecisionSchema),
16014
+ canDecide: external_exports.boolean(),
16015
+ replanLine: external_exports.string().min(1).optional()
16016
+ }).strict();
16017
+ var scopeReadSchema = external_exports.union([scopeReadNotPlannedSchema, scopeReadPlannedSchema]);
16018
+
16019
+ // ../events/dist/client-version.js
16020
+ var CLIENT_VERSION_HEADER = "x-halfcycle-client";
16021
+ var TYPED_APPROVAL_OFF_SENTENCE = "Approving by typing is switched off for now.";
16022
+ var CLIENT_UPDATE_SENTENCE = `To approve by typing in a session, Halfcycle needs updating: run \`${HALFCYCLE_COMMAND}\` in this project's folder, then reopen your session and ask again.`;
16023
+
16024
+ // ../events/dist/scope-notices.js
16025
+ var scopePartNameSchema = external_exports.string().min(1).max(SYSTEM_PART_NAME_MAX);
16026
+ var scopeNoteEntrySchema = external_exports.object({
16027
+ part: scopePartNameSchema.optional(),
16028
+ note: external_exports.string().min(1).max(SCOPE_NOTE_MAX)
16029
+ }).strict();
16030
+ var scopeSentBackSchema = external_exports.object({
16031
+ phase: scopePhaseSchema,
16032
+ number: external_exports.number().int().min(1),
16033
+ notes: external_exports.array(scopeNoteEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX + 1)
16034
+ }).strict().superRefine((entry, ctx) => {
16035
+ entry.notes.forEach((n, i) => {
16036
+ if (n.part === void 0 && i !== entry.notes.length - 1) {
16037
+ ctx.addIssue({
16038
+ code: external_exports.ZodIssueCode.custom,
16039
+ path: ["notes", i],
16040
+ message: "the note on the whole phase comes after every note on a part"
16041
+ });
16042
+ }
16043
+ });
16044
+ });
16045
+ var scopeWaitingSchema = external_exports.object({
16046
+ phase: scopePhaseSchema,
16047
+ number: external_exports.number().int().min(1),
16048
+ changedParts: external_exports.array(scopePartNameSchema).max(PHASE_PLAN_CHANGES_MAX)
16049
+ }).strict();
16050
+ var scopeNoticesSchema = external_exports.object({
16051
+ sentBack: external_exports.array(scopeSentBackSchema),
16052
+ waiting: external_exports.array(scopeWaitingSchema),
16053
+ typedApprovalOffered: external_exports.boolean()
16054
+ }).strict().refine((v) => v.sentBack.length > 0 || v.waiting.length > 0, {
16055
+ message: "carried only when a phase was sent back or waits for the owner"
16056
+ });
16057
+ function quotedParts(parts) {
16058
+ return parts.map((p) => `"${p}"`).join(", ");
16059
+ }
16060
+ function approvalSentence(phase, offerTyping) {
16061
+ if (offerTyping === "off")
16062
+ return TYPED_APPROVAL_OFF_SENTENCE;
16063
+ if (offerTyping === "update")
16064
+ return CLIENT_UPDATE_SENTENCE;
16065
+ return `In their own Claude Code session, they ask to see the plan, check each part, and then type: approve ${phase}`;
16066
+ }
16067
+ function sentBackNotice(entry) {
16068
+ return [
16069
+ `Before anything else: the project's owner sent the scope of phase ${entry.number}, "${entry.phase}", back with these notes.`,
16070
+ ...entry.notes.map((n) => n.part === void 0 ? `- ${n.note}` : `- "${n.part}": ${n.note}`),
16071
+ `Change the plan to answer every note, then send it again with \`${HALFCYCLE_COMMAND} plan ${quoteForShell(entry.phase)}\`.`
16072
+ ].join("\n");
16073
+ }
16074
+ function waitingNotice(entry, offerTyping) {
16075
+ return [
16076
+ ...entry.changedParts.length > 0 ? [`These parts changed since the owner last decided on this phase, and wait for their approval: ${quotedParts(entry.changedParts)}.`] : [],
16077
+ `The scope of phase ${entry.number}, "${entry.phase}", is waiting for the project's owner to approve it.`,
16078
+ approvalSentence(entry.phase, offerTyping)
16079
+ ].join(" ");
16080
+ }
16081
+ function scopeNoticeText(scope, options) {
16082
+ const omit2 = options.omitActivePhase === true ? options.activePhase : void 0;
16083
+ return {
16084
+ sentBack: scope.sentBack.map(sentBackNotice),
16085
+ waiting: scope.waiting.filter((w) => omit2 === void 0 || w.phase !== omit2).map((w) => waitingNotice(w, options.offerTyping))
16086
+ };
16087
+ }
16088
+
15298
16089
  // ../events/dist/credential-wire.js
15299
16090
  var engagementCredentialResponseSchema = external_exports.object({
15300
16091
  engagementId: external_exports.string(),
@@ -15324,12 +16115,60 @@ var NOT_THIS_ACCOUNT_SENTENCE = `Halfcycle is not connected: this project belong
15324
16115
  var UPDATE_CLAUDE_CODE_SENTENCE = "Halfcycle is not connected: this Claude Code version does not name the server it connects to, so no credential was sent. Update Claude Code, then reopen.";
15325
16116
  var CANNOT_REACH_SENTENCE = "Halfcycle is not connected: its server could not be reached, so its tools are unavailable in this session. Check your network, then reopen the project.";
15326
16117
  var SERVICE_PROBLEM_SENTENCE = "Halfcycle is not connected: its service had a problem just now, so nothing in this session is being checked. Reopen the project to try again.";
16118
+ var SCOPE_UNCHECKED_SENTENCE = "If this project needs approval before building, that cannot be checked either, so work done now shows as built without approval unless already approved.";
15327
16119
  var PLACEHOLDER_SENTENCES = Object.freeze({
15328
16120
  [NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
15329
16121
  [NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
15330
16122
  [UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
15331
16123
  });
15332
16124
 
16125
+ // ../events/dist/scope-chat.js
16126
+ var chatScopeApproveRequestSchema = external_exports.object({
16127
+ act: external_exports.literal("approve"),
16128
+ reference: phaseIdentitySchema,
16129
+ planVersion: planVersionSchema
16130
+ }).strict();
16131
+ var chatScopeSendBackRequestSchema = external_exports.object({
16132
+ act: external_exports.literal("send-back"),
16133
+ reference: phaseIdentitySchema,
16134
+ planVersion: planVersionSchema,
16135
+ note: scopeNoteWireSchema.optional()
16136
+ }).strict();
16137
+ var chatScopeRequestSchema = external_exports.discriminatedUnion("act", [
16138
+ chatScopeApproveRequestSchema,
16139
+ chatScopeSendBackRequestSchema
16140
+ ]);
16141
+ var CHAT_SCOPE_ACTS = ["approve", "send-back"];
16142
+ var chatScopeRecordedSchema = external_exports.object({
16143
+ outcome: external_exports.literal("recorded"),
16144
+ act: external_exports.enum(CHAT_SCOPE_ACTS),
16145
+ phase: scopePhaseSchema,
16146
+ planVersion: planVersionSchema
16147
+ }).strict();
16148
+ var chatScopeAlreadyApprovedSchema = external_exports.object({
16149
+ outcome: external_exports.literal("already-approved"),
16150
+ replanNeeded: external_exports.boolean()
16151
+ }).strict();
16152
+ var chatScopeRefusedSchema = external_exports.object({
16153
+ outcome: external_exports.literal("refused"),
16154
+ problems: external_exports.array(systemDescriptionProblemSchema).min(1)
16155
+ }).strict();
16156
+ function bareOutcome(outcome) {
16157
+ return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
16158
+ }
16159
+ var chatScopeResultSchema = external_exports.discriminatedUnion("outcome", [
16160
+ chatScopeRecordedSchema,
16161
+ chatScopeAlreadyApprovedSchema,
16162
+ chatScopeRefusedSchema,
16163
+ bareOutcome("show-first"),
16164
+ bareOutcome("plan-changed"),
16165
+ bareOutcome("build-started"),
16166
+ bareOutcome("not-owner"),
16167
+ bareOutcome("no-owner"),
16168
+ bareOutcome("not-offered"),
16169
+ bareOutcome("unavailable")
16170
+ ]);
16171
+
15333
16172
  // dist/hook-protocol.js
15334
16173
  import { writeSync } from "node:fs";
15335
16174
  function emitBlock(reason) {
@@ -15915,16 +16754,16 @@ function detectEnvReads(diffContent, jsComments) {
15915
16754
  if (raw.startsWith("-") && !raw.startsWith("---"))
15916
16755
  continue;
15917
16756
  const isSource = raw.startsWith("+") && !raw.startsWith("+++") || raw.startsWith(" ");
15918
- const line = jsComments && isSource ? raw.slice(1) : raw;
15919
- const comment = jsComments && isSource ? commentedOut(line) : void 0;
16757
+ const line2 = jsComments && isSource ? raw.slice(1) : raw;
16758
+ const comment = jsComments && isSource ? commentedOut(line2) : void 0;
15920
16759
  const live = (match) => comment === void 0 || !comment(match.index, match.index + match[0].length);
15921
- for (const match of line.matchAll(dotPattern))
16760
+ for (const match of line2.matchAll(dotPattern))
15922
16761
  if (live(match))
15923
16762
  found.add(match[1]);
15924
- for (const match of line.matchAll(bracketPattern))
16763
+ for (const match of line2.matchAll(bracketPattern))
15925
16764
  if (live(match))
15926
16765
  found.add(match[1]);
15927
- for (const destructure of line.matchAll(destructurePattern)) {
16766
+ for (const destructure of line2.matchAll(destructurePattern)) {
15928
16767
  if (!live(destructure))
15929
16768
  continue;
15930
16769
  for (const part of destructure[1].split(",")) {
@@ -15937,25 +16776,25 @@ function detectEnvReads(diffContent, jsComments) {
15937
16776
  return found;
15938
16777
  }
15939
16778
  var LINE_TERMINATOR = /[\r\u2028\u2029]/g;
15940
- function commentedOut(line) {
15941
- const lead = line.length - line.trimStart().length;
16779
+ function commentedOut(line2) {
16780
+ const lead = line2.length - line2.trimStart().length;
15942
16781
  let body;
15943
16782
  let close;
15944
16783
  let expression;
15945
- if (line.startsWith("//", lead)) {
16784
+ if (line2.startsWith("//", lead)) {
15946
16785
  body = lead + 2;
15947
16786
  LINE_TERMINATOR.lastIndex = body;
15948
- close = LINE_TERMINATOR.exec(line)?.index ?? line.length;
16787
+ close = LINE_TERMINATOR.exec(line2)?.index ?? line2.length;
15949
16788
  expression = "{";
15950
- } else if (line.startsWith("/*", lead) || line.startsWith("{/*", lead)) {
15951
- body = lead + (line[lead] === "{" ? 3 : 2);
15952
- const found = line.indexOf("*/", body);
15953
- close = found === -1 ? line.length : found;
15954
- expression = line[lead] === "{" ? "${" : "{";
16789
+ } else if (line2.startsWith("/*", lead) || line2.startsWith("{/*", lead)) {
16790
+ body = lead + (line2[lead] === "{" ? 3 : 2);
16791
+ const found = line2.indexOf("*/", body);
16792
+ close = found === -1 ? line2.length : found;
16793
+ expression = line2[lead] === "{" ? "${" : "{";
15955
16794
  } else {
15956
16795
  return void 0;
15957
16796
  }
15958
- const opened = line.indexOf(expression, body);
16797
+ const opened = line2.indexOf(expression, body);
15959
16798
  return (start, end) => start >= body && end <= close && (opened === -1 || opened >= start || opened >= close);
15960
16799
  }
15961
16800
  function collectDeclarationFiles(context) {
@@ -16034,8 +16873,8 @@ function flyTomlSectionHas(name, content, sectionHeader) {
16034
16873
  const lines = content.split("\n");
16035
16874
  let inSection = false;
16036
16875
  const keyPattern = new RegExp(`^${name}\\s*=`);
16037
- for (const line of lines) {
16038
- const trimmed = line.trim();
16876
+ for (const line2 of lines) {
16877
+ const trimmed = line2.trim();
16039
16878
  if (trimmed === sectionHeader) {
16040
16879
  inSection = true;
16041
16880
  continue;
@@ -16086,10 +16925,10 @@ function extractSqlWrites(diffContent) {
16086
16925
  seen.add(key);
16087
16926
  writes.push(write);
16088
16927
  };
16089
- for (const line of diffContent.split("\n")) {
16090
- if (isRemovedLine(line) || isContextLine(line) || isDiffHeader(line))
16928
+ for (const line2 of diffContent.split("\n")) {
16929
+ if (isRemovedLine(line2) || isContextLine(line2) || isDiffHeader(line2))
16091
16930
  continue;
16092
- const code = stripComments(stripDiffPrefix(line));
16931
+ const code = stripComments(stripDiffPrefix(line2));
16093
16932
  push(parseRawSql(code));
16094
16933
  push(parseQueryBuilder(code));
16095
16934
  push(parseOrmCall(code));
@@ -16099,23 +16938,23 @@ function extractSqlWrites(diffContent) {
16099
16938
  }
16100
16939
  return writes;
16101
16940
  }
16102
- function isRemovedLine(line) {
16103
- return line.startsWith("-") && !line.startsWith("---");
16941
+ function isRemovedLine(line2) {
16942
+ return line2.startsWith("-") && !line2.startsWith("---");
16104
16943
  }
16105
- function isContextLine(line) {
16106
- return line.startsWith(" ");
16944
+ function isContextLine(line2) {
16945
+ return line2.startsWith(" ");
16107
16946
  }
16108
- function isDiffHeader(line) {
16109
- return line.startsWith("+++") || line.startsWith("---");
16947
+ function isDiffHeader(line2) {
16948
+ return line2.startsWith("+++") || line2.startsWith("---");
16110
16949
  }
16111
- function stripDiffPrefix(line) {
16112
- if (line.startsWith("+") && !line.startsWith("+++"))
16113
- return line.slice(1);
16114
- if (line.startsWith("-") && !line.startsWith("---"))
16115
- return line.slice(1);
16116
- if (line.startsWith(" "))
16117
- return line.slice(1);
16118
- return line;
16950
+ function stripDiffPrefix(line2) {
16951
+ if (line2.startsWith("+") && !line2.startsWith("+++"))
16952
+ return line2.slice(1);
16953
+ if (line2.startsWith("-") && !line2.startsWith("---"))
16954
+ return line2.slice(1);
16955
+ if (line2.startsWith(" "))
16956
+ return line2.slice(1);
16957
+ return line2;
16119
16958
  }
16120
16959
  function assembleStatements(diffContent) {
16121
16960
  const statements = [];
@@ -16128,16 +16967,16 @@ function assembleStatements(diffContent) {
16128
16967
  buffer = [];
16129
16968
  hasAddedLine = false;
16130
16969
  };
16131
- for (const line of diffContent.split("\n")) {
16132
- if (line.startsWith("@@") || isDiffHeader(line)) {
16970
+ for (const line2 of diffContent.split("\n")) {
16971
+ if (line2.startsWith("@@") || isDiffHeader(line2)) {
16133
16972
  flush(true);
16134
16973
  continue;
16135
16974
  }
16136
- if (isRemovedLine(line)) {
16975
+ if (isRemovedLine(line2)) {
16137
16976
  flush(false);
16138
16977
  continue;
16139
16978
  }
16140
- const rawContent = stripDiffPrefix(line);
16979
+ const rawContent = stripDiffPrefix(line2);
16141
16980
  if (rawContent.trim() === "") {
16142
16981
  flush(true);
16143
16982
  continue;
@@ -16145,7 +16984,7 @@ function assembleStatements(diffContent) {
16145
16984
  const content = stripComments(rawContent);
16146
16985
  if (content.trim() === "")
16147
16986
  continue;
16148
- if (line.startsWith("+"))
16987
+ if (line2.startsWith("+"))
16149
16988
  hasAddedLine = true;
16150
16989
  buffer.push(content.trim());
16151
16990
  if (content.includes(";"))
@@ -16162,44 +17001,44 @@ function normaliseTable(raw) {
16162
17001
  return name.toLowerCase();
16163
17002
  }
16164
17003
  var TABLE_IDENT = String.raw`((?:"[^"]+"|[a-zA-Z_][a-zA-Z0-9_]*)(?:\.(?:"[^"]+"|[a-zA-Z_][a-zA-Z0-9_]*))?)`;
16165
- function parseRawSql(line) {
16166
- if (/\bON\s+CONFLICT\b/i.test(line)) {
16167
- const m = line.match(new RegExp(String.raw`INSERT\s+INTO\s+${TABLE_IDENT}`, "i"));
17004
+ function parseRawSql(line2) {
17005
+ if (/\bON\s+CONFLICT\b/i.test(line2)) {
17006
+ const m = line2.match(new RegExp(String.raw`INSERT\s+INTO\s+${TABLE_IDENT}`, "i"));
16168
17007
  if (m)
16169
17008
  return { kind: "upsert", table: normaliseTable(m[1]), whereColumns: [] };
16170
17009
  }
16171
- const upsertM = line.match(new RegExp(String.raw`UPSERT\s+INTO\s+${TABLE_IDENT}`, "i"));
17010
+ const upsertM = line2.match(new RegExp(String.raw`UPSERT\s+INTO\s+${TABLE_IDENT}`, "i"));
16172
17011
  if (upsertM)
16173
17012
  return { kind: "upsert", table: normaliseTable(upsertM[1]), whereColumns: [] };
16174
- const insertM = line.match(new RegExp(String.raw`INSERT\s+INTO\s+${TABLE_IDENT}`, "i"));
17013
+ const insertM = line2.match(new RegExp(String.raw`INSERT\s+INTO\s+${TABLE_IDENT}`, "i"));
16175
17014
  if (insertM)
16176
17015
  return { kind: "insert", table: normaliseTable(insertM[1]), whereColumns: [] };
16177
- const updateM = line.match(new RegExp(String.raw`\bUPDATE\s+${TABLE_IDENT}\s+[\s\S]*?\bSET\b`, "i"));
17016
+ const updateM = line2.match(new RegExp(String.raw`\bUPDATE\s+${TABLE_IDENT}\s+[\s\S]*?\bSET\b`, "i"));
16178
17017
  if (updateM) {
16179
17018
  return {
16180
17019
  kind: "update",
16181
17020
  table: normaliseTable(updateM[1]),
16182
- whereColumns: extractWhereColumns(line)
17021
+ whereColumns: extractWhereColumns(line2)
16183
17022
  };
16184
17023
  }
16185
- const deleteM = line.match(new RegExp(String.raw`DELETE\s+FROM\s+${TABLE_IDENT}`, "i"));
17024
+ const deleteM = line2.match(new RegExp(String.raw`DELETE\s+FROM\s+${TABLE_IDENT}`, "i"));
16186
17025
  if (deleteM) {
16187
17026
  return {
16188
17027
  kind: "delete",
16189
17028
  table: normaliseTable(deleteM[1]),
16190
- whereColumns: extractWhereColumns(line)
17029
+ whereColumns: extractWhereColumns(line2)
16191
17030
  };
16192
17031
  }
16193
17032
  return null;
16194
17033
  }
16195
17034
  function parseQueryBuilder(rawLine) {
16196
- const line = stripDiffPrefix(rawLine);
16197
- const m = line.match(/(?<!\bcreate(?:Hash|Hmac|Sign|Verify|Cipheriv|Decipheriv)\s*\([^)]*)['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
17035
+ const line2 = stripDiffPrefix(rawLine);
17036
+ const m = line2.match(/(?<!\bcreate(?:Hash|Hmac|Sign|Verify|Cipheriv|Decipheriv)\s*\([^)]*)['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
16198
17037
  if (m) {
16199
17038
  const kind = m[2].toLowerCase();
16200
17039
  return { kind, table: m[1].toLowerCase(), whereColumns: [] };
16201
17040
  }
16202
- const fromM = line.match(/\.(?:from|into|table)\s*\(\s*['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
17041
+ const fromM = line2.match(/\.(?:from|into|table)\s*\(\s*['"]([\w]+)['"]\s*\)\s*\.(insert|update|upsert|delete)\s*\(/i);
16203
17042
  if (fromM) {
16204
17043
  const kind = fromM[2].toLowerCase();
16205
17044
  return { kind, table: fromM[1].toLowerCase(), whereColumns: [] };
@@ -16223,11 +17062,11 @@ var TYPEORM_KIND = {
16223
17062
  delete: "delete"
16224
17063
  };
16225
17064
  function parseOrmCall(rawLine) {
16226
- const line = stripDiffPrefix(rawLine);
16227
- return parsePrisma(line) ?? parseTypeorm(line) ?? parseDrizzle(line);
17065
+ const line2 = stripDiffPrefix(rawLine);
17066
+ return parsePrisma(line2) ?? parseTypeorm(line2) ?? parseDrizzle(line2);
16228
17067
  }
16229
- function parsePrisma(line) {
16230
- const m = line.match(/\b[\w$]+\.([a-zA-Z_$][\w$]*)\.(create|createMany|update|updateMany|upsert|delete|deleteMany)\s*\(/);
17068
+ function parsePrisma(line2) {
17069
+ const m = line2.match(/\b[\w$]+\.([a-zA-Z_$][\w$]*)\.(create|createMany|update|updateMany|upsert|delete|deleteMany)\s*\(/);
16231
17070
  if (!m)
16232
17071
  return null;
16233
17072
  const kind = PRISMA_KIND[m[2].toLowerCase()];
@@ -16235,29 +17074,29 @@ function parsePrisma(line) {
16235
17074
  return null;
16236
17075
  return { kind, table: m[1].toLowerCase(), whereColumns: [] };
16237
17076
  }
16238
- function parseDrizzle(line) {
16239
- const upd = line.match(/\b[\w$]+\.update\s*\(\s*['"]?([\w$]+)['"]?\s*\)\s*\.set\s*\(/);
17077
+ function parseDrizzle(line2) {
17078
+ const upd = line2.match(/\b[\w$]+\.update\s*\(\s*['"]?([\w$]+)['"]?\s*\)\s*\.set\s*\(/);
16240
17079
  if (upd)
16241
17080
  return { kind: "update", table: upd[1].toLowerCase(), whereColumns: [] };
16242
- const ins = line.match(/\b[\w$]+\.insert\s*\(\s*['"]?([\w$]+)['"]?\s*\)/);
17081
+ const ins = line2.match(/\b[\w$]+\.insert\s*\(\s*['"]?([\w$]+)['"]?\s*\)/);
16243
17082
  if (ins)
16244
17083
  return { kind: "insert", table: ins[1].toLowerCase(), whereColumns: [] };
16245
- const del = line.match(/\b[\w$]+\.delete\s*\(\s*['"]?([\w$]+)['"]?\s*\)/);
17084
+ const del = line2.match(/\b[\w$]+\.delete\s*\(\s*['"]?([\w$]+)['"]?\s*\)/);
16246
17085
  if (del)
16247
17086
  return { kind: "delete", table: del[1].toLowerCase(), whereColumns: [] };
16248
17087
  return null;
16249
17088
  }
16250
- function parseTypeorm(line) {
16251
- const qb = line.match(/\.createQueryBuilder\s*\([^)]*\)[\s\S]*?\.update\s*\(\s*([a-zA-Z_$][\w$]*)/);
17089
+ function parseTypeorm(line2) {
17090
+ const qb = line2.match(/\.createQueryBuilder\s*\([^)]*\)[\s\S]*?\.update\s*\(\s*([a-zA-Z_$][\w$]*)/);
16252
17091
  if (qb)
16253
17092
  return { kind: "update", table: qb[1].toLowerCase(), whereColumns: [] };
16254
- const saveUpsert = line.match(/(?:^|[^.\w$])([a-zA-Z_$][\w$]*)\.(save|upsert)\s*\(/);
17093
+ const saveUpsert = line2.match(/(?:^|[^.\w$])([a-zA-Z_$][\w$]*)\.(save|upsert)\s*\(/);
16255
17094
  if (saveUpsert) {
16256
17095
  const kind = TYPEORM_KIND[saveUpsert[2].toLowerCase()];
16257
17096
  if (kind)
16258
17097
  return { kind, table: saveUpsert[1].toLowerCase(), whereColumns: [] };
16259
17098
  }
16260
- const repo = line.match(/(?:^|[^.\w$])([a-zA-Z_$][\w$]*(?:repo|repository))\.(insert|update|delete)\s*\(/i);
17099
+ const repo = line2.match(/(?:^|[^.\w$])([a-zA-Z_$][\w$]*(?:repo|repository))\.(insert|update|delete)\s*\(/i);
16261
17100
  if (repo) {
16262
17101
  const kind = TYPEORM_KIND[repo[2].toLowerCase()];
16263
17102
  if (kind)
@@ -16265,8 +17104,8 @@ function parseTypeorm(line) {
16265
17104
  }
16266
17105
  return null;
16267
17106
  }
16268
- function extractWhereColumns(line) {
16269
- const whereM = line.match(/\bWHERE\s+(.+)/i);
17107
+ function extractWhereColumns(line2) {
17108
+ const whereM = line2.match(/\bWHERE\s+(.+)/i);
16270
17109
  if (!whereM)
16271
17110
  return [];
16272
17111
  const fragment = whereM[1].replace(/\s+(?:ORDER|LIMIT|GROUP|HAVING)\b.*/i, "");
@@ -16282,17 +17121,17 @@ function extractSchemaConstraints(diffContent) {
16282
17121
  const results = [];
16283
17122
  const seen = /* @__PURE__ */ new Set();
16284
17123
  let currentTable;
16285
- for (const line of diffContent.split("\n")) {
16286
- if (line.startsWith("-") && !line.startsWith("---")) {
17124
+ for (const line2 of diffContent.split("\n")) {
17125
+ if (line2.startsWith("-") && !line2.startsWith("---")) {
16287
17126
  currentTable = void 0;
16288
17127
  continue;
16289
17128
  }
16290
- if (line.startsWith("@@")) {
17129
+ if (line2.startsWith("@@")) {
16291
17130
  currentTable = void 0;
16292
17131
  continue;
16293
17132
  }
16294
- const isAdded = line.startsWith("+") && !line.startsWith("+++");
16295
- const raw = stripDiffPrefix2(line);
17133
+ const isAdded = line2.startsWith("+") && !line2.startsWith("+++");
17134
+ const raw = stripDiffPrefix2(line2);
16296
17135
  if (raw.trim() === "") {
16297
17136
  currentTable = void 0;
16298
17137
  continue;
@@ -16316,22 +17155,22 @@ function extractSchemaConstraints(diffContent) {
16316
17155
  }
16317
17156
  return results;
16318
17157
  }
16319
- function stripDiffPrefix2(line) {
16320
- if (line.startsWith("+") && !line.startsWith("+++"))
16321
- return line.slice(1);
16322
- if (line.startsWith(" "))
16323
- return line.slice(1);
16324
- return line;
17158
+ function stripDiffPrefix2(line2) {
17159
+ if (line2.startsWith("+") && !line2.startsWith("+++"))
17160
+ return line2.slice(1);
17161
+ if (line2.startsWith(" "))
17162
+ return line2.slice(1);
17163
+ return line2;
16325
17164
  }
16326
- function parseConstraintLine(line, trackedTable) {
16327
- const alterTable = extractAlterTableName(line) ?? trackedTable ?? "unknown";
16328
- const createUniqueIndex = line.match(new RegExp(String.raw`CREATE\s+UNIQUE\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+)?ON\s+${TABLE_IDENT2}`, "i"));
17165
+ function parseConstraintLine(line2, trackedTable) {
17166
+ const alterTable = extractAlterTableName(line2) ?? trackedTable ?? "unknown";
17167
+ const createUniqueIndex = line2.match(new RegExp(String.raw`CREATE\s+UNIQUE\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+)?ON\s+${TABLE_IDENT2}`, "i"));
16329
17168
  if (createUniqueIndex) {
16330
17169
  const constraintName = createUniqueIndex[1] ?? void 0;
16331
17170
  const table = normaliseTable2(createUniqueIndex[2]);
16332
17171
  return { table, kind: "unique", ...constraintName ? { constraintName } : {} };
16333
17172
  }
16334
- const addConstraint = line.match(/ADD\s+CONSTRAINT\s+(\w+)\s+(CHECK|UNIQUE|FOREIGN\s+KEY)\b/i);
17173
+ const addConstraint = line2.match(/ADD\s+CONSTRAINT\s+(\w+)\s+(CHECK|UNIQUE|FOREIGN\s+KEY)\b/i);
16335
17174
  if (addConstraint) {
16336
17175
  const constraintName = addConstraint[1];
16337
17176
  const kindRaw = addConstraint[2].toUpperCase();
@@ -16345,22 +17184,22 @@ function parseConstraintLine(line, trackedTable) {
16345
17184
  }
16346
17185
  return { table: alterTable, kind, constraintName };
16347
17186
  }
16348
- const addCheck = line.match(/ADD\s+CHECK\s*\(/i);
17187
+ const addCheck = line2.match(/ADD\s+CHECK\s*\(/i);
16349
17188
  if (addCheck) {
16350
17189
  return { table: alterTable, kind: "check" };
16351
17190
  }
16352
- const addUnique = line.match(/ADD\s+UNIQUE\s*\(/i);
17191
+ const addUnique = line2.match(/ADD\s+UNIQUE\s*\(/i);
16353
17192
  if (addUnique) {
16354
17193
  return { table: alterTable, kind: "unique" };
16355
17194
  }
16356
- const addForeignKey = line.match(/ADD\s+FOREIGN\s+KEY\s*\(/i);
17195
+ const addForeignKey = line2.match(/ADD\s+FOREIGN\s+KEY\s*\(/i);
16357
17196
  if (addForeignKey) {
16358
17197
  return { table: alterTable, kind: "foreignKey" };
16359
17198
  }
16360
17199
  return null;
16361
17200
  }
16362
- function extractAlterTableName(line) {
16363
- const m = line.match(new RegExp(String.raw`ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?${TABLE_IDENT2}`, "i"));
17201
+ function extractAlterTableName(line2) {
17202
+ const m = line2.match(new RegExp(String.raw`ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?${TABLE_IDENT2}`, "i"));
16364
17203
  return m ? normaliseTable2(m[1]) : void 0;
16365
17204
  }
16366
17205
  var TABLE_IDENT2 = String.raw`((?:"[^"]+"|[a-zA-Z_][a-zA-Z0-9_]*)(?:\.(?:"[^"]+"|[a-zA-Z_][a-zA-Z0-9_]*))?)`;
@@ -16381,10 +17220,10 @@ function detectBlastRadiusMarkers(filePath, diffContent) {
16381
17220
  markerKind: "patch-every-field-path"
16382
17221
  });
16383
17222
  }
16384
- for (const line of diffContent.split("\n")) {
16385
- if (line.startsWith("-") && !line.startsWith("---"))
17223
+ for (const line2 of diffContent.split("\n")) {
17224
+ if (line2.startsWith("-") && !line2.startsWith("---"))
16386
17225
  continue;
16387
- const symbolMatch = line.match(/\bUPDATABLE_FIELDS(?:_FOR_(\w+))?\b/);
17226
+ const symbolMatch = line2.match(/\bUPDATABLE_FIELDS(?:_FOR_(\w+))?\b/);
16388
17227
  if (symbolMatch) {
16389
17228
  const table = symbolMatch[1] ? symbolMatch[1].toLowerCase() : "unknown";
16390
17229
  markers.push({ table, markerKind: "updatable-fields-symbol" });
@@ -16398,10 +17237,10 @@ import { readFileSync as readFileSync2, readdirSync } from "node:fs";
16398
17237
  import { join } from "node:path";
16399
17238
  function extractNumericColumns(diffContent, context = {}) {
16400
17239
  const addedLines = [];
16401
- for (const line of diffContent.split("\n")) {
16402
- if (line.startsWith("-") && !line.startsWith("---"))
17240
+ for (const line2 of diffContent.split("\n")) {
17241
+ if (line2.startsWith("-") && !line2.startsWith("---"))
16403
17242
  continue;
16404
- addedLines.push(stripDiffPrefix3(line));
17243
+ addedLines.push(stripDiffPrefix3(line2));
16405
17244
  }
16406
17245
  const columnDecls = extractColumnDeclarations(addedLines);
16407
17246
  if (columnDecls.length === 0)
@@ -16480,19 +17319,19 @@ function walkForRegistrations(dir, repoRoot, acc) {
16480
17319
  acc.add(m[1]);
16481
17320
  }
16482
17321
  }
16483
- function stripDiffPrefix3(line) {
16484
- if (line.startsWith("+") && !line.startsWith("+++"))
16485
- return line.slice(1);
16486
- if (line.startsWith(" "))
16487
- return line.slice(1);
16488
- return line;
17322
+ function stripDiffPrefix3(line2) {
17323
+ if (line2.startsWith("+") && !line2.startsWith("+++"))
17324
+ return line2.slice(1);
17325
+ if (line2.startsWith(" "))
17326
+ return line2.slice(1);
17327
+ return line2;
16489
17328
  }
16490
17329
  function extractColumnDeclarations(lines) {
16491
17330
  const decls = [];
16492
17331
  let currentTable = null;
16493
17332
  let insideCreateTable = false;
16494
- for (const line of lines) {
16495
- const trimmed = line.trim();
17333
+ for (const line2 of lines) {
17334
+ const trimmed = line2.trim();
16496
17335
  const alterAdd = trimmed.match(/ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(\w+)\s+ADD\s+(?:COLUMN\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+(NUMERIC|BIGINT|DECIMAL)(?:\s*\([^)]*\))?/i);
16497
17336
  if (alterAdd) {
16498
17337
  const table = alterAdd[1].toLowerCase();
@@ -16519,8 +17358,8 @@ function extractColumnDeclarations(lines) {
16519
17358
  }
16520
17359
  return decls;
16521
17360
  }
16522
- function checkInlineColumn(line, table, decls) {
16523
- const colMatch = line.match(/^\s*(\w+)\s+(NUMERIC|BIGINT|DECIMAL)(?:\s*\([^)]*\))?(?:\s|,|$)/i);
17361
+ function checkInlineColumn(line2, table, decls) {
17362
+ const colMatch = line2.match(/^\s*(\w+)\s+(NUMERIC|BIGINT|DECIMAL)(?:\s*\([^)]*\))?(?:\s|,|$)/i);
16524
17363
  if (colMatch) {
16525
17364
  const name = colMatch[1].toUpperCase();
16526
17365
  if (SQL_KEYWORDS.has(name))
@@ -16559,8 +17398,8 @@ function resolveWireFields(lines, decls) {
16559
17398
  existing.push(decl);
16560
17399
  colToDecls.set(decl.column, existing);
16561
17400
  }
16562
- for (const line of lines) {
16563
- const trimmed = line.trim();
17401
+ for (const line2 of lines) {
17402
+ const trimmed = line2.trim();
16564
17403
  const assignment = trimmed.match(/^(\w+)\s*:\s*(?:\w+\.)?(\w+)\s*[,;]?\s*(?:\/\/.*)?$/);
16565
17404
  if (assignment) {
16566
17405
  const wireField = assignment[1];
@@ -16678,10 +17517,10 @@ function extractThirdPartyEndpoints(diffContent, context = {}) {
16678
17517
  }
16679
17518
  function detectEndpointMarkers(diffContent) {
16680
17519
  const detected = [];
16681
- for (const line of diffContent.split("\n")) {
16682
- if (line.startsWith("-") && !line.startsWith("---"))
17520
+ for (const line2 of diffContent.split("\n")) {
17521
+ if (line2.startsWith("-") && !line2.startsWith("---"))
16683
17522
  continue;
16684
- const stripped = stripDiffPrefix4(line).trim();
17523
+ const stripped = stripDiffPrefix4(line2).trim();
16685
17524
  const decoratorSimple = stripped.match(/@ThirdParty(?:Endpoint)?\s*\(\s*['"]([^'"]+)['"]\s*\)/);
16686
17525
  if (decoratorSimple) {
16687
17526
  detected.push({ signature: decoratorSimple[1] });
@@ -16728,12 +17567,12 @@ function detectEndpointMarkers(diffContent) {
16728
17567
  }
16729
17568
  return detected;
16730
17569
  }
16731
- function stripDiffPrefix4(line) {
16732
- if (line.startsWith("+") && !line.startsWith("+++"))
16733
- return line.slice(1);
16734
- if (line.startsWith(" "))
16735
- return line.slice(1);
16736
- return line;
17570
+ function stripDiffPrefix4(line2) {
17571
+ if (line2.startsWith("+") && !line2.startsWith("+++"))
17572
+ return line2.slice(1);
17573
+ if (line2.startsWith(" "))
17574
+ return line2.slice(1);
17575
+ return line2;
16737
17576
  }
16738
17577
 
16739
17578
  // dist/changeset.js
@@ -17358,6 +18197,9 @@ function writeRefreshedToken(opts) {
17358
18197
  // dist/credential-refresh.js
17359
18198
  var CONTROL_TIMEOUT_MS = 5e3;
17360
18199
  var CONNECTION_CHECK_DEADLINE_MS = 3e3;
18200
+ var SCOPE_NOTICE_DEADLINE_MS = 1500;
18201
+ var SESSION_START_HOOK_TIMEOUT_MS = 1e4;
18202
+ var SESSION_START_MARGIN_MS = 1500;
17361
18203
  var UNAUTHORIZED = 401;
17362
18204
  async function probeAccount(controlOrigin, credential, fetchFn = globalThis.fetch) {
17363
18205
  const controller = new AbortController();
@@ -17649,7 +18491,7 @@ function readSeatConfiguration(repoRoot) {
17649
18491
  paths: readPathStates(projectDir, repoRoot)
17650
18492
  };
17651
18493
  }
17652
- function isPlainObject2(value) {
18494
+ function isPlainObject3(value) {
17653
18495
  return typeof value === "object" && value !== null && !Array.isArray(value);
17654
18496
  }
17655
18497
  function readSettingsFile(projectDir, rel) {
@@ -17667,12 +18509,12 @@ function readSettingsFile(projectDir, rel) {
17667
18509
  } catch {
17668
18510
  return { path: rel, state: "unreadable", hooks: [] };
17669
18511
  }
17670
- if (!isPlainObject2(parsed))
18512
+ if (!isPlainObject3(parsed))
17671
18513
  return { path: rel, state: "unreadable", hooks: [] };
17672
18514
  return { path: rel, state: "read", hooks: ourHooks(parsed["hooks"]) };
17673
18515
  }
17674
18516
  function ourHooks(hooks) {
17675
- if (!isPlainObject2(hooks))
18517
+ if (!isPlainObject3(hooks))
17676
18518
  return [];
17677
18519
  const rows = [];
17678
18520
  const seen = /* @__PURE__ */ new Set();
@@ -17680,7 +18522,7 @@ function ourHooks(hooks) {
17680
18522
  if (!Array.isArray(entries))
17681
18523
  continue;
17682
18524
  for (const entry of entries) {
17683
- if (!isPlainObject2(entry))
18525
+ if (!isPlainObject3(entry))
17684
18526
  continue;
17685
18527
  let matcher = null;
17686
18528
  if (Object.hasOwn(entry, "matcher")) {
@@ -17692,7 +18534,7 @@ function ourHooks(hooks) {
17692
18534
  if (!Array.isArray(commands))
17693
18535
  continue;
17694
18536
  for (const hook of commands) {
17695
- if (!isPlainObject2(hook))
18537
+ if (!isPlainObject3(hook))
17696
18538
  continue;
17697
18539
  const command = hook["command"];
17698
18540
  if (typeof command !== "string" || !HALFCYCLE_HOOK_COMMANDS.has(command))
@@ -18138,18 +18980,233 @@ async function runStop() {
18138
18980
  await runSessionDiff(input, "stop");
18139
18981
  }
18140
18982
 
18141
- // dist/resolve-engagement.js
18142
- import { existsSync as existsSync6, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18143
- import { join as join8, sep as sep2 } from "node:path";
18983
+ // dist/hooks/user-prompt-submit.js
18984
+ import { readFileSync as readFileSync8 } from "node:fs";
18144
18985
  import { homedir as homedir4 } from "node:os";
18986
+ import { join as join8 } from "node:path";
18987
+
18988
+ // dist/hooks/scope-phrase.js
18989
+ var APPROVE_PREFIX = /^approve /i;
18990
+ var SEND_BACK_PREFIX = /^send back /i;
18991
+ var LINE_BREAK = /[\n\r\u000b\u000c\u0085\u2028\u2029]/;
18992
+ function scopePhraseShape(prompt) {
18993
+ if (typeof prompt !== "string")
18994
+ return null;
18995
+ const line2 = prompt.trim();
18996
+ if (LINE_BREAK.test(line2))
18997
+ return null;
18998
+ const approve = APPROVE_PREFIX.exec(line2);
18999
+ if (approve)
19000
+ return { act: "approve", rest: line2.slice(approve[0].length) };
19001
+ const sendBack = SEND_BACK_PREFIX.exec(line2);
19002
+ if (sendBack)
19003
+ return { act: "send-back", rest: line2.slice(sendBack[0].length) };
19004
+ return null;
19005
+ }
19006
+ function matchScopePhraseShape(shape, shown) {
19007
+ if (shape.act === "approve") {
19008
+ const entry = resolvePhaseReference(shape.rest, shown);
19009
+ return entry === null ? { kind: "show-first", act: "approve" } : { kind: "approve", entry, reference: shape.rest };
19010
+ }
19011
+ for (let colon = shape.rest.lastIndexOf(":"); colon > 0; colon = shape.rest.lastIndexOf(":", colon - 1)) {
19012
+ const reference = shape.rest.slice(0, colon);
19013
+ const entry = resolvePhaseReference(reference, shown);
19014
+ if (entry !== null) {
19015
+ return { kind: "send-back", entry, reference, note: shape.rest.slice(colon + 1).trim() };
19016
+ }
19017
+ }
19018
+ return { kind: "show-first", act: "send-back" };
19019
+ }
19020
+
19021
+ // dist/hooks/user-prompt-submit-lines.js
19022
+ var TELL_THE_PERSON = "Tell the person.";
19023
+ function line(text) {
19024
+ return `${text} ${TELL_THE_PERSON}`;
19025
+ }
19026
+ function nothingDone(act) {
19027
+ return act === "approve" ? "Nothing was approved" : "Nothing was sent back";
19028
+ }
19029
+ function showCommand(phase) {
19030
+ return `\`${HALFCYCLE_COMMAND} plan ${quoteForShell(phase)} --show\``;
19031
+ }
19032
+ function recordedLine(act, phase, planVersion) {
19033
+ return act === "approve" ? line(`Approved ${phase}, plan version ${planVersion}, in your session.`) : line(`Sent ${phase} back with your note, plan version ${planVersion}, in your session. The note is given first to the next session that asks what to do.`);
19034
+ }
19035
+ function notAttendedLine(act) {
19036
+ return line(`${nothingDone(act)}: approving or sending back by typing counts only when a person types it into an open session.`);
19037
+ }
19038
+ function notSignedInLine(act) {
19039
+ return line(`${nothingDone(act)}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
19040
+ }
19041
+ function showFirstLine(act) {
19042
+ 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
+ }
19044
+ function tooOldLine(act, phase) {
19045
+ return line(`${nothingDone(act)}: ${phase} was listed an hour or more ago. List it again with ${showCommand(phase)}, then type the line again within the hour.`);
19046
+ }
19047
+ function noteNeededLine() {
19048
+ return line("Nothing was sent back: a send-back needs a note after the colon saying what should change.");
19049
+ }
19050
+ function noteLooksSecretLine() {
19051
+ return line("Nothing was sent back: the note looks like it holds a password or key, so it never left this computer. Type the send-back again without it.");
19052
+ }
19053
+ function noteTooLongLine() {
19054
+ return line("Nothing was sent back: the note is too long. Shorten it and type the send-back again.");
19055
+ }
19056
+ function noteRefusedLine() {
19057
+ return line("Nothing was sent back: the note was not accepted. Reword it in plain words and type the send-back again.");
19058
+ }
19059
+ function approveRefusedLine() {
19060
+ return line(`${nothingDone("approve")}: Halfcycle did not accept the request. ${CLIENT_UPDATE_SENTENCE}`);
19061
+ }
19062
+ function cannotReachLine(act) {
19063
+ return line(`${nothingDone(act)}: Halfcycle could not be reached just now. Type the line again in a minute.`);
19064
+ }
19065
+ function lineForAnswer(act, phase, answer) {
19066
+ switch (answer.outcome) {
19067
+ case "recorded":
19068
+ return recordedLine(answer.act, answer.phase, answer.planVersion);
19069
+ case "show-first":
19070
+ return showFirstLine(act);
19071
+ case "plan-changed":
19072
+ return line(`${nothingDone(act)}: the plan for ${phase} changed after it was listed here. List it again with ${showCommand(phase)}, check the parts, then type the line again.`);
19073
+ case "already-approved":
19074
+ return answer.replanNeeded ? line(`${phase} is already approved, so nothing changed. Some parts wait because the description of the system changed since the plan was written: write the plan for ${phase} again, run ${showCommand(phase)}, then approve the new version.`) : line(`${phase} is already approved, so nothing changed.`);
19075
+ case "build-started":
19076
+ 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
+ case "not-owner":
19078
+ return line(`${nothingDone(act)}: only the project's owner can approve or send back this scope.`);
19079
+ case "no-owner":
19080
+ return line(`${nothingDone(act)}: this project has no owner, so nobody can approve or send back its scope.`);
19081
+ case "not-offered":
19082
+ return line(`${nothingDone(act)}. ${TYPED_APPROVAL_OFF_SENTENCE}`);
19083
+ case "refused":
19084
+ return act === "approve" ? approveRefusedLine() : noteRefusedLine();
19085
+ case "unavailable":
19086
+ return cannotReachLine(act);
19087
+ default: {
19088
+ const unreachable = answer;
19089
+ return unreachable;
19090
+ }
19091
+ }
19092
+ }
19093
+
19094
+ // dist/hooks/user-prompt-submit.js
19095
+ var CHAT_REQUEST_TIMEOUT_MS = 6e3;
19096
+ var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
19097
+ async function runUserPromptSubmit(opts) {
19098
+ let act = null;
19099
+ try {
19100
+ const prompt = promptOf(opts.input);
19101
+ const shape = scopePhraseShape(prompt);
19102
+ if (shape === null)
19103
+ return "";
19104
+ act = shape.act;
19105
+ return `${await decide(shape, opts)}
19106
+ `;
19107
+ } catch {
19108
+ return act === null ? "" : `${cannotReachLine(act)}
19109
+ `;
19110
+ }
19111
+ }
19112
+ async function decide(shape, opts) {
19113
+ const { act } = shape;
19114
+ const env = opts.env ?? process.env;
19115
+ if (!readSessionAttended(env))
19116
+ return notAttendedLine(act);
19117
+ const read = readConfig(env);
19118
+ if (read.config === null)
19119
+ return notSignedInLine(act);
19120
+ const { guardServiceToken, guardEngagementId } = read.config;
19121
+ const shown = readShownPlans(guardEngagementId, opts.home);
19122
+ const match = matchScopePhraseShape(shape, shown);
19123
+ if (match.kind === "show-first")
19124
+ return showFirstLine(act);
19125
+ const { entry } = match;
19126
+ if (!shownRecently(entry, (opts.now ?? Date.now)()))
19127
+ return tooOldLine(act, entry.phase);
19128
+ let candidate;
19129
+ if (match.kind === "send-back") {
19130
+ if (match.note === "")
19131
+ return noteNeededLine();
19132
+ if (!scopeNoteWireSchema.safeParse(match.note).success)
19133
+ return noteTooLongLine();
19134
+ if (looksLikeSecret(match.note))
19135
+ return noteLooksSecretLine();
19136
+ candidate = { act: "send-back", reference: match.reference, planVersion: entry.version, note: match.note };
19137
+ } else {
19138
+ candidate = { act: "approve", reference: match.reference, planVersion: entry.version };
19139
+ }
19140
+ const request = chatScopeRequestSchema.safeParse(candidate);
19141
+ if (!request.success)
19142
+ return showFirstLine(act);
19143
+ return post(act, entry, request.data, guardEngagementId, guardServiceToken, env, opts.fetchFn);
19144
+ }
19145
+ function promptOf(input) {
19146
+ try {
19147
+ const parsed = JSON.parse(input);
19148
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
19149
+ return void 0;
19150
+ return parsed["prompt"];
19151
+ } catch {
19152
+ return void 0;
19153
+ }
19154
+ }
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);
19168
+ return Number.isFinite(age) && age >= 0 && age < SHOWN_ENTRY_MAX_AGE_MS;
19169
+ }
19170
+ async function post(act, entry, request, engagementId, credential, env, fetchFn = globalThis.fetch) {
19171
+ const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/phases/${encodeURIComponent(entry.phase)}/scope/chat`;
19172
+ const controller = new AbortController();
19173
+ const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
19174
+ try {
19175
+ const response = await fetchFn(url2, {
19176
+ method: "POST",
19177
+ headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json" },
19178
+ body: JSON.stringify(request),
19179
+ signal: controller.signal
19180
+ });
19181
+ if (response.status === 401)
19182
+ return notSignedInLine(act);
19183
+ if (response.status === 429 || response.status >= 500)
19184
+ return cannotReachLine(act);
19185
+ const parsed = chatScopeResultSchema.safeParse(await response.json());
19186
+ if (!parsed.success)
19187
+ return cannotReachLine(act);
19188
+ if (parsed.data.outcome === "recorded" && response.status !== 200)
19189
+ return cannotReachLine(act);
19190
+ return lineForAnswer(act, entry.phase, parsed.data);
19191
+ } catch {
19192
+ return cannotReachLine(act);
19193
+ } finally {
19194
+ clearTimeout(timer);
19195
+ }
19196
+ }
19197
+
19198
+ // dist/resolve-engagement.js
19199
+ import { existsSync as existsSync6, readFileSync as readFileSync9, realpathSync as realpathSync3 } from "node:fs";
19200
+ import { join as join9, sep as sep2 } from "node:path";
19201
+ import { homedir as homedir5 } from "node:os";
18145
19202
  function defaultRegistryPath() {
18146
- return join8(homedir4(), ".halfcycle", "registry.json");
19203
+ return join9(homedir5(), ".halfcycle", "registry.json");
18147
19204
  }
18148
19205
  function readRegistry(registryPath = defaultRegistryPath()) {
18149
19206
  if (!existsSync6(registryPath))
18150
19207
  return {};
18151
19208
  try {
18152
- const parsed = JSON.parse(readFileSync8(registryPath, "utf-8"));
19209
+ const parsed = JSON.parse(readFileSync9(registryPath, "utf-8"));
18153
19210
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
18154
19211
  return {};
18155
19212
  const out = {};
@@ -18197,18 +19254,21 @@ function runResolveEngagement(cwd, registryPath) {
18197
19254
  }
18198
19255
 
18199
19256
  // dist/refresh-credential.js
18200
- import { readFileSync as readFileSync10 } from "node:fs";
19257
+ import { readFileSync as readFileSync11 } from "node:fs";
18201
19258
  import { resolve as resolve3 } from "node:path";
18202
19259
 
18203
19260
  // dist/mcp-headers.js
18204
- import { readFileSync as readFileSync9 } from "node:fs";
18205
- import { homedir as homedir5 } from "node:os";
19261
+ import { readFileSync as readFileSync10 } from "node:fs";
19262
+ import { homedir as homedir6 } from "node:os";
18206
19263
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
18207
19264
  function defaultWriteErr(text) {
18208
19265
  process.stderr.write(text);
18209
19266
  }
18210
- function headerLine(bearer) {
18211
- return `${JSON.stringify({ Authorization: `Bearer ${bearer}` })}
19267
+ function headerLine(bearer, clientVersion) {
19268
+ const header = { Authorization: `Bearer ${bearer}` };
19269
+ if (clientVersion !== void 0)
19270
+ header[CLIENT_VERSION_HEADER] = clientVersion;
19271
+ return `${JSON.stringify(header)}
18212
19272
  `;
18213
19273
  }
18214
19274
  function projectRootFromBinary(binaryPath) {
@@ -18218,7 +19278,7 @@ function projectRootFromBinary(binaryPath) {
18218
19278
  function readPinnedId(root) {
18219
19279
  let raw;
18220
19280
  try {
18221
- raw = readFileSync9(resolve2(root, BUNDLE_PIN_REL), "utf-8");
19281
+ raw = readFileSync10(resolve2(root, BUNDLE_PIN_REL), "utf-8");
18222
19282
  } catch {
18223
19283
  return { problem: "no-pin" };
18224
19284
  }
@@ -18231,6 +19291,22 @@ function readPinnedId(root) {
18231
19291
  const id = typeof pin === "object" && pin !== null ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
18232
19292
  return typeof id === "string" && id !== "" ? { id } : { problem: "no-id" };
18233
19293
  }
19294
+ function readPinnedVersion(root) {
19295
+ let raw;
19296
+ try {
19297
+ raw = readFileSync10(resolve2(root, BUNDLE_PIN_REL), "utf-8");
19298
+ } catch {
19299
+ return void 0;
19300
+ }
19301
+ let pin;
19302
+ try {
19303
+ pin = JSON.parse(raw);
19304
+ } catch {
19305
+ return void 0;
19306
+ }
19307
+ const version2 = typeof pin === "object" && pin !== null ? pin["version"] : void 0;
19308
+ return typeof version2 === "string" && version2 !== "" ? version2 : void 0;
19309
+ }
18234
19310
  function resolveStore(root, home) {
18235
19311
  const pinned = readPinnedId(root);
18236
19312
  if ("problem" in pinned)
@@ -18255,7 +19331,7 @@ function shownUrl(url2) {
18255
19331
  return url2.replace(/[^\x21-\x7e]/g, "").slice(0, 200);
18256
19332
  }
18257
19333
  function sentences(lines) {
18258
- return lines.map((line) => `halfcycle: ${line}
19334
+ return lines.map((line2) => `halfcycle: ${line2}
18259
19335
  `).join("");
18260
19336
  }
18261
19337
  function badIdSentence(root) {
@@ -18312,7 +19388,7 @@ async function refreshWithinDeadline(source, opts, writeErr) {
18312
19388
  }
18313
19389
  async function chooseBearer(opts) {
18314
19390
  const writeErr = opts.writeErr ?? defaultWriteErr;
18315
- const home = opts.home ?? homedir5();
19391
+ const home = opts.home ?? homedir6();
18316
19392
  const root = projectRootFromBinary(opts.binaryPath);
18317
19393
  const environmentToken = opts.environmentTokenFirst ? readEnvironmentToken() : void 0;
18318
19394
  const store = resolveStore(root, home);
@@ -18376,6 +19452,7 @@ async function chooseBearer(opts) {
18376
19452
  }
18377
19453
  async function mcpHeaders(opts) {
18378
19454
  const writeErr = opts.writeErr ?? defaultWriteErr;
19455
+ const root = projectRootFromBinary(opts.binaryPath);
18379
19456
  try {
18380
19457
  return headerLine(await chooseBearer({
18381
19458
  ...opts,
@@ -18383,7 +19460,7 @@ async function mcpHeaders(opts) {
18383
19460
  requestedUrl: readMcpServerUrl(),
18384
19461
  renewal: "renew-here",
18385
19462
  environmentTokenFirst: true
18386
- }));
19463
+ }), readPinnedVersion(root));
18387
19464
  } catch (err) {
18388
19465
  try {
18389
19466
  writeErr(sentences([
@@ -18391,9 +19468,9 @@ async function mcpHeaders(opts) {
18391
19468
  `run "npx halfcycle" in this repository to rewrite this machine's credential.`
18392
19469
  ]));
18393
19470
  } catch {
18394
- return headerLine(NO_CREDENTIAL_PLACEHOLDER);
19471
+ return headerLine(NO_CREDENTIAL_PLACEHOLDER, readPinnedVersion(root));
18395
19472
  }
18396
- return headerLine(NO_CREDENTIAL_PLACEHOLDER);
19473
+ return headerLine(NO_CREDENTIAL_PLACEHOLDER, readPinnedVersion(root));
18397
19474
  }
18398
19475
  }
18399
19476
 
@@ -18441,7 +19518,7 @@ var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initializ
18441
19518
  function registeredMcpUrl(projectRoot) {
18442
19519
  let parsed;
18443
19520
  try {
18444
- parsed = JSON.parse(readFileSync10(resolve3(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
19521
+ parsed = JSON.parse(readFileSync11(resolve3(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
18445
19522
  } catch {
18446
19523
  return void 0;
18447
19524
  }
@@ -18535,16 +19612,83 @@ async function checkConnection(renewedToken, opts) {
18535
19612
  environmentTokenFirst: false
18536
19613
  });
18537
19614
  if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
18538
- return PLACEHOLDER_SENTENCES[bearer];
19615
+ return { sentence: PLACEHOLDER_SENTENCES[bearer], url: url2, bearer };
18539
19616
  }
18540
19617
  const outcome = await probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
18541
- return outcome ?? CONNECTED_SENTENCE;
19618
+ return { sentence: outcome ?? CONNECTED_SENTENCE, url: url2, bearer };
19619
+ }
19620
+ function positionRequestBody(engagementId) {
19621
+ return JSON.stringify({
19622
+ jsonrpc: "2.0",
19623
+ id: 1,
19624
+ method: "tools/call",
19625
+ params: { name: "halfcycle_position", arguments: { engagementId } }
19626
+ });
19627
+ }
19628
+ async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
19629
+ const controller = new AbortController();
19630
+ const timer = setTimeout(() => controller.abort(), deadlineMs);
19631
+ try {
19632
+ let res;
19633
+ try {
19634
+ res = await fetchFn(url2, {
19635
+ method: "POST",
19636
+ headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
19637
+ body: positionRequestBody(engagementId),
19638
+ redirect: "manual",
19639
+ signal: controller.signal
19640
+ });
19641
+ } catch {
19642
+ return void 0;
19643
+ }
19644
+ if (res.status < 200 || res.status > 299) {
19645
+ discardBody(res);
19646
+ return void 0;
19647
+ }
19648
+ let text;
19649
+ try {
19650
+ text = await res.text();
19651
+ } catch {
19652
+ return void 0;
19653
+ }
19654
+ let rpc;
19655
+ try {
19656
+ rpc = JSON.parse(text);
19657
+ } catch {
19658
+ return void 0;
19659
+ }
19660
+ const result = typeof rpc === "object" && rpc !== null ? rpc["result"] : void 0;
19661
+ if (typeof result !== "object" || result === null || result["isError"] === true) {
19662
+ return void 0;
19663
+ }
19664
+ const content = result["content"];
19665
+ const first = Array.isArray(content) ? content[0] : void 0;
19666
+ const bodyText = typeof first === "object" && first !== null ? first["text"] : void 0;
19667
+ if (typeof bodyText !== "string")
19668
+ return void 0;
19669
+ let body;
19670
+ try {
19671
+ body = JSON.parse(bodyText);
19672
+ } catch {
19673
+ return void 0;
19674
+ }
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;
19678
+ } finally {
19679
+ clearTimeout(timer);
19680
+ }
19681
+ }
19682
+ function scopeNoticeLines(scope) {
19683
+ const text = scopeNoticeText(scope, { offerTyping: scope.typedApprovalOffered ? "yes" : "off" });
19684
+ return [...text.sentBack, ...text.waiting];
18542
19685
  }
18543
19686
  function defaultWriteErr2(text) {
18544
19687
  process.stderr.write(text);
18545
19688
  }
18546
19689
  async function runRefreshCredential(opts) {
18547
19690
  const writeErr = opts.writeErr ?? defaultWriteErr2;
19691
+ const startedAt = Date.now();
18548
19692
  try {
18549
19693
  const renewalSaid = [];
18550
19694
  let renewalOpen = true;
@@ -18558,9 +19702,24 @@ async function runRefreshCredential(opts) {
18558
19702
  renewalOpen = false;
18559
19703
  const parts = [];
18560
19704
  if (CHECKED_SOURCES.has(input["source"])) {
18561
- const outcome = await checkConnection(renewedToken, { ...opts, writeErr });
18562
- if (outcome !== void 0)
18563
- parts.push(outcome);
19705
+ const connection = await checkConnection(renewedToken, { ...opts, writeErr });
19706
+ if (connection !== void 0) {
19707
+ parts.push(connection.sentence);
19708
+ if (connection.sentence === CONNECTED_SENTENCE) {
19709
+ const engagementId = readConfig().config?.guardEngagementId;
19710
+ if (engagementId !== void 0) {
19711
+ const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
19712
+ const positionDeadline = Math.min(opts.scopeNoticeDeadlineMs ?? SCOPE_NOTICE_DEADLINE_MS, remainingForMargin);
19713
+ 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));
19717
+ }
19718
+ }
19719
+ } else if (connection.sentence === CANNOT_REACH_SENTENCE || connection.sentence === SERVICE_PROBLEM_SENTENCE) {
19720
+ parts.push(SCOPE_UNCHECKED_SENTENCE);
19721
+ }
19722
+ }
18564
19723
  }
18565
19724
  if (readConfig().config === null) {
18566
19725
  const remedySaid = parts.some(namesTheSignInCommand);
@@ -18588,7 +19747,7 @@ async function runRefreshCredential(opts) {
18588
19747
  import { fileURLToPath } from "node:url";
18589
19748
 
18590
19749
  // dist/capture.js
18591
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
19750
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
18592
19751
  import { dirname as dirname5 } from "node:path";
18593
19752
  function runCapture(provider, endpoint, opts = {}) {
18594
19753
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -18656,7 +19815,7 @@ function readInputFile(fromFile) {
18656
19815
  if (!fromFile)
18657
19816
  return void 0;
18658
19817
  try {
18659
- return readFileSync11(fromFile, "utf-8");
19818
+ return readFileSync12(fromFile, "utf-8");
18660
19819
  } catch {
18661
19820
  return void 0;
18662
19821
  }
@@ -18681,8 +19840,8 @@ async function main() {
18681
19840
  return;
18682
19841
  }
18683
19842
  if (cmd === "mcp-headers") {
18684
- const line = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
18685
- process.stdout.write(line, () => process.exit(0));
19843
+ const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
19844
+ process.stdout.write(line2, () => process.exit(0));
18686
19845
  return;
18687
19846
  }
18688
19847
  if (cmd === "resolve-engagement") {
@@ -18738,8 +19897,18 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
18738
19897
  process.exit(process.exitCode ?? 0);
18739
19898
  return;
18740
19899
  }
19900
+ if (sub === "user-prompt-submit") {
19901
+ let reply = "";
19902
+ try {
19903
+ reply = await runUserPromptSubmit({ input: await readStdin2() });
19904
+ } catch {
19905
+ reply = "";
19906
+ }
19907
+ process.stdout.write(reply, () => process.exit(0));
19908
+ return;
19909
+ }
18741
19910
  process.stderr.write(`halfcycle-runner: unknown hook "${sub ?? ""}"
18742
- Valid hooks: post-tool-use, stop, subagent-stop
19911
+ Valid hooks: post-tool-use, stop, subagent-stop, user-prompt-submit
18743
19912
  `);
18744
19913
  process.exit(1);
18745
19914
  return;
@@ -18749,6 +19918,7 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
18749
19918
  halfcycle-runner hook post-tool-use
18750
19919
  halfcycle-runner hook stop
18751
19920
  halfcycle-runner hook subagent-stop
19921
+ halfcycle-runner hook user-prompt-submit
18752
19922
  halfcycle-runner refresh-credential
18753
19923
  halfcycle-runner mcp-headers
18754
19924
  halfcycle-runner resolve-engagement <cwd>