@pmelab/gtd 50.0.0 → 51.0.1

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 (55) 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/gtd.bundle.mjs +26 -25
  7. package/dist/testkit.mjs +26 -25
  8. package/dist/workflows/cleanup.mjs +2 -2
  9. package/dist/workflows/feature.mjs +144 -11
  10. package/dist/workflows/fix.mjs +2 -2
  11. package/dist/workflows/review.mjs +3 -3
  12. package/dist/workflows/rollout.mjs +1 -1
  13. package/dist/workflows.mjs +234 -12
  14. package/package.json +1 -1
  15. package/src/edge/Edge.ts +1 -30
  16. package/src/replay/Replay.test.ts +4 -8
  17. package/src/replay/Replay.ts +35 -3
  18. package/src/replay/index.ts +7 -1
  19. package/src/workflows/feature/coverage.feature +1 -0
  20. package/src/workflows/feature/fast-path.feature +1 -0
  21. package/src/workflows/feature/feature.ts +4 -1
  22. package/src/workflows/feature/flag-mode.feature +1 -1
  23. package/src/workflows/feature/lens-models.feature +1 -0
  24. package/src/workflows/feature/lens-review.feature +1 -0
  25. package/src/workflows/feature/one-step-land.feature +1 -0
  26. package/src/workflows/feature/prototype-rounds.feature +152 -0
  27. package/src/workflows/feature/prototype.feature +419 -0
  28. package/src/workflows/feature/prototype.test.ts +43 -0
  29. package/src/workflows/feature/prototype.ts +181 -0
  30. package/src/workflows/feature/refactor-absorbs.feature +9 -1
  31. package/src/workflows/feature/refactor-first.feature +13 -3
  32. package/src/workflows/feature/refactor-rebuild.feature +29 -5
  33. package/src/workflows/feature/refactor-review.feature +111 -13
  34. package/src/workflows/feature/refactor.test.ts +14 -4
  35. package/src/workflows/feature/refactor.ts +2 -0
  36. package/src/workflows/feature/review-answers.feature +1 -0
  37. package/src/workflows/feature/review-gate.feature +1 -0
  38. package/src/workflows/feature/reviewer-walkthrough.feature +1 -0
  39. package/src/workflows/feature/seam-rejection.feature +1 -0
  40. package/src/workflows/feature/seam.feature +1 -0
  41. package/src/workflows/feature/seam.test.ts +3 -2
  42. package/src/workflows/feature/seam.ts +19 -5
  43. package/src/workflows/feature/severity-budget.feature +1 -0
  44. package/src/workflows/feature/severity-sorted-review.feature +1 -0
  45. package/src/workflows/feature/slice-findings.feature +1 -0
  46. package/src/workflows/feature/slicing-cap.feature +9 -0
  47. package/src/workflows/feature/slicing.feature +1 -0
  48. package/src/workflows/feature/standards.feature +1 -0
  49. package/src/workflows/shared/options.ts +1 -0
  50. package/src/workflows/shared/prose.ts +6 -0
  51. package/src/workflows/shared/skills.test.ts +4 -0
  52. package/src/workflows/shared/skills.ts +5 -0
  53. package/src/workflows/shared/steps.ts +1 -0
  54. package/src/workflows/shared/text.ts +104 -3
  55. package/src/workflows/shared/vars.ts +3 -0
@@ -1,8 +1,8 @@
1
1
  import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf } from "../workflow-kit.mjs";
3
- import { $ as naiveApplyFailure, At as defaults, B as coverageMessage, C as collecting, Ct as uncoveredMessage, Et as unwindFailure, F as builderSystem, G as designSystem, H as coveringPrompt, K as designTriagePrompt, L as clearCriteriaMessage, M as agentWithSkills, Mt as testFileGlobs, N as buildReviewCollectingPrompt, O as planner, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, T as fixNits, Tt as unwindFailedMessage, U as criterionLine, V as coverageQuestion, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REQUIREMENTS, _t as reviewerSystem, a as flagModeOn, b as answerReviewQuestions, bt as slicingPrompt, ct as refactorFixPrompt, d as healthy, et as naivePassPrompt, f as FEEDBACK, g as REFACTOR_NOTE, h as REFACTOR, i as skills, jt as envDefaults, l as gate, lt as refactorNotePrompt, n as summary, nt as oversizedMessage, ot as refactorBuildingPrompt, p as HANDOFF, r as access, s as shipped, st as refactorDroppedRefusal, t as steering, tt as normaliseCriteriaScript, ut as refactorRules, vt as seamByHandMessage, xt as startGateBlockedMessage, y as SLICES, yt as seamSteerMessage } from "../chunks/reserved-CdOzKFVX.mjs";
4
- import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-DRM2eC1z.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BChXEl2Y.mjs";
3
+ import { $ as implementFixPrompt, C as coder, Ct as seamByHandMessage, Dt as testerSystem, E as fixNits, Et as startGateBlockedMessage, G as designGateAnswerMessage, H as coverageQuestion, I as builderSystem, It as defaults, K as designSystem, Lt as envDefaults, Mt as unwindFailure, N as agentWithSkills, P as buildReviewCollectingPrompt, Q as implementBuildingPrompt, R as clearCriteriaMessage, Rt as testFileGlobs, St as reviewerSystem, Tt as slicingPrompt, U as coveringPrompt, V as coverageMessage, W as criterionLine, Z as idleMessage, _ as REFACTOR_NOTE, a as flagModeOn, at as prototypeOpenMessage, b as SLICES, d as healthy, dt as refactorBuildingPrompt, et as naiveApplyFailure, f as FEEDBACK, ft as refactorDroppedRefusal, g as REFACTOR, ht as refactorRules, i as skills, it as prototypeFixingPrompt, jt as unwindFailedMessage, k as planner, kt as uncoveredMessage, l as gate, m as PROTOTYPE, mt as refactorNotePrompt, n as summary, nt as normaliseCriteriaScript, ot as prototypeTestingPrompt, p as HANDOFF, pt as refactorFixPrompt, q as designTriagePrompt, r as access, rt as oversizedMessage, s as shipped, st as prototypeUnguidedMessage, t as steering, tt as naivePassPrompt, v as REQUIREMENTS, w as collecting, wt as seamSteerMessage, x as answerReviewQuestions, z as clearCriteriaQuestion } from "../chunks/reserved-CUkk9XgL.mjs";
4
+ import { a as refusePendingHandOff, c as joinReports, o as tail } from "../chunks/tail-CX6Pdsql.mjs";
5
+ import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-BaCXlIT2.mjs";
6
6
  import { posix } from "node:path";
7
7
  //#region src/workflows/shared/fences.ts
8
8
  const markFenced = (text) => {
@@ -561,7 +561,9 @@ const reviewedRefactor = async (shape, options = {}) => {
561
561
  feedback,
562
562
  fixAccess: strayAccess(p.strays),
563
563
  lensBase: entry,
564
- rules: refactorRules(p.strays, p.pattern)
564
+ rules: refactorRules(p.strays, p.pattern),
565
+ walkthrough: true,
566
+ lap: "refactor"
565
567
  });
566
568
  if (outcome.verdict === "signoff") return true;
567
569
  await reUnwind(outcome);
@@ -576,7 +578,7 @@ const reviewedRefactor = async (shape, options = {}) => {
576
578
  const COVERED_MIN_P = .7;
577
579
  /** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
578
580
  const COVERING_RETRIES = 2;
579
- const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
581
+ const frozen$1 = () => requireCriteriaFrozen(REQUIREMENTS);
580
582
  const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
581
583
  label: "Fixing the check",
582
584
  file: FEEDBACK,
@@ -584,7 +586,7 @@ const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", imple
584
586
  system: builderSystem()
585
587
  }), {
586
588
  guard: () => {
587
- frozen();
589
+ frozen$1();
588
590
  guard?.();
589
591
  },
590
592
  guardRests: false
@@ -637,7 +639,7 @@ const requireCoverage = async (base = start(), slice) => {
637
639
  system: builderSystem(),
638
640
  allowEmpty: true
639
641
  });
640
- frozen();
642
+ frozen$1();
641
643
  } else {
642
644
  await human("uncovered", {
643
645
  message: uncoveredMessage(uncovered),
@@ -646,13 +648,136 @@ const requireCoverage = async (base = start(), slice) => {
646
648
  acceptClean: true
647
649
  });
648
650
  if (!changes().some((c) => isCode$1(c.path))) return record(covered, uncovered);
649
- frozen();
651
+ frozen$1();
650
652
  retries = 0;
651
653
  }
652
654
  await green();
653
655
  }
654
656
  };
655
657
  //#endregion
658
+ //#region src/workflows/feature/prototype.ts
659
+ /** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
660
+ const prototypeRounds = () => {
661
+ const value = vars.prototypeRounds ?? defaults.prototypeRounds;
662
+ if (!/^(0|[1-9]\d*)$/.test(value)) refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`);
663
+ return Number(value);
664
+ };
665
+ const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i;
666
+ const UNGUIDED = /^no testing instructions$/i;
667
+ /** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
668
+ const reportOf = (text) => {
669
+ let unguided = false;
670
+ const items = [];
671
+ const malformed = [];
672
+ for (const { line, fenced } of markFenced(text)) {
673
+ const heading = fenced ? null : HEADING.exec(line);
674
+ if (heading?.[1] !== "##") continue;
675
+ if (UNGUIDED.test(heading[2])) unguided = true;
676
+ const chunk = CHUNK.exec(heading[2]);
677
+ if (chunk === null && /^(ac-\d|problem)/i.test(heading[2])) malformed.push(heading[2]);
678
+ if (chunk !== null) items.push({
679
+ subject: /^ac-/i.test(chunk[1]) ? chunk[1].toUpperCase() : chunk[1],
680
+ verdict: chunk[2].toLowerCase()
681
+ });
682
+ }
683
+ return {
684
+ unguided,
685
+ items,
686
+ malformed
687
+ };
688
+ };
689
+ /**
690
+ * The report the testing turn left, refused unless it is the only change and
691
+ * names every criterion once, with its open items: another slice's criteria
692
+ * are not this slice's to fix.
693
+ */
694
+ const requireReport = (slice, retest) => {
695
+ const others = changes().map((c) => c.path).filter((path) => path !== PROTOTYPE);
696
+ 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")}`);
697
+ const text = read(PROTOTYPE);
698
+ if (text === void 0) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`);
699
+ const report = reportOf(text);
700
+ 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")}`);
701
+ if (report.unguided && report.items.length === 0 && !retest) return {
702
+ unguided: true,
703
+ open: []
704
+ };
705
+ const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
706
+ const ids = criterionIds(requirements, slice);
707
+ const elsewhere = criterionIds(requirements).filter((id) => !ids.includes(id));
708
+ const count = (id) => report.items.filter((i) => i.subject === id).length;
709
+ const wrong = ids.filter((id) => count(id) !== 1);
710
+ if (wrong.length > 0) refuse(`gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`);
711
+ return {
712
+ unguided: false,
713
+ open: report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
714
+ };
715
+ };
716
+ const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
717
+ const testing = async (slice, retest) => {
718
+ await agentWithSkills("testing", prototypeTestingPrompt(sliceIds(slice), retest), {
719
+ label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
720
+ file: PROTOTYPE,
721
+ model: coder(),
722
+ system: testerSystem(),
723
+ allowEmpty: true
724
+ });
725
+ return requireReport(slice, retest);
726
+ };
727
+ const fixing = async (slice, guard) => {
728
+ await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(sliceIds(slice)), {
729
+ label: "Fixing what the hands-on test found",
730
+ file: PROTOTYPE,
731
+ model: coder(),
732
+ system: builderSystem(),
733
+ allowEmpty: true
734
+ }));
735
+ 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`);
736
+ frozen();
737
+ guard?.();
738
+ };
739
+ /** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
740
+ const askPerson = async (name, message, label) => {
741
+ await human(name, {
742
+ message,
743
+ label,
744
+ file: PROTOTYPE,
745
+ acceptClean: true
746
+ });
747
+ if (!changes().some((c) => c.path !== ".gtd/PROTOTYPE.md")) return false;
748
+ frozen();
749
+ return true;
750
+ };
751
+ /**
752
+ * Use the slice by hand: a testing turn reports on every criterion, a fix turn
753
+ * takes each failure, and the slice is tested again — until the report is
754
+ * clean, or `prototypeRounds` fixes are spent and a person decides. `guard`
755
+ * holds every fix turn, as it holds the implementation's.
756
+ */
757
+ const prototypeTest = async (slice, guard) => {
758
+ const cap = prototypeRounds();
759
+ if (cap === 0) return;
760
+ await scope("prototype", async () => {
761
+ let fixes = 0;
762
+ let retest = false;
763
+ for (;;) {
764
+ const { unguided, open } = await testing(slice, retest);
765
+ retest = !unguided;
766
+ if (unguided) {
767
+ if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
768
+ } else if (open.length === 0) return;
769
+ else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
770
+ fixes++;
771
+ await fixing(slice, guard);
772
+ } else {
773
+ if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
774
+ fixes = 0;
775
+ }
776
+ await green(guard);
777
+ }
778
+ });
779
+ };
780
+ //#endregion
656
781
  //#region src/workflows/feature/seam.ts
657
782
  /** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
658
783
  const NAIVE_RETRIES = 2;
@@ -859,8 +984,10 @@ const implementing = (options) => scope("implement", async () => {
859
984
  ...acceptance,
860
985
  ids: settled(acceptance.ids, options.slice)
861
986
  }, since);
862
- await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
987
+ const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
988
+ await green(keepsTests);
863
989
  await requireCoverage(options.base, options.slice);
990
+ await prototypeTest(options.slice, keepsTests);
864
991
  const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
865
992
  await closing(moreToBuild(options.more));
866
993
  return ids;
@@ -869,11 +996,16 @@ const moreToBuild = (more) => typeof more === "function" ? more() : more === tru
869
996
  /** The last slice's closing, for a last slice a steer left with no criterion to build. */
870
997
  const closeOut = () => scope("implement", () => closing(false));
871
998
  /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
872
- const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [
999
+ const closing = (more) => run("closing", removeScript(more ? [
1000
+ REFACTOR_NOTE,
1001
+ HANDOFF,
1002
+ PROTOTYPE
1003
+ ] : [
873
1004
  REQUIREMENTS,
874
1005
  REFACTOR_NOTE,
875
1006
  SLICES,
876
- HANDOFF
1007
+ HANDOFF,
1008
+ PROTOTYPE
877
1009
  ]), { label: "Closing out the implementation" });
878
1010
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
879
1011
  const steer = async (retry) => {
@@ -1238,6 +1370,7 @@ const ordinaryStart = async () => {
1238
1370
  refusePendingHandOff();
1239
1371
  flagModeOn();
1240
1372
  sliceCap();
1373
+ prototypeRounds();
1241
1374
  const stated = statedCriteria(changes(), head());
1242
1375
  await unwind();
1243
1376
  if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { _ as reviewRisks, g as reviewNotes } from "../chunks/steering-DtUVUz9l.mjs";
3
- import { At as defaults, J as fixReviewNotesPrompt, M as agentWithSkills, S as coder, c as baseline, i as skills, jt as envDefaults, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, v as REVIEW } from "../chunks/reserved-CdOzKFVX.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-DRM2eC1z.mjs";
3
+ import { C as coder, It as defaults, J as finisherSystem, Lt as envDefaults, N as agentWithSkills, Y as fixReviewNotesPrompt, c as baseline, i as skills, n as summary, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-CUkk9XgL.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CX6Pdsql.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, start } from "../flows.mjs";
2
- import { At as defaults, M as agentWithSkills, S as coder, _ as REQUIREMENTS, i as skills, jt as envDefaults, l as gate, mt as reviewGateBlockedMessage, n as summary, pt as reviewFeedbackFixPrompt, q as finisherSystem, r as access, s as shipped, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
3
- import { a as refusePendingHandOff } from "../chunks/tail-DRM2eC1z.mjs";
4
- import { i as reUnwind, t as buildTail } from "../chunks/review-BChXEl2Y.mjs";
2
+ import { C as coder, It as defaults, J as finisherSystem, Lt as envDefaults, N as agentWithSkills, i as skills, l as gate, n as summary, r as access, s as shipped, t as steering, v as REQUIREMENTS, vt as reviewFeedbackFixPrompt, yt as reviewGateBlockedMessage } from "../chunks/reserved-CUkk9XgL.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-CX6Pdsql.mjs";
4
+ import { i as reUnwind, t as buildTail } from "../chunks/review-BaCXlIT2.mjs";
5
5
  //#region src/workflows/review/review.ts
6
6
  const fixFeedback = async () => {
7
7
  await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { At as defaults, Ht as flagMentions, Kt as requireFlag, S as coder, Wt as flipProblems, f as FEEDBACK, i as skills, jt as envDefaults, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CdOzKFVX.mjs";
2
+ import { C as coder, It as defaults, Jt as flagMentions, Lt as envDefaults, Qt as requireFlag, Xt as flipProblems, f as FEEDBACK, i as skills, l as gate, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-CUkk9XgL.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
5
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
@@ -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.1",
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": {