tldr-experts 0.15.0 → 0.16.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.
- package/CHANGELOG.md +51 -0
- package/README.md +1 -0
- package/dist/hooks/{chunk-zccma7kt.js → chunk-vvr4rk82.js} +14 -2
- package/dist/hooks/session-start.js +1 -1
- package/dist/hooks/statusline.js +1 -1
- package/dist/tldrx.js +97 -14
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 — 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`tldrx run auto --retry-failed <n>` — a bounded retry on a failed stage (#233).** Measured
|
|
8
|
+
on a real unattended run at 0.15.0: the loop drove itself through what → how → plan and
|
|
9
|
+
signed all three `auto` gates by itself, and still needed a person four times. Three of the
|
|
10
|
+
four were content decisions or money, which a loop must not make. The fourth was a plan that
|
|
11
|
+
failed its `plan` check by five characters over a cap — a person relaunched `run auto`, the
|
|
12
|
+
next attempt fixed the two files and passed, and nothing else happened. So the loop stopped
|
|
13
|
+
on the one failure it could have cleared, and it stopped because `run auto` returned on every
|
|
14
|
+
non-zero exit except `4`. Now `--retry-failed <n>` lets it run that stage again, at most `n`
|
|
15
|
+
times in a row. It bounds exit `5` and nothing else: a usage error (`1`), a money refusal
|
|
16
|
+
(`2`) and an awaiting-human park (`4`) are each attempted ONCE however large `n` is, because
|
|
17
|
+
a phase ceiling means *a human decides about money* and a retry would turn that sentence into
|
|
18
|
+
a delay. Only CONSECUTIVE failures count — any other outcome puts the count back to zero,
|
|
19
|
+
since what is bounded is "this run is stuck", not "this run has ever failed". A retry SPENDS:
|
|
20
|
+
it is a fresh metered stage under the same phase ceiling and the same `--max-usd`, which is
|
|
21
|
+
what stops it running up a bill. `0` is the default and a default invocation's lines are
|
|
22
|
+
byte-identical to what they were; anything outside `0..3` is refused by name with exit `1`,
|
|
23
|
+
from the one constant the loop and the flag parser share. When the bound is spent the loop
|
|
24
|
+
stops on the failure's own exit `5` and SAYS the count last — `3 consecutive stage failures
|
|
25
|
+
at 03-plan/plan …` — so the sentence that reaches a phone is what the loop tried, not a bare
|
|
26
|
+
`5`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **An `auto` gate that REFUSES now writes the verdict down, so `run status` says which of the
|
|
31
|
+
seven conditions is holding it (#230).** The note has always been the designed answer to
|
|
32
|
+
"which of the seven stopped it" — and it was written only by a gate that CLOSED, so the one
|
|
33
|
+
record built for that question was `note: ""` in exactly the case it exists for. Measured
|
|
34
|
+
2026-09-10 on an unattended `run auto`: a gate sat pending ~40 minutes, `run status` and
|
|
35
|
+
`run status --verbose` named no condition, and the reason (`claim-sources`, one unresolvable
|
|
36
|
+
source) surfaced only when a person guessed at the `tldrx approve` the status line suggested
|
|
37
|
+
— the one route nobody unattended is going to take. Each re-measure that refuses now records
|
|
38
|
+
`auto-gate refused — held by: <ids> · <all seven with their values>` on the gate that is
|
|
39
|
+
still `pending`, and `run status` names the ids on the gate row and on the `waiting` line
|
|
40
|
+
(`--verbose` still quotes the whole note). All seven values, not just the failures: a note
|
|
41
|
+
that dropped `budget=$0.30 of $6.00` would answer "was it the money" with the same silence.
|
|
42
|
+
It writes only a `pending` gate — a gate a person has since signed keeps THEIR words — and
|
|
43
|
+
only when the note would change, so a four-hour `--wait-gates` poll writes `run.yml` once per
|
|
44
|
+
distinct verdict rather than thousands of times. That `pending` test and the write are a
|
|
45
|
+
**compare-and-set under the workspace lock**, over a `run.yml` read inside it: a check-then-act
|
|
46
|
+
over a snapshot taken a moment earlier erased a concurrent `approve` outright — `save()` writes
|
|
47
|
+
the whole snapshot and re-reads only `budget.yml`'s ceilings, so the gate fell back to
|
|
48
|
+
`pending`, `by`/`at` to null and the person's words were gone. Caught in pre-merge review and
|
|
49
|
+
reproduced with two real processes, which is now the test: the poll runs every two seconds
|
|
50
|
+
precisely while a person is deciding, so an audit record that destroys the evidence of a human
|
|
51
|
+
decision is not a theoretical interleaving. A gate carrying a refusal note is no longer
|
|
52
|
+
counted among the "signed gates carry a note" rows, because nobody closed it.
|
|
53
|
+
|
|
3
54
|
## 0.15.0 — 2026-09-10
|
|
4
55
|
|
|
5
56
|
### Added
|
package/README.md
CHANGED
|
@@ -316,6 +316,7 @@ back on the registry is 0.3.0.
|
|
|
316
316
|
|
|
317
317
|
| Version | Date | Status | Contains |
|
|
318
318
|
|---|---|---|---|
|
|
319
|
+
| 0.16.0 | 2026-09-11 | `beta` | an unattended run can now clear the one kind of failure it was stopping on, and a gate that refuses says why it refused: measured 2026-09-10 on a real unattended `run auto`, the loop drove itself through what → how → plan and signed all three `auto` gates by itself, and still needed a person four times — three of those were content or money decisions a loop must not make, and the fourth was a plan that failed its own check by five characters over a cap, where a person relaunched the same command and the next attempt fixed the two files and passed, so the loop stopped on the one failure it could have cleared; `tldrx run auto --retry-failed <n>` now runs a failed stage again at most `n` times in a row, bounding exit `5` and nothing else — a usage error (`1`), a money refusal (`2`) and an awaiting-human park (`4`) are each attempted ONCE however large `n` is, because a phase ceiling means a human decides about money and a retry would turn that sentence into a delay — only CONSECUTIVE failures count since what is bounded is "this run is stuck" and not "this run has ever failed", a retry SPENDS as a fresh metered stage under the same phase ceiling and the same `--max-usd`, `0` is the default and a default invocation's lines are byte-identical to what they were, and when the bound is spent the loop stops on the failure's own exit `5` and says the count LAST, so the sentence that reaches a phone is what the loop tried and not a bare number; and an `auto` gate that REFUSES now writes down the verdict its note was always designed to carry — a gate sat pending ~40 minutes while `run status` and `--verbose` named no condition at all, and the reason surfaced only when a person guessed at the `tldrx approve` the status line suggested, which is the one route nobody unattended is going to take — recording all seven conditions WITH THEIR VALUES on the still-`pending` gate, since a note that dropped the passing ones would answer "was it the money" with the same silence, and naming the holding ids on the gate row and on the `waiting` line; it writes only over a `pending` gate, so a gate a person has since signed keeps THEIR words, and only when the verdict would change, so a four-hour `--wait-gates` poll writes once per distinct verdict rather than thousands of times — and that test and that write are a compare-and-set under the workspace lock, because pre-merge review reproduced, with two real processes, a check-then-act over an earlier snapshot erasing a concurrent `approve` outright, and the poll runs every two seconds precisely while a person is deciding |
|
|
319
320
|
| 0.15.0 | 2026-09-10 | `beta` | defaults for the models actually running today, and records that name what happened: measured 2026-09-07/09 across three real workspaces, the first engine-driven run of each was ended by a calibration rather than by the work — a `how` turn and two Build developer turns killed at a 900 s per-turn clock while Opus turns on real repositories run 15-50 minutes, a 202 KB prompt refused by a ceiling whose own message called it "29% of a 200k window", and a 169 KB `facts.yml` sliced to 96 KB on its way into a design turn that then died. So a turn gets two hours (`timeout_s` 900 → 7200), a prompt 400 KB and inputs 256 KB, a phase ceiling holds every attempt its stages may take so the first retry of a stage that spent anything is no longer refused by arithmetic — `warn_at_pct` still measured against one attempt's share, so the warning still arrives before the money — and the four numbers that were calibrations rather than invariants (`attempts`, `fixlist_rounds`, `reviewer_share`, `gate_signer_share`) became optional `stage.yml` keys, refused by name out of range instead of clamped, absent meaning today's constant byte for byte, with `tldrx run auto --prompt-max-bytes` and `--max-reads` for the unattended run that would otherwise need a file edit to get past one refusal; a story's Definition of Done now runs with its dependencies installed — the `install:` slot has sat unread in `templates/workspace.yml` since the beginning and now runs in every fresh story worktree through the same allowlist-and-argv runner, recorded with its own exit code and duration, blocking the story rather than paying a turn to discover it — an exit 127 is reported as a named absent binary and not as a red test, a declared command may be run WITH ARGUMENTS (the exact `Bash(npm run test)` grant matched nothing the developer actually typed, so its own 127 was first seen by the gate, after the turn was paid for), and every DoD check says which tree it ran in; the Build gate now names story outcomes on every policy and not only `auto` — two runs approved from a phone printed `run is done` over zero stories delivered — `run.yml` records an additive `outcome:` written once by all three commands that close a run and rendered by six surfaces, and `tldrx ship` refuses with exit 1 instead of opening a PR over nothing; a red DoD keeps its real failure — the last 200 lines on disk (gitignored, since a tail can carry a secret), up to five failure-looking lines as the detail rather than the last `DeprecationWarning` on stderr, the failing line cited at the line it starts on, and the next attempt told it was the check and not a reviewer; a watcher card may honestly say `Query: none — <reason> [src: …]`, earned only over a card whose own `## Signal` cites `absent:` and refused like any unsourced item otherwise, after a stage spent real money writing the honest answer and was refused for it; a truncated input is told to the OWNER at spawn and not only to the sub-agent, a turn killed on timeout keeps the usage it had already streamed and never a price; and the maintain skill says which sha a review record must cite — the code head — a rule that cost a wave and was written down nowhere an agent reads |
|
|
320
321
|
| 0.14.3 | 2026-09-10 | `beta` | foreign uncommitted work no longer stops a Build, and the dashboard flake that blocked four merges in two days has a root cause: the dirty-tree guard used to count every `git status --porcelain` entry and refuse, offering only "commit it" or "stash it" — neither of which an agent may take with another person's files — and measured across three real workspaces on 0.14.2, every first engine-driven run reaching Build stopped at `04-build`, over seed docs, a data export and one untracked note; the dirt is now classified, `own` and `overlapping` refusing or passing exactly as before while everything `foreign` is set aside with a pathspec-limited `git stash push` as the LAST step before the epic branch is cut, recorded as `worktree.foreign_work_aside` and given back with `--index` on every exit path, success or failure, nothing ever deleted and nothing force-popped, a repo mid-merge, rebase, cherry-pick or bisect refused outright because that state has no clean undo, and a pop git refuses said as the stage's last line and carried into the handoff and the notification; the refusal's printed remedy is now the SAME string the engine runs, limited to the paths it listed and relaunching by mode, after an owner ran the pathspec-less line exactly as printed and it swept the run's own records under `tldrx-work/<run>/` into the stash until `tldrx next` answered `no run`; every path handed to git for a write is `:(literal)` and `git status` is read with `-z`, since a glob pathspec moved the neighbouring `x.txt` for a file called `[x].txt`; and the dashboard's live tests stop racing a typed millisecond — five consecutive runs of the two files went red 3 times, at 5084.27 / 5108.01 / 5256.49 ms against a hard-coded 5000 under load averages 65–107 on 14 cores — every deadline now deriving from one `eventWaitMs()` helper that scales like every other budget, with `test/machine-load.test.ts` refusing a hard-coded deadline in either file so it cannot come back at somebody's merge, while that measurement surfaced the product half: `watchWorkspace` armed its mtime sweep only in `poll` mode, so a dropped FSEvents notification left a live dashboard silently stale for the life of the process — measured with `fseventsd` at 98–115% CPU, directory events that never arrived AT ALL at 82,556 ms and 113,942 ms — and the sweep now runs in watch mode too, at 2 s, so a dropped notification is bounded rather than fatal |
|
|
321
322
|
| 0.14.2 | 2026-09-09 | `beta` | every citation check starts from a fresh view of `questions.md` and `facts.yml`: `srcToken.ts` memoised both indexes at module scope and nothing outside `test/` ever dropped them, so in `tldrx run auto` — one Node process for a whole run — the FIRST citation resolved anywhere froze the view every later stage was then judged against; measured on three real unattended workspaces at 0.14.1, a `how` stage refused with `no such question Q2 … declared: Q1` over questions it had itself written minutes earlier, and another refused over `145 live fact(s)` when `facts.yml` held 148, the three extra written by the owner's answers two seconds before the stage started — roughly $11 of paid turns thrown away for ids that were real the whole time; the indexes are now refreshed inside `toSrcContext`, the one place a citation context is built and a place every caller reaches exactly once per check, gate or hook, so a document's forty citations still read `facts.yml` once while nothing survives the check that read it |
|
|
@@ -507,6 +507,16 @@ function statusWithOutcome(status, outcome) {
|
|
|
507
507
|
|
|
508
508
|
// src/core/run/autoGate.ts
|
|
509
509
|
var AUTO_GATE_ACTOR = "auto";
|
|
510
|
+
var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
|
|
511
|
+
var HELD_SEPARATOR = " · ";
|
|
512
|
+
function heldByNote(note) {
|
|
513
|
+
if (!note.startsWith(AUTO_GATE_REFUSED_PREFIX))
|
|
514
|
+
return [];
|
|
515
|
+
const rest = note.slice(AUTO_GATE_REFUSED_PREFIX.length);
|
|
516
|
+
const end = rest.indexOf(HELD_SEPARATOR);
|
|
517
|
+
const ids = (end === -1 ? rest : rest.slice(0, end)).split(", ").map((id) => id.trim());
|
|
518
|
+
return ids.filter((id) => id !== "");
|
|
519
|
+
}
|
|
510
520
|
|
|
511
521
|
// src/core/run/gateAuthority.ts
|
|
512
522
|
var UNRECORDED_AUTHORIZER = "unrecorded";
|
|
@@ -729,12 +739,14 @@ function waitingFor(run, runDir) {
|
|
|
729
739
|
}
|
|
730
740
|
const open = blockingQuestionIds(join6(runDir, cursor.phase, "questions.md"));
|
|
731
741
|
switch (entry.stage.status) {
|
|
732
|
-
case "awaiting_gate":
|
|
742
|
+
case "awaiting_gate": {
|
|
743
|
+
const held = heldByNote(entry.stage.gate?.note ?? "");
|
|
733
744
|
return {
|
|
734
745
|
kind: "gate",
|
|
735
|
-
message: `gate on ${entry.phase.id}/${entry.stage.id} — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
746
|
+
message: `gate on ${entry.phase.id}/${entry.stage.id}` + (held.length === 0 ? "" : ` — held by ${held.join(", ")}`) + ` — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
736
747
|
questions: open
|
|
737
748
|
};
|
|
749
|
+
}
|
|
738
750
|
case "awaiting_answer":
|
|
739
751
|
return {
|
|
740
752
|
kind: "answer",
|
package/dist/hooks/statusline.js
CHANGED
package/dist/tldrx.js
CHANGED
|
@@ -15643,6 +15643,12 @@ var ENTRIES = [
|
|
|
15643
15643
|
meaning: "Send the workspace's declared notify hook a `status` payload this often while the loop runs — `30s`, `10m`, `2h`, or a bare number of seconds. Off by default, and it does nothing at all unless `.tldrx/workspace.yml` declares a `notify:` command (§2.18). A `status` payload carries what `tldrx run status` prints. It asks for nothing while the run is moving — and when the run is PARKED on an open question it says so and repeats the literal answer command, because a heartbeat that keeps saying nothing is waiting on you while a run waits on you is worse than silence.",
|
|
15644
15644
|
sub: "auto"
|
|
15645
15645
|
},
|
|
15646
|
+
{
|
|
15647
|
+
name: "retry-failed",
|
|
15648
|
+
arg: "<n>",
|
|
15649
|
+
meaning: 'How many times in a row the loop may run a FAILED stage again before it stops. 0 — the default, and what every invocation before this got — means one attempt and then exit 5. A retry is the same `tldrx next` a person would have typed: the stage is on disk as `failed` with its reason recorded, and the next attempt is told what the last one did. It bounds EXIT 5 AND NOTHING ELSE — a usage error (1), a money refusal (2) and an awaiting-human park (4) are attempted once however large the bound, because each is a decision a person owns; a phase ceiling especially, which means "a human decides about money" and would otherwise become a delay. Only CONSECUTIVE failures count: a stage that succeeds puts the count back to zero. A retry SPENDS — it is a fresh metered stage under the same phase ceiling and the same --max-usd — and when the bound is spent the loop stops on the failure\'s own exit 5, with the count in the last line.',
|
|
15650
|
+
sub: "auto"
|
|
15651
|
+
},
|
|
15646
15652
|
{
|
|
15647
15653
|
name: "wait-gates",
|
|
15648
15654
|
arg: "<duration>",
|
|
@@ -15695,6 +15701,7 @@ var ENTRIES = [
|
|
|
15695
15701
|
"tldrx run auto --parallel 3",
|
|
15696
15702
|
"tldrx run auto --prompt-max-bytes 500000 --max-reads 300",
|
|
15697
15703
|
"tldrx run auto --notify-every 10m",
|
|
15704
|
+
"tldrx run auto --retry-failed 2",
|
|
15698
15705
|
"tldrx run auto --wait-answers 30m",
|
|
15699
15706
|
"tldrx run auto --wait-answers 4h --wait-gates 4h",
|
|
15700
15707
|
"tldrx run unlock 260101-checkout --force",
|
|
@@ -15703,7 +15710,7 @@ var ENTRIES = [
|
|
|
15703
15710
|
exits: [EXIT_OK, EXIT_USAGE, EXIT_GATE_REFUSED, EXIT_NOT_FOUND, EXIT_AWAITING_HUMAN, EXIT_AGENT_FAILED],
|
|
15704
15711
|
notes: [
|
|
15705
15712
|
"`run attend host` is a LOCK, not an engine. It sets one field, spends nothing, runs no stage and touches no branch — and from then on THE FRAMEWORK WILL NOT SPAWN on that run: every turn is a `tldrx next --prepare` / `tldrx next --commit` handshake with the session driving it, the Build reviewer included. `run attend --none` hands it back.",
|
|
15706
|
-
"`run auto` is an ENGINE, not a lock. It calls `next` HEADLESS over and over, so THE FRAMEWORK spawns a metered sub-agent stage after stage, and it stops at the first thing it may not decide: a human gate or an open question (4), a stage failure (5), a phase ceiling or this loop's own --max-usd (2). It is REFUSED ON AN ATTENDED RUN (exit 1, before the event log is opened) — a lock and an engine are alternatives, never layers.",
|
|
15713
|
+
"`run auto` is an ENGINE, not a lock. It calls `next` HEADLESS over and over, so THE FRAMEWORK spawns a metered sub-agent stage after stage, and it stops at the first thing it may not decide: a human gate or an open question (4), a stage failure (5) — unless `--retry-failed <n>` lets it run that stage again, bounded, up to n times in a row — a phase ceiling or this loop's own --max-usd (2). It is REFUSED ON AN ATTENDED RUN (exit 1, before the event log is opened) — a lock and an engine are alternatives, never layers.",
|
|
15707
15714
|
"`run auto` can also TELL SOMEBODY. When `.tldrx/workspace.yml` declares a `notify:` command (§2.18), the loop hands that command one `version: 1` JSON object on stdin at every moment a person is needed — an open question with its options and the literal `tldrx answer` line, a gate with the literal approve line, a finished or failed run with its exit code and family — plus a periodic `status` under `--notify-every`. The framework names no chat tool: the command is the owner's own, run as argv with no shell, and its exit code is recorded as `notify.sent` / `notify.failed` and NEVER changes the run's outcome.",
|
|
15708
15715
|
"Under `run auto`, a stage whose `gates_policy` is `agent` gets one bounded GATE-SIGNER turn of its own. When the stage's checks have passed, the engine spawns a single sub-agent at the stage's model and effort, on a quarter of the stage's per-agent ceiling, allowed to read and to write exactly one file: `.agent/<stage>/evidence.md`. The note then goes through the UNCHANGED `approve --as-agent` path — the same validator a person's note goes through — so `verdict: sign` plus every condition holding closes the gate under the note's own `by:`, and anything else leaves it pending for a person with the reasons named. The turn is recorded like any other (`agent.spawned` / `agent.result`, `role: gate-signer`) and shows up in `tldrx cost`. There is no flag for it: `gates_policy: agent` is already the owner's recorded decision that an agent may close this gate, and `human` gates are never touched.",
|
|
15709
15716
|
"`run status` with several runs open LISTS them and exits 0 — it is the screen you read to find the id every other command wants.",
|
|
@@ -29357,6 +29364,22 @@ async function evaluateAutoGate(input) {
|
|
|
29357
29364
|
function heldBy(verdict) {
|
|
29358
29365
|
return verdict.conditions.filter((condition) => !condition.ok).map((condition) => condition.id);
|
|
29359
29366
|
}
|
|
29367
|
+
var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
|
|
29368
|
+
var HELD_SEPARATOR = " · ";
|
|
29369
|
+
function refusalNote(verdict) {
|
|
29370
|
+
const held2 = heldBy(verdict);
|
|
29371
|
+
if (held2.length === 0)
|
|
29372
|
+
return "";
|
|
29373
|
+
return `${AUTO_GATE_REFUSED_PREFIX}${held2.join(", ")}${HELD_SEPARATOR}` + verdict.conditions.map(render2).join("; ");
|
|
29374
|
+
}
|
|
29375
|
+
function heldByNote(note) {
|
|
29376
|
+
if (!note.startsWith(AUTO_GATE_REFUSED_PREFIX))
|
|
29377
|
+
return [];
|
|
29378
|
+
const rest = note.slice(AUTO_GATE_REFUSED_PREFIX.length);
|
|
29379
|
+
const end = rest.indexOf(HELD_SEPARATOR);
|
|
29380
|
+
const ids = (end === -1 ? rest : rest.slice(0, end)).split(", ").map((id) => id.trim());
|
|
29381
|
+
return ids.filter((id) => id !== "");
|
|
29382
|
+
}
|
|
29360
29383
|
async function reevaluateAutoGate(input) {
|
|
29361
29384
|
const phase = input.run.phases.find((entry) => entry.stages.some((stage3) => stage3.id === input.stageId));
|
|
29362
29385
|
const stage2 = phase?.stages.find((entry) => entry.id === input.stageId);
|
|
@@ -29377,7 +29400,7 @@ async function reevaluateAutoGate(input) {
|
|
|
29377
29400
|
runDir: input.runDir,
|
|
29378
29401
|
stage: planned
|
|
29379
29402
|
});
|
|
29380
|
-
|
|
29403
|
+
const verdict = await evaluateAutoGate({
|
|
29381
29404
|
root: input.root,
|
|
29382
29405
|
runDir: input.runDir,
|
|
29383
29406
|
phaseId: phase.id,
|
|
@@ -29386,6 +29409,30 @@ async function reevaluateAutoGate(input) {
|
|
|
29386
29409
|
budget: input.budget,
|
|
29387
29410
|
checks
|
|
29388
29411
|
});
|
|
29412
|
+
if (!verdict.ok)
|
|
29413
|
+
recordRefusal(input.runDir, input.stageId, verdict);
|
|
29414
|
+
return verdict;
|
|
29415
|
+
}
|
|
29416
|
+
function recordRefusal(runDir, stageId, verdict) {
|
|
29417
|
+
const note = refusalNote(verdict);
|
|
29418
|
+
if (note === "")
|
|
29419
|
+
return;
|
|
29420
|
+
try {
|
|
29421
|
+
withWorkspaceLock(workspaceRootOfRunDir(runDir), () => {
|
|
29422
|
+
const store = RunStore.open(runDir);
|
|
29423
|
+
const current = store.run.phases.flatMap((phase) => phase.stages).find((stage2) => stage2.id === stageId);
|
|
29424
|
+
if (current === undefined || current.gate.status !== "pending" || current.gate.note === note)
|
|
29425
|
+
return;
|
|
29426
|
+
store.mutate((run) => ({
|
|
29427
|
+
...run,
|
|
29428
|
+
phases: run.phases.map((phase) => ({
|
|
29429
|
+
...phase,
|
|
29430
|
+
stages: phase.stages.map((stage2) => stage2.id === stageId && stage2.gate.status === "pending" ? { ...stage2, gate: { ...stage2.gate, note } } : stage2)
|
|
29431
|
+
}))
|
|
29432
|
+
}));
|
|
29433
|
+
store.save();
|
|
29434
|
+
});
|
|
29435
|
+
} catch {}
|
|
29389
29436
|
}
|
|
29390
29437
|
function render2(condition) {
|
|
29391
29438
|
return `${condition.id}=${condition.detail}`;
|
|
@@ -29708,12 +29755,14 @@ function waitingFor(run, runDir) {
|
|
|
29708
29755
|
}
|
|
29709
29756
|
const open = blockingQuestionIds(join66(runDir, cursor.phase, "questions.md"));
|
|
29710
29757
|
switch (entry.stage.status) {
|
|
29711
|
-
case "awaiting_gate":
|
|
29758
|
+
case "awaiting_gate": {
|
|
29759
|
+
const held2 = heldByNote(entry.stage.gate?.note ?? "");
|
|
29712
29760
|
return {
|
|
29713
29761
|
kind: "gate",
|
|
29714
|
-
message: `gate on ${entry.phase.id}/${entry.stage.id} — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
29762
|
+
message: `gate on ${entry.phase.id}/${entry.stage.id}` + (held2.length === 0 ? "" : ` — held by ${held2.join(", ")}`) + ` — \`tldrx approve\` or \`tldrx reject --note "…"\``,
|
|
29715
29763
|
questions: open
|
|
29716
29764
|
};
|
|
29765
|
+
}
|
|
29717
29766
|
case "awaiting_answer":
|
|
29718
29767
|
return {
|
|
29719
29768
|
kind: "answer",
|
|
@@ -30020,7 +30069,7 @@ function renderGates(rows, verbose = false) {
|
|
|
30020
30069
|
const auto = rows.filter((row2) => row2.policy === "auto").length;
|
|
30021
30070
|
const agent = rows.filter((row2) => row2.policy === "agent").length;
|
|
30022
30071
|
const human = rows.length - auto - agent;
|
|
30023
|
-
const noted2 = rows.filter((row2) => row2.note !== null).length;
|
|
30072
|
+
const noted2 = rows.filter((row2) => isSigned(row2) && row2.note !== null).length;
|
|
30024
30073
|
const lines = [
|
|
30025
30074
|
`gates ${String(human)} human, ${String(auto)} auto` + (agent === 0 ? "" : `, ${String(agent)} agent`)
|
|
30026
30075
|
];
|
|
@@ -30043,10 +30092,13 @@ function renderGates(rows, verbose = false) {
|
|
|
30043
30092
|
var NOTE_MARK = "✎";
|
|
30044
30093
|
function gateTail(row2) {
|
|
30045
30094
|
const parts = [briefDuration(row2.started_at, row2.ended_at)];
|
|
30046
|
-
if (row2.note !== null)
|
|
30095
|
+
if (isSigned(row2) && row2.note !== null)
|
|
30047
30096
|
parts.push(NOTE_MARK);
|
|
30048
30097
|
return parts.filter((part) => part !== "").join(" ");
|
|
30049
30098
|
}
|
|
30099
|
+
function isSigned(row2) {
|
|
30100
|
+
return row2.status === "approved" || row2.status === "rejected";
|
|
30101
|
+
}
|
|
30050
30102
|
function briefDuration(startedAt, endedAt) {
|
|
30051
30103
|
const measured = dashDuration(startedAt, endedAt);
|
|
30052
30104
|
if (measured !== "")
|
|
@@ -30076,7 +30128,8 @@ function describeGate(row2) {
|
|
|
30076
30128
|
return `rejected by ${describeGateSignature(row2)}`;
|
|
30077
30129
|
if (row2.status === "n-a")
|
|
30078
30130
|
return `${row2.type}: n-a`;
|
|
30079
|
-
|
|
30131
|
+
const held2 = heldByNote(row2.note ?? "");
|
|
30132
|
+
return `${row2.type}: ${row2.status}` + (held2.length === 0 ? "" : ` — held by ${held2.join(", ")}`);
|
|
30080
30133
|
}
|
|
30081
30134
|
var NOTES_SHOWN = 3;
|
|
30082
30135
|
function renderOperatorNotes(notes) {
|
|
@@ -40210,6 +40263,8 @@ var EXIT_USAGE4 = 1;
|
|
|
40210
40263
|
var EXIT_REFUSED3 = 2;
|
|
40211
40264
|
var EXIT_NOT_FOUND4 = 3;
|
|
40212
40265
|
var EXIT_AWAITING_HUMAN3 = 4;
|
|
40266
|
+
var EXIT_AGENT_FAILED3 = 5;
|
|
40267
|
+
var MAX_RETRY_FAILED = 3;
|
|
40213
40268
|
var MAX_ITERATIONS = 96;
|
|
40214
40269
|
async function runAuto(options) {
|
|
40215
40270
|
const lines = [];
|
|
@@ -40349,6 +40404,7 @@ async function runAuto(options) {
|
|
|
40349
40404
|
}
|
|
40350
40405
|
return { costUsd: null, deferredGate: null };
|
|
40351
40406
|
};
|
|
40407
|
+
let consecutiveFailures = 0;
|
|
40352
40408
|
try {
|
|
40353
40409
|
for (let iteration = 0;iteration < MAX_ITERATIONS; iteration++) {
|
|
40354
40410
|
const store = RunStore.open(runDir2);
|
|
@@ -40400,7 +40456,19 @@ async function runAuto(options) {
|
|
|
40400
40456
|
if (send !== null)
|
|
40401
40457
|
await send();
|
|
40402
40458
|
};
|
|
40459
|
+
if (outcome.code !== EXIT_AGENT_FAILED3)
|
|
40460
|
+
consecutiveFailures = 0;
|
|
40403
40461
|
if (outcome.code !== EXIT_OK5) {
|
|
40462
|
+
const retryBound = options.retryFailedStages ?? 0;
|
|
40463
|
+
if (outcome.code === EXIT_AGENT_FAILED3) {
|
|
40464
|
+
consecutiveFailures += 1;
|
|
40465
|
+
if (consecutiveFailures <= retryBound) {
|
|
40466
|
+
for (const line of outcome.lines)
|
|
40467
|
+
say(` ${line}`);
|
|
40468
|
+
say(`retrying ${cursorBefore} — failure ${String(consecutiveFailures)} of ` + `${String(retryBound)} retries allowed (--retry-failed ${String(retryBound)})`);
|
|
40469
|
+
continue;
|
|
40470
|
+
}
|
|
40471
|
+
}
|
|
40404
40472
|
let rejection = null;
|
|
40405
40473
|
if (outcome.code === EXIT_AWAITING_HUMAN3) {
|
|
40406
40474
|
const card = openQuestions2();
|
|
@@ -40447,6 +40515,9 @@ async function runAuto(options) {
|
|
|
40447
40515
|
say(line);
|
|
40448
40516
|
if (rejection !== null)
|
|
40449
40517
|
say(rejection);
|
|
40518
|
+
if (outcome.code === EXIT_AGENT_FAILED3 && retryBound > 0) {
|
|
40519
|
+
say(`stopped: ${String(consecutiveFailures)} consecutive stage failures at ${cursorBefore} ` + `— the --retry-failed ${String(retryBound)} bound is spent, and the exit code is still ` + "5, the failure's own");
|
|
40520
|
+
}
|
|
40450
40521
|
return await finish(outcome.code, spentByLoop);
|
|
40451
40522
|
}
|
|
40452
40523
|
const after = RunStore.open(runDir2);
|
|
@@ -42691,7 +42762,8 @@ var VALUE_FLAGS = [
|
|
|
42691
42762
|
"wait-answers",
|
|
42692
42763
|
"wait-gates",
|
|
42693
42764
|
"prompt-max-bytes",
|
|
42694
|
-
"max-reads"
|
|
42765
|
+
"max-reads",
|
|
42766
|
+
"retry-failed"
|
|
42695
42767
|
];
|
|
42696
42768
|
function durationFlag(args, name) {
|
|
42697
42769
|
const raw = stringFlag(args, name);
|
|
@@ -42703,6 +42775,15 @@ function durationFlag(args, name) {
|
|
|
42703
42775
|
}
|
|
42704
42776
|
return ms;
|
|
42705
42777
|
}
|
|
42778
|
+
function retryFailedFlag(args) {
|
|
42779
|
+
const value = numberFlag(args, "retry-failed");
|
|
42780
|
+
if (value === undefined)
|
|
42781
|
+
return;
|
|
42782
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_RETRY_FAILED) {
|
|
42783
|
+
throw new UsageError(`--retry-failed must be a whole number between 0 and ${String(MAX_RETRY_FAILED)} ` + "(0 is the default: a failed stage stops the loop). A stage that has failed that many " + "times in a row is failing for a reason another spawn will not discover, and every " + "attempt is real money.");
|
|
42784
|
+
}
|
|
42785
|
+
return value;
|
|
42786
|
+
}
|
|
42706
42787
|
var runCommand = {
|
|
42707
42788
|
name: "run",
|
|
42708
42789
|
summary: "Create, inspect or auto-run a piece of work",
|
|
@@ -42715,6 +42796,7 @@ var runCommand = {
|
|
|
42715
42796
|
` + ` tldrx run auto [<run>] [--max-usd <n>] [--until <stage>] [--model <m>] [--effort <level>]
|
|
42716
42797
|
` + ` [--notify-every <duration>] [--wait-answers <duration>]
|
|
42717
42798
|
` + ` [--wait-gates <duration>] [--prompt-max-bytes <n>] [--max-reads <n>]
|
|
42799
|
+
` + ` [--retry-failed <n>]
|
|
42718
42800
|
` + ` [--yolo] [--parallel <n>] [--gate-agent] [--ui scene|compact|plain|off]
|
|
42719
42801
|
` + ` [--run <id>] [--root <path>]
|
|
42720
42802
|
` + ` tldrx run gates set <stage>:<human|auto|agent> --note <text> [--run <id>] [--root <path>]
|
|
@@ -42877,6 +42959,7 @@ async function runAutoLoop(argv) {
|
|
|
42877
42959
|
notifyEveryMs: durationFlag(args, "notify-every"),
|
|
42878
42960
|
waitAnswersMs: durationFlag(args, "wait-answers"),
|
|
42879
42961
|
waitGatesMs: durationFlag(args, "wait-gates"),
|
|
42962
|
+
retryFailedStages: retryFailedFlag(args),
|
|
42880
42963
|
actor: currentActor(),
|
|
42881
42964
|
at: nowRfc3339(),
|
|
42882
42965
|
onLine: (line) => ui2.log(() => process.stdout.write(`${line}
|
|
@@ -43294,7 +43377,7 @@ var PROPOSE_STAGE = "propose";
|
|
|
43294
43377
|
var EXIT_OK9 = 0;
|
|
43295
43378
|
var EXIT_USAGE6 = 1;
|
|
43296
43379
|
var EXIT_REFUSED7 = 2;
|
|
43297
|
-
var
|
|
43380
|
+
var EXIT_AGENT_FAILED4 = 5;
|
|
43298
43381
|
var DEFAULT_TRIAGE_EFFORT = "low";
|
|
43299
43382
|
var DEFAULT_TRIAGE_USD = 1;
|
|
43300
43383
|
var MIN_TRIAGE_USD = 0.25;
|
|
@@ -43428,7 +43511,7 @@ async function propose(options, workspaceCommands, inventory, seed, outDir) {
|
|
|
43428
43511
|
metered = outcome.metered;
|
|
43429
43512
|
if (!outcome.ok) {
|
|
43430
43513
|
return {
|
|
43431
|
-
code:
|
|
43514
|
+
code: EXIT_AGENT_FAILED4,
|
|
43432
43515
|
costUsd,
|
|
43433
43516
|
outDir,
|
|
43434
43517
|
inventory,
|
|
@@ -43449,7 +43532,7 @@ async function propose(options, workspaceCommands, inventory, seed, outDir) {
|
|
|
43449
43532
|
if (mode === "commit")
|
|
43450
43533
|
writeRaw(outDir, PROPOSE_STAGE, JSON.stringify(raw ?? null, null, 2));
|
|
43451
43534
|
return {
|
|
43452
|
-
code:
|
|
43535
|
+
code: EXIT_AGENT_FAILED4,
|
|
43453
43536
|
costUsd,
|
|
43454
43537
|
outDir,
|
|
43455
43538
|
inventory,
|
|
@@ -48535,7 +48618,7 @@ var EXIT_OK14 = 0;
|
|
|
48535
48618
|
var EXIT_USAGE9 = 1;
|
|
48536
48619
|
var EXIT_GATE_REFUSED2 = 2;
|
|
48537
48620
|
var EXIT_NOT_FOUND12 = 3;
|
|
48538
|
-
var
|
|
48621
|
+
var EXIT_AGENT_FAILED5 = 5;
|
|
48539
48622
|
var TRAIN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
48540
48623
|
async function runTraining(options) {
|
|
48541
48624
|
const said2 = [];
|
|
@@ -48775,7 +48858,7 @@ async function trainWithPreflight(options, said2) {
|
|
|
48775
48858
|
}));
|
|
48776
48859
|
if (!outcome.ok) {
|
|
48777
48860
|
rollback(previous);
|
|
48778
|
-
return fail3(
|
|
48861
|
+
return fail3(EXIT_AGENT_FAILED5, [
|
|
48779
48862
|
`${options.expert}/${area.id}: the ${task2.key} sub-agent failed — ${outcome.error ?? "no result"}`,
|
|
48780
48863
|
outcome.metered ? ` $${round29(outcome.costUsd).toFixed(2)} spent and recorded; nothing was written to competencies.yml` : ` ${providerWarning ?? "the provider turn was unmetered in dollars"}; nothing was written to competencies.yml`
|
|
48781
48864
|
], sum(tasks));
|
|
@@ -49114,7 +49197,7 @@ function reject2(log, options, area, costUsd, lines, detail) {
|
|
|
49114
49197
|
...fitProblems(detail.problems)
|
|
49115
49198
|
}
|
|
49116
49199
|
}));
|
|
49117
|
-
return { code:
|
|
49200
|
+
return { code: EXIT_AGENT_FAILED5, lines, costUsd };
|
|
49118
49201
|
}
|
|
49119
49202
|
function fitProblems(problems) {
|
|
49120
49203
|
const budget = MAX_PAYLOAD_BYTES2 - 1024;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tldr-experts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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.16.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Martinez"
|
|
8
8
|
}
|