tldr-experts 0.14.2 → 0.14.3

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,6 +1,117 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## 0.14.3 — 2026-09-10
5
+
6
+ ### Changed
7
+
8
+ - **A Build no longer stops because somebody else's work is uncommitted in the checkout
9
+ (#164).** The dirty-tree guard used to count every `git status --porcelain` entry and refuse,
10
+ and its only exits were "commit it" or "stash it" — neither of which an agent may take with
11
+ another person's files. Measured twice on two consecutive days on a Next.js workspace: 14
12
+ uncommitted paths, 13 untracked, none of them the run's, and nine stories waited three hours
13
+ for a human to grant one `git stash` at 4 a.m. Measured again across three real workspaces on
14
+ 0.14.2: every first engine-driven run reaching Build stopped at `04-build`, over seed docs, a
15
+ data export and one untracked note. The dirt is now CLASSIFIED, and only one verdict still
16
+ refuses. `own` — anything under this workspace's own `tldrx-work/` or `.tldrx/` — is not dirt,
17
+ as before. `overlapping` — a dirty path inside a pending story's `touches:`, a submodule, or a
18
+ directory of the operator's that merely looks like tldrx state — refuses exactly as it did,
19
+ and names which path and why. Everything else is `foreign`: the engine sets it aside with a
20
+ pathspec-limited `git stash push` before it cuts the epic branch, records
21
+ `worktree.foreign_work_aside`, and pops it back when the stage ends, recording
22
+ `worktree.foreign_work_restored` — on every exit path, success or failure. A repo in the
23
+ middle of a merge, rebase, cherry-pick or bisect refuses outright and is never stashed into:
24
+ that state has no clean undo. Nothing is ever deleted and nothing is ever force-popped; a pop
25
+ git refuses is `restored: false` with the paths and the literal command, said as the stage's
26
+ LAST line, carried into the handoff's `## Unknowns` (and so into a PR body) and into the
27
+ `stage.done` / `run.finished` / `run.failed` notification's own summary — no new notify kind.
28
+ The run's exit code does not move for it, because that code answers for the run's work and not
29
+ for the operator's tree.
30
+
31
+ **The stash is the LAST step before anything is cut**, and every exit from the Build executor
32
+ — a refusal, a failure, a green stage — gives it back before it returns. An earlier draft took
33
+ the stash at the first door, so the foreign-epic refusal and the base pre-flight could both
34
+ refuse with the operator's files already moved, through a return path that neither restored
35
+ them nor said they had gone: reproduced in review as exit 2, a stash on the list, the file
36
+ missing, nothing printed, and a re-run that refused forever.
37
+ - **The restore reinstates the INDEX, not just the files (#164).** `git stash pop --index`, so a
38
+ path staged at one version and modified further in the worktree comes back both staged and
39
+ modified — the shape one of the three measured workspaces actually had (a script and a
40
+ `package.json` line staged in a sub-repo). Measured: a plain pop left `git show :f.txt` reading
41
+ the commit instead of what was staged. When `--index` itself refuses, the fallback is a plain
42
+ pop and the record says so — `index_restored: false` on the event and a sentence saying those
43
+ paths are back unstaged. `--keep-index` is deliberately not used on the push side: measured, it
44
+ leaves the staged content in the working tree, which is the dirt this path exists to remove.
45
+
46
+ ### Fixed
47
+
48
+ - **The dashboard's live tests wait on the machine's clock, not a literal — the flake that
49
+ blocked four merges in two days (#193).** `test/dashboard-live.test.ts` and
50
+ `test/dashboard-server.test.ts` both call `setDefaultTimeout(spawnTestTimeout())`, so the
51
+ per-test BUDGET already scaled with load; every deadline that actually decided a result was
52
+ a number typed inside the assertion (`sse.next("reload", 5_000)`, `Date.now() + 2_000`), and
53
+ the load factor could never reach it. Each of those waits is an `fs.watch` notification
54
+ racing a stopwatch, so on a busy box a different SSE test reddened every time — measured
55
+ 2026-09-09 under `load averages: 65–107` on 14 cores, five consecutive runs of the two files
56
+ went red 3 times, at 5084.27 / 5108.01 / 5256.49 ms against the 5000 and 2002.30 / 2002.55 /
57
+ 2005.47 ms against the 2000. Not one failed on CONTENT: they timed out. The deadlines now go
58
+ through one helper, `eventWaitMs()` in `test/fixtures/machineLoad.ts`, which is
59
+ `spawnTestTimeout` with a 15 s base rather than a second scaler — the same derivation, so a
60
+ box given twice the budget to start a process is given twice the patience for a notification
61
+ from it — and the per-test budgets scale with it. The assertions are otherwise unchanged: the
62
+ fix is the instrument, not the behaviour, and `test/machine-load.test.ts` now refuses a
63
+ hard-coded millisecond deadline in either file so it cannot come back at somebody's merge.
64
+ The server's own timings are not what the 5 s bound was papering over — `DEBOUNCE_MS` is 300
65
+ (20 in these tests) and the mtime fallback sweeps every 500 ms, both an order of magnitude
66
+ under it — but the measurement did surface a second, distinct cause, filed as #213: with the
67
+ deadlines scaled, the failures that remain are all directory create/remove events that never
68
+ arrive AT ALL (114 s, 82 s, no frame), tracking `fseventsd` at ~100% CPU rather than load
69
+ average, and `watchWorkspace` runs its fingerprint sweep only in poll mode, so a dropped
70
+ FSEvents notification leaves a live dashboard silently stale for the life of the process.
71
+ This entry fixes the instrument; the bullet below is the product half.
72
+
73
+ - **A live dashboard can no longer go silently stale because the OS dropped a notification
74
+ (#213).** `fs.watch` is the fast path, never a guarantee — FSEvents drops and coalesces
75
+ under queue pressure — and `watchWorkspace` armed its mtime sweep only in `poll` mode, so
76
+ a dropped event meant nothing fired, nothing re-armed, and nothing ever swept: the page
77
+ stopped updating for the life of the process and said nothing about it. Measured on a
78
+ 14-core macOS box with `fseventsd` at 98-115% CPU while `mdbulkimport` indexed: directory
79
+ create and remove events that never arrived at all — 82,556 ms and 113,942 ms with no
80
+ frame — while appends inside an already-watched directory kept arriving throughout. Load
81
+ average was not the predictor; fseventsd saturation was. The sweep now runs in BOTH modes:
82
+ it is the notifier in `poll` at 500 ms and a backstop in `watch` at `SWEEP_MS` (2 s,
83
+ overridable like `debounceMs`), so a dropped notification is bounded rather than fatal, and
84
+ the watcher is re-armed by the sweep's own `fire()`. Same `fingerprint`, one derivation, two
85
+ cadences. It is idempotent with the fast path — a change the watcher reported re-baselines
86
+ the sweep inside the same debounce, so one change is still one reload frame. RED first:
87
+ `test/dashboard-live.test.ts`'s new "a change the watcher missed still reaches the page, by
88
+ sweep" hangs a real change off a server whose `fs.watch` handles have been closed
89
+ (`simulateWatcherLoss()` — you cannot ask the OS to drop an event on demand) and went red
90
+ before this change with no frame at all.
91
+ - **The refusal's printed remedy no longer makes the run disappear (#164).** It printed
92
+ `git stash push -u -m "tldrx <run> foreign work"` with no pathspec. Measured on 0.14.2: an
93
+ owner ran it exactly as printed and it swept the run's OWN untracked records under
94
+ `tldrx-work/<run>/` into the stash, after which `tldrx next` answered
95
+ `no run '<id>' in tldrx-work/` and `tldrx status` said `nothing pending`. Nothing was lost —
96
+ `git stash pop` brings it all back — but the framework's own advice had made its own run
97
+ vanish, and an owner who does not know git internals reads that as the work being gone. The
98
+ remedy is now limited to exactly the paths the refusal listed, each passed after a literal
99
+ `--`, and the relaunch verb is chosen by MODE: `tldrx run auto <run>` when the engine is
100
+ driving, `tldrx next` when a person is. The owner was in `run auto` and the message told him
101
+ to run `next`, which is the cursor verb. The printed line is also the SAME string the engine
102
+ runs, `:(literal)` pathspecs and all, shell-quoted so it survives being retyped: it used to
103
+ join the raw paths while the engine passed `:(literal)`, and the docstring claiming they were
104
+ one thing was simply false — measured, the printed line for a file called `[x].txt` moved the
105
+ neighbouring `x.txt`.
106
+ - **A filename with a space, a leading dash or a bracket is the file that moves (#164).** Git
107
+ pathspecs are globs by default, so `git stash push -u -- 'a[b].txt'` takes the neighbouring
108
+ `ab.txt` with it — measured in a scratch repo, the tree came back empty where one file should
109
+ have remained dirty. Every path the framework hands to git for a write is now
110
+ `:(literal)<path>`, and `git status` is read with `-z` so the path is the bytes on disk rather
111
+ than git's quoted-and-escaped rendering of them. This is the first thing tldrx does that
112
+ WRITES to the operator's uncommitted work, and it is the one operation with no undo.
113
+
114
+
4
115
  ## 0.14.2 — 2026-09-09
5
116
 
6
117
  ### Fixed
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.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 |
319
320
  | 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 |
320
321
  | 0.14.1 | 2026-09-09 | `beta` | an auto gate that says what holds it and closes itself, and three derivations cut to one: measured on a real workspace at 0.14.0, the first `run auto --wait-answers 4h --wait-gates 4h` with a notify hook finished its What stage for $1.98 with every declared check green and told the owner *"waiting at a auto gate that did not close by itself — a person signs it"* — no reason, because `evaluateAutoGate` had computed exactly that sentence into a stdout line nobody was watching while `gate.requested` was appended a hundred lines BEFORE the verdict existed; the verdict is now taken one statement earlier and `why`/`held_by` ride the event to the phone (present only for an `auto` policy, since `held_by: []` on a `human` gate would read as "checked, and nothing held it"), the questions that hold a gate are notified BEFORE the gate downstream of them so the owner stops being told to sign a thing before being told what it is, and an `auto` gate whose only blocker was an open question stops permanently degrading into a `human` one — each poll re-runs the seven conditions off disk and signs through the same `approve` door `next` uses, never for `human` and never for `agent`, a person's `approve` or `reject` still landing first; a question can carry its own `Recommended: <letter> — <why> [src: …]`, parsed tolerantly so an older note reads as not recorded rather than refusing; `tldrx facts add --repo <name>` is now checked against `workspace.yml` through the ONE leaf `answer --repo` already refused on, because the unvalidated flag wrote `repos: [ghost]` at exit 0 and every prompt or filter keyed on a real repo name was then silently blind to that fact; the five phase ids are written out in one file instead of three — `PHASE_IDS`, `QUESTION_PHASES`, and an inline literal walked by `questions lint` that `QUESTION_PHASES`' own doc comment claimed could not exist — pinned from both ends, because an identical second copy and a copy that has already drifted redden different guards and neither alone is enough; a training test stops running on bun's fixed 5000 ms after a spawn probe, validated first against a file the heuristic already claimed (87 spawns recorded, so the instrument can see the thing), found it spawning three real children unclaimed by any marker, with the 14 further unclaimed files filed rather than fixed; and `release-check.sh` refuses a rewritten released section — every dated heading whose tag is present must equal `git show vX.Y.Z:CHANGELOG.md` byte for byte, three sections having drifted with nothing checking, and an amendment recorded in `CHANGELOG.amendments` must still contain the tag's section as an ordered subsequence and may only add lines that exist verbatim in its source sha, after a reviewer proved the first version of that check happily passed an invented bullet |
321
322
  | 0.14.0 | 2026-09-09 | `beta` | an `agent` gate the engine can actually close, and three computations of the same answer cut to one: `gates_policy: agent` named who MAY sign a gate but nothing in the engine produced the evidence note it is signed over — measured on 0.13.1, an owner ran `tldrx run gates set what:agent`, was told "an agent may now close it", and the loop stopped at the next gate anyway with exit 4 — so `run auto` now spawns one bounded **gate signer** when a stage's checks pass under an `agent` policy: the stage's own model and effort, a quarter of its per-agent ceiling, a tool allowance that reads anything and writes exactly one file, and a prompt carrying the stage's declared outputs, the seven `auto` conditions as measured and the §2.8 skeleton `gate template` itself renders — the note going through the UNCHANGED `approve --as-agent` path, so a refusal, a note that does not validate, a signer that wrote nothing and a signer that died are one outcome, pending for a person with the reason named on stdout and now in the `gate.requested` summary, and there is no flag to turn it on because an `agent` policy is already the owner's recorded decision; the turn is recorded like any other (`role: gate-signer`, a `run.yml` task row, a row in `tldrx cost`) and taken BEFORE the stage moves to `awaiting_gate`, because the other order had a person sign the gate the engine was mid-signing, three runs out of three; and the suite stops being run three times per change — the pre-merge reviewer now runs only the test files that cover its diff plus `typecheck`, never the full `bun test`, since the wave re-runs every gate on the MERGED tree anyway, `publish.yml` refuses unless `ci` has a `success` run for the same sha instead of recomputing typecheck/tests/build (~87 min/week of runner time, with a `cancelled` ci run failing by name and the remedy), `ci` cancels a run a newer push has already superseded (25 of 122 push runs began under 10 minutes apart), the docs deploy finally fires on `src/cli/helpText.ts` so a help-registry change stops deploying nothing while the published CLI reference goes stale, and `AGENTS.md` §2 now says out loud that a slash in a branch name is a directory, which is what the review-record gate builds |
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-w8qm1jbn.js";
4
+ } from "./chunk-taj1vrbe.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  EventLog,
18
18
  PHASE_ID_RE
19
- } from "./chunk-dnat9z30.js";
19
+ } from "./chunk-mt8f2czb.js";
20
20
  import"./chunk-db7wf3hy.js";
21
21
  import {
22
22
  ADVISORY_KEY,
@@ -32,7 +32,7 @@ import {
32
32
  } from "./chunk-fefnr71h.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-dnat9z30.js";
35
+ } from "./chunk-mt8f2czb.js";
36
36
  import {
37
37
  noteDeprecations
38
38
  } from "./chunk-bqmstq21.js";
@@ -45,6 +45,8 @@ var EVENT_TYPES = [
45
45
  "story.review_retried",
46
46
  "story.work_rescued",
47
47
  "story.touches_widened",
48
+ "worktree.foreign_work_aside",
49
+ "worktree.foreign_work_restored",
48
50
  "result.unreadable",
49
51
  "operator_note",
50
52
  "check.passed",
@@ -22,7 +22,7 @@ import {
22
22
  isTerminal,
23
23
  stageAt,
24
24
  validateRunFile
25
- } from "./chunk-dnat9z30.js";
25
+ } from "./chunk-mt8f2czb.js";
26
26
  import {
27
27
  backupPathFor,
28
28
  isAlive,
@@ -5,7 +5,7 @@ import {
5
5
  evidencePath,
6
6
  gateEvidencePath,
7
7
  parseEvidence
8
- } from "./chunk-dnat9z30.js";
8
+ } from "./chunk-mt8f2czb.js";
9
9
  import {
10
10
  openBlocks,
11
11
  parseQuestions
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  questionsCard
4
- } from "./chunk-w8qm1jbn.js";
4
+ } from "./chunk-taj1vrbe.js";
5
5
  import"./chunk-mzwqb08p.js";
6
6
  import {
7
7
  allow,
@@ -18,7 +18,7 @@ import {
18
18
  openQuestions,
19
19
  runSnapshot,
20
20
  whatIsWaiting
21
- } from "./chunk-97zs34wb.js";
21
+ } from "./chunk-ppgbs5ag.js";
22
22
  import"./chunk-dzkg8c68.js";
23
23
  import {
24
24
  expertsDir,
@@ -28,7 +28,7 @@ import {
28
28
  readExpertDomain,
29
29
  stackExpertNames
30
30
  } from "./chunk-fefnr71h.js";
31
- import"./chunk-dnat9z30.js";
31
+ import"./chunk-mt8f2czb.js";
32
32
  import"./chunk-db7wf3hy.js";
33
33
  import {
34
34
  openBlocks,
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  bar,
4
4
  runSnapshot
5
- } from "./chunk-97zs34wb.js";
5
+ } from "./chunk-ppgbs5ag.js";
6
6
  import"./chunk-dzkg8c68.js";
7
7
  import"./chunk-fefnr71h.js";
8
- import"./chunk-dnat9z30.js";
8
+ import"./chunk-mt8f2czb.js";
9
9
  import"./chunk-db7wf3hy.js";
10
10
  import"./chunk-bqmstq21.js";
11
11
  import {