tldr-experts 0.25.0 → 0.26.1
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/CHANGELOG.md +164 -0
- package/README.md +2 -0
- package/dist/hooks/answer-capture.js +2 -2
- package/dist/hooks/budget-gate.js +1 -1
- package/dist/hooks/{chunk-tjx8g39f.js → chunk-d0jhr5wh.js} +1 -1
- package/dist/hooks/{chunk-rgn23z77.js → chunk-pwzy19er.js} +1 -1
- package/dist/hooks/session-start.js +3 -3
- package/dist/hooks/statusline.js +2 -2
- package/dist/tldrx.js +256 -46
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- /package/dist/hooks/{chunk-6z5rmj0b.js → chunk-41k19q8t.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,169 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.1 — 2026-09-14
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **A patch is fixes again, and this release is one: 0.26.1, not 0.27.0 (owner decision).**
|
|
8
|
+
MEASURED with `git tag -l`: v0.19.0 through v0.26.0 are eight releases, every one a minor and
|
|
9
|
+
none a patch, while 0.21.0, 0.22.0, 0.23.0, 0.25.0 and 0.26.0 carry nothing but `### Fixed`.
|
|
10
|
+
Mechanism: `docs/RELEASING.md` said "bump minor when a command, file schema or hook changes
|
|
11
|
+
behaviour; patch for fixes only", and every fix changes some command's behaviour — so the
|
|
12
|
+
rule had no fix left to call a patch, and the version number stopped telling an upgrader
|
|
13
|
+
whether a release was safe to take blind. The rule now reads by capability: a *patch* is
|
|
14
|
+
fixes, including a fix that makes a command do what it already promised (a refusal where it
|
|
15
|
+
should have refused, a message that keeps what it should say); a *minor* is a new
|
|
16
|
+
capability — a new command, flag, file field or schema, hook or event, or a changed meaning
|
|
17
|
+
of an existing one. The maintain skill used to gloss the old rule beside its citation, which
|
|
18
|
+
made two copies to move; it now cites `docs/RELEASING.md` "What to consider" only, and
|
|
19
|
+
`test/maintain-skill.test.ts` refuses a skill line that restates what a patch or a minor is
|
|
20
|
+
(RED on the old gloss, which it quoted).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **A Build fix round whose developer lands no diff is refused, not settled `done` (#308).**
|
|
25
|
+
MEASURED live on a client workspace (0.24.0, two `--for-fix` reopens, one Build re-run): the
|
|
26
|
+
developer read the file, said it "already satisfies every acceptance criterion", spent $1.69
|
|
27
|
+
across two spawns and changed nothing — and the stage closed with the story `done`, the epic
|
|
28
|
+
head unchanged and every gap the reopen note named still in the tree. Mechanism, confirmed at
|
|
29
|
+
`f1c9aa8`: `commitIfDirty` hands back the OLD head sha on a clean tree, so `settleHalf`'s "no
|
|
30
|
+
commit to review" gate never fired, `mergeNoFf` was a no-op on an already-merged tip, and the
|
|
31
|
+
ledger closes a fix round on `done` alone. `workSince` — the "did the tree move since the one
|
|
32
|
+
the developer was handed" comparison the two refusal branches already use — was never asked on
|
|
33
|
+
the success path, where a fix round is precisely the case "nothing changed and the DoD is still
|
|
34
|
+
green" looks normal, because that tree was accepted once already. Now, for a story a PERSON put
|
|
35
|
+
back with a note — a fix round or a plain reopen, off the ledger — `buildHalf` asks it BEFORE
|
|
36
|
+
the DoD: no work, and the attempt blocks with the existing `task.done`/`blocked` vocabulary and
|
|
37
|
+
a reason that carries the note's first line (`NO_DIFF_MARK`, exported and pinned) — no DoD paid
|
|
38
|
+
for, no reviewer asked to judge an empty diff, the fix round still open because nothing landed.
|
|
39
|
+
A first attempt that lands nothing is deliberately NOT gated: "added nothing — identical to the
|
|
40
|
+
epic" is a pinned rendering decision, not this defect. Three RED tests in
|
|
41
|
+
`test/story-reopen.test.ts` (no-write developer on a fix round, the same bytes rewritten, a
|
|
42
|
+
no-write developer on a plain reopen — each settled `done` before); the neighbouring fix-round
|
|
43
|
+
tests now land a REAL diff, because the fake developer's default rewrote the bytes the first
|
|
44
|
+
attempt committed, which is #308's shape exactly. Golden byte-identical.
|
|
45
|
+
- **`tldrx ship` refuses an epic carrying a story the reviewer rejected (#282).** MEASURED on a
|
|
46
|
+
live unattended run (0.18.2, 8 stories), story S5: DoD green, `48f8bdd merge(S5)` on the epic,
|
|
47
|
+
THEN `check.failed check:review verdict:changes`; the requeue did the same (`7fd2468 merge(S5)`,
|
|
48
|
+
`changes` again) and the story settled `blocked` — the epic carried, twice, code the reviewer
|
|
49
|
+
rejected twice. Merging BEFORE the review is design (#166: the reviewer reads the merged diff from
|
|
50
|
+
the epic base) and every verdict settles with `merged: true`; the defect was downstream. `ship`'s
|
|
51
|
+
only story refusal was #210's "zero stories done", so with one `done` story beside S5 it pushed
|
|
52
|
+
the epic and opened the PR with the rejected code in its diff, while the body listed S5 under
|
|
53
|
+
"Not done" with no word that the diff was there — a record lying in the dangerous direction (§7).
|
|
54
|
+
Reproduced at `bdc43e7` with the fake agent (`GOLDEN_ROUNDS`, S2 scripted `changes` twice): S1
|
|
55
|
+
`done`, S2 `blocked`, every S2 `task.done` commit an ancestor of `epic/e1`, and `ship --dry-run`
|
|
56
|
+
exited 0 printing the `gh pr create` line. Now `ship` asks the ledger's `lastMerge` (#295 — the
|
|
57
|
+
last `task.done` carrying `commit` and `epic_base`, which survives a reopen precisely so "what
|
|
58
|
+
does the epic hold" can be asked) for every story not `done`, and a `changes` there is a
|
|
59
|
+
rejection that stands: exit 2 — a gate DID say no, unlike #210's 1 — naming the story, the merge
|
|
60
|
+
and the handoff's own reason, with #210's remedy (`tldrx story reopen <id> --note`, re-run Build,
|
|
61
|
+
or open the PR by hand). Not widened to a merged diff nobody judged (`n-a`/`error`) — that is #311,
|
|
62
|
+
filed from this change. Guards, all passing before: a `changes` a later attempt merged over and got approved
|
|
63
|
+
ships; a story whose diff never merged ships; a story parked at `review` under a SIGNED fix list
|
|
64
|
+
ships with its findings listed. Two mutations of the predicate each redden a different test.
|
|
65
|
+
Golden byte-identical: nothing in Build changed. The unattended path was already closed one
|
|
66
|
+
door earlier — the auto gate refuses to self-sign over any story not `done`
|
|
67
|
+
(`autoGate.ts` `UNFINISHED_STORIES`, pinned in `test/build-executor.test.ts`) — so this reaches
|
|
68
|
+
a PR only after a person approved the Build gate, which is exactly when the record has to say
|
|
69
|
+
what the diff contains.
|
|
70
|
+
|
|
71
|
+
- **A stage-failure line keeps the branch it exists to name when the worktree path is wider than
|
|
72
|
+
the line (#310, #293).** MEASURED 2026-09-14 under a ~115-char session scratchpad, the very place
|
|
73
|
+
`AGENTS.md §2` sends every agent to work: the epic-worktree refusal put the absolute path FIRST
|
|
74
|
+
and the two branch names after it, `oneLine` kept the first 220 chars, and the operator line —
|
|
75
|
+
and `run.yml`'s task row, and the `stage.failed` event — read `epic worktree /private/tmp/…/
|
|
76
|
+
_epic-260829-build-E1 is checked out on \`epic/someb…`: the branch cut two characters in, while
|
|
77
|
+
the same test passed on the same sha under a short `TMPDIR`. A refusal that loses its subject
|
|
78
|
+
exactly when the path is long is a refusal that names the wrong thing when it matters most.
|
|
79
|
+
Two halves, one implementation each: `assertWorktreeOn` now says the branches first and the
|
|
80
|
+
path last (`… is checked out on \`X\`, not \`Y\` — refusing … — at <path>`), and `oneLine`'s
|
|
81
|
+
cap keeps a line's head AND its tail, losing the middle — so what a cut takes is the middle of
|
|
82
|
+
a directory whose root the operator already knows. The cap itself stays at 220: each of the
|
|
83
|
+
three records is still one bounded line. `test/build-executor.test.ts` builds the refusal under
|
|
84
|
+
a path wider than the cap on purpose (the fixture roots in `os.tmpdir()`, which reads
|
|
85
|
+
`TMPDIR` at call time) and pins both branches, the worktree's own directory and the bound in
|
|
86
|
+
all three records — RED on the old order (only the path and `…` survived), and each half
|
|
87
|
+
reverted alone reddens it on its own assertion; the unit test on `assertWorktreeOn` pins the
|
|
88
|
+
order. The existing WRONG-branch test asserts the worktree's relative directory rather than
|
|
89
|
+
the whole absolute path, so it no longer depends on how long the box's `TMPDIR` is.
|
|
90
|
+
|
|
91
|
+
## 0.26.0 — 2026-09-14
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **`tldrx replay` says what a failed stage died of, instead of `FAILED: no reason recorded` for
|
|
96
|
+
every one (#309).** MEASURED by inspection at `03d53e8`: the ONLY writer of `stage.failed` in the
|
|
97
|
+
tree — and, per `git log -S`, in its whole history — carries the failure as `payload.reason`
|
|
98
|
+
(`runNext.ts`, the fail path; the same field `stage.skipped` uses, and the same field `run auto`'s
|
|
99
|
+
own summary reads). The narrative renderer read `payload.error`, a field nothing ever set, so
|
|
100
|
+
`text()` was always empty and the fallback always fired — a stakeholder reading a replay saw
|
|
101
|
+
that a stage failed and never why, while the reason sat one field over in the same line of
|
|
102
|
+
`events.jsonl`. Same class as the `message`/`detail` drift #249's round-1 review found:
|
|
103
|
+
payloads are untyped and nothing checks a reader's field name against its writer's. The
|
|
104
|
+
renderer now reads `reason`, like its sibling one line down; the `FAILED: ` marker and the
|
|
105
|
+
fallback sentence are exported constants, and `test/replay.test.ts` feeds a hand-built
|
|
106
|
+
`stage.failed` event and pins the rendered line to the reason it carried (RED on the old read:
|
|
107
|
+
the line was never found; the fallback case is a guard). No record changes shape — nothing
|
|
108
|
+
wrote `error`, so there is no old record to tolerate.
|
|
109
|
+
|
|
110
|
+
- **A wide story's measured widening no longer kills the invocation that earned it, and an executor that
|
|
111
|
+
throws after paid turns leaves their rows and their money in `run.yml` (#249).** MEASURED on a live 0.24.0
|
|
112
|
+
field run: a story with 16 declared touches changed 32 files, and the `story.touches_widened` the framework
|
|
113
|
+
measures at settle carried `paths`, `before` and `after` — three unbounded path lists, 5556 bytes against
|
|
114
|
+
the 4096-byte cap. `capPayload` knew `detail` and `outputs` by name and nothing else, so `EventLog.append`
|
|
115
|
+
threw; `measureSurface`'s "advisory, never throws" contract guarded the git read and not the emit, so the
|
|
116
|
+
throw went out through `settle` — after the story's `done` was on disk and before its `task.done` was in
|
|
117
|
+
the ledger — into `runNext`'s catch, which failed the stage with `tasks_recorded: false`: the developer's
|
|
118
|
+
and the reviewer's paid turns never reached `run.yml`, and every ceiling that derives from recorded spend
|
|
119
|
+
was short by exactly that, so the next economy refusal that should have fired would not have. Three fixes,
|
|
120
|
+
one per seam. The cap seam knows the three lists BY NAME in one declared table (`DROPPABLE_LISTS`):
|
|
121
|
+
`after` first (the largest, and derivable from the other two), then `before`, then `paths` last — each
|
|
122
|
+
dropped WHOLE and only while the payload is still over, each replaced by `<field>_omitted: N` and the same
|
|
123
|
+
reason-and-pointer sentence `outputs_omitted` carries, never a shortened list, which would read downstream
|
|
124
|
+
as the whole one; a `paths_omitted` a writer already put there (`worktree.foreign_work_aside`) is added
|
|
125
|
+
to, not overwritten. Every reader — `wideningRows`, the Build handoff, `tldrx replay` — renders
|
|
126
|
+
`(16 → 29 path(s))` from ONE derivation, `listCount`, so a row the cap could not carry reads the same
|
|
127
|
+
everywhere. `measureSurface` never throws: a refused emit records a bounded absence on the same event (the
|
|
128
|
+
counts, and a `note` naming the failure), and a log that refuses even that says so on stderr while the
|
|
129
|
+
story still settles. And the structural half, the expensive one: the Build executor carries the rows it
|
|
130
|
+
had already earned ON the error it throws (`withPartialTasks` — data on the same object, never a wrapper,
|
|
131
|
+
so what threw is still what the caller sees), and `runNext`'s catch records them through the same
|
|
132
|
+
`recordExecutorTasks` the return path uses BEFORE it fails the stage: the `error` event says
|
|
133
|
+
`tasks_recorded: true` with `rows_written`/`rows_expected` measured off the store after the write, the
|
|
134
|
+
rows keep their own `done` rather than being repainted with a throw they did not produce, and
|
|
135
|
+
`budget.spent_usd` is what was paid. Pinned by a real fake-agent build whose settle throws after both turns:
|
|
136
|
+
MEASURED on that case, the ledger before the fix recorded 0 rows / $0.00 for two paid turns, and after it
|
|
137
|
+
records 2 rows / $0.50. Which is the sentence an operator needs after this release: the cost half does not
|
|
138
|
+
just stop losing money, it stops LYING in favour of spend. With paid turns now recorded, ceilings bite
|
|
139
|
+
earlier — a run that used to reach the end because the ledger counted short may now be refused mid-way,
|
|
140
|
+
correctly but differently, so a new economy refusal after upgrading is this change working, not a
|
|
141
|
+
regression to hunt elsewhere. Pre-merge review found the double fault behind that: when recording the
|
|
142
|
+
carried rows itself threw (an `agent.result` the cap cannot rescue), rows and events were interleaved, so
|
|
143
|
+
a throw at event k stranded rows k+1… in memory and nothing the spend surfaces read said so — a plausible
|
|
144
|
+
short total, worse than a loud zero — and the `error` event was emitted BEFORE the save with a free-text
|
|
145
|
+
`recording_error` the cap did not trim, so a long one could throw the seam and lose the rows with no
|
|
146
|
+
trace. Now every row goes into the store before any event is appended (the in-memory row write cannot
|
|
147
|
+
throw; the append is the only line that can), the store is saved before the `error` event is emitted and
|
|
148
|
+
that emit is wrapped, `recording_error` sits in the cap's prose table beside `detail`, and no new
|
|
149
|
+
vocabulary reaches `budget show`, the dashboard or status: `run.yml` is whole, so they read a
|
|
150
|
+
measurement. Pre-merge review round 2 found the events side was NOT labelled — `tldrx cost --stories`
|
|
151
|
+
reads only `agent.result` events, so a story the fault left un-evented came back with no measured cost
|
|
152
|
+
and zero unmetered turns, a silent null at odds with the `budget.spent_usd` run.yml records. Now the
|
|
153
|
+
story ledger walks the log in order: a spawn opens a slot for its story, a result closes one (or, arriving
|
|
154
|
+
late from an orphaned agent after its slot was already counted lost, retires that lost slot — a result is
|
|
155
|
+
proof the turn was metered whenever it lands, never both the dollars and a lost turn), and an
|
|
156
|
+
invocation-terminal event on that stage (`stage.done`/`failed`/`skipped`, the executor or record-tasks
|
|
157
|
+
`error`, or a later `stage.started` that superseded it) turns every slot still open into a LOST turn,
|
|
158
|
+
which enters the SAME unmetered/lower-bound door: the story reads as a LOWER BOUND with the turn count,
|
|
159
|
+
not a confident zero (the dollars are in run.yml; a task row carries no story key, so they are named
|
|
160
|
+
absent per story, never invented). A spawn with nothing terminal after it is a turn IN FLIGHT — every
|
|
161
|
+
healthy mid-run read of `tldrx cost` and of the handoff — and is counted as nothing, review round 3's
|
|
162
|
+
finding: `spawned − accounted` alone had flagged every live build as a lower bound. Events carry no
|
|
163
|
+
attempt id, so the log's own order is the attempt scope. `tldrx replay`
|
|
164
|
+
renders the error line from `detail` (it read `message`, a field the event never carried) and reports
|
|
165
|
+
`rows_written` of `rows_expected` — reports, never sums.
|
|
166
|
+
|
|
3
167
|
## 0.25.0 — 2026-09-14
|
|
4
168
|
|
|
5
169
|
### Fixed
|
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.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
|
+
| 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. |
|
|
338
340
|
| 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. |
|
|
339
341
|
| 0.24.0 | 2026-09-14 | `beta` | Five fixes from one unattended night, all measured on live runs. The two-session protocol is mechanical where it was chat: the merge wave refuses a merged tree with two unreleased CHANGELOG headings or one at or below the last release (exit 12), `release.sh` holds a `.RELEASE-IN-PROGRESS` marker the wave waits on (exit 13 when it gives up), `merge-wave.sh --status` reads holder, branch and phase in one line, and the `maintain` skill gains a worker-mode section (#299). A per-story cap says what it was derived from — plan price × stage scale — and names the lever that moves it, and a plan whose prices exceed the stage budget is told so instead of being scaled in silence (#281). A story whose dependency is still at `review` waits in `todo` instead of being written `blocked`, and a dependency block left by an earlier version is released once the dependency is `done` (#280). `--until-done` compares the refusal across attempts, not the advice line printed under it, so a failure that made progress keeps its relaunches (#297). And the Build handoff no longer fails its own `claim-sources` check over a refused command that spans lines: one physical line per element, the break shown as `⏎` and explained once in the document (#283). Minor release: the wave and `--until-done` behave differently in situations that exist today. |
|
|
340
342
|
| 0.23.0 | 2026-09-14 | `beta` | `tldrx story reopen --as-is` now settles a story whose work is already merged into the epic but whose review never completed (`n-a` or `error` on its last recorded merge): a new, named review-only case that merges nothing and only routes the story to its reviewer, read from an additive ledger field; the existing refusal ("no commit the epic has not got") is untouched and now names the standing verdict when one exists (`changes`, `approve`, `fixlist` still refuse). A fix list whose findings are all deferred settles the story `done` instead of spawning a developer with nothing to fix, measured on a live run that paid for four such developers (#295). And a turn that died on a provider limit no longer records `success` borrowed from the provider's own subtype: the failure record says what the host saw (#296, first half — DETECTING the provider limit as its own non-execution kind is #298 and is NOT in this release). Minor release: `story reopen --as-is` behaves differently in a situation that exists today. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
conflictOf
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-d0jhr5wh.js";
|
|
5
5
|
import {
|
|
6
6
|
FactsStore,
|
|
7
7
|
formatJaccard
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
EventLog,
|
|
15
15
|
PHASE_ID_RE
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-41k19q8t.js";
|
|
17
17
|
import {
|
|
18
18
|
PHASE_IDS
|
|
19
19
|
} from "./chunk-m1s8a6s0.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
questionsCard
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-d0jhr5wh.js";
|
|
5
5
|
import"./chunk-jpp137mb.js";
|
|
6
6
|
import {
|
|
7
7
|
allow,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
runSnapshot,
|
|
21
21
|
statusWithOutcome,
|
|
22
22
|
whatIsWaiting
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-pwzy19er.js";
|
|
24
24
|
import {
|
|
25
25
|
expertsDir,
|
|
26
26
|
loadExperts,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "./chunk-bbrhs055.js";
|
|
31
31
|
import {
|
|
32
32
|
isFinished
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-41k19q8t.js";
|
|
34
34
|
import"./chunk-m1s8a6s0.js";
|
|
35
35
|
import {
|
|
36
36
|
openRunViews
|
package/dist/hooks/statusline.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
bar,
|
|
4
4
|
runSnapshot
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-pwzy19er.js";
|
|
6
6
|
import"./chunk-bbrhs055.js";
|
|
7
|
-
import"./chunk-
|
|
7
|
+
import"./chunk-41k19q8t.js";
|
|
8
8
|
import"./chunk-m1s8a6s0.js";
|
|
9
9
|
import"./chunk-yre2scxn.js";
|
|
10
10
|
import"./chunk-bznt6jb2.js";
|
package/dist/tldrx.js
CHANGED
|
@@ -7362,24 +7362,34 @@ function capPayload(payload, save) {
|
|
|
7362
7362
|
const where = (text, field) => save === undefined ? "the full text was not preserved and is not recoverable from this event" : save(text, field);
|
|
7363
7363
|
const named = (size, text, field) => `${String(size)} bytes exceeds the ${String(MAX_PAYLOAD_BYTES)}-byte cap — ${where(text, field)}`;
|
|
7364
7364
|
let current = payload;
|
|
7365
|
-
|
|
7366
|
-
const
|
|
7367
|
-
|
|
7365
|
+
for (const field of DROPPABLE_PROSE) {
|
|
7366
|
+
const dropped = current[field];
|
|
7367
|
+
if (typeof dropped !== "string")
|
|
7368
|
+
continue;
|
|
7369
|
+
const { [field]: _gone, ...rest } = current;
|
|
7370
|
+
current = { ...rest, [`${field}_omitted`]: named(byteSize(current), dropped, field) };
|
|
7368
7371
|
if (byteSize(current) <= MAX_PAYLOAD_BYTES)
|
|
7369
7372
|
return current;
|
|
7370
7373
|
}
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
+
for (const field of DROPPABLE_LISTS) {
|
|
7375
|
+
if (byteSize(current) <= MAX_PAYLOAD_BYTES)
|
|
7376
|
+
return current;
|
|
7377
|
+
const list = current[field];
|
|
7378
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
7379
|
+
continue;
|
|
7380
|
+
const { [field]: dropped, ...rest } = current;
|
|
7381
|
+
const already = current[`${field}_omitted`];
|
|
7374
7382
|
current = {
|
|
7375
7383
|
...rest,
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
`),
|
|
7384
|
+
[`${field}_omitted`]: list.length + (typeof already === "number" ? already : 0),
|
|
7385
|
+
[`${field}_omitted_reason`]: named(byteSize(current), dropped.map((p) => String(p)).join(`
|
|
7386
|
+
`), field)
|
|
7379
7387
|
};
|
|
7380
7388
|
}
|
|
7381
7389
|
return current;
|
|
7382
7390
|
}
|
|
7391
|
+
var DROPPABLE_LISTS = ["outputs", "after", "before", "paths"];
|
|
7392
|
+
var DROPPABLE_PROSE = ["recording_error", "detail"];
|
|
7383
7393
|
function byteSize(payload) {
|
|
7384
7394
|
return Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
7385
7395
|
}
|
|
@@ -16563,6 +16573,7 @@ var ENTRIES = [
|
|
|
16563
16573
|
"It pushes ONLY when the run says so. Publishing a branch is a decision, so by default a branch the remote has not seen is a refusal that names the `git push` command rather than running it. The decision is taken once, at `tldrx run new --ship <push|pr|merge>` (#253): then this verb pushes the epic first, through the one push wrapper in the codebase (the Build phase itself still has none, spec §5), and under `merge` arms `gh pr merge --auto --merge` after the PR opens — unless the PR reports NO check at all, or the BASE branch was not seen to require one (#274: `--auto` waits on the base's requirements, not on the PR's reported checks, so over a base that requires nothing it merges at once), in which case the PR is left open and run.yml says which absence it was. `run auto` calls this verb when it sees the run close and skips a run whose record already carries `shipped_at`; typing `tldrx ship` again is the recovery after a partial failure — a repo whose merge is recorded `failed` is armed again, one recorded `queued` is left alone, and the record is the union, so a recorded failure is never erased. A run without the block is never written.",
|
|
16564
16574
|
"The body is WRITTEN for a PR (#167): an **Outcome** line saying how much of the plan landed (`partial: 1 of 3 stories delivered; S2 — …`); what shipped and what did not, from the handoff's own done/not-done split; the reviewer findings still open, read from the run's fix lists; and the LAST phase handoff the run has on disk — `04-build/handoff.md` on a run that built something — verbatim and complete, inside a `<details>` block. It goes to `gh` as a file, never as an argument, so a long body cannot overflow an argv limit.",
|
|
16565
16575
|
"It REFUSES a run that delivered no story (exit 1 — nothing behind it, #210). `ship` already knew: the body it renders keeps only the handoff bullets that say `done`, and with none it wrote `(nothing settled `done` in this run)` into a PR it opened anyway. The refusal names the counts and the first blocked story's own reason, and `--dry-run` is refused in the same words. A run that delivered at least one story ships exactly as it did before.",
|
|
16576
|
+
"It REFUSES an epic carrying a story the reviewer REJECTED (exit 2 — a gate said no, #282). A story merges into its epic BEFORE its review, by design, so a `changes` verdict leaves the diff on the branch: with one story `done` beside it the PR opened with the rejected code in its diff and a body that listed the story under \"Not done\" without saying so. The refusal reads the ledger's last merge for every story not `done` — the last `task.done` carrying `commit` and `epic_base` — and names the story, that merge and the handoff's own reason. A `changes` a later attempt merged over and got approved is not a rejection, a story whose diff never merged has nothing to refuse, and a story parked at `review` under a SIGNED fix list ships with its findings listed. `--dry-run` is refused in the same words.",
|
|
16566
16577
|
"It moves no gate and no cursor and appends no event. On a run with no `ship:` block it never writes to the run at all; on a run with one it writes exactly that block's record (`pr_urls`, `merge`, `merges`, `shipped_at`) and nothing else. To mirror the plan's epics and stories to a ticket tool, `tldrx tickets sync` is the verb that does that, and it stays separate.",
|
|
16567
16578
|
"It refuses cleanly, in a sentence, when there is no epic branch, no handoff, no remote, no `gh` on PATH, or when several epic branches leave the choice open.",
|
|
16568
16579
|
"When the branch exists in SEVERAL repos — the normal shape of a chained multi-repo run, whose epics share one integration branch — it opens one PR per repo: the same body, the repo name in the title, and every URL listed at the end. `--repo` narrows it to one.",
|
|
@@ -25898,7 +25909,7 @@ async function assertWorktreeOn(path, branch, what = "worktree") {
|
|
|
25898
25909
|
const on = await currentBranch(path);
|
|
25899
25910
|
if (on === branch)
|
|
25900
25911
|
return;
|
|
25901
|
-
throw new WorktreeBranchMismatchError(`${what}
|
|
25912
|
+
throw new WorktreeBranchMismatchError(`${what} is checked out on \`${on === "" ? "(no branch)" : on}\`, not \`${branch}\` — ` + `refusing to touch it, because a merge here would land on another run's branch — at ${path}`);
|
|
25902
25913
|
}
|
|
25903
25914
|
async function shaReachability(cwd, sha, ref) {
|
|
25904
25915
|
const resolved = await git(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`], cwd);
|
|
@@ -26562,6 +26573,11 @@ var AS_IS_MARK = "the branch was taken AS IT STANDS — no developer was spawned
|
|
|
26562
26573
|
var AS_IS_NOT_AHEAD_MARK = "nothing to take as it stands";
|
|
26563
26574
|
var AS_IS_JUDGED_MARK = "the last review of that work stands";
|
|
26564
26575
|
var AS_IS_REVIEW_ONLY_MARK = "the review was run AS IT STANDS — nothing was merged and no developer was spawned";
|
|
26576
|
+
var NO_DIFF_MARK = "the developer produced no diff";
|
|
26577
|
+
function noDiffAfterReopenReason(parts) {
|
|
26578
|
+
const round4 = parts.fix ? "the fix round" : "the reopen";
|
|
26579
|
+
return `${NO_DIFF_MARK}: the tree is the one it was handed (\`${parts.handed.slice(0, 7)}\`), ` + `and ${round4} ${parts.actor} signed says \`${firstLine7(parts.note)}\`. ` + "Nothing changed, so nothing could have landed it — no DoD ran and no reviewer was spawned. " + "Reopen it with a note that names what the developer has to change";
|
|
26580
|
+
}
|
|
26565
26581
|
function asIsNotAheadReason(branch, epicBranch, ahead, judged) {
|
|
26566
26582
|
if (ahead === null) {
|
|
26567
26583
|
return `${AS_IS_NOT_AHEAD_MARK}: git could not count what \`${branch}\` carries beyond ` + `\`${epicBranch}\`, and a settlement that spawns no developer will not merge on a ` + "measurement it does not have. Nothing was merged";
|
|
@@ -30038,8 +30054,9 @@ function wideningBullets(rows) {
|
|
|
30038
30054
|
for (const row2 of rows.slice(0, MAX_WIDENING_BULLETS)) {
|
|
30039
30055
|
const named = row2.paths.slice(0, NAMED_WIDENED_PATHS).map((path) => `\`${path}\``).join(", ");
|
|
30040
30056
|
const more = row2.paths.length > NAMED_WIDENED_PATHS ? `, +${String(row2.paths.length - NAMED_WIDENED_PATHS)} more` : "";
|
|
30057
|
+
const unlisted = (row2.pathsOmitted ?? 0) > 0 ? `${row2.paths.length === 0 ? "" : ", "}${String(row2.pathsOmitted)} path(s) not listed on the event` : "";
|
|
30041
30058
|
const opening = row2.basis === "measured" ? `${row2.story}'s declared surface was under the work it did (measured)` : `${row2.story}'s surface was widened by a person (declared)`;
|
|
30042
|
-
out.push(`- ${opening}: +${named}${more} ` + `(${String(row2.before)} → ${String(row2.after)} path(s))` + `${row2.note === "" ? "" : ` — ${row2.note}`} [src: events.jsonl:${String(row2.line)}]`);
|
|
30059
|
+
out.push(`- ${opening}: +${named}${more}${unlisted} ` + `(${String(row2.before)} → ${String(row2.after)} path(s))` + `${row2.note === "" ? "" : ` — ${row2.note}`} [src: events.jsonl:${String(row2.line)}]`);
|
|
30043
30060
|
}
|
|
30044
30061
|
if (rows.length > MAX_WIDENING_BULLETS) {
|
|
30045
30062
|
const next = rows[MAX_WIDENING_BULLETS]?.line ?? 1;
|
|
@@ -35798,6 +35815,11 @@ var MEASURED_NOTE_TAIL = " — measured off the story's own diff, the range the
|
|
|
35798
35815
|
function basisOf(payload) {
|
|
35799
35816
|
return payload.basis === "measured" ? "measured" : "declared";
|
|
35800
35817
|
}
|
|
35818
|
+
function listCount(payload, field) {
|
|
35819
|
+
const list = payload[field];
|
|
35820
|
+
const omitted = payload[`${field}_omitted`];
|
|
35821
|
+
return (Array.isArray(list) ? list.length : 0) + (typeof omitted === "number" ? omitted : 0);
|
|
35822
|
+
}
|
|
35801
35823
|
function stringList3(value) {
|
|
35802
35824
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
35803
35825
|
}
|
|
@@ -35822,8 +35844,9 @@ function wideningRows(eventsText) {
|
|
|
35822
35844
|
story: typeof payload.story === "string" ? payload.story : "?",
|
|
35823
35845
|
basis: basisOf(payload),
|
|
35824
35846
|
paths: stringList3(payload.paths),
|
|
35825
|
-
|
|
35826
|
-
|
|
35847
|
+
...typeof payload.paths_omitted === "number" && payload.paths_omitted > 0 ? { pathsOmitted: payload.paths_omitted } : {},
|
|
35848
|
+
before: listCount(payload, "before"),
|
|
35849
|
+
after: listCount(payload, "after"),
|
|
35827
35850
|
note: typeof payload.note === "string" ? payload.note : "",
|
|
35828
35851
|
line: i + 1
|
|
35829
35852
|
});
|
|
@@ -38830,6 +38853,9 @@ function storyLedger(runDir2, scope = null, extraTurns = []) {
|
|
|
38830
38853
|
const ceilings = new Map;
|
|
38831
38854
|
const measured = new Map;
|
|
38832
38855
|
const unmetered = new Map;
|
|
38856
|
+
const open = new Map;
|
|
38857
|
+
const openStage = new Map;
|
|
38858
|
+
const lost = new Map;
|
|
38833
38859
|
const storyKeys = new Set;
|
|
38834
38860
|
const storyPhases = new Set;
|
|
38835
38861
|
let excluded = 0;
|
|
@@ -38878,6 +38904,39 @@ function storyLedger(runDir2, scope = null, extraTurns = []) {
|
|
|
38878
38904
|
else
|
|
38879
38905
|
measured.set(turn.key, round9((measured.get(turn.key) ?? 0) + turn.costUsd));
|
|
38880
38906
|
}
|
|
38907
|
+
for (const event4 of events) {
|
|
38908
|
+
if (event4.type === "agent.spawned" && inScope(event4)) {
|
|
38909
|
+
const story = str10(event4.payload.story);
|
|
38910
|
+
if (story === null)
|
|
38911
|
+
continue;
|
|
38912
|
+
open.set(story, (open.get(story) ?? 0) + 1);
|
|
38913
|
+
openStage.set(story, event4.stage);
|
|
38914
|
+
continue;
|
|
38915
|
+
}
|
|
38916
|
+
if (event4.type === "agent.result" && inScope(event4)) {
|
|
38917
|
+
const key = str10(event4.payload.key);
|
|
38918
|
+
if (key === null || !isStory(key, str10(event4.payload.phase)))
|
|
38919
|
+
continue;
|
|
38920
|
+
const opened = open.get(key) ?? 0;
|
|
38921
|
+
if (opened > 0)
|
|
38922
|
+
open.set(key, opened - 1);
|
|
38923
|
+
else if ((lost.get(key) ?? 0) > 0)
|
|
38924
|
+
lost.set(key, (lost.get(key) ?? 0) - 1);
|
|
38925
|
+
continue;
|
|
38926
|
+
}
|
|
38927
|
+
if (!closesAttempt(event4))
|
|
38928
|
+
continue;
|
|
38929
|
+
for (const [story, count] of open) {
|
|
38930
|
+
if (count === 0 || openStage.get(story) !== event4.stage)
|
|
38931
|
+
continue;
|
|
38932
|
+
lost.set(story, (lost.get(story) ?? 0) + count);
|
|
38933
|
+
open.set(story, 0);
|
|
38934
|
+
}
|
|
38935
|
+
}
|
|
38936
|
+
for (const [story, count] of lost) {
|
|
38937
|
+
if (count > 0)
|
|
38938
|
+
unmetered.set(story, (unmetered.get(story) ?? 0) + count);
|
|
38939
|
+
}
|
|
38881
38940
|
return {
|
|
38882
38941
|
rows: order.map((story) => ({
|
|
38883
38942
|
story,
|
|
@@ -38889,6 +38948,14 @@ function storyLedger(runDir2, scope = null, extraTurns = []) {
|
|
|
38889
38948
|
events: events.length
|
|
38890
38949
|
};
|
|
38891
38950
|
}
|
|
38951
|
+
function closesAttempt(event4) {
|
|
38952
|
+
if (event4.type === "stage.started" || event4.type === "stage.done" || event4.type === "stage.failed" || event4.type === "stage.skipped")
|
|
38953
|
+
return true;
|
|
38954
|
+
if (event4.type !== "error")
|
|
38955
|
+
return false;
|
|
38956
|
+
const where = str10(event4.payload.where);
|
|
38957
|
+
return where === "executor" || where === "record-tasks";
|
|
38958
|
+
}
|
|
38892
38959
|
function buildStoryCost(runDir2) {
|
|
38893
38960
|
let title = "";
|
|
38894
38961
|
let runId = basename13(runDir2);
|
|
@@ -39006,7 +39073,7 @@ function storyVerdict(cost) {
|
|
|
39006
39073
|
return "no total: a story above is missing one side, and an absent figure is not summed as zero.";
|
|
39007
39074
|
}
|
|
39008
39075
|
if (floor) {
|
|
39009
|
-
return `no verdict: ${turns} unmetered, so at least one story's measurement is a
|
|
39076
|
+
return `no verdict: ${turns} unmetered, so at least one story's measurement is a ${LOWER_BOUND_MARK} ` + "— an unmetered turn could put it over a spawn ceiling this cannot see.";
|
|
39010
39077
|
}
|
|
39011
39078
|
return "every story measured inside the spawn ceilings it was given.";
|
|
39012
39079
|
}
|
|
@@ -39035,7 +39102,7 @@ function renderStoryCost(cost) {
|
|
|
39035
39102
|
lines.push(" measured not recorded — no metered `agent.result` carried a cost for that story. " + "A turn this process never metered is a cost it did not see, never a free one.");
|
|
39036
39103
|
}
|
|
39037
39104
|
if (cost.unmeteredStories.length > 0) {
|
|
39038
|
-
lines.push(` ${cost.unmeteredStories.join(", ")}: ${plural3(cost.unmeteredTurns, "turn")} UNMETERED, so ` +
|
|
39105
|
+
lines.push(` ${cost.unmeteredStories.join(", ")}: ${plural3(cost.unmeteredTurns, "turn")} UNMETERED, so ` + `that measurement is a ${LOWER_BOUND_MARK}, not a total.`);
|
|
39039
39106
|
}
|
|
39040
39107
|
if (cost.excludedKeyedResults > 0)
|
|
39041
39108
|
lines.push(` ${excludedLine(cost)}`);
|
|
@@ -39104,6 +39171,7 @@ function readBudget(runDir2) {
|
|
|
39104
39171
|
var ECONOMY_WIDTH = 12;
|
|
39105
39172
|
var DASH = "—";
|
|
39106
39173
|
var NOT_RECORDED = "not recorded";
|
|
39174
|
+
var LOWER_BOUND_MARK = "LOWER BOUND";
|
|
39107
39175
|
function tokenColumns(t, declared2 = 0) {
|
|
39108
39176
|
const measured = `${bigTokens(t.input)} in · ${bigTokens(t.output)} out · ` + `${bigTokens(t.cacheCreation)} cache write · ${bigTokens(t.cacheRead)} cache read`;
|
|
39109
39177
|
if (declared2 <= 0)
|
|
@@ -39287,7 +39355,7 @@ async function buildExecutor(ctx) {
|
|
|
39287
39355
|
try {
|
|
39288
39356
|
await session.restoreForeignWorkAside();
|
|
39289
39357
|
} catch {}
|
|
39290
|
-
throw error;
|
|
39358
|
+
throw withPartialTasks(error, session.tasks);
|
|
39291
39359
|
}
|
|
39292
39360
|
}
|
|
39293
39361
|
function refusedOnBase(session, error) {
|
|
@@ -40024,6 +40092,26 @@ class BuildSession {
|
|
|
40024
40092
|
before
|
|
40025
40093
|
};
|
|
40026
40094
|
}
|
|
40095
|
+
const reopen = this.reopenFor(planned);
|
|
40096
|
+
if (reopen !== null) {
|
|
40097
|
+
const proven = await workSince({
|
|
40098
|
+
workspaceRoot: this.workspace.root,
|
|
40099
|
+
repoDir: story.repoDir,
|
|
40100
|
+
worktree: story.worktree,
|
|
40101
|
+
since: handed
|
|
40102
|
+
});
|
|
40103
|
+
if (!proven) {
|
|
40104
|
+
return {
|
|
40105
|
+
story,
|
|
40106
|
+
cost: spent,
|
|
40107
|
+
dod: [],
|
|
40108
|
+
commit: null,
|
|
40109
|
+
failure: noDiffAfterReopenReason({ handed, ...reopen }),
|
|
40110
|
+
developerError: null,
|
|
40111
|
+
before
|
|
40112
|
+
};
|
|
40113
|
+
}
|
|
40114
|
+
}
|
|
40027
40115
|
const dod = await this.runDod(story);
|
|
40028
40116
|
if (!this.dodProves(dod)) {
|
|
40029
40117
|
const failing = dodFailure(dod);
|
|
@@ -40393,6 +40481,14 @@ class BuildSession {
|
|
|
40393
40481
|
return;
|
|
40394
40482
|
this.lines.push(` · ${planned.story.id} was reopened by ${ledger4.reopened.actor} (${ledger4.reopened.note}) — ` + `the verdicts before that do not count against it, so it runs as attempt 1 of ${String(this.attempts)}`);
|
|
40395
40483
|
}
|
|
40484
|
+
reopenFor(planned) {
|
|
40485
|
+
const ledger4 = readReviewLedger(this.ctx.runDir, planned.story.id);
|
|
40486
|
+
if (ledger4.fixRound !== null)
|
|
40487
|
+
return { note: ledger4.fixRound.note, actor: ledger4.fixRound.actor, fix: true };
|
|
40488
|
+
if (ledger4.reopened !== null)
|
|
40489
|
+
return { note: ledger4.reopened.note, actor: ledger4.reopened.actor, fix: false };
|
|
40490
|
+
return null;
|
|
40491
|
+
}
|
|
40396
40492
|
waveFailed(wave) {
|
|
40397
40493
|
return wave.stories.some((planned) => this.statusOf(planned) === "blocked");
|
|
40398
40494
|
}
|
|
@@ -41173,14 +41269,33 @@ class BuildSession {
|
|
|
41173
41269
|
const measured = measuredWidening(changed, declared2);
|
|
41174
41270
|
if (measured === null)
|
|
41175
41271
|
return;
|
|
41176
|
-
|
|
41177
|
-
|
|
41178
|
-
|
|
41179
|
-
|
|
41180
|
-
|
|
41181
|
-
|
|
41182
|
-
|
|
41183
|
-
|
|
41272
|
+
const id = story.planned.story.id;
|
|
41273
|
+
const actor = "framework";
|
|
41274
|
+
try {
|
|
41275
|
+
this.ctx.emit("story.touches_widened", {
|
|
41276
|
+
story: id,
|
|
41277
|
+
paths: [...measured.paths],
|
|
41278
|
+
note: measured.note,
|
|
41279
|
+
before: [...measured.before],
|
|
41280
|
+
after: [...measured.after],
|
|
41281
|
+
basis: "measured"
|
|
41282
|
+
}, 0, actor);
|
|
41283
|
+
return;
|
|
41284
|
+
} catch (error) {
|
|
41285
|
+
const why = firstLine7(error instanceof Error ? error.message : String(error));
|
|
41286
|
+
try {
|
|
41287
|
+
this.ctx.emit("story.touches_widened", {
|
|
41288
|
+
story: id,
|
|
41289
|
+
paths_omitted: measured.paths.length,
|
|
41290
|
+
before_omitted: measured.before.length,
|
|
41291
|
+
after_omitted: measured.after.length,
|
|
41292
|
+
note: `${measured.note} — the lists could not be recorded on this event: ${why}`,
|
|
41293
|
+
basis: "measured"
|
|
41294
|
+
}, 0, actor);
|
|
41295
|
+
} catch (again) {
|
|
41296
|
+
this.advisories.push(`${id}: the measured widening of its surface (${String(measured.paths.length)} path(s) outside ` + `\`touches:\`) could not be recorded in events.jsonl — ` + firstLine7(again instanceof Error ? again.message : String(again)));
|
|
41297
|
+
}
|
|
41298
|
+
}
|
|
41184
41299
|
}
|
|
41185
41300
|
orderedOutcomes() {
|
|
41186
41301
|
const rows = [];
|
|
@@ -41791,6 +41906,20 @@ function failed3(ctx, error, tasks) {
|
|
|
41791
41906
|
}
|
|
41792
41907
|
|
|
41793
41908
|
// src/core/facilitator/executors/index.ts
|
|
41909
|
+
var PARTIAL_TASKS = Symbol.for("tldrx.partialTasks");
|
|
41910
|
+
function withPartialTasks(error, tasks) {
|
|
41911
|
+
if (tasks.length === 0)
|
|
41912
|
+
return error;
|
|
41913
|
+
const carrier = typeof error === "object" && error !== null ? error : new Error(String(error));
|
|
41914
|
+
Object.defineProperty(carrier, PARTIAL_TASKS, { value: [...tasks], enumerable: false, configurable: true });
|
|
41915
|
+
return carrier;
|
|
41916
|
+
}
|
|
41917
|
+
function partialTasksOf(error) {
|
|
41918
|
+
if (typeof error !== "object" || error === null)
|
|
41919
|
+
return [];
|
|
41920
|
+
const tasks = error[PARTIAL_TASKS];
|
|
41921
|
+
return Array.isArray(tasks) ? tasks : [];
|
|
41922
|
+
}
|
|
41794
41923
|
var EXECUTORS = new Map([
|
|
41795
41924
|
[BUILD_PHASE, buildExecutor],
|
|
41796
41925
|
[WATCH_PHASE, watchExecutor]
|
|
@@ -42535,14 +42664,33 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
|
|
|
42535
42664
|
outcome = await executor(executorCtx);
|
|
42536
42665
|
} catch (error) {
|
|
42537
42666
|
const why = error instanceof Error ? error.message : String(error);
|
|
42538
|
-
|
|
42667
|
+
const partial = partialTasksOf(error);
|
|
42668
|
+
const progress2 = { rows: 0, events: 0 };
|
|
42669
|
+
let recordingFailed = null;
|
|
42670
|
+
if (partial.length > 0) {
|
|
42671
|
+
try {
|
|
42672
|
+
recordExecutorTasks(store, options, phaseId, stageId, spec, partial, progress2);
|
|
42673
|
+
} catch (again) {
|
|
42674
|
+
recordingFailed = again instanceof Error ? again.message : String(again);
|
|
42675
|
+
}
|
|
42676
|
+
}
|
|
42677
|
+
const written = requireStage(store, phaseId, stageId).tasks.length - tasksBefore;
|
|
42678
|
+
const whole = partial.length > 0 && written === partial.length;
|
|
42679
|
+
store.save();
|
|
42680
|
+
const extra = appendOrSay(() => appendCapped("error", {
|
|
42539
42681
|
phase: phaseId,
|
|
42540
42682
|
where: "executor",
|
|
42541
42683
|
detail: why,
|
|
42542
|
-
tasks_recorded:
|
|
42543
|
-
|
|
42544
|
-
|
|
42545
|
-
|
|
42684
|
+
tasks_recorded: whole,
|
|
42685
|
+
rows_written: written,
|
|
42686
|
+
rows_expected: partial.length,
|
|
42687
|
+
events_written: progress2.events,
|
|
42688
|
+
...recordingFailed === null ? {} : { recording_error: recordingFailed }
|
|
42689
|
+
}, 0));
|
|
42690
|
+
const n = String(partial.length);
|
|
42691
|
+
const eventsShort = recordingFailed === null ? "" : `; ${String(progress2.events)} of ${n} agent.result events could not be appended: ${recordingFailed}`;
|
|
42692
|
+
const reason = partial.length === 0 ? `the executor threw before returning its task rows — this invocation's turn costs are not in run.yml: ${why}` : whole ? `the executor threw after ${n} turn(s) — their rows and costs are in run.yml${eventsShort}: ${why}` : `the executor threw after ${n} turn(s) and recording their rows threw too — ` + `${String(written)} of ${n} rows are in run.yml and the rest are not: ${why}; recording: ${recordingFailed ?? "unknown"}`;
|
|
42693
|
+
return failStage(store, options, phaseId, stageId, reason, [...notes, ...extra], tasksBefore + written);
|
|
42546
42694
|
}
|
|
42547
42695
|
if (isSequencingRefusal(outcome)) {
|
|
42548
42696
|
if (!started) {
|
|
@@ -42552,20 +42700,25 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
|
|
|
42552
42700
|
return out(EXIT_USAGE3, [...notes, ...outcome.lines], [], executorSignature(outcome));
|
|
42553
42701
|
}
|
|
42554
42702
|
claimEpicBranches(store, outcome.epicBranches, outcome.branchModel);
|
|
42703
|
+
const progress = { rows: 0, events: 0 };
|
|
42555
42704
|
try {
|
|
42556
|
-
recordExecutorTasks(store, options, phaseId, stageId, spec, outcome);
|
|
42705
|
+
recordExecutorTasks(store, options, phaseId, stageId, spec, outcome.tasks, progress);
|
|
42557
42706
|
} catch (error) {
|
|
42558
42707
|
const why = error instanceof Error ? error.message : String(error);
|
|
42559
42708
|
const recorded = requireStage(store, phaseId, stageId).tasks.length;
|
|
42560
|
-
|
|
42709
|
+
const written = recorded - tasksBefore;
|
|
42710
|
+
const expected = outcome.tasks.length;
|
|
42711
|
+
store.save();
|
|
42712
|
+
const extra = appendOrSay(() => appendCappedEvent(store, options, phaseId, stageId, "error", {
|
|
42561
42713
|
phase: phaseId,
|
|
42562
42714
|
where: "record-tasks",
|
|
42563
42715
|
detail: why,
|
|
42564
|
-
rows_written:
|
|
42565
|
-
rows_expected:
|
|
42566
|
-
|
|
42567
|
-
|
|
42568
|
-
|
|
42716
|
+
rows_written: written,
|
|
42717
|
+
rows_expected: expected,
|
|
42718
|
+
events_written: progress.events
|
|
42719
|
+
}, 0));
|
|
42720
|
+
const rows = written === expected ? `all ${String(expected)} rows are in run.yml and ${String(progress.events)} of ${String(expected)} ` + "agent.result events were appended" : `${String(written)} of ${String(expected)} rows are in run.yml and the rest are not`;
|
|
42721
|
+
return failStage(store, options, phaseId, stageId, `recording this invocation's task rows threw — ${rows}: ${why}`, [...notes, ...extra], recorded);
|
|
42569
42722
|
}
|
|
42570
42723
|
store.save();
|
|
42571
42724
|
if (store.cancelledUnder) {
|
|
@@ -42595,11 +42748,22 @@ async function runExecutor(store, options, phaseId, stageId, spec, notes, execut
|
|
|
42595
42748
|
function isSequencingRefusal(outcome) {
|
|
42596
42749
|
return outcome.sequencing === true && outcome.refused === true && outcome.tasks.length === 0 && outcome.costUsd === 0 && (outcome.epicBranches?.length ?? 0) === 0;
|
|
42597
42750
|
}
|
|
42598
|
-
function
|
|
42751
|
+
function appendOrSay(append) {
|
|
42752
|
+
try {
|
|
42753
|
+
append();
|
|
42754
|
+
return [];
|
|
42755
|
+
} catch (error) {
|
|
42756
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
42757
|
+
return [`the error event for this failure could not be appended to events.jsonl — ${why}; run.yml is saved regardless`];
|
|
42758
|
+
}
|
|
42759
|
+
}
|
|
42760
|
+
function recordExecutorTasks(store, options, phaseId, stageId, spec, tasks, progress = { rows: 0, events: 0 }) {
|
|
42599
42761
|
const version = frameworkVersionSync();
|
|
42600
|
-
|
|
42762
|
+
const rows = [];
|
|
42763
|
+
for (const task2 of tasks) {
|
|
42601
42764
|
const id = nextTaskId(store, phaseId, stageId);
|
|
42602
42765
|
const metered = task2.metered !== false;
|
|
42766
|
+
rows.push({ id, task: task2, metered });
|
|
42603
42767
|
recordTask(store, phaseId, stageId, {
|
|
42604
42768
|
id,
|
|
42605
42769
|
status: task2.error === null ? "done" : "failed",
|
|
@@ -42618,6 +42782,9 @@ function recordExecutorTasks(store, options, phaseId, stageId, spec, outcome) {
|
|
|
42618
42782
|
...cacheSplit(task2.usage?.cache_creation_input_tokens, task2.usage?.cache_read_input_tokens),
|
|
42619
42783
|
...task2.durationMs === undefined ? {} : { duration_ms: task2.durationMs, duration_basis: "spawned" }
|
|
42620
42784
|
});
|
|
42785
|
+
progress.rows += 1;
|
|
42786
|
+
}
|
|
42787
|
+
for (const { id, task: task2, metered } of rows) {
|
|
42621
42788
|
appendCappedEvent(store, options, phaseId, stageId, "agent.result", {
|
|
42622
42789
|
phase: phaseId,
|
|
42623
42790
|
task: id,
|
|
@@ -42632,6 +42799,7 @@ function recordExecutorTasks(store, options, phaseId, stageId, spec, outcome) {
|
|
|
42632
42799
|
...task2.tokens === undefined ? {} : { tokens: task2.tokens },
|
|
42633
42800
|
...task2.usage === undefined ? {} : { usage: usagePayload(task2.usage) }
|
|
42634
42801
|
}, metered ? round26(task2.costUsd) : 0);
|
|
42802
|
+
progress.events += 1;
|
|
42635
42803
|
}
|
|
42636
42804
|
}
|
|
42637
42805
|
async function commitStage(store, options, phaseId, stageId, spec, notes) {
|
|
@@ -43397,7 +43565,10 @@ function nowish(options) {
|
|
|
43397
43565
|
function oneLine7(text3, max = 220) {
|
|
43398
43566
|
const line = text3.split(`
|
|
43399
43567
|
`)[0]?.trim() ?? "";
|
|
43400
|
-
|
|
43568
|
+
if (line.length <= max)
|
|
43569
|
+
return line;
|
|
43570
|
+
const tail2 = Math.floor((max - 1) / 2);
|
|
43571
|
+
return `${line.slice(0, max - 1 - tail2)}…${line.slice(line.length - tail2)}`;
|
|
43401
43572
|
}
|
|
43402
43573
|
function phaseMoney(store, phaseId) {
|
|
43403
43574
|
const phase = store.budget.phases.find((entry) => entry.id === phaseId);
|
|
@@ -43612,6 +43783,19 @@ async function shipRun(options) {
|
|
|
43612
43783
|
};
|
|
43613
43784
|
}
|
|
43614
43785
|
const stories = runStories(store);
|
|
43786
|
+
const rejected = rejectedOnEpic(store.runDir, stories, view);
|
|
43787
|
+
if (rejected.length > 0) {
|
|
43788
|
+
const ids = rejected.map((row2) => row2.id);
|
|
43789
|
+
return refuse2([
|
|
43790
|
+
`${store.runId} carries a story the reviewer rejected, and its diff is on \`${branch}\``,
|
|
43791
|
+
...rejected.map((row2) => ` ${row2.id} — its last merge into the epic (commit ${row2.commit.slice(0, 7)} over epic base ` + `${row2.epicBase.slice(0, 7)}) was judged \`changes\`${row2.reason === null ? "" : `: ${row2.reason}`}`),
|
|
43792
|
+
" A story merges into the epic BEFORE its review, so a `changes` verdict leaves the diff on the branch;",
|
|
43793
|
+
` a PR over \`${branch}\` would carry that code, and its body would list ${ids.join(", ")} under "Not done"`,
|
|
43794
|
+
" without saying the diff is there.",
|
|
43795
|
+
` Unblock ${ids.length === 1 ? "it" : "them"} (` + (ids.length === 1 ? `\`tldrx story reopen ${ids[0] ?? ""} --note "<why>"\`` : `\`tldrx story reopen <id> --note "<why>"\` for each`) + ") and re-run Build so a verdict that stands lands over it,",
|
|
43796
|
+
" or open the PR by hand if you mean to ship the branch as it stands."
|
|
43797
|
+
]);
|
|
43798
|
+
}
|
|
43615
43799
|
const body = writeShipBody(store, branch, handoff, stories, new Set(loadWorkspace(options.root).repos.keys()));
|
|
43616
43800
|
try {
|
|
43617
43801
|
return await shipTo(options, store, branch, body, stories);
|
|
@@ -44061,6 +44245,23 @@ function remedyPaths(refused2, excused) {
|
|
|
44061
44245
|
return `${PROJECT_WORK_DIR} ${PROJECT_FRAMEWORK_DIR}`;
|
|
44062
44246
|
return refused2.map(quote2).join(" ");
|
|
44063
44247
|
}
|
|
44248
|
+
function rejectedOnEpic(runDir2, stories, view) {
|
|
44249
|
+
const rows = [];
|
|
44250
|
+
for (const story of stories) {
|
|
44251
|
+
if (story.status === "done")
|
|
44252
|
+
continue;
|
|
44253
|
+
const merge = readReviewLedger(runDir2, story.id).lastMerge;
|
|
44254
|
+
if (merge === null || merge.verdict !== "changes")
|
|
44255
|
+
continue;
|
|
44256
|
+
rows.push({
|
|
44257
|
+
id: story.id,
|
|
44258
|
+
commit: merge.commit,
|
|
44259
|
+
epicBase: merge.epicBase,
|
|
44260
|
+
reason: view?.unfinished.find((row2) => row2.id === story.id)?.reason ?? null
|
|
44261
|
+
});
|
|
44262
|
+
}
|
|
44263
|
+
return rows;
|
|
44264
|
+
}
|
|
44064
44265
|
function settledTouches(stories) {
|
|
44065
44266
|
const rows = [];
|
|
44066
44267
|
for (const story of stories) {
|
|
@@ -53512,6 +53713,8 @@ import { isAbsolute as isAbsolute13, join as join135 } from "node:path";
|
|
|
53512
53713
|
import { existsSync as existsSync111, readFileSync as readFileSync98, readdirSync as readdirSync34 } from "node:fs";
|
|
53513
53714
|
import { basename as basename17, join as join131 } from "node:path";
|
|
53514
53715
|
// src/core/replay/renderReplay.ts
|
|
53716
|
+
var STAGE_FAILED_MARKER = "FAILED: ";
|
|
53717
|
+
var NO_REASON_RECORDED = "no reason recorded";
|
|
53515
53718
|
function renderReplay(loaded) {
|
|
53516
53719
|
const run2 = loaded.run;
|
|
53517
53720
|
const out2 = [
|
|
@@ -53639,9 +53842,9 @@ function bullet2(item, trail) {
|
|
|
53639
53842
|
case "stage.done":
|
|
53640
53843
|
return `${prefix5}ended`;
|
|
53641
53844
|
case "stage.failed":
|
|
53642
|
-
return `${prefix5}
|
|
53845
|
+
return `${prefix5}${STAGE_FAILED_MARKER}${text3(payload2.reason) || NO_REASON_RECORDED}`;
|
|
53643
53846
|
case "stage.skipped":
|
|
53644
|
-
return `${prefix5}skipped: ${text3(payload2.reason) ||
|
|
53847
|
+
return `${prefix5}skipped: ${text3(payload2.reason) || NO_REASON_RECORDED}`;
|
|
53645
53848
|
case "question.asked":
|
|
53646
53849
|
return `${prefix5}${q || "a question"} asked by ${actor}: ${text3(payload2.question)}`.trimEnd();
|
|
53647
53850
|
case "question.answered":
|
|
@@ -53657,7 +53860,7 @@ function bullet2(item, trail) {
|
|
|
53657
53860
|
case "story.reopened":
|
|
53658
53861
|
return `${prefix5}story ${text3(payload2.story) || "?"} REOPENED by ${actor}` + ` — back to \`${text3(payload2.to_status) || "todo"}\` from \`${text3(payload2.from_status) || "?"}\`` + `${note(payload2.note)}`;
|
|
53659
53862
|
case "story.touches_widened":
|
|
53660
|
-
return `${prefix5}story ${text3(payload2.story) || "?"}'s \`touches:\` WIDENED by ${actor}` + ` (${basisOf(payload2)})` + ` — +${
|
|
53863
|
+
return `${prefix5}story ${text3(payload2.story) || "?"}'s \`touches:\` WIDENED by ${actor}` + ` (${basisOf(payload2)})` + ` — +${widenedPaths(payload2)}` + ` (${String(listCount(payload2, "before"))} → ${String(listCount(payload2, "after"))} path(s))` + `${note(payload2.note)}`;
|
|
53661
53864
|
case "story.base_fastforwarded":
|
|
53662
53865
|
return `${prefix5}story ${text3(payload2.story) || "?"}'s base fast-forwarded to` + ` \`${text3(payload2.base) || "?"}\` — ${text3(payload2.from) || "?"} → ${text3(payload2.to) || "?"}` + ` (${String(payload2.commits ?? "?")} commit(s))`;
|
|
53663
53866
|
case "story.base_updated":
|
|
@@ -53694,8 +53897,11 @@ function bullet2(item, trail) {
|
|
|
53694
53897
|
return `${prefix5}map refreshed`;
|
|
53695
53898
|
case "result.unreadable":
|
|
53696
53899
|
return `${prefix5}UNREADABLE ${text3(payload2.path) || "result.json"}` + ` — ${text3(payload2.error) || "no parse error recorded"}` + " (nothing failed; rewrite it and run the same command again)";
|
|
53697
|
-
case "error":
|
|
53698
|
-
|
|
53900
|
+
case "error": {
|
|
53901
|
+
const what = text3(payload2.detail) || text3(payload2.detail_omitted) || text3(payload2.message) || "no message recorded";
|
|
53902
|
+
const rows = typeof payload2.rows_expected === "number" ? ` (${String(payload2.rows_written ?? "?")} of ${String(payload2.rows_expected)} task rows recorded)` : "";
|
|
53903
|
+
return `${prefix5}error: ${what}${rows}`;
|
|
53904
|
+
}
|
|
53699
53905
|
default:
|
|
53700
53906
|
return null;
|
|
53701
53907
|
}
|
|
@@ -53753,8 +53959,12 @@ function pathList(value) {
|
|
|
53753
53959
|
const items = value.filter((item) => typeof item === "string");
|
|
53754
53960
|
return items.length === 0 ? "?" : items.join(", ");
|
|
53755
53961
|
}
|
|
53756
|
-
function
|
|
53757
|
-
|
|
53962
|
+
function widenedPaths(payload2) {
|
|
53963
|
+
const listed = pathList(payload2.paths);
|
|
53964
|
+
const omitted = typeof payload2.paths_omitted === "number" && payload2.paths_omitted > 0 ? `${String(payload2.paths_omitted)} path(s) not listed on the event` : "";
|
|
53965
|
+
if (omitted === "")
|
|
53966
|
+
return listed;
|
|
53967
|
+
return listed === "?" ? omitted : `${listed}, ${omitted}`;
|
|
53758
53968
|
}
|
|
53759
53969
|
function signature(actor, payload2) {
|
|
53760
53970
|
const executed = payload2.executed_by;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tldr-experts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "tldr-experts: an evidence-first, file-based AI development framework - five stages, a gate on every one, and every claim cited or refused. Installs the `tldrx` (and `tldr-experts`) command. Beta.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alan Martinez",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$doc": "Shape verified from https://code.claude.com/docs/en/plugins.md (Quickstart > Create the plugin manifest). Fields used here: name, description, version, author.name. Only plugin.json goes inside .claude-plugin/; skills/, agents/ and hooks/ live at the plugin root.",
|
|
3
3
|
"name": "tldrx",
|
|
4
4
|
"description": "tldr-experts: an evidence-first, file-based AI development framework. Five stages, a gate on every one, every claim cited or refused. Beta.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.26.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Martinez"
|
|
8
8
|
}
|
|
File without changes
|