tldr-experts 0.18.2 → 0.19.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/dist/tldrx.js CHANGED
@@ -7290,6 +7290,7 @@ var EVENT_TYPES = [
7290
7290
  "questions.policy_changed",
7291
7291
  "story.reopened",
7292
7292
  "story.base_fastforwarded",
7293
+ "story.base_updated",
7293
7294
  "story.review_retried",
7294
7295
  "story.work_rescued",
7295
7296
  "story.touches_widened",
@@ -8361,6 +8362,16 @@ function withoutSrcToken(line) {
8361
8362
  function hasSrcMarker(line) {
8362
8363
  return line.includes(SRC_MARKER);
8363
8364
  }
8365
+ function unclosedSrcMarker(text) {
8366
+ const open = text.lastIndexOf(SRC_MARKER);
8367
+ if (open < 0)
8368
+ return null;
8369
+ return text.includes("]", open + SRC_MARKER.length) ? null : open;
8370
+ }
8371
+ function foldsUnclosedSrcMarker(line) {
8372
+ const token = parseSrcToken(line);
8373
+ return token !== null && token.raw.indexOf(SRC_MARKER, SRC_MARKER.length) >= 0;
8374
+ }
8364
8375
  function classifySrc(src, repos) {
8365
8376
  const raw = src;
8366
8377
  if (raw === "")
@@ -10405,8 +10416,7 @@ function splitArgv(command) {
10405
10416
  if (/[\n\r]/.test(command))
10406
10417
  return null;
10407
10418
  const argv = [];
10408
- const pattern = /"([^"]*)"|'([^']*)'|([^\s"']+)/g;
10409
- for (const match of command.matchAll(pattern)) {
10419
+ for (const match of command.matchAll(TOKEN_RE)) {
10410
10420
  if (match[1] !== undefined || match[2] !== undefined) {
10411
10421
  argv.push(match[1] ?? match[2] ?? "");
10412
10422
  continue;
@@ -10418,6 +10428,21 @@ function splitArgv(command) {
10418
10428
  }
10419
10429
  return argv.length === 0 ? null : argv;
10420
10430
  }
10431
+ var TOKEN_RE = /"([^"]*)"|'([^']*)'|([^\s"']+)/g;
10432
+ var SEPARATOR_RE = /\|\||&&|2>&1|>>|\$\((?!\()|[|&;<>`]/;
10433
+ function unquotedShellSeparator(command) {
10434
+ if (/[\n\r]/.test(command))
10435
+ return `
10436
+ `;
10437
+ for (const match of command.matchAll(TOKEN_RE)) {
10438
+ if (match[1] !== undefined || match[2] !== undefined)
10439
+ continue;
10440
+ const found = SEPARATOR_RE.exec(match[3] ?? "");
10441
+ if (found !== null)
10442
+ return found[0];
10443
+ }
10444
+ return null;
10445
+ }
10421
10446
 
10422
10447
  class DodCommandRefused extends Error {
10423
10448
  }
@@ -11300,7 +11325,12 @@ function clean(text) {
11300
11325
  const stripped = text.replace(GROUNDING_TAG_RE, "").replace(/\s+/g, " ").replace(/\*\*/g, "").trim();
11301
11326
  if (stripped.length <= MAX_CLAIM_CHARS)
11302
11327
  return stripped;
11303
- return `${stripped.slice(0, MAX_CLAIM_CHARS - 1).trimEnd()}…`;
11328
+ const cut = stripped.slice(0, MAX_CLAIM_CHARS - 1);
11329
+ const open = unclosedSrcMarker(cut);
11330
+ if (open === null)
11331
+ return `${cut.trimEnd()}…`;
11332
+ const kept = cut.slice(0, open).trimEnd();
11333
+ return kept === "" ? stripped : `${kept}…`;
11304
11334
  }
11305
11335
 
11306
11336
  // src/core/distill/aidlcQuestions.ts
@@ -13892,7 +13922,7 @@ function createRunLocked(options) {
13892
13922
  write(temp, handoffPath, handoff, written);
13893
13923
  const check = validateHandoff(handoff, toSrcContext(workspace, temp));
13894
13924
  if (!check.ok) {
13895
- throw new NewRunError(`seeded handoff is invalid: ${describeHandoff(check.missingSections, check.emptySections, check.unsourced, check.unresolved)}`);
13925
+ throw new NewRunError(`seeded handoff is invalid: ${describeHandoff(check.missingSections, check.emptySections, check.unsourced, check.unresolved, handoff)}`);
13896
13926
  }
13897
13927
  }
13898
13928
  let factsYaml = null;
@@ -13926,7 +13956,7 @@ function createRunLocked(options) {
13926
13956
  const srcCtx = toSrcContext(workspace, temp);
13927
13957
  const handoffCheck = validateHandoff(handoff, srcCtx);
13928
13958
  if (!handoffCheck.ok) {
13929
- throw new NewRunError(`distilled handoff is invalid: ${describeHandoff(handoffCheck.missingSections, handoffCheck.emptySections, handoffCheck.unsourced, handoffCheck.unresolved)}`);
13959
+ throw new NewRunError(`distilled handoff is invalid: ${describeHandoff(handoffCheck.missingSections, handoffCheck.emptySections, handoffCheck.unsourced, handoffCheck.unresolved, handoff)}`);
13930
13960
  }
13931
13961
  if (result2.conflicts.length > 0) {
13932
13962
  const text = renderQuestions(runId, phaseId, at, result2.conflicts);
@@ -14096,7 +14126,7 @@ function assertValid(what, validation) {
14096
14126
  const first = validation.issues[0];
14097
14127
  throw new NewRunError(`refusing to create a run with an invalid ${what}: ${first?.path ?? ""} ${first?.message ?? ""}`);
14098
14128
  }
14099
- function describeHandoff(missing, empty, unsourced, unresolved) {
14129
+ function describeHandoff(missing, empty, unsourced, unresolved, text = "") {
14100
14130
  if (missing.length > 0)
14101
14131
  return `missing section(s) ${missing.join(", ")}`;
14102
14132
  if (empty.length > 0) {
@@ -14104,7 +14134,24 @@ function describeHandoff(missing, empty, unsourced, unresolved) {
14104
14134
  }
14105
14135
  if (unsourced.length > 0)
14106
14136
  return `unsourced bullet(s) on line(s) ${unsourced.join(", ")}`;
14107
- return unresolved[0]?.message ?? "unknown problem";
14137
+ const first = unresolved[0];
14138
+ if (first === undefined)
14139
+ return "unknown problem";
14140
+ return `${first.message}${foldedCitationNote(text, first.line)}`;
14141
+ }
14142
+ var MAX_QUOTED_CHARS2 = 120;
14143
+ function foldedCitationNote(text, line) {
14144
+ if (text === "" || line < 1)
14145
+ return "";
14146
+ const raw = text.split(`
14147
+ `)[line - 1];
14148
+ if (raw === undefined || raw.trim() === "")
14149
+ return "";
14150
+ if (!foldsUnclosedSrcMarker(raw))
14151
+ return "";
14152
+ const trimmed = raw.trim();
14153
+ const quoted = trimmed.length <= MAX_QUOTED_CHARS2 ? trimmed : `${trimmed.slice(0, MAX_QUOTED_CHARS2)}…`;
14154
+ return ` — L${String(line)} carries an unterminated \`[src:\` citation, so the path above is two ` + `citations folded into one and is not a file to look for; close or remove the inline citation ` + `in the seed (shorter bullets are safest). L${String(line)} reads \`${quoted}\``;
14108
14155
  }
14109
14156
  function yymmdd(now) {
14110
14157
  return now.toISOString().slice(2, 10).replace(/-/g, "");
@@ -16380,11 +16427,18 @@ var ENTRIES = [
16380
16427
  meaning: "Open a FIX ROUND on a story that is `done`: one named defect in work a reviewer already approved. No attempt is consumed, the fix passes the same dod and the same reviewer as the original, and the story's acceptance criteria are not touched — it is not a way to relitigate scope. Refused when the story is not done, when --note is missing, and when that story already has a fix round open (the bound is one).",
16381
16428
  sub: "reopen"
16382
16429
  },
16430
+ {
16431
+ name: "as-is",
16432
+ arg: null,
16433
+ meaning: "Settle the story from its BRANCH AS IT STANDS: the next Build turn runs the dod, the review and the merge over the commits already on `story/<run>/<id>`, and spawns NO developer. For a story a person finished by hand — a rebase after a conflict, a fix typed directly on the branch — which the plain verb cannot land, because a developer handed finished work has nothing to do and the story blocks on the no-work rule. It is not a shortcut past a gate: a red dod blocks the story, and so does a branch carrying no commit its epic has not already got. Requires --note; refused together with --for-fix.",
16434
+ sub: "reopen"
16435
+ },
16383
16436
  runFlag(),
16384
16437
  root()
16385
16438
  ],
16386
16439
  examples: [
16387
16440
  'tldrx story reopen S3 --note "it gates wave 3 (S4, S6) and the owner has decided it ships"',
16441
+ 'tldrx story reopen S2 --as-is --note "rebased onto the epic by hand after the merge conflict; dod green"',
16388
16442
  'tldrx story reopen S11 --for-fix --note "linkEmail succeeds then setDisplayName fails: account linked, score never claimable"',
16389
16443
  'tldrx story widen S3 platform/Auth.cs --note "the tenancy check the story is for lives here too"'
16390
16444
  ],
@@ -16395,6 +16449,7 @@ var ENTRIES = [
16395
16449
  "The story goes back to `todo` and its attempt counter restarts at 1 of 2. Nothing is erased to make that true: `story.reopened` is a reset boundary the review ledger reads, every earlier attempt stays in events.jsonl, and the event records how many verdicts the closed run consumed.",
16396
16450
  "It runs no agent, spends nothing, deletes nothing and refunds nothing. The story's branch is kept — that is what carries the last developer's commits forward — and its worktree is left exactly as the build left it, to be reopened from the branch if the build had removed it.",
16397
16451
  'It does NOT make the stage runnable. If the Build stage is at its gate, `tldrx reject --note "…"` sends it back to `ready` first; if the gate is already signed, `tldrx reject --stage` takes that back.',
16452
+ "`--as-is` is the other arc the plain verb cannot cover: a story a PERSON finished. `reopen` hands the story to a developer, and a developer handed a branch whose work is already done has nothing to commit — the story then blocks on the rule that measures work SINCE THE SPAWN, because work older than the spawn cannot be told apart from a developer that did nothing. `--as-is` spawns nothing at all, so there is nothing to measure: the dod runs in the story's worktree (reopened from the branch when the build had removed it), the reviewer judges the same diff, and the merge into the epic is the same merge. It records `story.reopened` with `reason: as_is`, and the story's row and review log say the branch was taken as it stands and name who signed that — no record claims a developer delivered it.",
16398
16453
  "`widen` adds paths to a story's `touches:` — the sanctioned form of the advice the boundary decision card gives when a Build stage changed a path nobody scoped. Widenable states are `todo`, `in_progress`, `review` and `blocked`. A `done` story refuses: its evidence was written against the surface it DECLARED, and widening it afterwards would make the record say the plan declared a path it did not — reopen it with `--for-fix` first. A path the story already declares refuses too, because a widening that widened nothing is a record of something that did not happen.",
16399
16454
  "`widen` runs no agent, spends nothing, consumes no attempt, changes no status and moves no cursor — it declares scope and nothing else. No gate code knows about it: the boundary condition re-reads `touches:` off disk at evaluation time, so the same run, the same branch and the same diff simply stop counting the widened path as outside the surface at the next evaluation. One `story.touches_widened` records the paths, the note and the list before and after."
16400
16455
  ]
@@ -25784,14 +25839,10 @@ async function mergeNoFf(cwd, branch, message4) {
25784
25839
  detail: conflicts.length > 0 ? `conflict in ${conflicts.join(", ")}` : firstLine7(merged.stderr) || firstLine7(merged.stdout) || `git merge exited ${String(merged.exitCode)}`
25785
25840
  };
25786
25841
  }
25787
- async function commitsBetween(cwd, base, head) {
25788
- const result2 = await git(["rev-list", "--count", `${base}..${head}`], cwd);
25789
- if (!result2.ok)
25790
- return 0;
25791
- const n = Number.parseInt(result2.stdout.trim(), 10);
25792
- return Number.isFinite(n) ? n : 0;
25842
+ function uncountedCount(base, head) {
25843
+ return `\`git rev-list --count ${base}..${head}\` could not count what it carries`;
25793
25844
  }
25794
- async function commitsAhead(cwd, base, head) {
25845
+ async function commitsBetween(cwd, base, head) {
25795
25846
  const result2 = await git(["rev-list", "--count", `${base}..${head}`], cwd);
25796
25847
  if (!result2.ok)
25797
25848
  return null;
@@ -25799,12 +25850,15 @@ async function commitsAhead(cwd, base, head) {
25799
25850
  return Number.isFinite(n) ? n : null;
25800
25851
  }
25801
25852
  async function baseStateOf(cwd, branch, base) {
25802
- const ahead = await commitsBetween(cwd, base, branch);
25803
- const behind = await commitsBetween(cwd, branch, base);
25853
+ const counted = await commitsBetween(cwd, base, branch);
25854
+ const countedBack = await commitsBetween(cwd, branch, base);
25855
+ const ahead = counted ?? 0;
25856
+ const behind = countedBack ?? 0;
25804
25857
  return {
25805
25858
  state: behind === 0 ? "current" : ahead === 0 ? "behind" : "diverged",
25806
25859
  ahead,
25807
25860
  behind,
25861
+ uncounted: counted === null ? uncountedCount(base, branch) : countedBack === null ? uncountedCount(branch, base) : null,
25808
25862
  branchSha: await shaOf(cwd, branch),
25809
25863
  baseSha: await shaOf(cwd, base)
25810
25864
  };
@@ -26356,6 +26410,11 @@ function dodRefused(result2) {
26356
26410
  }
26357
26411
  var DOD_REFUSAL_FALLBACK = "the gate declined to run it";
26358
26412
  var DEVELOPER_FAILED = "the developer sub-agent failed";
26413
+ var AS_IS_MARK = "the branch was taken AS IT STANDS — no developer was spawned for it";
26414
+ var AS_IS_NOT_AHEAD_MARK = "nothing to take as it stands";
26415
+ function asIsNotAheadReason(branch, epicBranch, ahead) {
26416
+ return ahead === null ? `${AS_IS_NOT_AHEAD_MARK}: git could not count what \`${branch}\` carries beyond ` + `\`${epicBranch}\`, and a settlement that spawns no developer will not merge on a ` + "measurement it does not have. Nothing was merged" : `${AS_IS_NOT_AHEAD_MARK}: \`${branch}\` carries no commit \`${epicBranch}\` has not already ` + "got, so there is no work to settle. Commit the fix to the story branch first, then reopen it " + "--as-is. Nothing was merged";
26417
+ }
26359
26418
  function scopedNote(result2) {
26360
26419
  if (result2.scope !== "paths" || result2.rendered === undefined)
26361
26420
  return "";
@@ -26364,6 +26423,9 @@ function scopedNote(result2) {
26364
26423
  function dodGreen(outcome) {
26365
26424
  return outcome.dod.length > 0 && outcome.dod.every((r) => !dodRefused(r) && r.exitCode === 0 && !r.timedOut);
26366
26425
  }
26426
+ function dodFailure(dod) {
26427
+ return dod.find((r) => dodRefused(r) || r.exitCode !== 0 || r.timedOut);
26428
+ }
26367
26429
  function dodFailureReason(result2, repo) {
26368
26430
  if (dodRefused(result2)) {
26369
26431
  return `\`${result2.command}\` was REFUSED in repo ${repo} and never ran — ` + `${result2.refusedBecause ?? DOD_REFUSAL_FALLBACK}`;
@@ -26374,6 +26436,15 @@ function dodFailureReason(result2, repo) {
26374
26436
  return `\`${result2.rendered ?? result2.command}\`${scopedNote(result2)} exited ${String(result2.exitCode ?? "?")} in repo ${repo}` + `${result2.timedOut ? " (timed out)" : ""} — ${result2.tail}` + (result2.outputPath === undefined ? "" : ` [src: ${result2.outputPath}:${String(result2.outputLine ?? 1)}]`);
26375
26437
  }
26376
26438
 
26439
+ // src/core/build/developerGrants.ts
26440
+ var DEVELOPER_GIT_VERBS = ["add", "commit", "rm", "mv", "restore"];
26441
+ function developerGitGrants() {
26442
+ return DEVELOPER_GIT_VERBS.map((verb) => `Bash(git ${verb} *)`);
26443
+ }
26444
+ function isDeveloperGitVerb(verb) {
26445
+ return DEVELOPER_GIT_VERBS.includes(verb);
26446
+ }
26447
+
26377
26448
  // src/core/build/prompts.ts
26378
26449
  var MAX_TOUCHED_BYTES = 64 * 1024;
26379
26450
  var MAX_TOUCHED_FILES = 24;
@@ -26403,6 +26474,10 @@ function testFastRule(fast, full) {
26403
26474
  ]
26404
26475
  ];
26405
26476
  }
26477
+ function gitVerbList() {
26478
+ const names = DEVELOPER_GIT_VERBS.map((verb) => `\`git ${verb}\``);
26479
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1] ?? ""}`;
26480
+ }
26406
26481
  function buildDeveloperPrompt(parts) {
26407
26482
  const { story } = parts.story;
26408
26483
  const extra = parts.story.extraInputs ?? [];
@@ -26472,7 +26547,9 @@ function buildDeveloperPrompt(parts) {
26472
26547
  ...parts.commands.length === 0 ? ["- (this repo declares no commands in workspace.yml — the DoD will run nothing)"] : parts.commands.map((command2) => `- \`${command2}\``),
26473
26548
  "",
26474
26549
  ...parts.testFast === undefined ? [] : [...testFastRule(parts.testFast.fast, parts.testFast.full), ""],
26475
- "Commit with `git add` and `git commit`. Nothing else about git is yours to do.",
26550
+ `Commit with \`git add\` and \`git commit\`. The git verbs you hold are exactly ${gitVerbList()}.`,
26551
+ "`git restore <path>` is how to put a file back (there is no checkout in that list). Nothing",
26552
+ "else about git is yours to do.",
26476
26553
  "",
26477
26554
  "## Rules",
26478
26555
  "",
@@ -28581,6 +28658,68 @@ function verdictReviewer(checkPayload, lastSpawn) {
28581
28658
  return lastSpawn;
28582
28659
  }
28583
28660
 
28661
+ // src/core/build/refusalKind.ts
28662
+ var MAX_SEPARATOR_RETRIES = 1;
28663
+ function classifyRefusal(command2) {
28664
+ const separator = unquotedShellSeparator(command2);
28665
+ if (separator !== null)
28666
+ return { kind: "separator", separator };
28667
+ const argv = command2.trim().split(/\s+/);
28668
+ if (argv[0] !== "git")
28669
+ return { kind: "unknown" };
28670
+ const verb = argv[1] ?? "";
28671
+ if (verb === "" || verb.startsWith("-"))
28672
+ return { kind: "unknown" };
28673
+ if (isDeveloperGitVerb(verb))
28674
+ return { kind: "unknown" };
28675
+ return { kind: "verb", verb, equivalent: grantedEquivalent(verb, argv.slice(2)) };
28676
+ }
28677
+ function grantedEquivalent(verb, args) {
28678
+ if (verb === "checkout") {
28679
+ const switching = args.some((a) => a === "-b" || a === "-B" || a === "--orphan" || a === "-t" || a === "--track");
28680
+ if (switching)
28681
+ return null;
28682
+ const hasTarget = args.some((a) => !a.startsWith("-")) || args.includes("--");
28683
+ return hasTarget ? "git restore <path>" : null;
28684
+ }
28685
+ if (verb === "reset") {
28686
+ const mode = args.some((a) => /^--(hard|soft|mixed|merge|keep)$/.test(a));
28687
+ if (mode)
28688
+ return null;
28689
+ if (args.includes("--"))
28690
+ return "git restore --staged <path>";
28691
+ const [first2, second] = args.filter((a) => !a.startsWith("-"));
28692
+ if (first2 === "HEAD" && second !== undefined)
28693
+ return "git restore --staged <path>";
28694
+ return null;
28695
+ }
28696
+ return null;
28697
+ }
28698
+ function refusalCure(kind) {
28699
+ switch (kind.kind) {
28700
+ case "separator":
28701
+ return "run each command alone — shell separators split a line into subcommands that each need their own grant";
28702
+ case "verb":
28703
+ return kind.equivalent === null ? `\`git ${kind.verb}\` is not granted` : `\`git ${kind.verb}\` is not granted; use \`${kind.equivalent}\``;
28704
+ case "unknown":
28705
+ return "";
28706
+ }
28707
+ }
28708
+ function withCure(sentence, command2) {
28709
+ const cure = refusalCure(classifyRefusal(command2));
28710
+ return cure === "" ? sentence : `${sentence}. The cure: ${cure}`;
28711
+ }
28712
+ function separatorCurePrefix(command2) {
28713
+ return [
28714
+ `Your previous command \`${command2}\` was refused because it chains commands. Run each command alone.`,
28715
+ "The permission layer splits a line at every shell separator (`&&`, `;`, `|`, `>`, `2>&1`, `$()`)",
28716
+ "and each fragment must match its own grant, so a compound line is refused even when every",
28717
+ "command in it is allowed on its own. This is the one retry: a second refusal blocks the story.",
28718
+ ""
28719
+ ].join(`
28720
+ `);
28721
+ }
28722
+
28584
28723
  // src/core/build/review.ts
28585
28724
  var VERDICT_WORDS = ["approve", "fixlist", "changes"];
28586
28725
  var VERDICT_ENUM = VERDICT_WORDS.join("|");
@@ -28751,8 +28890,9 @@ function renderReviewLog(outcome) {
28751
28890
  `- Repo: \`${outcome.repo}\` · wave ${outcome.wave} · epic ${outcome.epic}`,
28752
28891
  `- Branch: \`${outcome.branch}\` → \`${outcome.epicBranch}\` (${mergeWord(outcome)})`,
28753
28892
  `- Commit: ${outcome.commit ?? "(none)"}`,
28893
+ ...outcome.asIs == null ? [] : [`- Developer: **none** — ${AS_IS_MARK}. \`${outcome.branch}\` was settled as it stands, ` + `signed by ${outcome.asIs.actor}: ${outcome.asIs.note}. The Definition of Done below and ` + "the reviewer above judged that branch, unchanged."],
28754
28894
  ...outcome.developerError === null ? [] : [`- Developer: **FAILED** — ${outcome.developerError}`],
28755
- ...outcome.permissionRefused == null ? [] : [`- Developer: \`${outcome.permissionRefused}\` was refused for approval by the agent's own ` + "permission layer; the tree held committed work, so the Definition of Done below decided"],
28895
+ ...outcome.permissionRefused == null ? [] : [withCure(`- Developer: \`${outcome.permissionRefused}\` was refused for approval by the agent's own ` + "permission layer; the tree held committed work, so the Definition of Done below decided", outcome.permissionRefused)],
28756
28896
  ...outcome.budgetDeath == null ? [] : [`- Developer: died on its per-story cap — ${outcome.budgetDeath}; the tree held work, ` + "so the Definition of Done below decided"],
28757
28897
  "",
28758
28898
  "## Definition of done",
@@ -29354,7 +29494,7 @@ function renderBuildHandoff(parts) {
29354
29494
  "",
29355
29495
  ...notDone.length === 0 && (parts.carried ?? []).length === 0 && (parts.unreadableStories ?? []).length === 0 && (parts.foreignWork ?? []).length === 0 && (parts.notStarted ?? []).length === 0 && refusedButMeasured.length === 0 && diedButMeasured.length === 0 ? [`- none — every scheduled story reached \`done\` and no carried finding is unowned ` + `[src: absent:04-build/log]`] : [],
29356
29496
  ...notDone.map((o) => `- ${o.id} is \`${o.status}\` and needs a human: ${o.reason ?? "see the review"} ` + `[src: ${o.reviewRel}:1]`),
29357
- ...refusedButMeasured.map((o) => `- ${o.id}'s developer had \`${o.permissionRefused ?? ""}\` refused for approval by the agent's own ` + `permission layer; its tree held committed work, so the Definition of Done decided and the story is ` + `\`${o.status}\` — a person may still want to know the command it could not run [src: ${o.reviewRel}:1]`),
29497
+ ...refusedButMeasured.map((o) => withCure(`- ${o.id}'s developer had \`${o.permissionRefused ?? ""}\` refused for approval by the agent's own ` + `permission layer; its tree held committed work, so the Definition of Done decided and the story is ` + `\`${o.status}\` — a person may still want to know the command it could not run`, o.permissionRefused ?? "") + ` [src: ${o.reviewRel}:1]`),
29358
29498
  ...diedButMeasured.map((o) => `- ${o.id}'s developer died on its per-story cap (${o.budgetDeath ?? ""}) with work in its tree, ` + `so the Definition of Done decided and the story is \`${o.status}\` — the plan priced this story ` + `below what it cost, which is worth knowing before the next one [src: ${o.reviewRel}:1]`),
29359
29499
  ...(parts.notStarted ?? []).map((row2) => `- ${row2.id} was scheduled and never started, and is still \`${row2.status}\` — ` + `${row2.reason} [src: ${row2.rel}:1]`),
29360
29500
  ...carriedBullets(parts.carried ?? []),
@@ -29769,9 +29909,9 @@ function quoteLine(text3, line) {
29769
29909
  if (found === undefined || found.trim() === "")
29770
29910
  return null;
29771
29911
  const trimmed = found.trim();
29772
- return trimmed.length <= MAX_QUOTED_CHARS2 ? trimmed : `${trimmed.slice(0, MAX_QUOTED_CHARS2)}…`;
29912
+ return trimmed.length <= MAX_QUOTED_CHARS3 ? trimmed : `${trimmed.slice(0, MAX_QUOTED_CHARS3)}…`;
29773
29913
  }
29774
- var MAX_QUOTED_CHARS2 = 120;
29914
+ var MAX_QUOTED_CHARS3 = 120;
29775
29915
  function describeHandoff2(rel, validation, text3 = "") {
29776
29916
  if (validation.ok)
29777
29917
  return [];
@@ -35532,6 +35672,10 @@ function storyWorktreePath(root2, repo, runId, storyId) {
35532
35672
  async function refreshStoryBase(parts) {
35533
35673
  const id = parts.storyId;
35534
35674
  const base2 = await baseStateOf(parts.repoDir, parts.branch, parts.epicBranch);
35675
+ if (base2.uncounted !== null && base2.branchSha !== "") {
35676
+ parts.lines.push(` · ${id}: ${base2.uncounted} — \`${parts.branch}\` was left exactly as it is, ` + `and the dispatch below is on its current tip`);
35677
+ return;
35678
+ }
35535
35679
  if (base2.state === "current")
35536
35680
  return;
35537
35681
  const where = relative14(parts.root, parts.worktree) || parts.worktree;
@@ -35646,6 +35790,24 @@ async function rescueUncommitted(parts) {
35646
35790
  return { sha, branch: parts.branch, worktree: null, failure: null };
35647
35791
  });
35648
35792
  }
35793
+ async function updateStoryBase(parts) {
35794
+ const base2 = await baseStateOf(parts.repoDir, parts.branch, parts.epicBranch);
35795
+ if (base2.state === "current" && base2.uncounted === null)
35796
+ return { kind: "current", why: null };
35797
+ await assertWorktreeOn(parts.worktree, parts.branch, "story worktree");
35798
+ const from = await headSha(parts.worktree);
35799
+ const merged = await mergeNoFf(parts.worktree, parts.epicBranch, `sync(${parts.storyId}): \`${parts.epicBranch}\` moved under this story before it merged back`);
35800
+ if (!merged.ok)
35801
+ return { kind: "blocked", conflicts: merged.conflicts, detail: merged.detail };
35802
+ const to = await headSha(parts.worktree);
35803
+ if (to === from || to === "")
35804
+ return { kind: "current", why: base2.uncounted };
35805
+ return { kind: "updated", from, to, behind: base2.behind, uncounted: base2.uncounted };
35806
+ }
35807
+ function staleBaseConflict(branch, epicBranch, conflicts, detail, where) {
35808
+ const files = conflicts.length > 0 ? conflicts.join(", ") : detail;
35809
+ return `\`${epicBranch}\` moved since \`${branch}\` was cut, and bringing the story up to it ` + `CONFLICTS in ${files} — nothing was merged and nothing is half-applied. ` + `Resolve it in ${where} (\`git merge ${epicBranch}\`), then reopen the story`;
35810
+ }
35649
35811
 
35650
35812
  // src/core/build/entryProbe.ts
35651
35813
  import { accessSync, constants, existsSync as existsSync72, statSync as statSync20 } from "node:fs";
@@ -35892,7 +36054,7 @@ function asideBranchOf(epicBranch, ownerRunId) {
35892
36054
  return `${epicBranch}${ASIDE_SEPARATOR}${ownerRunId}`;
35893
36055
  }
35894
36056
  function uncountedReason(base2, branch) {
35895
- return `\`git rev-list --count ${base2}..${branch}\` could not count what it carries, ` + "and an uncounted branch is never deleted";
36057
+ return `${uncountedCount(base2, branch)}, and an uncounted branch is never deleted`;
35896
36058
  }
35897
36059
  async function releaseEpicBranch(parts) {
35898
36060
  const { repoDir, branch } = parts;
@@ -35902,7 +36064,7 @@ async function releaseEpicBranch(parts) {
35902
36064
  if (checkout !== null) {
35903
36065
  return { kind: "kept", branch, reason: `it is checked out in ${checkout}` };
35904
36066
  }
35905
- const commits = await commitsAhead(repoDir, parts.base, branch);
36067
+ const commits = await commitsBetween(repoDir, parts.base, branch);
35906
36068
  if (commits === null)
35907
36069
  return { kind: "kept", branch, reason: uncountedReason(parts.base, branch) };
35908
36070
  const sha = await shaOf(repoDir, branch);
@@ -36309,6 +36471,7 @@ function readReviewLedger(runDir, storyId) {
36309
36471
  developerErroredWith: null,
36310
36472
  blockedWithNothingRun: false,
36311
36473
  reopened: null,
36474
+ asIs: null,
36312
36475
  fixRound: null,
36313
36476
  formatRetries: 0,
36314
36477
  formatRefusal: null,
@@ -36329,6 +36492,7 @@ function readReviewLedger(runDir, storyId) {
36329
36492
  let current = [];
36330
36493
  let lastDodOutputPath = null;
36331
36494
  let reopened = null;
36495
+ let asIs = null;
36332
36496
  let fixRound = null;
36333
36497
  let formatRetries = 0;
36334
36498
  let formatRefusal = null;
@@ -36366,6 +36530,7 @@ function readReviewLedger(runDir, storyId) {
36366
36530
  };
36367
36531
  if (payload.reason === "fix")
36368
36532
  fixRound = reopened;
36533
+ asIs = payload.reason === "as_is" ? reopened : null;
36369
36534
  formatRetries = 0;
36370
36535
  formatRefusal = null;
36371
36536
  spawned = null;
@@ -36397,6 +36562,7 @@ function readReviewLedger(runDir, storyId) {
36397
36562
  epicBase = payload.epic_base;
36398
36563
  if (payload.status === "done")
36399
36564
  fixRound = null;
36565
+ asIs = null;
36400
36566
  blockedWithNothingRun = payload.status === "blocked" && payload.verdict === "n-a" && (payload.commit === null || payload.commit === undefined || payload.commit === "") && !ranACheck && !sawReviewer;
36401
36567
  }
36402
36568
  if (event4.type !== "check.passed" && event4.type !== "check.failed")
@@ -36456,6 +36622,7 @@ function readReviewLedger(runDir, storyId) {
36456
36622
  developerErroredWith,
36457
36623
  blockedWithNothingRun,
36458
36624
  reopened,
36625
+ asIs,
36459
36626
  fixRound,
36460
36627
  formatRetries,
36461
36628
  formatRefusal,
@@ -38645,6 +38812,10 @@ async function rederiveImplicitPlan(ctx, workspace, parts, lines) {
38645
38812
  const content = implicitPlanContent(parts);
38646
38813
  const storyBranch = storyBranchOf(ctx.runId, IMPLICIT_STORY_ID);
38647
38814
  const commits = await commitsBetween(repoDirOf(workspace, content.repo), content.branch, storyBranch);
38815
+ if (commits === null) {
38816
+ kept(uncountedCount(content.branch, storyBranch));
38817
+ return;
38818
+ }
38648
38819
  if (commits > 0) {
38649
38820
  kept(`\`${storyBranch}\` carries ${String(commits)} commit(s) beyond \`${content.branch}\``);
38650
38821
  return;
@@ -38683,6 +38854,7 @@ class BuildSession {
38683
38854
  outcomes = new Map;
38684
38855
  refusals = new Map;
38685
38856
  capDeaths = new Map;
38857
+ asIsSettlements = new Map;
38686
38858
  epics = new EpicState;
38687
38859
  lines = [];
38688
38860
  counters = new ReviewCounters;
@@ -38765,6 +38937,10 @@ class BuildSession {
38765
38937
  const review = this.reviewWorkFor(planned);
38766
38938
  if (review !== null)
38767
38939
  return await this.prepareReview(planned, review);
38940
+ const asIs = this.asIsFor(planned);
38941
+ if (asIs !== null) {
38942
+ return refusedOnSequence(this.ctx, `${planned.story.id} was reopened \`--as-is\` by ${asIs.actor} (${asIs.note}) — the branch is to be ` + "settled as it stands, and `--prepare` dispatches a developer. `tldrx next` (headless) runs the " + "dod, the review and the merge over it; to build it with a developer instead, " + `\`tldrx story reopen ${planned.story.id} --note "…"\` replaces the signature.`);
38943
+ }
38768
38944
  let fixlist;
38769
38945
  try {
38770
38946
  fixlist = this.fixlistFor(planned.story.id);
@@ -39073,10 +39249,88 @@ class BuildSession {
39073
39249
  throw rejected.reason;
39074
39250
  return halves;
39075
39251
  }
39252
+ asIsFor(planned) {
39253
+ return readReviewLedger(this.ctx.runDir, planned.story.id).asIs;
39254
+ }
39255
+ async asIsHalf(planned, asIs) {
39256
+ const id = planned.story.id;
39257
+ const before = this.statusOf(planned);
39258
+ this.refusals.delete(id);
39259
+ this.capDeaths.delete(id);
39260
+ this.asIsSettlements.set(id, asIs);
39261
+ const story = await this.writes.run(() => this.openStory(planned));
39262
+ await this.writes.run(() => {
39263
+ this.ctx.emit("task.started", {
39264
+ phase: this.ctx.phaseId,
39265
+ story: id,
39266
+ wave: planned.wave,
39267
+ repo: planned.story.repo,
39268
+ branch: story.branch,
39269
+ attempt: story.attempt,
39270
+ as_is: true
39271
+ });
39272
+ this.setStoryStatus(planned, "in_progress");
39273
+ });
39274
+ this.lines.push(` · ${id}: settling \`${story.branch}\` as it stands — signed by ${asIs.actor}: ${asIs.note}`);
39275
+ const install = await this.installDeps(story);
39276
+ if (install !== null && installFailed(install)) {
39277
+ return {
39278
+ story,
39279
+ cost: 0,
39280
+ dod: [],
39281
+ commit: null,
39282
+ failure: installFailureReason(install, planned.story.repo),
39283
+ developerError: null,
39284
+ before
39285
+ };
39286
+ }
39287
+ const ahead = await commitsBetween(story.repoDir, story.epicBranch, story.branch);
39288
+ if (ahead === null || ahead === 0) {
39289
+ return {
39290
+ story,
39291
+ cost: 0,
39292
+ dod: [],
39293
+ commit: null,
39294
+ failure: asIsNotAheadReason(story.branch, story.epicBranch, ahead),
39295
+ developerError: null,
39296
+ before
39297
+ };
39298
+ }
39299
+ const dod = await this.runDod(story);
39300
+ if (!this.dodProves(dod)) {
39301
+ const failing = dodFailure(dod);
39302
+ return {
39303
+ story,
39304
+ cost: 0,
39305
+ dod,
39306
+ commit: null,
39307
+ failure: failing === undefined ? "the story declares no dod commands, so nothing could prove it" : dodFailureReason(failing, planned.story.repo),
39308
+ developerError: null,
39309
+ before
39310
+ };
39311
+ }
39312
+ const commit2 = await headSha(story.worktree);
39313
+ if (commit2 === "") {
39314
+ return {
39315
+ story,
39316
+ cost: 0,
39317
+ dod,
39318
+ commit: null,
39319
+ failure: `\`${story.branch}\` has no HEAD to take — git could not resolve it`,
39320
+ developerError: null,
39321
+ before
39322
+ };
39323
+ }
39324
+ return { story, cost: 0, dod, commit: commit2, failure: null, developerError: null, before };
39325
+ }
39076
39326
  async buildHalf(planned) {
39327
+ const asIs = this.asIsFor(planned);
39328
+ if (asIs !== null)
39329
+ return await this.asIsHalf(planned, asIs);
39077
39330
  const before = this.statusOf(planned);
39078
39331
  this.refusals.delete(planned.story.id);
39079
39332
  this.capDeaths.delete(planned.story.id);
39333
+ this.asIsSettlements.delete(planned.story.id);
39080
39334
  const story = await this.writes.run(() => this.openStory(planned, true));
39081
39335
  await this.writes.run(() => {
39082
39336
  this.ctx.emit("task.started", {
@@ -39102,55 +39356,64 @@ class BuildSession {
39102
39356
  };
39103
39357
  }
39104
39358
  const handed = await headSha(story.worktree);
39105
- const developer = await this.spawnDeveloper(story);
39106
- const capDeath = developer.error !== null && diedOnCap(developer.error);
39359
+ let developer = await this.spawnDeveloper(story);
39107
39360
  let budgetDeath = null;
39108
- if (developer.error !== null) {
39109
- const proven = capDeath && await workSince({
39110
- workspaceRoot: this.workspace.root,
39111
- repoDir: story.repoDir,
39112
- worktree: story.worktree,
39113
- since: handed
39114
- });
39115
- if (!proven) {
39116
- return {
39117
- story,
39118
- cost: developer.cost,
39119
- dod: [],
39120
- commit: null,
39121
- failure: null,
39122
- developerError: developer.error,
39123
- before
39124
- };
39361
+ let spent = 0;
39362
+ let separatorRetries = 0;
39363
+ for (;; ) {
39364
+ spent = round25(spent + developer.cost);
39365
+ const capDeath = developer.error !== null && diedOnCap(developer.error);
39366
+ if (developer.error !== null) {
39367
+ const proven2 = capDeath && await workSince({
39368
+ workspaceRoot: this.workspace.root,
39369
+ repoDir: story.repoDir,
39370
+ worktree: story.worktree,
39371
+ since: handed
39372
+ });
39373
+ if (!proven2) {
39374
+ return {
39375
+ story,
39376
+ cost: spent,
39377
+ dod: [],
39378
+ commit: null,
39379
+ failure: null,
39380
+ developerError: developer.error,
39381
+ before
39382
+ };
39383
+ }
39384
+ budgetDeath = developer.error;
39385
+ this.capDeaths.set(story.planned.story.id, developer.error);
39125
39386
  }
39126
- budgetDeath = developer.error;
39127
- this.capDeaths.set(story.planned.story.id, developer.error);
39128
- }
39129
- const spent = developer.cost;
39130
- if (developer.refused !== null) {
39387
+ if (developer.refused === null)
39388
+ break;
39131
39389
  const proven = await workSince({
39132
39390
  workspaceRoot: this.workspace.root,
39133
39391
  repoDir: story.repoDir,
39134
39392
  worktree: story.worktree,
39135
39393
  since: handed
39136
39394
  });
39137
- if (!proven) {
39138
- return {
39139
- story,
39140
- cost: spent,
39141
- dod: [],
39142
- commit: null,
39143
- failure: permissionBlockReason(developer.refused),
39144
- developerError: null,
39145
- before
39146
- };
39395
+ if (proven) {
39396
+ this.refusals.set(story.planned.story.id, developer.refused);
39397
+ break;
39398
+ }
39399
+ if (classifyRefusal(developer.refused).kind === "separator" && separatorRetries < MAX_SEPARATOR_RETRIES) {
39400
+ separatorRetries += 1;
39401
+ developer = await this.spawnDeveloper(story, { after: developer.refused });
39402
+ continue;
39147
39403
  }
39148
- this.refusals.set(story.planned.story.id, developer.refused);
39404
+ return {
39405
+ story,
39406
+ cost: spent,
39407
+ dod: [],
39408
+ commit: null,
39409
+ failure: permissionBlockReason(developer.refused, { retried: separatorRetries > 0 }),
39410
+ developerError: null,
39411
+ before
39412
+ };
39149
39413
  }
39150
39414
  const dod = await this.runDod(story);
39151
- const green = dod.length === 0 ? dodIsSatisfiedEmpty(this.plan) : dodGreen({ dod });
39152
- if (!green) {
39153
- const failing = dod.find((r) => dodRefused(r) || r.exitCode !== 0 || r.timedOut);
39415
+ if (!this.dodProves(dod)) {
39416
+ const failing = dodFailure(dod);
39154
39417
  const why = failing === undefined ? "the story declares no dod commands, so nothing could prove it" : dodFailureReason(failing, story.planned.story.repo);
39155
39418
  return {
39156
39419
  story,
@@ -39194,18 +39457,49 @@ class BuildSession {
39194
39457
  await this.block(story, "the story produced no commit to review", half.cost, dod);
39195
39458
  return "settled";
39196
39459
  }
39460
+ const update = await updateStoryBase({
39461
+ storyId: story.planned.story.id,
39462
+ repoDir: story.repoDir,
39463
+ worktree: story.worktree,
39464
+ branch: story.branch,
39465
+ epicBranch: story.epicBranch
39466
+ });
39467
+ if (update.kind === "blocked") {
39468
+ await this.block(story, staleBaseConflict(story.branch, story.epicBranch, update.conflicts, update.detail, relative16(this.ctx.root, story.worktree) || story.worktree), half.cost, dod, { commit: commit2, conflicts: update.conflicts });
39469
+ return "settled";
39470
+ }
39471
+ let proven = dod;
39472
+ if (update.kind === "updated") {
39473
+ this.lines.push(` · ${story.planned.story.id}: \`${story.branch}\` was ${String(update.behind)} commit(s) behind ` + `\`${story.epicBranch}\`${update.uncounted === null ? "" : ` (${update.uncounted})`} — merged the epic ` + `into it (${update.from.slice(0, 7)} → ${update.to.slice(0, 7)}) and re-ran the DoD on the result`);
39474
+ this.ctx.emit("story.base_updated", {
39475
+ phase: this.ctx.phaseId,
39476
+ story: story.planned.story.id,
39477
+ repo: story.planned.story.repo,
39478
+ branch: story.branch,
39479
+ base: story.epicBranch,
39480
+ from: update.from,
39481
+ to: update.to,
39482
+ commits: update.behind
39483
+ });
39484
+ proven = await this.runDod(story);
39485
+ if (!this.dodProves(proven)) {
39486
+ const failing = dodFailure(proven);
39487
+ await this.block(story, `${failing === undefined ? "the story declares no dod commands, so nothing could prove it" : dodFailureReason(failing, story.planned.story.repo)}` + ` — re-run after \`${story.epicBranch}\` moved under \`${story.branch}\` ` + `(${update.from.slice(0, 7)} → ${update.to.slice(0, 7)}); nothing was merged`, half.cost, proven, { commit: commit2 });
39488
+ return "settled";
39489
+ }
39490
+ }
39197
39491
  const carried = await commitsBetween(story.repoDir, story.epicBranch, story.branch);
39198
39492
  const epicShaBefore = await fullShaOf(story.repoDir, story.epicBranch);
39199
39493
  const merge = await this.mergeIntoEpic(story);
39200
39494
  if (!merge.ok) {
39201
- await this.block(story, `merge into \`${story.epicBranch}\` failed: ${merge.detail}`, half.cost, dod, {
39495
+ await this.block(story, `merge into \`${story.epicBranch}\` failed: ${merge.detail}`, half.cost, proven, {
39202
39496
  commit: commit2,
39203
39497
  conflicts: merge.conflicts
39204
39498
  });
39205
39499
  return "settled";
39206
39500
  }
39207
39501
  this.noteMerged(story, carried);
39208
- return await this.reviewAndSettle(story, dod, commit2, half.cost, carried, epicShaBefore);
39502
+ return await this.reviewAndSettle(story, proven, commit2, half.cost, carried, epicShaBefore);
39209
39503
  }
39210
39504
  async reviewAndSettle(story, dod, commit2, priorCost, carried, epicBase, supplied) {
39211
39505
  if (supplied === undefined && this.ctx.attendedByHost) {
@@ -39481,9 +39775,8 @@ class BuildSession {
39481
39775
  }
39482
39776
  async pipelineFromDod(story, developerCost) {
39483
39777
  const dod = await this.runDod(story);
39484
- const green = dod.length === 0 ? dodIsSatisfiedEmpty(this.plan) : dodGreen({ dod });
39485
- if (!green) {
39486
- const failing = dod.find((r) => dodRefused(r) || r.exitCode !== 0 || r.timedOut);
39778
+ if (!this.dodProves(dod)) {
39779
+ const failing = dodFailure(dod);
39487
39780
  await this.block(story, failing === undefined ? "the story declares no dod commands, so nothing could prove it" : dodFailureReason(failing, story.planned.story.repo), developerCost, dod);
39488
39781
  return "settled";
39489
39782
  }
@@ -39586,7 +39879,7 @@ class BuildSession {
39586
39879
  advisories: this.advisories
39587
39880
  });
39588
39881
  }
39589
- async spawnDeveloper(story) {
39882
+ async spawnDeveloper(story, retry = null) {
39590
39883
  const cap2 = developerCap(this.capParts, story.planned.story.id, story.attempt);
39591
39884
  const commands = this.repoCommands(story.planned.story.repo);
39592
39885
  this.ctx.emit("agent.spawned", {
@@ -39595,10 +39888,11 @@ class BuildSession {
39595
39888
  role: "developer",
39596
39889
  model: this.model(),
39597
39890
  effort: this.ctx.effort,
39598
- max_budget_usd: cap2
39891
+ max_budget_usd: cap2,
39892
+ ...retry === null ? {} : { retry: "separator-cure", retry_after: retry.after }
39599
39893
  }, 0, "developer");
39600
39894
  const agent = await spawnAgent({
39601
- prompt: this.developerPrompt(story),
39895
+ prompt: (retry === null ? "" : separatorCurePrefix(retry.after)) + this.developerPrompt(story),
39602
39896
  model: this.model(),
39603
39897
  effort: this.ctx.effort,
39604
39898
  maxBudgetUsd: cap2,
@@ -39640,6 +39934,9 @@ class BuildSession {
39640
39934
  });
39641
39935
  return { cost: round25(agent.costUsd), error, refused: null };
39642
39936
  }
39937
+ dodProves(dod) {
39938
+ return dod.length === 0 ? dodIsSatisfiedEmpty(this.plan) : dodGreen({ dod });
39939
+ }
39643
39940
  async runDod(story) {
39644
39941
  const scoped = await this.scopedParts(story);
39645
39942
  const results = await runStoryDod({
@@ -39973,6 +40270,7 @@ class BuildSession {
39973
40270
  reason: parts.reason,
39974
40271
  permissionRefused: this.refusals.get(id) ?? null,
39975
40272
  budgetDeath: this.capDeaths.get(id) ?? null,
40273
+ asIs: this.asIsSettlements.get(id) ?? null,
39976
40274
  rescued,
39977
40275
  cost_usd: parts.cost
39978
40276
  };
@@ -40003,12 +40301,13 @@ class BuildSession {
40003
40301
  attempt: story.attempt,
40004
40302
  ...parts.epicBase === null || parts.epicBase === undefined || parts.epicBase === "" ? {} : { epic_base: parts.epicBase },
40005
40303
  ...outcome.permissionRefused == null ? {} : { permission_refused: outcome.permissionRefused },
40006
- ...outcome.budgetDeath == null ? {} : { budget_death: outcome.budgetDeath }
40304
+ ...outcome.budgetDeath == null ? {} : { budget_death: outcome.budgetDeath },
40305
+ ...outcome.asIs == null ? {} : { as_is: true, as_is_by: outcome.asIs.actor, as_is_note: outcome.asIs.note }
40007
40306
  });
40008
40307
  if (pruning && (rescued === null || rescued.worktree === null)) {
40009
40308
  await removeWorktree(story.repoDir, story.worktree);
40010
40309
  }
40011
- this.lines.push(` ${status === "done" ? "✓" : "·"} ${id} → \`${status}\`` + (outcome.reason === null ? "" : ` (${outcome.reason})`) + (outcome.permissionRefused == null ? "" : ` — \`${outcome.permissionRefused}\` was refused for approval; the tree held committed work, so the DoD decided`));
40310
+ this.lines.push(` ${status === "done" ? "✓" : "·"} ${id} → \`${status}\`` + (outcome.reason === null ? "" : ` (${outcome.reason})`) + (outcome.asIs == null ? "" : ` — ${AS_IS_MARK}; ${outcome.asIs.actor} signed it`) + (outcome.permissionRefused == null ? "" : ` — ${withCure(`\`${outcome.permissionRefused}\` was refused for approval; the tree held committed work, so the DoD decided`, outcome.permissionRefused)}`));
40012
40311
  }
40013
40312
  writeLog(outcome) {
40014
40313
  const dir = join100(this.ctx.runDir, BUILD_PHASE, LOG_DIR);
@@ -40691,15 +40990,12 @@ function developerTools(repoCommands, options = {}) {
40691
40990
  ...BASE_TOOLS,
40692
40991
  ...options.skills === true ? ["Skill"] : [],
40693
40992
  ...repoCommands.flatMap((command2) => bashGrantsFor(command2)),
40694
- "Bash(git add *)",
40695
- "Bash(git commit *)",
40696
- "Bash(git rm *)",
40697
- "Bash(git mv *)",
40698
- "Bash(git restore *)"
40993
+ ...developerGitGrants()
40699
40994
  ];
40700
40995
  }
40701
- function permissionBlockReason(command2) {
40702
- return `permission — \`${command2}\` was refused for approval by the agent's own permission layer, ` + "and a headless turn has nobody to approve it: the same allowance would refuse it again, " + "so this attempt was not repeated";
40996
+ function permissionBlockReason(command2, options = {}) {
40997
+ const base2 = `permission — \`${command2}\` was refused for approval by the agent's own permission layer, ` + "and a headless turn has nobody to approve it: the same allowance would refuse it again, " + "so this attempt was not repeated" + (options.retried === true ? " beyond the one re-spawn with the cure stated, which was refused too" : "");
40998
+ return withCure(base2, command2);
40703
40999
  }
40704
41000
  function diedOnCap(error) {
40705
41001
  return error.includes("Reached maximum budget");
@@ -48138,8 +48434,13 @@ function reopenStory(options) {
48138
48434
  return refuse4(['story reopen needs a story id: `tldrx story reopen S3 --note "why"`']);
48139
48435
  }
48140
48436
  const forFix = options.forFix === true;
48437
+ const asIs = options.asIs === true;
48141
48438
  if (options.note.trim() === "") {
48142
- return forFix ? refuse4([
48439
+ return asIs ? refuse4([
48440
+ `story reopen --as-is needs --note: \`tldrx story reopen ${id} --as-is --note "<what you did by hand>"\``,
48441
+ " the note is the ONLY record of who finished this branch and how — no developer will run,",
48442
+ " so nothing else in the log will say where the commits came from"
48443
+ ]) : forFix ? refuse4([
48143
48444
  `story reopen --for-fix needs --note: \`tldrx story reopen ${id} --for-fix --note "<the defect>"\``,
48144
48445
  " the note IS the defect — it is what scopes the fix round, and what the reviewer reads",
48145
48446
  " to tell a fix from a second opinion about the story"
@@ -48233,7 +48534,7 @@ function reopenStory(options) {
48233
48534
  }
48234
48535
  throw error;
48235
48536
  }
48236
- const event7 = reopenEvent(options, store.runId, id, row2.wave, row2.status, ledger4.verdicts, forFix);
48537
+ const event7 = reopenEvent(options, store.runId, id, row2.wave, row2.status, ledger4.verdicts, forFix, asIs);
48237
48538
  const validation = validateEvent(event7);
48238
48539
  if (!validation.ok) {
48239
48540
  const first2 = validation.issues[0];
@@ -48264,6 +48565,19 @@ function reopenStory(options) {
48264
48565
  ]
48265
48566
  };
48266
48567
  }
48568
+ if (asIs) {
48569
+ return {
48570
+ code: EXIT_OK12,
48571
+ lines: [
48572
+ `reopened ${id} in ${store.runId} — \`${row2.status}\` → \`${REOPENED_TO}\` (${row2.wave}), ` + "to be settled from its branch AS IT STANDS",
48573
+ ` signed by ${options.actor}: ${options.note}`,
48574
+ " no developer will be spawned: the next Build turn runs the dod, the review and the merge " + "over the commits already on the story branch",
48575
+ " it is not a shortcut past either gate — a red dod BLOCKS the story, and so does a branch " + "that carries no commit its epic has not already got; the refusal says which",
48576
+ " the record will say the branch was taken as it stands and name you, so nothing reads as " + "though a developer delivered it",
48577
+ ...kept
48578
+ ]
48579
+ };
48580
+ }
48267
48581
  return {
48268
48582
  code: EXIT_OK12,
48269
48583
  lines: [
@@ -48289,7 +48603,7 @@ function nextStep(runId, stage2) {
48289
48603
  function buildStage(store) {
48290
48604
  return store.run.phases.find((phase) => phase.id === BUILD_PHASE2)?.stages[0];
48291
48605
  }
48292
- function reopenEvent(options, runId, storyId, wave, from, verdicts, forFix) {
48606
+ function reopenEvent(options, runId, storyId, wave, from, verdicts, forFix, asIs) {
48293
48607
  return {
48294
48608
  ts: options.at,
48295
48609
  run: runId,
@@ -48304,7 +48618,7 @@ function reopenEvent(options, runId, storyId, wave, from, verdicts, forFix) {
48304
48618
  from_status: from,
48305
48619
  to_status: REOPENED_TO,
48306
48620
  verdicts,
48307
- reason: forFix ? "fix" : "attempts",
48621
+ reason: forFix ? "fix" : asIs ? "as_is" : "attempts",
48308
48622
  note: options.note
48309
48623
  }
48310
48624
  };
@@ -48511,7 +48825,7 @@ var VALUE_FLAGS6 = ["run", "root", "note"];
48511
48825
  var storyCommand = {
48512
48826
  name: "story",
48513
48827
  summary: "Give one Build story another run of attempts, or widen the paths it declares",
48514
- usage: `tldrx story reopen <id> --note <text> [--for-fix] [--run <id>] [--root <path>]
48828
+ usage: `tldrx story reopen <id> --note <text> [--for-fix | --as-is] [--run <id>] [--root <path>]
48515
48829
  ` + " tldrx story widen <id> <path>… --note <text> [--run <id>] [--root <path>]",
48516
48830
  implemented: true,
48517
48831
  async run(argv) {
@@ -48529,11 +48843,20 @@ ${storyCommand.usage}
48529
48843
  function storyReopen(argv) {
48530
48844
  try {
48531
48845
  const args = parseArgs(argv, VALUE_FLAGS6);
48846
+ if (boolFlag(args, "for-fix") && boolFlag(args, "as-is")) {
48847
+ process.stderr.write(`tldrx story reopen: --for-fix and --as-is answer different questions — nothing was written
48848
+ ` + " --for-fix opens a fix round on a `done` story: one named defect, landed by a developer.\n" + ` --as-is settles an UNFINISHED story from its branch as it stands, with no developer at all.
48849
+ ` + ` Type the one you mean.
48850
+ ` + `${storyCommand.usage}
48851
+ `);
48852
+ return EXIT_USAGE;
48853
+ }
48532
48854
  const outcome = reopenStory({
48533
48855
  root: workspaceRootFrom(args),
48534
48856
  storyId: args.positionals[0] ?? "",
48535
48857
  note: stringFlag(args, "note") ?? "",
48536
48858
  forFix: boolFlag(args, "for-fix"),
48859
+ asIs: boolFlag(args, "as-is"),
48537
48860
  runId: stringFlag(args, "run"),
48538
48861
  actor: currentActor(),
48539
48862
  at: nowRfc3339()
@@ -48553,6 +48876,12 @@ function storyReopen(argv) {
48553
48876
  function storyWiden(argv) {
48554
48877
  try {
48555
48878
  const args = parseArgs(argv, VALUE_FLAGS6);
48879
+ if (boolFlag(args, "as-is")) {
48880
+ process.stderr.write("tldrx story widen: --as-is is a flag of `reopen`, not of `widen` — nothing was written\n" + " widening declares a path; `--as-is` settles a story from its branch as it stands.\n" + ` \`tldrx story reopen <id> --as-is --note "<what you did by hand>"\` is the verb you want.
48881
+ ` + `${storyCommand.usage}
48882
+ `);
48883
+ return EXIT_USAGE;
48884
+ }
48556
48885
  if (boolFlag(args, "for-fix")) {
48557
48886
  process.stderr.write("tldrx story widen: --for-fix is a flag of `reopen`, not of `widen` — nothing was written\n" + " widening declares a path; `--for-fix` reopens finished work for one named defect.\n" + ` \`tldrx story reopen <id> --for-fix --note "<the defect>"\` first, then widen the reopened story.
48558
48887
  ` + `${storyCommand.usage}
@@ -52045,6 +52374,8 @@ function bullet2(item, trail) {
52045
52374
  return `${prefix5}story ${text3(payload2.story) || "?"}'s \`touches:\` WIDENED by ${actor}` + ` (${basisOf(payload2)})` + ` — +${pathList(payload2.paths)}` + ` (${String(lengthOf(payload2.before))} → ${String(lengthOf(payload2.after))} path(s))` + `${note(payload2.note)}`;
52046
52375
  case "story.base_fastforwarded":
52047
52376
  return `${prefix5}story ${text3(payload2.story) || "?"}'s base fast-forwarded to` + ` \`${text3(payload2.base) || "?"}\` — ${text3(payload2.from) || "?"} → ${text3(payload2.to) || "?"}` + ` (${String(payload2.commits ?? "?")} commit(s))`;
52377
+ case "story.base_updated":
52378
+ return `${prefix5}story ${text3(payload2.story) || "?"}'s base UPDATED to` + ` \`${text3(payload2.base) || "?"}\` before merging — ${text3(payload2.from) || "?"} →` + ` ${text3(payload2.to) || "?"} (${String(payload2.commits ?? "?")} commit(s) behind), DoD re-run`;
52048
52379
  case "epic.released":
52049
52380
  return `${prefix5}epic \`${text3(payload2.branch) || "?"}\` of run ${text3(payload2.owner) || "?"} ` + (text3(payload2.outcome) === "deleted" ? `deleted — no commit beyond \`${text3(payload2.base) || "?"}\`` : `renamed to \`${text3(payload2.renamed_to) || "?"}\` — ${String(payload2.commits ?? "?")} commit(s) ` + `beyond \`${text3(payload2.base) || "?"}\` survive there`) + ` (${text3(payload2.via) || "?"})`;
52050
52381
  case "story.work_rescued":