truecourse 0.7.0-next.0 → 0.7.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.mjs CHANGED
@@ -28079,7 +28079,7 @@ function parseBlockedOnCapabilities(reason) {
28079
28079
  return [];
28080
28080
  return m[1].split(",").map((s) => s.trim()).filter(Boolean);
28081
28081
  }
28082
- var GuardWrittenScenarioSchema, GuardCoverageGapKindSchema, GuardCoverageGapSchema, GuardBirthFindingSchema, GuardGenerateErrorSchema, GuardExtractionFailureSchema, GuardOrphanedSectionSchema, GuardRecipeReportSchema, GuardGenerateUsageSchema, GuardGenerateReportSchema;
28082
+ var GuardWrittenScenarioSchema, GuardCoverageGapKindSchema, GuardCoverageGapSchema, GuardBirthFindingSchema, GuardGenerateErrorSchema, GuardEntryPreflightSchema, GuardExtractionFailureSchema, GuardOrphanedSectionSchema, GuardRecipeReportSchema, GuardGenerateUsageSchema, GuardGenerateReportSchema;
28083
28083
  var init_report = __esm({
28084
28084
  "packages/shared/dist/guard/report.js"() {
28085
28085
  "use strict";
@@ -28132,6 +28132,14 @@ var init_report = __esm({
28132
28132
  anchor: external_exports.string(),
28133
28133
  message: external_exports.string()
28134
28134
  }).strict();
28135
+ GuardEntryPreflightSchema = external_exports.object({
28136
+ /** Display form of the entry argv, e.g. `node tools/cli/dist/index.js`. */
28137
+ entry: external_exports.string(),
28138
+ /** The recipe build command, for the rebuild hint. */
28139
+ buildCommand: external_exports.string(),
28140
+ /** The full, UNTRUNCATED startup output the dead entry produced. */
28141
+ stderr: external_exports.string()
28142
+ }).strict();
28135
28143
  GuardExtractionFailureSchema = external_exports.object({
28136
28144
  doc: external_exports.string(),
28137
28145
  reason: external_exports.string()
@@ -28177,7 +28185,13 @@ var init_report = __esm({
28177
28185
  */
28178
28186
  birthPassed: external_exports.number().int().nonnegative().optional(),
28179
28187
  manifestPath: external_exports.string().optional(),
28180
- usage: GuardGenerateUsageSchema.optional()
28188
+ usage: GuardGenerateUsageSchema.optional(),
28189
+ /**
28190
+ * Present ONLY when the built entry failed to start — the whole birth phase was
28191
+ * short-circuited, so every changed section stayed unsettled. Optional so older
28192
+ * reports (written before this field existed) keep parsing.
28193
+ */
28194
+ entryPreflight: GuardEntryPreflightSchema.optional()
28181
28195
  }).strict();
28182
28196
  }
28183
28197
  });
@@ -194710,6 +194724,129 @@ function listSandboxFiles(dir) {
194710
194724
  return out.sort();
194711
194725
  }
194712
194726
 
194727
+ // packages/guard-runner/dist/executor.js
194728
+ import { spawn as spawn3 } from "node:child_process";
194729
+ var DEFAULT_STEP_TIMEOUT_MS = 3e4;
194730
+ function executeStep(opts) {
194731
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
194732
+ const [command, ...args] = opts.argv;
194733
+ const start = Date.now();
194734
+ return new Promise((resolve11) => {
194735
+ const child = spawn3(command, args, {
194736
+ cwd: opts.cwd,
194737
+ env: opts.env,
194738
+ stdio: ["pipe", "pipe", "pipe"]
194739
+ });
194740
+ let stdout = "";
194741
+ let stderr = "";
194742
+ let timedOut = false;
194743
+ let settled = false;
194744
+ const timer = setTimeout(() => {
194745
+ timedOut = true;
194746
+ child.kill("SIGKILL");
194747
+ }, timeoutMs);
194748
+ const finish = (capture) => {
194749
+ if (settled)
194750
+ return;
194751
+ settled = true;
194752
+ clearTimeout(timer);
194753
+ resolve11(capture);
194754
+ };
194755
+ child.stdout.on("data", (chunk3) => {
194756
+ stdout += chunk3.toString("utf-8");
194757
+ });
194758
+ child.stderr.on("data", (chunk3) => {
194759
+ stderr += chunk3.toString("utf-8");
194760
+ });
194761
+ child.on("error", (err) => {
194762
+ finish({
194763
+ exitCode: null,
194764
+ signal: null,
194765
+ stdout,
194766
+ stderr,
194767
+ timedOut,
194768
+ spawnError: err.message,
194769
+ durationMs: Date.now() - start
194770
+ });
194771
+ });
194772
+ child.on("close", (code, signal) => {
194773
+ finish({
194774
+ exitCode: code,
194775
+ signal,
194776
+ stdout,
194777
+ stderr,
194778
+ timedOut,
194779
+ durationMs: Date.now() - start
194780
+ });
194781
+ });
194782
+ if (opts.stdin !== void 0)
194783
+ child.stdin.write(opts.stdin);
194784
+ child.stdin.end();
194785
+ });
194786
+ }
194787
+
194788
+ // packages/guard-runner/dist/preflight.js
194789
+ var ENTRY_PROBE_ARGVS = [[], ["--help"]];
194790
+ var ENTRY_PREFLIGHT_TIMEOUT_MS = 2e4;
194791
+ var defaultEntryProbeExecutor = async (fullArgv, recipeEnv, timeoutMs) => {
194792
+ const sandbox = createSandbox({ recipeEnv });
194793
+ try {
194794
+ return await executeStep({ argv: fullArgv, cwd: sandbox.cwd, env: sandbox.env, timeoutMs });
194795
+ } finally {
194796
+ sandbox.cleanup();
194797
+ }
194798
+ };
194799
+ async function preflightEntry(opts) {
194800
+ const exec = opts.exec ?? defaultEntryProbeExecutor;
194801
+ const timeoutMs = opts.timeoutMs ?? ENTRY_PREFLIGHT_TIMEOUT_MS;
194802
+ const entry = opts.displayEntry.join(" ");
194803
+ const probes = await Promise.all(ENTRY_PROBE_ARGVS.map(async (argv) => ({
194804
+ argv: [...argv],
194805
+ capture: await exec([...opts.resolvedEntry, ...argv], opts.recipeEnv, timeoutMs)
194806
+ })));
194807
+ const ok = entryStarts(probes);
194808
+ return { ok, entry, stderr: ok ? "" : startupOutput(probes[0].capture, timeoutMs), probes };
194809
+ }
194810
+ function entryStarts(probes) {
194811
+ if (probes.some((p) => startedCleanly(p.capture)))
194812
+ return true;
194813
+ const shapes = probes.map((p) => failureShape(p.capture));
194814
+ return shapes.some((s) => s !== shapes[0]);
194815
+ }
194816
+ function startedCleanly(c) {
194817
+ return !c.spawnError && !c.timedOut && c.signal === null && c.exitCode === 0;
194818
+ }
194819
+ function failureShape(c) {
194820
+ return JSON.stringify({
194821
+ spawnError: c.spawnError ?? null,
194822
+ signal: c.signal ?? null,
194823
+ timedOut: c.timedOut,
194824
+ exitCode: c.exitCode,
194825
+ stdout: c.stdout,
194826
+ stderr: c.stderr
194827
+ });
194828
+ }
194829
+ function startupOutput(c, timeoutMs) {
194830
+ const parts = [];
194831
+ const primary = c.stderr || c.stdout;
194832
+ if (primary)
194833
+ parts.push(primary.trimEnd());
194834
+ if (c.spawnError)
194835
+ parts.push(`failed to spawn: ${c.spawnError}`);
194836
+ if (c.timedOut)
194837
+ parts.push(`(the entry produced no output within ${timeoutMs}ms and was killed)`);
194838
+ if (parts.length === 0) {
194839
+ parts.push(c.exitCode !== null ? `(the entry exited ${c.exitCode} with no output)` : `(the entry was killed by ${c.signal})`);
194840
+ }
194841
+ return parts.join("\n");
194842
+ }
194843
+ function entryPreflightHeadline(entry, buildCommand) {
194844
+ return `The recipe entry \`${entry}\` failed to start \u2014 it crashes before it runs, so every scenario would fail identically. Rebuild it with \`${buildCommand}\` (its build output is likely stale or incomplete), then retry.`;
194845
+ }
194846
+ function formatEntryPreflightError(opts) {
194847
+ return [entryPreflightHeadline(opts.entry, opts.buildCommand), "", "Startup output:", opts.stderr].join("\n");
194848
+ }
194849
+
194713
194850
  // packages/guard-runner/dist/capabilities/git.js
194714
194851
  import { spawnSync } from "node:child_process";
194715
194852
  import fs15 from "node:fs";
@@ -194790,67 +194927,6 @@ function applyCapabilities(setup, ctx) {
194790
194927
  }
194791
194928
  }
194792
194929
 
194793
- // packages/guard-runner/dist/executor.js
194794
- import { spawn as spawn3 } from "node:child_process";
194795
- var DEFAULT_STEP_TIMEOUT_MS = 3e4;
194796
- function executeStep(opts) {
194797
- const timeoutMs = opts.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
194798
- const [command, ...args] = opts.argv;
194799
- const start = Date.now();
194800
- return new Promise((resolve11) => {
194801
- const child = spawn3(command, args, {
194802
- cwd: opts.cwd,
194803
- env: opts.env,
194804
- stdio: ["pipe", "pipe", "pipe"]
194805
- });
194806
- let stdout = "";
194807
- let stderr = "";
194808
- let timedOut = false;
194809
- let settled = false;
194810
- const timer = setTimeout(() => {
194811
- timedOut = true;
194812
- child.kill("SIGKILL");
194813
- }, timeoutMs);
194814
- const finish = (capture) => {
194815
- if (settled)
194816
- return;
194817
- settled = true;
194818
- clearTimeout(timer);
194819
- resolve11(capture);
194820
- };
194821
- child.stdout.on("data", (chunk3) => {
194822
- stdout += chunk3.toString("utf-8");
194823
- });
194824
- child.stderr.on("data", (chunk3) => {
194825
- stderr += chunk3.toString("utf-8");
194826
- });
194827
- child.on("error", (err) => {
194828
- finish({
194829
- exitCode: null,
194830
- signal: null,
194831
- stdout,
194832
- stderr,
194833
- timedOut,
194834
- spawnError: err.message,
194835
- durationMs: Date.now() - start
194836
- });
194837
- });
194838
- child.on("close", (code, signal) => {
194839
- finish({
194840
- exitCode: code,
194841
- signal,
194842
- stdout,
194843
- stderr,
194844
- timedOut,
194845
- durationMs: Date.now() - start
194846
- });
194847
- });
194848
- if (opts.stdin !== void 0)
194849
- child.stdin.write(opts.stdin);
194850
- child.stdin.end();
194851
- });
194852
- }
194853
-
194854
194930
  // packages/guard-runner/dist/normalizers.js
194855
194931
  var CANONICAL_ORDER = [
194856
194932
  "abs-paths",
@@ -195481,16 +195557,27 @@ async function runGuard(opts) {
195481
195557
  }));
195482
195558
  const executable = planned.filter((p) => p.resolution.kind === "match" || p.resolution.kind === "remap");
195483
195559
  const nonExecutable = planned.filter((p) => p.resolution.kind === "stale" || p.resolution.kind === "orphaned");
195484
- if (!opts.skipBuild && executable.length > 0) {
195560
+ const buildsOwnEntry = !opts.skipBuild && executable.length > 0;
195561
+ if (buildsOwnEntry) {
195485
195562
  opts.onPhase?.("build");
195486
195563
  const build8 = await runBuild(repoRoot, loaded.recipe.build, loaded.recipe.env);
195487
195564
  if (!build8.ok)
195488
195565
  return { status: "build-failed", build: build8, loadErrors };
195489
195566
  }
195567
+ const resolvedEntry = resolveEntry(repoRoot, loaded.recipe.entry);
195568
+ if (buildsOwnEntry) {
195569
+ const preflight = await preflightEntry({
195570
+ resolvedEntry,
195571
+ displayEntry: loaded.recipe.entry,
195572
+ recipeEnv: loaded.recipe.env
195573
+ });
195574
+ if (!preflight.ok) {
195575
+ return { status: "entry-preflight-failed", preflight, buildCommand: loaded.recipe.build, loadErrors };
195576
+ }
195577
+ }
195490
195578
  opts.onPhase?.("run", selected.length);
195491
195579
  const runId = buildRunId();
195492
195580
  const ranAt = (/* @__PURE__ */ new Date()).toISOString();
195493
- const resolvedEntry = resolveEntry(repoRoot, loaded.recipe.entry);
195494
195581
  const stepTimeoutMs = opts.stepTimeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
195495
195582
  const concurrency = opts.concurrency ?? defaultRunConcurrency();
195496
195583
  const results = [];
@@ -196071,8 +196158,14 @@ var CorpusDocSchema = external_exports.object({
196071
196158
  var OverlapSectionSchema = external_exports.object({
196072
196159
  /** The doc this section lives in, by ref (one of the overlap's two docs). */
196073
196160
  doc: DocRefSchema,
196074
- /** The heading text of the conflicting section (verbatim from the doc). */
196075
- heading: external_exports.string()
196161
+ /**
196162
+ * The heading text of the conflicting section (verbatim from the doc), or
196163
+ * `null` when the conflicting passage sits in the doc's PREAMBLE — the block
196164
+ * before its first heading (README badges/tagline, intro line). A plain string
196165
+ * from older corpora still parses; `null` is the preamble marker the viewer
196166
+ * bands as the pre-first-heading block.
196167
+ */
196168
+ heading: external_exports.string().nullable()
196076
196169
  });
196077
196170
  var OverlapSchema = external_exports.object({
196078
196171
  /** The two docs that overlap, by ref. */
@@ -197009,7 +197102,9 @@ NOT a disagreement (do NOT flag):
197009
197102
 
197010
197103
  Bias: when there is a PLAUSIBLE contradiction a human should check, flag it. When the docs are clearly complementary or agree, do not.
197011
197104
 
197012
- When you flag an overlap, also identify the SECTIONS that conflict: the nearest markdown heading (the \`#\`/\`##\`/\`###\` title, verbatim, WITHOUT the leading #s) above the conflicting text in EACH doc. List one entry per side; omit a side only if it genuinely has no heading.
197105
+ When you flag an overlap, also identify the SECTIONS that conflict: the nearest markdown heading (the \`#\`/\`##\`/\`###\` title, verbatim, WITHOUT the leading #s) above the conflicting text in EACH doc. List one entry per side; omit a side only when it genuinely has no conflicting passage.
197106
+
197107
+ PREAMBLE: when a doc's conflicting passage sits ABOVE its first \`#\` heading \u2014 the preamble block (a README's badges, tagline, or the intro line before any heading) \u2014 there is no heading to name. Set that side's \`heading\` to the JSON literal \`null\` (not the string "null", not ""). Use \`null\` ONLY for that pre-first-heading block; whenever the passage is under a heading, name the heading verbatim.
197013
197108
 
197014
197109
  In the NOTE, refer to each doc by its FILENAME (the basename shown in the header, e.g. \`users.md\`) \u2014 NEVER "doc A" / "doc B", which mean nothing to the reader.
197015
197110
 
@@ -197019,6 +197114,12 @@ Output ONLY a JSON object, no prose, no code fences:
197019
197114
  "note": "users.md uses auth0_id; identity.md uses auth0_sub for the same user column",
197020
197115
  "sections": [ { "side": "A", "heading": "User model" }, { "side": "B", "heading": "Identity" } ] }
197021
197116
 
197117
+ Preamble example \u2014 doc A's claim is a README badge/tagline above any heading, so its side uses null:
197118
+
197119
+ { "overlap": true,
197120
+ "note": "README.md lists C# as a supported language in the preamble; plan.md's Tech Stack omits it",
197121
+ "sections": [ { "side": "A", "heading": null }, { "side": "B", "heading": "Tech Stack" } ] }
197122
+
197022
197123
  Use { "overlap": false, "note": "", "sections": [] } when they are complementary or agree. The note is shown to the user \u2014 name the specific thing that differs.`;
197023
197124
  var OVERLAP_PREVIEW_LINES = 120;
197024
197125
  function docBody2(doc) {
@@ -197070,12 +197171,12 @@ function buildOverlapUserPrompt(areaId, a, b) {
197070
197171
  var LlmOverlapSchema = external_exports.object({
197071
197172
  overlap: external_exports.boolean(),
197072
197173
  note: external_exports.string().default(""),
197073
- sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string() })).default([])
197174
+ sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string().nullable() })).default([])
197074
197175
  });
197075
197176
  var OverlapVerdictSchema = external_exports.object({
197076
197177
  overlap: external_exports.boolean(),
197077
197178
  note: external_exports.string().default(""),
197078
- sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string() })).default([])
197179
+ sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string().nullable() })).default([])
197079
197180
  });
197080
197181
  function spawnOverlapRunner(opts = {}) {
197081
197182
  const transport = opts.transport ?? cliTransport({ bin: opts.bin });
@@ -197265,35 +197366,49 @@ function prefilterDocs(docs, manualIncludes = []) {
197265
197366
  skipped: [...reasons].map(([path73, reason]) => ({ path: path73, reason }))
197266
197367
  };
197267
197368
  }
197369
+ async function planRelevanceWork(repoRoot, docs, manualIncludes = []) {
197370
+ const manualSet = new Set(manualIncludes);
197371
+ const { toClassify, skipped } = prefilterDocs(docs, manualIncludes);
197372
+ const needsCall = [];
197373
+ const known = /* @__PURE__ */ new Map();
197374
+ await Promise.all(toClassify.map(async (doc) => {
197375
+ if (manualSet.has(doc.path)) {
197376
+ known.set(doc.path, { path: doc.path, include: true, reason: "manual include" });
197377
+ return;
197378
+ }
197379
+ const cached = await readCache4(repoRoot, computeCacheKey5(doc));
197380
+ if (cached)
197381
+ known.set(doc.path, cached);
197382
+ else
197383
+ needsCall.push(doc);
197384
+ }));
197385
+ return { toClassify, prefilterSkipped: skipped, needsCall, known };
197386
+ }
197268
197387
  async function filterByRelevance(repoRoot, docs, opts = {}) {
197269
197388
  if (opts.enabled === false || docs.length === 0) {
197270
197389
  return { included: docs, skipped: [] };
197271
197390
  }
197272
- const manualSet = new Set(opts.manualIncludes ?? []);
197273
197391
  const runner2 = opts.runner ?? spawnRelevanceRunner({ transport: opts.transport, model: opts.model, fallbackModel: opts.fallbackModel });
197274
197392
  const concurrency = opts.concurrency ?? defaultConcurrency();
197275
- const { toClassify, skipped: prefilterSkipped } = prefilterDocs(docs, opts.manualIncludes ?? []);
197276
- const prefilterReason = new Map(prefilterSkipped.map((s) => [s.path, s.reason]));
197393
+ const plan = await planRelevanceWork(repoRoot, docs, opts.manualIncludes ?? []);
197394
+ const prefilterReason = new Map(plan.prefilterSkipped.map((s) => [s.path, s.reason]));
197277
197395
  const total = docs.length;
197278
197396
  let done = 0;
197279
197397
  const markDone = () => opts.onProgress?.(++done, total);
197280
197398
  opts.onProgress?.(0, total);
197281
- for (let i = 0; i < prefilterReason.size; i++)
197399
+ const resolvedUpfront = prefilterReason.size + plan.known.size;
197400
+ for (let i = 0; i < resolvedUpfront; i++)
197282
197401
  markDone();
197283
- const verdicts = /* @__PURE__ */ new Map();
197402
+ const verdicts = new Map(plan.known);
197403
+ const pending = plan.needsCall;
197284
197404
  let cursor = 0;
197285
197405
  let active12 = 0;
197286
197406
  await new Promise((resolve11) => {
197407
+ if (pending.length === 0)
197408
+ return resolve11();
197287
197409
  const launch = () => {
197288
- while (active12 < concurrency && cursor < toClassify.length) {
197289
- const doc = toClassify[cursor++];
197290
- if (manualSet.has(doc.path)) {
197291
- verdicts.set(doc.path, { path: doc.path, include: true, reason: "manual include" });
197292
- markDone();
197293
- if (cursor >= toClassify.length && active12 === 0)
197294
- resolve11();
197295
- continue;
197296
- }
197410
+ while (active12 < concurrency && cursor < pending.length) {
197411
+ const doc = pending[cursor++];
197297
197412
  active12++;
197298
197413
  classifyOne(repoRoot, doc, runner2).then((verdict) => {
197299
197414
  verdicts.set(doc.path, verdict);
@@ -197306,14 +197421,12 @@ async function filterByRelevance(repoRoot, docs, opts = {}) {
197306
197421
  }).finally(() => {
197307
197422
  markDone();
197308
197423
  active12--;
197309
- if (cursor >= toClassify.length && active12 === 0)
197424
+ if (cursor >= pending.length && active12 === 0)
197310
197425
  resolve11();
197311
197426
  else
197312
197427
  launch();
197313
197428
  });
197314
197429
  }
197315
- if (cursor >= toClassify.length && active12 === 0)
197316
- resolve11();
197317
197430
  };
197318
197431
  launch();
197319
197432
  });
@@ -197427,7 +197540,7 @@ async function classifyOne(repoRoot, doc, runner2) {
197427
197540
  await writeCache4(repoRoot, cacheKey, verdict);
197428
197541
  return verdict;
197429
197542
  }
197430
- var RELEVANCE_SYSTEM_PROMPT = `You are a documentation relevance classifier. Classify by CONTENT, not by folder or filename: does this doc state durable, intended behavior or decisions about THE SYSTEM IN THIS REPOSITORY (its endpoints, data, auth, events, invariants, business rules, architecture)?
197543
+ var RELEVANCE_SYSTEM_PROMPT = `You are a documentation relevance classifier. Judge mainly by CONTENT \u2014 does this doc state durable, intended behavior or decisions about THE SYSTEM IN THIS REPOSITORY (its endpoints, data, auth, events, invariants, business rules, architecture)? The doc's PATH (given below) is evidence too, never an automatic verdict: weigh path and content together.
197431
197544
 
197432
197545
  ${OUTPUT_ONLY_GUARDRAIL}
197433
197546
 
@@ -197440,22 +197553,23 @@ INCLUDE (spec-source material):
197440
197553
  SKIP (not spec-source material):
197441
197554
  - Pure status / TODO checklists, kanban boards, release notes / changelog drafts
197442
197555
  - Docs about a THIRD-PARTY / external system (vendor API research, integration notes) \u2014 that is someone else's contract, not ours, and cannot be verified against this codebase
197556
+ - TEST-DATA specs: a doc under a test / fixture / sample / example tree (PATH segments like tests/, fixtures/, __fixtures__/, examples/, sample-*) that describes a FICTIONAL or sample product used as test data for THIS repo's own tooling \u2014 that product is not this repository's system, so its "spec" is not ours. (The path is evidence, not proof: if the doc plainly describes THIS repository's real product or engineering, keep it.)
197443
197557
  - SUPERSEDED docs \u2014 an older version of a newer doc covering the same subject
197444
197558
  - Process / meta docs not about product behavior: contribution / onboarding guides, code-style guides, deployment runbooks (keep a deployment doc ONLY if it states our runtime contracts)
197445
197559
  - Exploratory scratch with no committed decisions (brain dumps, open-questions-only notes)
197446
197560
  - AI-agent instructions / prompt templates; personal engineering journals
197447
197561
 
197448
- Distinguish "states a decision about our system" (INCLUDE) from "tracks status / describes an external system / is superseded / is process" (SKIP). The SKIP categories above are explicit \u2014 they are not "doubt." WHEN GENUINELY AMBIGUOUS: include (dropping a real spec doc costs more than keeping noise).
197562
+ Distinguish "states a decision about our system" (INCLUDE) from "tracks status / describes an external or test-data product / is superseded / is process" (SKIP). The SKIP categories above are explicit \u2014 they are not "doubt." WHEN GENUINELY AMBIGUOUS: include (dropping a real spec doc costs more than keeping noise).
197449
197563
 
197450
197564
  Output ONLY a JSON object:
197451
197565
 
197452
197566
  { "include": true|false, "reason": "short explanation" }
197453
197567
 
197454
- The reason is shown to the user in the dashboard \u2014 be specific ("vendor API research (ServiceTitan)", "superseded by capacity-ml-plan-v3", "deployment runbook, no product contracts") so they can verify the call.`;
197568
+ The reason is shown to the user in the dashboard \u2014 be specific ("vendor API research (ServiceTitan)", "superseded by capacity-ml-plan-v3", "deployment runbook, no product contracts", "test-data spec for a sample product (tests/fixtures/\u2026)") so they can verify the call.`;
197455
197569
  function buildRelevanceUserPrompt(doc) {
197456
197570
  const preview = doc.preview.split("\n").slice(0, 60).join("\n");
197457
197571
  return [
197458
- `Path: ${doc.path}`,
197572
+ `PATH (repo-relative): ${doc.path}`,
197459
197573
  `Detected kind: ${doc.kind}`,
197460
197574
  `Size: ${doc.size} bytes`,
197461
197575
  "",
@@ -197509,9 +197623,6 @@ async function readCache4(scope, cacheKey) {
197509
197623
  async function writeCache4(scope, cacheKey, verdict) {
197510
197624
  await setCacheEntry(scope, CACHE_NAME5, cacheKey, verdict);
197511
197625
  }
197512
- async function readRelevanceCache(repoRoot, doc) {
197513
- return readCache4(repoRoot, computeCacheKey5(doc));
197514
- }
197515
197626
 
197516
197627
  // packages/spec-consolidator/dist/curate.js
197517
197628
  async function curate(repoRoot, opts = {}) {
@@ -224308,6 +224419,9 @@ var STAGE_DEFAULTS = {
224308
224419
  // Authoring an executable scenario faithful to a spec claim is the hard,
224309
224420
  // load-bearing call (a weak scenario is false confidence) — opus.
224310
224421
  "guard.generate": "opus",
224422
+ // The one evidence-retry per birth-failed claim — the same authoring task, so
224423
+ // the same tier; a distinct stage so retry spend is attributed to the birth line.
224424
+ "guard.retry": "opus",
224311
224425
  // Proposing a build/entry recipe is a modest structured task — sonnet.
224312
224426
  "guard.recipe": "sonnet",
224313
224427
  "rules.violationGen": "opus"
@@ -225009,11 +225123,11 @@ function spawnGenerateRunner2(opts = {}) {
225009
225123
  return async (ctx) => {
225010
225124
  const refs = ctx.claims.map((c) => c.ref).join(",");
225011
225125
  const isRetry = ctx.claims.some((c) => c.retry);
225012
- const suffix = `${isRetry ? ":retry" : ""}${ctx.correction ? ":correction" : ""}`;
225126
+ const stage = isRetry ? "guard.retry" : "guard.generate";
225013
225127
  const raw = await transport({
225014
- id: `guard.generate:${ctx.doc}:${refs}${suffix}`,
225015
- stage: "guard.generate",
225016
- model: opts.model,
225128
+ id: `${stage}:${ctx.doc}:${refs}${ctx.correction ? ":correction" : ""}`,
225129
+ stage,
225130
+ model: isRetry ? opts.retryModel ?? opts.model : opts.model,
225017
225131
  fallbackModel: opts.fallbackModel,
225018
225132
  system: GENERATE_SYSTEM_PROMPT,
225019
225133
  user: buildAuthorUserPrompt(ctx),
@@ -225325,12 +225439,15 @@ async function captureProbes(opts) {
225325
225439
  const cached = await getCacheEntry(opts.repoRoot, GROUND_CACHE_NAME, key4);
225326
225440
  if (cached) {
225327
225441
  const parsed = ProbeTranscriptSchema.safeParse(cached);
225328
- if (parsed.success)
225442
+ if (parsed.success) {
225443
+ opts.onProbeCaptured?.();
225329
225444
  return parsed.data;
225445
+ }
225330
225446
  }
225331
225447
  const capture = await exec([...opts.resolvedEntry, ...argv], opts.recipeEnv);
225332
225448
  const transcript = toTranscript(argv, [...opts.displayEntry, ...argv], capture);
225333
225449
  await setCacheEntry(opts.repoRoot, GROUND_CACHE_NAME, key4, transcript);
225450
+ opts.onProbeCaptured?.();
225334
225451
  return transcript;
225335
225452
  }));
225336
225453
  }
@@ -225595,6 +225712,7 @@ function deleteScenarioFiles(repoRoot, ids) {
225595
225712
 
225596
225713
  // packages/guard-generator/dist/generate.js
225597
225714
  var GENERATE_CACHE_NAME = "guard/generate";
225715
+ var ENTRY_PREFLIGHT_ANCHOR = "(entry preflight)";
225598
225716
  function defaultConcurrency3() {
225599
225717
  const env = process.env.TRUECOURSE_MAX_CONCURRENCY;
225600
225718
  if (env) {
@@ -225664,7 +225782,12 @@ async function generateGuards(options) {
225664
225782
  const limit = pLimit(Math.max(1, options.concurrency ?? defaultConcurrency3()));
225665
225783
  const batchSize = Math.max(1, options.batchSize ?? defaultGenerateBatch2());
225666
225784
  const extractRunner = options.extractRunner ?? spawnExtractRunner({ transport: options.transport, model: options.models?.extract, fallbackModel: options.models?.fallback });
225667
- const generateRunner = options.generateRunner ?? spawnGenerateRunner2({ transport: options.transport, model: options.models?.generate, fallbackModel: options.models?.fallback });
225785
+ const generateRunner = options.generateRunner ?? spawnGenerateRunner2({
225786
+ transport: options.transport,
225787
+ model: options.models?.generate,
225788
+ retryModel: options.models?.retry,
225789
+ fallbackModel: options.models?.fallback
225790
+ });
225668
225791
  const coverageGaps = [];
225669
225792
  const errors = [];
225670
225793
  const extractionFailures = [];
@@ -225735,11 +225858,15 @@ async function generateGuards(options) {
225735
225858
  return buildPromise;
225736
225859
  };
225737
225860
  let resolvedEntryMemo = null;
225861
+ let groundPlanned = 0;
225862
+ let groundCaptured = 0;
225738
225863
  const groundClaims = async (claimTexts) => {
225739
225864
  const probes = deriveProbes(claimTexts, recipe.entry);
225740
225865
  const build8 = await buildPromise;
225741
- if (!build8.ok)
225866
+ if (!build8.ok || probes.length === 0)
225742
225867
  return [];
225868
+ groundPlanned += probes.length;
225869
+ options.onGroundProgress?.(groundCaptured, groundPlanned);
225743
225870
  resolvedEntryMemo ??= resolveEntry(repoRoot, recipe.entry);
225744
225871
  return captureProbes({
225745
225872
  repoRoot,
@@ -225747,9 +225874,21 @@ async function generateGuards(options) {
225747
225874
  resolvedEntry: resolvedEntryMemo,
225748
225875
  displayEntry: recipe.entry,
225749
225876
  recipeFingerprint,
225750
- recipeEnv: recipe.env
225877
+ recipeEnv: recipe.env,
225878
+ onProbeCaptured: () => options.onGroundProgress?.(++groundCaptured, groundPlanned)
225751
225879
  });
225752
225880
  };
225881
+ let entryPreflightMemo = null;
225882
+ const preflightEntryOnce = () => {
225883
+ entryPreflightMemo ??= (async () => {
225884
+ const build8 = await buildPromise;
225885
+ if (!build8.ok)
225886
+ return null;
225887
+ resolvedEntryMemo ??= resolveEntry(repoRoot, recipe.entry);
225888
+ return preflightEntry({ resolvedEntry: resolvedEntryMemo, displayEntry: recipe.entry, recipeEnv: recipe.env });
225889
+ })();
225890
+ return entryPreflightMemo;
225891
+ };
225753
225892
  const priorSections = readPriorManifest(repoRoot);
225754
225893
  const manifestByKey = new Map(priorSections.map((e) => [`${e.doc}\0${e.anchor}`, e]));
225755
225894
  const usedIds = existingScenarioIds(repoRoot);
@@ -225773,9 +225912,11 @@ async function generateGuards(options) {
225773
225912
  });
225774
225913
  settledKeys.add(k);
225775
225914
  writeWorkingManifest();
225915
+ options.onSectionSettled?.(settledKeys.size, plan.work.length);
225776
225916
  };
225777
225917
  const written = [];
225778
225918
  const birthFindings = [];
225919
+ let entryPreflightFailure = null;
225779
225920
  let birthTotal = 0;
225780
225921
  let birthSettled = 0;
225781
225922
  let birthPassed = 0;
@@ -225822,6 +225963,20 @@ async function generateGuards(options) {
225822
225963
  return;
225823
225964
  settleChain = settleChain.then(() => settleCliSection(sectionByKey.get(k), refsBySection.get(k)));
225824
225965
  };
225966
+ const deadEntry = async () => {
225967
+ const preflight = await preflightEntryOnce();
225968
+ if (!preflight || preflight.ok)
225969
+ return false;
225970
+ if (!entryPreflightFailure) {
225971
+ entryPreflightFailure = { entry: preflight.entry, buildCommand: recipe.build, stderr: preflight.stderr };
225972
+ errors.push({
225973
+ doc: preflight.entry,
225974
+ anchor: ENTRY_PREFLIGHT_ANCHOR,
225975
+ message: formatEntryPreflightError(entryPreflightFailure)
225976
+ });
225977
+ }
225978
+ return true;
225979
+ };
225825
225980
  async function settleCliSection(section, refs) {
225826
225981
  const k = key2(section);
225827
225982
  for (const id of priorIdsOf(k))
@@ -225856,6 +226011,8 @@ async function generateGuards(options) {
225856
226011
  const message = `build failed (\`${build8.command}\`)${build8.timedOut ? " \u2014 timed out" : ""}`;
225857
226012
  for (const c of round1)
225858
226013
  localErrors.push(errorFrom({ candidate: c, result: { failure: { actual: message } } }));
226014
+ } else if (await deadEntry()) {
226015
+ return;
225859
226016
  } else {
225860
226017
  birthTotal += round1.length;
225861
226018
  const r1 = await birthValidate(repoRoot, round1, { skipBuild: true, onPhase: options.onBirthPhase, onScenarioSettled: bumpBirth });
@@ -225932,6 +226089,8 @@ async function generateGuards(options) {
225932
226089
  const authorTotal = authTasks.length;
225933
226090
  let authorDone = 0;
225934
226091
  const bumpAuthor = (n) => options.onAuthorProgress?.(authorDone += n, authorTotal);
226092
+ if (authorTotal > 0)
226093
+ options.onAuthorProgress?.(authorDone, authorTotal);
225935
226094
  const missTasks = [];
225936
226095
  for (const t of authTasks) {
225937
226096
  const cached = await readAuthorCache(repoRoot, t.claim, t.section, recipeFingerprint);
@@ -226003,7 +226162,8 @@ async function generateGuards(options) {
226003
226162
  extractionFailures,
226004
226163
  orphaned,
226005
226164
  birthPassed,
226006
- manifestPath: manifestPath(repoRoot)
226165
+ manifestPath: manifestPath(repoRoot),
226166
+ ...entryPreflightFailure ? { entryPreflight: entryPreflightFailure } : {}
226007
226167
  };
226008
226168
  }
226009
226169
  var key2 = (s) => `${s.doc}\0${s.anchor}`;
@@ -226814,12 +226974,14 @@ function previewChars() {
226814
226974
  return 60 * 50;
226815
226975
  }
226816
226976
  async function estimateScanTokens(repoRoot, prices) {
226977
+ const decisions = readCorpusDecisions(repoRoot);
226978
+ const manualIncludes = decisions.manualIncludes ?? [];
226979
+ const manualExcludes = new Set(decisions.manualExcludes ?? []);
226817
226980
  const docs = discoverDocs(repoRoot);
226818
- const { toClassify } = prefilterDocs(docs);
226819
- const nClassify = toClassify.length;
226820
- const relevance = await Promise.all(toClassify.map((d) => readRelevanceCache(repoRoot, d)));
226821
- const relevanceMissDocs = toClassify.filter((_, i) => relevance[i] === null);
226822
- const cachedKeptDocs = toClassify.filter((_, i) => relevance[i]?.include === true);
226981
+ const plan = await planRelevanceWork(repoRoot, docs, manualIncludes);
226982
+ const nClassify = plan.toClassify.length;
226983
+ const relevanceMissDocs = plan.needsCall;
226984
+ const cachedKeptDocs = plan.toClassify.filter((d) => plan.known.get(d.path)?.include === true && !manualExcludes.has(d.path));
226823
226985
  const cachedKeptTagged = await Promise.all(cachedKeptDocs.map((d) => isAreaTagCached(repoRoot, d)));
226824
226986
  const cachedKeptTagMisses = cachedKeptTagged.filter((cached) => !cached).length;
226825
226987
  const estChangedKept = Math.round(relevanceMissDocs.length * KEEP_RATE);
@@ -231237,7 +231399,7 @@ function readToolVersion() {
231237
231399
  if (cachedVersion)
231238
231400
  return cachedVersion;
231239
231401
  if (true) {
231240
- cachedVersion = "0.7.0-next.0";
231402
+ cachedVersion = "0.7.0-next.2";
231241
231403
  return cachedVersion;
231242
231404
  }
231243
231405
  try {
@@ -237598,32 +237760,20 @@ router10.delete(
237598
237760
  }
237599
237761
  }
237600
237762
  );
237601
- async function mutateAndRecurate(repoPath, repoId, mutate) {
237602
- await mutate();
237603
- const tracker = createSocketSpecTracker(repoId, CURATE_STEPS.map((s) => ({ ...s })));
237604
- try {
237605
- await curateInProcess(repoPath, { tracker, source: "dashboard" });
237606
- emitSpecComplete(repoId, "scan");
237607
- } catch (e) {
237608
- emitSpecProgress(repoId, { step: "error", percent: 100, detail: e.message });
237609
- throw e;
237610
- }
237611
- return corpusPayload(repoPath);
237612
- }
237613
- async function mutateSpecDecision(repoPath, repoId, res, mutate) {
237763
+ async function mutateSpecDecision(repoPath, res, mutate) {
237614
237764
  if (!contractsMaterializeInPlace()) {
237615
237765
  await mutate();
237616
237766
  await recurateAndRegenIfResolved(repoPath);
237617
237767
  res.json(await corpusPayload(repoPath));
237618
237768
  return;
237619
237769
  }
237620
- if (!await isGitRepo(repoPath)) {
237621
- res.status(400).json({ error: NOT_A_GIT_REPO_MESSAGE });
237622
- return;
237623
- }
237624
- res.json(await mutateAndRecurate(repoPath, repoId, mutate));
237770
+ const decisions = await mutate();
237771
+ res.json({
237772
+ manualIncludes: decisions.manualIncludes ?? [],
237773
+ manualExcludes: decisions.manualExcludes ?? []
237774
+ });
237625
237775
  }
237626
- async function applySpecMutation(req, res, repoPath, repoId, mutate) {
237776
+ async function applySpecMutation(req, res, repoPath, mutate) {
237627
237777
  const parsed = parsePrScope(req);
237628
237778
  if ("error" in parsed) {
237629
237779
  res.status(400).json({ error: parsed.error });
@@ -237633,7 +237783,7 @@ async function applySpecMutation(req, res, repoPath, repoId, mutate) {
237633
237783
  await mutateSpecDecisionPr(repoPath, parsed.scope, res, mutate);
237634
237784
  return;
237635
237785
  }
237636
- await mutateSpecDecision(repoPath, repoId, res, () => mutate());
237786
+ await mutateSpecDecision(repoPath, res, () => mutate());
237637
237787
  }
237638
237788
  router10.post(
237639
237789
  "/:id/spec/includes",
@@ -237650,7 +237800,6 @@ router10.post(
237650
237800
  req,
237651
237801
  res,
237652
237802
  repo.path,
237653
- req.params.id,
237654
237803
  (opts) => addManualInclude(repo.path, ref, opts)
237655
237804
  );
237656
237805
  } catch (e) {
@@ -237673,7 +237822,6 @@ router10.delete(
237673
237822
  req,
237674
237823
  res,
237675
237824
  repo.path,
237676
- req.params.id,
237677
237825
  (opts) => removeManualInclude(repo.path, ref, opts)
237678
237826
  );
237679
237827
  } catch (e) {
@@ -237696,7 +237844,6 @@ router10.post(
237696
237844
  req,
237697
237845
  res,
237698
237846
  repo.path,
237699
- req.params.id,
237700
237847
  (opts) => addManualExclude(repo.path, ref, opts)
237701
237848
  );
237702
237849
  } catch (e) {
@@ -237719,7 +237866,6 @@ router10.delete(
237719
237866
  req,
237720
237867
  res,
237721
237868
  repo.path,
237722
- req.params.id,
237723
237869
  (opts) => removeManualExclude(repo.path, ref, opts)
237724
237870
  );
237725
237871
  } catch (e) {
@@ -237741,6 +237887,8 @@ router10.get(
237741
237887
  res.json({
237742
237888
  contractsStale: false,
237743
237889
  verifyStale: false,
237890
+ // EE re-curates on every decision, so decisions never outrun the corpus.
237891
+ decisionsPending: false,
237744
237892
  hasCorpus: corpus !== null,
237745
237893
  hasGenerated: contractFiles.length > 0,
237746
237894
  hasVerified: verify2 !== null
@@ -237755,6 +237903,7 @@ router10.get(
237755
237903
  res.json({
237756
237904
  contractsStale,
237757
237905
  verifyStale,
237906
+ decisionsPending: hasPendingDecisions(repo.path),
237758
237907
  hasCorpus: corpusMtime !== null,
237759
237908
  hasGenerated: generatedMtime !== null,
237760
237909
  hasVerified: verifiedMtime !== null
@@ -237771,6 +237920,18 @@ function mtimeIfExists(file) {
237771
237920
  return null;
237772
237921
  }
237773
237922
  }
237923
+ function hasPendingDecisions(repoPath) {
237924
+ const decisionsMtime = mtimeIfExists(decisionsPath(repoPath));
237925
+ if (decisionsMtime === null) return false;
237926
+ try {
237927
+ const corpus = JSON.parse(fs62.readFileSync(corpusFilePath(repoPath), "utf8"));
237928
+ const generatedAt = Date.parse(corpus.generatedAt ?? "");
237929
+ if (Number.isNaN(generatedAt)) return false;
237930
+ return decisionsMtime > generatedAt;
237931
+ } catch {
237932
+ return false;
237933
+ }
237934
+ }
237774
237935
  var spec_default = router10;
237775
237936
 
237776
237937
  // apps/dashboard/server/src/routes/contracts.ts
@@ -238862,7 +239023,8 @@ var GUARD_GENERATE_STEPS = [
238862
239023
  var GUARD_STEP_STAGES = {
238863
239024
  index: ["guard.recipe"],
238864
239025
  extract: ["guard.extract"],
238865
- author: ["guard.generate"]
239026
+ author: ["guard.generate"],
239027
+ validate: ["guard.retry"]
238866
239028
  };
238867
239029
  async function estimateGuard(repoRoot) {
238868
239030
  return estimateGuardTokens(repoRoot, await getModelPrices());
@@ -238871,6 +239033,7 @@ function resolveGuardModels(repoRoot) {
238871
239033
  return {
238872
239034
  extract: resolveModel("guard.extract", void 0, repoRoot),
238873
239035
  generate: resolveModel("guard.generate", void 0, repoRoot),
239036
+ retry: resolveModel("guard.retry", void 0, repoRoot),
238874
239037
  recipe: resolveModel("guard.recipe", void 0, repoRoot),
238875
239038
  fallback: resolveFallbackModel(repoRoot) ?? void 0
238876
239039
  };
@@ -238912,10 +239075,20 @@ async function guardGenerateInProcess(repoRoot, options = {}) {
238912
239075
  cur = ni;
238913
239076
  };
238914
239077
  const withUsage = (key4, base) => `${base}${stageUsageTag(GUARD_STEP_STAGES[key4] ?? [], repoRoot)}`;
239078
+ let authorDone = 0;
239079
+ let authorTotal = 0;
239080
+ let authorFinished = false;
239081
+ let groundCaptured = 0;
239082
+ let groundPlanned = 0;
239083
+ const authorDetail = () => {
239084
+ const claims = `${authorDone}/${authorTotal} claim${authorTotal === 1 ? "" : "s"}`;
239085
+ const base = groundPlanned > 0 ? `grounding probes ${groundCaptured}/${groundPlanned} \xB7 authoring ${claims}` : claims;
239086
+ return withUsage("author", base);
239087
+ };
238915
239088
  let building = false;
238916
- let birthSeen = false;
238917
239089
  let birthDone = 0;
238918
- let birthTotal = 0;
239090
+ let sectionsDone = 0;
239091
+ let sectionsTotal = 0;
238919
239092
  let retrySeen = false;
238920
239093
  let retryDone = 0;
238921
239094
  let retryTotal = 0;
@@ -238926,10 +239099,10 @@ async function guardGenerateInProcess(repoRoot, options = {}) {
238926
239099
  tracker?.start("validate");
238927
239100
  validateStarted = true;
238928
239101
  }
238929
- const parts = [building ? "building\u2026" : `birth ${birthDone}/${birthTotal}`];
239102
+ const parts = [`sections ${sectionsDone}/${sectionsTotal}`, building ? "building\u2026" : `birth ${birthDone}`];
238930
239103
  if (retrySeen)
238931
- parts.push(`retrying failed claims ${retryDone}/${retryTotal}`);
238932
- tracker?.detail("validate", parts.join(" \xB7 "));
239104
+ parts.push(`retrying ${retryDone}/${retryTotal}`);
239105
+ tracker?.detail("validate", withUsage("validate", parts.join(" \xB7 ")));
238933
239106
  };
238934
239107
  tracker?.start("index");
238935
239108
  try {
@@ -238941,6 +239114,7 @@ async function guardGenerateInProcess(repoRoot, options = {}) {
238941
239114
  generateRunner: options.generateRunner,
238942
239115
  recipeRunner: options.recipeRunner,
238943
239116
  onPlan: (total, work) => {
239117
+ sectionsTotal = work;
238944
239118
  tracker?.done("index", withUsage("index", `${work} of ${total} section${total === 1 ? "" : "s"} changed`));
238945
239119
  cur = STEPS.indexOf("extract");
238946
239120
  tracker?.start("extract", `0 views`);
@@ -238957,27 +239131,30 @@ async function guardGenerateInProcess(repoRoot, options = {}) {
238957
239131
  },
238958
239132
  onAuthorProgress: (done, total) => {
238959
239133
  advanceTo("author");
238960
- const detail = withUsage("author", `${done}/${total} claim${total === 1 ? "" : "s"}`);
238961
- if (done >= total)
238962
- tracker?.done("author", detail);
238963
- else
238964
- tracker?.detail("author", detail);
238965
- },
238966
- onBirthPhase: (phase, total) => {
238967
- if (phase === "build") {
238968
- building = true;
239134
+ authorDone = done;
239135
+ authorTotal = total;
239136
+ if (done >= total) {
239137
+ authorFinished = true;
239138
+ tracker?.done("author", withUsage("author", `${done}/${total} claim${total === 1 ? "" : "s"}`));
238969
239139
  } else {
238970
- building = false;
238971
- if (!birthSeen && total !== void 0)
238972
- birthTotal = total;
239140
+ tracker?.detail("author", authorDetail());
238973
239141
  }
239142
+ },
239143
+ onGroundProgress: (captured, planned) => {
239144
+ groundCaptured = captured;
239145
+ groundPlanned = planned;
239146
+ if (authorFinished)
239147
+ return;
239148
+ advanceTo("author");
239149
+ tracker?.detail("author", authorDetail());
239150
+ },
239151
+ onBirthPhase: (phase) => {
239152
+ building = phase === "build";
238974
239153
  renderValidate();
238975
239154
  },
238976
- onBirthProgress: (done, total) => {
239155
+ onBirthProgress: (done) => {
238977
239156
  building = false;
238978
- birthSeen = true;
238979
239157
  birthDone = done;
238980
- birthTotal = total;
238981
239158
  renderValidate();
238982
239159
  },
238983
239160
  onRetryProgress: (done, total) => {
@@ -238985,6 +239162,12 @@ async function guardGenerateInProcess(repoRoot, options = {}) {
238985
239162
  retryDone = done;
238986
239163
  retryTotal = total;
238987
239164
  renderValidate();
239165
+ },
239166
+ onSectionSettled: (settled, total) => {
239167
+ sectionsDone = settled;
239168
+ sectionsTotal = total;
239169
+ if (validateStarted)
239170
+ renderValidate();
238988
239171
  }
238989
239172
  });
238990
239173
  for (let i = cur; i < STEPS.length; i++)
@@ -239008,7 +239191,7 @@ async function guardGenerateInProcess(repoRoot, options = {}) {
239008
239191
  }
239009
239192
  }
239010
239193
  }
239011
- var GUARD_USAGE_STAGES = ["guard.recipe", "guard.extract", "guard.generate"];
239194
+ var GUARD_USAGE_STAGES = ["guard.recipe", "guard.extract", "guard.generate", "guard.retry"];
239012
239195
  function sumGuardUsage() {
239013
239196
  const usage = getStageUsage();
239014
239197
  let calls = 0;
@@ -239056,6 +239239,8 @@ async function guardRunInProcess(repoRoot, options = {}) {
239056
239239
  tracker?.done("run", `${n} scenario${n === 1 ? "" : "s"}`);
239057
239240
  } else if (result.status === "build-failed") {
239058
239241
  tracker?.error("build", `Build failed (\`${result.build.command}\`)${result.build.timedOut ? " \u2014 timed out" : ""}`);
239242
+ } else if (result.status === "entry-preflight-failed") {
239243
+ tracker?.error("build", `Entry failed to start: \`${result.preflight.entry}\` (rebuild via \`${result.buildCommand}\`)`);
239059
239244
  }
239060
239245
  return result;
239061
239246
  }
@@ -239136,7 +239321,9 @@ router14.post("/:id/guard/run", async (req, res, next) => {
239136
239321
  res.json({ status: "ok", summary: result.latest.summary });
239137
239322
  return;
239138
239323
  }
239139
- if (result.status !== "build-failed") emitSpecComplete(repoId, "guard-run");
239324
+ if (result.status !== "build-failed" && result.status !== "entry-preflight-failed") {
239325
+ emitSpecComplete(repoId, "guard-run");
239326
+ }
239140
239327
  res.json({ status: result.status, message: runFailureMessage(result) });
239141
239328
  } catch (e) {
239142
239329
  emitSpecProgress(repoId, { step: "error", percent: 100, detail: e.message });
@@ -239155,6 +239342,12 @@ function runFailureMessage(result) {
239155
239342
  return "No scenarios found under .truecourse/scenarios/. Generate scenarios first.";
239156
239343
  case "build-failed":
239157
239344
  return `Build failed (\`${result.build.command}\`)${result.build.timedOut ? " \u2014 timed out" : ""}. No scenarios ran.`;
239345
+ case "entry-preflight-failed":
239346
+ return formatEntryPreflightError({
239347
+ entry: result.preflight.entry,
239348
+ buildCommand: result.buildCommand,
239349
+ stderr: result.preflight.stderr
239350
+ });
239158
239351
  }
239159
239352
  }
239160
239353
  var guard_actions_default = router14;