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/CHANGELOG.md +140 -0
- package/README.md +2 -0
- package/dist/hooks/answer-capture.js +9 -9
- package/dist/hooks/budget-gate.js +9 -9
- package/dist/hooks/{chunk-c1h38et6.js → chunk-0sy3tmfg.js} +2 -3
- package/dist/hooks/{chunk-afttzftj.js → chunk-53wgfn01.js} +6 -6
- package/dist/hooks/{chunk-8ak5kwd5.js → chunk-5stgwp3g.js} +1 -1
- package/dist/hooks/{chunk-gx6qa496.js → chunk-6z5rmj0b.js} +3 -2
- package/dist/hooks/{chunk-4y3pk4n2.js → chunk-bznt6jb2.js} +1 -1
- package/dist/hooks/{chunk-vf992pfw.js → chunk-ezfvp5kx.js} +1 -1
- package/dist/hooks/{chunk-wx1jce1t.js → chunk-j5d0nard.js} +2 -2
- package/dist/hooks/{chunk-kwaqdq9w.js → chunk-jpp137mb.js} +3 -3
- package/dist/hooks/{chunk-2caxb41h.js → chunk-ka7768vx.js} +1 -1
- package/dist/hooks/{chunk-x00a2vaz.js → chunk-m1s8a6s0.js} +2 -2
- package/dist/hooks/{chunk-1v05a468.js → chunk-n5bxz2cw.js} +1 -1
- package/dist/hooks/{chunk-pday823f.js → chunk-qzsxhsyj.js} +1 -1
- package/dist/hooks/{chunk-2ym87z6d.js → chunk-t44983nq.js} +1 -1
- package/dist/hooks/{chunk-nf2j945p.js → chunk-tjx8g39f.js} +3 -3
- package/dist/hooks/{chunk-hwsng0xq.js → chunk-yre2scxn.js} +1 -1
- package/dist/hooks/claim-sources.js +5 -5
- package/dist/hooks/dod-gate.js +8 -8
- package/dist/hooks/no-reask.js +8 -8
- package/dist/hooks/session-start.js +13 -13
- package/dist/hooks/statusline.js +9 -9
- package/dist/tldrx.js +414 -83
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25788
|
-
|
|
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
|
|
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
|
|
25803
|
-
const
|
|
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
|
-
|
|
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 <=
|
|
29912
|
+
return trimmed.length <= MAX_QUOTED_CHARS3 ? trimmed : `${trimmed.slice(0, MAX_QUOTED_CHARS3)}…`;
|
|
29773
29913
|
}
|
|
29774
|
-
var
|
|
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
|
|
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
|
|
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
|
-
|
|
39106
|
-
const capDeath = developer.error !== null && diedOnCap(developer.error);
|
|
39359
|
+
let developer = await this.spawnDeveloper(story);
|
|
39107
39360
|
let budgetDeath = null;
|
|
39108
|
-
|
|
39109
|
-
|
|
39110
|
-
|
|
39111
|
-
|
|
39112
|
-
|
|
39113
|
-
|
|
39114
|
-
|
|
39115
|
-
|
|
39116
|
-
|
|
39117
|
-
story,
|
|
39118
|
-
|
|
39119
|
-
|
|
39120
|
-
|
|
39121
|
-
|
|
39122
|
-
|
|
39123
|
-
|
|
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
|
-
|
|
39127
|
-
|
|
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 (
|
|
39138
|
-
|
|
39139
|
-
|
|
39140
|
-
|
|
39141
|
-
|
|
39142
|
-
|
|
39143
|
-
|
|
39144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39152
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
39485
|
-
|
|
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 ? "" : ` —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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":
|