tldr-experts 0.26.1 → 0.28.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 (30) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +2 -0
  3. package/dist/hooks/answer-capture.js +9 -9
  4. package/dist/hooks/budget-gate.js +9 -9
  5. package/dist/hooks/{chunk-jpp137mb.js → chunk-48ctk154.js} +3 -3
  6. package/dist/hooks/{chunk-41k19q8t.js → chunk-abswp1vt.js} +3 -2
  7. package/dist/hooks/{chunk-d0jhr5wh.js → chunk-c62y35wh.js} +3 -3
  8. package/dist/hooks/{chunk-0sy3tmfg.js → chunk-f6t1xc3f.js} +26 -3
  9. package/dist/hooks/{chunk-bznt6jb2.js → chunk-g7wz339n.js} +1 -1
  10. package/dist/hooks/{chunk-yre2scxn.js → chunk-hws2gnxj.js} +1 -1
  11. package/dist/hooks/{chunk-qzsxhsyj.js → chunk-khjm356r.js} +1 -1
  12. package/dist/hooks/{chunk-5stgwp3g.js → chunk-khy21x6e.js} +30 -12
  13. package/dist/hooks/{chunk-pwzy19er.js → chunk-kx45kt9n.js} +40 -6
  14. package/dist/hooks/{chunk-ka7768vx.js → chunk-q3d4xdhb.js} +1 -1
  15. package/dist/hooks/{chunk-t44983nq.js → chunk-tna1bz8r.js} +1 -1
  16. package/dist/hooks/{chunk-m1s8a6s0.js → chunk-veffvzw5.js} +2 -2
  17. package/dist/hooks/{chunk-bbrhs055.js → chunk-xhd77h0p.js} +26 -22
  18. package/dist/hooks/{chunk-n5bxz2cw.js → chunk-zrwq6r7k.js} +1 -1
  19. package/dist/hooks/{chunk-ezfvp5kx.js → chunk-zvxgdssb.js} +1 -1
  20. package/dist/hooks/claim-sources.js +5 -5
  21. package/dist/hooks/dod-gate.js +8 -8
  22. package/dist/hooks/no-reask.js +8 -8
  23. package/dist/hooks/session-start.js +13 -13
  24. package/dist/hooks/statusline.js +9 -9
  25. package/dist/tldrx.js +2364 -1178
  26. package/package.json +1 -1
  27. package/plugin/.claude-plugin/plugin.json +1 -1
  28. package/plugin/skills/tldrx-plan/SKILL.md +16 -5
  29. package/stages/plan/stage.md +1 -0
  30. package/templates/questions.md +2 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,161 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.28.0 — 2026-09-15
4
+
5
+ ### Added
6
+
7
+ - **A story whose merge-up to its epic conflicts in a few of its own files gets one conflict
8
+ turn instead of a person (#286).** MEASURED on a field run: a story with a green DoD conflicted
9
+ bringing itself up to the epic (#268) in one counted test list both stories bumped, blocked
10
+ correctly — and nothing in the framework could resolve it: the developer holds no merge verb, a
11
+ plain reopen had no work, and a person did it in a scratch worktree; 1.5 h later the same story
12
+ conflicted again in four files. Now, when the conflict is in at most three files, every one in
13
+ the story's declared `touches` (the boundary matcher, `inSurface`), with no conflict turn since
14
+ its last `story.reopened` and an attempt left, the story is requeued the way #313 requeues a
15
+ red DoD — prior status, worktree kept, one attempt spent — and one new `story.conflict_turn`
16
+ event is written. Its next attempt is dispatched with the CURRENT epic tip merged into the
17
+ worktree and the merge left open (markers, `MERGE_HEAD`); the prompt names the files, the
18
+ story's intent and the stories that landed on the epic, and `git add` + `git commit` — verbs the
19
+ developer already holds — close it, with the DoD as judge. A marker guard in `git.ts`
20
+ (each conflicted file handed, any path it was renamed to, and every added or untracked path a
21
+ commit could sweep in, read for any `<<<<<<<`, `|||||||`
22
+ or `>>>>>>>` line — a lone `=======` heading underline is not a marker — plus `MERGE_HEAD`)
23
+ blocks, never requeues, a turn that left a marker in one of those files or never closed the
24
+ merge, naming the files — without it, measured
25
+ by mutation, markers were committed and merged onto the epic as `done`. Anything else still
26
+ blocks exactly as before, with the reason it got no turn appended. The bound is counted from
27
+ `events.jsonl`, so it holds across invocations, and `tldrx replay` says an agent, not a person,
28
+ resolved the merge. Headless only: the host's `--commit` half and an as-is settlement block as
29
+ they did.
30
+
31
+ ### Fixed
32
+
33
+ - **`tldrx story reopen` on a `blocked` story also releases the dependents it alone was holding
34
+ (#312).** Reopening a blocked dependency left every dependent `blocked` with a
35
+ `dependency S<n> blocked` reason that had just gone stale, so each needed its own reopen —
36
+ measured three times in one day on a live run. Every `blocked` story whose recorded hold names
37
+ the reopened story (or a story released this way, transitively) now goes back to `todo` in the
38
+ same command when no other dependency still holds it. Each release is its own signed
39
+ `story.reopened` naming the dependency that caused it (additive `released_by` and `dependency`),
40
+ consuming no attempt. A dependent still held by another dependency, or blocked by a verdict
41
+ rather than a hold, stays `blocked`, and the output says which and why.
42
+ - **`Recommended: A [src: …]` is a recommendation, and `seed check` reads the line the way the
43
+ loop does (#323).** On a live run the loop answered Q8's `Recommended: A — <why> [src: …]`
44
+ and left Q7's `Recommended: A [src: …]` for a person, parking an unattended run: the question
45
+ parser demanded a dash reason before any citation, while `seed check` tested only the
46
+ substring `Recommended:` and had passed the same shape clean — two derivations of "has a
47
+ recommendation" that disagreed. There is now one grammar and one reader
48
+ (`parseRecommendedLine`): the letter first, then any of its `)`, a `— <why>` and trailing
49
+ `[src: …]` tokens. `seed check` calls it and names, as exit-1 findings, a `Recommended:` line
50
+ it cannot read (`question-recommended-unreadable`, with the line and the shape expected) and
51
+ a letter naming none of the options the question lists (`question-recommended-option`). A
52
+ seed question that lists no options is not judged on its letter — the options are the What
53
+ stage's to write. A §2.7 block's unreadable line is still ignored rather than refused.
54
+ - **An unattended run is no longer stranded by its own provenance, or by a refused check it could
55
+ fix itself (#231).** Measured 2026-09-14 on a `run auto` validation run (0.27.0): a What stage
56
+ wrote `[src: absent:.tldrx/memory/facts.yml#tenant-credits]`, true when written; the loop then
57
+ auto-answered that stage's questions, the new facts' `source:` maps read
58
+ `run: 260914-tenant-credits`, and `claim-sources` refused the absence on the run id. The refused
59
+ `auto` gate then waited for a person, who typed `reject --and-continue` with the finding —
60
+ intervention #2 of that run. Two fixes. An `absent:…#<needle>` over the workspace's
61
+ `facts.yml` no longer searches a fact's `source: {who, when, run, q, decided_by}` map — that is
62
+ who recorded it, not what it says; a map with any other key is searched in full, and a needle in
63
+ a fact's content still refuses, even on a fact this run just wrote. And under `--wait-gates`,
64
+ an `auto` gate refused by failed checks ALONE (a declared check, or `claim-sources` failing)
65
+ re-runs its stage once through the same `reject --and-continue` path, signed `run auto` with the
66
+ findings as the note; past that bound, or on a `human`/`agent` gate, or when questions, budget,
67
+ stories, boundary or status also hold it, the loop waits for a person as before and prints why.
68
+
69
+ ## 0.27.0 — 2026-09-14
70
+
71
+ ### Added
72
+
73
+ - **The Plan gate holds a plan to its shape, not only to whether it can execute (#316, #317,
74
+ #318, #319).** A planning audit of 9 runs (relayed, not re-run) found 25 of 54 build-gate
75
+ rejections were dependency reopens and every multi-story plan in one workspace a
76
+ one-story-per-wave chain; a 4-wave plan left 2 stories never attempted, while the 2-wave cap
77
+ lived only as skill prose and a `seed check` advisory nothing enforced. The `plan` check now
78
+ refuses more waves than the framework carries per run unless `waves.yml` records the new
79
+ optional `wave_cap_reason` (an additive root key — `version: 1` only grows), refuses a story
80
+ scheduled later than its `depends_on` requires, and names in its detail a dod command some
81
+ stories of one epic and repo carry and a sibling does not — an advisory, because which
82
+ stories share a shape is not machine-readable and the evidence is one epic. Vertical slices,
83
+ inventory files in `touches:` and end-to-end placement are Plan-prompt rules rather than
84
+ checks, because no stack-independent name marks a route file and no changed-file set exists
85
+ at Plan time to compare against. Every rule, number and sentence comes from one file,
86
+ `src/core/plan/planShape.ts`, which the gate, the rendered "Plan shape" section
87
+ (`tldrx plan schema`) and `seed check` all read; the gate's pass is its own, not
88
+ `validatePlan`'s, so a plan approved before it still loads at Build.
89
+
90
+ - **`tldrx run auto --rebalance-finished` moves a blocked phase's shortfall out of money a
91
+ finished phase can no longer spend (#314).** MEASURED on a field run: `04-build` was refused
92
+ $11.07 short while `01-what` had finished $16.25 under its ceiling and the run total had $141
93
+ of room; `run auto` stopped and a person typed `budget raise 04-build 12 --take-from 01-what`
94
+ and relaunched. The move was already sanctioned — nothing looked for it. With the flag, the
95
+ budget gate moves EXACTLY the shortfall before refusing, out of phases whose every stage is
96
+ `done`/`skipped`, none stale, priced in `metered-usd` and with no unmetered turn (a lower-bound
97
+ spend proves nothing about what is left) — so `05-watch` with a stage still to run never gives.
98
+ It goes through `budget raise --take-from`'s own code, so the run ceiling never grows; it never
99
+ passes a recorded grant, not even under `on_grant_exceed: warn`; it never makes a partial move.
100
+ Each move is one `budget.raised` with both phases' before/after, `source` and the launcher as
101
+ actor. OPT-IN, not a default or a `budget.yml` key, because a phase ceiling is a person's
102
+ decision about money — the reason `--retry-failed` and `--until-done` never touch exit 2.
103
+ Without the flag every money refusal now names the finished phases' unspent money and the exact
104
+ `--take-from` move, and `budget.blocked` records `short_usd`, `finished_unspent_usd` and
105
+ `uncovered_usd`. An agent gate on the stage a move unblocked still falls to a person, and its
106
+ reason now names the flag, the launcher and the donor phase instead of "a person moved" it.
107
+
108
+ ### Fixed
109
+
110
+ - **`tldrx ship` no longer opens a PR over a stale or red epic (#315).** Four PRs it opened in
111
+ one audit window came back red and needed a person, because between choosing the base and
112
+ `gh pr create` nothing fetched the base or ran a workspace command. Ship now fetches the base,
113
+ merges it into the epic in a throwaway worktree when behind, and runs the `done` stories'
114
+ Definition of Done on that tree; a conflict or a red command refuses (exit 2) naming the
115
+ paths or the command. Under `ship.push` the base is merged into the epic (forward, never
116
+ rebased or forced) and pushed with it; without it the branch is untouched. `--dry-run` runs
117
+ the same checks and exits the same way.
118
+
119
+ - **A reviewer is given at least $2.00, not $1.00 — the old floor sat below the reviews that
120
+ finish (#307, owner decision: "A: piso $2").** MEASURED in the issue across 44 reviewer task
121
+ rows in two workspaces: the 37 that returned a verdict cost $0.17–$1.02 (median $0.50), so the
122
+ most expensive COMPLETED review already spent more than the $1.00 floor allowed; and a 3-story
123
+ run whose Plan priced every story at $8–$9 put every reviewer on that floor — at the shipped
124
+ defaults an $8 story derives `8 × 0.25 / (2 × 1.25)` = $0.80 — and one died with
125
+ `Reached maximum budget ($1)` before reading the diff, parking its story at `review`. $1.00
126
+ was never measured; it was chosen because it looked reasonable. `REVIEWER_FLOOR_USD` is now
127
+ $2.00, a fixed constant — no new key, event or schema. The consequence the owner accepted:
128
+ #289's refuse-before-spawn now fires when a stage has less than $2.00 left, and the refusal
129
+ already says so with the figure (`a review costs at least $2.00`). The budget brake's copy of
130
+ the floor in `budget/remainingWork.ts` is now IMPORTED from `build/caps.ts` rather than
131
+ restated, so the brake's estimate and the executor's cap read one definition (`caps.ts`
132
+ imports only two schema leaves, so the budget-gate hook stays cheap). The Build golden moves
133
+ by exactly that figure: the four reviewer `agent.spawned` lines carry `max_budget_usd: 2`.
134
+ `test/build-parallel.test.ts`'s three-story wave moves from a $9 to a $12 stage, because at $9
135
+ three $2.00 floors plus three developers sum past the ceiling the test proves the wave fits
136
+ inside. The seed-check / plan-gate warning (part 3 of #307) is not in this change.
137
+
138
+ - **A developer whose DoD goes red gets the story's next attempt, instead of blocking it on the
139
+ first miss (#313).** Reported in #313's audit of 12 run folders (relayed, not re-measured
140
+ here): 17 human `tldrx story reopen`s were a red DoD — a snapshot not regenerated, a pinned
141
+ inventory the story's own change moved, a lint rule — that the developer never got a second
142
+ look at, each one waiting on a person to read the kept output and type the reopen. MEASURED on
143
+ `70da3d1` with the fake agent: a story at `attempts: 2` whose attempt 1 left a red DoD ended
144
+ `blocked` with ONE `task.started`. Mechanism: `settleHalf` blocked on any half-A failure,
145
+ while the reviewer's `changes` verdict already requeued while `attempt < attempts` — two
146
+ failures on the merits, one retry budget honoured for only one of them. A red DoD now takes
147
+ the same bound: the attempt settles back at the status it started from (not `blocked` — it is
148
+ about to run again) with `the DoD was red on attempt 1 of 2`, keeps its worktree, and the next developer prompt carries the kept output under `## Previous
149
+ attempt` with the DoD header (#211); the last attempt blocks, saying `the DoD stayed red on 2
150
+ of 2 attempts` when both were red. Narrow on purpose — a refused developer (#271), a cap death
151
+ (#277), a REFUSED or absent-binary dod command (#165, #209), no dod commands and a reopen that
152
+ changed nothing (#308) still block on the first attempt, because a second one buys the same
153
+ outcome. The attempts a red DoD spent are counted off `events.jsonl`, not memory: review of
154
+ the first cut MEASURED three red DoDs under `attempts: 2` when a spawn fault ended an
155
+ invocation between attempts and the next process started counting from zero. No new event,
156
+ no new field; the host door (`--prepare`/`--commit`) is unchanged. `tldrx learn`'s chapter 5
157
+ now narrates — and asserts — the two red attempts before the reopen.
158
+
3
159
  ## 0.26.1 — 2026-09-14
4
160
 
5
161
  ### Changed
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.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). |
339
+ | 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). |
338
340
  | 0.26.1 | 2026-09-14 | `beta` | Three refusals that stop a bad state from settling quietly. A Build fix round whose developer lands no diff is refused before the DoD instead of closing `done` on an unmoved tree, because `commitIfDirty` hands back the old head sha on a clean commit and the existing "no commit to review" gate never fires for it — `workSince` is now asked on the success path too, for any story a person put back with a note. `tldrx ship` refuses an epic carrying a story the reviewer marked `changes`: it asks the ledger's `lastMerge` for every story not `done` and exits 2 naming the story, the merge and the handoff's reason, instead of opening a PR whose "Not done" list hid rejected code already merged into the diff. And a stage-failure line keeps the branch it names when the worktree path is wider than the line: the refusal puts the branches before the path, and `oneLine` keeps a line's head and tail instead of dropping the identifiers that sat at the end. |
339
341
  | 0.26.0 | 2026-09-14 | `beta` | Two field defects from the same class — a reader of an event payload trusting a field its writer never set — plus the ceiling that killed the turn that earned it. A wide story's measured `touches_widened` no longer throws after the turn has settled: the cap knows the three lists by name and drops them whole by count, every reader derives before/after through one function, and an executor that throws after paid turns leaves their rows and money in `run.yml` and saves before it emits. The cost surfaces then agree with each other: a turn whose result was lost to that fault reads as UNMETERED, LOWER BOUND on `tldrx cost` and in the Build handoff through the door other unmeasured turns already use, a turn still in flight reads as nothing, and a late result retires the lost mark instead of counting twice. Replay reads the reason a failed stage recorded instead of printing "no reason recorded" for every one. |
340
342
  | 0.25.0 | 2026-09-14 | `beta` | The other half of the two-session protocol and four holes measured on live runs. `scripts/release.sh` now waits on a running wave's lock before touching the tree and never hands back the marker it wrote — release has precedence, the wave yields, exit 14 when it gives up; the heading gate also refuses an unreleased version at or below `package.json`; the marker carries its phase and `--status` reports a release queued behind a wave (#304). In-session `tldrx next --prepare` and `--commit` ask the same dependency frontier the headless loop asks: a terminal hold records the dependent `blocked`, a pending hold refuses without writing, a held `--commit` keeps the work it sits on and says where it is (#300). A Watch card refused for an unsourced `## Where` item comes back to its writer marked, the refusal names the cure, and the brief shows one non-file example, so a retry edits the card instead of regenerating it blind (#301). And `run.yml` no longer loses an external write to a live loop: a save carries only the fields the store changed over a fresh read, ownership is declared, `cancelled` is terminal and wins in the file and in the process, a file that cannot be merged is kept beside as `.bak` and said so, and a file recording another run is refused (#305). Minor release: a new exit code on a public script, a stricter merge gate, and `--prepare`/`--commit` refuse where they used to proceed. |
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-d0jhr5wh.js";
4
+ } from "./chunk-c62y35wh.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-jpp137mb.js";
8
+ } from "./chunk-48ctk154.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-qzsxhsyj.js";
12
+ } from "./chunk-khjm356r.js";
13
13
  import {
14
14
  EventLog,
15
15
  PHASE_ID_RE
16
- } from "./chunk-41k19q8t.js";
16
+ } from "./chunk-abswp1vt.js";
17
17
  import {
18
18
  PHASE_IDS
19
- } from "./chunk-m1s8a6s0.js";
20
- import"./chunk-bznt6jb2.js";
21
- import"./chunk-ezfvp5kx.js";
19
+ } from "./chunk-veffvzw5.js";
20
+ import"./chunk-g7wz339n.js";
21
+ import"./chunk-zvxgdssb.js";
22
22
  import {
23
23
  ADVISORY_KEY,
24
24
  MAX_FACT_CHARS,
@@ -28,7 +28,7 @@ import {
28
28
  renderQuestionBlock,
29
29
  replaceBlock,
30
30
  serializeQuestions
31
- } from "./chunk-5stgwp3g.js";
31
+ } from "./chunk-khy21x6e.js";
32
32
  import {
33
33
  ITERATION_ONLY_SLOT,
34
34
  PROJECT_FRAMEWORK_DIR,
@@ -38,7 +38,7 @@ import {
38
38
  isScopedTemplate,
39
39
  parseYaml,
40
40
  scopedSlotOf
41
- } from "./chunk-0sy3tmfg.js";
41
+ } from "./chunk-f6t1xc3f.js";
42
42
 
43
43
  // src/hooks/answer-capture.ts
44
44
  import { existsSync as existsSync5 } from "fs";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-ka7768vx.js";
4
+ } from "./chunk-q3d4xdhb.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-t44983nq.js";
12
- import"./chunk-qzsxhsyj.js";
11
+ } from "./chunk-tna1bz8r.js";
12
+ import"./chunk-khjm356r.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -22,10 +22,10 @@ import {
22
22
  validateRunBudget,
23
23
  wouldExceed,
24
24
  wouldExceedHostTokens
25
- } from "./chunk-bbrhs055.js";
25
+ } from "./chunk-xhd77h0p.js";
26
26
  import {
27
27
  EventLog
28
- } from "./chunk-41k19q8t.js";
28
+ } from "./chunk-abswp1vt.js";
29
29
  import {
30
30
  cursorStage,
31
31
  hostTokensIn,
@@ -34,18 +34,18 @@ import {
34
34
  newestActiveRun,
35
35
  renderRunEconomies,
36
36
  runSpend
37
- } from "./chunk-yre2scxn.js";
38
- import"./chunk-bznt6jb2.js";
37
+ } from "./chunk-hws2gnxj.js";
38
+ import"./chunk-g7wz339n.js";
39
39
  import {
40
40
  noteDeprecations
41
- } from "./chunk-5stgwp3g.js";
41
+ } from "./chunk-khy21x6e.js";
42
42
  import {
43
43
  PROJECT_WORK_DIR,
44
44
  findWorkspaceRoot,
45
45
  locateWork,
46
46
  parseYaml,
47
47
  stageYamlPath
48
- } from "./chunk-0sy3tmfg.js";
48
+ } from "./chunk-f6t1xc3f.js";
49
49
 
50
50
  // src/hooks/budget-gate.ts
51
51
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-ezfvp5kx.js";
6
+ } from "./chunk-zvxgdssb.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-5stgwp3g.js";
18
+ } from "./chunk-khy21x6e.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-0sy3tmfg.js";
31
+ } from "./chunk-f6t1xc3f.js";
32
32
 
33
33
  // src/core/facts/FactsStore.ts
34
34
  import { existsSync, readFileSync } from "node:fs";
@@ -2,7 +2,7 @@ import {
2
2
  GATE_POLICIES,
3
3
  validateGatesPolicy,
4
4
  validateStagePolicy
5
- } from "./chunk-bznt6jb2.js";
5
+ } from "./chunk-g7wz339n.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-0sy3tmfg.js";
18
+ } from "./chunk-f6t1xc3f.js";
19
19
 
20
20
  // src/core/events/EventLog.ts
21
21
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -50,6 +50,7 @@ var EVENT_TYPES = [
50
50
  "story.reopened",
51
51
  "story.base_fastforwarded",
52
52
  "story.base_updated",
53
+ "story.conflict_turn",
53
54
  "story.review_retried",
54
55
  "story.work_rescued",
55
56
  "story.touches_widened",
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  findDuplicate
3
- } from "./chunk-jpp137mb.js";
3
+ } from "./chunk-48ctk154.js";
4
4
  import {
5
5
  evidencePath,
6
6
  gateEvidencePath,
7
7
  parseEvidence
8
- } from "./chunk-41k19q8t.js";
8
+ } from "./chunk-abswp1vt.js";
9
9
  import {
10
10
  openBlocks,
11
11
  parseQuestions
12
- } from "./chunk-5stgwp3g.js";
12
+ } from "./chunk-khy21x6e.js";
13
13
 
14
14
  // src/core/distill/distill.ts
15
15
  var CONFLICT_THRESHOLD = 0.6;
@@ -8240,7 +8240,7 @@ function resolveAbsent(ref, ctx, section, claim) {
8240
8240
  if (found === null)
8241
8241
  return OK;
8242
8242
  if (needle !== null)
8243
- return searchForNeedle(found, needle, path);
8243
+ return searchForNeedle(found, needle, path, isWorkspaceFacts(found, ctx));
8244
8244
  const held = whatIsThere(found);
8245
8245
  if (held === null)
8246
8246
  return OK;
@@ -8248,7 +8248,7 @@ function resolveAbsent(ref, ctx, section, claim) {
8248
8248
  }
8249
8249
  var NEEDLE_MAX_BYTES = 512 * 1024;
8250
8250
  var EMPTY_PROBE_MAX_BYTES = 64 * 1024;
8251
- function searchForNeedle(abs, needle, path) {
8251
+ function searchForNeedle(abs, needle, path, factsFile) {
8252
8252
  let stat;
8253
8253
  try {
8254
8254
  stat = statSync2(abs);
@@ -8271,12 +8271,35 @@ function searchForNeedle(abs, needle, path) {
8271
8271
  const lines = text.split(`
8272
8272
  `);
8273
8273
  for (let i = 0;i < lines.length; i++) {
8274
- if ((lines[i] ?? "").toLowerCase().includes(wanted)) {
8274
+ const line = factsFile ? withoutFactProvenance(lines[i] ?? "") : lines[i] ?? "";
8275
+ if (line.toLowerCase().includes(wanted)) {
8275
8276
  return refused(`\`${needle}\` IS at ${path}:${String(i + 1)} — that is a presence, not an absence`, abs);
8276
8277
  }
8277
8278
  }
8278
8279
  return OK;
8279
8280
  }
8281
+ function isWorkspaceFacts(abs, ctx) {
8282
+ return normalize(abs) === normalize(join2(ctx.root, PROJECT_FRAMEWORK_DIR, "memory", "facts.yml"));
8283
+ }
8284
+ var PROVENANCE_KEYS = new Set(["who", "when", "run", "q", "decided_by"]);
8285
+ var SOURCE_OPEN_RE = /(^|[\s,{])source:\s*\{/;
8286
+ var PROVENANCE_ENTRY_RE = /^\s*([a-z_]+):\s*("(?:[^"\\]|\\.)*"|[^,{}"]*?)\s*([,}])/;
8287
+ function withoutFactProvenance(line) {
8288
+ const open = SOURCE_OPEN_RE.exec(line);
8289
+ if (open === null)
8290
+ return line;
8291
+ const start = open.index + (open[1] ?? "").length;
8292
+ let at = open.index + open[0].length;
8293
+ for (;; ) {
8294
+ const entry = PROVENANCE_ENTRY_RE.exec(line.slice(at));
8295
+ if (entry === null || !PROVENANCE_KEYS.has(entry[1] ?? ""))
8296
+ return line;
8297
+ at += entry[0].length;
8298
+ if (entry[3] === "}")
8299
+ break;
8300
+ }
8301
+ return line.slice(0, start) + " ".repeat(at - start) + line.slice(at);
8302
+ }
8280
8303
  function whatIsThere(abs) {
8281
8304
  let stat;
8282
8305
  try {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isRecord
3
- } from "./chunk-0sy3tmfg.js";
3
+ } from "./chunk-f6t1xc3f.js";
4
4
 
5
5
  // src/core/run/stagePolicy.ts
6
6
  function validateStagePolicy(value, stageIds, issues, spec) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  listRunDirs,
3
3
  parseYaml
4
- } from "./chunk-0sy3tmfg.js";
4
+ } from "./chunk-f6t1xc3f.js";
5
5
 
6
6
  // src/hooks/lib/runFile.ts
7
7
  import { existsSync, readFileSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-0sy3tmfg.js";
3
+ } from "./chunk-f6t1xc3f.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {
@@ -3,7 +3,7 @@ import {
3
3
  parseSrcToken,
4
4
  srcToken,
5
5
  withoutSrcToken
6
- } from "./chunk-0sy3tmfg.js";
6
+ } from "./chunk-f6t1xc3f.js";
7
7
 
8
8
  // src/core/text/questions.ts
9
9
  var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
@@ -11,7 +11,9 @@ var HEADING_RE = /^##\s+(Q\d{1,6})\s+·\s+(.+?)\s*$/;
11
11
  var METADATA_RE = /^<!--\s*(.*?)\s*-->$/;
12
12
  var WHY_RE = /^Why asked:\s*(.*)$/;
13
13
  var OPTION_RE = /^-\s+([A-E])\)\s*(.*)$/;
14
- var RECOMMENDED_RE = /^Recommended:[ \t]+([A-E])\)?[ \t]*(?:[—–-][ \t]+(.*))?$/;
14
+ var RECOMMENDED_PREFIX_RE = /^Recommended:/;
15
+ var RECOMMENDED_RE = /^Recommended:[ \t]+([A-E])\)?(?![^ \t])[ \t]*(.*)$/;
16
+ var RECOMMENDED_REASON_RE = /^[—–-](?:[ \t]+(.*))?$/;
15
17
  var ANSWER_RE = /^\[Answer\]:[ \t]*(\S.*)$/;
16
18
  var ANSWER_SLOT_RE = /^\[Answer\]:/;
17
19
  var FOOTER_KEYS = ["answered_by", "answered_at", "fact"];
@@ -55,6 +57,7 @@ function buildBlock(startLine, lines) {
55
57
  let whySrc = null;
56
58
  const options = [];
57
59
  let recommended = null;
60
+ let sawRecommendedLine = false;
58
61
  let answer = "";
59
62
  let answerIndex = -1;
60
63
  let footer = null;
@@ -82,9 +85,10 @@ function buildBlock(startLine, lines) {
82
85
  options.push({ letter: option[1], text: option[2] });
83
86
  continue;
84
87
  }
85
- const recommendation = RECOMMENDED_RE.exec(line);
86
- if (recommendation !== null && recommendation[1] !== undefined && recommended === null) {
87
- recommended = toRecommendation(recommendation[1], recommendation[2] ?? "");
88
+ if (RECOMMENDED_PREFIX_RE.test(line) && !sawRecommendedLine) {
89
+ sawRecommendedLine = true;
90
+ const reading = parseRecommendedLine(line);
91
+ recommended = reading.kind === "ok" ? reading.recommendation : null;
88
92
  continue;
89
93
  }
90
94
  if (ANSWER_SLOT_RE.test(line) && answerIndex === -1) {
@@ -109,13 +113,27 @@ function buildBlock(startLine, lines) {
109
113
  lines: [...lines]
110
114
  };
111
115
  }
112
- function toRecommendation(option, rest) {
113
- const token = parseSrcToken(rest);
114
- return {
115
- option,
116
- why: withoutSrcToken(rest).trim(),
117
- src: token === null ? "" : token.refs.map((ref) => ref.raw).join(SRC_SEPARATOR)
118
- };
116
+ function parseRecommendedLine(line) {
117
+ if (!RECOMMENDED_PREFIX_RE.test(line))
118
+ return { kind: "absent" };
119
+ const match = RECOMMENDED_RE.exec(line.replace(/\s+$/, ""));
120
+ const option = match?.[1];
121
+ if (match === null || option === undefined)
122
+ return { kind: "unreadable", line };
123
+ let rest = (match[2] ?? "").trim();
124
+ const tokens = [];
125
+ for (let token = parseSrcToken(rest);token !== null; token = parseSrcToken(rest)) {
126
+ tokens.unshift(token.refs.map((ref) => ref.raw));
127
+ rest = withoutSrcToken(rest).replace(/[\s.,;:!?]+$/, "").trim();
128
+ }
129
+ let why = "";
130
+ if (rest !== "") {
131
+ const reason = RECOMMENDED_REASON_RE.exec(rest);
132
+ if (reason === null)
133
+ return { kind: "unreadable", line };
134
+ why = (reason[1] ?? "").trim();
135
+ }
136
+ return { kind: "ok", line, recommendation: { option, why, src: tokens.flat().join(SRC_SEPARATOR) } };
119
137
  }
120
138
  function parsePipeComment(inner) {
121
139
  const pairs = [];
@@ -3,12 +3,13 @@ import {
3
3
  DEFAULT_ECONOMY,
4
4
  DEFAULT_ON_GRANT_EXCEED,
5
5
  DEFAULT_ON_HOST_TOKENS_EXCEED,
6
+ WAVES_FILE,
6
7
  asRunBudget,
7
8
  nowRfc3339,
8
9
  spentBasis,
9
10
  tallyOf,
10
11
  validateRunBudget
11
- } from "./chunk-bbrhs055.js";
12
+ } from "./chunk-xhd77h0p.js";
12
13
  import {
13
14
  EventLog,
14
15
  OUTCOME_NOT_RECORDED,
@@ -21,12 +22,12 @@ import {
21
22
  isTerminal,
22
23
  stageAt,
23
24
  validateRunFile
24
- } from "./chunk-41k19q8t.js";
25
+ } from "./chunk-abswp1vt.js";
25
26
  import {
26
27
  cursorStage,
27
28
  isAttendedByHostView,
28
29
  openRunViews
29
- } from "./chunk-yre2scxn.js";
30
+ } from "./chunk-hws2gnxj.js";
30
31
  import {
31
32
  backupPathFor,
32
33
  isAlive,
@@ -35,20 +36,20 @@ import {
35
36
  workspaceRootOfRunDir,
36
37
  writeAtomic,
37
38
  yamlScalar
38
- } from "./chunk-ezfvp5kx.js";
39
+ } from "./chunk-zvxgdssb.js";
39
40
  import {
40
41
  isAdvisory,
41
42
  noteDeprecations,
42
43
  openBlocks,
43
44
  parseQuestions
44
- } from "./chunk-5stgwp3g.js";
45
+ } from "./chunk-khy21x6e.js";
45
46
  import {
46
47
  FRAMEWORK_ROOT,
47
48
  PLAN_STATUSES,
48
49
  listRunDirs,
49
50
  parseYaml,
50
51
  parseYamlRepairing
51
- } from "./chunk-0sy3tmfg.js";
52
+ } from "./chunk-f6t1xc3f.js";
52
53
 
53
54
  // src/core/statusline/runSnapshot.ts
54
55
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
@@ -421,6 +422,11 @@ class RunStore {
421
422
  this.currentBudget = fn(structuredClone(this.currentBudget));
422
423
  this.budgetMutated = true;
423
424
  }
425
+ refreshCeilings() {
426
+ if (this.budgetMutated)
427
+ return;
428
+ this.currentBudget = this.ceilingsToWrite();
429
+ }
424
430
  append(event) {
425
431
  this.events.append(event);
426
432
  }
@@ -621,6 +627,33 @@ var MIN_SLICE_BYTES = 2 * 1024;
621
627
  // src/core/facilitator/contextLedger.ts
622
628
  var DEFAULT_PROMPT_MAX_BYTES = 400 * 1024;
623
629
 
630
+ // src/core/plan/planShape.ts
631
+ var MAX_WAVES_PER_RUN = 2;
632
+ var RUN_SIZE_ISSUES = "#286/#244/#280";
633
+ var WAVE_CAP_REASON_KEY = "wave_cap_reason";
634
+ var PLAN_SHAPE_RULES = [
635
+ {
636
+ 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
+ },
639
+ {
640
+ issue: "#316",
641
+ text: "**Every story runs in the EARLIEST wave its `depends_on` allows.** `depends_on: []` runs in W1; a story " + "whose dependencies all finish in W1 runs in W2. The `plan` check refuses a story held later with nothing " + "making it wait. If something does — two stories editing one file — that is a `depends_on` edge: write it."
642
+ },
643
+ {
644
+ issue: "#317",
645
+ text: "**Each story is a vertical slice.** When its own dod goes green, what it adds is reachable from a route, " + "endpoint, command or job that a user or caller actually hits — not only from its own tests. A component " + "no route renders, or a handler nothing registers, is refused at Build review as unreachable: put the wiring " + "in the same story, and the wiring's file in its `touches`. No check reads this; the review does."
646
+ },
647
+ {
648
+ issue: "#318",
649
+ text: "**Inventory files go in `touches` up front.** Grep for the tests, snapshots and generated files that " + "ENUMERATE what the story adds — route trees, guard tables, allow-lists, architecture or coverage " + "inventories, approved snapshots — and declare them. They change because the story's change forces them " + "to, not because anyone meant to edit them, and each one left out is a `touches` widening at Build."
650
+ },
651
+ {
652
+ issue: "#319",
653
+ text: "**End-to-end coverage is not the last story.** Either a test-harness story runs in W1 and later stories " + "depend on it and run against it, or each UI or feature story carries the e2e command in its own dod. A " + "final e2e story that depends on everything is the story never reached. The `plan` check names a command " + "some stories of one epic and repo carry and a sibling does not — an advisory: say in that story why not."
654
+ }
655
+ ];
656
+
624
657
  // src/core/run/runOutcome.ts
625
658
  import { join as join3 } from "node:path";
626
659
 
@@ -651,6 +684,7 @@ function statusWithOutcome(status, outcome) {
651
684
 
652
685
  // src/core/run/autoGate.ts
653
686
  var AUTO_GATE_ACTOR = "auto";
687
+ var RETRYABLE_CONDITIONS = new Set(["checks", "claim-sources"]);
654
688
  var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
655
689
  var HELD_SEPARATOR = " · ";
656
690
  function heldByNote(note) {
@@ -3,7 +3,7 @@ import {
3
3
  EMPTY_SECTION_RULE,
4
4
  noneBullet,
5
5
  srcRule
6
- } from "./chunk-0sy3tmfg.js";
6
+ } from "./chunk-f6t1xc3f.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-qzsxhsyj.js";
4
+ } from "./chunk-khjm356r.js";
5
5
 
6
6
  // src/hooks/lib/decide.ts
7
7
  function deny(reason) {