tldr-experts 0.35.0 → 0.36.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 +174 -0
- package/README.md +1 -0
- package/dist/hooks/answer-capture.js +19 -18
- package/dist/hooks/budget-gate.js +9 -9
- package/dist/hooks/{chunk-4hxdn67p.js → chunk-6cptynke.js} +2 -2
- package/dist/hooks/{chunk-chmh9vva.js → chunk-73h7hmfx.js} +1 -1
- package/dist/hooks/{chunk-qzabvwj1.js → chunk-7p9vw1s2.js} +1 -1
- package/dist/hooks/{chunk-1vqvq0nm.js → chunk-btr3m70g.js} +12 -6
- package/dist/hooks/{chunk-0nh8p76n.js → chunk-fbn9q035.js} +37 -4
- package/dist/hooks/{chunk-jp88kw3z.js → chunk-gqfbnr1h.js} +8 -4
- package/dist/hooks/{chunk-mgjrgmvg.js → chunk-h543njq3.js} +1 -1
- package/dist/hooks/{chunk-gzw2ehqh.js → chunk-kcwv344p.js} +3 -2
- package/dist/hooks/{chunk-gbrgvhjc.js → chunk-mx1g05pq.js} +1 -1
- package/dist/hooks/{chunk-jr5gmwq5.js → chunk-qpy8tgtc.js} +1 -1
- package/dist/hooks/{chunk-prj7pysh.js → chunk-re1efynj.js} +1 -1
- package/dist/hooks/{chunk-hpeazv3q.js → chunk-sqkp3405.js} +3 -3
- package/dist/hooks/{chunk-2yedmja1.js → chunk-y0bk666d.js} +1 -1
- package/dist/hooks/claim-sources.js +5 -5
- package/dist/hooks/dod-gate.js +7 -7
- package/dist/hooks/no-reask.js +8 -8
- package/dist/hooks/session-start.js +12 -12
- package/dist/hooks/statusline.js +8 -8
- package/dist/tldrx.js +1400 -838
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- /package/dist/hooks/{chunk-1thvqqp8.js → chunk-d2g7h2qb.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,179 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.36.0 — 2026-09-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`tldrx facts dedupe [--dry-run]` retires the duplicate facts already on a ledger written
|
|
8
|
+
before the append-time check below existed (see #216).** A field audit measured 27 verbatim
|
|
9
|
+
duplicates in one workspace's `facts.yml` (F028-F054 == F001-F027, one import that ran
|
|
10
|
+
twice), inlined into every what/how/build prompt with no filter. The new subcommand groups
|
|
11
|
+
live facts by normalised text, keeps the earliest id per group, and marks every later member
|
|
12
|
+
`superseded_by` — chained rather than fanned onto the earliest id directly for a group of
|
|
13
|
+
three or more, since the reciprocal link `validateFactsFile` enforces is one-to-one; `headOf`
|
|
14
|
+
still resolves every chained member to the one still live. Nothing is deleted, `--dry-run`
|
|
15
|
+
writes nothing, and a ledger with no duplicates is byte-identical after.
|
|
16
|
+
- **The context ledger carries the facts share as its own line, `facts_bytes` in `pending.json`
|
|
17
|
+
and `(facts N B)` in `--prepare`/`--dry-run` (see #216).** A subset of `inputs_bytes`, the same
|
|
18
|
+
shape `questions_bytes` already is of `stage_bytes` — never summed twice into `total_bytes`.
|
|
19
|
+
The field audit that opened #216 found this share reaching 57% of a bundle with nothing
|
|
20
|
+
anywhere naming it; the number used to exist only as something you could `wc -c` a rendered
|
|
21
|
+
prompt to discover by hand.
|
|
22
|
+
- **The `plan` gate refuses a story whose own body names an edit of a path outside its own
|
|
23
|
+
`touches` allowlist (see #376).** Measured on a live autonomous run (#332): a story's step
|
|
24
|
+
asked for a CHANGELOG bullet under a new `## <v> — unreleased` heading while `CHANGELOG.md`
|
|
25
|
+
sat outside its `touches` — the Build developer correctly left the file untouched under the
|
|
26
|
+
write-allowlist rule, and the pre-merge reviewer had to send the branch back for AGENTS.md
|
|
27
|
+
§5's bullet, exactly the human intervention the autonomy north star measures. Same shape as
|
|
28
|
+
the #365 sequencing rule: a short, exported edit-verb set (`TOUCH_EDIT_VERBS`) and a
|
|
29
|
+
backtick-quoted path pattern (`TOUCHED_PATH_PATTERN`, never a `*` glob) in the SAME sentence
|
|
30
|
+
or bullet — a step that only reads or points at a path is unaffected. A `## <v> —
|
|
31
|
+
unreleased` heading named alongside an edit verb counts as an edit of `CHANGELOG.md` even
|
|
32
|
+
when the step never spells the filename. The issue's second half — a `touches_always:`
|
|
33
|
+
workspace-level allowlist addition — is not in scope here; it stays available as a
|
|
34
|
+
follow-up if this half proves insufficient in the field.
|
|
35
|
+
- **An over-cap `acceptance`/`test_plan` item is now split mechanically, for $0.00, before the
|
|
36
|
+
`plan` check ever judges it (see #352, part of #345 family).** `requireStringList`'s own
|
|
37
|
+
refusal already said "split it into several items"; `checkPlan` now tries exactly that,
|
|
38
|
+
reusing `[src:]`'s own trailing-token span finder (`trailingTokenSpan`, `srcToken.ts`,
|
|
39
|
+
exported for this — one derivation, AGENTS.md §7) so a split never cuts through, or drops, a
|
|
40
|
+
citation: the token is set aside before any sentence boundary is looked for, and copied
|
|
41
|
+
VERBATIM onto every resulting piece, not just the last. `splitOverCapItem`
|
|
42
|
+
(`src/core/text/listSplit.ts`) does the mechanical half; `repairOverCapItems`
|
|
43
|
+
(`src/core/plan/repairOverCapItems.ts`) does the file half — locating the field surgically in
|
|
44
|
+
the story's raw front-matter lines (both the flow-list and block-list shapes a story is seen
|
|
45
|
+
in) and rewriting only that one field, never a full YAML round-trip that would reflow
|
|
46
|
+
everything else's quoting. A split is written back to disk ONLY when every piece is under the
|
|
47
|
+
cap AND re-validates through the same checks the original item failed
|
|
48
|
+
(`requireStringList` + the `[src:]` grammar when the item carried a token) — an item with no
|
|
49
|
+
sentence boundary, or whose split still fails re-validation, is left byte-identical and
|
|
50
|
+
refused exactly as before, including by the existing `plan-fix` round (#288) when applicable.
|
|
51
|
+
Because the repair runs inside the SAME check pass, a formatting-only over-cap item never
|
|
52
|
+
even reaches that bounded, paid round. `CheckOutcome` grows an additive `repairs?` field
|
|
53
|
+
(`src/core/run/checks.ts`), surfaced on the stage's own report line and on the
|
|
54
|
+
`check.passed`/`check.failed` event — the same "an auto-repair must be visible" owner
|
|
55
|
+
decision (2026-09-15) #345's Watch repair already follows.
|
|
56
|
+
- **A retry of a failed single-agent stage whose declared outputs are already complete and
|
|
57
|
+
valid on disk settles at $0.00 instead of buying a fresh turn to reproduce them (see #353,
|
|
58
|
+
part of #345 family).** MEASURED first: `runStage`'s headless path called `spawnAgent`
|
|
59
|
+
unconditionally on every re-entry — `validateOutputs` was only ever reached from
|
|
60
|
+
`finishStage`, AFTER a turn was dispatched and paid for, so a stage that failed for a reason
|
|
61
|
+
unrelated to its declared outputs (a `checks:` failure since fixed by hand, a timeout after
|
|
62
|
+
the last file was flushed) bought a full fresh turn to redo work already on disk. Now, on
|
|
63
|
+
re-entry to a stage whose `status` is already `"failed"` (never a first attempt, and headless
|
|
64
|
+
mode only — never `--prepare`/`--commit`, never `--dry-run`), `trySettleFromDisk` runs
|
|
65
|
+
`validateOutputs` against what is already there and, when the stage declares any `checks:`,
|
|
66
|
+
runs them for real too (they are local — free to run, and a checks failure there means the
|
|
67
|
+
"already finished" premise is false and a real turn is still owed, exactly as today). Only
|
|
68
|
+
when BOTH clear does the stage settle: one task row (`status: "done"`, `cost_usd: 0`,
|
|
69
|
+
`model: null`, `session_id: null`) carrying the additive `settled_from: "prior-attempt
|
|
70
|
+
outputs"` (`RunFile.ts`, and `emitRunYaml.ts`'s hand-written task emitter, which needed the
|
|
71
|
+
new key taught to it the same way #375's `exit_disagreement` did), and a matching
|
|
72
|
+
`agent.result` event — named on the stage's own report line too, never silently. Any problem
|
|
73
|
+
falls straight through to the ordinary spawn path, unchanged. **Pre-merge review
|
|
74
|
+
(2026-09-18): structurally valid files are not proof of finished work when the agent that
|
|
75
|
+
wrote them reported the failure itself.** `trySettleFromDisk` now refuses to settle before
|
|
76
|
+
even looking at disk when the prior attempt's own task row names an agent-reported
|
|
77
|
+
`failure_kind` (`AGENT_REPORTED_FAILURE_KINDS`, `spawnAgent.ts`: `result_error`,
|
|
78
|
+
`malformed_result`, `empty_result`, `asked_no_diff`), `unclassified`, an absent kind on a
|
|
79
|
+
`"failed"` row, or `non_zero_exit` whose own `error` text shows a result document that parsed
|
|
80
|
+
and disagreed with itself (`… with is_error=true: …`) — only a `checks:`-only failure or the
|
|
81
|
+
wall dying on it (`timeout`, `process_killed`, `rate_limit`, or a clean `non_zero_exit` with
|
|
82
|
+
nothing parseable) settles. Named on the report line as `prior attempt reported <kind>;
|
|
83
|
+
spawning`. A knob two `run auto` retry-counting test fixtures grew to work around the
|
|
84
|
+
original, too-permissive settle is removed along with it — the failure-kind gate alone keeps
|
|
85
|
+
their canned "fails then succeeds" spawns real, which is the smaller, more honest fix.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **A turn killed by the provider's rate limit with no attested `rate_limit_event` frame can
|
|
90
|
+
now be classified `rate_limit` from its raw text (part of #341).** `describeFailure`'s
|
|
91
|
+
residual `unclassified` bucket now checks stdout/stderr against a small exported regex
|
|
92
|
+
(`RATE_LIMIT_TEXT_RE`) for the REPORTED (not measured — no raw capture of a turn that
|
|
93
|
+
actually died against the wall exists in this repo) shape of a session-limit API error
|
|
94
|
+
before giving up; it never overrides a more specific cause (timeout, a kill signal, a
|
|
95
|
+
malformed envelope, a named error or subtype disagreement). The frame-attested capture
|
|
96
|
+
#341 actually asks for still does not exist — this narrows the "no reason named" bucket,
|
|
97
|
+
it does not close the issue.
|
|
98
|
+
- **`renderFacts`'s rendered list — Build's `### Facts already on record` and the `{{facts}}`
|
|
99
|
+
template value alike — sits under a byte ceiling (`DEFAULT_FACTS_MAX_BYTES`, 32 KB) for the
|
|
100
|
+
first time (see #216).** Previously unbounded: one measured section reached 123,938 B, 57% of
|
|
101
|
+
a 218 KB bundle. Over the ceiling the list is cut on a WHOLE fact, never mid-word (#161), and
|
|
102
|
+
the cut is named on the page rather than silently dropped.
|
|
103
|
+
- **What and How stop receiving `.tldrx/memory/facts.yml` raw — an INDEX rides in its place
|
|
104
|
+
(owner decision "Index", 2026-09-18; see #216).** Measured on a 120-fact fixture ledger: a
|
|
105
|
+
48,150 B `## Inputs` entry became a 17,490 B index (63.7% smaller), one line per live fact —
|
|
106
|
+
`- [F<n>] <area> · <first ~120 chars>…` — with a closing pointer at the file for the full
|
|
107
|
+
text. Build and Watch declare the same input in their `stage.yml` too but read facts through
|
|
108
|
+
their own executors, never `inlineInputs`, so they are unaffected — measured, the declaration
|
|
109
|
+
there was already unused. The `## Inputs` preamble marks the summarised file with its own
|
|
110
|
+
sentence instead of the "nothing else to find" line that would otherwise contradict
|
|
111
|
+
what/how's own prose telling the agent to grep the real file.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
|
|
115
|
+
- **The Build executor's `ExecutorTask` rows never copied `AgentOutcome.failureKind` across, so a
|
|
116
|
+
Build developer or reviewer turn that died with a classifiable cause carried no
|
|
117
|
+
`failure_kind` on its `run.yml` row (see #357, see #348).** `ExecutorTask.failureKind` and
|
|
118
|
+
its `recordExecutorTasks` mapping already existed; the four `this.tasks.push` sites in
|
|
119
|
+
`executors/build.ts` were the only thing not copying it. The spawned-developer and
|
|
120
|
+
spawned-reviewer sites (and the format-retry row beside them) now do; the two HOST-turn
|
|
121
|
+
push sites, which have no `AgentOutcome` to read a kind off, are unchanged and continue to
|
|
122
|
+
leave the key absent rather than guess one. `test/build-golden.test.ts`'s `rounds-run-tasks.txt`
|
|
123
|
+
golden is regenerated ON PURPOSE — a failed developer's row in that fixture now carries
|
|
124
|
+
`failure_kind: "non_zero_exit"`, which it did not before; this is the change this commit
|
|
125
|
+
means, not a drift.
|
|
126
|
+
- **A spawned turn whose result document said `subtype: "success"` but whose process exit
|
|
127
|
+
code disagreed settled as a stage failure — costing a full `run auto` relaunch for work
|
|
128
|
+
that was already done (see #375, see #348).** Measured on a live autonomous run: a watch
|
|
129
|
+
developer wrote its declared output, `claude` exited 1 with `is_error: true`, and the
|
|
130
|
+
result document's own `subtype` said `"success"`; the facilitator classified this as
|
|
131
|
+
`unclassified` ("no reason named (the provider's own subtype said "success")") and `run
|
|
132
|
+
auto` spent one of five relaunches re-running work attempt 1 had already finished. The
|
|
133
|
+
three headless spawn sites in `runNext.ts` now settle the turn as `done` — recording the
|
|
134
|
+
disagreement as an additive `exit_disagreement` note on the row and the `agent.result`
|
|
135
|
+
event — whenever every output the stage declared is on disk and non-empty at the moment
|
|
136
|
+
the verdict is taken (`exitDisagreement.ts`'s `settleExitDisagreement`, one derivation,
|
|
137
|
+
called from all three rather than pasted three times); a turn missing even one declared
|
|
138
|
+
output stays `failed` under its ordinary `failure_kind`, exactly as before. The Build
|
|
139
|
+
executor's `spawnDeveloper` path has the same exit/subtype disagreement shape but no
|
|
140
|
+
per-story declared-outputs list to check existence against, so it is deliberately left
|
|
141
|
+
out of this change — see the follow-up draft at
|
|
142
|
+
`scratchpad/issue-draft-375-build-spawndeveloper-exit-disagreement.md`.
|
|
143
|
+
- **`FactsStore.append` dedupes on normalised text, so re-running a driver's import or
|
|
144
|
+
`tldrx facts add` on the same sentence no longer mints a second row (see #216).** Both
|
|
145
|
+
writers — `captureAnswers` and `tldrx facts add` — now get the existing live fact back
|
|
146
|
+
(`duplicate: true`) instead of a new one when the incoming text, trimmed/whitespace-
|
|
147
|
+
collapsed/case-folded, matches a LIVE fact's; a retired or superseded twin does not block a
|
|
148
|
+
fresh assertion of the same text. `facts add` prints "`<id>` is already on record" and exits
|
|
149
|
+
0 rather than writing a duplicate.
|
|
150
|
+
- **`merge-wave.sh`'s #336 immutability guard no longer prints `printf: write error: Broken
|
|
151
|
+
pipe` once per dated CHANGELOG section (see #380).** Its `changelog_section()` awk helper
|
|
152
|
+
`exit`ed as soon as it saw the NEXT `## ` heading, while the `printf | changelog_section`
|
|
153
|
+
pipe feeding it the pre-merge CHANGELOG was still writing the rest of the file — so the
|
|
154
|
+
writer could get EPIPE (measured live: 45-46 lines of noise per wave, once per dated
|
|
155
|
+
section that was not the last one in the file). `exit` is now `p = 0`: the awk process
|
|
156
|
+
keeps consuming to EOF instead of closing its end of the pipe early, so the writer always
|
|
157
|
+
finishes; the guard's own verdict (`$WAS`/`$NOW`) is unchanged.
|
|
158
|
+
- **`emitFactsYaml` writes `facts: []` for an empty list instead of a bare `facts:` line (see
|
|
159
|
+
#383).** Both YAML readers behind the runtime seam parse a key with nothing after it as
|
|
160
|
+
`null`, not `[]`, so a facts.yml with zero facts never round-tripped — `FactsStore.save()`
|
|
161
|
+
re-validates its own serialized output before writing and threw on exactly this file,
|
|
162
|
+
`refusing to write an invalid facts.yml: facts expected an array, got null`. Latent until
|
|
163
|
+
now (every shipped writer appends a fact before calling `save()`), but reachable from any
|
|
164
|
+
fresh `FactsStore.loadOrEmpty(...).save()` with nothing appended; non-empty output is
|
|
165
|
+
unchanged.
|
|
166
|
+
- **Watch's kept-card pre-pass (#306) never got the `[src:]` mechanical repair the
|
|
167
|
+
validation loop a few lines below it already had, so a card left on disk with only a
|
|
168
|
+
punctuation slip — the marker missing its space, a token sitting mid-sentence, an ASCII
|
|
169
|
+
`->` in a `cmd` source — re-spawned a writer for text a script already knows how to fix
|
|
170
|
+
(see #351, part of #345).** `keptCard` now runs the same `repairSrcSyntax` before its
|
|
171
|
+
`parseWatcherCard` check: a repair that turns an invalid card valid is written back to
|
|
172
|
+
disk and the feature is kept, at $0.00, with the repair named on the stage's own report
|
|
173
|
+
line the same way the validation loop names its own. A card that stays invalid after
|
|
174
|
+
repair is still not kept (today's behaviour, byte-identical file); a card already valid
|
|
175
|
+
needs no repair and is never rewritten.
|
|
176
|
+
|
|
3
177
|
## 0.35.0 — 2026-09-17
|
|
4
178
|
|
|
5
179
|
### 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.36.0 | 2026-09-18 | `beta` | Wave six, the P1 cost and autonomy backlog, every branch reviewed before main: facts.yml stops riding whole into every prompt (a one-line index for what/how, a named byte ceiling for Build, dedupe at append and `tldrx facts dedupe`, the facts share visible in the context ledger); a headless turn whose exit code disagrees with its own `subtype: success` settles as done with the disagreement recorded instead of buying a relaunch; Build rows carry `failure_kind`; a rate-limit death is classified from its raw text; three more savings before a paid turn — kept Watch cards get the `[src:]` repair, over-cap acceptance items are split mechanically, and a retry whose prior attempt died by the wall with complete outputs settles at $0, never when the agent itself reported the failure; the plan gate refuses a step that edits a path outside its `touches`; an empty facts ledger round-trips; merge-wave stops spraying Broken pipe — 10 issues |
|
|
338
339
|
| 0.35.0 | 2026-09-17 | `beta` | Wave five of the audited backlog, every branch reviewed before main: a human gate approval whose note names the fixing commit closes that fix-list finding (reachable from the story's own branch, or `claimed-unverified` — never a silent close), a fact keeps a `was_id` alias across an id change, `validateWorkspace` enforces the quote-aware single-argv rule the DoD gate already applied, `tldrx story reopen` refuses a finished or Build-approved run and names the way back, a damaged `budget.yml` reads as absent-with-reason instead of resurrecting a frozen ceiling, the fix list's on-disk door is documented as the human escape hatch, and the public surface stops naming private workspaces — fixtures are synthetic, citations carry neutral labels, and a blanket guard refuses the next one — 9 issues |
|
|
339
340
|
| 0.34.0 | 2026-09-17 | `beta` | Four waves of the audited backlog, every branch reviewed before main: run auto no longer stops itself (parked auto gates self-close, Build parks at a story boundary on budget), gates cannot sign stale or backdated evidence, interrupted turns bank their cost as absent instead of a measured zero, the boundary condition diffs the branch the run recorded, mutating generators are refused from dod blocks, merge-wave refuses edits to released CHANGELOG sections and answers `unknown` instead of `idle`, and the test harness refuses a TMPDIR inside a work tree — 14 issues, the first of them shipped end-to-end by a tldrx run against this repo |
|
|
340
341
|
| 0.33.0 | 2026-09-17 | `beta` | Six follow-ups from the 0.32.0 cycle, each measured on the second unattended run (run #42) or on the review that shipped it. The plan sequencing rule counts a bare camelCase field only when it is unambiguous or the plan spells it elsewhere as snake_case or in backticks, so unrelated stories sharing a word like `toString` no longer refuse each other (#370); `task.done` clamps an oversized `as_is_note` the way it clamps `permission_refused` (#368); the asked-no-diff and red-DoD requeues settle through one helper that takes data (#369); a workspace can opt its base pre-flight probe into a story-shaped worktree with `probe_in_worktree: true`, and the Build-entry probe runs `tool_restore:` first (#371, the remainder of #363); AGENTS §11 names the negated closing verb that still auto-closes and widens the pre-wave grep (#372); and CI keeps merge-wave's sandboxes on failure so the next #237 red carries its `merge.log` (#373). Minor release: a new workspace field and a new plan refusal shape. |
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
conflictOf
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-sqkp3405.js";
|
|
5
5
|
import {
|
|
6
6
|
FactsStore,
|
|
7
7
|
formatJaccard
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-fbn9q035.js";
|
|
9
9
|
import {
|
|
10
10
|
parseHookInput,
|
|
11
11
|
readStdin
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-73h7hmfx.js";
|
|
13
13
|
import"./chunk-ae6bkfs5.js";
|
|
14
14
|
import {
|
|
15
15
|
EventLog,
|
|
16
16
|
PHASE_ID_RE
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-6cptynke.js";
|
|
18
18
|
import {
|
|
19
19
|
PHASE_IDS
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import"./chunk-
|
|
20
|
+
} from "./chunk-mx1g05pq.js";
|
|
21
|
+
import"./chunk-gqfbnr1h.js";
|
|
22
22
|
import {
|
|
23
23
|
ADVISORY_KEY,
|
|
24
24
|
MAX_FACT_CHARS,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
renderQuestionBlock,
|
|
29
29
|
replaceBlock,
|
|
30
30
|
serializeQuestions
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-7p9vw1s2.js";
|
|
32
32
|
import {
|
|
33
33
|
ITERATION_ONLY_SLOT,
|
|
34
34
|
PROJECT_FRAMEWORK_DIR,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
isScopedTemplate,
|
|
39
39
|
parseYaml,
|
|
40
40
|
scopedSlotOf
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-d2g7h2qb.js";
|
|
42
42
|
|
|
43
43
|
// src/hooks/answer-capture.ts
|
|
44
44
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -466,7 +466,7 @@ function captureAnswers(questionsPath, ctx) {
|
|
|
466
466
|
const prov = answerProvenance(block, ctx.overrides, ctx.repoNames, []);
|
|
467
467
|
const text = factTextFor(block.title, block.answer);
|
|
468
468
|
const clash = conflictOf({ match: block.title, area, text }, store.active);
|
|
469
|
-
const fact = store.append({
|
|
469
|
+
const { fact, duplicate } = store.append({
|
|
470
470
|
fact: text,
|
|
471
471
|
...truncated ? { truncated: true } : {},
|
|
472
472
|
...clash === null ? {} : { conflicts_with: [clash.fact.id] },
|
|
@@ -489,15 +489,16 @@ function captureAnswers(questionsPath, ctx) {
|
|
|
489
489
|
cost_usd: 0,
|
|
490
490
|
payload: { q: block.id, answer: block.answer, fact: fact.id }
|
|
491
491
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
492
|
+
if (!duplicate)
|
|
493
|
+
log.tryAppend({
|
|
494
|
+
ts: ctx.at,
|
|
495
|
+
run: ctx.run,
|
|
496
|
+
stage: null,
|
|
497
|
+
type: "fact.added",
|
|
498
|
+
actor: ctx.actor,
|
|
499
|
+
cost_usd: 0,
|
|
500
|
+
payload: { fact: fact.id, area: fact.area, kind: fact.kind, q: block.id }
|
|
501
|
+
});
|
|
501
502
|
captured.push({
|
|
502
503
|
q: block.id,
|
|
503
504
|
fact: fact.id,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
budgetGateDeny
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-h543njq3.js";
|
|
5
5
|
import {
|
|
6
6
|
allow,
|
|
7
7
|
deny,
|
|
8
8
|
readPayload,
|
|
9
9
|
runHook,
|
|
10
10
|
toolInput
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import"./chunk-
|
|
11
|
+
} from "./chunk-re1efynj.js";
|
|
12
|
+
import"./chunk-73h7hmfx.js";
|
|
13
13
|
import {
|
|
14
14
|
REBALANCE_SOURCE,
|
|
15
15
|
applyRebalance,
|
|
@@ -25,12 +25,12 @@ import {
|
|
|
25
25
|
validateRunBudget,
|
|
26
26
|
wouldExceed,
|
|
27
27
|
wouldExceedHostTokens
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-kcwv344p.js";
|
|
29
29
|
import {
|
|
30
30
|
EventLog,
|
|
31
31
|
currentActor,
|
|
32
32
|
nowRfc3339
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-6cptynke.js";
|
|
34
34
|
import {
|
|
35
35
|
cursorStage,
|
|
36
36
|
hostTokensIn,
|
|
@@ -39,20 +39,20 @@ import {
|
|
|
39
39
|
newestActiveRun,
|
|
40
40
|
renderRunEconomies,
|
|
41
41
|
runSpend
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-qpy8tgtc.js";
|
|
43
43
|
import {
|
|
44
44
|
buildStageDefaults
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-mx1g05pq.js";
|
|
46
46
|
import {
|
|
47
47
|
noteDeprecations
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-7p9vw1s2.js";
|
|
49
49
|
import {
|
|
50
50
|
PROJECT_WORK_DIR,
|
|
51
51
|
findWorkspaceRoot,
|
|
52
52
|
locateWork,
|
|
53
53
|
parseYaml,
|
|
54
54
|
stageYamlPath
|
|
55
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-d2g7h2qb.js";
|
|
56
56
|
|
|
57
57
|
// src/hooks/budget-gate.ts
|
|
58
58
|
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
GATE_POLICIES,
|
|
3
3
|
validateGatesPolicy,
|
|
4
4
|
validateStagePolicy
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-mx1g05pq.js";
|
|
6
6
|
import {
|
|
7
7
|
asDocument,
|
|
8
8
|
isRecord,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
requireString,
|
|
16
16
|
requireVersion,
|
|
17
17
|
result
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-d2g7h2qb.js";
|
|
19
19
|
|
|
20
20
|
// src/core/events/EventLog.ts
|
|
21
21
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
storyDependsOn,
|
|
11
11
|
tallyOf,
|
|
12
12
|
validateRunBudget
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-kcwv344p.js";
|
|
14
14
|
import {
|
|
15
15
|
EventLog,
|
|
16
16
|
OUTCOME_NOT_RECORDED,
|
|
@@ -24,12 +24,12 @@ import {
|
|
|
24
24
|
nowRfc3339,
|
|
25
25
|
stageAt,
|
|
26
26
|
validateRunFile
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-6cptynke.js";
|
|
28
28
|
import {
|
|
29
29
|
cursorStage,
|
|
30
30
|
isAttendedByHostView,
|
|
31
31
|
openRunViews
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-qpy8tgtc.js";
|
|
33
33
|
import {
|
|
34
34
|
backupPathFor,
|
|
35
35
|
isAlive,
|
|
@@ -38,13 +38,13 @@ import {
|
|
|
38
38
|
workspaceRootOfRunDir,
|
|
39
39
|
writeAtomic,
|
|
40
40
|
yamlScalar
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-gqfbnr1h.js";
|
|
42
42
|
import {
|
|
43
43
|
isAdvisory,
|
|
44
44
|
noteDeprecations,
|
|
45
45
|
openBlocks,
|
|
46
46
|
parseQuestions
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-7p9vw1s2.js";
|
|
48
48
|
import {
|
|
49
49
|
FRAMEWORK_ROOT,
|
|
50
50
|
MAX_ITEM_CHARS,
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
parseYaml,
|
|
56
56
|
parseYamlRepairing,
|
|
57
57
|
withoutSrcToken
|
|
58
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-d2g7h2qb.js";
|
|
59
59
|
|
|
60
60
|
// src/core/statusline/runSnapshot.ts
|
|
61
61
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
@@ -140,6 +140,8 @@ function task(t, indent) {
|
|
|
140
140
|
`${inner}started_at: ${yamlScalar(t.started_at)}, ended_at: ${yamlScalar(t.ended_at)},`,
|
|
141
141
|
...t.stopped_by === undefined || t.stopped_by === null ? [] : [`${inner}stopped_by: ${yamlScalar(t.stopped_by)},`],
|
|
142
142
|
...t.failure_kind === undefined || t.failure_kind === null ? [] : [`${inner}failure_kind: ${yamlScalar(t.failure_kind)},`],
|
|
143
|
+
...t.exit_disagreement === undefined || t.exit_disagreement === null ? [] : [`${inner}exit_disagreement: ${yamlScalar(t.exit_disagreement)},`],
|
|
144
|
+
...t.settled_from === undefined || t.settled_from === null ? [] : [`${inner}settled_from: ${yamlScalar(t.settled_from)},`],
|
|
143
145
|
...t.banked_before_refusal === undefined ? [] : [`${inner}banked_before_refusal: true,`],
|
|
144
146
|
...t.dedupe === undefined ? [] : [`${inner}dedupe: ${yamlScalar(t.dedupe)},`],
|
|
145
147
|
...t.duration_ms === undefined ? [] : [`${inner}duration_ms: ${String(Math.round(t.duration_ms))}, ` + `duration_basis: ${yamlScalar(t.duration_basis ?? null)},`],
|
|
@@ -665,6 +667,10 @@ var PLAN_SHAPE_RULES = [
|
|
|
665
667
|
{
|
|
666
668
|
issue: "#365",
|
|
667
669
|
text: "**A story that adds an invariant over EXISTING data lands in the same story as, or in a wave after, the " + "story that makes existing rows and fixtures satisfy it.** A check constraint, a `NOT NULL`, a required " + "field or a validation rule written ahead of the story that populates what it enforces makes that earlier " + "story's own dod structurally red — every fixture the enforcing story's own tests run against still lacks " + "the value, and two stories in the SAME wave run in separate worktrees that never see each other's writes, " + "so 'later wave' is the only way one story's data can satisfy another's rule. If the invariant must land " + "first, write it NON-ENFORCING — nullable, consistency-only, no constraint — and let the later story " + "tighten it once the data exists. The `plan` check refuses a story whose acceptance or test plan names an " + "enforcement over a field a later story's acceptance or test plan names populating."
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
issue: "#376",
|
|
673
|
+
text: "**A story's own body never names an edit of a file its own `touches` forbids.** A step that tells the " + "developer to edit, add, write, create, rename, delete or otherwise change a backtick-quoted path — or to " + "add a bullet under a `## <v> — unreleased` CHANGELOG heading, which is an edit of `CHANGELOG.md` even when " + "the step never spells the filename — is a plan contradicting its own write allowlist: the developer who " + "obeys `touches` correctly leaves the file untouched, and a reviewer has to send the branch back for it. " + "The `plan` check refuses a story whose body names such an edit outside its own `touches`; a step that " + 'only reads or points at a path ("read `x` for the shape") is unaffected.'
|
|
668
674
|
}
|
|
669
675
|
];
|
|
670
676
|
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
withWorkspaceLock,
|
|
4
4
|
workspaceRootOfFactsPath,
|
|
5
5
|
writeAtomic
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-gqfbnr1h.js";
|
|
7
7
|
import {
|
|
8
8
|
FACT_CONFIDENCES,
|
|
9
9
|
FACT_DECIDERS,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
isLive,
|
|
16
16
|
isRetired,
|
|
17
17
|
noteDeprecations
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-7p9vw1s2.js";
|
|
19
19
|
import {
|
|
20
20
|
SRC_PATTERNS,
|
|
21
21
|
asDocument,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
requireString,
|
|
29
29
|
requireVersion,
|
|
30
30
|
result
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-d2g7h2qb.js";
|
|
32
32
|
|
|
33
33
|
// src/core/facts/FactsStore.ts
|
|
34
34
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -227,6 +227,11 @@ function findDuplicate(question, area, facts, threshold = DEFAULT_JACCARD_THRESH
|
|
|
227
227
|
return best;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// src/core/facts/normaliseFactText.ts
|
|
231
|
+
function normaliseFactText(text) {
|
|
232
|
+
return text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
|
|
230
235
|
// src/core/facts/FactsStore.ts
|
|
231
236
|
var NEW_FILE_HEADER = `# .tldrx/memory/facts.yml — durable, provenanced answers (spec §2.5).
|
|
232
237
|
` + "# Append-mostly: supersede or retire, never edit. A fact without a source is not a fact.";
|
|
@@ -271,6 +276,13 @@ class FactsStore {
|
|
|
271
276
|
return formatFactId(highest + 1);
|
|
272
277
|
}
|
|
273
278
|
append(input) {
|
|
279
|
+
const target = normaliseFactText(input.fact);
|
|
280
|
+
const existing = this.rows.find((f) => isLive(f) && normaliseFactText(f.fact) === target);
|
|
281
|
+
if (existing !== undefined)
|
|
282
|
+
return { fact: existing, duplicate: true };
|
|
283
|
+
return { fact: this.appendRaw(input), duplicate: false };
|
|
284
|
+
}
|
|
285
|
+
appendRaw(input) {
|
|
274
286
|
const cut = input.fact.length > MAX_FACT_CHARS;
|
|
275
287
|
const fact = {
|
|
276
288
|
id: this.nextId(),
|
|
@@ -310,7 +322,7 @@ class FactsStore {
|
|
|
310
322
|
}
|
|
311
323
|
if (isRetired(old))
|
|
312
324
|
throw new Error(`${oldId} is retired; a retired fact is not superseded`);
|
|
313
|
-
const replacement = this.
|
|
325
|
+
const replacement = this.appendRaw({ ...input, supersedes: oldId });
|
|
314
326
|
this.rows[index] = { ...old, superseded_by: replacement.id };
|
|
315
327
|
return replacement;
|
|
316
328
|
}
|
|
@@ -326,6 +338,27 @@ class FactsStore {
|
|
|
326
338
|
this.rows[index] = retired;
|
|
327
339
|
return retired;
|
|
328
340
|
}
|
|
341
|
+
retireDuplicate(id, by) {
|
|
342
|
+
const index = this.rows.findIndex((f) => f.id === id);
|
|
343
|
+
if (index === -1)
|
|
344
|
+
throw new Error(`cannot supersede ${id}: no such fact`);
|
|
345
|
+
const old = this.rows[index];
|
|
346
|
+
if (old.superseded_by !== null) {
|
|
347
|
+
throw new Error(`${id} is already superseded by ${old.superseded_by}`);
|
|
348
|
+
}
|
|
349
|
+
if (isRetired(old))
|
|
350
|
+
throw new Error(`${id} is retired; a retired fact is not superseded`);
|
|
351
|
+
const byIndex = this.rows.findIndex((f) => f.id === by);
|
|
352
|
+
if (byIndex === -1)
|
|
353
|
+
throw new Error(`cannot supersede ${id} by ${by}: no such fact`);
|
|
354
|
+
const target = this.rows[byIndex];
|
|
355
|
+
if (target.supersedes !== null) {
|
|
356
|
+
throw new Error(`${by} already supersedes ${target.supersedes}; the reciprocal link is one-to-one`);
|
|
357
|
+
}
|
|
358
|
+
this.rows[index] = { ...old, superseded_by: by };
|
|
359
|
+
this.rows[byIndex] = { ...target, supersedes: id };
|
|
360
|
+
return this.rows[index];
|
|
361
|
+
}
|
|
329
362
|
findDuplicate(question, area, threshold) {
|
|
330
363
|
return findDuplicate(question, area, this.active, threshold);
|
|
331
364
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROJECT_FRAMEWORK_DIR,
|
|
3
3
|
PROJECT_WORK_DIR
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-d2g7h2qb.js";
|
|
5
5
|
|
|
6
6
|
// src/core/lock/workspaceLock.ts
|
|
7
7
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, rmSync as rmSync2, writeSync, closeSync } from "node:fs";
|
|
@@ -174,9 +174,13 @@ function emitFactsYaml(file, header) {
|
|
|
174
174
|
if (header !== undefined && header !== "")
|
|
175
175
|
out.push(header.replace(/\n$/, ""));
|
|
176
176
|
out.push(`version: ${file.version}`);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
if (file.facts.length === 0) {
|
|
178
|
+
out.push("facts: []");
|
|
179
|
+
} else {
|
|
180
|
+
out.push("facts:");
|
|
181
|
+
for (const fact of file.facts)
|
|
182
|
+
out.push(emitFact(fact));
|
|
183
|
+
}
|
|
180
184
|
return `${out.join(`
|
|
181
185
|
`)}
|
|
182
186
|
`;
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
EMPTY_SECTION_RULE,
|
|
4
4
|
noneBullet,
|
|
5
5
|
srcRule
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-d2g7h2qb.js";
|
|
7
7
|
|
|
8
8
|
// src/hooks/lib/messages.ts
|
|
9
9
|
var GRAMMAR = "[src: <repo:path:line> | https://… | Q<n> | F<n> | $ <cmd> → exit <n> | graph:<node> | absent:<path>]";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
STAGE_TUNING_DEFAULTS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-mx1g05pq.js";
|
|
4
4
|
import {
|
|
5
5
|
MAX_PLAN_STORIES,
|
|
6
6
|
MAX_STORIES_PER_WAVE,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
requireVersion,
|
|
27
27
|
requireVersion1,
|
|
28
28
|
result
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-d2g7h2qb.js";
|
|
30
30
|
|
|
31
31
|
// src/core/budget/spentFigure.ts
|
|
32
32
|
var SPENT_BASES = ["lower-bound", "complete"];
|
|
@@ -680,6 +680,7 @@ function stackExpertNames(root, repos) {
|
|
|
680
680
|
|
|
681
681
|
// src/core/facilitator/prompt.ts
|
|
682
682
|
var MAX_PREVIOUS_ATTEMPT_BYTES = 32 * 1024;
|
|
683
|
+
var DEFAULT_FACTS_MAX_BYTES = 32 * 1024;
|
|
683
684
|
|
|
684
685
|
// src/core/text/srcGrammarContract.ts
|
|
685
686
|
var KINDS = {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findDuplicate
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-fbn9q035.js";
|
|
4
4
|
import {
|
|
5
5
|
evidencePath,
|
|
6
6
|
gateEvidencePath,
|
|
7
7
|
parseEvidence
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6cptynke.js";
|
|
9
9
|
import {
|
|
10
10
|
openBlocks,
|
|
11
11
|
parseQuestions
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-7p9vw1s2.js";
|
|
13
13
|
|
|
14
14
|
// src/core/distill/distill.ts
|
|
15
15
|
var CONFLICT_THRESHOLD = 0.6;
|