@pmelab/gtd 50.0.0 → 51.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +32 -24
  3. package/dist/chunks/{reserved-CdOzKFVX.mjs → reserved-CUkk9XgL.mjs} +93 -4
  4. package/dist/chunks/{review-BChXEl2Y.mjs → review-BaCXlIT2.mjs} +2 -2
  5. package/dist/chunks/{tail-DRM2eC1z.mjs → tail-CX6Pdsql.mjs} +1 -1
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +144 -11
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +3 -3
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +234 -12
  12. package/package.json +1 -1
  13. package/src/workflows/feature/coverage.feature +1 -0
  14. package/src/workflows/feature/fast-path.feature +1 -0
  15. package/src/workflows/feature/feature.ts +4 -1
  16. package/src/workflows/feature/flag-mode.feature +1 -1
  17. package/src/workflows/feature/lens-models.feature +1 -0
  18. package/src/workflows/feature/lens-review.feature +1 -0
  19. package/src/workflows/feature/one-step-land.feature +1 -0
  20. package/src/workflows/feature/prototype-rounds.feature +152 -0
  21. package/src/workflows/feature/prototype.feature +419 -0
  22. package/src/workflows/feature/prototype.test.ts +43 -0
  23. package/src/workflows/feature/prototype.ts +181 -0
  24. package/src/workflows/feature/refactor-absorbs.feature +9 -1
  25. package/src/workflows/feature/refactor-first.feature +13 -3
  26. package/src/workflows/feature/refactor-rebuild.feature +29 -5
  27. package/src/workflows/feature/refactor-review.feature +111 -13
  28. package/src/workflows/feature/refactor.test.ts +14 -4
  29. package/src/workflows/feature/refactor.ts +2 -0
  30. package/src/workflows/feature/review-answers.feature +1 -0
  31. package/src/workflows/feature/review-gate.feature +1 -0
  32. package/src/workflows/feature/reviewer-walkthrough.feature +1 -0
  33. package/src/workflows/feature/seam-rejection.feature +1 -0
  34. package/src/workflows/feature/seam.feature +1 -0
  35. package/src/workflows/feature/seam.test.ts +3 -2
  36. package/src/workflows/feature/seam.ts +19 -5
  37. package/src/workflows/feature/severity-budget.feature +1 -0
  38. package/src/workflows/feature/severity-sorted-review.feature +1 -0
  39. package/src/workflows/feature/slice-findings.feature +1 -0
  40. package/src/workflows/feature/slicing-cap.feature +9 -0
  41. package/src/workflows/feature/slicing.feature +1 -0
  42. package/src/workflows/feature/standards.feature +1 -0
  43. package/src/workflows/shared/options.ts +1 -0
  44. package/src/workflows/shared/prose.ts +6 -0
  45. package/src/workflows/shared/skills.test.ts +4 -0
  46. package/src/workflows/shared/skills.ts +5 -0
  47. package/src/workflows/shared/steps.ts +1 -0
  48. package/src/workflows/shared/text.ts +104 -3
  49. package/src/workflows/shared/vars.ts +3 -0
@@ -17241,6 +17241,7 @@ const defaults = {
17241
17241
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
17242
17242
  refactorQualityReviews: "correctness, ponytail-review, conventions",
17243
17243
  continuations: "3",
17244
+ prototypeRounds: "3",
17244
17245
  continuationStall: "2"
17245
17246
  };
17246
17247
  /** Environment settings. */
@@ -17318,6 +17319,11 @@ task in front of you end to end, tests first, and come back to fix
17318
17319
  things when redirected — a failing check, or reviewer feedback. Stay
17319
17320
  strictly inside that task; never touch code outside it. Treat feedback as the
17320
17321
  diagnosis to act on; discard it, with reason, only when simply wrong.`;
17322
+ const testerPersona = `You are the hands-on tester in gtd's build pipeline: use what was just
17323
+ built the way a person would — run the real program, open the real
17324
+ page, call the real endpoint — and report exactly what you did and
17325
+ what you saw. Never fix anything yourself, and never pass a criterion
17326
+ you did not watch hold.`;
17321
17327
  const finisherPersona = `You are the closing identity in gtd's build pipeline — the last coder
17322
17328
  before it closes. Fix a late-breaking failing check after sign-off,
17323
17329
  focused and minimal. Every turn lands its own commit — nothing here
@@ -17743,8 +17749,11 @@ const acceptanceCriteriaBlock = `
17743
17749
  Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
17744
17750
  \`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
17745
17751
  per criterion, numbered from \`AC-1\`. They bind every later step —
17746
- the build is done when each one holds — so each must be checkable: a
17747
- test or a command shows it holds or not. A later lap keeps every ID:
17752
+ the build is done when each one holds — so each must be behaviour a
17753
+ test can pin: one that fails when it does not hold. A green suite, a
17754
+ passing typecheck or lint, a generator or build that runs — the health
17755
+ check enforces those on every lap, so none is a criterion; fold what it
17756
+ must produce into a behaviour a test can observe. A later lap keeps every ID:
17748
17757
  reword a criterion in place, append a new one with the next unused
17749
17758
  number, delete a dropped one — never renumber, never reuse an ID. When
17750
17759
  the file holds no list (a review loop-back rewrites it), recover the
@@ -17852,7 +17861,7 @@ const clearCriteriaQuestion = {
17852
17861
  id: "clear",
17853
17862
  primitive: "noul",
17854
17863
  instructions: "state.criteria lists the acceptance criteria the human's sketch (state.sketch) stated, as gtd numbered them. Decide whether they can stand as the binding definition of done, with no design conversation.",
17855
- 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."
17864
+ criteria: "yes: every criterion is clear behaviour a test can pin — a test would fail if it did not hold — and together they say what to build. no: any one criterion is vague, subjective or untestable, only a command can check it (a suite, typecheck, lint, build or generator run that passes), or the sketch leaves an open product question. Unsure means no."
17856
17865
  };
17857
17866
  const clearCriteriaMessage = () => `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
17858
17867
  pipe \`yes\` when every one is clear and checkable, so design is skipped and
@@ -18771,6 +18780,80 @@ Two ways forward (then run \`gtd land\`):
18771
18780
  - **Overrule** — land a clean tree to finish without them. The slice records
18772
18781
  the IDs it covers and, as \`uncovered\`, the ones it does not
18773
18782
  `;
18783
+ const testerSystem = () => `${testerPersona}
18784
+
18785
+ ${agentConduct}`;
18786
+ const PROTOTYPE_FORMAT = `- Write \`.gtd/PROTOTYPE.md\`, replacing any earlier report: one \`## \` chunk
18787
+ per criterion, headed \`## AC-<n>: pass\`, \`## AC-<n>: fail\` or
18788
+ \`## AC-<n>: blocked\`, then one chunk per problem you found outside any
18789
+ criterion, headed \`## Problem <short title>: fail\` (\`: pass\` once a
18790
+ re-test shows it fixed). Under each: the exact steps you ran, what you
18791
+ observed (output, a screenshot path, the error), and for a failure what
18792
+ you expected instead
18793
+ - \`blocked\` is for a criterion you could not exercise, or one whose
18794
+ expected behaviour itself looks wrong — say which and why: a person
18795
+ decides it, never a fixer`;
18796
+ const prototypeTestingPrompt = (slice, retest) => `${stateFileRules}
18797
+ - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18798
+ ${sliceLine(slice)}- The change is built and its suite is green. Now test it by hand: use
18799
+ every acceptance criterion the way a person would and watch what
18800
+ happens. Running the test suite is not this — it already ran
18801
+ - Find how this project is exercised for real, in this order: a project
18802
+ skill that launches or drives it (e.g. \`.claude/skills/*\`); a manual
18803
+ testing or running section in \`AGENTS.md\`, \`CLAUDE.md\` or \`README.md\`;
18804
+ failing those, the project's shape — a browser or Playwright for a web
18805
+ project, the built CLI in a scratch directory for a CLI, a request client
18806
+ for an API
18807
+ - Found none and recognised no shape: write only a chunk headed
18808
+ \`## No testing instructions\` saying what you looked for, and finish —
18809
+ a person is asked; never guess
18810
+ - Test in a disposable place only — a scratch directory outside this
18811
+ repository, a throwaway repository, a dev server on a free port — never
18812
+ against the user's real data or this working tree. Point every tool's
18813
+ output (screenshots, traces, test results) outside the repository too
18814
+ ${retest ? `- This is a re-test: \`.gtd/PROTOTYPE.md\` holds the last round's report and
18815
+ a fix turn or a person has changed the code since. Re-test every failed or blocked criterion and
18816
+ every problem it lists in full, then re-run the passed ones quickly
18817
+ ` : ""}${PROTOTYPE_FORMAT}
18818
+ - Change nothing but \`.gtd/PROTOTYPE.md\` — a turn that does is refused
18819
+ - Leave everything uncommitted and finish your turn — do not commit
18820
+ `;
18821
+ const prototypeFixingPrompt = (slice) => `${stateFileRules}
18822
+ - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18823
+ ${sliceLine(slice)}${criteriaBinding(true)}
18824
+ - Read \`.gtd/PROTOTYPE.md\`: a hands-on test of the change. Fix every
18825
+ \`fail\` chunk of this change's criteria${slice === void 0 ? "" : " (this slice's only)"} or a problem found
18826
+ beside them, at its cause, with a test that shows it; leave \`blocked\`
18827
+ ones to the person
18828
+ - A \`fail\` the code does not explain (the tester misread what it saw) has
18829
+ nothing to fix: leave it and change nothing for it — the next round re-tests
18830
+ - Leave \`.gtd/PROTOTYPE.md\` as it is: the next round re-tests from it
18831
+ - Run \`${env.testCommand}\` and finish only once it is green
18832
+ - Leave everything uncommitted and finish your turn — do not commit
18833
+ `;
18834
+ const openItems = (items) => items.map((item) => `- ${item.subject}: ${item.verdict}`).join("\n");
18835
+ const prototypeOpenMessage = (items, rounds) => `The hands-on test of this slice still has open items${rounds > 0 ? ` after ${rounds} fix round${rounds === 1 ? "" : "s"}` : ", and none a fixer can take"}:
18836
+ ${openItems(items)}
18837
+
18838
+ \`.gtd/PROTOTYPE.md\` holds the last report: the steps run and what was seen.
18839
+
18840
+ Two ways forward (then run \`gtd land\`):
18841
+ - **Fix** — change the code, or the testing instructions, yourself (the
18842
+ criteria are settled: an edit to them is refused); the suite runs and the
18843
+ slice is tested again, with a fresh round budget
18844
+ - **Overrule** — land a clean tree to go on to the quality lap with these
18845
+ items open
18846
+ `;
18847
+ const prototypeUnguidedMessage = () => `The hands-on test found no way to exercise this project for real — no
18848
+ project skill, no manual testing section in \`AGENTS.md\`, \`CLAUDE.md\` or
18849
+ \`README.md\`, and no project shape it recognised. \`.gtd/PROTOTYPE.md\` says
18850
+ what it looked for.
18851
+
18852
+ Two ways forward (then run \`gtd land\`):
18853
+ - **Instruct** — add testing instructions (a project skill, or a manual
18854
+ testing section); the slice is tested again
18855
+ - **Skip** — land a clean tree to go on to the quality lap untested
18856
+ `;
18774
18857
  //#endregion
18775
18858
  //#region src/workflows/shared/steps.ts
18776
18859
  const FEEDBACK = ".gtd/FEEDBACK.md";
@@ -18782,6 +18865,7 @@ const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
18782
18865
  const QUALITY = ".gtd/QUALITY.md";
18783
18866
  const SLICES = ".gtd/SLICES.md";
18784
18867
  const HANDOFF = ".gtd/HANDOFF.md";
18868
+ const PROTOTYPE = ".gtd/PROTOTYPE.md";
18785
18869
  const planner = () => env.plannerModel ?? "";
18786
18870
  const coder = () => env.coderModel ?? "";
18787
18871
  const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
@@ -38499,6 +38583,7 @@ const ShippedOptions = Struct({
38499
38583
  qualityReviews: setting$1,
38500
38584
  refactorQualityReviews: setting$1,
38501
38585
  continuations: setting$1,
38586
+ prototypeRounds: setting$1,
38502
38587
  continuationStall: setting$1,
38503
38588
  testCommand: setting$1,
38504
38589
  plannerModel: setting$1,
@@ -39748,7 +39833,9 @@ const reviewedRefactor = async (shape, options = {}) => {
39748
39833
  feedback,
39749
39834
  fixAccess: strayAccess(p.strays),
39750
39835
  lensBase: entry,
39751
- rules: refactorRules(p.strays, p.pattern)
39836
+ rules: refactorRules(p.strays, p.pattern),
39837
+ walkthrough: true,
39838
+ lap: "refactor"
39752
39839
  });
39753
39840
  if (outcome.verdict === "signoff") return true;
39754
39841
  await reUnwind(outcome);
@@ -39763,7 +39850,7 @@ const reviewedRefactor = async (shape, options = {}) => {
39763
39850
  const COVERED_MIN_P = .7;
39764
39851
  /** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
39765
39852
  const COVERING_RETRIES = 2;
39766
- const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
39853
+ const frozen$1 = () => requireCriteriaFrozen(REQUIREMENTS);
39767
39854
  const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
39768
39855
  label: "Fixing the check",
39769
39856
  file: FEEDBACK,
@@ -39771,7 +39858,7 @@ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", imple
39771
39858
  system: builderSystem()
39772
39859
  }), {
39773
39860
  guard: () => {
39774
- frozen();
39861
+ frozen$1();
39775
39862
  guard?.();
39776
39863
  },
39777
39864
  guardRests: false
@@ -39824,7 +39911,7 @@ const requireCoverage = async (base = start(), slice) => {
39824
39911
  system: builderSystem(),
39825
39912
  allowEmpty: true
39826
39913
  });
39827
- frozen();
39914
+ frozen$1();
39828
39915
  } else {
39829
39916
  await human("uncovered", {
39830
39917
  message: uncoveredMessage(uncovered),
@@ -39833,13 +39920,136 @@ const requireCoverage = async (base = start(), slice) => {
39833
39920
  acceptClean: true
39834
39921
  });
39835
39922
  if (!changes().some((c) => isCode$1(c.path))) return record(covered, uncovered);
39836
- frozen();
39923
+ frozen$1();
39837
39924
  retries = 0;
39838
39925
  }
39839
39926
  await green();
39840
39927
  }
39841
39928
  };
39842
39929
  //#endregion
39930
+ //#region src/workflows/feature/prototype.ts
39931
+ /** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
39932
+ const prototypeRounds = () => {
39933
+ const value = vars.prototypeRounds ?? defaults.prototypeRounds;
39934
+ if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
39935
+ return Number(value);
39936
+ };
39937
+ const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
39938
+ const UNGUIDED = /^no testing instructions$/i;
39939
+ /** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
39940
+ const reportOf = (text) => {
39941
+ let unguided = false;
39942
+ const items = [];
39943
+ const malformed = [];
39944
+ for (const { line, fenced } of markFenced(text)) {
39945
+ const heading = fenced ? null : HEADING.exec(line);
39946
+ if (heading?.[1] !== "##") continue;
39947
+ if (UNGUIDED.test(heading[2])) unguided = true;
39948
+ const chunk = CHUNK.exec(heading[2]);
39949
+ if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
39950
+ if (chunk !== null) items.push({
39951
+ subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
39952
+ verdict: chunk[2].toLowerCase()
39953
+ });
39954
+ }
39955
+ return {
39956
+ unguided,
39957
+ items,
39958
+ malformed
39959
+ };
39960
+ };
39961
+ /**
39962
+ * The report the testing turn left, refused unless it is the only change and
39963
+ * names every criterion once, with its open items: another slice's criteria
39964
+ * are not this slice's to fix.
39965
+ */
39966
+ const requireReport = (slice, retest) => {
39967
+ const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
39968
+ if (others.length > 0) refuse(`gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`);
39969
+ const text = read(PROTOTYPE);
39970
+ if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
39971
+ const report = reportOf(text);
39972
+ if (report.malformed.length > 0) refuse(`gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`);
39973
+ if (report.unguided && report.items.length === 0 && !retest) return {
39974
+ unguided: true,
39975
+ open: []
39976
+ };
39977
+ const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
39978
+ const ids = criterionIds(requirements, slice);
39979
+ const elsewhere = criterionIds(requirements).filter((id) => !ids.includes(id));
39980
+ const count = (id) => report.items.filter((i) => i.subject === id).length;
39981
+ const wrong = ids.filter((id) => count(id) !== 1);
39982
+ if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
39983
+ return {
39984
+ unguided: false,
39985
+ open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
39986
+ };
39987
+ };
39988
+ const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
39989
+ const testing = async (slice, retest) => {
39990
+ await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), retest), {
39991
+ label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
39992
+ file: PROTOTYPE,
39993
+ model: coder(),
39994
+ system: testerSystem(),
39995
+ allowEmpty: true
39996
+ });
39997
+ return requireReport(slice, retest);
39998
+ };
39999
+ const fixing = async (slice, guard) => {
40000
+ await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
40001
+ label: "Fixing what the hands-on test found",
40002
+ file: PROTOTYPE,
40003
+ model: coder(),
40004
+ system: builderSystem(),
40005
+ allowEmpty: true
40006
+ }));
40007
+ if (changes().some((c) => c.path === ".gtd/PROTOTYPE.md")) refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`);
40008
+ frozen();
40009
+ guard?.();
40010
+ };
40011
+ /** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
40012
+ const askPerson = async (name, message, label) => {
40013
+ await human(name, {
40014
+ message,
40015
+ label,
40016
+ file: PROTOTYPE,
40017
+ acceptClean: true
40018
+ });
40019
+ if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
40020
+ frozen();
40021
+ return true;
40022
+ };
40023
+ /**
40024
+ * Use the slice by hand: a testing turn reports on every criterion, a fix turn
40025
+ * takes each failure, and the slice is tested again — until the report is
40026
+ * clean, or `prototypeRounds` fixes are spent and a person decides. `guard`
40027
+ * holds every fix turn, as it holds the implementation's.
40028
+ */
40029
+ const prototypeTest = async (slice, guard) => {
40030
+ const cap = prototypeRounds();
40031
+ if (cap === 0) return;
40032
+ await scope("prototype", async () => {
40033
+ let fixes = 0;
40034
+ let retest = false;
40035
+ for (;;) {
40036
+ const { unguided, open } = await testing(slice, retest);
40037
+ retest = !unguided;
40038
+ if (unguided) {
40039
+ if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
40040
+ } else if (open.length === 0) return;
40041
+ else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
40042
+ fixes++;
40043
+ await fixing(slice, guard);
40044
+ } else {
40045
+ if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
40046
+ fixes = 0;
40047
+ }
40048
+ await green(guard);
40049
+ }
40050
+ });
40051
+ };
40052
+ //#endregion
39843
40053
  //#region src/workflows/feature/seam.ts
39844
40054
  /** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
39845
40055
  const NAIVE_RETRIES = 2;
@@ -40046,8 +40256,10 @@ const implementing = (options) => scope("implement", async () => {
40046
40256
  ...acceptance,
40047
40257
  ids: settled(acceptance.ids, options.slice)
40048
40258
  }, since);
40049
- await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
40259
+ const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
40260
+ await green(keepsTests);
40050
40261
  await requireCoverage(options.base, options.slice);
40262
+ await prototypeTest(options.slice, keepsTests);
40051
40263
  const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
40052
40264
  await closing(moreToBuild(options.more));
40053
40265
  return ids;
@@ -40056,11 +40268,16 @@ const moreToBuild = (more) => typeof more === "function" ? more() : more === tru
40056
40268
  /** The last slice's closing, for a last slice a steer left with no criterion to build. */
40057
40269
  const closeOut = () => scope("implement", () => closing(false));
40058
40270
  /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
40059
- const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [
40271
+ const closing = (more) => run("closing", removeScript(more ? [
40272
+ REFACTOR_NOTE,
40273
+ HANDOFF,
40274
+ PROTOTYPE
40275
+ ] : [
40060
40276
  REQUIREMENTS,
40061
40277
  REFACTOR_NOTE,
40062
40278
  SLICES,
40063
- HANDOFF
40279
+ HANDOFF,
40280
+ PROTOTYPE
40064
40281
  ]), { label: "Closing out the implementation" });
40065
40282
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
40066
40283
  const steer = async (retry) => {
@@ -40425,6 +40642,7 @@ const ordinaryStart = async () => {
40425
40642
  refusePendingHandOff();
40426
40643
  flagModeOn();
40427
40644
  sliceCap();
40645
+ prototypeRounds();
40428
40646
  const stated = statedCriteria(changes(), head$1());
40429
40647
  await unwind();
40430
40648
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
@@ -40459,6 +40677,10 @@ const skills = (vars) => {
40459
40677
  seam: ["incremental-implementation"],
40460
40678
  implement: ["test-driven-development", "incremental-implementation"],
40461
40679
  "implement.fix.suite": ["debugging-and-error-recovery"],
40680
+ "implement.prototype": [],
40681
+ "implement.prototype.fix": ["test-driven-development", "incremental-implementation"],
40682
+ "implement.prototype.fix.suite": ["debugging-and-error-recovery"],
40683
+ "implement.prototype.health": ["debugging-and-error-recovery"],
40462
40684
  "implement.health": ["debugging-and-error-recovery"],
40463
40685
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens]),
40464
40686
  ...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
@@ -40636,4 +40858,4 @@ var workflows_default = feature;
40636
40858
  */
40637
40859
  const doors = {};
40638
40860
  //#endregion
40639
- export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
40861
+ export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, PROTOTYPE, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "50.0.0",
3
+ "version": "51.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -9,6 +9,7 @@ Feature: a feature is done only when every acceptance criterion has a passing te
9
9
 
10
10
  Background:
11
11
  Given the shipped workflows, as gtd install scaffolds them
12
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
12
13
  And an environment variable "GTD_QUALITYREVIEWS" set to ""
13
14
  And an environment variable "GTD_FLAGMODE" set to "false"
14
15
  And a file "src/calc.ts" with:
@@ -8,6 +8,7 @@ Feature: a trivial change takes the same few beats at either risk tier
8
8
 
9
9
  Background:
10
10
  Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
11
12
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
12
13
  And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
13
14
  And a file "src/greeting.ts" with:
@@ -28,6 +28,7 @@ import {
28
28
  } from "./criteria.js"
29
29
  import { design } from "./planning.js"
30
30
  import { buildTail, humanReview, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
31
+ import { prototypeRounds } from "./prototype.js"
31
32
  import { implement, implementing, seamBuild } from "./seam.js"
32
33
  import { sliceCap, sliceCriteria, slicing } from "./slicing.js"
33
34
  import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
@@ -36,7 +37,8 @@ import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
36
37
  // `unwind` reverts the sketch (its intent survives in history) → a
37
38
  // green-baseline gate → design → per slice of the criteria, a naive pass,
38
39
  // judged with `flagMode` on: it lands as the implementation, or (refactoring
39
- // first when the judge asks) is built fresh from → the health loop → the
40
+ // first when the judge asks) is built fresh from → the health loop → a
41
+ // hands-on test of every criterion, fixed and re-tested → the
40
42
  // triaged quality lap, whose leftovers are carried on → after the last slice,
41
43
  // a reviewer's walkthrough of the whole change, every slice's findings
42
44
  // among its chunks, for the human, which
@@ -227,6 +229,7 @@ export const ordinaryStart = async (): Promise<void> => {
227
229
  // Read right after idle, so a bad value refuses the landing that would pin it.
228
230
  flagModeOn()
229
231
  sliceCap()
232
+ prototypeRounds()
230
233
  const stated = statedCriteria(changes(), head())
231
234
  await unwind()
232
235
  if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
@@ -11,7 +11,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
11
11
  """
12
12
  import { feature } from "@pmelab/gtd/workflows/feature"
13
13
  export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
14
- export default feature({ flagMode: false })
14
+ export default feature({ flagMode: false, prototypeRounds: "0" })
15
15
  """
16
16
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
17
17
  And a file "src/calc.ts" with:
@@ -12,6 +12,7 @@ Feature: each quality lens runs on its own model hint
12
12
  export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
13
13
  export default feature({
14
14
  flagMode: false,
15
+ prototypeRounds: "0",
15
16
  qualityReviews: "correctness, conventions, test-audit=opus",
16
17
  })
17
18
  """
@@ -9,6 +9,7 @@ Feature: the quality lap's findings are triaged before the review
9
9
 
10
10
  Background:
11
11
  Given the shipped workflows, as gtd install scaffolds them
12
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
12
13
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
13
14
  And an environment variable "GTD_FLAGMODE" set to "false"
14
15
  And a file "src/calc.ts" with:
@@ -10,6 +10,7 @@ Feature: a naive pass the judge rules land is itself the implementation
10
10
 
11
11
  Background:
12
12
  Given the shipped workflows, as gtd install scaffolds them
13
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
13
14
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
14
15
  And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
15
16
  And a file "src/greeting.ts" with:
@@ -0,0 +1,152 @@
1
+ @inmem
2
+ Feature: past its round cap the hands-on test rests at a person's gate
3
+
4
+ `prototypeRounds` caps the fix turns a slice's hands-on test gets. Once they
5
+ are spent and the report still has open items, the process rests at
6
+ implement.prototype.open naming them; a person's fix there is tested again
7
+ with a fresh budget.
8
+
9
+ Background:
10
+ Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
12
+ And an environment variable "GTD_FLAGMODE" set to "false"
13
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "1"
14
+ And a file "src/calc.ts" with:
15
+ """
16
+ export const add = (a: number, b: number) => a + b
17
+ """
18
+ And the working tree is committed as "feat: add calculator"
19
+ And a file "TODO.md" with:
20
+ """
21
+ # Add subtract
22
+
23
+ subtract(a, b) returns the difference.
24
+ """
25
+ And step "unwind" changes nothing
26
+ And step "start-gate.check" changes nothing
27
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
28
+ """
29
+ # Requirements
30
+
31
+ ## Acceptance Criteria
32
+
33
+ - AC-1: subtract(a, b) returns a - b.
34
+ - AC-2: add is unchanged.
35
+ """
36
+ And step "design.gate.answer" changes nothing
37
+ And step "seam.naive" writes "src/calc.ts" with:
38
+ """
39
+ export const add = (a: number, b: number) => a + b
40
+ export const subtract = (a: number, b: number) => a + b
41
+ """
42
+ And step "seam.naive" writes "src/calc.test.ts" with:
43
+ """
44
+ it("[AC-1] subtracts", () => expect(subtract(3, 1)).toBe(2))
45
+ it("[AC-2] adds", () => expect(add(1, 1)).toBe(2))
46
+ """
47
+ And step "seam.check" changes nothing
48
+ And step "implement.applying" runs its script
49
+ And step "implement.health.check" changes nothing
50
+ And judge step "implement.coverage" answers:
51
+ """
52
+ [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
53
+ """
54
+ And step "implement.closing" runs its script
55
+
56
+ Scenario: past the round cap a person is asked, naming the open items
57
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
58
+ """
59
+ ## AC-1: fail
60
+
61
+ `subtract(3, 1)` printed 4. Expected 2.
62
+
63
+ ## AC-2: pass
64
+
65
+ Printed 2.
66
+ """
67
+ And step "implement.prototype.testing", when prompted with "This is a re-test", writes ".gtd/PROTOTYPE.md" with:
68
+ """
69
+ ## AC-1: fail
70
+
71
+ Re-tested: `subtract(3, 1)` printed -2. Expected 2.
72
+
73
+ ## AC-2: pass
74
+
75
+ Printed 2.
76
+ """
77
+ And step "implement.prototype.fix.fixing" writes "src/calc.ts" with:
78
+ """
79
+ export const add = (a: number, b: number) => a + b
80
+ export const subtract = (a: number, b: number) => b - a
81
+ """
82
+ And step "implement.prototype.health.check" changes nothing
83
+ When the process is driven
84
+ Then the process rests at "implement.prototype.open"
85
+ And the resting step's content contains "- AC-1: fail"
86
+ And the resting step's content contains "after 1 fix round"
87
+ And the resting step's content does not contain "AC-2"
88
+
89
+ Scenario: a person's change at the gate is tested again
90
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
91
+ """
92
+ ## AC-1: blocked
93
+
94
+ `subtract` is not exported from any entry point I could run.
95
+
96
+ ## AC-2: pass
97
+
98
+ Printed 2.
99
+ """
100
+ And step "implement.prototype.open" writes "src/calc.ts" with:
101
+ """
102
+ export const add = (a: number, b: number) => a + b
103
+ export const subtract = (a: number, b: number) => a - b
104
+ """
105
+ And step "implement.prototype.health.check" changes nothing
106
+ And step "implement.prototype.testing", when prompted with "This is a re-test", writes ".gtd/PROTOTYPE.md" with:
107
+ """
108
+ ## AC-1: pass
109
+
110
+ `subtract(3, 1)` printed 2.
111
+
112
+ ## AC-2: pass
113
+
114
+ Printed 2.
115
+ """
116
+ When the process is driven
117
+ Then the process rests at "build.quality.correctness.reviewing"
118
+ And the commit subjects end with:
119
+ """
120
+ gtd(agent): implement.prototype.testing → implement.prototype.open
121
+ gtd(human): implement.prototype.open → implement.prototype.health.check
122
+ gtd(check): implement.prototype.health.check → implement.prototype.testing
123
+ gtd(agent): implement.prototype.testing → implement.closing
124
+ gtd(check): implement.closing → build.quality.correctness.reviewing
125
+ """
126
+
127
+ Scenario: a re-test that sees exactly what the last round saw still ends the round
128
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
129
+ """
130
+ ## AC-1: fail
131
+
132
+ `subtract(3, 1)` printed 4. Expected 2.
133
+
134
+ ## AC-2: pass
135
+
136
+ Printed 2.
137
+ """
138
+ And step "implement.prototype.fix.fixing" writes "src/calc.ts" with:
139
+ """
140
+ export const add = (a: number, b: number) => a + b
141
+ export const subtract = (a: number, b: number) => b - a
142
+ """
143
+ And step "implement.prototype.health.check" changes nothing
144
+ When the process is driven
145
+ Then the process rests at "implement.prototype.open"
146
+ And the commit subjects end with:
147
+ """
148
+ gtd(agent): implement.prototype.testing → implement.prototype.fix.fixing
149
+ gtd(agent): implement.prototype.fix.fixing → implement.prototype.health.check
150
+ gtd(check): implement.prototype.health.check → implement.prototype.testing
151
+ gtd(agent): implement.prototype.testing → implement.prototype.open
152
+ """