@pmelab/gtd 49.0.0 → 51.0.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/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -3
- package/dist/chunks/{reserved-CYZ1bSBy.mjs → reserved-CUkk9XgL.mjs} +151 -5
- package/dist/chunks/{review-BewyJflS.mjs → review-BaCXlIT2.mjs} +84 -20
- package/dist/chunks/{steering-BXwjmocB.mjs → steering-DtUVUz9l.mjs} +44 -14
- package/dist/chunks/{tail-C1Oi2YEy.mjs → tail-CX6Pdsql.mjs} +1 -1
- package/dist/gtd.bundle.mjs +22 -5
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/web/main.js +27 -3
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +144 -11
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +415 -42
- package/package.json +1 -1
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +60 -2
- package/src/steering/review.ts +66 -9
- package/src/workflows/feature/coverage.feature +1 -0
- package/src/workflows/feature/fast-path.feature +1 -0
- package/src/workflows/feature/feature.ts +4 -1
- package/src/workflows/feature/flag-mode.feature +1 -1
- package/src/workflows/feature/lens-models.feature +1 -0
- package/src/workflows/feature/lens-review.feature +17 -4
- package/src/workflows/feature/one-step-land.feature +1 -0
- package/src/workflows/feature/prototype-rounds.feature +152 -0
- package/src/workflows/feature/prototype.feature +419 -0
- package/src/workflows/feature/prototype.test.ts +43 -0
- package/src/workflows/feature/prototype.ts +181 -0
- package/src/workflows/feature/refactor-absorbs.feature +9 -1
- package/src/workflows/feature/refactor-first.feature +13 -3
- package/src/workflows/feature/refactor-rebuild.feature +29 -5
- package/src/workflows/feature/refactor-review.feature +111 -13
- package/src/workflows/feature/refactor.test.ts +14 -4
- package/src/workflows/feature/refactor.ts +2 -0
- package/src/workflows/feature/review-answers.feature +9 -1
- package/src/workflows/feature/review-gate.feature +21 -3
- package/src/workflows/feature/reviewer-walkthrough.feature +15 -3
- package/src/workflows/feature/seam-rejection.feature +1 -0
- package/src/workflows/feature/seam.feature +1 -0
- package/src/workflows/feature/seam.test.ts +3 -2
- package/src/workflows/feature/seam.ts +19 -5
- package/src/workflows/feature/severity-budget.feature +112 -0
- package/src/workflows/feature/severity-sorted-review.feature +198 -0
- package/src/workflows/feature/slice-findings.feature +15 -2
- package/src/workflows/feature/slicing-cap.feature +9 -0
- package/src/workflows/feature/slicing.feature +1 -0
- package/src/workflows/feature/standards.feature +5 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/prose.ts +6 -0
- package/src/workflows/shared/review.ts +124 -18
- package/src/workflows/shared/skills.test.ts +4 -0
- package/src/workflows/shared/skills.ts +5 -0
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +90 -0
- package/src/workflows/shared/text.ts +181 -4
- package/src/workflows/shared/vars.ts +3 -0
package/dist/workflows.mjs
CHANGED
|
@@ -15648,6 +15648,9 @@ const qaDescriptor = {
|
|
|
15648
15648
|
const ANSWER_LEAD = "Tick exactly one:";
|
|
15649
15649
|
const ANSWER_FIX = "fix as suggested";
|
|
15650
15650
|
const ANSWER_WONT_FIX = "won't fix";
|
|
15651
|
+
/** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
|
|
15652
|
+
const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/;
|
|
15653
|
+
const severityLine = (label) => `_${label}_`;
|
|
15651
15654
|
/**
|
|
15652
15655
|
* The canonical sample. Every detail here is load-bearing for the formatter
|
|
15653
15656
|
* round-trip: both footnote bodies exceed 80 characters so a reflow actually
|
|
@@ -15938,12 +15941,26 @@ const answerListOf = (content, body) => {
|
|
|
15938
15941
|
})
|
|
15939
15942
|
};
|
|
15940
15943
|
};
|
|
15944
|
+
/** A leading severity line is the chunk's tier, never its prose. */
|
|
15945
|
+
const splitSeverity = (content, run) => {
|
|
15946
|
+
const first = run[0];
|
|
15947
|
+
const label = first?.type === "paragraph" ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1] : void 0;
|
|
15948
|
+
if (label === void 0) return { leadingRun: run };
|
|
15949
|
+
return {
|
|
15950
|
+
severity: {
|
|
15951
|
+
label,
|
|
15952
|
+
line: toLspPosition(first.position.start).line
|
|
15953
|
+
},
|
|
15954
|
+
leadingRun: run.slice(1)
|
|
15955
|
+
};
|
|
15956
|
+
};
|
|
15941
15957
|
/** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
|
|
15942
15958
|
const parseChunkBody = (content, lines, title, body) => {
|
|
15943
15959
|
const answer = answerListOf(content, body);
|
|
15944
15960
|
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
15945
15961
|
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
15946
|
-
const
|
|
15962
|
+
const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
15963
|
+
const { severity, leadingRun } = splitSeverity(content, run);
|
|
15947
15964
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
15948
15965
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
15949
15966
|
const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
@@ -15962,6 +15979,7 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
15962
15979
|
files,
|
|
15963
15980
|
errors,
|
|
15964
15981
|
...answer === void 0 ? {} : { answers: answer.options },
|
|
15982
|
+
...severity === void 0 ? {} : { severity },
|
|
15965
15983
|
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
15966
15984
|
};
|
|
15967
15985
|
};
|
|
@@ -15998,7 +16016,7 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15998
16016
|
const errors = [];
|
|
15999
16017
|
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
16000
16018
|
const title = headingText$1(content, heading);
|
|
16001
|
-
const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
16019
|
+
const { description, descriptionNodes, descriptionInline, files, answers, severity, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
16002
16020
|
errors.push(...bodyErrors);
|
|
16003
16021
|
if (files.length === 0 && boxed) errors.push({
|
|
16004
16022
|
message: `Chunk "${title}" has no file pointers`,
|
|
@@ -16012,7 +16030,8 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
16012
16030
|
descriptionInline,
|
|
16013
16031
|
files,
|
|
16014
16032
|
headingLine,
|
|
16015
|
-
...answers === void 0 ? {} : { answers }
|
|
16033
|
+
...answers === void 0 ? {} : { answers },
|
|
16034
|
+
...severity === void 0 ? {} : { severity }
|
|
16016
16035
|
});
|
|
16017
16036
|
}
|
|
16018
16037
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -16313,6 +16332,7 @@ const reviewView = (content) => {
|
|
|
16313
16332
|
detail: chunk.description,
|
|
16314
16333
|
detailInline: chunk.descriptionInline,
|
|
16315
16334
|
body: chunk.descriptionNodes,
|
|
16335
|
+
...chunk.severity === void 0 ? {} : { status: chunk.severity.label },
|
|
16316
16336
|
anchor: {
|
|
16317
16337
|
kind: "chunk",
|
|
16318
16338
|
index: chunkIndex
|
|
@@ -16460,15 +16480,19 @@ const reviewDescriptor = {
|
|
|
16460
16480
|
openThreads: openThreadsOf
|
|
16461
16481
|
};
|
|
16462
16482
|
const pointerLabel = (f) => `${f.path}${f.line === void 0 ? "" : `#${f.line}${f.rangeEnd === void 0 ? "" : `-${f.rangeEnd}`}`}`;
|
|
16463
|
-
const
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16483
|
+
const chunkProse = (chunk, retiered) => [chunk?.description ?? "", retiered && chunk?.severity !== void 0 ? severityLine(chunk.severity.label) : ""].filter((p) => p !== "").join("\n\n");
|
|
16484
|
+
const chunkNote = (chunk, prior) => {
|
|
16485
|
+
const retiered = chunk.severity !== void 0 && chunk.severity.label !== prior?.severity?.label;
|
|
16486
|
+
return chunkProse(chunk, retiered) === chunkProse(prior, retiered) || prior === void 0 && chunk.answers !== void 0 ? [] : [{
|
|
16487
|
+
line: chunk.headingLine,
|
|
16488
|
+
note: {
|
|
16489
|
+
kind: "chunk",
|
|
16490
|
+
anchor: chunk.title,
|
|
16491
|
+
before: chunkProse(prior, retiered),
|
|
16492
|
+
text: chunkProse(chunk, retiered)
|
|
16493
|
+
}
|
|
16494
|
+
}];
|
|
16495
|
+
};
|
|
16472
16496
|
const pointerNotes = (chunk, prior) => chunk.files.flatMap((file) => {
|
|
16473
16497
|
const label = pointerLabel(file);
|
|
16474
16498
|
const was = prior?.files.find((f) => pointerLabel(f) === label);
|
|
@@ -16582,7 +16606,13 @@ const reviewDecisions = (before, after) => {
|
|
|
16582
16606
|
const reviewChunks = (content) => parseReviewDoc(content).changesets.map((c) => ({
|
|
16583
16607
|
title: c.title,
|
|
16584
16608
|
line: c.headingLine,
|
|
16585
|
-
pointers: c.files.length
|
|
16609
|
+
pointers: c.files.length,
|
|
16610
|
+
...c.severity === void 0 ? {} : { severityLine: c.severity.line },
|
|
16611
|
+
files: c.files.map((f) => ({
|
|
16612
|
+
path: f.path,
|
|
16613
|
+
...f.line === void 0 ? {} : { line: f.line },
|
|
16614
|
+
...f.rangeEnd === void 0 ? {} : { rangeEnd: f.rangeEnd }
|
|
16615
|
+
}))
|
|
16586
16616
|
}));
|
|
16587
16617
|
//#endregion
|
|
16588
16618
|
//#region src/steering/CodeThreads.ts
|
|
@@ -17211,6 +17241,7 @@ const defaults = {
|
|
|
17211
17241
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
17212
17242
|
refactorQualityReviews: "correctness, ponytail-review, conventions",
|
|
17213
17243
|
continuations: "3",
|
|
17244
|
+
prototypeRounds: "3",
|
|
17214
17245
|
continuationStall: "2"
|
|
17215
17246
|
};
|
|
17216
17247
|
/** Environment settings. */
|
|
@@ -17288,6 +17319,11 @@ task in front of you end to end, tests first, and come back to fix
|
|
|
17288
17319
|
things when redirected — a failing check, or reviewer feedback. Stay
|
|
17289
17320
|
strictly inside that task; never touch code outside it. Treat feedback as the
|
|
17290
17321
|
diagnosis to act on; discard it, with reason, only when simply wrong.`;
|
|
17322
|
+
const testerPersona = `You are the hands-on tester in gtd's build pipeline: use what was just
|
|
17323
|
+
built the way a person would — run the real program, open the real
|
|
17324
|
+
page, call the real endpoint — and report exactly what you did and
|
|
17325
|
+
what you saw. Never fix anything yourself, and never pass a criterion
|
|
17326
|
+
you did not watch hold.`;
|
|
17291
17327
|
const finisherPersona = `You are the closing identity in gtd's build pipeline — the last coder
|
|
17292
17328
|
before it closes. Fix a late-breaking failing check after sign-off,
|
|
17293
17329
|
focused and minimal. Every turn lands its own commit — nothing here
|
|
@@ -17609,6 +17645,56 @@ const withWalkthrough = (review, report) => {
|
|
|
17609
17645
|
const body = lines.join("\n").trimEnd();
|
|
17610
17646
|
return summary.length === 0 ? `${body}\n` : `${body}\n\n${summary.join("\n\n")}\n`;
|
|
17611
17647
|
};
|
|
17648
|
+
const chunkBlocks = (review) => {
|
|
17649
|
+
const lines = review.split("\n");
|
|
17650
|
+
const chunks = reviewChunks(review);
|
|
17651
|
+
return {
|
|
17652
|
+
head: lines.slice(0, chunks[0]?.line ?? lines.length),
|
|
17653
|
+
blocks: chunks.map((c, i) => ({
|
|
17654
|
+
title: c.title,
|
|
17655
|
+
lines: lines.slice(c.line, chunks[i + 1]?.line ?? lines.length)
|
|
17656
|
+
}))
|
|
17657
|
+
};
|
|
17658
|
+
};
|
|
17659
|
+
const isBlank = (line) => line !== void 0 && line.trim() === "";
|
|
17660
|
+
/** \`review\` with every chunk's severity line removed, so a re-review is ranked afresh. */
|
|
17661
|
+
const unranked = (review) => {
|
|
17662
|
+
const drop = new Set(reviewChunks(review).flatMap((c) => c.severityLine ?? []));
|
|
17663
|
+
if (drop.size === 0) return review;
|
|
17664
|
+
const lines = review.split("\n");
|
|
17665
|
+
return lines.filter((_, i) => !drop.has(i) && !(drop.has(i - 1) && isBlank(lines[i]))).join("\n");
|
|
17666
|
+
};
|
|
17667
|
+
const chunkTexts = (review) => new Map(chunkBlocks(review).blocks.map((b) => [b.title, b.lines.join("\n").trim()]));
|
|
17668
|
+
const trimBlankLines = (lines) => {
|
|
17669
|
+
let start = 0;
|
|
17670
|
+
let end = lines.length;
|
|
17671
|
+
while (start < end && isBlank(lines[start])) start++;
|
|
17672
|
+
while (end > start && isBlank(lines[end - 1])) end--;
|
|
17673
|
+
return lines.slice(start, end);
|
|
17674
|
+
};
|
|
17675
|
+
/**
|
|
17676
|
+
* \`review\` with the \`ranked\` chunks first, in their order, each led by its
|
|
17677
|
+
* severity line; every other chunk follows in place.
|
|
17678
|
+
*/
|
|
17679
|
+
const rankedReview = (review, ranked) => {
|
|
17680
|
+
const { head, blocks } = chunkBlocks(review);
|
|
17681
|
+
const place = new Map(ranked.map((r, k) => [r.title, k]));
|
|
17682
|
+
const labels = new Map(ranked.map((r) => [r.title, r.label]));
|
|
17683
|
+
const order = (i) => place.get(blocks[i].title) ?? ranked.length;
|
|
17684
|
+
const chunk = (i) => {
|
|
17685
|
+
const { title, lines } = blocks[i];
|
|
17686
|
+
const label = labels.get(title);
|
|
17687
|
+
const heading = /^ {0,3}-+\s*$/.test(lines[1] ?? "") ? 2 : 1;
|
|
17688
|
+
const body = trimBlankLines(lines.slice(heading)).join("\n");
|
|
17689
|
+
return [
|
|
17690
|
+
lines.slice(0, heading).join("\n"),
|
|
17691
|
+
...label === void 0 ? [] : [severityLine(label)],
|
|
17692
|
+
...body === "" ? [] : [body]
|
|
17693
|
+
].join("\n\n");
|
|
17694
|
+
};
|
|
17695
|
+
const sorted = blocks.map((_, i) => i).sort((x, y) => order(x) - order(y) || x - y);
|
|
17696
|
+
return `${[head.join("\n").trim(), ...sorted.map(chunk)].filter((p) => p !== "").join("\n\n")}\n`;
|
|
17697
|
+
};
|
|
17612
17698
|
/**
|
|
17613
17699
|
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
17614
17700
|
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
@@ -17663,8 +17749,11 @@ const acceptanceCriteriaBlock = `
|
|
|
17663
17749
|
Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
|
|
17664
17750
|
\`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
|
|
17665
17751
|
per criterion, numbered from \`AC-1\`. They bind every later step —
|
|
17666
|
-
the build is done when each one holds — so each must be
|
|
17667
|
-
test
|
|
17752
|
+
the build is done when each one holds — so each must be behaviour a
|
|
17753
|
+
test can pin: one that fails when it does not hold. A green suite, a
|
|
17754
|
+
passing typecheck or lint, a generator or build that runs — the health
|
|
17755
|
+
check enforces those on every lap, so none is a criterion; fold what it
|
|
17756
|
+
must produce into a behaviour a test can observe. A later lap keeps every ID:
|
|
17668
17757
|
reword a criterion in place, append a new one with the next unused
|
|
17669
17758
|
number, delete a dropped one — never renumber, never reuse an ID. When
|
|
17670
17759
|
the file holds no list (a review loop-back rewrites it), recover the
|
|
@@ -17772,7 +17861,7 @@ const clearCriteriaQuestion = {
|
|
|
17772
17861
|
id: "clear",
|
|
17773
17862
|
primitive: "noul",
|
|
17774
17863
|
instructions: "state.criteria lists the acceptance criteria the human's sketch (state.sketch) stated, as gtd numbered them. Decide whether they can stand as the binding definition of done, with no design conversation.",
|
|
17775
|
-
criteria: "yes: every criterion is clear
|
|
17864
|
+
criteria: "yes: every criterion is clear behaviour a test can pin — a test would fail if it did not hold — and together they say what to build. no: any one criterion is vague, subjective or untestable, only a command can check it (a suite, typecheck, lint, build or generator run that passes), or the sketch leaves an open product question. Unsure means no."
|
|
17776
17865
|
};
|
|
17777
17866
|
const clearCriteriaMessage = () => `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
|
|
17778
17867
|
pipe \`yes\` when every one is clear and checkable, so design is skipped and
|
|
@@ -18163,6 +18252,13 @@ const wrapItem = (item) => {
|
|
|
18163
18252
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
18164
18253
|
return [...out, line];
|
|
18165
18254
|
};
|
|
18255
|
+
const reviewRankingMessage = () => `Ranking each chunk of the review before the human reads it. Run
|
|
18256
|
+
\`gtd judge answer\` and pipe, per chunk \`C<n>\`: \`C<n>.security\`
|
|
18257
|
+
(yes/no), \`C<n>.reversibility\` (one-way/two-way),
|
|
18258
|
+
\`C<n>.blastRadius\` (local/module/contract) and \`C<n>.newConcept\`
|
|
18259
|
+
(yes/no) — or land without a verdict, which lists every chunk as
|
|
18260
|
+
critical.
|
|
18261
|
+
`;
|
|
18166
18262
|
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
18167
18263
|
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
18168
18264
|
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
@@ -18684,6 +18780,80 @@ Two ways forward (then run \`gtd land\`):
|
|
|
18684
18780
|
- **Overrule** — land a clean tree to finish without them. The slice records
|
|
18685
18781
|
the IDs it covers and, as \`uncovered\`, the ones it does not
|
|
18686
18782
|
`;
|
|
18783
|
+
const testerSystem = () => `${testerPersona}
|
|
18784
|
+
|
|
18785
|
+
${agentConduct}`;
|
|
18786
|
+
const PROTOTYPE_FORMAT = `- Write \`.gtd/PROTOTYPE.md\`, replacing any earlier report: one \`## \` chunk
|
|
18787
|
+
per criterion, headed \`## AC-<n>: pass\`, \`## AC-<n>: fail\` or
|
|
18788
|
+
\`## AC-<n>: blocked\`, then one chunk per problem you found outside any
|
|
18789
|
+
criterion, headed \`## Problem <short title>: fail\` (\`: pass\` once a
|
|
18790
|
+
re-test shows it fixed). Under each: the exact steps you ran, what you
|
|
18791
|
+
observed (output, a screenshot path, the error), and for a failure what
|
|
18792
|
+
you expected instead
|
|
18793
|
+
- \`blocked\` is for a criterion you could not exercise, or one whose
|
|
18794
|
+
expected behaviour itself looks wrong — say which and why: a person
|
|
18795
|
+
decides it, never a fixer`;
|
|
18796
|
+
const prototypeTestingPrompt = (slice, retest) => `${stateFileRules}
|
|
18797
|
+
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18798
|
+
${sliceLine(slice)}- The change is built and its suite is green. Now test it by hand: use
|
|
18799
|
+
every acceptance criterion the way a person would and watch what
|
|
18800
|
+
happens. Running the test suite is not this — it already ran
|
|
18801
|
+
- Find how this project is exercised for real, in this order: a project
|
|
18802
|
+
skill that launches or drives it (e.g. \`.claude/skills/*\`); a manual
|
|
18803
|
+
testing or running section in \`AGENTS.md\`, \`CLAUDE.md\` or \`README.md\`;
|
|
18804
|
+
failing those, the project's shape — a browser or Playwright for a web
|
|
18805
|
+
project, the built CLI in a scratch directory for a CLI, a request client
|
|
18806
|
+
for an API
|
|
18807
|
+
- Found none and recognised no shape: write only a chunk headed
|
|
18808
|
+
\`## No testing instructions\` saying what you looked for, and finish —
|
|
18809
|
+
a person is asked; never guess
|
|
18810
|
+
- Test in a disposable place only — a scratch directory outside this
|
|
18811
|
+
repository, a throwaway repository, a dev server on a free port — never
|
|
18812
|
+
against the user's real data or this working tree. Point every tool's
|
|
18813
|
+
output (screenshots, traces, test results) outside the repository too
|
|
18814
|
+
${retest ? `- This is a re-test: \`.gtd/PROTOTYPE.md\` holds the last round's report and
|
|
18815
|
+
a fix turn or a person has changed the code since. Re-test every failed or blocked criterion and
|
|
18816
|
+
every problem it lists in full, then re-run the passed ones quickly
|
|
18817
|
+
` : ""}${PROTOTYPE_FORMAT}
|
|
18818
|
+
- Change nothing but \`.gtd/PROTOTYPE.md\` — a turn that does is refused
|
|
18819
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
18820
|
+
`;
|
|
18821
|
+
const prototypeFixingPrompt = (slice) => `${stateFileRules}
|
|
18822
|
+
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18823
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
18824
|
+
- Read \`.gtd/PROTOTYPE.md\`: a hands-on test of the change. Fix every
|
|
18825
|
+
\`fail\` chunk of this change's criteria${slice === void 0 ? "" : " (this slice's only)"} or a problem found
|
|
18826
|
+
beside them, at its cause, with a test that shows it; leave \`blocked\`
|
|
18827
|
+
ones to the person
|
|
18828
|
+
- A \`fail\` the code does not explain (the tester misread what it saw) has
|
|
18829
|
+
nothing to fix: leave it and change nothing for it — the next round re-tests
|
|
18830
|
+
- Leave \`.gtd/PROTOTYPE.md\` as it is: the next round re-tests from it
|
|
18831
|
+
- Run \`${env.testCommand}\` and finish only once it is green
|
|
18832
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
18833
|
+
`;
|
|
18834
|
+
const openItems = (items) => items.map((item) => `- ${item.subject}: ${item.verdict}`).join("\n");
|
|
18835
|
+
const prototypeOpenMessage = (items, rounds) => `The hands-on test of this slice still has open items${rounds > 0 ? ` after ${rounds} fix round${rounds === 1 ? "" : "s"}` : ", and none a fixer can take"}:
|
|
18836
|
+
${openItems(items)}
|
|
18837
|
+
|
|
18838
|
+
\`.gtd/PROTOTYPE.md\` holds the last report: the steps run and what was seen.
|
|
18839
|
+
|
|
18840
|
+
Two ways forward (then run \`gtd land\`):
|
|
18841
|
+
- **Fix** — change the code, or the testing instructions, yourself (the
|
|
18842
|
+
criteria are settled: an edit to them is refused); the suite runs and the
|
|
18843
|
+
slice is tested again, with a fresh round budget
|
|
18844
|
+
- **Overrule** — land a clean tree to go on to the quality lap with these
|
|
18845
|
+
items open
|
|
18846
|
+
`;
|
|
18847
|
+
const prototypeUnguidedMessage = () => `The hands-on test found no way to exercise this project for real — no
|
|
18848
|
+
project skill, no manual testing section in \`AGENTS.md\`, \`CLAUDE.md\` or
|
|
18849
|
+
\`README.md\`, and no project shape it recognised. \`.gtd/PROTOTYPE.md\` says
|
|
18850
|
+
what it looked for.
|
|
18851
|
+
|
|
18852
|
+
Two ways forward (then run \`gtd land\`):
|
|
18853
|
+
- **Instruct** — add testing instructions (a project skill, or a manual
|
|
18854
|
+
testing section); the slice is tested again
|
|
18855
|
+
- **Skip** — land a clean tree to go on to the quality lap untested
|
|
18856
|
+
`;
|
|
18687
18857
|
//#endregion
|
|
18688
18858
|
//#region src/workflows/shared/steps.ts
|
|
18689
18859
|
const FEEDBACK = ".gtd/FEEDBACK.md";
|
|
@@ -18695,6 +18865,7 @@ const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
|
18695
18865
|
const QUALITY = ".gtd/QUALITY.md";
|
|
18696
18866
|
const SLICES = ".gtd/SLICES.md";
|
|
18697
18867
|
const HANDOFF = ".gtd/HANDOFF.md";
|
|
18868
|
+
const PROTOTYPE = ".gtd/PROTOTYPE.md";
|
|
18698
18869
|
const planner = () => env.plannerModel ?? "";
|
|
18699
18870
|
const coder = () => env.coderModel ?? "";
|
|
18700
18871
|
const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
|
|
@@ -38412,6 +38583,7 @@ const ShippedOptions = Struct({
|
|
|
38412
38583
|
qualityReviews: setting$1,
|
|
38413
38584
|
refactorQualityReviews: setting$1,
|
|
38414
38585
|
continuations: setting$1,
|
|
38586
|
+
prototypeRounds: setting$1,
|
|
38415
38587
|
continuationStall: setting$1,
|
|
38416
38588
|
testCommand: setting$1,
|
|
38417
38589
|
plannerModel: setting$1,
|
|
@@ -39052,7 +39224,8 @@ const severityOf = (note, judgment, minP) => {
|
|
|
39052
39224
|
if (critical.length > 0) return {
|
|
39053
39225
|
tier: "critical",
|
|
39054
39226
|
reason: `critical: ${critical.join(", ")}`,
|
|
39055
|
-
rank: rank()
|
|
39227
|
+
rank: rank(),
|
|
39228
|
+
...a("security") === "yes" ? { security: true } : {}
|
|
39056
39229
|
};
|
|
39057
39230
|
const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
|
|
39058
39231
|
if (should.length > 0) return {
|
|
@@ -39066,29 +39239,30 @@ const severityOf = (note, judgment, minP) => {
|
|
|
39066
39239
|
rank: 2
|
|
39067
39240
|
};
|
|
39068
39241
|
};
|
|
39069
|
-
|
|
39242
|
+
/** `asks` names the change in full, `change` refers back to it. */
|
|
39243
|
+
const severityQuestions = (id, asks, change) => [
|
|
39070
39244
|
{
|
|
39071
|
-
id: `${
|
|
39245
|
+
id: `${id}.security`,
|
|
39072
39246
|
primitive: "noul",
|
|
39073
|
-
instructions: `Does
|
|
39247
|
+
instructions: `Does ${asks} touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
|
|
39074
39248
|
criteria: "yes: it changes or could weaken a security property. no: it does not."
|
|
39075
39249
|
},
|
|
39076
39250
|
{
|
|
39077
|
-
id: `${
|
|
39251
|
+
id: `${id}.reversibility`,
|
|
39078
39252
|
primitive: "choice",
|
|
39079
|
-
instructions: `Once
|
|
39253
|
+
instructions: `Once ${change} ships, can it be undone without breaking anyone who already depends on it?`,
|
|
39080
39254
|
criteria: "one-way: a published API, file format, persisted data, protocol or other contract that cannot be taken back. two-way: can be reverted freely."
|
|
39081
39255
|
},
|
|
39082
39256
|
{
|
|
39083
|
-
id: `${
|
|
39257
|
+
id: `${id}.blastRadius`,
|
|
39084
39258
|
primitive: "choice",
|
|
39085
|
-
instructions: `How far does
|
|
39259
|
+
instructions: `How far does ${change} reach?`,
|
|
39086
39260
|
criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
|
|
39087
39261
|
},
|
|
39088
39262
|
{
|
|
39089
|
-
id: `${
|
|
39263
|
+
id: `${id}.newConcept`,
|
|
39090
39264
|
primitive: "noul",
|
|
39091
|
-
instructions: `Does
|
|
39265
|
+
instructions: `Does ${change} introduce a concept new to this codebase — a new abstraction, entity or term? Judge from the evidence, never a glossary; reusing or extending an existing concept is not new.`,
|
|
39092
39266
|
criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
|
|
39093
39267
|
}
|
|
39094
39268
|
];
|
|
@@ -39105,7 +39279,7 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
39105
39279
|
answers: {},
|
|
39106
39280
|
truncated: []
|
|
39107
39281
|
} : await judge("review.triage", {
|
|
39108
|
-
questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n)]),
|
|
39282
|
+
questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n.id, `the change the human asks for in note "${n.id}" (on "${n.anchor}")`, `the change in note "${n.id}"`)]),
|
|
39109
39283
|
evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
|
|
39110
39284
|
message: buildReviewTriageMessage(),
|
|
39111
39285
|
label: "Judging each note"
|
|
@@ -39121,6 +39295,67 @@ const triage$1 = (severities) => async (notes) => {
|
|
|
39121
39295
|
return [n.id, "edit"];
|
|
39122
39296
|
}));
|
|
39123
39297
|
};
|
|
39298
|
+
/** Review order: security, one-way doors, the judge unsure, then should, then could. */
|
|
39299
|
+
const chunkKey = (s) => s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2;
|
|
39300
|
+
/** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
|
|
39301
|
+
const chunkEvidence = (text, files) => [text, ...files.map(({ path, line, rangeEnd }) => {
|
|
39302
|
+
const where = `${path}${line === void 0 ? "" : `#${line}${rangeEnd === void 0 ? "" : `-${rangeEnd}`}`}`;
|
|
39303
|
+
const lines = read(path.replace(/^\.\//, ""))?.split("\n");
|
|
39304
|
+
if (lines === void 0) return `${where}: no longer exists`;
|
|
39305
|
+
return `${where}:\n${(line === void 0 ? lines : lines.slice(line - 1, rangeEnd ?? line)).join("\n")}`;
|
|
39306
|
+
})].join("\n\n");
|
|
39307
|
+
const bytes = (text) => new TextEncoder().encode(text).length;
|
|
39308
|
+
/** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
|
|
39309
|
+
const batched = (items, budget) => {
|
|
39310
|
+
const batches = [];
|
|
39311
|
+
let batch = [];
|
|
39312
|
+
let widest = 0;
|
|
39313
|
+
for (const item of items) {
|
|
39314
|
+
const size = bytes(item.evidence);
|
|
39315
|
+
if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
|
|
39316
|
+
batches.push(batch);
|
|
39317
|
+
batch = [];
|
|
39318
|
+
widest = 0;
|
|
39319
|
+
}
|
|
39320
|
+
batch.push(item);
|
|
39321
|
+
widest = Math.max(widest, size);
|
|
39322
|
+
}
|
|
39323
|
+
return batch.length === 0 ? batches : [...batches, batch];
|
|
39324
|
+
};
|
|
39325
|
+
/**
|
|
39326
|
+
* Rates every chunk the reviewer wrote — a walkthrough chunk or a carried
|
|
39327
|
+
* finding — and sorts \`full\` by it, each chunk led by its tier and reason.
|
|
39328
|
+
* gtd's summary chunks are not rated and stay last.
|
|
39329
|
+
*/
|
|
39330
|
+
const ranked = async (review, full) => {
|
|
39331
|
+
const texts = chunkTexts(full);
|
|
39332
|
+
const chunks = reviewChunks(review).map((c, i) => ({
|
|
39333
|
+
id: `C${i + 1}`,
|
|
39334
|
+
title: c.title,
|
|
39335
|
+
evidence: chunkEvidence(texts.get(c.title) ?? "", c.files)
|
|
39336
|
+
}));
|
|
39337
|
+
if (chunks.length === 0) return full;
|
|
39338
|
+
const minP = numeric(vars.reviewSeverityMinP, Infinity);
|
|
39339
|
+
const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768));
|
|
39340
|
+
const severities = /* @__PURE__ */ new Map();
|
|
39341
|
+
for (const [i, batch] of batches.entries()) {
|
|
39342
|
+
const judgment = await judge("review.ranking", {
|
|
39343
|
+
questions: batch.flatMap((c) => severityQuestions(c.id, `the change review chunk "${c.id}" ("${c.title}") walks through`, `the change in review chunk "${c.id}"`)),
|
|
39344
|
+
evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
|
|
39345
|
+
message: reviewRankingMessage(),
|
|
39346
|
+
label: batches.length === 1 ? "Ranking the review" : `Ranking the review (${i + 1}/${batches.length})`
|
|
39347
|
+
});
|
|
39348
|
+
for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP));
|
|
39349
|
+
}
|
|
39350
|
+
return rankedReview(full, chunks.map((c, i) => ({
|
|
39351
|
+
c,
|
|
39352
|
+
i,
|
|
39353
|
+
s: severities.get(c.id)
|
|
39354
|
+
})).sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i).map(({ c, s }) => ({
|
|
39355
|
+
title: c.title,
|
|
39356
|
+
label: s.reason
|
|
39357
|
+
})));
|
|
39358
|
+
};
|
|
39124
39359
|
const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
39125
39360
|
const composedOf = (base, report, nitFix, answered) => ({
|
|
39126
39361
|
...report,
|
|
@@ -39172,12 +39407,13 @@ const requirePointers = (review, notes, codeChanged) => {
|
|
|
39172
39407
|
const walkthrough = async (base, report, lap, carry) => {
|
|
39173
39408
|
const notes = titledNotes(report);
|
|
39174
39409
|
await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
|
|
39175
|
-
const
|
|
39176
|
-
if (
|
|
39177
|
-
|
|
39178
|
-
|
|
39179
|
-
|
|
39180
|
-
|
|
39410
|
+
const written = read(REVIEW);
|
|
39411
|
+
if (written === void 0) return;
|
|
39412
|
+
const review = unranked(written);
|
|
39413
|
+
requirePointers(written, notes, changesSince(base).some((c) => isCode$1(c.path)));
|
|
39414
|
+
const full = await ranked(review, withWalkthrough(review, report));
|
|
39415
|
+
if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
|
|
39416
|
+
label: "Ordering the review and adding the quality lap's findings",
|
|
39181
39417
|
file: REVIEW,
|
|
39182
39418
|
base
|
|
39183
39419
|
});
|
|
@@ -39597,7 +39833,9 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
39597
39833
|
feedback,
|
|
39598
39834
|
fixAccess: strayAccess(p.strays),
|
|
39599
39835
|
lensBase: entry,
|
|
39600
|
-
rules: refactorRules(p.strays, p.pattern)
|
|
39836
|
+
rules: refactorRules(p.strays, p.pattern),
|
|
39837
|
+
walkthrough: true,
|
|
39838
|
+
lap: "refactor"
|
|
39601
39839
|
});
|
|
39602
39840
|
if (outcome.verdict === "signoff") return true;
|
|
39603
39841
|
await reUnwind(outcome);
|
|
@@ -39612,7 +39850,7 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
39612
39850
|
const COVERED_MIN_P = .7;
|
|
39613
39851
|
/** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
|
|
39614
39852
|
const COVERING_RETRIES = 2;
|
|
39615
|
-
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
39853
|
+
const frozen$1 = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
39616
39854
|
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
39617
39855
|
label: "Fixing the check",
|
|
39618
39856
|
file: FEEDBACK,
|
|
@@ -39620,7 +39858,7 @@ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", imple
|
|
|
39620
39858
|
system: builderSystem()
|
|
39621
39859
|
}), {
|
|
39622
39860
|
guard: () => {
|
|
39623
|
-
frozen();
|
|
39861
|
+
frozen$1();
|
|
39624
39862
|
guard?.();
|
|
39625
39863
|
},
|
|
39626
39864
|
guardRests: false
|
|
@@ -39673,7 +39911,7 @@ const requireCoverage = async (base = start(), slice) => {
|
|
|
39673
39911
|
system: builderSystem(),
|
|
39674
39912
|
allowEmpty: true
|
|
39675
39913
|
});
|
|
39676
|
-
frozen();
|
|
39914
|
+
frozen$1();
|
|
39677
39915
|
} else {
|
|
39678
39916
|
await human("uncovered", {
|
|
39679
39917
|
message: uncoveredMessage(uncovered),
|
|
@@ -39682,13 +39920,136 @@ const requireCoverage = async (base = start(), slice) => {
|
|
|
39682
39920
|
acceptClean: true
|
|
39683
39921
|
});
|
|
39684
39922
|
if (!changes().some((c) => isCode$1(c.path))) return record(covered, uncovered);
|
|
39685
|
-
frozen();
|
|
39923
|
+
frozen$1();
|
|
39686
39924
|
retries = 0;
|
|
39687
39925
|
}
|
|
39688
39926
|
await green();
|
|
39689
39927
|
}
|
|
39690
39928
|
};
|
|
39691
39929
|
//#endregion
|
|
39930
|
+
//#region src/workflows/feature/prototype.ts
|
|
39931
|
+
/** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
|
|
39932
|
+
const prototypeRounds = () => {
|
|
39933
|
+
const value = vars.prototypeRounds ?? defaults.prototypeRounds;
|
|
39934
|
+
if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
39935
|
+
return Number(value);
|
|
39936
|
+
};
|
|
39937
|
+
const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
|
|
39938
|
+
const UNGUIDED = /^no testing instructions$/i;
|
|
39939
|
+
/** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
|
|
39940
|
+
const reportOf = (text) => {
|
|
39941
|
+
let unguided = false;
|
|
39942
|
+
const items = [];
|
|
39943
|
+
const malformed = [];
|
|
39944
|
+
for (const { line, fenced } of markFenced(text)) {
|
|
39945
|
+
const heading = fenced ? null : HEADING.exec(line);
|
|
39946
|
+
if (heading?.[1] !== "##") continue;
|
|
39947
|
+
if (UNGUIDED.test(heading[2])) unguided = true;
|
|
39948
|
+
const chunk = CHUNK.exec(heading[2]);
|
|
39949
|
+
if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
|
|
39950
|
+
if (chunk !== null) items.push({
|
|
39951
|
+
subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
|
|
39952
|
+
verdict: chunk[2].toLowerCase()
|
|
39953
|
+
});
|
|
39954
|
+
}
|
|
39955
|
+
return {
|
|
39956
|
+
unguided,
|
|
39957
|
+
items,
|
|
39958
|
+
malformed
|
|
39959
|
+
};
|
|
39960
|
+
};
|
|
39961
|
+
/**
|
|
39962
|
+
* The report the testing turn left, refused unless it is the only change and
|
|
39963
|
+
* names every criterion once, with its open items: another slice's criteria
|
|
39964
|
+
* are not this slice's to fix.
|
|
39965
|
+
*/
|
|
39966
|
+
const requireReport = (slice, retest) => {
|
|
39967
|
+
const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
|
|
39968
|
+
if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
|
|
39969
|
+
const text = read(PROTOTYPE);
|
|
39970
|
+
if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
|
|
39971
|
+
const report = reportOf(text);
|
|
39972
|
+
if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
|
|
39973
|
+
if (report.unguided && report.items.length === 0 && !retest) return {
|
|
39974
|
+
unguided: true,
|
|
39975
|
+
open: []
|
|
39976
|
+
};
|
|
39977
|
+
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
39978
|
+
const ids = criterionIds(requirements, slice);
|
|
39979
|
+
const elsewhere = criterionIds(requirements).filter((id) => !ids.includes(id));
|
|
39980
|
+
const count = (id) => report.items.filter((i) => i.subject === id).length;
|
|
39981
|
+
const wrong = ids.filter((id) => count(id) !== 1);
|
|
39982
|
+
if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
|
|
39983
|
+
return {
|
|
39984
|
+
unguided: false,
|
|
39985
|
+
open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
|
|
39986
|
+
};
|
|
39987
|
+
};
|
|
39988
|
+
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
39989
|
+
const testing = async (slice, retest) => {
|
|
39990
|
+
await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), retest), {
|
|
39991
|
+
label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
|
|
39992
|
+
file: PROTOTYPE,
|
|
39993
|
+
model: coder(),
|
|
39994
|
+
system: testerSystem(),
|
|
39995
|
+
allowEmpty: true
|
|
39996
|
+
});
|
|
39997
|
+
return requireReport(slice, retest);
|
|
39998
|
+
};
|
|
39999
|
+
const fixing = async (slice, guard) => {
|
|
40000
|
+
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
|
|
40001
|
+
label: "Fixing what the hands-on test found",
|
|
40002
|
+
file: PROTOTYPE,
|
|
40003
|
+
model: coder(),
|
|
40004
|
+
system: builderSystem(),
|
|
40005
|
+
allowEmpty: true
|
|
40006
|
+
}));
|
|
40007
|
+
if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
|
|
40008
|
+
frozen();
|
|
40009
|
+
guard?.();
|
|
40010
|
+
};
|
|
40011
|
+
/** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
|
|
40012
|
+
const askPerson = async (name, message, label) => {
|
|
40013
|
+
await human(name, {
|
|
40014
|
+
message,
|
|
40015
|
+
label,
|
|
40016
|
+
file: PROTOTYPE,
|
|
40017
|
+
acceptClean: true
|
|
40018
|
+
});
|
|
40019
|
+
if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
|
|
40020
|
+
frozen();
|
|
40021
|
+
return true;
|
|
40022
|
+
};
|
|
40023
|
+
/**
|
|
40024
|
+
* Use the slice by hand: a testing turn reports on every criterion, a fix turn
|
|
40025
|
+
* takes each failure, and the slice is tested again — until the report is
|
|
40026
|
+
* clean, or `prototypeRounds` fixes are spent and a person decides. `guard`
|
|
40027
|
+
* holds every fix turn, as it holds the implementation's.
|
|
40028
|
+
*/
|
|
40029
|
+
const prototypeTest = async (slice, guard) => {
|
|
40030
|
+
const cap = prototypeRounds();
|
|
40031
|
+
if (cap === 0) return;
|
|
40032
|
+
await scope("prototype", async () => {
|
|
40033
|
+
let fixes = 0;
|
|
40034
|
+
let retest = false;
|
|
40035
|
+
for (;;) {
|
|
40036
|
+
const { unguided, open } = await testing(slice, retest);
|
|
40037
|
+
retest = !unguided;
|
|
40038
|
+
if (unguided) {
|
|
40039
|
+
if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
|
|
40040
|
+
} else if (open.length === 0) return;
|
|
40041
|
+
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
40042
|
+
fixes++;
|
|
40043
|
+
await fixing(slice, guard);
|
|
40044
|
+
} else {
|
|
40045
|
+
if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
|
|
40046
|
+
fixes = 0;
|
|
40047
|
+
}
|
|
40048
|
+
await green(guard);
|
|
40049
|
+
}
|
|
40050
|
+
});
|
|
40051
|
+
};
|
|
40052
|
+
//#endregion
|
|
39692
40053
|
//#region src/workflows/feature/seam.ts
|
|
39693
40054
|
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
39694
40055
|
const NAIVE_RETRIES = 2;
|
|
@@ -39895,8 +40256,10 @@ const implementing = (options) => scope("implement", async () => {
|
|
|
39895
40256
|
...acceptance,
|
|
39896
40257
|
ids: settled(acceptance.ids, options.slice)
|
|
39897
40258
|
}, since);
|
|
39898
|
-
|
|
40259
|
+
const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
|
|
40260
|
+
await green(keepsTests);
|
|
39899
40261
|
await requireCoverage(options.base, options.slice);
|
|
40262
|
+
await prototypeTest(options.slice, keepsTests);
|
|
39900
40263
|
const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
39901
40264
|
await closing(moreToBuild(options.more));
|
|
39902
40265
|
return ids;
|
|
@@ -39905,11 +40268,16 @@ const moreToBuild = (more) => typeof more === "function" ? more() : more === tru
|
|
|
39905
40268
|
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
39906
40269
|
const closeOut = () => scope("implement", () => closing(false));
|
|
39907
40270
|
/** A slice with `more` after it keeps the criteria and the slicing for the next one. */
|
|
39908
|
-
const closing = (more) => run("closing", removeScript(more ? [
|
|
40271
|
+
const closing = (more) => run("closing", removeScript(more ? [
|
|
40272
|
+
REFACTOR_NOTE,
|
|
40273
|
+
HANDOFF,
|
|
40274
|
+
PROTOTYPE
|
|
40275
|
+
] : [
|
|
39909
40276
|
REQUIREMENTS,
|
|
39910
40277
|
REFACTOR_NOTE,
|
|
39911
40278
|
SLICES,
|
|
39912
|
-
HANDOFF
|
|
40279
|
+
HANDOFF,
|
|
40280
|
+
PROTOTYPE
|
|
39913
40281
|
]), { label: "Closing out the implementation" });
|
|
39914
40282
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
39915
40283
|
const steer = async (retry) => {
|
|
@@ -40274,6 +40642,7 @@ const ordinaryStart = async () => {
|
|
|
40274
40642
|
refusePendingHandOff();
|
|
40275
40643
|
flagModeOn();
|
|
40276
40644
|
sliceCap();
|
|
40645
|
+
prototypeRounds();
|
|
40277
40646
|
const stated = statedCriteria(changes(), head$1());
|
|
40278
40647
|
await unwind();
|
|
40279
40648
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
@@ -40308,6 +40677,10 @@ const skills = (vars) => {
|
|
|
40308
40677
|
seam: ["incremental-implementation"],
|
|
40309
40678
|
implement: ["test-driven-development", "incremental-implementation"],
|
|
40310
40679
|
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
40680
|
+
"implement.prototype": [],
|
|
40681
|
+
"implement.prototype.fix": ["test-driven-development", "incremental-implementation"],
|
|
40682
|
+
"implement.prototype.fix.suite": ["debugging-and-error-recovery"],
|
|
40683
|
+
"implement.prototype.health": ["debugging-and-error-recovery"],
|
|
40311
40684
|
"implement.health": ["debugging-and-error-recovery"],
|
|
40312
40685
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens]),
|
|
40313
40686
|
...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
|
|
@@ -40485,4 +40858,4 @@ var workflows_default = feature;
|
|
|
40485
40858
|
*/
|
|
40486
40859
|
const doors = {};
|
|
40487
40860
|
//#endregion
|
|
40488
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
|
|
40861
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, PROTOTYPE, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
|