tldr-experts 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +635 -0
  2. package/README.md +2 -0
  3. package/dist/hooks/answer-capture.js +8 -9
  4. package/dist/hooks/budget-gate.js +47 -11
  5. package/dist/hooks/{chunk-hws2gnxj.js → chunk-0d4csx4s.js} +1 -1
  6. package/dist/hooks/{chunk-abswp1vt.js → chunk-77x6v0gg.js} +4 -2
  7. package/dist/hooks/{chunk-zrwq6r7k.js → chunk-7xascvee.js} +1 -1
  8. package/dist/hooks/{chunk-zvxgdssb.js → chunk-857jhjmb.js} +1 -1
  9. package/dist/hooks/{chunk-c62y35wh.js → chunk-fbmj1bk6.js} +3 -3
  10. package/dist/hooks/{chunk-veffvzw5.js → chunk-g4d2v4jc.js} +101 -10
  11. package/dist/hooks/{chunk-48ctk154.js → chunk-gr83pq7x.js} +3 -3
  12. package/dist/hooks/{chunk-q3d4xdhb.js → chunk-mg3aj0sa.js} +1 -1
  13. package/dist/hooks/{chunk-tna1bz8r.js → chunk-pne7afnf.js} +1 -1
  14. package/dist/hooks/{chunk-fjeefnqr.js → chunk-przta0a0.js} +44 -8
  15. package/dist/hooks/{chunk-f6t1xc3f.js → chunk-qmp5p77c.js} +2 -2
  16. package/dist/hooks/{chunk-khy21x6e.js → chunk-yanmz0vz.js} +1 -1
  17. package/dist/hooks/{chunk-khjm356r.js → chunk-z9hv47ps.js} +1 -1
  18. package/dist/hooks/{chunk-e618k6te.js → chunk-zpch8e2j.js} +193 -5
  19. package/dist/hooks/claim-sources.js +5 -5
  20. package/dist/hooks/dod-gate.js +9 -10
  21. package/dist/hooks/no-reask.js +8 -8
  22. package/dist/hooks/session-start.js +16 -13
  23. package/dist/hooks/statusline.js +8 -9
  24. package/dist/tldrx.js +2597 -990
  25. package/package.json +1 -1
  26. package/plugin/.claude-plugin/plugin.json +1 -1
  27. package/templates/experts/stack/dotnet.md +4 -3
  28. package/templates/experts/stack/javascript.md +4 -3
  29. package/templates/experts/stack/overlays/postgres-testcontainers.md +7 -2
  30. package/templates/experts/stack/python.md +4 -3
  31. package/templates/experts/stack/typescript.md +4 -3
  32. package/dist/hooks/chunk-g7wz339n.js +0 -102
package/README.md CHANGED
@@ -335,6 +335,8 @@ back on the registry is 0.3.0.
335
335
 
336
336
  | Version | Date | Status | Contains |
337
337
  |---|---|---|---|
338
+ | 0.30.0 | 2026-09-15 | `beta` | Seventeen changes that keep an unattended run moving instead of stranding. A cached Build refusal says so and a relaunch re-measures it (#339); a rate-limit warning is read instead of dropped, so a Build parks before the wall (#298); the plan-over-stage advisory speaks only past 2× (#302); a stage death stops recommending a retry the budget refuses (#232); a DoD command is measured twice, a fix-list finding is checked against the epic tip, and an over-long sha is refused by name (#163); a formatting slip gets one bounded repair turn (#288); five Checks stop asking the reviewer for what it can't do (#195); a Codex review no longer dies before it runs (#148); `tldrx init` probes four commands at once (#180); `tldrx ship` refuses a merged diff nobody judged (#311). Docs: the zero-touch recipe pairs `run auto` with `--wait-gates`/`--wait-answers` (#342). |
339
+ | 0.29.0 | 2026-09-15 | `beta` | Sixteen changes that make an unattended run trust its own gates and its own numbers. Verdict integrity: a reviewer's `changes` must cite evidence (#326), a re-reviewed story's `changes` is consumed instead of parking it (#327), and a story blocked on a stale fix list settles `done` once resolved (#329). Fix-list flow: a headless fix list now buys its own fix round, and an approving reviewer's signature closes what it was shown (#327). Money and phase sizing: `run auto` rebalances a blocked phase's shortfall out of finished phases by default (#330), the `budget-gate` hook stops denying the launch that would fix the shortfall (#321), a Build gate no longer stops on paths outside the declared surface but carries them into the PR instead (#331), a parallel wave no longer hands two developers the same unspent money (#325), `budget show`/`run estimate`/the budget-gate hook price remaining work off the stage's own `attempts:` (#214), `run auto --until-done` states a token refusal in tokens rather than invented dollar words (#270), a Watch turn is recorded as unmetered rather than a false `$0.00` (#224), and a Watch spawn now records the ceiling it was given (#190). Dependency and safety: a Build gate names the story a dependent waits on (#303), the conflict-turn marker guard names unreadable paths, bounds its scan, and covers modified files too (#324), a developer's array-valued `notes` is joined instead of silently dropped (#217), and the Plan prompt states its per-item character cap where each field is described so an over-cap finding survives into the retry (#328). |
338
340
  | 0.28.1 | 2026-09-15 | `beta` | Two fixes so a reopened story's reason reaches the agents that need it. The developer and reviewer prompts now carry `## Why this story was reopened` — who signed it, whether it's a fix round, the note verbatim — off the same ledger value #308 reads, instead of the note reaching only a report line and #308's check (closes #322). `tldrx note --help` now says an operator note reaches no agent and names `dispatch-notes.md` as the channel that does (closes #151). |
339
341
  | 0.28.0 | 2026-09-15 | `beta` | Four changes that keep an unattended run moving instead of stranding on a person. A story whose merge-up to its epic conflicts in at most three of its own touched files gets one automated conflict turn instead of a person, guarded against committing conflict markers (#286). `story reopen` on a blocked story now releases every dependent it alone was holding, in the same command (#312). Seed's `Recommended:` line and the loop's question parser now share one grammar, so a citation-only recommendation no longer parks a run that `seed check` had already passed (#323). And an `absent:` needle over `facts.yml` no longer trips on a fact's own recording metadata, while an `auto` gate refused only by failed checks gets one automatic re-run before waiting for a person (#231). |
340
342
  | 0.27.0 | 2026-09-14 | `beta` | Five changes for unattended runs, from a planning audit and live field measurement. The `plan` check now refuses a plan with more waves than the framework carries per run (unless `waves.yml` names a `wave_cap_reason`), refuses a story scheduled before its `depends_on` allows, and flags a dod command siblings carry that one story doesn't (#316–#319). `tldrx run auto --rebalance-finished` moves a blocked phase's exact shortfall out of a finished phase's unspent ceiling before refusing, through `budget raise --take-from` itself, so the run ceiling never grows and no grant is ever assumed; every money refusal now names the unspent total and the exact command to fix it (#314). `tldrx ship` fetches and merges the base into the epic and re-runs the `done` stories' DoD before opening a PR, so a stale or red epic refuses instead of shipping a PR that comes back red (#315). A reviewer's budget floor rises from $1.00 to $2.00, matching what completed reviews actually cost, so a review no longer dies mid-diff on a floor that was never measured (#307). And a developer whose DoD goes red gets the story's next attempt instead of blocking on the first miss, with the kept output handed to the next attempt and the bound counted from `events.jsonl` so it holds across process restarts; a refused developer, a cap death, or a broken dod command still blocks immediately (#313). |
@@ -1,24 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-c62y35wh.js";
4
+ } from "./chunk-fbmj1bk6.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-48ctk154.js";
8
+ } from "./chunk-gr83pq7x.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-khjm356r.js";
12
+ } from "./chunk-z9hv47ps.js";
13
13
  import {
14
14
  EventLog,
15
15
  PHASE_ID_RE
16
- } from "./chunk-abswp1vt.js";
16
+ } from "./chunk-77x6v0gg.js";
17
17
  import {
18
18
  PHASE_IDS
19
- } from "./chunk-veffvzw5.js";
20
- import"./chunk-g7wz339n.js";
21
- import"./chunk-zvxgdssb.js";
19
+ } from "./chunk-g4d2v4jc.js";
20
+ import"./chunk-857jhjmb.js";
22
21
  import {
23
22
  ADVISORY_KEY,
24
23
  MAX_FACT_CHARS,
@@ -28,7 +27,7 @@ import {
28
27
  renderQuestionBlock,
29
28
  replaceBlock,
30
29
  serializeQuestions
31
- } from "./chunk-khy21x6e.js";
30
+ } from "./chunk-yanmz0vz.js";
32
31
  import {
33
32
  ITERATION_ONLY_SLOT,
34
33
  PROJECT_FRAMEWORK_DIR,
@@ -38,7 +37,7 @@ import {
38
37
  isScopedTemplate,
39
38
  parseYaml,
40
39
  scopedSlotOf
41
- } from "./chunk-f6t1xc3f.js";
40
+ } from "./chunk-qmp5p77c.js";
42
41
 
43
42
  // src/hooks/answer-capture.ts
44
43
  import { existsSync as existsSync5 } from "fs";
@@ -1,31 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-q3d4xdhb.js";
4
+ } from "./chunk-mg3aj0sa.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-tna1bz8r.js";
12
- import"./chunk-khjm356r.js";
11
+ } from "./chunk-pne7afnf.js";
12
+ import"./chunk-z9hv47ps.js";
13
13
  import {
14
+ REBALANCE_SOURCE,
15
+ applyRebalance,
14
16
  asRunBudget,
15
17
  currentActor,
18
+ describeRebalance,
16
19
  economyFor,
17
20
  isHostTokens,
18
21
  nowRfc3339,
22
+ planRebalance,
19
23
  raiseCommand,
24
+ raiseGrantVerdict,
20
25
  remainingWork,
21
26
  shortBy,
22
27
  validateRunBudget,
23
28
  wouldExceed,
24
29
  wouldExceedHostTokens
25
- } from "./chunk-e618k6te.js";
30
+ } from "./chunk-zpch8e2j.js";
26
31
  import {
27
32
  EventLog
28
- } from "./chunk-abswp1vt.js";
33
+ } from "./chunk-77x6v0gg.js";
29
34
  import {
30
35
  cursorStage,
31
36
  hostTokensIn,
@@ -34,18 +39,20 @@ import {
34
39
  newestActiveRun,
35
40
  renderRunEconomies,
36
41
  runSpend
37
- } from "./chunk-hws2gnxj.js";
38
- import"./chunk-g7wz339n.js";
42
+ } from "./chunk-0d4csx4s.js";
43
+ import {
44
+ buildStageDefaults
45
+ } from "./chunk-g4d2v4jc.js";
39
46
  import {
40
47
  noteDeprecations
41
- } from "./chunk-khy21x6e.js";
48
+ } from "./chunk-yanmz0vz.js";
42
49
  import {
43
50
  PROJECT_WORK_DIR,
44
51
  findWorkspaceRoot,
45
52
  locateWork,
46
53
  parseYaml,
47
54
  stageYamlPath
48
- } from "./chunk-f6t1xc3f.js";
55
+ } from "./chunk-qmp5p77c.js";
49
56
 
50
57
  // src/hooks/budget-gate.ts
51
58
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -72,6 +79,8 @@ function loadRunBudget(runDir) {
72
79
  // src/hooks/budget-gate.ts
73
80
  var SPAWN_RE = /^(claude -p|tldrx next|tldrx run auto|tldrx expert train|tldrx seed triage)\b/;
74
81
  var RUN_ARG_RE = /--run[= ]([\w.-]+)/;
82
+ var RUN_AUTO_RE = /^tldrx run auto\b/;
83
+ var NO_REBALANCE_RE = /(?:^|\s)--no-rebalance-finished(?:[\s=]|$)/;
75
84
  var DEFAULT_TRAIN_USD = 2;
76
85
  var DEFAULT_FULL_TRAIN_USD = 3;
77
86
  var DEFAULT_TRIAGE_USD = 1;
@@ -116,6 +125,7 @@ await runHook("budget-gate", async () => {
116
125
  const attended = isAttendedByHostView(view);
117
126
  const stage = cursorStage(view);
118
127
  const declared = stage?.budget_usd ?? stageBudgetFromLibrary(root, view.cursor.stage);
128
+ const attempts = buildStageDefaults(root, view.scope, view.cursor.stage).attempts;
119
129
  const work = declared === null ? null : remainingWork({
120
130
  runDir: view.dir,
121
131
  phaseId: view.cursor.phase,
@@ -124,7 +134,8 @@ await runHook("budget-gate", async () => {
124
134
  perAgentMaxUsd: budget.per_agent_max_usd,
125
135
  maxUsd: null,
126
136
  economy: economyFor(budget, view.cursor.phase),
127
- attended
137
+ attended,
138
+ attempts
128
139
  });
129
140
  const estimate = estimateFor(command, work === null ? null : work.usd);
130
141
  if (estimate <= 0)
@@ -156,7 +167,7 @@ ${economies}`}`);
156
167
  `);
157
168
  return;
158
169
  }
159
- const decision = wouldExceed(budget, view.cursor.phase, estimate);
170
+ const decision = wouldExceed(budget, view.cursor.phase, estimate, attempts);
160
171
  if (!decision.blocked)
161
172
  return;
162
173
  if (attended) {
@@ -176,6 +187,15 @@ ${economies}`}`);
176
187
  `);
177
188
  return;
178
189
  }
190
+ if (decision.scope === "phase" && RUN_AUTO_RE.test(command) && !NO_REBALANCE_RE.test(command)) {
191
+ const short = shortBy(estimate, decision.remaining);
192
+ const covered = finishedPhasesCover(view.dir, budget, view.cursor.phase, short);
193
+ if (covered !== null) {
194
+ process.stderr.write(`tldrx hook budget-gate: ${view.cursor.phase} has $${decision.remaining.toFixed(2)} left of ` + `$${decision.ceiling.toFixed(2)} and the stage estimate is $${estimate.toFixed(2)} — NOT refusing: ` + `\`run auto\` rebalances finished phases before it refuses, and ${covered}, which covers the ` + `$${short.toFixed(2)} shortfall. The loop makes that move on its own record (\`budget.raised\`, ` + `source: ${REBALANCE_SOURCE}); launch with --no-rebalance-finished to be refused here instead.` + `${economies === null ? "" : ` ${economies}`}
195
+ `);
196
+ return;
197
+ }
198
+ }
179
199
  new EventLog(join2(view.dir, "events.jsonl")).tryAppend({
180
200
  ts: nowRfc3339(),
181
201
  run: view.run,
@@ -226,6 +246,22 @@ function estimateFor(command, stageBudget) {
226
246
  }
227
247
  return stageBudget ?? 0;
228
248
  }
249
+ function finishedPhasesCover(runDir, budget, phaseId, shortUsd) {
250
+ try {
251
+ const doc = parseYaml(readFileSync2(join2(runDir, "run.yml"), "utf8"));
252
+ if (doc === null || !Array.isArray(doc.phases))
253
+ return null;
254
+ const plan = planRebalance(budget, doc, phaseId, shortUsd);
255
+ if (plan.moves.length === 0)
256
+ return null;
257
+ const applied = applyRebalance(budget, plan);
258
+ if (applied.outcomes.some((outcome) => raiseGrantVerdict(budget, outcome).exceeds))
259
+ return null;
260
+ return describeRebalance(plan);
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
229
265
  function failClosed(command, why) {
230
266
  deny(`[tldrx] budget-gate: refusing \`${command.slice(0, 80)}\` — this gate could not read the budget ` + `it is supposed to enforce (${why}).
231
267
  ` + "It fails CLOSED: a spend nothing can check is exactly the one that must not start. Fix the run's " + "budget.yml, or pass `--run <id>` so the gate knows which run to charge.");
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  listRunDirs,
3
3
  parseYaml
4
- } from "./chunk-f6t1xc3f.js";
4
+ } from "./chunk-qmp5p77c.js";
5
5
 
6
6
  // src/hooks/lib/runFile.ts
7
7
  import { existsSync, readFileSync } from "node:fs";
@@ -2,7 +2,7 @@ import {
2
2
  GATE_POLICIES,
3
3
  validateGatesPolicy,
4
4
  validateStagePolicy
5
- } from "./chunk-g7wz339n.js";
5
+ } from "./chunk-g4d2v4jc.js";
6
6
  import {
7
7
  asDocument,
8
8
  isRecord,
@@ -15,7 +15,7 @@ import {
15
15
  requireString,
16
16
  requireVersion,
17
17
  result
18
- } from "./chunk-f6t1xc3f.js";
18
+ } from "./chunk-qmp5p77c.js";
19
19
 
20
20
  // src/core/events/EventLog.ts
21
21
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -39,6 +39,7 @@ var EVENT_TYPES = [
39
39
  "task.done",
40
40
  "agent.spawned",
41
41
  "agent.result",
42
+ "agent.rate_limited",
42
43
  "question.asked",
43
44
  "question.answered",
44
45
  "gate.requested",
@@ -54,6 +55,7 @@ var EVENT_TYPES = [
54
55
  "story.review_retried",
55
56
  "story.work_rescued",
56
57
  "story.touches_widened",
58
+ "plan.fix_round",
57
59
  "epic.released",
58
60
  "worktree.foreign_work_aside",
59
61
  "worktree.foreign_work_restored",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toolInput
3
- } from "./chunk-tna1bz8r.js";
3
+ } from "./chunk-pne7afnf.js";
4
4
 
5
5
  // src/hooks/lib/wouldBe.ts
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PROJECT_FRAMEWORK_DIR,
3
3
  PROJECT_WORK_DIR
4
- } from "./chunk-f6t1xc3f.js";
4
+ } from "./chunk-qmp5p77c.js";
5
5
 
6
6
  // src/core/lock/workspaceLock.ts
7
7
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, rmSync as rmSync2, writeSync, closeSync } from "node:fs";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  findDuplicate
3
- } from "./chunk-48ctk154.js";
3
+ } from "./chunk-gr83pq7x.js";
4
4
  import {
5
5
  evidencePath,
6
6
  gateEvidencePath,
7
7
  parseEvidence
8
- } from "./chunk-abswp1vt.js";
8
+ } from "./chunk-77x6v0gg.js";
9
9
  import {
10
10
  openBlocks,
11
11
  parseQuestions
12
- } from "./chunk-khy21x6e.js";
12
+ } from "./chunk-yanmz0vz.js";
13
13
 
14
14
  // src/core/distill/distill.ts
15
15
  var CONFLICT_THRESHOLD = 0.6;
@@ -1,9 +1,3 @@
1
- import {
2
- GatePolicyError,
3
- STAGE_TUNING_DEFAULTS,
4
- parseWorkflowGates,
5
- readStageTuning
6
- } from "./chunk-g7wz339n.js";
7
1
  import {
8
2
  PROJECT_FRAMEWORK_DIR,
9
3
  STAGES_DIR,
@@ -12,12 +6,109 @@ import {
12
6
  isRecord,
13
7
  loadWorkspace,
14
8
  parseYaml
15
- } from "./chunk-f6t1xc3f.js";
9
+ } from "./chunk-qmp5p77c.js";
16
10
 
17
11
  // src/core/run/workflowPreset.ts
18
12
  import { existsSync, readFileSync } from "node:fs";
19
13
  import { join } from "node:path";
20
14
 
15
+ // src/core/run/stagePolicy.ts
16
+ function validateStagePolicy(value, stageIds, issues, spec) {
17
+ if (value === undefined || value === null)
18
+ return;
19
+ if (!isRecord(value)) {
20
+ issues.push({
21
+ path: spec.field,
22
+ message: `expected a mapping of stage id -> ${spec.policies.join(" | ")}`
23
+ });
24
+ return;
25
+ }
26
+ const known = new Set(stageIds);
27
+ for (const [key, raw] of Object.entries(value)) {
28
+ if (typeof raw !== "string" || !spec.policies.includes(raw)) {
29
+ issues.push({
30
+ path: `${spec.field}.${key}`,
31
+ message: `expected one of ${spec.policies.join(" | ")}, got ${JSON.stringify(raw)}`
32
+ });
33
+ }
34
+ if (known.size > 0 && !known.has(key)) {
35
+ issues.push({ path: `${spec.field}.${key}`, message: `names no stage in this run` });
36
+ }
37
+ }
38
+ }
39
+
40
+ // src/core/run/gatePolicy.ts
41
+ var GATE_POLICIES = ["human", "auto", "agent"];
42
+ var RESERVED_GATE_KEYS = ["collapse"];
43
+
44
+ class GatePolicyError extends Error {
45
+ }
46
+ function isGatePolicy(value) {
47
+ return typeof value === "string" && GATE_POLICIES.includes(value);
48
+ }
49
+ function parseWorkflowGates(value, stageIds, source) {
50
+ if (value === undefined || value === null)
51
+ return {};
52
+ if (!isRecord(value)) {
53
+ throw new GatePolicyError(`${source}: gates must be a mapping of stage id -> ${GATE_POLICIES.join(" | ")}`);
54
+ }
55
+ const known = new Set(stageIds);
56
+ const out = {};
57
+ for (const [key, raw] of Object.entries(value)) {
58
+ if (RESERVED_GATE_KEYS.includes(key))
59
+ continue;
60
+ if (!known.has(key)) {
61
+ throw new GatePolicyError(`${source}: gates names '${key}', which is not one of this workflow's stages (${stageIds.join(", ")})`);
62
+ }
63
+ if (!isGatePolicy(raw)) {
64
+ throw new GatePolicyError(`${source}: gates.${key} must be ${GATE_POLICIES.join(" | ")}, got ${JSON.stringify(raw)}`);
65
+ }
66
+ out[key] = raw;
67
+ }
68
+ return out;
69
+ }
70
+ function validateGatesPolicy(value, stageIds, issues) {
71
+ validateStagePolicy(value, stageIds, issues, { field: "gates_policy", policies: GATE_POLICIES });
72
+ }
73
+
74
+ // src/core/schemas/stageTuning.ts
75
+ var STAGE_TUNING_DEFAULTS = {
76
+ attempts: 2,
77
+ fixlistRounds: 1,
78
+ reviewerShare: 0.25,
79
+ gateSignerShare: 0.25,
80
+ storyCapMultiplier: 3,
81
+ storyCapFloorUsd: 4
82
+ };
83
+ var STAGE_TUNING_RANGES = {
84
+ attempts: { key: "attempts", min: 1, max: 5, integer: true },
85
+ fixlistRounds: { key: "fixlist_rounds", min: 0, max: 3, integer: true },
86
+ reviewerShare: { key: "reviewer_share", min: 0, max: 1, integer: false },
87
+ gateSignerShare: { key: "gate_signer_share", min: 0, max: 1, integer: false },
88
+ storyCapMultiplier: { key: "story_cap_multiplier", min: 1, max: 20, integer: false },
89
+ storyCapFloorUsd: { key: "story_cap_floor_usd", min: 0, max: 200, integer: false }
90
+ };
91
+ var FIELDS = Object.keys(STAGE_TUNING_RANGES);
92
+ function inTuningRange(range, value) {
93
+ if (!Number.isFinite(value))
94
+ return false;
95
+ if (range.integer && !Number.isInteger(value))
96
+ return false;
97
+ return value >= range.min && value <= range.max;
98
+ }
99
+ function readStageTuning(doc) {
100
+ if (!isRecord(doc))
101
+ return STAGE_TUNING_DEFAULTS;
102
+ const out = { ...STAGE_TUNING_DEFAULTS };
103
+ for (const field of FIELDS) {
104
+ const range = STAGE_TUNING_RANGES[field];
105
+ const value = doc[range.key];
106
+ if (typeof value === "number" && inTuningRange(range, value))
107
+ out[field] = value;
108
+ }
109
+ return out;
110
+ }
111
+
21
112
  // src/core/schemas/stage.ts
22
113
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
23
114
  function isEffortLevel(value) {
@@ -30,11 +121,11 @@ var PRECONDITION_TIMEOUT_S = 60;
30
121
  var PHASE_IDS = ["01-what", "02-how", "03-plan", "04-build", "05-watch"];
31
122
  var PHASE_SEGMENT_RE = /^0[1-5]-[a-z]+$/;
32
123
  var DEFAULT_TIMEOUT_S = 7200;
33
- function buildStageDefaults(root, scope) {
124
+ function buildStageDefaults(root, scope, stageId) {
34
125
  const fallback = { attempts: STAGE_TUNING_DEFAULTS.attempts, timeoutS: DEFAULT_TIMEOUT_S };
35
126
  try {
36
127
  const preset = loadWorkflowPreset(root, scope);
37
- const stage = preset.stages.find((s) => s.phase === PHASE_IDS[3]);
128
+ const stage = (stageId === undefined ? undefined : preset.stages.find((s) => s.id === stageId)) ?? preset.stages.find((s) => s.phase === PHASE_IDS[3]);
38
129
  if (stage === undefined)
39
130
  return fallback;
40
131
  return { attempts: stage.attempts, timeoutS: stage.timeout_s };
@@ -275,4 +366,4 @@ function isString(value) {
275
366
  return typeof value === "string";
276
367
  }
277
368
 
278
- export { PHASE_IDS, DEFAULT_TIMEOUT_S, buildStageDefaults };
369
+ export { validateStagePolicy, GATE_POLICIES, validateGatesPolicy, STAGE_TUNING_DEFAULTS, PHASE_IDS, DEFAULT_TIMEOUT_S, buildStageDefaults };
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-zvxgdssb.js";
6
+ } from "./chunk-857jhjmb.js";
7
7
  import {
8
8
  FACT_CONFIDENCES,
9
9
  FACT_DECIDERS,
@@ -15,7 +15,7 @@ import {
15
15
  isLive,
16
16
  isRetired,
17
17
  noteDeprecations
18
- } from "./chunk-khy21x6e.js";
18
+ } from "./chunk-yanmz0vz.js";
19
19
  import {
20
20
  SRC_PATTERNS,
21
21
  asDocument,
@@ -28,7 +28,7 @@ import {
28
28
  requireString,
29
29
  requireVersion,
30
30
  result
31
- } from "./chunk-f6t1xc3f.js";
31
+ } from "./chunk-qmp5p77c.js";
32
32
 
33
33
  // src/core/facts/FactsStore.ts
34
34
  import { existsSync, readFileSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  EMPTY_SECTION_RULE,
4
4
  noneBullet,
5
5
  srcRule
6
- } from "./chunk-f6t1xc3f.js";
6
+ } from "./chunk-qmp5p77c.js";
7
7
 
8
8
  // src/hooks/lib/messages.ts
9
9
  var GRAMMAR = "[src: <repo:path:line> | https://… | Q<n> | F<n> | $ <cmd> → exit <n> | graph:<node> | absent:<path>]";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseHookInput,
3
3
  readStdin
4
- } from "./chunk-khjm356r.js";
4
+ } from "./chunk-z9hv47ps.js";
5
5
 
6
6
  // src/hooks/lib/decide.ts
7
7
  function deny(reason) {
@@ -9,7 +9,7 @@ import {
9
9
  spentBasis,
10
10
  tallyOf,
11
11
  validateRunBudget
12
- } from "./chunk-e618k6te.js";
12
+ } from "./chunk-zpch8e2j.js";
13
13
  import {
14
14
  EventLog,
15
15
  OUTCOME_NOT_RECORDED,
@@ -22,12 +22,12 @@ import {
22
22
  isTerminal,
23
23
  stageAt,
24
24
  validateRunFile
25
- } from "./chunk-abswp1vt.js";
25
+ } from "./chunk-77x6v0gg.js";
26
26
  import {
27
27
  cursorStage,
28
28
  isAttendedByHostView,
29
29
  openRunViews
30
- } from "./chunk-hws2gnxj.js";
30
+ } from "./chunk-0d4csx4s.js";
31
31
  import {
32
32
  backupPathFor,
33
33
  isAlive,
@@ -36,20 +36,21 @@ import {
36
36
  workspaceRootOfRunDir,
37
37
  writeAtomic,
38
38
  yamlScalar
39
- } from "./chunk-zvxgdssb.js";
39
+ } from "./chunk-857jhjmb.js";
40
40
  import {
41
41
  isAdvisory,
42
42
  noteDeprecations,
43
43
  openBlocks,
44
44
  parseQuestions
45
- } from "./chunk-khy21x6e.js";
45
+ } from "./chunk-yanmz0vz.js";
46
46
  import {
47
47
  FRAMEWORK_ROOT,
48
+ MAX_ITEM_CHARS,
48
49
  PLAN_STATUSES,
49
50
  listRunDirs,
50
51
  parseYaml,
51
52
  parseYamlRepairing
52
- } from "./chunk-f6t1xc3f.js";
53
+ } from "./chunk-qmp5p77c.js";
53
54
 
54
55
  // src/core/statusline/runSnapshot.ts
55
56
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
@@ -634,7 +635,7 @@ var WAVE_CAP_REASON_KEY = "wave_cap_reason";
634
635
  var PLAN_SHAPE_RULES = [
635
636
  {
636
637
  issue: "#316",
637
- text: `**At most ${String(MAX_WAVES_PER_RUN)} waves** (patch for ${RUN_SIZE_ISSUES} — the framework's measured ` + "carry today, not a design preference). A later wave waits on every earlier merge, so each extra wave is " + "a stall: a 4-wave plan left 2 stories never attempted. The `plan` check refuses more unless the root of " + `\`${WAVES_FILE}\` records \`${WAVE_CAP_REASON_KEY}: "<why these stories cannot share a wave>"\`.`
638
+ text: `**At most ${String(MAX_WAVES_PER_RUN)} waves** (patch for ${RUN_SIZE_ISSUES} — the framework's measured ` + "carry today, not a design preference). A later wave waits on every earlier merge, so each extra wave is " + "a stall: a 4-wave plan left 2 stories never attempted. The `plan` check refuses more unless the root of " + `\`${WAVES_FILE}\` records \`${WAVE_CAP_REASON_KEY}: "<why these stories cannot share a wave>"\` — one sentence ` + `on one line, at most ${String(MAX_ITEM_CHARS)} characters (#328).`
638
639
  },
639
640
  {
640
641
  issue: "#316",
@@ -663,6 +664,9 @@ var LINE_BREAK_NOTE = `\`${LINE_BREAK_MARK}\` marks a line break inside a quoted
663
664
  var FINDING_STATUS_RE = new RegExp(`—\\s+(${PLAN_STATUSES.join("|")})\\s+—`);
664
665
  var FINDING_REASON_RE = new RegExp(`—\\s+(?:${PLAN_STATUSES.join("|")})\\s+—\\s+[^:]*:\\s*(\\S.*)$`);
665
666
 
667
+ // src/core/build/dependencyHold.ts
668
+ var NOT_A_HOLD_REASON = "its block is not a dependency hold — a reviewer, a DoD or a developer recorded it, " + "and reopening a dependency does not answer that; `tldrx story reopen <id>` does";
669
+
666
670
  // src/core/run/runOutcome.ts
667
671
  var BUILD_HANDOFF = join3(BUILD_PHASE, "handoff.md");
668
672
  var NO_BUILD_PHASE = "this run's workflow has no Build phase, so no story could be delivered";
@@ -877,8 +881,33 @@ function hasPreparedBundle(runDir, stageId) {
877
881
  return preparedBundles(runDir, stageId).length > 0;
878
882
  }
879
883
 
884
+ // src/core/run/lastStart.ts
885
+ function lastStageStartMode(runDir, stageId) {
886
+ const events = EventLog.forRun(runDir).readAll().events;
887
+ for (let index = events.length - 1;index >= 0; index -= 1) {
888
+ const event = events[index];
889
+ if (event === undefined)
890
+ continue;
891
+ if (event.type !== "stage.started" || event.stage !== stageId)
892
+ continue;
893
+ const mode = event.payload.mode;
894
+ return typeof mode === "string" && mode !== "" ? mode : null;
895
+ }
896
+ return null;
897
+ }
898
+ function startedHeadless(runDir, stageId) {
899
+ return lastStageStartMode(runDir, stageId) === "headless";
900
+ }
901
+
880
902
  // src/core/run/waiting.ts
881
- var MOVABLE_KINDS = ["gate", "answer", "ready", "failed", "prepared"];
903
+ var MOVABLE_KINDS = [
904
+ "gate",
905
+ "answer",
906
+ "ready",
907
+ "failed",
908
+ "prepared",
909
+ "interrupted"
910
+ ];
882
911
  function isMovable(kind) {
883
912
  return MOVABLE_KINDS.includes(kind);
884
913
  }
@@ -948,6 +977,13 @@ function waitingFor(run, runDir) {
948
977
  questions: open
949
978
  };
950
979
  }
980
+ if (startedHeadless(runDir, entry.stage.id)) {
981
+ return {
982
+ kind: "interrupted",
983
+ message: `${entry.phase.id}/${entry.stage.id} was interrupted — its headless turn's process is gone — ` + `\`tldrx run auto ${basename2(runDir)}\``,
984
+ questions: open
985
+ };
986
+ }
951
987
  if (hasPreparedBundle(runDir, entry.stage.id)) {
952
988
  return {
953
989
  kind: "prepared",
@@ -7548,7 +7548,7 @@ function requireStringList(value, path, issues, rules = {}) {
7548
7548
  if (item.length > MAX_ITEM_CHARS) {
7549
7549
  issues.push({
7550
7550
  path: at,
7551
- message: `${item.length} characters exceeds the ${MAX_ITEM_CHARS}-character cap on one list item ` + "— split it into several items"
7551
+ message: `${item.length} characters (cap ${MAX_ITEM_CHARS}) — split it into several items`
7552
7552
  });
7553
7553
  }
7554
7554
  if (rules.pattern !== undefined && !rules.pattern.test(item)) {
@@ -9202,4 +9202,4 @@ function noneBullet(lookedAt) {
9202
9202
  return `- none [src: absent:${lookedAt}]`;
9203
9203
  }
9204
9204
 
9205
- export { runtime, parseYaml, parseYamlRepairing, FRAMEWORK_ROOT, STAGES_DIR, WORKFLOWS_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, SRC_SEPARATOR, srcToken, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, withoutSrcToken, classifySrc, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, PLAN_STATUSES, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, allowlistIssue, ITERATION_ONLY_SLOT, scopedSlotOf, isScopedTemplate, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
9205
+ export { runtime, parseYaml, parseYamlRepairing, FRAMEWORK_ROOT, STAGES_DIR, WORKFLOWS_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, SRC_SEPARATOR, srcToken, readableSource, SRC_PATTERNS, srcRule, parseSrcToken, withoutSrcToken, classifySrc, result, requireVersion, isRecord, requireKeys, requireEnum, requireArray, requireNumber, requireString, requireRecord, asDocument, STORY_ID_RE, WAVE_ID_RE, PLAN_STATUSES, MAX_ITEM_CHARS, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, allowlistIssue, ITERATION_ONLY_SLOT, scopedSlotOf, isScopedTemplate, readStory, DodCommandRefused, runDodCommand, commandProbeIssues, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs, parseHandoff, isHandoff, BULLET_RULE, EMPTY_SECTION_RULE, validateHandoff, validateCitations, noneBullet };
@@ -3,7 +3,7 @@ import {
3
3
  parseSrcToken,
4
4
  srcToken,
5
5
  withoutSrcToken
6
- } from "./chunk-f6t1xc3f.js";
6
+ } from "./chunk-qmp5p77c.js";
7
7
 
8
8
  // src/core/text/questions.ts
9
9
  var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-f6t1xc3f.js";
3
+ } from "./chunk-qmp5p77c.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {