@pmelab/gtd 30.1.0 → 32.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 +29 -23
- package/claude/hooks/ship.ts +7 -3
- package/dist/chunks/{reserved-DDiiAZ8U.mjs → reserved-CHTJTzpA.mjs} +52 -3
- package/dist/chunks/{tail-ILPlELuy.mjs → tail-CCsDBbZe.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +137 -8
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +188 -10
- package/package.json +1 -1
- package/src/workflows/feature/acceptance-criteria-tier.feature +25 -0
- package/src/workflows/feature/acceptance-criteria.feature +189 -0
- package/src/workflows/feature/criteria.test.ts +106 -0
- package/src/workflows/feature/criteria.ts +168 -0
- package/src/workflows/feature/fast-path.feature +4 -0
- package/src/workflows/feature/feature.test.ts +41 -11
- package/src/workflows/feature/feature.ts +7 -4
- package/src/workflows/feature/flag-mode.feature +8 -0
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/planning.ts +2 -0
- package/src/workflows/feature/refactor-absorbs.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +4 -0
- package/src/workflows/feature/refactor-review.feature +24 -0
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +8 -0
- package/src/workflows/feature/seam.feature +8 -0
- package/src/workflows/feature/seam.ts +12 -7
- package/src/workflows/feature/tier-pin.feature +4 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/text.ts +77 -2
package/README.md
CHANGED
|
@@ -339,28 +339,29 @@ asks, and moves the process on only when you say so; a review you leave
|
|
|
339
339
|
untouched signs off. While a step runs you can keep asking; requests for changes
|
|
340
340
|
wait for the next rest. When the process finishes, Claude offers to squash it
|
|
341
341
|
and open its pull request(s) (or run `/gtd ship`; `GTD_SHIP_MODE` picks one
|
|
342
|
-
|
|
343
|
-
[driver doc](docs/driver.md)); a process
|
|
344
|
-
`Closes #N` on its top pull request; a
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
a slice
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
commit per slice under one pull request (the default) or, set to `stack`, one
|
|
343
|
+
stacked pull request per slice, see the [driver doc](docs/driver.md)); a process
|
|
344
|
+
started for an issue ships with `Closes #N` on its top pull request; a closed
|
|
345
|
+
pull request whose branch was deleted from origin is left behind and a new one
|
|
346
|
+
opens; on a branch an earlier ship in this clone stacked (unless
|
|
347
|
+
`GTD_SHIP_MODE=commits`), a later process, such as review fixes, lands one
|
|
348
|
+
commit per slice on the top pull request and keeps the stack, and re-running
|
|
349
|
+
`/gtd ship` after a lower stacked pull request merged restacks the rest (it
|
|
350
|
+
refuses, with nothing changed, when that pull request merged anywhere but the
|
|
351
|
+
default branch (unless into a lower slice branch before that one merged into
|
|
352
|
+
it), a slice branch is checked out in another worktree, an unrelated local
|
|
353
|
+
branch sits inside the rebased commits, or a slice's change is already upstream;
|
|
354
|
+
a failed rebase is rolled back; to tear a stack down from its worktree, delete
|
|
355
|
+
the top branch with `git push origin --delete`, since
|
|
356
|
+
`gh pr close --delete-branch` cannot). A process can change hands at any gate:
|
|
357
|
+
ask Claude to hand it off (or run `/gtd throw @dev`) and it opens a draft pull
|
|
358
|
+
request assigned to them, and `/gtd catch <pr>` picks it up exactly where it
|
|
359
|
+
waits, so whoever wrote the requirements can hand the build to someone else.
|
|
360
|
+
Ship, hand-off and throw answer at once and run in the background, with progress
|
|
361
|
+
on the status line; they report by waking Claude, a toast, a transcript line and
|
|
362
|
+
a push notification, and a failed push or `gh` call reports as a failure. While
|
|
363
|
+
a ship or hand-off runs or is queued, `/gtd catch`, the doors,
|
|
364
|
+
`/gtd <requirements>` and bare `/gtd` wait for its report. See
|
|
364
365
|
[Inside Claude Code](https://github.com/pmelab/gtd/blob/main/docs/driver.md#inside-claude-code-the-gtd-mod).
|
|
365
366
|
|
|
366
367
|
### Then let an agent build your own
|
|
@@ -418,7 +419,12 @@ noted in steps 3 and 4 below.
|
|
|
418
419
|
with `- A: <reply>` and the process stops at the same gate again. Close a
|
|
419
420
|
thread by replying with a conclusion or deleting it. While a thread's last
|
|
420
421
|
entry is the agent's, moving on is refused. Leave the file untouched and
|
|
421
|
-
start the loop to accept the plan as-is, unanswered questions and all.
|
|
422
|
+
start the loop to accept the plan as-is, unanswered questions and all. The
|
|
423
|
+
file lists numbered acceptance criteria (`AC-1`, `AC-2`, …) that bind the
|
|
424
|
+
build. Sketch your own under an `Acceptance criteria` heading and, when a
|
|
425
|
+
judge finds every one clear and checkable, they are recorded as they are and
|
|
426
|
+
this moment is skipped; one vague criterion brings it back. See
|
|
427
|
+
[Acceptance criteria](docs/configuration.md#acceptance-criteria).
|
|
422
428
|
|
|
423
429
|
3. **You wait.** The agent first implements the requirements the quickest way
|
|
424
430
|
that works — the naive pass — and a judge reads where it changed code (see
|
package/claude/hooks/ship.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function costTrailers(log: string) {
|
|
|
49
49
|
export type ShipMode = "stack" | "commits"
|
|
50
50
|
|
|
51
51
|
export function shipMode(raw: string | undefined): ShipMode | { refusal: string } {
|
|
52
|
-
if (!raw) return "
|
|
52
|
+
if (!raw) return "commits"
|
|
53
53
|
if (raw === "stack" || raw === "commits") return raw
|
|
54
54
|
return { refusal: `GTD_SHIP_MODE must be 'stack' or 'commits', not '${raw}'.` }
|
|
55
55
|
}
|
|
@@ -209,8 +209,8 @@ async function preflight(git: Git, ok: Ok) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export async function ship(io: ShipIo, isDry: boolean, rawMode?: string): Promise<Shipped> {
|
|
212
|
-
const
|
|
213
|
-
if (typeof
|
|
212
|
+
const valid = shipMode(rawMode)
|
|
213
|
+
if (typeof valid === "object") return fail(valid.refusal)
|
|
214
214
|
const git = async (...args: string[]) => (await io.run(["git", ...args])).out.trim()
|
|
215
215
|
const ok = async (...args: string[]) => (await io.run(["git", ...args])).code === 0
|
|
216
216
|
|
|
@@ -223,6 +223,10 @@ export async function ship(io: ShipIo, isDry: boolean, rawMode?: string): Promis
|
|
|
223
223
|
const live = names(await git("config", "--local", "--get", STACK_KEY(branch)))
|
|
224
224
|
const retired = names(await git("config", "--local", "--get", RETIRED_KEY(branch)))
|
|
225
225
|
const recorded = [...live, ...retired]
|
|
226
|
+
// Unset keeps a live stack stacking: commits mode would retarget its top PR
|
|
227
|
+
// and strand the lower ones. A dead record ships as commits.
|
|
228
|
+
const stacked = !rawMode && live.length > 0 && (await chainsToHead(git, ok, branch, live))
|
|
229
|
+
const mode: ShipMode = stacked ? "stack" : valid
|
|
226
230
|
const squashed = await squash(io, git, ok, branch, baseBranch, isDry, mode)
|
|
227
231
|
if (!squashed.ok) return squashed
|
|
228
232
|
// Until the PR step succeeds the record only moves aside: a retry must not
|
|
@@ -439,6 +439,21 @@ const codeThreadReplies = () => {
|
|
|
439
439
|
concluded thread in and delete its lines:
|
|
440
440
|
${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
|
|
441
441
|
};
|
|
442
|
+
const acceptanceCriteriaBlock = `
|
|
443
|
+
## Acceptance criteria
|
|
444
|
+
|
|
445
|
+
Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
|
|
446
|
+
\`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
|
|
447
|
+
per criterion, numbered from \`AC-1\`. They bind every later step —
|
|
448
|
+
the build is done when each one holds — so each must be checkable: a
|
|
449
|
+
test or a command shows it holds or not. A later lap keeps every ID:
|
|
450
|
+
reword a criterion in place, append a new one with the next unused
|
|
451
|
+
number, delete a dropped one — never renumber, never reuse an ID. When
|
|
452
|
+
the file holds no list (a review loop-back rewrites it), recover the
|
|
453
|
+
last settled one from history (\`git log -p -- .gtd/REQUIREMENTS.md\`)
|
|
454
|
+
and carry its IDs over. Landing is refused while a criterion has no ID or
|
|
455
|
+
two share one.
|
|
456
|
+
`;
|
|
442
457
|
const riskTierBlock = `
|
|
443
458
|
## Risk tier and production signals
|
|
444
459
|
|
|
@@ -482,7 +497,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
482
497
|
or both
|
|
483
498
|
- Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
|
|
484
499
|
uncommitted and finish once it reflects this lap's own work
|
|
485
|
-
|
|
500
|
+
${acceptanceCriteriaBlock}
|
|
486
501
|
## First lap
|
|
487
502
|
|
|
488
503
|
\`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
|
|
@@ -537,6 +552,34 @@ ${questionBarReturn}
|
|
|
537
552
|
made). If red, make that breakage the first concern, ahead of
|
|
538
553
|
everything else — the build assumes the suite started green
|
|
539
554
|
${riskTier ? riskTierBlock : ""}`;
|
|
555
|
+
const clearCriteriaQuestion = {
|
|
556
|
+
id: "clear",
|
|
557
|
+
primitive: "noul",
|
|
558
|
+
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.",
|
|
559
|
+
criteria: "yes: every criterion is clear and checkable — a test or a command can show it holds or not — and together they say what to build. no: any one criterion is vague, subjective or untestable, or the sketch leaves an open product question. Unsure means no."
|
|
560
|
+
};
|
|
561
|
+
const clearCriteriaMessage = () => `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
|
|
562
|
+
pipe \`yes\` when every one is clear and checkable, so design is skipped and
|
|
563
|
+
they are recorded as they are — or land without a verdict to run the
|
|
564
|
+
ordinary design conversation.
|
|
565
|
+
`;
|
|
566
|
+
const criterionLine = (c) => `- ${c.id}: ${c.text.replace(/\s+/g, " ")}`;
|
|
567
|
+
/** Writes \`.gtd/REQUIREMENTS.md\` from criteria the sketch stated, wrapped the way oxfmt writes it. */
|
|
568
|
+
const normaliseCriteriaScript = (stated) => {
|
|
569
|
+
return `mkdir -p .gtd
|
|
570
|
+
printf '%s\\n' ${[
|
|
571
|
+
"# Requirements",
|
|
572
|
+
"",
|
|
573
|
+
`Stated in \`${stated.path}\` by the sketch:`,
|
|
574
|
+
"",
|
|
575
|
+
`\`git show ${stated.commit}:${stated.path}\``,
|
|
576
|
+
"",
|
|
577
|
+
"## Acceptance Criteria",
|
|
578
|
+
"",
|
|
579
|
+
...stated.criteria.flatMap((c) => wrapItem(criterionLine(c)))
|
|
580
|
+
].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
|
|
581
|
+
`;
|
|
582
|
+
};
|
|
540
583
|
const designSystem = () => `${designPersona}
|
|
541
584
|
|
|
542
585
|
${agentConduct}`;
|
|
@@ -861,7 +904,8 @@ What each change does next (then run \`gtd land\`):
|
|
|
861
904
|
const wrapItem = (item) => {
|
|
862
905
|
const out = [];
|
|
863
906
|
let line = "";
|
|
864
|
-
|
|
907
|
+
const words = item.split(" ").reduce((acc, word) => acc.length > 0 && /^([-+*>#]|\d+[.)])$/.test(word) ? [...acc.slice(0, -1), `${acc.at(-1)} ${word}`] : [...acc, word], []);
|
|
908
|
+
for (const word of words) if (line !== "" && line.length + 1 + word.length > 80) {
|
|
865
909
|
out.push(line);
|
|
866
910
|
line = ` ${word}`;
|
|
867
911
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
@@ -1159,8 +1203,12 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
1159
1203
|
`;
|
|
1160
1204
|
};
|
|
1161
1205
|
const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
|
|
1206
|
+
const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
1207
|
+
the file, never from memory, and stop only when each one holds. They are
|
|
1208
|
+
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
|
|
1162
1209
|
const naivePassPrompt = (rejection) => `${stateFileRules}
|
|
1163
1210
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1211
|
+
${criteriaBinding(false)}
|
|
1164
1212
|
- Implement them the quickest way that works — the naive version. Do not
|
|
1165
1213
|
restructure, abstract or refactor around the change; a later step judges
|
|
1166
1214
|
the shape of your diff and a reviewer makes it fit
|
|
@@ -1175,6 +1223,7 @@ Start over and choose a different approach; do not rebuild the same diff.
|
|
|
1175
1223
|
`}`;
|
|
1176
1224
|
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
|
|
1177
1225
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1226
|
+
${criteriaBinding(true)}
|
|
1178
1227
|
${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
|
|
1179
1228
|
though never a release flag. Build only what is still missing, held to every
|
|
1180
1229
|
rule below; if nothing is, change nothing and finish your turn
|
|
@@ -1519,4 +1568,4 @@ const steering = {
|
|
|
1519
1568
|
[REVIEW]: "review"
|
|
1520
1569
|
};
|
|
1521
1570
|
//#endregion
|
|
1522
|
-
export {
|
|
1571
|
+
export { reviewEditNotesCapture as $, buildReviewCollectingPrompt as A, designTriagePrompt as B, planner as C, agentWithSkills as D, reviewing as E, clearCriteriaQuestion as F, implementBuildingPrompt as G, fixReviewNotesPrompt as H, criterionLine as I, normaliseCriteriaScript as J, implementFixPrompt as K, deferNotesScript as L, buildReviewTriageMessage as M, builderSystem as N, buildReviewAnswerQuestionsPrompt as O, clearCriteriaMessage as P, refactorRules as Q, designGateAnswerMessage as R, fixRisks as S, reviewQuality as T, handOffAwaitReviewMessage as U, finisherSystem as V, idleMessage as W, refactorDroppedRefusal as X, refactorBuildingPrompt as Y, refactorFixPrompt as Z, REVIEW as _, flagModeOn as a, seamSteerMessage as at, fixCheck as b, baseline as c, unwindFailure as ct, guarded as d, testFileGlobs as dt, reviewEditsCapture as et, healthy as f, flagMentions as ft, REQUIREMENTS as g, requireFlag as gt, REFACTOR as h, registryEntry as ht, skills as i, seamByHandMessage as it, buildReviewFixNitsPrompt as j, buildReviewAwaitAfterDeferMessage as k, escalation as l, defaults as lt, QUALITY as m, flipProblems as mt, summary as n, reviewNotesCapture as nt, flagShipped as o, startGateBlockedMessage as ot, FEEDBACK as p, flagReferences as pt, naivePassPrompt as q, access as r, reviewerSystem as rt, shipped as s, unwindFailedMessage as st, steering as t, reviewGateBlockedMessage as tt, gate as u, envDefaults as ut, awaitReview as v, reviewMissing as w, fixQuality as x, coder as y, designSystem as z };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { E as reviewing, S as fixRisks, T as reviewQuality, U as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, d as guarded, f as healthy, l as escalation, m as QUALITY, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-CHTJTzpA.mjs";
|
|
4
4
|
//#region src/workflows/shared/tail.ts
|
|
5
5
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
6
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { gt as requireFlag, ht as registryEntry, i as skills, lt as defaults, n as summary, o as flagShipped, pt as flagReferences, r as access, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CCsDBbZe.mjs";
|
|
4
4
|
//#region src/workflows/cleanup/cleanup.ts
|
|
5
5
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
6
6
|
const cleanupFlow = async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { answered, changes, changesSince, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
|
|
2
2
|
import { lensesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-
|
|
3
|
+
import { $ as reviewEditNotesCapture, A as buildReviewCollectingPrompt, B as designTriagePrompt, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, G as implementBuildingPrompt, I as criterionLine, J as normaliseCriteriaScript, K as implementFixPrompt, L as deferNotesScript, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as refactorRules, R as designGateAnswerMessage, S as fixRisks, W as idleMessage, X as refactorDroppedRefusal, Y as refactorBuildingPrompt, Z as refactorFixPrompt, _ as REVIEW, a as flagModeOn, at as seamSteerMessage, b as fixCheck, ct as unwindFailure, d as guarded, dt as testFileGlobs, et as reviewEditsCapture, f as healthy, g as REQUIREMENTS, h as REFACTOR, i as skills, it as seamByHandMessage, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as defaults, n as summary, nt as reviewNotesCapture, ot as startGateBlockedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as reviewerSystem, s as shipped, st as unwindFailedMessage, t as steering, u as gate, ut as envDefaults, v as awaitReview, w as reviewMissing, y as coder, z as designSystem } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-CCsDBbZe.mjs";
|
|
5
5
|
//#region src/workflows/shared/fences.ts
|
|
6
6
|
const markFenced = (text) => {
|
|
7
7
|
let fence;
|
|
@@ -17,6 +17,128 @@ const markFenced = (text) => {
|
|
|
17
17
|
});
|
|
18
18
|
};
|
|
19
19
|
//#endregion
|
|
20
|
+
//#region src/workflows/feature/criteria.ts
|
|
21
|
+
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
22
|
+
const CLEAR_MIN_P = .8;
|
|
23
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
24
|
+
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
25
|
+
const ID = /^(AC-\d+):\s*(.*)$/;
|
|
26
|
+
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
27
|
+
const listUnder = (text, title, level) => {
|
|
28
|
+
const lines = markFenced(text);
|
|
29
|
+
const at = lines.findIndex((l) => {
|
|
30
|
+
const heading = l.fenced ? null : HEADING.exec(l.line);
|
|
31
|
+
return heading !== null && (level === void 0 || heading[1].length === level) && title.test(heading[2]);
|
|
32
|
+
});
|
|
33
|
+
if (at === -1) return void 0;
|
|
34
|
+
const items = [];
|
|
35
|
+
let open = false;
|
|
36
|
+
for (const { line, fenced } of lines.slice(at + 1)) {
|
|
37
|
+
if (fenced) {
|
|
38
|
+
open = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (HEADING.test(line)) break;
|
|
42
|
+
const item = ITEM.exec(line);
|
|
43
|
+
if (item !== null) {
|
|
44
|
+
items.push(item[1].trim());
|
|
45
|
+
open = true;
|
|
46
|
+
} else if (open && line.trim() !== "") items[items.length - 1] = `${items.at(-1)} ${line.trim()}`.trim();
|
|
47
|
+
else open = false;
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
};
|
|
51
|
+
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
52
|
+
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
53
|
+
const id = ID.exec(item);
|
|
54
|
+
return id === null ? {
|
|
55
|
+
id: "",
|
|
56
|
+
text: item
|
|
57
|
+
} : {
|
|
58
|
+
id: id[1],
|
|
59
|
+
text: id[2]
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
63
|
+
const numbered = (items) => {
|
|
64
|
+
const ids = items.map((item) => ID.exec(item));
|
|
65
|
+
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
66
|
+
return items.map((item, i) => own ? {
|
|
67
|
+
id: ids[i][1],
|
|
68
|
+
text: ids[i][2]
|
|
69
|
+
} : {
|
|
70
|
+
id: `AC-${i + 1}`,
|
|
71
|
+
text: item.replace(ID, "$2")
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const STATED = /^acceptance criteria:?$/i;
|
|
75
|
+
const statedItems = (text) => text === void 0 ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "");
|
|
76
|
+
/**
|
|
77
|
+
* The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
|
|
78
|
+
* added or changed under an `Acceptance criteria` heading. More than one such
|
|
79
|
+
* file is ambiguous, and any other change in the sketch is input only triage
|
|
80
|
+
* folds in, so none.
|
|
81
|
+
*/
|
|
82
|
+
const statedCriteria = (sketch, commit) => {
|
|
83
|
+
const found = sketch.flatMap((change) => {
|
|
84
|
+
if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === void 0) return [];
|
|
85
|
+
const items = statedItems(change.after);
|
|
86
|
+
if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return [];
|
|
87
|
+
return [{
|
|
88
|
+
path: change.path,
|
|
89
|
+
commit,
|
|
90
|
+
text: change.after,
|
|
91
|
+
criteria: numbered(items)
|
|
92
|
+
}];
|
|
93
|
+
});
|
|
94
|
+
return found.length === 1 && sketch.length === 1 ? found[0] : void 0;
|
|
95
|
+
};
|
|
96
|
+
const listing = (criteria) => criteria.map(criterionLine).join("\n");
|
|
97
|
+
/** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
|
|
98
|
+
const requireCriteria = (file) => {
|
|
99
|
+
const text = read(file);
|
|
100
|
+
if (text === void 0) return;
|
|
101
|
+
const criteria = criteriaOf(text) ?? [];
|
|
102
|
+
const unnamed = criteria.filter((c) => c.id === "");
|
|
103
|
+
const ids = criteria.map((c) => c.id).filter((id) => id !== "");
|
|
104
|
+
const twice = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
105
|
+
if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
|
|
106
|
+
refuse(`gtd land: acceptance-criteria: ${file} has ${criteria.length === 0 ? `no "## Acceptance Criteria" list` : unnamed.length > 0 ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}` : `duplicate IDs: ${[...new Set(twice)].join(", ")}`} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`);
|
|
107
|
+
};
|
|
108
|
+
/** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
|
|
109
|
+
const requireCriteriaFrozen = (file) => {
|
|
110
|
+
const change = changes().get(file);
|
|
111
|
+
if (change === void 0) return;
|
|
112
|
+
if (listing(criteriaOf(change.before ?? "") ?? []) === listing(criteriaOf(change.after ?? "") ?? [])) return;
|
|
113
|
+
refuse(`gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`);
|
|
114
|
+
};
|
|
115
|
+
/** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
|
|
116
|
+
const clear = async (stated) => {
|
|
117
|
+
const { answers, truncated } = await judge("clear", {
|
|
118
|
+
questions: [clearCriteriaQuestion],
|
|
119
|
+
evidence: {
|
|
120
|
+
criteria: listing(stated.criteria),
|
|
121
|
+
sketch: stated.text
|
|
122
|
+
},
|
|
123
|
+
message: clearCriteriaMessage(),
|
|
124
|
+
label: "Judging the stated acceptance criteria"
|
|
125
|
+
});
|
|
126
|
+
return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
130
|
+
* to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
|
|
131
|
+
* design gate. `false` when design must run.
|
|
132
|
+
*/
|
|
133
|
+
const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scope("design", async () => {
|
|
134
|
+
if (!await clear(stated)) return false;
|
|
135
|
+
await run("normalise", normaliseCriteriaScript(stated), {
|
|
136
|
+
label: "Recording the stated acceptance criteria",
|
|
137
|
+
file: REQUIREMENTS
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
//#endregion
|
|
20
142
|
//#region src/workflows/feature/tier.ts
|
|
21
143
|
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
22
144
|
const riskTierOn = () => vars.flag_risk_tier === "1";
|
|
@@ -137,6 +259,7 @@ const design = (base) => scope("design", async () => {
|
|
|
137
259
|
await triage(base);
|
|
138
260
|
requireProgress(REQUIREMENTS);
|
|
139
261
|
requireReplies(REQUIREMENTS);
|
|
262
|
+
requireCriteria(REQUIREMENTS);
|
|
140
263
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
141
264
|
});
|
|
142
265
|
//#endregion
|
|
@@ -631,12 +754,16 @@ function implement(first, tier, amend) {
|
|
|
631
754
|
system: builderSystem(),
|
|
632
755
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
633
756
|
});
|
|
757
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
634
758
|
await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
635
759
|
label: "Fixing the check",
|
|
636
760
|
file: FEEDBACK,
|
|
637
761
|
model: coder(),
|
|
638
762
|
system: builderSystem()
|
|
639
|
-
})
|
|
763
|
+
}), {
|
|
764
|
+
guard: () => requireCriteriaFrozen(REQUIREMENTS),
|
|
765
|
+
guardRests: false
|
|
766
|
+
});
|
|
640
767
|
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
641
768
|
});
|
|
642
769
|
}
|
|
@@ -667,6 +794,7 @@ const decide = () => scope("seam", async () => {
|
|
|
667
794
|
acceptClean: true
|
|
668
795
|
});
|
|
669
796
|
if (changes().length === 0) return { reference: tip.hash };
|
|
797
|
+
requireCriteria(REQUIREMENTS);
|
|
670
798
|
rejection = void 0;
|
|
671
799
|
retries = 0;
|
|
672
800
|
}
|
|
@@ -723,9 +851,9 @@ const buildFeature = async (firstBase) => {
|
|
|
723
851
|
}), base);
|
|
724
852
|
});
|
|
725
853
|
};
|
|
726
|
-
/** Design
|
|
727
|
-
const planAndBuild = async (firstBase) => {
|
|
728
|
-
await design(firstBase);
|
|
854
|
+
/** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
|
|
855
|
+
const planAndBuild = async (firstBase, stated) => {
|
|
856
|
+
if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
|
|
729
857
|
await buildFeature(firstBase);
|
|
730
858
|
};
|
|
731
859
|
const featureFeedback = async (first, lensBase) => {
|
|
@@ -755,6 +883,7 @@ const ordinaryStart = async () => {
|
|
|
755
883
|
});
|
|
756
884
|
refusePendingHandOff();
|
|
757
885
|
flagModeOn();
|
|
886
|
+
const stated = statedCriteria(changes(), head());
|
|
758
887
|
await unwind();
|
|
759
888
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
760
889
|
message: unwindFailedMessage(),
|
|
@@ -762,11 +891,11 @@ const ordinaryStart = async () => {
|
|
|
762
891
|
file: FEEDBACK
|
|
763
892
|
});
|
|
764
893
|
await gate("start-gate", startGateBlockedMessage());
|
|
765
|
-
await planAndBuild(start());
|
|
894
|
+
await planAndBuild(start(), stated);
|
|
766
895
|
};
|
|
767
896
|
const feature = shipped(ordinaryStart);
|
|
768
897
|
//#endregion
|
|
769
898
|
//#region src/workflows/feature/index.ts
|
|
770
899
|
var feature_default = feature;
|
|
771
900
|
//#endregion
|
|
772
|
-
export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, implement, judgeSeam, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, steering, summary, triage, unwind };
|
|
901
|
+
export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, implement, judgeSeam, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import { D as agentWithSkills,
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { D as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, _ as REVIEW, c as baseline, i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CCsDBbZe.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, tt as reviewGateBlockedMessage, u as gate, ut as envDefaults } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CCsDBbZe.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { ft as flagMentions, gt as requireFlag, i as skills, lt as defaults, mt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|