tldr-experts 0.27.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +2 -0
  3. package/dist/hooks/answer-capture.js +9 -9
  4. package/dist/hooks/budget-gate.js +9 -9
  5. package/dist/hooks/{chunk-jpp137mb.js → chunk-48ctk154.js} +3 -3
  6. package/dist/hooks/{chunk-41k19q8t.js → chunk-abswp1vt.js} +3 -2
  7. package/dist/hooks/{chunk-d0jhr5wh.js → chunk-c62y35wh.js} +3 -3
  8. package/dist/hooks/{chunk-2qkbh9b6.js → chunk-e618k6te.js} +2 -2
  9. package/dist/hooks/{chunk-0sy3tmfg.js → chunk-f6t1xc3f.js} +26 -3
  10. package/dist/hooks/{chunk-w685jj9h.js → chunk-fjeefnqr.js} +7 -6
  11. package/dist/hooks/{chunk-bznt6jb2.js → chunk-g7wz339n.js} +1 -1
  12. package/dist/hooks/{chunk-yre2scxn.js → chunk-hws2gnxj.js} +1 -1
  13. package/dist/hooks/{chunk-qzsxhsyj.js → chunk-khjm356r.js} +1 -1
  14. package/dist/hooks/{chunk-5stgwp3g.js → chunk-khy21x6e.js} +30 -12
  15. package/dist/hooks/{chunk-ka7768vx.js → chunk-q3d4xdhb.js} +1 -1
  16. package/dist/hooks/{chunk-t44983nq.js → chunk-tna1bz8r.js} +1 -1
  17. package/dist/hooks/{chunk-m1s8a6s0.js → chunk-veffvzw5.js} +2 -2
  18. package/dist/hooks/{chunk-n5bxz2cw.js → chunk-zrwq6r7k.js} +1 -1
  19. package/dist/hooks/{chunk-ezfvp5kx.js → chunk-zvxgdssb.js} +1 -1
  20. package/dist/hooks/claim-sources.js +5 -5
  21. package/dist/hooks/dod-gate.js +8 -8
  22. package/dist/hooks/no-reask.js +8 -8
  23. package/dist/hooks/session-start.js +13 -13
  24. package/dist/hooks/statusline.js +9 -9
  25. package/dist/tldrx.js +881 -274
  26. package/package.json +1 -1
  27. package/plugin/.claude-plugin/plugin.json +1 -1
  28. package/templates/questions.md +2 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,97 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.28.1 — 2026-09-15
4
+
5
+ ### Fixed
6
+
7
+ - **The note a story was reopened with now reaches the developer and the reviewer (closes #322).**
8
+ On a field run a person reopened a story with a note naming the exact gap two reviews had
9
+ refused it for; the next developer made a docstring-only commit and a reviewer approved it, and
10
+ neither turn had been shown the note. It was written only into `story.reopened` and read back
11
+ for a report line and #308's no-diff check — while the refusal for a `--for-fix` with no note
12
+ told the operator the note "is what scopes the fix round, and what the reviewer reads". Measured
13
+ before the fix: 0 of the post-reopen developer and reviewer prompts contained the note, for a
14
+ plain reopen and for `--for-fix`, and after a plain reopen the developer prompt had no
15
+ `## Previous attempt` either, so it was told nothing about why the story came back. Both
16
+ prompts now carry `## Why this story was reopened` — who signed it, whether it is a fix round,
17
+ the note verbatim — read off the same ledger value #308 already reads, so the prompt and the
18
+ refusal cannot name different notes; a story nobody reopened renders nothing new. The
19
+ reviewer's copy tells it that a diff leaving the named gap as it was is `changes`, but that
20
+ instruction is **prompt-advisory**: the framework does not enforce a `changes` verdict against
21
+ the note, and an `approve` is still accepted as the reviewer's judgement.
22
+ - **`tldrx note --help` says where an operator note goes, and where it does not (closes #151,
23
+ docs only).** An `operator_note` is read by `run status`, `replay` and the dashboard and by no
24
+ prompt — so a note an owner wrote for the next stage's agents silently reached nobody. The gap
25
+ was the unwritten contract, not a missing reader: the help and the CLI reference now say the
26
+ note is for people and name `.agent/<stage>/dispatch-notes.md` as the channel that reaches
27
+ agents.
28
+
29
+ ## 0.28.0 — 2026-09-15
30
+
31
+ ### Added
32
+
33
+ - **A story whose merge-up to its epic conflicts in a few of its own files gets one conflict
34
+ turn instead of a person (#286).** MEASURED on a field run: a story with a green DoD conflicted
35
+ bringing itself up to the epic (#268) in one counted test list both stories bumped, blocked
36
+ correctly — and nothing in the framework could resolve it: the developer holds no merge verb, a
37
+ plain reopen had no work, and a person did it in a scratch worktree; 1.5 h later the same story
38
+ conflicted again in four files. Now, when the conflict is in at most three files, every one in
39
+ the story's declared `touches` (the boundary matcher, `inSurface`), with no conflict turn since
40
+ its last `story.reopened` and an attempt left, the story is requeued the way #313 requeues a
41
+ red DoD — prior status, worktree kept, one attempt spent — and one new `story.conflict_turn`
42
+ event is written. Its next attempt is dispatched with the CURRENT epic tip merged into the
43
+ worktree and the merge left open (markers, `MERGE_HEAD`); the prompt names the files, the
44
+ story's intent and the stories that landed on the epic, and `git add` + `git commit` — verbs the
45
+ developer already holds — close it, with the DoD as judge. A marker guard in `git.ts`
46
+ (each conflicted file handed, any path it was renamed to, and every added or untracked path a
47
+ commit could sweep in, read for any `<<<<<<<`, `|||||||`
48
+ or `>>>>>>>` line — a lone `=======` heading underline is not a marker — plus `MERGE_HEAD`)
49
+ blocks, never requeues, a turn that left a marker in one of those files or never closed the
50
+ merge, naming the files — without it, measured
51
+ by mutation, markers were committed and merged onto the epic as `done`. Anything else still
52
+ blocks exactly as before, with the reason it got no turn appended. The bound is counted from
53
+ `events.jsonl`, so it holds across invocations, and `tldrx replay` says an agent, not a person,
54
+ resolved the merge. Headless only: the host's `--commit` half and an as-is settlement block as
55
+ they did.
56
+
57
+ ### Fixed
58
+
59
+ - **`tldrx story reopen` on a `blocked` story also releases the dependents it alone was holding
60
+ (#312).** Reopening a blocked dependency left every dependent `blocked` with a
61
+ `dependency S<n> blocked` reason that had just gone stale, so each needed its own reopen —
62
+ measured three times in one day on a live run. Every `blocked` story whose recorded hold names
63
+ the reopened story (or a story released this way, transitively) now goes back to `todo` in the
64
+ same command when no other dependency still holds it. Each release is its own signed
65
+ `story.reopened` naming the dependency that caused it (additive `released_by` and `dependency`),
66
+ consuming no attempt. A dependent still held by another dependency, or blocked by a verdict
67
+ rather than a hold, stays `blocked`, and the output says which and why.
68
+ - **`Recommended: A [src: …]` is a recommendation, and `seed check` reads the line the way the
69
+ loop does (#323).** On a live run the loop answered Q8's `Recommended: A — <why> [src: …]`
70
+ and left Q7's `Recommended: A [src: …]` for a person, parking an unattended run: the question
71
+ parser demanded a dash reason before any citation, while `seed check` tested only the
72
+ substring `Recommended:` and had passed the same shape clean — two derivations of "has a
73
+ recommendation" that disagreed. There is now one grammar and one reader
74
+ (`parseRecommendedLine`): the letter first, then any of its `)`, a `— <why>` and trailing
75
+ `[src: …]` tokens. `seed check` calls it and names, as exit-1 findings, a `Recommended:` line
76
+ it cannot read (`question-recommended-unreadable`, with the line and the shape expected) and
77
+ a letter naming none of the options the question lists (`question-recommended-option`). A
78
+ seed question that lists no options is not judged on its letter — the options are the What
79
+ stage's to write. A §2.7 block's unreadable line is still ignored rather than refused.
80
+ - **An unattended run is no longer stranded by its own provenance, or by a refused check it could
81
+ fix itself (#231).** Measured 2026-09-14 on a `run auto` validation run (0.27.0): a What stage
82
+ wrote `[src: absent:.tldrx/memory/facts.yml#tenant-credits]`, true when written; the loop then
83
+ auto-answered that stage's questions, the new facts' `source:` maps read
84
+ `run: 260914-tenant-credits`, and `claim-sources` refused the absence on the run id. The refused
85
+ `auto` gate then waited for a person, who typed `reject --and-continue` with the finding —
86
+ intervention #2 of that run. Two fixes. An `absent:…#<needle>` over the workspace's
87
+ `facts.yml` no longer searches a fact's `source: {who, when, run, q, decided_by}` map — that is
88
+ who recorded it, not what it says; a map with any other key is searched in full, and a needle in
89
+ a fact's content still refuses, even on a fact this run just wrote. And under `--wait-gates`,
90
+ an `auto` gate refused by failed checks ALONE (a declared check, or `claim-sources` failing)
91
+ re-runs its stage once through the same `reject --and-continue` path, signed `run auto` with the
92
+ findings as the note; past that bound, or on a `human`/`agent` gate, or when questions, budget,
93
+ stories, boundary or status also hold it, the loop waits for a person as before and prints why.
94
+
3
95
  ## 0.27.0 — 2026-09-14
4
96
 
5
97
  ### Added
package/README.md CHANGED
@@ -335,6 +335,8 @@ back on the registry is 0.3.0.
335
335
 
336
336
  | Version | Date | Status | Contains |
337
337
  |---|---|---|---|
338
+ | 0.28.1 | 2026-09-15 | `beta` | Two fixes so a reopened story's reason reaches the agents that need it. The developer and reviewer prompts now carry `## Why this story was reopened` — who signed it, whether it's a fix round, the note verbatim — off the same ledger value #308 reads, instead of the note reaching only a report line and #308's check (closes #322). `tldrx note --help` now says an operator note reaches no agent and names `dispatch-notes.md` as the channel that does (closes #151). |
339
+ | 0.28.0 | 2026-09-15 | `beta` | Four changes that keep an unattended run moving instead of stranding on a person. A story whose merge-up to its epic conflicts in at most three of its own touched files gets one automated conflict turn instead of a person, guarded against committing conflict markers (#286). `story reopen` on a blocked story now releases every dependent it alone was holding, in the same command (#312). Seed's `Recommended:` line and the loop's question parser now share one grammar, so a citation-only recommendation no longer parks a run that `seed check` had already passed (#323). And an `absent:` needle over `facts.yml` no longer trips on a fact's own recording metadata, while an `auto` gate refused only by failed checks gets one automatic re-run before waiting for a person (#231). |
338
340
  | 0.27.0 | 2026-09-14 | `beta` | Five changes for unattended runs, from a planning audit and live field measurement. The `plan` check now refuses a plan with more waves than the framework carries per run (unless `waves.yml` names a `wave_cap_reason`), refuses a story scheduled before its `depends_on` allows, and flags a dod command siblings carry that one story doesn't (#316–#319). `tldrx run auto --rebalance-finished` moves a blocked phase's exact shortfall out of a finished phase's unspent ceiling before refusing, through `budget raise --take-from` itself, so the run ceiling never grows and no grant is ever assumed; every money refusal now names the unspent total and the exact command to fix it (#314). `tldrx ship` fetches and merges the base into the epic and re-runs the `done` stories' DoD before opening a PR, so a stale or red epic refuses instead of shipping a PR that comes back red (#315). A reviewer's budget floor rises from $1.00 to $2.00, matching what completed reviews actually cost, so a review no longer dies mid-diff on a floor that was never measured (#307). And a developer whose DoD goes red gets the story's next attempt instead of blocking on the first miss, with the kept output handed to the next attempt and the bound counted from `events.jsonl` so it holds across process restarts; a refused developer, a cap death, or a broken dod command still blocks immediately (#313). |
339
341
  | 0.26.1 | 2026-09-14 | `beta` | Three refusals that stop a bad state from settling quietly. A Build fix round whose developer lands no diff is refused before the DoD instead of closing `done` on an unmoved tree, because `commitIfDirty` hands back the old head sha on a clean commit and the existing "no commit to review" gate never fires for it — `workSince` is now asked on the success path too, for any story a person put back with a note. `tldrx ship` refuses an epic carrying a story the reviewer marked `changes`: it asks the ledger's `lastMerge` for every story not `done` and exits 2 naming the story, the merge and the handoff's reason, instead of opening a PR whose "Not done" list hid rejected code already merged into the diff. And a stage-failure line keeps the branch it names when the worktree path is wider than the line: the refusal puts the branches before the path, and `oneLine` keeps a line's head and tail instead of dropping the identifiers that sat at the end. |
340
342
  | 0.26.0 | 2026-09-14 | `beta` | Two field defects from the same class — a reader of an event payload trusting a field its writer never set — plus the ceiling that killed the turn that earned it. A wide story's measured `touches_widened` no longer throws after the turn has settled: the cap knows the three lists by name and drops them whole by count, every reader derives before/after through one function, and an executor that throws after paid turns leaves their rows and money in `run.yml` and saves before it emits. The cost surfaces then agree with each other: a turn whose result was lost to that fault reads as UNMETERED, LOWER BOUND on `tldrx cost` and in the Build handoff through the door other unmeasured turns already use, a turn still in flight reads as nothing, and a late result retires the lost mark instead of counting twice. Replay reads the reason a failed stage recorded instead of printing "no reason recorded" for every one. |
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  conflictOf
4
- } from "./chunk-d0jhr5wh.js";
4
+ } from "./chunk-c62y35wh.js";
5
5
  import {
6
6
  FactsStore,
7
7
  formatJaccard
8
- } from "./chunk-jpp137mb.js";
8
+ } from "./chunk-48ctk154.js";
9
9
  import {
10
10
  parseHookInput,
11
11
  readStdin
12
- } from "./chunk-qzsxhsyj.js";
12
+ } from "./chunk-khjm356r.js";
13
13
  import {
14
14
  EventLog,
15
15
  PHASE_ID_RE
16
- } from "./chunk-41k19q8t.js";
16
+ } from "./chunk-abswp1vt.js";
17
17
  import {
18
18
  PHASE_IDS
19
- } from "./chunk-m1s8a6s0.js";
20
- import"./chunk-bznt6jb2.js";
21
- import"./chunk-ezfvp5kx.js";
19
+ } from "./chunk-veffvzw5.js";
20
+ import"./chunk-g7wz339n.js";
21
+ import"./chunk-zvxgdssb.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-5stgwp3g.js";
31
+ } from "./chunk-khy21x6e.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-0sy3tmfg.js";
41
+ } from "./chunk-f6t1xc3f.js";
42
42
 
43
43
  // src/hooks/answer-capture.ts
44
44
  import { existsSync as existsSync5 } from "fs";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-ka7768vx.js";
4
+ } from "./chunk-q3d4xdhb.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-t44983nq.js";
12
- import"./chunk-qzsxhsyj.js";
11
+ } from "./chunk-tna1bz8r.js";
12
+ import"./chunk-khjm356r.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -22,10 +22,10 @@ import {
22
22
  validateRunBudget,
23
23
  wouldExceed,
24
24
  wouldExceedHostTokens
25
- } from "./chunk-2qkbh9b6.js";
25
+ } from "./chunk-e618k6te.js";
26
26
  import {
27
27
  EventLog
28
- } from "./chunk-41k19q8t.js";
28
+ } from "./chunk-abswp1vt.js";
29
29
  import {
30
30
  cursorStage,
31
31
  hostTokensIn,
@@ -34,18 +34,18 @@ import {
34
34
  newestActiveRun,
35
35
  renderRunEconomies,
36
36
  runSpend
37
- } from "./chunk-yre2scxn.js";
38
- import"./chunk-bznt6jb2.js";
37
+ } from "./chunk-hws2gnxj.js";
38
+ import"./chunk-g7wz339n.js";
39
39
  import {
40
40
  noteDeprecations
41
- } from "./chunk-5stgwp3g.js";
41
+ } from "./chunk-khy21x6e.js";
42
42
  import {
43
43
  PROJECT_WORK_DIR,
44
44
  findWorkspaceRoot,
45
45
  locateWork,
46
46
  parseYaml,
47
47
  stageYamlPath
48
- } from "./chunk-0sy3tmfg.js";
48
+ } from "./chunk-f6t1xc3f.js";
49
49
 
50
50
  // src/hooks/budget-gate.ts
51
51
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-ezfvp5kx.js";
6
+ } from "./chunk-zvxgdssb.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-5stgwp3g.js";
18
+ } from "./chunk-khy21x6e.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-0sy3tmfg.js";
31
+ } from "./chunk-f6t1xc3f.js";
32
32
 
33
33
  // src/core/facts/FactsStore.ts
34
34
  import { existsSync, readFileSync } from "node:fs";
@@ -2,7 +2,7 @@ import {
2
2
  GATE_POLICIES,
3
3
  validateGatesPolicy,
4
4
  validateStagePolicy
5
- } from "./chunk-bznt6jb2.js";
5
+ } from "./chunk-g7wz339n.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-0sy3tmfg.js";
18
+ } from "./chunk-f6t1xc3f.js";
19
19
 
20
20
  // src/core/events/EventLog.ts
21
21
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -50,6 +50,7 @@ var EVENT_TYPES = [
50
50
  "story.reopened",
51
51
  "story.base_fastforwarded",
52
52
  "story.base_updated",
53
+ "story.conflict_turn",
53
54
  "story.review_retried",
54
55
  "story.work_rescued",
55
56
  "story.touches_widened",
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  findDuplicate
3
- } from "./chunk-jpp137mb.js";
3
+ } from "./chunk-48ctk154.js";
4
4
  import {
5
5
  evidencePath,
6
6
  gateEvidencePath,
7
7
  parseEvidence
8
- } from "./chunk-41k19q8t.js";
8
+ } from "./chunk-abswp1vt.js";
9
9
  import {
10
10
  openBlocks,
11
11
  parseQuestions
12
- } from "./chunk-5stgwp3g.js";
12
+ } from "./chunk-khy21x6e.js";
13
13
 
14
14
  // src/core/distill/distill.ts
15
15
  var CONFLICT_THRESHOLD = 0.6;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  STAGE_TUNING_DEFAULTS
3
- } from "./chunk-bznt6jb2.js";
3
+ } from "./chunk-g7wz339n.js";
4
4
  import {
5
5
  MAX_PLAN_STORIES,
6
6
  MAX_STORIES_PER_WAVE,
@@ -25,7 +25,7 @@ import {
25
25
  requireVersion,
26
26
  requireVersion1,
27
27
  result
28
- } from "./chunk-0sy3tmfg.js";
28
+ } from "./chunk-f6t1xc3f.js";
29
29
 
30
30
  // src/hooks/lib/actor.ts
31
31
  function currentActor() {
@@ -8240,7 +8240,7 @@ function resolveAbsent(ref, ctx, section, claim) {
8240
8240
  if (found === null)
8241
8241
  return OK;
8242
8242
  if (needle !== null)
8243
- return searchForNeedle(found, needle, path);
8243
+ return searchForNeedle(found, needle, path, isWorkspaceFacts(found, ctx));
8244
8244
  const held = whatIsThere(found);
8245
8245
  if (held === null)
8246
8246
  return OK;
@@ -8248,7 +8248,7 @@ function resolveAbsent(ref, ctx, section, claim) {
8248
8248
  }
8249
8249
  var NEEDLE_MAX_BYTES = 512 * 1024;
8250
8250
  var EMPTY_PROBE_MAX_BYTES = 64 * 1024;
8251
- function searchForNeedle(abs, needle, path) {
8251
+ function searchForNeedle(abs, needle, path, factsFile) {
8252
8252
  let stat;
8253
8253
  try {
8254
8254
  stat = statSync2(abs);
@@ -8271,12 +8271,35 @@ function searchForNeedle(abs, needle, path) {
8271
8271
  const lines = text.split(`
8272
8272
  `);
8273
8273
  for (let i = 0;i < lines.length; i++) {
8274
- if ((lines[i] ?? "").toLowerCase().includes(wanted)) {
8274
+ const line = factsFile ? withoutFactProvenance(lines[i] ?? "") : lines[i] ?? "";
8275
+ if (line.toLowerCase().includes(wanted)) {
8275
8276
  return refused(`\`${needle}\` IS at ${path}:${String(i + 1)} — that is a presence, not an absence`, abs);
8276
8277
  }
8277
8278
  }
8278
8279
  return OK;
8279
8280
  }
8281
+ function isWorkspaceFacts(abs, ctx) {
8282
+ return normalize(abs) === normalize(join2(ctx.root, PROJECT_FRAMEWORK_DIR, "memory", "facts.yml"));
8283
+ }
8284
+ var PROVENANCE_KEYS = new Set(["who", "when", "run", "q", "decided_by"]);
8285
+ var SOURCE_OPEN_RE = /(^|[\s,{])source:\s*\{/;
8286
+ var PROVENANCE_ENTRY_RE = /^\s*([a-z_]+):\s*("(?:[^"\\]|\\.)*"|[^,{}"]*?)\s*([,}])/;
8287
+ function withoutFactProvenance(line) {
8288
+ const open = SOURCE_OPEN_RE.exec(line);
8289
+ if (open === null)
8290
+ return line;
8291
+ const start = open.index + (open[1] ?? "").length;
8292
+ let at = open.index + open[0].length;
8293
+ for (;; ) {
8294
+ const entry = PROVENANCE_ENTRY_RE.exec(line.slice(at));
8295
+ if (entry === null || !PROVENANCE_KEYS.has(entry[1] ?? ""))
8296
+ return line;
8297
+ at += entry[0].length;
8298
+ if (entry[3] === "}")
8299
+ break;
8300
+ }
8301
+ return line.slice(0, start) + " ".repeat(at - start) + line.slice(at);
8302
+ }
8280
8303
  function whatIsThere(abs) {
8281
8304
  let stat;
8282
8305
  try {
@@ -9,7 +9,7 @@ import {
9
9
  spentBasis,
10
10
  tallyOf,
11
11
  validateRunBudget
12
- } from "./chunk-2qkbh9b6.js";
12
+ } from "./chunk-e618k6te.js";
13
13
  import {
14
14
  EventLog,
15
15
  OUTCOME_NOT_RECORDED,
@@ -22,12 +22,12 @@ import {
22
22
  isTerminal,
23
23
  stageAt,
24
24
  validateRunFile
25
- } from "./chunk-41k19q8t.js";
25
+ } from "./chunk-abswp1vt.js";
26
26
  import {
27
27
  cursorStage,
28
28
  isAttendedByHostView,
29
29
  openRunViews
30
- } from "./chunk-yre2scxn.js";
30
+ } from "./chunk-hws2gnxj.js";
31
31
  import {
32
32
  backupPathFor,
33
33
  isAlive,
@@ -36,20 +36,20 @@ import {
36
36
  workspaceRootOfRunDir,
37
37
  writeAtomic,
38
38
  yamlScalar
39
- } from "./chunk-ezfvp5kx.js";
39
+ } from "./chunk-zvxgdssb.js";
40
40
  import {
41
41
  isAdvisory,
42
42
  noteDeprecations,
43
43
  openBlocks,
44
44
  parseQuestions
45
- } from "./chunk-5stgwp3g.js";
45
+ } from "./chunk-khy21x6e.js";
46
46
  import {
47
47
  FRAMEWORK_ROOT,
48
48
  PLAN_STATUSES,
49
49
  listRunDirs,
50
50
  parseYaml,
51
51
  parseYamlRepairing
52
- } from "./chunk-0sy3tmfg.js";
52
+ } from "./chunk-f6t1xc3f.js";
53
53
 
54
54
  // src/core/statusline/runSnapshot.ts
55
55
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
@@ -684,6 +684,7 @@ function statusWithOutcome(status, outcome) {
684
684
 
685
685
  // src/core/run/autoGate.ts
686
686
  var AUTO_GATE_ACTOR = "auto";
687
+ var RETRYABLE_CONDITIONS = new Set(["checks", "claim-sources"]);
687
688
  var AUTO_GATE_REFUSED_PREFIX = "auto-gate refused — held by: ";
688
689
  var HELD_SEPARATOR = " · ";
689
690
  function heldByNote(note) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isRecord
3
- } from "./chunk-0sy3tmfg.js";
3
+ } from "./chunk-f6t1xc3f.js";
4
4
 
5
5
  // src/core/run/stagePolicy.ts
6
6
  function validateStagePolicy(value, stageIds, issues, spec) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  listRunDirs,
3
3
  parseYaml
4
- } from "./chunk-0sy3tmfg.js";
4
+ } from "./chunk-f6t1xc3f.js";
5
5
 
6
6
  // src/hooks/lib/runFile.ts
7
7
  import { existsSync, readFileSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-0sy3tmfg.js";
3
+ } from "./chunk-f6t1xc3f.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {
@@ -3,7 +3,7 @@ import {
3
3
  parseSrcToken,
4
4
  srcToken,
5
5
  withoutSrcToken
6
- } from "./chunk-0sy3tmfg.js";
6
+ } from "./chunk-f6t1xc3f.js";
7
7
 
8
8
  // src/core/text/questions.ts
9
9
  var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
@@ -11,7 +11,9 @@ var HEADING_RE = /^##\s+(Q\d{1,6})\s+·\s+(.+?)\s*$/;
11
11
  var METADATA_RE = /^<!--\s*(.*?)\s*-->$/;
12
12
  var WHY_RE = /^Why asked:\s*(.*)$/;
13
13
  var OPTION_RE = /^-\s+([A-E])\)\s*(.*)$/;
14
- var RECOMMENDED_RE = /^Recommended:[ \t]+([A-E])\)?[ \t]*(?:[—–-][ \t]+(.*))?$/;
14
+ var RECOMMENDED_PREFIX_RE = /^Recommended:/;
15
+ var RECOMMENDED_RE = /^Recommended:[ \t]+([A-E])\)?(?![^ \t])[ \t]*(.*)$/;
16
+ var RECOMMENDED_REASON_RE = /^[—–-](?:[ \t]+(.*))?$/;
15
17
  var ANSWER_RE = /^\[Answer\]:[ \t]*(\S.*)$/;
16
18
  var ANSWER_SLOT_RE = /^\[Answer\]:/;
17
19
  var FOOTER_KEYS = ["answered_by", "answered_at", "fact"];
@@ -55,6 +57,7 @@ function buildBlock(startLine, lines) {
55
57
  let whySrc = null;
56
58
  const options = [];
57
59
  let recommended = null;
60
+ let sawRecommendedLine = false;
58
61
  let answer = "";
59
62
  let answerIndex = -1;
60
63
  let footer = null;
@@ -82,9 +85,10 @@ function buildBlock(startLine, lines) {
82
85
  options.push({ letter: option[1], text: option[2] });
83
86
  continue;
84
87
  }
85
- const recommendation = RECOMMENDED_RE.exec(line);
86
- if (recommendation !== null && recommendation[1] !== undefined && recommended === null) {
87
- recommended = toRecommendation(recommendation[1], recommendation[2] ?? "");
88
+ if (RECOMMENDED_PREFIX_RE.test(line) && !sawRecommendedLine) {
89
+ sawRecommendedLine = true;
90
+ const reading = parseRecommendedLine(line);
91
+ recommended = reading.kind === "ok" ? reading.recommendation : null;
88
92
  continue;
89
93
  }
90
94
  if (ANSWER_SLOT_RE.test(line) && answerIndex === -1) {
@@ -109,13 +113,27 @@ function buildBlock(startLine, lines) {
109
113
  lines: [...lines]
110
114
  };
111
115
  }
112
- function toRecommendation(option, rest) {
113
- const token = parseSrcToken(rest);
114
- return {
115
- option,
116
- why: withoutSrcToken(rest).trim(),
117
- src: token === null ? "" : token.refs.map((ref) => ref.raw).join(SRC_SEPARATOR)
118
- };
116
+ function parseRecommendedLine(line) {
117
+ if (!RECOMMENDED_PREFIX_RE.test(line))
118
+ return { kind: "absent" };
119
+ const match = RECOMMENDED_RE.exec(line.replace(/\s+$/, ""));
120
+ const option = match?.[1];
121
+ if (match === null || option === undefined)
122
+ return { kind: "unreadable", line };
123
+ let rest = (match[2] ?? "").trim();
124
+ const tokens = [];
125
+ for (let token = parseSrcToken(rest);token !== null; token = parseSrcToken(rest)) {
126
+ tokens.unshift(token.refs.map((ref) => ref.raw));
127
+ rest = withoutSrcToken(rest).replace(/[\s.,;:!?]+$/, "").trim();
128
+ }
129
+ let why = "";
130
+ if (rest !== "") {
131
+ const reason = RECOMMENDED_REASON_RE.exec(rest);
132
+ if (reason === null)
133
+ return { kind: "unreadable", line };
134
+ why = (reason[1] ?? "").trim();
135
+ }
136
+ return { kind: "ok", line, recommendation: { option, why, src: tokens.flat().join(SRC_SEPARATOR) } };
119
137
  }
120
138
  function parsePipeComment(inner) {
121
139
  const pairs = [];
@@ -3,7 +3,7 @@ import {
3
3
  EMPTY_SECTION_RULE,
4
4
  noneBullet,
5
5
  srcRule
6
- } from "./chunk-0sy3tmfg.js";
6
+ } from "./chunk-f6t1xc3f.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,7 +1,7 @@
1
1
  import {
2
2
  parseHookInput,
3
3
  readStdin
4
- } from "./chunk-qzsxhsyj.js";
4
+ } from "./chunk-khjm356r.js";
5
5
 
6
6
  // src/hooks/lib/decide.ts
7
7
  function deny(reason) {
@@ -3,7 +3,7 @@ import {
3
3
  STAGE_TUNING_DEFAULTS,
4
4
  parseWorkflowGates,
5
5
  readStageTuning
6
- } from "./chunk-bznt6jb2.js";
6
+ } from "./chunk-g7wz339n.js";
7
7
  import {
8
8
  PROJECT_FRAMEWORK_DIR,
9
9
  STAGES_DIR,
@@ -12,7 +12,7 @@ import {
12
12
  isRecord,
13
13
  loadWorkspace,
14
14
  parseYaml
15
- } from "./chunk-0sy3tmfg.js";
15
+ } from "./chunk-f6t1xc3f.js";
16
16
 
17
17
  // src/core/run/workflowPreset.ts
18
18
  import { existsSync, readFileSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toolInput
3
- } from "./chunk-t44983nq.js";
3
+ } from "./chunk-tna1bz8r.js";
4
4
 
5
5
  // src/hooks/lib/wouldBe.ts
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PROJECT_FRAMEWORK_DIR,
3
3
  PROJECT_WORK_DIR
4
- } from "./chunk-0sy3tmfg.js";
4
+ } from "./chunk-f6t1xc3f.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";
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  wouldBeContent
4
- } from "./chunk-n5bxz2cw.js";
4
+ } from "./chunk-zrwq6r7k.js";
5
5
  import {
6
6
  claimSourcesDeny,
7
7
  claimSourcesEmptySectionDeny,
8
8
  claimSourcesMalformedDeny,
9
9
  claimSourcesUnresolvedDeny
10
- } from "./chunk-ka7768vx.js";
10
+ } from "./chunk-q3d4xdhb.js";
11
11
  import {
12
12
  allow,
13
13
  deny,
@@ -16,8 +16,8 @@ import {
16
16
  postContext,
17
17
  readPayload,
18
18
  runHook
19
- } from "./chunk-t44983nq.js";
20
- import"./chunk-qzsxhsyj.js";
19
+ } from "./chunk-tna1bz8r.js";
20
+ import"./chunk-khjm356r.js";
21
21
  import {
22
22
  isHandoff,
23
23
  loadWorkspace,
@@ -25,7 +25,7 @@ import {
25
25
  toSrcContext,
26
26
  validateCitations,
27
27
  validateHandoff
28
- } from "./chunk-0sy3tmfg.js";
28
+ } from "./chunk-f6t1xc3f.js";
29
29
 
30
30
  // src/hooks/claim-sources.ts
31
31
  await runHook("claim-sources", async () => {