@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/dist/workflows.mjs
CHANGED
|
@@ -499,7 +499,7 @@ const fixFindings = async (f) => {
|
|
|
499
499
|
const DOC_STRING = /^("""|```)/;
|
|
500
500
|
const THEN = /^Then\s/;
|
|
501
501
|
const CONTINUATION = /^(And|But|\*)\s/;
|
|
502
|
-
const HEADING = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/;
|
|
502
|
+
const HEADING$1 = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/;
|
|
503
503
|
/** Trimmed lines outside doc strings, whose content is test input, not steps. */
|
|
504
504
|
const stepLines = (source) => {
|
|
505
505
|
const out = [];
|
|
@@ -519,7 +519,7 @@ const assertionLines = (source) => {
|
|
|
519
519
|
let inThen = false;
|
|
520
520
|
let scenario = "";
|
|
521
521
|
for (const line of stepLines(source)) {
|
|
522
|
-
if (HEADING.test(line)) scenario = line;
|
|
522
|
+
if (HEADING$1.test(line)) scenario = line;
|
|
523
523
|
if (THEN.test(line)) inThen = true;
|
|
524
524
|
else if (!CONTINUATION.test(line) && !neutral(line)) inThen = false;
|
|
525
525
|
if (inThen && !neutral(line)) out.push({
|
|
@@ -17223,6 +17223,21 @@ const codeThreadReplies = () => {
|
|
|
17223
17223
|
concluded thread in and delete its lines:
|
|
17224
17224
|
${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
|
|
17225
17225
|
};
|
|
17226
|
+
const acceptanceCriteriaBlock = `
|
|
17227
|
+
## Acceptance criteria
|
|
17228
|
+
|
|
17229
|
+
Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
|
|
17230
|
+
\`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
|
|
17231
|
+
per criterion, numbered from \`AC-1\`. They bind every later step —
|
|
17232
|
+
the build is done when each one holds — so each must be checkable: a
|
|
17233
|
+
test or a command shows it holds or not. A later lap keeps every ID:
|
|
17234
|
+
reword a criterion in place, append a new one with the next unused
|
|
17235
|
+
number, delete a dropped one — never renumber, never reuse an ID. When
|
|
17236
|
+
the file holds no list (a review loop-back rewrites it), recover the
|
|
17237
|
+
last settled one from history (\`git log -p -- .gtd/REQUIREMENTS.md\`)
|
|
17238
|
+
and carry its IDs over. Landing is refused while a criterion has no ID or
|
|
17239
|
+
two share one.
|
|
17240
|
+
`;
|
|
17226
17241
|
const riskTierBlock = `
|
|
17227
17242
|
## Risk tier and production signals
|
|
17228
17243
|
|
|
@@ -17266,7 +17281,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
17266
17281
|
or both
|
|
17267
17282
|
- Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
|
|
17268
17283
|
uncommitted and finish once it reflects this lap's own work
|
|
17269
|
-
|
|
17284
|
+
${acceptanceCriteriaBlock}
|
|
17270
17285
|
## First lap
|
|
17271
17286
|
|
|
17272
17287
|
\`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
|
|
@@ -17321,6 +17336,34 @@ ${questionBarReturn}
|
|
|
17321
17336
|
made). If red, make that breakage the first concern, ahead of
|
|
17322
17337
|
everything else — the build assumes the suite started green
|
|
17323
17338
|
${riskTier ? riskTierBlock : ""}`;
|
|
17339
|
+
const clearCriteriaQuestion = {
|
|
17340
|
+
id: "clear",
|
|
17341
|
+
primitive: "noul",
|
|
17342
|
+
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.",
|
|
17343
|
+
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."
|
|
17344
|
+
};
|
|
17345
|
+
const clearCriteriaMessage = () => `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
|
|
17346
|
+
pipe \`yes\` when every one is clear and checkable, so design is skipped and
|
|
17347
|
+
they are recorded as they are — or land without a verdict to run the
|
|
17348
|
+
ordinary design conversation.
|
|
17349
|
+
`;
|
|
17350
|
+
const criterionLine = (c) => `- ${c.id}: ${c.text.replace(/\s+/g, " ")}`;
|
|
17351
|
+
/** Writes \`.gtd/REQUIREMENTS.md\` from criteria the sketch stated, wrapped the way oxfmt writes it. */
|
|
17352
|
+
const normaliseCriteriaScript = (stated) => {
|
|
17353
|
+
return `mkdir -p .gtd
|
|
17354
|
+
printf '%s\\n' ${[
|
|
17355
|
+
"# Requirements",
|
|
17356
|
+
"",
|
|
17357
|
+
`Stated in \`${stated.path}\` by the sketch:`,
|
|
17358
|
+
"",
|
|
17359
|
+
`\`git show ${stated.commit}:${stated.path}\``,
|
|
17360
|
+
"",
|
|
17361
|
+
"## Acceptance Criteria",
|
|
17362
|
+
"",
|
|
17363
|
+
...stated.criteria.flatMap((c) => wrapItem(criterionLine(c)))
|
|
17364
|
+
].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
|
|
17365
|
+
`;
|
|
17366
|
+
};
|
|
17324
17367
|
const designSystem = () => `${designPersona}
|
|
17325
17368
|
|
|
17326
17369
|
${agentConduct}`;
|
|
@@ -17645,7 +17688,8 @@ What each change does next (then run \`gtd land\`):
|
|
|
17645
17688
|
const wrapItem = (item) => {
|
|
17646
17689
|
const out = [];
|
|
17647
17690
|
let line = "";
|
|
17648
|
-
|
|
17691
|
+
const words = item.split(" ").reduce((acc, word) => acc.length > 0 && /^([-+*>#]|\d+[.)])$/.test(word) ? [...acc.slice(0, -1), `${acc.at(-1)} ${word}`] : [...acc, word], []);
|
|
17692
|
+
for (const word of words) if (line !== "" && line.length + 1 + word.length > 80) {
|
|
17649
17693
|
out.push(line);
|
|
17650
17694
|
line = ` ${word}`;
|
|
17651
17695
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
@@ -17943,8 +17987,12 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
17943
17987
|
`;
|
|
17944
17988
|
};
|
|
17945
17989
|
const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
|
|
17990
|
+
const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
17991
|
+
the file, never from memory, and stop only when each one holds. They are
|
|
17992
|
+
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
|
|
17946
17993
|
const naivePassPrompt = (rejection) => `${stateFileRules}
|
|
17947
17994
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
17995
|
+
${criteriaBinding(false)}
|
|
17948
17996
|
- Implement them the quickest way that works — the naive version. Do not
|
|
17949
17997
|
restructure, abstract or refactor around the change; a later step judges
|
|
17950
17998
|
the shape of your diff and a reviewer makes it fit
|
|
@@ -17959,6 +18007,7 @@ Start over and choose a different approach; do not rebuild the same diff.
|
|
|
17959
18007
|
`}`;
|
|
17960
18008
|
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
|
|
17961
18009
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18010
|
+
${criteriaBinding(true)}
|
|
17962
18011
|
${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
|
|
17963
18012
|
though never a release flag. Build only what is still missing, held to every
|
|
17964
18013
|
rule below; if nothing is, change nothing and finish your turn
|
|
@@ -37694,6 +37743,128 @@ const markFenced = (text) => {
|
|
|
37694
37743
|
});
|
|
37695
37744
|
};
|
|
37696
37745
|
//#endregion
|
|
37746
|
+
//#region src/workflows/feature/criteria.ts
|
|
37747
|
+
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
37748
|
+
const CLEAR_MIN_P = .8;
|
|
37749
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
37750
|
+
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
37751
|
+
const ID = /^(AC-\d+):\s*(.*)$/;
|
|
37752
|
+
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
37753
|
+
const listUnder = (text, title, level) => {
|
|
37754
|
+
const lines = markFenced(text);
|
|
37755
|
+
const at = lines.findIndex((l) => {
|
|
37756
|
+
const heading = l.fenced ? null : HEADING.exec(l.line);
|
|
37757
|
+
return heading !== null && (level === void 0 || heading[1].length === level) && title.test(heading[2]);
|
|
37758
|
+
});
|
|
37759
|
+
if (at === -1) return void 0;
|
|
37760
|
+
const items = [];
|
|
37761
|
+
let open = false;
|
|
37762
|
+
for (const { line, fenced } of lines.slice(at + 1)) {
|
|
37763
|
+
if (fenced) {
|
|
37764
|
+
open = false;
|
|
37765
|
+
continue;
|
|
37766
|
+
}
|
|
37767
|
+
if (HEADING.test(line)) break;
|
|
37768
|
+
const item = ITEM.exec(line);
|
|
37769
|
+
if (item !== null) {
|
|
37770
|
+
items.push(item[1].trim());
|
|
37771
|
+
open = true;
|
|
37772
|
+
} else if (open && line.trim() !== "") items[items.length - 1] = `${items.at(-1)} ${line.trim()}`.trim();
|
|
37773
|
+
else open = false;
|
|
37774
|
+
}
|
|
37775
|
+
return items;
|
|
37776
|
+
};
|
|
37777
|
+
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
37778
|
+
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
37779
|
+
const id = ID.exec(item);
|
|
37780
|
+
return id === null ? {
|
|
37781
|
+
id: "",
|
|
37782
|
+
text: item
|
|
37783
|
+
} : {
|
|
37784
|
+
id: id[1],
|
|
37785
|
+
text: id[2]
|
|
37786
|
+
};
|
|
37787
|
+
});
|
|
37788
|
+
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
37789
|
+
const numbered = (items) => {
|
|
37790
|
+
const ids = items.map((item) => ID.exec(item));
|
|
37791
|
+
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
37792
|
+
return items.map((item, i) => own ? {
|
|
37793
|
+
id: ids[i][1],
|
|
37794
|
+
text: ids[i][2]
|
|
37795
|
+
} : {
|
|
37796
|
+
id: `AC-${i + 1}`,
|
|
37797
|
+
text: item.replace(ID, "$2")
|
|
37798
|
+
});
|
|
37799
|
+
};
|
|
37800
|
+
const STATED = /^acceptance criteria:?$/i;
|
|
37801
|
+
const statedItems = (text) => text === void 0 ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "");
|
|
37802
|
+
/**
|
|
37803
|
+
* The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
|
|
37804
|
+
* added or changed under an `Acceptance criteria` heading. More than one such
|
|
37805
|
+
* file is ambiguous, and any other change in the sketch is input only triage
|
|
37806
|
+
* folds in, so none.
|
|
37807
|
+
*/
|
|
37808
|
+
const statedCriteria = (sketch, commit) => {
|
|
37809
|
+
const found = sketch.flatMap((change) => {
|
|
37810
|
+
if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === void 0) return [];
|
|
37811
|
+
const items = statedItems(change.after);
|
|
37812
|
+
if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return [];
|
|
37813
|
+
return [{
|
|
37814
|
+
path: change.path,
|
|
37815
|
+
commit,
|
|
37816
|
+
text: change.after,
|
|
37817
|
+
criteria: numbered(items)
|
|
37818
|
+
}];
|
|
37819
|
+
});
|
|
37820
|
+
return found.length === 1 && sketch.length === 1 ? found[0] : void 0;
|
|
37821
|
+
};
|
|
37822
|
+
const listing = (criteria) => criteria.map(criterionLine).join("\n");
|
|
37823
|
+
/** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
|
|
37824
|
+
const requireCriteria = (file) => {
|
|
37825
|
+
const text = read(file);
|
|
37826
|
+
if (text === void 0) return;
|
|
37827
|
+
const criteria = criteriaOf(text) ?? [];
|
|
37828
|
+
const unnamed = criteria.filter((c) => c.id === "");
|
|
37829
|
+
const ids = criteria.map((c) => c.id).filter((id) => id !== "");
|
|
37830
|
+
const twice = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
37831
|
+
if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
|
|
37832
|
+
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`);
|
|
37833
|
+
};
|
|
37834
|
+
/** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
|
|
37835
|
+
const requireCriteriaFrozen = (file) => {
|
|
37836
|
+
const change = changes().get(file);
|
|
37837
|
+
if (change === void 0) return;
|
|
37838
|
+
if (listing(criteriaOf(change.before ?? "") ?? []) === listing(criteriaOf(change.after ?? "") ?? [])) return;
|
|
37839
|
+
refuse(`gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`);
|
|
37840
|
+
};
|
|
37841
|
+
/** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
|
|
37842
|
+
const clear = async (stated) => {
|
|
37843
|
+
const { answers, truncated } = await judge("clear", {
|
|
37844
|
+
questions: [clearCriteriaQuestion],
|
|
37845
|
+
evidence: {
|
|
37846
|
+
criteria: listing(stated.criteria),
|
|
37847
|
+
sketch: stated.text
|
|
37848
|
+
},
|
|
37849
|
+
message: clearCriteriaMessage(),
|
|
37850
|
+
label: "Judging the stated acceptance criteria"
|
|
37851
|
+
});
|
|
37852
|
+
return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
|
|
37853
|
+
};
|
|
37854
|
+
/**
|
|
37855
|
+
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
37856
|
+
* to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
|
|
37857
|
+
* design gate. `false` when design must run.
|
|
37858
|
+
*/
|
|
37859
|
+
const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scope("design", async () => {
|
|
37860
|
+
if (!await clear(stated)) return false;
|
|
37861
|
+
await run("normalise", normaliseCriteriaScript(stated), {
|
|
37862
|
+
label: "Recording the stated acceptance criteria",
|
|
37863
|
+
file: REQUIREMENTS
|
|
37864
|
+
});
|
|
37865
|
+
return true;
|
|
37866
|
+
});
|
|
37867
|
+
//#endregion
|
|
37697
37868
|
//#region src/workflows/feature/tier.ts
|
|
37698
37869
|
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
37699
37870
|
const riskTierOn = () => vars.flag_risk_tier === "1";
|
|
@@ -37814,6 +37985,7 @@ const design = (base) => scope("design", async () => {
|
|
|
37814
37985
|
await triage(base);
|
|
37815
37986
|
requireProgress(REQUIREMENTS);
|
|
37816
37987
|
requireReplies(REQUIREMENTS);
|
|
37988
|
+
requireCriteria(REQUIREMENTS);
|
|
37817
37989
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
37818
37990
|
});
|
|
37819
37991
|
//#endregion
|
|
@@ -38308,12 +38480,16 @@ function implement(first, tier, amend) {
|
|
|
38308
38480
|
system: builderSystem(),
|
|
38309
38481
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
38310
38482
|
});
|
|
38483
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
38311
38484
|
await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38312
38485
|
label: "Fixing the check",
|
|
38313
38486
|
file: FEEDBACK,
|
|
38314
38487
|
model: coder(),
|
|
38315
38488
|
system: builderSystem()
|
|
38316
|
-
})
|
|
38489
|
+
}), {
|
|
38490
|
+
guard: () => requireCriteriaFrozen(REQUIREMENTS),
|
|
38491
|
+
guardRests: false
|
|
38492
|
+
});
|
|
38317
38493
|
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
38318
38494
|
});
|
|
38319
38495
|
}
|
|
@@ -38344,6 +38520,7 @@ const decide = () => scope("seam", async () => {
|
|
|
38344
38520
|
acceptClean: true
|
|
38345
38521
|
});
|
|
38346
38522
|
if (changes().length === 0) return { reference: tip.hash };
|
|
38523
|
+
requireCriteria(REQUIREMENTS);
|
|
38347
38524
|
rejection = void 0;
|
|
38348
38525
|
retries = 0;
|
|
38349
38526
|
}
|
|
@@ -38400,9 +38577,9 @@ const buildFeature = async (firstBase) => {
|
|
|
38400
38577
|
}), base);
|
|
38401
38578
|
});
|
|
38402
38579
|
};
|
|
38403
|
-
/** Design
|
|
38404
|
-
const planAndBuild = async (firstBase) => {
|
|
38405
|
-
await design(firstBase);
|
|
38580
|
+
/** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
|
|
38581
|
+
const planAndBuild = async (firstBase, stated) => {
|
|
38582
|
+
if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
|
|
38406
38583
|
await buildFeature(firstBase);
|
|
38407
38584
|
};
|
|
38408
38585
|
const featureFeedback = async (first, lensBase) => {
|
|
@@ -38432,6 +38609,7 @@ const ordinaryStart = async () => {
|
|
|
38432
38609
|
});
|
|
38433
38610
|
refusePendingHandOff();
|
|
38434
38611
|
flagModeOn();
|
|
38612
|
+
const stated = statedCriteria(changes(), head$1());
|
|
38435
38613
|
await unwind();
|
|
38436
38614
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
38437
38615
|
message: unwindFailedMessage(),
|
|
@@ -38439,7 +38617,7 @@ const ordinaryStart = async () => {
|
|
|
38439
38617
|
file: FEEDBACK
|
|
38440
38618
|
});
|
|
38441
38619
|
await gate("start-gate", startGateBlockedMessage());
|
|
38442
|
-
await planAndBuild(start());
|
|
38620
|
+
await planAndBuild(start(), stated);
|
|
38443
38621
|
};
|
|
38444
38622
|
const feature = shipped(ordinaryStart);
|
|
38445
38623
|
//#endregion
|
|
@@ -38625,4 +38803,4 @@ var workflows_default = feature;
|
|
|
38625
38803
|
*/
|
|
38626
38804
|
const doors = {};
|
|
38627
38805
|
//#endregion
|
|
38628
|
-
export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, steering, summary, tail, threads, triage, unwind };
|
|
38806
|
+
export { ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: with GTD_FLAG_RISK_TIER on, stated acceptance criteria never skip design
|
|
3
|
+
|
|
4
|
+
The risk tier is proposed at `design.triage` and set at the design gate, so
|
|
5
|
+
with the flag on a sketch that states its own criteria still runs triage and
|
|
6
|
+
is never judged at `design.clear`.
|
|
7
|
+
|
|
8
|
+
Scenario: stated criteria go through triage, where the tier is proposed and set
|
|
9
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
10
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
11
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
|
|
12
|
+
And step "unwind" changes nothing
|
|
13
|
+
And step "start-gate.check" changes nothing
|
|
14
|
+
And a file "TODO.md" with:
|
|
15
|
+
"""
|
|
16
|
+
## Acceptance criteria
|
|
17
|
+
|
|
18
|
+
- `greeting` reads "Hello, world".
|
|
19
|
+
"""
|
|
20
|
+
When the process is driven
|
|
21
|
+
Then the process rests at "design.triage"
|
|
22
|
+
And the commit subjects end with:
|
|
23
|
+
"""
|
|
24
|
+
gtd(check): start-gate.check → design.triage
|
|
25
|
+
"""
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: acceptance criteria carry stable IDs, and a sketch that states clear ones skips design
|
|
3
|
+
|
|
4
|
+
`.gtd/REQUIREMENTS.md` lists the change's acceptance criteria as
|
|
5
|
+
`- AC-<n>: …` under `## Acceptance Criteria`. On the full path
|
|
6
|
+
`design.triage` writes them and the design gate settles them; a sketch
|
|
7
|
+
that already states its criteria under an `Acceptance criteria` heading
|
|
8
|
+
is judged at `design.clear`, and a `yes` records them as they are — no
|
|
9
|
+
triage, no design gate. After design they are frozen.
|
|
10
|
+
|
|
11
|
+
Background:
|
|
12
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
13
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
14
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
15
|
+
And a file "src/greeting.ts" with:
|
|
16
|
+
"""
|
|
17
|
+
export const greeting = "Helo, world"
|
|
18
|
+
"""
|
|
19
|
+
And the working tree is committed as "feat: add greeting"
|
|
20
|
+
And step "unwind" changes nothing
|
|
21
|
+
And step "start-gate.check" changes nothing
|
|
22
|
+
|
|
23
|
+
Rule: the full path
|
|
24
|
+
|
|
25
|
+
Background:
|
|
26
|
+
Given a file "TODO.md" with:
|
|
27
|
+
"""
|
|
28
|
+
# Fix the typo
|
|
29
|
+
|
|
30
|
+
greeting says "Helo"; make it "Hello".
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
Scenario: triage writes ID'd criteria that reach the naive pass through the design gate
|
|
34
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
35
|
+
"""
|
|
36
|
+
# Requirements
|
|
37
|
+
|
|
38
|
+
## Acceptance Criteria
|
|
39
|
+
|
|
40
|
+
- AC-1: `greeting` reads "Hello, world".
|
|
41
|
+
- AC-2: no other export changes.
|
|
42
|
+
"""
|
|
43
|
+
And step "design.gate.answer" changes nothing
|
|
44
|
+
When the process is driven
|
|
45
|
+
Then the process rests at "seam.naive"
|
|
46
|
+
And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
|
|
47
|
+
And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
|
|
48
|
+
And the resting step's content contains "Acceptance Criteria"
|
|
49
|
+
And the commit subjects end with:
|
|
50
|
+
"""
|
|
51
|
+
gtd(check): start-gate.check → design.triage
|
|
52
|
+
gtd(agent): design.triage → design.gate.answer
|
|
53
|
+
gtd(human): design.gate.answer → seam.naive
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
Scenario: a triage turn whose criteria carry no ID is refused
|
|
57
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
58
|
+
"""
|
|
59
|
+
# Requirements
|
|
60
|
+
|
|
61
|
+
## Acceptance Criteria
|
|
62
|
+
|
|
63
|
+
- `greeting` reads "Hello, world".
|
|
64
|
+
"""
|
|
65
|
+
Then driving the process is refused with "acceptance-criteria: .gtd/REQUIREMENTS.md has 1 criterion without an ID"
|
|
66
|
+
|
|
67
|
+
Scenario: a triage turn reusing an ID is refused
|
|
68
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
69
|
+
"""
|
|
70
|
+
# Requirements
|
|
71
|
+
|
|
72
|
+
## Acceptance Criteria
|
|
73
|
+
|
|
74
|
+
- AC-1: `greeting` reads "Hello, world".
|
|
75
|
+
- AC-1: no other export changes.
|
|
76
|
+
"""
|
|
77
|
+
Then driving the process is refused with "duplicate IDs: AC-1"
|
|
78
|
+
|
|
79
|
+
Scenario: an implementation turn that edits the settled criteria is refused
|
|
80
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
81
|
+
"""
|
|
82
|
+
# Requirements
|
|
83
|
+
|
|
84
|
+
## Acceptance Criteria
|
|
85
|
+
|
|
86
|
+
- AC-1: `greeting` reads "Hello, world".
|
|
87
|
+
"""
|
|
88
|
+
And step "design.gate.answer" changes nothing
|
|
89
|
+
And step "seam.naive" writes "src/greeting.ts" with:
|
|
90
|
+
"""
|
|
91
|
+
export const greeting = "Hello, world"
|
|
92
|
+
"""
|
|
93
|
+
And judge step "seam.judge" answers:
|
|
94
|
+
"""
|
|
95
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
96
|
+
"""
|
|
97
|
+
And step "implement.building" writes ".gtd/REQUIREMENTS.md" with:
|
|
98
|
+
"""
|
|
99
|
+
# Requirements
|
|
100
|
+
|
|
101
|
+
## Acceptance Criteria
|
|
102
|
+
|
|
103
|
+
- AC-1: `greeting` reads "Hi".
|
|
104
|
+
"""
|
|
105
|
+
Then driving the process is refused with "criteria-frozen"
|
|
106
|
+
|
|
107
|
+
Rule: the skip path
|
|
108
|
+
|
|
109
|
+
Scenario: clear stated criteria are normalised into ID'd requirements with no triage and no design gate
|
|
110
|
+
Given a file "TODO.md" with:
|
|
111
|
+
"""
|
|
112
|
+
# Fix the typo
|
|
113
|
+
|
|
114
|
+
## Acceptance criteria
|
|
115
|
+
|
|
116
|
+
- [ ] `greeting` reads "Hello, world".
|
|
117
|
+
- [ ] no other export changes.
|
|
118
|
+
"""
|
|
119
|
+
And judge step "design.clear" answers:
|
|
120
|
+
"""
|
|
121
|
+
[{"id":"clear","answer":"yes","p":0.9}]
|
|
122
|
+
"""
|
|
123
|
+
And step "design.normalise" runs its script
|
|
124
|
+
When the process is driven
|
|
125
|
+
Then the process rests at "seam.naive"
|
|
126
|
+
And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
|
|
127
|
+
And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
|
|
128
|
+
And the commit subjects end with:
|
|
129
|
+
"""
|
|
130
|
+
gtd(human): idle → unwind
|
|
131
|
+
gtd(check): unwind → start-gate.check
|
|
132
|
+
gtd(check): start-gate.check → design.clear
|
|
133
|
+
gtd(judge): design.clear → design.normalise
|
|
134
|
+
gtd(check): design.normalise → seam.naive
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
Scenario: IDs the sketch already gave its criteria are kept
|
|
138
|
+
Given a file "TODO.md" with:
|
|
139
|
+
"""
|
|
140
|
+
## Acceptance criteria
|
|
141
|
+
|
|
142
|
+
1. AC-3: `greeting` reads "Hello, world".
|
|
143
|
+
2. AC-7: no other export changes.
|
|
144
|
+
"""
|
|
145
|
+
And judge step "design.clear" answers:
|
|
146
|
+
"""
|
|
147
|
+
[{"id":"clear","answer":"yes","p":0.9}]
|
|
148
|
+
"""
|
|
149
|
+
And step "design.normalise" runs its script
|
|
150
|
+
When the process is driven
|
|
151
|
+
Then the process rests at "seam.naive"
|
|
152
|
+
And ".gtd/REQUIREMENTS.md" contains "- AC-3: `greeting` reads \"Hello, world\"."
|
|
153
|
+
And ".gtd/REQUIREMENTS.md" contains "- AC-7: no other export changes."
|
|
154
|
+
|
|
155
|
+
Scenario: one criterion the judge finds uncheckable falls back to the full path
|
|
156
|
+
Given a file "TODO.md" with:
|
|
157
|
+
"""
|
|
158
|
+
# Fix the typo
|
|
159
|
+
|
|
160
|
+
## Acceptance criteria
|
|
161
|
+
|
|
162
|
+
- `greeting` reads "Hello, world".
|
|
163
|
+
- the greeting feels friendlier.
|
|
164
|
+
"""
|
|
165
|
+
And judge step "design.clear" answers:
|
|
166
|
+
"""
|
|
167
|
+
[{"id":"clear","answer":"no","p":0.9}]
|
|
168
|
+
"""
|
|
169
|
+
When the process is driven
|
|
170
|
+
Then the process rests at "design.triage"
|
|
171
|
+
And the commit subjects end with:
|
|
172
|
+
"""
|
|
173
|
+
gtd(check): start-gate.check → design.clear
|
|
174
|
+
gtd(judge): design.clear → design.triage
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
Scenario: a sketch with no criteria heading goes straight to triage, unjudged
|
|
178
|
+
Given a file "TODO.md" with:
|
|
179
|
+
"""
|
|
180
|
+
# Fix the typo
|
|
181
|
+
|
|
182
|
+
- `greeting` reads "Hello, world".
|
|
183
|
+
"""
|
|
184
|
+
When the process is driven
|
|
185
|
+
Then the process rests at "design.triage"
|
|
186
|
+
And the commit subjects end with:
|
|
187
|
+
"""
|
|
188
|
+
gtd(check): start-gate.check → design.triage
|
|
189
|
+
"""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import type { Change } from "../../flows/index.js"
|
|
3
|
+
import { criteriaOf, statedCriteria } from "./criteria.js"
|
|
4
|
+
|
|
5
|
+
const added = (path: string, after: string): Change => ({
|
|
6
|
+
path,
|
|
7
|
+
status: "added",
|
|
8
|
+
before: undefined,
|
|
9
|
+
after,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe("criteriaOf", () => {
|
|
13
|
+
it("reads ID'd items, joining wrapped lines, and stops at the next section", () => {
|
|
14
|
+
expect(
|
|
15
|
+
criteriaOf(
|
|
16
|
+
"# R\n\n## Acceptance Criteria\n\n- AC-1: one\n continued\n- two\n\n## Notes\n\n- AC-9: not one\n",
|
|
17
|
+
),
|
|
18
|
+
).toEqual([
|
|
19
|
+
{ id: "AC-1", text: "one continued" },
|
|
20
|
+
{ id: "", text: "two" },
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("skips fenced lines, reads an indented item, and stops at a subheading", () => {
|
|
25
|
+
expect(
|
|
26
|
+
criteriaOf(
|
|
27
|
+
"## Acceptance Criteria\n\n- AC-1: runs\n\n```sh\n# setup\n- key: v\n```\n - AC-2: two\n\n### Notes\n\n- note\n",
|
|
28
|
+
),
|
|
29
|
+
).toEqual([
|
|
30
|
+
{ id: "AC-1", text: "runs" },
|
|
31
|
+
{ id: "AC-2", text: "two" },
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("reads only the level-2 section and joins an unindented wrapped line", () => {
|
|
36
|
+
expect(
|
|
37
|
+
criteriaOf(
|
|
38
|
+
"### Acceptance criteria\n\n- x\n\n## Acceptance Criteria\n\n- AC-1: exits 2 when the config\nfile is missing\n",
|
|
39
|
+
),
|
|
40
|
+
).toEqual([{ id: "AC-1", text: "exits 2 when the config file is missing" }])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("folds a nested sub-bullet into its criterion", () => {
|
|
44
|
+
expect(
|
|
45
|
+
criteriaOf(
|
|
46
|
+
"## Acceptance Criteria\n\n- AC-1: exits 2 when config is bad\n - missing file\n",
|
|
47
|
+
),
|
|
48
|
+
).toEqual([{ id: "AC-1", text: "exits 2 when config is bad - missing file" }])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("is undefined without the section", () => {
|
|
52
|
+
expect(criteriaOf("# R\n\n- AC-1: x\n")).toBeUndefined()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("statedCriteria", () => {
|
|
57
|
+
it("numbers a stated checklist from AC-1", () => {
|
|
58
|
+
const sketch = [added("TODO.md", "# T\n\n### Acceptance criteria:\n\n- [ ] a\n* b\n\nprose\n")]
|
|
59
|
+
expect(statedCriteria(sketch, "abc")).toMatchObject({
|
|
60
|
+
path: "TODO.md",
|
|
61
|
+
commit: "abc",
|
|
62
|
+
criteria: [
|
|
63
|
+
{ id: "AC-1", text: "a" },
|
|
64
|
+
{ id: "AC-2", text: "b" },
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("renumbers when the stated IDs are partial or clash", () => {
|
|
70
|
+
const sketch = [added("TODO.md", "## Acceptance criteria\n\n- AC-4: a\n- b\n")]
|
|
71
|
+
expect(statedCriteria(sketch, "abc")?.criteria).toEqual([
|
|
72
|
+
{ id: "AC-1", text: "a" },
|
|
73
|
+
{ id: "AC-2", text: "b" },
|
|
74
|
+
])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("ignores a list the sketch left unchanged, a file other than TODO.md, a sketch with more in it, and an ambiguous pair", () => {
|
|
78
|
+
const list = "## Acceptance criteria\n\n- a\n"
|
|
79
|
+
expect(
|
|
80
|
+
statedCriteria(
|
|
81
|
+
[{ path: "TODO.md", status: "modified", before: list, after: `${list}\nmore\n` }],
|
|
82
|
+
"abc",
|
|
83
|
+
),
|
|
84
|
+
).toBeUndefined()
|
|
85
|
+
expect(
|
|
86
|
+
statedCriteria(
|
|
87
|
+
[
|
|
88
|
+
{
|
|
89
|
+
path: "TODO.md",
|
|
90
|
+
status: "modified",
|
|
91
|
+
before: `${list}- \n`,
|
|
92
|
+
after: `${list}- \n\nmore\n`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
"abc",
|
|
96
|
+
),
|
|
97
|
+
).toBeUndefined()
|
|
98
|
+
expect(statedCriteria([added("docs/guide.md", list)], "abc")).toBeUndefined()
|
|
99
|
+
expect(
|
|
100
|
+
statedCriteria([added("TODO.md", list), added("src/a.ts", "x\n")], "abc"),
|
|
101
|
+
).toBeUndefined()
|
|
102
|
+
expect(
|
|
103
|
+
statedCriteria([added("TODO.md", list), added(".gtd/TODO.md", list)], "abc"),
|
|
104
|
+
).toBeUndefined()
|
|
105
|
+
})
|
|
106
|
+
})
|