@pmelab/gtd 38.0.0 → 38.2.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 (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +36 -3
  3. package/dist/chunks/{Schema-DO5ttJaI.mjs → Schema-C6fLlQzx.mjs} +1 -1
  4. package/dist/chunks/{reserved-sfMdy7v7.mjs → reserved-C3PgP7XB.mjs} +56 -12
  5. package/dist/chunks/{tail-Cgca5Gp-.mjs → tail-Bkxki2nK.mjs} +1 -1
  6. package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +292 -53
  8. package/dist/testkit.mjs +257 -21
  9. package/dist/wire.mjs +1 -1
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +158 -42
  12. package/dist/workflows/fix.mjs +2 -2
  13. package/dist/workflows/review.mjs +2 -2
  14. package/dist/workflows/rollout.mjs +1 -1
  15. package/dist/workflows.mjs +230 -70
  16. package/package.json +1 -1
  17. package/src/cli/Cli.test.ts +15 -3
  18. package/src/cli/Cli.ts +61 -28
  19. package/src/cli/History.ts +1 -19
  20. package/src/cli/Metrics.test.ts +224 -0
  21. package/src/cli/Metrics.ts +310 -0
  22. package/src/cli/Output.ts +13 -0
  23. package/src/cli/Program.test.ts +3 -1
  24. package/src/cli/Program.ts +4 -0
  25. package/src/testkit/steps.ts +4 -0
  26. package/src/wire/ContextLimit.ts +1 -1
  27. package/src/workflows/feature/continuation-cap.feature +65 -0
  28. package/src/workflows/feature/continuation-rebuild.feature +88 -0
  29. package/src/workflows/feature/continuation-slices.feature +64 -0
  30. package/src/workflows/feature/continuation.feature +205 -0
  31. package/src/workflows/feature/criteria.ts +11 -0
  32. package/src/workflows/feature/refactor.ts +64 -30
  33. package/src/workflows/feature/seam.ts +102 -30
  34. package/src/workflows/shared/continuation.ts +92 -0
  35. package/src/workflows/shared/options.ts +2 -0
  36. package/src/workflows/shared/steps.ts +1 -0
  37. package/src/workflows/shared/text.test.ts +6 -2
  38. package/src/workflows/shared/text.ts +104 -8
  39. package/src/workflows/shared/vars.ts +5 -0
@@ -2784,7 +2784,7 @@ const characterEntities = {
2784
2784
  };
2785
2785
  //#endregion
2786
2786
  //#region node_modules/decode-named-character-reference/index.js
2787
- const own$1 = {}.hasOwnProperty;
2787
+ const own$2 = {}.hasOwnProperty;
2788
2788
  /**
2789
2789
  * Decode a single character reference (without the `&` or `;`).
2790
2790
  * You probably only need this when you’re building parsers yourself that follow
@@ -2797,7 +2797,7 @@ const own$1 = {}.hasOwnProperty;
2797
2797
  * Decoded reference.
2798
2798
  */
2799
2799
  function decodeNamedCharacterReference(value) {
2800
- return own$1.call(characterEntities, value) ? characterEntities[value] : false;
2800
+ return own$2.call(characterEntities, value) ? characterEntities[value] : false;
2801
2801
  }
2802
2802
  //#endregion
2803
2803
  //#region node_modules/micromark-util-chunked/index.js
@@ -10261,7 +10261,7 @@ function index(value) {
10261
10261
  * Options
10262
10262
  * } from './types.js'
10263
10263
  */
10264
- const own = {}.hasOwnProperty;
10264
+ const own$1 = {}.hasOwnProperty;
10265
10265
  /**
10266
10266
  * Turn markdown into a syntax tree.
10267
10267
  *
@@ -10440,7 +10440,7 @@ function compiler(options) {
10440
10440
  index = -1;
10441
10441
  while (++index < events.length) {
10442
10442
  const handler = config[events[index][0]];
10443
- if (own.call(handler, events[index][1].type)) handler[events[index][1].type].call(Object.assign({ sliceSerialize: events[index][2].sliceSerialize }, context), events[index][1]);
10443
+ if (own$1.call(handler, events[index][1].type)) handler[events[index][1].type].call(Object.assign({ sliceSerialize: events[index][2].sliceSerialize }, context), events[index][1]);
10444
10444
  }
10445
10445
  if (context.tokenStack.length > 0) {
10446
10446
  const tail = context.tokenStack[context.tokenStack.length - 1];
@@ -11152,7 +11152,7 @@ function configure(combined, extensions) {
11152
11152
  function extension(combined, extension) {
11153
11153
  /** @type {keyof Extension} */
11154
11154
  let key;
11155
- for (key in extension) if (own.call(extension, key)) switch (key) {
11155
+ for (key in extension) if (own$1.call(extension, key)) switch (key) {
11156
11156
  case "canContainEols": {
11157
11157
  const right = extension[key];
11158
11158
  if (right) combined[key].push(...right);
@@ -16937,7 +16937,9 @@ const defaults = {
16937
16937
  sliceMaxCriteria: "8",
16938
16938
  flag_risk_tier: "0",
16939
16939
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
16940
- refactorQualityReviews: "correctness, ponytail-review, conventions"
16940
+ refactorQualityReviews: "correctness, ponytail-review, conventions",
16941
+ continuations: "3",
16942
+ continuationStall: "2"
16941
16943
  };
16942
16944
  /** Environment settings. */
16943
16945
  const envDefaults = {
@@ -17455,7 +17457,7 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
17455
17457
  - Introduce no new flag read and add no second read of an existing one:
17456
17458
  every match of \`${pattern}\` may move, never increase in number`;
17457
17459
  const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
17458
- const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
17460
+ const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false, from }) => `${stateFileRules}
17459
17461
  - The package to implement is \`${pkg}\`; never delete it
17460
17462
  ${note ? `- The package's \`## Refactor note\` section names the seams to open and the
17461
17463
  shape each takes, written from the whole naive pass: build exactly those
@@ -17470,8 +17472,9 @@ ${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${st
17470
17472
  restructuring afresh here, minus the feature's own behaviour. A
17471
17473
  refactor that touches none of it is wrong, however clean — never
17472
17474
  swap in an unrelated tidy-up
17473
- `}- Leave everything uncommitted and finish your turn — do not commit
17474
- `;
17475
+ `}${handoffRule()}
17476
+ - Leave everything uncommitted and finish your turn — do not commit
17477
+ ${from === void 0 ? "" : continuationLines(from)}`;
17475
17478
  const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
17476
17479
  ${fixFeedbackPrompt}
17477
17480
  ${refactorRules(strays, pattern)}
@@ -18039,7 +18042,7 @@ Print the closing message and stop — this writes nothing itself.
18039
18042
  const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
18040
18043
  const isRed = (retry) => "red" in retry;
18041
18044
  const isFault = (retry) => "fault" in retry;
18042
- const pickUp = (hash) => `Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``;
18045
+ const pickUp = (hash, exclude) => `Pick it up — \`git diff --binary --no-color --no-ext-diff --no-textconv ${hash}^ ${hash}${exclude === void 0 ? "" : ` -- ${quote(":(top)")} ${quote(`:(top,exclude)${exclude}`)}`} | git apply\``;
18043
18046
  const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
18044
18047
  the file, never from memory, and stop only when each one holds. They are
18045
18048
  settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
@@ -18058,7 +18061,33 @@ re-implement. It changed these files outside the expected set:
18058
18061
  ${listed(retry.strays)}
18059
18062
  Start over and choose a different approach; do not rebuild the same diff.
18060
18063
  `;
18061
- const naivePassPrompt = (retry, slice) => `${stateFileRules}
18064
+ const quoted = (text) => text.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n");
18065
+ /** Every turn a continuation may pick up tells its agent where the note goes. */
18066
+ const handoffRule = () => `- Should you be told to stop at a context limit, write the handoff note to
18067
+ \`.gtd/HANDOFF.md\` — what you touched, what was next — and finish your turn:
18068
+ a fresh turn picks up from what you leave, broken or not. That file is the one
18069
+ exception to any path rule here; write it at no other time`;
18070
+ /** `pickUp`: how the snapshot gets into the tree — absent when it already is. */
18071
+ const continuationLines = (from, pickUp) => `
18072
+ Your previous turn stopped at its context limit; this fresh turn picks up from
18073
+ its snapshot (\`${from.snapshot}\`). ${pickUp === void 0 ? "Its work is already in the tree." : `${pickUp}.`} Its handoff note:
18074
+
18075
+ ${quoted(from.note === "" ? "(empty)" : from.note)}
18076
+
18077
+ Still open: ${from.open.length === 0 ? "nothing the workflow can count — finish what the note says is next" : from.open.join(", ")}.
18078
+ ${pickUp === void 0 ? "Delete `.gtd/HANDOFF.md` once you are done; rewrite it should you be stopped again.\n" : ""}`;
18079
+ const oversizedMessage = (phase, from, why, narrowable = true) => `The ${phase} keeps outgrowing its context: ${"cap" in why ? why.cap === 0 ? "continuations are off (`continuations` is 0) and the turn stopped at its context limit" : `${why.cap} continuation${why.cap === 1 ? "" : "s"} ran and the last still stopped at its context limit` : `the last ${why.stall} continuation${why.stall === 1 ? "" : "s"} closed nothing still open`}. The last snapshot is \`${from.snapshot}\`; still open: ${from.open.length === 0 ? "nothing countable" : from.open.join(", ")}.
18080
+
18081
+ Its handoff note:
18082
+
18083
+ ${quoted(from.note === "" ? "(empty)" : from.note)}
18084
+
18085
+ Land to go on: a fresh round of continuations picks up from that snapshot${narrowable ? ` — narrow
18086
+ \`.gtd/REQUIREMENTS.md\` first if the change is too big, or raise the context limit` : ` — raise
18087
+ the context limit first if the refactor is too big`}. \`gtd abandon\` gives the change up.
18088
+ `;
18089
+ /** `from`: a continuation of a pass that stopped at its context limit, in place of `retry`'s pick-up. */
18090
+ const naivePassPrompt = (retry, slice, from) => `${stateFileRules}
18062
18091
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18063
18092
  ${sliceLine(slice)}${criteriaBinding(false)}
18064
18093
  - Implement them the quickest way that works — the naive version. Do not
@@ -18075,9 +18104,20 @@ ${sliceLine(slice)}${criteriaBinding(false)}
18075
18104
  - Run \`${env.testCommand}\` and finish only once it is green with every
18076
18105
  criterion's test passing: the next step runs it on your diff and sends a red
18077
18106
  pass back
18078
- - Leave \`.gtd/\` untouched
18107
+ - Leave \`.gtd/\` untouched, a handoff note excepted
18108
+ ${handoffRule()}
18079
18109
  - Leave everything uncommitted and finish your turn — do not commit
18080
- ${retry === void 0 ? "" : naiveRetry(retry)}`;
18110
+ ${from !== void 0 ? `${continuationLines(from, pickUp(from.snapshot, ".gtd/HANDOFF.md"))}${retry === void 0 ? "" : isFault(retry) ? `
18111
+ The pass this continues was a retry: its tests fell short, and still must not:
18112
+ ${retry.fault}
18113
+ ` : isRed(retry) ? `
18114
+ The pass this continues was a retry after a red suite (\`.gtd/FEEDBACK.md\`
18115
+ holds the output): finish only once the suite is green.
18116
+ ` : `
18117
+ The pass this continues was a retry after a \`reject\`: keep away from the
18118
+ approach that changed these files outside the expected set:
18119
+ ${listed(retry.strays)}
18120
+ `}` : retry === void 0 ? "" : naiveRetry(retry)}`;
18081
18121
  const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
18082
18122
  const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
18083
18123
  opened; plug the change into them:
@@ -18114,7 +18154,7 @@ const flagLine = (flagMode, tier, amend, sliced) => !flagMode ? "- Add no releas
18114
18154
  change added; only when none did, behind a new one, following the
18115
18155
  repository's flag conventions` : `- Put the new behaviour behind a new release flag, following the
18116
18156
  repository's flag conventions`;
18117
- const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice) => `${stateFileRules}
18157
+ const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice, from) => `${stateFileRules}
18118
18158
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
18119
18159
  ${sliceLine(slice)}${criteriaBinding(true)}
18120
18160
  ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
@@ -18129,9 +18169,10 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `-
18129
18169
  - Write the tests first, then the code that makes them pass. Name each
18130
18170
  criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
18131
18171
  green a judge checks every ID has a passing test, and sends uncovered ones back
18172
+ ${handoffRule()}
18132
18173
  - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
18133
18174
  finish your turn
18134
- `;
18175
+ ${from === void 0 ? "" : continuationLines(from)}`;
18135
18176
  const implementFixPrompt = () => `${stateFileRules}
18136
18177
  ${fixFeedbackPrompt}
18137
18178
  - Leave everything uncommitted and finish your turn — do not commit
@@ -18236,6 +18277,7 @@ const REFACTOR = ".gtd/REFACTOR.md";
18236
18277
  const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
18237
18278
  const QUALITY = ".gtd/QUALITY.md";
18238
18279
  const SLICES = ".gtd/SLICES.md";
18280
+ const HANDOFF = ".gtd/HANDOFF.md";
18239
18281
  const planner = () => env.plannerModel ?? "";
18240
18282
  const coder = () => env.coderModel ?? "";
18241
18283
  const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
@@ -37738,21 +37780,23 @@ Struct({
37738
37780
  });
37739
37781
  //#endregion
37740
37782
  //#region src/workflows/shared/options.ts
37741
- const setting = optionalWith(String$, { exact: true });
37783
+ const setting$1 = optionalWith(String$, { exact: true });
37742
37784
  const ShippedOptions = Struct({
37743
- reviewBase: setting,
37744
- judgeBudgetBytes: setting,
37745
- judgeIdenticalMinP: setting,
37746
- reviewNoteActionable: setting,
37747
- reviewSeverityMinP: setting,
37748
- refactorFlagPattern: setting,
37749
- seamAllow: setting,
37750
- sliceMaxCriteria: setting,
37751
- qualityReviews: setting,
37752
- refactorQualityReviews: setting,
37753
- testCommand: setting,
37754
- plannerModel: setting,
37755
- coderModel: setting,
37785
+ reviewBase: setting$1,
37786
+ judgeBudgetBytes: setting$1,
37787
+ judgeIdenticalMinP: setting$1,
37788
+ reviewNoteActionable: setting$1,
37789
+ reviewSeverityMinP: setting$1,
37790
+ refactorFlagPattern: setting$1,
37791
+ seamAllow: setting$1,
37792
+ sliceMaxCriteria: setting$1,
37793
+ qualityReviews: setting$1,
37794
+ refactorQualityReviews: setting$1,
37795
+ continuations: setting$1,
37796
+ continuationStall: setting$1,
37797
+ testCommand: setting$1,
37798
+ plannerModel: setting$1,
37799
+ coderModel: setting$1,
37756
37800
  flagMode: optionalWith(Boolean$, { exact: true }),
37757
37801
  contextLimit: optionalWith(Struct({
37758
37802
  soft: optionalWith(Number$, { exact: true }),
@@ -37783,7 +37827,7 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
37783
37827
  }));
37784
37828
  const FlagOptions = Struct({
37785
37829
  ...ShippedOptions.fields,
37786
- flagRegistry: setting
37830
+ flagRegistry: setting$1
37787
37831
  });
37788
37832
  const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
37789
37833
  onExcessProperty: "error",
@@ -38056,6 +38100,13 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
38056
38100
  });
38057
38101
  return true;
38058
38102
  });
38103
+ /** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
38104
+ const requireCriteriaIfEdited = () => {
38105
+ const change = changes().get(REQUIREMENTS);
38106
+ if (change === void 0) return;
38107
+ if (change.status === "deleted") refuse(`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`);
38108
+ requireCriteria(REQUIREMENTS);
38109
+ };
38059
38110
  //#endregion
38060
38111
  //#region src/workflows/feature/tier.ts
38061
38112
  /** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
@@ -38404,6 +38455,53 @@ const reUnwind = async (feedback) => {
38404
38455
  requireRevert(edited, base);
38405
38456
  };
38406
38457
  //#endregion
38458
+ //#region src/workflows/shared/continuation.ts
38459
+ const phase = (name, steered) => ({
38460
+ name,
38461
+ spent: false,
38462
+ count: 0,
38463
+ stalled: 0,
38464
+ steered
38465
+ });
38466
+ const own = (fn) => fn();
38467
+ const continued = (fn) => scope("continue", fn);
38468
+ const afresh = (fn) => scope("fresh", fn);
38469
+ const setting = (name, least) => {
38470
+ const raw = vars[name] ?? defaults[name];
38471
+ const n = Number(raw);
38472
+ if (raw.trim() === "" || !Number.isInteger(n) || n < least) refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`);
38473
+ return n;
38474
+ };
38475
+ /**
38476
+ * Run `turn` until one ends without a handoff note: each stopped turn is picked
38477
+ * up by a continuation in a fresh conversation. Past `continuations` of them in
38478
+ * the phase, or after `continuationStall` in a row that close nothing still
38479
+ * open, the phase rests at `oversized` for a person; landing there grants a new round.
38480
+ */
38481
+ const continuing = async (phase, turn) => {
38482
+ const cap = setting("continuations", 0);
38483
+ const stall = setting("continuationStall", 1);
38484
+ let last = await turn(void 0, phase.spent ? afresh : own);
38485
+ while (last.handoff !== void 0) {
38486
+ phase.spent = true;
38487
+ const from = last.handoff;
38488
+ if (phase.count >= cap || phase.stalled >= stall) {
38489
+ await human("oversized", {
38490
+ message: oversizedMessage(phase.name, from, phase.count >= cap ? { cap } : { stall }, phase.steered !== void 0),
38491
+ label: "Too big for its context",
38492
+ acceptClean: true
38493
+ });
38494
+ phase.steered?.();
38495
+ phase.count = 0;
38496
+ phase.stalled = 0;
38497
+ }
38498
+ last = await turn(from, continued);
38499
+ phase.count++;
38500
+ phase.stalled = from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item)) ? 0 : phase.stalled + 1;
38501
+ }
38502
+ return last.result;
38503
+ };
38504
+ //#endregion
38407
38505
  //#region src/workflows/feature/refactor.ts
38408
38506
  const REFACTOR_PACKAGE = REFACTOR;
38409
38507
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
@@ -38549,7 +38647,7 @@ const guardOf = ({ pattern, strays }, entry) => () => {
38549
38647
  requireNoNewFlags(pattern, entry);
38550
38648
  };
38551
38649
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
38552
- const refactorLap = async (p, guard, feedback) => {
38650
+ const refactorLap = async (p, guard, refactor, feedback) => {
38553
38651
  const { pattern, strays, reference, note } = p;
38554
38652
  await run("deriving", deriveScript([
38555
38653
  ...packageLines(strays, pattern, reference),
@@ -38569,32 +38667,48 @@ const refactorLap = async (p, guard, feedback) => {
38569
38667
  label: "Deriving the refactor package",
38570
38668
  file: REFACTOR_PACKAGE
38571
38669
  });
38572
- await agentWithSkills("building", refactorBuildingPrompt({
38573
- pkg: REFACTOR_PACKAGE,
38574
- strays,
38575
- pattern,
38576
- reference,
38577
- feedback: feedback !== void 0,
38578
- note: note !== void 0
38579
- }), {
38580
- label: "Refactoring",
38581
- model: coder(),
38582
- system: builderSystem()
38583
- });
38584
- guard();
38585
- await healthy(async () => {
38586
- await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
38587
- label: "Fixing the check",
38588
- file: FEEDBACK,
38670
+ const reached = /* @__PURE__ */ new Set();
38671
+ await continuing(refactor, async (from, fresh) => {
38672
+ await fresh(() => agentWithSkills("building", refactorBuildingPrompt({
38673
+ pkg: REFACTOR_PACKAGE,
38674
+ strays,
38675
+ pattern,
38676
+ reference,
38677
+ feedback: feedback !== void 0,
38678
+ note: note !== void 0,
38679
+ from
38680
+ }), {
38681
+ label: from === void 0 ? "Refactoring" : "Continuing the refactor",
38589
38682
  model: coder(),
38590
38683
  system: builderSystem()
38591
- });
38684
+ }));
38685
+ const left = read(HANDOFF);
38592
38686
  guard();
38593
- }, {
38594
- describeGuard: guard,
38595
- rules: refactorRules(strays, pattern)
38687
+ const snapshot = head$1();
38688
+ for (const c of changes()) reached.add(c.path);
38689
+ await healthy(async () => {
38690
+ await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
38691
+ label: "Fixing the check",
38692
+ file: FEEDBACK,
38693
+ model: coder(),
38694
+ system: builderSystem()
38695
+ });
38696
+ guard();
38697
+ }, {
38698
+ describeGuard: guard,
38699
+ rules: refactorRules(strays, pattern)
38700
+ });
38701
+ if (left === void 0) return { result: void 0 };
38702
+ return {
38703
+ result: void 0,
38704
+ handoff: {
38705
+ snapshot,
38706
+ note: left.trim(),
38707
+ open: strays.filter((path) => !reached.has(path))
38708
+ }
38709
+ };
38596
38710
  });
38597
- await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
38711
+ await run("closing", removeScript([REFACTOR_PACKAGE, HANDOFF]), { label: "Closing out the refactor" });
38598
38712
  };
38599
38713
  const strayAccess = (strays) => ({ write: [
38600
38714
  ...strays,
@@ -38608,7 +38722,7 @@ const refactorFirst = async (shape, options = {}) => {
38608
38722
  name: "refactor",
38609
38723
  access: strayAccess(p.strays)
38610
38724
  }, async () => {
38611
- await refactorLap(p, guardOf(p, head$1()));
38725
+ await refactorLap(p, guardOf(p, head$1()), phase("refactor"));
38612
38726
  return true;
38613
38727
  });
38614
38728
  };
@@ -38633,9 +38747,10 @@ const reviewedRefactor = async (shape, options = {}) => {
38633
38747
  unmatched: "gtd land: refactor-feedback: the review feedback changed files but wrote no .gtd/REFACTOR.md — a refactor cannot act on it; revert the change, or `gtd abandon` and restart",
38634
38748
  dropped: refactorDroppedRefusal
38635
38749
  };
38750
+ const refactor = phase("refactor");
38636
38751
  let collected;
38637
38752
  for (;;) {
38638
- await refactorLap(p, guard, collected);
38753
+ await refactorLap(p, guard, refactor, collected);
38639
38754
  const outcome = await buildTail(entry, {
38640
38755
  green: true,
38641
38756
  guard,
@@ -38746,12 +38861,32 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
38746
38861
  allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
38747
38862
  exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
38748
38863
  });
38749
- const naivePass = (retry, slice) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice)), {
38750
- label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
38864
+ const naivePass = (retry, slice, from) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice), from), {
38865
+ label: from !== void 0 ? "Continuing the naive pass" : retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
38751
38866
  keep: true,
38752
38867
  model: coder(),
38753
38868
  system: builderSystem()
38754
38869
  });
38870
+ const handoffOf = (tip, slice) => {
38871
+ const note = tip.changes.find((c) => c.path === ".gtd/HANDOFF.md" && c.after !== void 0);
38872
+ if (note === void 0) return void 0;
38873
+ return {
38874
+ snapshot: tip.hash,
38875
+ note: note.after.trim(),
38876
+ open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes))
38877
+ };
38878
+ };
38879
+ /** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
38880
+ const naiveContinued = (retry, slice, naive) => continuing(naive, async (from, fresh) => {
38881
+ const tip = await fresh(async () => {
38882
+ await naivePass(retry, slice, from);
38883
+ return kept("naive");
38884
+ });
38885
+ return {
38886
+ result: tip,
38887
+ handoff: handoffOf(tip, slice)
38888
+ };
38889
+ });
38755
38890
  const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
38756
38891
  const EXTENSION = /\.[\w-]+$/;
38757
38892
  const ALIAS = /^(?:[@~]\/|#)/;
@@ -38879,6 +39014,32 @@ const applied = async (tip) => {
38879
39014
  await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
38880
39015
  return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
38881
39016
  };
39017
+ /** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
39018
+ const settled = (carried, slice) => {
39019
+ const listed = idsOf(REQUIREMENTS, slice);
39020
+ if (carried === void 0) return listed;
39021
+ return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id));
39022
+ };
39023
+ const build = (options) => continuing(phase("build", requireCriteriaIfEdited), async (from, fresh) => {
39024
+ await fresh(() => agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice), from), {
39025
+ label: from === void 0 ? "Implementing" : "Continuing the implementation",
39026
+ model: coder(),
39027
+ system: builderSystem(),
39028
+ ...options.afterRefactor === true ? { allowEmpty: true } : {}
39029
+ }));
39030
+ requireCriteriaFrozen(REQUIREMENTS);
39031
+ const note = read(HANDOFF);
39032
+ if (note === void 0) return { result: void 0 };
39033
+ const ids = settled(options.rebuild?.acceptance?.ids, options.slice);
39034
+ return {
39035
+ result: void 0,
39036
+ handoff: {
39037
+ snapshot: head$1(),
39038
+ note: note.trim(),
39039
+ open: unnamed(ids, acceptanceTests(changesSince(start())))
39040
+ }
39041
+ };
39042
+ });
38882
39043
  function implement(first, tier, amend) {
38883
39044
  const options = typeof first === "object" && first !== null ? first : {
38884
39045
  reference: first,
@@ -38889,14 +39050,12 @@ function implement(first, tier, amend) {
38889
39050
  const since = head$1();
38890
39051
  const acceptance = options.rebuild?.acceptance;
38891
39052
  const built = options.naive === void 0 || !await applied(options.naive);
38892
- if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice)), {
38893
- label: "Implementing",
38894
- model: coder(),
38895
- system: builderSystem(),
38896
- ...options.afterRefactor === true ? { allowEmpty: true } : {}
38897
- });
38898
- requireCriteriaFrozen(REQUIREMENTS);
38899
- if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
39053
+ if (built) await build(options);
39054
+ else requireCriteriaFrozen(REQUIREMENTS);
39055
+ if (built && acceptance !== void 0) requireAcceptance({
39056
+ ...acceptance,
39057
+ ids: settled(acceptance.ids, options.slice)
39058
+ }, since);
38900
39059
  await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
38901
39060
  await requireCoverage(options.base, options.slice);
38902
39061
  await closing(options.more === true);
@@ -38905,10 +39064,11 @@ function implement(first, tier, amend) {
38905
39064
  /** The last slice's closing, for a last slice a steer left with no criterion to build. */
38906
39065
  const closeOut = () => scope("implement", () => closing(false));
38907
39066
  /** A slice with `more` after it keeps the criteria and the slicing for the next one. */
38908
- const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE] : [
39067
+ const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [
38909
39068
  REQUIREMENTS,
38910
39069
  REFACTOR_NOTE,
38911
- SLICES
39070
+ SLICES,
39071
+ HANDOFF
38912
39072
  ]), { label: "Closing out the implementation" });
38913
39073
  /** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
38914
39074
  const steer = async (retry) => {
@@ -38980,10 +39140,10 @@ const decide = (slice) => scope("seam", async () => {
38980
39140
  again: budget(),
38981
39141
  waived: false
38982
39142
  };
39143
+ const naive = phase("naive pass", requireCriteriaIfEdited);
38983
39144
  let retry;
38984
39145
  for (;;) {
38985
- await naivePass(retry, slice);
38986
- const tip = kept("naive");
39146
+ const tip = await naiveContinued(retry, slice, naive);
38987
39147
  const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
38988
39148
  reference: tip.hash,
38989
39149
  tip
@@ -39418,4 +39578,4 @@ var workflows_default = feature;
39418
39578
  */
39419
39579
  const doors = {};
39420
39580
  //#endregion
39421
- export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, 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, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireSlices, 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, unwind };
39581
+ 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, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, 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, unwind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "38.0.0",
3
+ "version": "38.2.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": {
@@ -1021,14 +1021,21 @@ describe("parseArgv — gtd history", () => {
1021
1021
  expect(parse("history", "fetch", "upstream")).toMatchObject({
1022
1022
  command: { kind: "historyFetch", remote: "upstream" },
1023
1023
  })
1024
+ expect(parse("history", "metrics")).toMatchObject({
1025
+ command: { kind: "historyMetrics", refs: [] },
1026
+ })
1027
+ expect(parse("history", "metrics", "a", "feat/b")).toMatchObject({
1028
+ command: { kind: "historyMetrics", refs: ["a", "feat/b"] },
1029
+ })
1024
1030
  })
1025
1031
 
1026
- it("bare history and an unknown subcommand are usage errors naming list, show, fetch", () => {
1032
+ it("bare history and an unknown subcommand are usage errors naming list, show, metrics, fetch", () => {
1027
1033
  for (const args of [["history"], ["history", "nope"]]) {
1028
1034
  const message = usage(parse(...args))
1029
1035
  expect(message).toContain("list")
1030
1036
  expect(message).toContain("show")
1031
1037
  expect(message).toContain("fetch")
1038
+ expect(message).toContain("metrics")
1032
1039
  }
1033
1040
  })
1034
1041
 
@@ -1039,19 +1046,23 @@ describe("parseArgv — gtd history", () => {
1039
1046
  expect(usage(parse("history", "fetch", "a", "b"))).toContain("too many arguments")
1040
1047
  })
1041
1048
 
1042
- it("--json is accepted for list and show, rejected for fetch", () => {
1049
+ it("--json is accepted for list, show and metrics, rejected for fetch", () => {
1043
1050
  expect(parse("history", "list", "--json")).toMatchObject({ json: { kind: "document" } })
1044
1051
  expect(parse("history", "show", "x", "--json=ended")).toMatchObject({
1045
1052
  json: { kind: "select", path: "ended" },
1046
1053
  })
1054
+ expect(parse("history", "metrics", "--json=total.refactorFirstRate")).toMatchObject({
1055
+ json: { kind: "select", path: "total.refactorFirstRate" },
1056
+ })
1047
1057
  expect(parse("history", "fetch", "--json").kind).toBe("usage")
1048
1058
  })
1049
1059
 
1050
- it("help lists the three history rows", () => {
1060
+ it("help lists the four history rows", () => {
1051
1061
  const help = renderHelp()
1052
1062
  expect(help).toMatch(/^ {2}history list\b/m)
1053
1063
  expect(help).toContain("history show <ref>")
1054
1064
  expect(help).toContain("history fetch [remote]")
1065
+ expect(help).toContain("history metrics [ref...]")
1055
1066
  })
1056
1067
  })
1057
1068
 
@@ -1136,6 +1147,7 @@ describe("standaloneKinds / needsOf", () => {
1136
1147
  "slices",
1137
1148
  "historyList",
1138
1149
  "historyShow",
1150
+ "historyMetrics",
1139
1151
  "historyFetch",
1140
1152
  ] as const) {
1141
1153
  expect(needsOf(kind)).toBe("state")