@pmelab/gtd 30.0.0 → 31.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 +23 -9
- package/claude/hooks/register.tsx +5 -0
- package/dist/chunks/{reserved-iG86IR3I.mjs → reserved-CHTJTzpA.mjs} +68 -9
- package/dist/chunks/{tail-DA2UuJNP.mjs → tail-CCsDBbZe.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +141 -11
- 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 +207 -18
- 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 +81 -10
- package/src/workflows/feature/feature.ts +18 -10
- package/src/workflows/feature/flag-mode.feature +165 -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 +14 -7
- package/src/workflows/feature/tier-pin.feature +4 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/options.test.ts +12 -0
- package/src/workflows/shared/options.ts +17 -2
- package/src/workflows/shared/text.test.ts +16 -0
- package/src/workflows/shared/text.ts +90 -8
- package/src/workflows/shared/vars.ts +1 -0
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({
|
|
@@ -16916,6 +16916,7 @@ const defaults = {
|
|
|
16916
16916
|
reviewSeverityMinP: "0.7",
|
|
16917
16917
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
16918
16918
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
16919
|
+
flagMode: "true",
|
|
16919
16920
|
flag_risk_tier: "0",
|
|
16920
16921
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
16921
16922
|
refactorQualityReviews: "correctness, ponytail-review, conventions"
|
|
@@ -17222,6 +17223,21 @@ const codeThreadReplies = () => {
|
|
|
17222
17223
|
concluded thread in and delete its lines:
|
|
17223
17224
|
${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
|
|
17224
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
|
+
`;
|
|
17225
17241
|
const riskTierBlock = `
|
|
17226
17242
|
## Risk tier and production signals
|
|
17227
17243
|
|
|
@@ -17265,7 +17281,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
17265
17281
|
or both
|
|
17266
17282
|
- Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
|
|
17267
17283
|
uncommitted and finish once it reflects this lap's own work
|
|
17268
|
-
|
|
17284
|
+
${acceptanceCriteriaBlock}
|
|
17269
17285
|
## First lap
|
|
17270
17286
|
|
|
17271
17287
|
\`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
|
|
@@ -17320,6 +17336,34 @@ ${questionBarReturn}
|
|
|
17320
17336
|
made). If red, make that breakage the first concern, ahead of
|
|
17321
17337
|
everything else — the build assumes the suite started green
|
|
17322
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
|
+
};
|
|
17323
17367
|
const designSystem = () => `${designPersona}
|
|
17324
17368
|
|
|
17325
17369
|
${agentConduct}`;
|
|
@@ -17644,7 +17688,8 @@ What each change does next (then run \`gtd land\`):
|
|
|
17644
17688
|
const wrapItem = (item) => {
|
|
17645
17689
|
const out = [];
|
|
17646
17690
|
let line = "";
|
|
17647
|
-
|
|
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) {
|
|
17648
17693
|
out.push(line);
|
|
17649
17694
|
line = ` ${word}`;
|
|
17650
17695
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
@@ -17942,8 +17987,12 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
17942
17987
|
`;
|
|
17943
17988
|
};
|
|
17944
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"}`;
|
|
17945
17993
|
const naivePassPrompt = (rejection) => `${stateFileRules}
|
|
17946
17994
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
17995
|
+
${criteriaBinding(false)}
|
|
17947
17996
|
- Implement them the quickest way that works — the naive version. Do not
|
|
17948
17997
|
restructure, abstract or refactor around the change; a later step judges
|
|
17949
17998
|
the shape of your diff and a reviewer makes it fit
|
|
@@ -17956,18 +18005,21 @@ re-implement. It changed these files outside the expected set:
|
|
|
17956
18005
|
${listed(rejection.strays)}
|
|
17957
18006
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
17958
18007
|
`}`;
|
|
17959
|
-
const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
|
|
18008
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
|
|
17960
18009
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18010
|
+
${criteriaBinding(true)}
|
|
17961
18011
|
${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
|
|
17962
18012
|
though never a release flag. Build only what is still missing, held to every
|
|
17963
18013
|
rule below; if nothing is, change nothing and finish your turn
|
|
17964
18014
|
` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
17965
18015
|
the re-derived requirements instead of rewriting it
|
|
17966
|
-
` : `- Implement them properly, fitting the codebase's seams and conventions
|
|
18016
|
+
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
18017
|
+
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
18018
|
+
restructure, abstract or refactor around the change
|
|
17967
18019
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
17968
18020
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
17969
18021
|
the change fresh
|
|
17970
|
-
`}${tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
18022
|
+
`}${!flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
17971
18023
|
repository's flag conventions`}
|
|
17972
18024
|
- Write the tests first, then the code that makes them pass
|
|
17973
18025
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
@@ -37522,12 +37574,19 @@ const ShippedOptions = Struct({
|
|
|
37522
37574
|
refactorQualityReviews: setting,
|
|
37523
37575
|
testCommand: setting,
|
|
37524
37576
|
plannerModel: setting,
|
|
37525
|
-
coderModel: setting
|
|
37577
|
+
coderModel: setting,
|
|
37578
|
+
flagMode: optionalWith(Boolean$, { exact: true })
|
|
37526
37579
|
});
|
|
37527
37580
|
const over = (base, options) => ({
|
|
37528
37581
|
...base,
|
|
37529
|
-
...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)))
|
|
37582
|
+
...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
|
|
37530
37583
|
});
|
|
37584
|
+
/** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
|
|
37585
|
+
const flagModeOn = () => {
|
|
37586
|
+
const value = vars.flagMode ?? defaults.flagMode;
|
|
37587
|
+
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
37588
|
+
return value === "true";
|
|
37589
|
+
};
|
|
37531
37590
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
37532
37591
|
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
37533
37592
|
onExcessProperty: "error",
|
|
@@ -37684,6 +37743,128 @@ const markFenced = (text) => {
|
|
|
37684
37743
|
});
|
|
37685
37744
|
};
|
|
37686
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
|
|
37687
37868
|
//#region src/workflows/feature/tier.ts
|
|
37688
37869
|
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
37689
37870
|
const riskTierOn = () => vars.flag_risk_tier === "1";
|
|
@@ -37804,6 +37985,7 @@ const design = (base) => scope("design", async () => {
|
|
|
37804
37985
|
await triage(base);
|
|
37805
37986
|
requireProgress(REQUIREMENTS);
|
|
37806
37987
|
requireReplies(REQUIREMENTS);
|
|
37988
|
+
requireCriteria(REQUIREMENTS);
|
|
37807
37989
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
37808
37990
|
});
|
|
37809
37991
|
//#endregion
|
|
@@ -38292,18 +38474,22 @@ function implement(first, tier, amend) {
|
|
|
38292
38474
|
amend
|
|
38293
38475
|
};
|
|
38294
38476
|
return scope("implement", async () => {
|
|
38295
|
-
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
|
|
38477
|
+
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
|
|
38296
38478
|
label: "Implementing",
|
|
38297
38479
|
model: coder(),
|
|
38298
38480
|
system: builderSystem(),
|
|
38299
38481
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
38300
38482
|
});
|
|
38483
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
38301
38484
|
await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38302
38485
|
label: "Fixing the check",
|
|
38303
38486
|
file: FEEDBACK,
|
|
38304
38487
|
model: coder(),
|
|
38305
38488
|
system: builderSystem()
|
|
38306
|
-
})
|
|
38489
|
+
}), {
|
|
38490
|
+
guard: () => requireCriteriaFrozen(REQUIREMENTS),
|
|
38491
|
+
guardRests: false
|
|
38492
|
+
});
|
|
38307
38493
|
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
38308
38494
|
});
|
|
38309
38495
|
}
|
|
@@ -38334,6 +38520,7 @@ const decide = () => scope("seam", async () => {
|
|
|
38334
38520
|
acceptClean: true
|
|
38335
38521
|
});
|
|
38336
38522
|
if (changes().length === 0) return { reference: tip.hash };
|
|
38523
|
+
requireCriteria(REQUIREMENTS);
|
|
38337
38524
|
rejection = void 0;
|
|
38338
38525
|
retries = 0;
|
|
38339
38526
|
}
|
|
@@ -38373,10 +38560,10 @@ const unwind = () => {
|
|
|
38373
38560
|
const commit = head$1();
|
|
38374
38561
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
38375
38562
|
};
|
|
38376
|
-
/** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
|
|
38563
|
+
/** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
|
|
38377
38564
|
const buildFeature = async (firstBase) => {
|
|
38378
38565
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
38379
|
-
const seam = await seamBuild(firstBase);
|
|
38566
|
+
const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
|
|
38380
38567
|
await slice("feature", async () => {
|
|
38381
38568
|
await implement({
|
|
38382
38569
|
reference: seam.reference,
|
|
@@ -38390,9 +38577,9 @@ const buildFeature = async (firstBase) => {
|
|
|
38390
38577
|
}), base);
|
|
38391
38578
|
});
|
|
38392
38579
|
};
|
|
38393
|
-
/** Design
|
|
38394
|
-
const planAndBuild = async (firstBase) => {
|
|
38395
|
-
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);
|
|
38396
38583
|
await buildFeature(firstBase);
|
|
38397
38584
|
};
|
|
38398
38585
|
const featureFeedback = async (first, lensBase) => {
|
|
@@ -38421,6 +38608,8 @@ const ordinaryStart = async () => {
|
|
|
38421
38608
|
file: ".gtd/TODO.md"
|
|
38422
38609
|
});
|
|
38423
38610
|
refusePendingHandOff();
|
|
38611
|
+
flagModeOn();
|
|
38612
|
+
const stated = statedCriteria(changes(), head$1());
|
|
38424
38613
|
await unwind();
|
|
38425
38614
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
38426
38615
|
message: unwindFailedMessage(),
|
|
@@ -38428,7 +38617,7 @@ const ordinaryStart = async () => {
|
|
|
38428
38617
|
file: FEEDBACK
|
|
38429
38618
|
});
|
|
38430
38619
|
await gate("start-gate", startGateBlockedMessage());
|
|
38431
|
-
await planAndBuild(start());
|
|
38620
|
+
await planAndBuild(start(), stated);
|
|
38432
38621
|
};
|
|
38433
38622
|
const feature = shipped(ordinaryStart);
|
|
38434
38623
|
//#endregion
|
|
@@ -38614,4 +38803,4 @@ var workflows_default = feature;
|
|
|
38614
38803
|
*/
|
|
38615
38804
|
const doors = {};
|
|
38616
38805
|
//#endregion
|
|
38617
|
-
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
|
+
"""
|