tldr-experts 0.21.0 → 0.22.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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.0 — 2026-09-14
4
+
5
+ ### Fixed
6
+
7
+ - **A developer may now READ its own tree, `git -C` is refused on purpose, and every cure says
8
+ WHY it is a cure (#287, #294).** Two issues, one edit, because they are the same defect seen
9
+ twice. The developer's git allowance held `add`, `commit`, `rm`, `mv`, `restore` and **no read
10
+ verb at all** — it could write its worktree and never look at it. Measured on a live
11
+ unattended run: a developer ran `git -C <worktree> log --oneline -5`, was refused, was
12
+ re-spawned with the cure, was refused again, and the story died — ~$5 for a command that
13
+ changes nothing. `status`, `log`, `diff` and `show` join the ONE constant the grant, the
14
+ prompt's list and the refusal classifier all read (`build/developerGrants.ts`), so all three
15
+ gained them in the same edit. **`-C <path>` (and `--git-dir`, `--work-tree`) stays ungranted
16
+ as a decision, and the code now says so**: `-C` points git at ANY directory — the epic
17
+ worktree, another story's worktree, the shared checkout — so granting it would turn a read
18
+ verb into a read of trees the story does not own, and a mutating verb into a write there. It
19
+ is its own refusal kind (`elsewhere`) whose cure is "drop `-C <path>` and run git from your
20
+ own worktree", never "ask for `-C`". The second half is the reason these shipped together:
21
+ the three cures we had — "run each command alone" (#278), "run `git log` from your own
22
+ worktree" (#287), "don't append `echo`" (#294) — all said WHAT and none said WHY, and an agent
23
+ that does not know why finds another way to do the same thing. #294 is the proof: three
24
+ consecutive developers on ONE story appended `; echo "EXIT:$?"` to a DoD command, each was
25
+ refused, and the cure kept telling them to separate the commands — but from their side the
26
+ `echo` was not an extra command, it was HOW you read an exit code, so they re-added it. A
27
+ `separator` classification now carries `capturing` (the chain exists only to capture the
28
+ outcome: an `echo` naming `$?`, or a redirect of the command's own output), and that cure, the
29
+ retry prefix and the developer prompt all append the same one clause, from one constant: the
30
+ facilitator re-runs the Definition of Done after you and records each command's exit code, so
31
+ the number the `echo` would print is written down whether you capture it or not. That
32
+ clause says only what this repo measures (`build/dodRunner.ts`) and only what holds for every
33
+ provider: whether an agent's own execution tool hands the exit code back to the model is a
34
+ property of the provider's CLI — measured for Claude Code's `Bash` tool, established nowhere
35
+ for `codex exec` — and the sentence goes into the prompt BOTH providers read, so asserting it
36
+ would have been this very bug one level up, a plausible explanation instead of a true one. The
37
+ developer prompt goldens moved for exactly those two paragraphs.
38
+
3
39
  ## 0.21.0 — 2026-09-13
4
40
 
5
41
  ### Fixed
package/README.md CHANGED
@@ -335,6 +335,7 @@ back on the registry is 0.3.0.
335
335
 
336
336
  | Version | Date | Status | Contains |
337
337
  |---|---|---|---|
338
+ | 0.22.0 | 2026-09-14 | `beta` | A developer may now READ its own tree — `git status`, `log`, `diff` and `show` join the one constant the grant, the developer prompt and the refusal classifier all read; measured on a live unattended run where a developer was refused `git -C <worktree> log` twice and the story died, ~$5 for a command that changes nothing. `-C <path>` (and `--git-dir`, `--work-tree`) stays ungranted as a decision with its own refusal kind, `elsewhere`, because it points git at trees the story does not own. And every refusal cure now says WHY it is a cure: three consecutive developers on one story re-appended `; echo "EXIT:$?"` to a DoD command because the cure said what to drop and never that the facilitator re-runs the Definition of Done itself and records each exit code (#287, #294). Minor release: the developer's git allowance grew and a new refusal kind was added. |
338
339
  | 0.21.0 | 2026-09-13 | `beta` | `tldrx budget raise <phase> <usd> --stage <id>` now moves the stage's own `budget_usd` — the figure that actually sets a developer's and a reviewer's spawn ceiling — instead of the phase figure, which caps no spawn at all: measured on a live unattended run where raising the plan's per-story price and the phase ceiling moved a developer's cap by nothing, and only raising the stage figure moved it; a raise naming no `--stage` now says outright that it moved no spawn ceiling. And a reviewer a nearly-exhausted stage cannot fund is refused before it is spawned — the run that surfaced this handed a reviewer **$0.43**, which died before reading a line of diff and recorded `verdict: error`, parking the story with its dependents blocked; the refusal now costs $0 and records `verdict: n-a`, not an error the reviewer never formed (#244, #289). Minor release: a new flag, `--stage`, on `budget raise`. |
339
340
  | 0.20.0 | 2026-09-13 | `beta` | `tldrx seed check <file\|dir>` runs a hand-written seed through the same importer chain `run new --seed` runs, plus the authoring rules four unattended runs paid to learn — bullets under 200 characters, `[src:]` citations that resolve, `dod` lines byte-equal to a declared command with no shell separator, `depends_on` wherever two stories touch one file, a `Recommended:` on every open question — and prints the budget split and per-story caps a run would set, creating no run and spending nothing; `tldrx install --claude` now writes a second managed skill, `tldrx-plan`, that reads `workspace.yml` and the repo tree and turns "I want X" into seeds that pass the same check, naming only declared tools (#291). And a permission refusal over a command the workspace's `commands:` could have granted now names the operator's cure on the first dead developer instead of the fourth, measured on a live unattended run where four developers died and two review rounds ran before the missing command turned out to be one `workspace.yml` line away (#285). Minor release: a new CLI verb, `tldrx seed check`. |
340
341
  | 0.19.0 | 2026-09-13 | `beta` | `tldrx story reopen <id> --as-is --note "…"` settles a story a person finished by hand from its branch as it stands, spawning no developer — measured on a live unattended run where a hand-rebased branch had a green DoD and a clean merge-tree but no way to land, blocked by #271's since-the-spawn rule from a developer with nothing to do (#279). Everything after the missing developer is the ordinary path: the epic merges in first if it moved, the same DoD and reviewer judge the diff, the same merge lands it. The record never lies about who did the work — `story.reopened` carries `reason: as_is` with the actor and note, `task.done` carries `as_is`/`as_is_by`/`as_is_note`, and the review log names the branch and the signer. Two refusals are structural with no flag to skip either: a branch with no commit ahead of its epic, and a red DoD. The signature clears the moment a real developer attempt starts, so a requeued story after a `changes` verdict records a developer, not an as-is. Minor release: a new flag, `--as-is`, on `story reopen`. |
@@ -22,7 +22,7 @@ import {
22
22
  validateRunBudget,
23
23
  wouldExceed,
24
24
  wouldExceedHostTokens
25
- } from "./chunk-f5wkn522.js";
25
+ } from "./chunk-1p5027fn.js";
26
26
  import {
27
27
  EventLog
28
28
  } from "./chunk-6z5rmj0b.js";
@@ -728,6 +728,9 @@ var KINDS = {
728
728
  var MAX_FIXLIST_ROUNDS = STAGE_TUNING_DEFAULTS.fixlistRounds;
729
729
  var FINDING_KINDS = ["correctness", "security", "docs", "style"];
730
730
 
731
+ // src/core/build/refusalKind.ts
732
+ var OUTCOME_ALREADY_REPORTED = "the facilitator re-runs the Definition of Done after you and records each command's exit " + "code, so the number the `echo` would print is measured and written down whether you " + "capture it or not";
733
+
731
734
  // src/core/build/prompts.ts
732
735
  var MAX_TOUCHED_BYTES = 64 * 1024;
733
736
  var REVIEW_SCHEMA = {
@@ -8,7 +8,7 @@ import {
8
8
  spentBasis,
9
9
  tallyOf,
10
10
  validateRunBudget
11
- } from "./chunk-f5wkn522.js";
11
+ } from "./chunk-1p5027fn.js";
12
12
  import {
13
13
  EventLog,
14
14
  OUTCOME_NOT_RECORDED,
@@ -20,14 +20,14 @@ import {
20
20
  runSnapshot,
21
21
  statusWithOutcome,
22
22
  whatIsWaiting
23
- } from "./chunk-6qnsx1tz.js";
23
+ } from "./chunk-ah1pdax3.js";
24
24
  import {
25
25
  expertsDir,
26
26
  loadExperts,
27
27
  pathsIntersect,
28
28
  readExpertDomain,
29
29
  stackExpertNames
30
- } from "./chunk-f5wkn522.js";
30
+ } from "./chunk-1p5027fn.js";
31
31
  import {
32
32
  isFinished
33
33
  } from "./chunk-6z5rmj0b.js";
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  bar,
4
4
  runSnapshot
5
- } from "./chunk-6qnsx1tz.js";
6
- import"./chunk-f5wkn522.js";
5
+ } from "./chunk-ah1pdax3.js";
6
+ import"./chunk-1p5027fn.js";
7
7
  import"./chunk-6z5rmj0b.js";
8
8
  import"./chunk-m1s8a6s0.js";
9
9
  import"./chunk-yre2scxn.js";
package/dist/tldrx.js CHANGED
@@ -26477,7 +26477,23 @@ function dodFailureReason(result2, repo) {
26477
26477
  }
26478
26478
 
26479
26479
  // src/core/build/developerGrants.ts
26480
- var DEVELOPER_GIT_VERBS = ["add", "commit", "rm", "mv", "restore"];
26480
+ var DEVELOPER_GIT_VERBS = [
26481
+ "add",
26482
+ "commit",
26483
+ "rm",
26484
+ "mv",
26485
+ "restore",
26486
+ "status",
26487
+ "log",
26488
+ "diff",
26489
+ "show"
26490
+ ];
26491
+ var GIT_ELSEWHERE_OPTIONS = ["-C", "--git-dir", "--work-tree"];
26492
+ function gitElsewhereOption(argv) {
26493
+ const word = argv[1] ?? "";
26494
+ const flag = word.includes("=") ? word.slice(0, word.indexOf("=")) : word;
26495
+ return GIT_ELSEWHERE_OPTIONS.includes(flag) ? flag : null;
26496
+ }
26481
26497
  function developerGitGrants() {
26482
26498
  return DEVELOPER_GIT_VERBS.map((verb) => `Bash(git ${verb} *)`);
26483
26499
  }
@@ -26502,6 +26518,94 @@ function slotForCommand(line) {
26502
26518
  return SUBCOMMAND_RE.test(second) ? `${head} ${second}` : head;
26503
26519
  }
26504
26520
 
26521
+ // src/core/build/refusalKind.ts
26522
+ var MAX_SEPARATOR_RETRIES = 1;
26523
+ function classifyRefusal(command2, declared) {
26524
+ const separator = unquotedShellSeparator(command2);
26525
+ if (separator !== null) {
26526
+ return { kind: "separator", separator, capturing: capturesOutcome(command2, separator) };
26527
+ }
26528
+ const argv = command2.trim().split(/\s+/);
26529
+ if (argv[0] !== "git")
26530
+ return undeclaredKind(command2, declared);
26531
+ const verb = argv[1] ?? "";
26532
+ const elsewhere = gitElsewhereOption(argv);
26533
+ if (elsewhere !== null)
26534
+ return { kind: "elsewhere", option: elsewhere };
26535
+ if (verb === "" || verb.startsWith("-"))
26536
+ return { kind: "unknown" };
26537
+ if (isDeveloperGitVerb(verb))
26538
+ return { kind: "unknown" };
26539
+ return { kind: "verb", verb, equivalent: grantedEquivalent(verb, argv.slice(2)) };
26540
+ }
26541
+ function undeclaredKind(command2, declared) {
26542
+ if (declared === undefined)
26543
+ return { kind: "unknown" };
26544
+ if (grantsCommand(declared, command2))
26545
+ return { kind: "unknown" };
26546
+ const slot = slotForCommand(command2);
26547
+ return slot === "" ? { kind: "unknown" } : { kind: "undeclared", slot };
26548
+ }
26549
+ function grantedEquivalent(verb, args) {
26550
+ if (verb === "checkout") {
26551
+ const switching = args.some((a) => a === "-b" || a === "-B" || a === "--orphan" || a === "-t" || a === "--track");
26552
+ if (switching)
26553
+ return null;
26554
+ const hasTarget = args.some((a) => !a.startsWith("-")) || args.includes("--");
26555
+ return hasTarget ? "git restore <path>" : null;
26556
+ }
26557
+ if (verb === "reset") {
26558
+ const mode = args.some((a) => /^--(hard|soft|mixed|merge|keep)$/.test(a));
26559
+ if (mode)
26560
+ return null;
26561
+ if (args.includes("--"))
26562
+ return "git restore --staged <path>";
26563
+ const [first, second] = args.filter((a) => !a.startsWith("-"));
26564
+ if (first === "HEAD" && second !== undefined)
26565
+ return "git restore --staged <path>";
26566
+ return null;
26567
+ }
26568
+ return null;
26569
+ }
26570
+ var EXIT_CODE_ECHO_RE = /(^|[;&|])\s*echo\b[^;&|]*\$\?/;
26571
+ function capturesOutcome(command2, separator) {
26572
+ if (EXIT_CODE_ECHO_RE.test(command2))
26573
+ return true;
26574
+ return separator === ">" || separator === ">>" || separator === "2>&1";
26575
+ }
26576
+ var OUTCOME_ALREADY_REPORTED = "the facilitator re-runs the Definition of Done after you and records each command's exit " + "code, so the number the `echo` would print is measured and written down whether you " + "capture it or not";
26577
+ function refusalCure(kind) {
26578
+ switch (kind.kind) {
26579
+ case "separator":
26580
+ return "run each command alone — shell separators split a line into subcommands that each need their own grant" + (kind.capturing ? `. The exit code is not lost by dropping it: ${OUTCOME_ALREADY_REPORTED}` : "");
26581
+ case "elsewhere":
26582
+ return `drop \`${kind.option} <path>\` and run git from your own worktree — \`${kind.option}\` ` + "points git at another directory, so it is refused on purpose rather than missing from the " + "allowance: it would reach trees this story does not own. Your working directory already is " + "the worktree, and the read verbs run there";
26583
+ case "verb":
26584
+ return kind.equivalent === null ? `\`git ${kind.verb}\` is not granted` : `\`git ${kind.verb}\` is not granted; use \`${kind.equivalent}\``;
26585
+ case "undeclared":
26586
+ return `nothing in .tldrx/workspace.yml's \`commands:\` grants \`${kind.slot}\` — a developer's ` + "grant is built from the declared commands, so no re-run and no reopen note can make this " + `line runnable. The operator's cure: add a \`commands:\` slot whose value is exactly ` + `\`${kind.slot}\` (a slot grants that string plus any arguments), or a longer prefix of the ` + "refused line if less should be granted";
26587
+ case "unknown":
26588
+ return "";
26589
+ }
26590
+ }
26591
+ function withCure(sentence, command2, declared) {
26592
+ const cure = refusalCure(classifyRefusal(command2, declared));
26593
+ return cure === "" ? sentence : `${sentence}. The cure: ${cure}`;
26594
+ }
26595
+ function separatorCurePrefix(command2) {
26596
+ const separator = unquotedShellSeparator(command2);
26597
+ const capturing = separator !== null && capturesOutcome(command2, separator);
26598
+ return [
26599
+ `Your previous command \`${command2}\` was refused because it chains commands. Run each command alone.`,
26600
+ "The permission layer splits a line at every shell separator (`&&`, `;`, `|`, `>`, `2>&1`, `$()`)",
26601
+ "and each fragment must match its own grant, so a compound line is refused even when every",
26602
+ "command in it is allowed on its own. This is the one retry: a second refusal blocks the story.",
26603
+ ...capturing ? [`The exit code is not lost by dropping it: ${OUTCOME_ALREADY_REPORTED}.`] : [],
26604
+ ""
26605
+ ].join(`
26606
+ `);
26607
+ }
26608
+
26505
26609
  // src/core/build/prompts.ts
26506
26610
  var MAX_TOUCHED_BYTES = 64 * 1024;
26507
26611
  var MAX_TOUCHED_FILES = 24;
@@ -26605,8 +26709,11 @@ function buildDeveloperPrompt(parts) {
26605
26709
  "",
26606
26710
  ...parts.testFast === undefined ? [] : [...testFastRule(parts.testFast.fast, parts.testFast.full), ""],
26607
26711
  `Commit with \`git add\` and \`git commit\`. The git verbs you hold are exactly ${gitVerbList()}.`,
26608
- "`git restore <path>` is how to put a file back (there is no checkout in that list). Nothing",
26609
- "else about git is yours to do.",
26712
+ "`git restore <path>` is how to put a file back (there is no checkout in that list). The read",
26713
+ "verbs are yours so you can look at your own tree before you commit — run them from your working",
26714
+ "directory, which already IS this worktree. Never `-C <path>`: it points git at another directory,",
26715
+ "which is not yours to read or write, so it is refused on purpose. Nothing else about git is",
26716
+ "yours to do.",
26610
26717
  "",
26611
26718
  "## Rules",
26612
26719
  "",
@@ -26619,8 +26726,9 @@ function buildDeveloperPrompt(parts) {
26619
26726
  "- Run each Definition of Done command verbatim and alone: no redirection, pipes or chaining.",
26620
26727
  " Shell separators (`>`, `>>`, `2>&1`, `<`, `|`, `;`, `&&`, `||`, `&`, `$()`) split a line into",
26621
26728
  " subcommands, and each subcommand must match its own grant, so a compound line is refused",
26622
- " even when the script itself is allowed. The facilitator re-runs the Definition of Done",
26623
- " after you anyway.",
26729
+ " even when the script itself is allowed.",
26730
+ `- Do not append \`; echo $?\` or redirect a command's output to a file to capture its outcome.`,
26731
+ ` The exit code is not lost by dropping it: ${OUTCOME_ALREADY_REPORTED}.`,
26624
26732
  "",
26625
26733
  "### Conventions",
26626
26734
  "",
@@ -28715,78 +28823,6 @@ function verdictReviewer(checkPayload, lastSpawn) {
28715
28823
  return lastSpawn;
28716
28824
  }
28717
28825
 
28718
- // src/core/build/refusalKind.ts
28719
- var MAX_SEPARATOR_RETRIES = 1;
28720
- function classifyRefusal(command2, declared) {
28721
- const separator = unquotedShellSeparator(command2);
28722
- if (separator !== null)
28723
- return { kind: "separator", separator };
28724
- const argv = command2.trim().split(/\s+/);
28725
- if (argv[0] !== "git")
28726
- return undeclaredKind(command2, declared);
28727
- const verb = argv[1] ?? "";
28728
- if (verb === "" || verb.startsWith("-"))
28729
- return { kind: "unknown" };
28730
- if (isDeveloperGitVerb(verb))
28731
- return { kind: "unknown" };
28732
- return { kind: "verb", verb, equivalent: grantedEquivalent(verb, argv.slice(2)) };
28733
- }
28734
- function undeclaredKind(command2, declared) {
28735
- if (declared === undefined)
28736
- return { kind: "unknown" };
28737
- if (grantsCommand(declared, command2))
28738
- return { kind: "unknown" };
28739
- const slot = slotForCommand(command2);
28740
- return slot === "" ? { kind: "unknown" } : { kind: "undeclared", slot };
28741
- }
28742
- function grantedEquivalent(verb, args) {
28743
- if (verb === "checkout") {
28744
- const switching = args.some((a) => a === "-b" || a === "-B" || a === "--orphan" || a === "-t" || a === "--track");
28745
- if (switching)
28746
- return null;
28747
- const hasTarget = args.some((a) => !a.startsWith("-")) || args.includes("--");
28748
- return hasTarget ? "git restore <path>" : null;
28749
- }
28750
- if (verb === "reset") {
28751
- const mode = args.some((a) => /^--(hard|soft|mixed|merge|keep)$/.test(a));
28752
- if (mode)
28753
- return null;
28754
- if (args.includes("--"))
28755
- return "git restore --staged <path>";
28756
- const [first2, second] = args.filter((a) => !a.startsWith("-"));
28757
- if (first2 === "HEAD" && second !== undefined)
28758
- return "git restore --staged <path>";
28759
- return null;
28760
- }
28761
- return null;
28762
- }
28763
- function refusalCure(kind) {
28764
- switch (kind.kind) {
28765
- case "separator":
28766
- return "run each command alone — shell separators split a line into subcommands that each need their own grant";
28767
- case "verb":
28768
- return kind.equivalent === null ? `\`git ${kind.verb}\` is not granted` : `\`git ${kind.verb}\` is not granted; use \`${kind.equivalent}\``;
28769
- case "undeclared":
28770
- return `nothing in .tldrx/workspace.yml's \`commands:\` grants \`${kind.slot}\` — a developer's ` + "grant is built from the declared commands, so no re-run and no reopen note can make this " + `line runnable. The operator's cure: add a \`commands:\` slot whose value is exactly ` + `\`${kind.slot}\` (a slot grants that string plus any arguments), or a longer prefix of the ` + "refused line if less should be granted";
28771
- case "unknown":
28772
- return "";
28773
- }
28774
- }
28775
- function withCure(sentence, command2, declared) {
28776
- const cure = refusalCure(classifyRefusal(command2, declared));
28777
- return cure === "" ? sentence : `${sentence}. The cure: ${cure}`;
28778
- }
28779
- function separatorCurePrefix(command2) {
28780
- return [
28781
- `Your previous command \`${command2}\` was refused because it chains commands. Run each command alone.`,
28782
- "The permission layer splits a line at every shell separator (`&&`, `;`, `|`, `>`, `2>&1`, `$()`)",
28783
- "and each fragment must match its own grant, so a compound line is refused even when every",
28784
- "command in it is allowed on its own. This is the one retry: a second refusal blocks the story.",
28785
- ""
28786
- ].join(`
28787
- `);
28788
- }
28789
-
28790
28826
  // src/core/build/review.ts
28791
28827
  var VERDICT_WORDS = ["approve", "fixlist", "changes"];
28792
28828
  var VERDICT_ENUM = VERDICT_WORDS.join("|");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tldr-experts",
3
- "version": "0.21.0",
3
+ "version": "0.22.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.21.0",
5
+ "version": "0.22.0",
6
6
  "author": {
7
7
  "name": "Alan Martinez"
8
8
  }