@pmelab/gtd 53.0.0 → 54.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "53.0.0",
3
+ "version": "54.0.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
@@ -387,13 +387,14 @@ const requirePointers = (review, notes, codeChanged) => {
387
387
  * pointing at its code and each of the lap's notes a chunk of its own; gtd
388
388
  * then adds each note's text and answer boxes, and the summary chunks.
389
389
  */
390
- const walkthrough = async (base, report, lap, carry) => {
390
+ const walkthrough = async (base, report, lap, carry, afterRiskFix) => {
391
391
  const notes = titledNotes(report);
392
392
  await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
393
393
  const written = read(REVIEW);
394
394
  if (written === void 0) return;
395
395
  const review = unranked(written);
396
396
  requirePointers(written, notes, changesSince(base).some((c) => isCode(c.path)));
397
+ if (!afterRiskFix && reviewRisks(written).length > 0) return;
397
398
  const full = sectionedReview(await ranked(review, withWalkthrough(review, report), new Set(notes.map((n) => n.title))), new Set(reviewChunks(review).map((c) => c.title)));
398
399
  if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
399
400
  label: "Ordering the review and adding the quality lap's findings",
@@ -419,16 +420,22 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
419
420
  const severities = /* @__PURE__ */ new Map();
420
421
  let nitFix;
421
422
  const answered = /* @__PURE__ */ new Set();
423
+ let riskFixed = false;
422
424
  return review(base, {
423
425
  file: REVIEW,
424
426
  feedback: feedback.file,
425
427
  isCode,
426
428
  reviewing: (b, carry) => {
427
429
  const fixedAt = nitFix;
430
+ const afterRiskFix = riskFixed;
431
+ riskFixed = false;
428
432
  const report = options.report ?? NO_FINDINGS;
429
- return options.walkthrough === true ? guarded(() => walkthrough(b, composedOf(b, report, fixedAt, answered), options.lap ?? "first", carry), guard)() : composing(b, report, carry, fixedAt, answered);
433
+ return options.walkthrough === true ? guarded(() => walkthrough(b, composedOf(b, report, fixedAt, answered), options.lap ?? "first", carry, afterRiskFix), guard)() : composing(b, report, carry, fixedAt, answered);
430
434
  },
431
- fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
435
+ fixRisks: guarded((notes) => {
436
+ riskFixed = true;
437
+ return fixRisks(notes, fixAccess, rules);
438
+ }, guard),
432
439
  keepGreen: () => healthy(() => fixCheck(rules), {
433
440
  escalations,
434
441
  guard,
@@ -2,7 +2,7 @@ import { agent, answered, applyScript, changes, changesSince, check, codeChanges
2
2
  import { lensEntriesOf } from "../workflow-kit.mjs";
3
3
  import { $ as observableMessage, B as designGateAnswerMessage, Ct as testerSystem, D as planner, Dt as unwindFailure, Et as unwindFailedMessage, H as designSystem, I as clearCriteriaMessage, J as implementBuildingPrompt, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, Mt as envDefaults, Nt as testFileGlobs, P as builderSystem, Q as normaliseCriteriaScript, S as collecting, St as startGateBlockedMessage, U as designTriagePrompt, V as designImpactPrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, a as flagModeOn, bt as seamByHandMessage, ct as refactorBuildingPrompt, d as healthy, dt as refactorNotePrompt, et as observableQuestion, f as FEEDBACK, ft as refactorRules, g as REFACTOR_NOTE, h as REFACTOR, i as skills, it as prototypeUnguidedMessage, j as agentWithSkills, jt as defaults, l as gate, lt as refactorDroppedRefusal, n as summary, nt as prototypeOpenMessage, p as PROTOTYPE, q as idleMessage, r as access, rt as prototypeTestingPrompt, s as shipped, t as steering, tt as prototypeFixingPrompt, ut as refactorFixPrompt, w as fixNits, x as coder, xt as seamSteerMessage, y as answerReviewQuestions, yt as reviewerSystem, z as criterionLine } from "../chunks/reserved-CQ7amcKG.mjs";
4
4
  import { a as refusePendingHandOff, o as tail } from "../chunks/tail-JZ9PAOwp.mjs";
5
- import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-CNgN_V8T.mjs";
5
+ import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-CXzUXJHL.mjs";
6
6
  import { posix } from "node:path";
7
7
  //#region src/workflows/shared/fences.ts
8
8
  const markFenced = (text) => {
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, start } from "../flows.mjs";
2
2
  import { Mt as envDefaults, W as finisherSystem, _ as REQUIREMENTS, gt as reviewGateBlockedMessage, ht as reviewFeedbackFixPrompt, i as skills, j as agentWithSkills, jt as defaults, l as gate, n as summary, r as access, s as shipped, t as steering, x as coder } from "../chunks/reserved-CQ7amcKG.mjs";
3
3
  import { a as refusePendingHandOff } from "../chunks/tail-JZ9PAOwp.mjs";
4
- import { i as reUnwind, t as buildTail } from "../chunks/review-CNgN_V8T.mjs";
4
+ import { i as reUnwind, t as buildTail } from "../chunks/review-CXzUXJHL.mjs";
5
5
  //#region src/workflows/review/review.ts
6
6
  const fixFeedback = async () => {
7
7
  await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
@@ -39584,13 +39584,14 @@ const requirePointers = (review, notes, codeChanged) => {
39584
39584
  * pointing at its code and each of the lap's notes a chunk of its own; gtd
39585
39585
  * then adds each note's text and answer boxes, and the summary chunks.
39586
39586
  */
39587
- const walkthrough = async (base, report, lap, carry) => {
39587
+ const walkthrough = async (base, report, lap, carry, afterRiskFix) => {
39588
39588
  const notes = titledNotes(report);
39589
39589
  await reviewing(base, carry, lap, walkthroughBlock(notes, report.nitFix));
39590
39590
  const written = read(REVIEW);
39591
39591
  if (written === void 0) return;
39592
39592
  const review = unranked(written);
39593
39593
  requirePointers(written, notes, changesSince(base).some((c) => isCode$1(c.path)));
39594
+ if (!afterRiskFix && reviewRisks(written).length > 0) return;
39594
39595
  const full = sectionedReview(await ranked(review, withWalkthrough(review, report), new Set(notes.map((n) => n.title))), new Set(reviewChunks(review).map((c) => c.title)));
39595
39596
  if (full !== written) await run("review.carrying", writeScript(REVIEW, full), {
39596
39597
  label: "Ordering the review and adding the quality lap's findings",
@@ -39616,16 +39617,22 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
39616
39617
  const severities = /* @__PURE__ */ new Map();
39617
39618
  let nitFix;
39618
39619
  const answered = /* @__PURE__ */ new Set();
39620
+ let riskFixed = false;
39619
39621
  return review$1(base, {
39620
39622
  file: REVIEW,
39621
39623
  feedback: feedback.file,
39622
39624
  isCode: isCode$1,
39623
39625
  reviewing: (b, carry) => {
39624
39626
  const fixedAt = nitFix;
39627
+ const afterRiskFix = riskFixed;
39628
+ riskFixed = false;
39625
39629
  const report = options.report ?? NO_FINDINGS;
39626
- return options.walkthrough === true ? guarded(() => walkthrough(b, composedOf(b, report, fixedAt, answered), options.lap ?? "first", carry), guard)() : composing(b, report, carry, fixedAt, answered);
39630
+ return options.walkthrough === true ? guarded(() => walkthrough(b, composedOf(b, report, fixedAt, answered), options.lap ?? "first", carry, afterRiskFix), guard)() : composing(b, report, carry, fixedAt, answered);
39627
39631
  },
39628
- fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
39632
+ fixRisks: guarded((notes) => {
39633
+ riskFixed = true;
39634
+ return fixRisks(notes, fixAccess, rules);
39635
+ }, guard),
39629
39636
  keepGreen: () => healthy(() => fixCheck(rules), {
39630
39637
  escalations,
39631
39638
  guard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "53.0.0",
3
+ "version": "54.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": {
@@ -193,6 +193,21 @@ Given(
193
193
  },
194
194
  )
195
195
 
196
+ // An agent's edit in place, keeping whatever gtd wrote around it.
197
+ Given(
198
+ "step {string} replaces the text {string} in {string} with {string}",
199
+ (world: TestkitWorld, step: string, text: string, path: string, replacement: string) => {
200
+ world.act(step, (_, repo) => {
201
+ const content = repo.read(path) ?? ""
202
+ assert.ok(content.includes(text), `${path}: ${content}`)
203
+ repo.write(
204
+ path,
205
+ content.replace(text, () => replacement),
206
+ )
207
+ })
208
+ },
209
+ )
210
+
196
211
  Given("step {string} deletes {string}", (world: TestkitWorld, step: string, path: string) => {
197
212
  world.act(step, (_, repo) => repo.remove(path))
198
213
  })
@@ -390,6 +405,14 @@ Then(
390
405
  },
391
406
  )
392
407
 
408
+ Then(
409
+ "{string} contains {string} exactly once",
410
+ (world: TestkitWorld, path: string, text: string) => {
411
+ const content = world.repo().read(path) ?? ""
412
+ assert.strictEqual(content.split(text).length - 1, 1, `${path}: ${content}`)
413
+ },
414
+ )
415
+
393
416
  Then("{string} does not contain {string}", (world: TestkitWorld, path: string, text: string) => {
394
417
  const content = world.repo().read(path)
395
418
  assert.ok(content !== undefined && !content.includes(text), `${path}: ${content}`)
@@ -161,6 +161,56 @@ Feature: the quality lap's findings are triaged before the review
161
161
  gtd(check): build.review.carrying → build.review.await-review
162
162
  """
163
163
 
164
+ Scenario: a risk is fixed before the human sees the review, re-reviewed in the reviewer's own draft
165
+ Given judge step "build.quality.triage" answers:
166
+ """
167
+ [{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"noise","p":0.85},{"id":"Q3","answer":"design","p":0.8}]
168
+ """
169
+ And step "build.fix.quality.fixing" writes "src/calc.test.ts" with:
170
+ """
171
+ it("[AC-1] subtracts", () => {})
172
+ it("subtracts a larger number", () => {})
173
+ """
174
+ And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
175
+ And step "build.health.check" changes nothing
176
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
177
+ """
178
+ # Review: abc1234
179
+
180
+ <!-- base: 0000000 -->
181
+
182
+ ## Subtract
183
+
184
+ - [ ] ./src/calc.ts#2-2 — Risk: overflows on large numbers
185
+
186
+ ## AC-1 ignores floating point
187
+
188
+ - [ ] ./src/calc.ts#2-2 — where it applies
189
+ """
190
+ When the process is driven
191
+ Then the process rests at "build.review.fix.risks.fixing"
192
+ Given step "build.review.reviewing" is no longer scripted
193
+ And step "build.review.reviewing" replaces the text "Risk: overflows on large numbers" in ".gtd/REVIEW.md" with "subtract(a, b), now guarded"
194
+ And step "build.review.fix.risks.fixing" changes nothing
195
+ When the process is driven
196
+ Then the process rests at "build.review.await-review"
197
+ And ".gtd/REVIEW.md" contains "subtract(a, b), now guarded"
198
+ And ".gtd/REVIEW.md" contains "## Fixed automatically" exactly once
199
+ And ".gtd/REVIEW.md" contains "## Dropped as noise" exactly once
200
+ And ".gtd/REVIEW.md" contains "## Walkthrough" exactly once
201
+ And ".gtd/REVIEW.md" contains "Should subtract round? The criterion does not say." exactly once
202
+ And ".gtd/REVIEW.md" contains "Tick exactly one:" exactly once
203
+ And the commit subjects end with:
204
+ """
205
+ gtd(check): build.health.check → build.review.reviewing
206
+ gtd(agent): build.review.reviewing → build.review.fix.risks.fixing
207
+ gtd(agent): build.review.fix.risks.fixing → build.health.check
208
+ gtd(check): build.health.check → build.review.reviewing
209
+ gtd(agent): build.review.reviewing → build.review.ranking
210
+ gtd(judge): build.review.ranking → build.review.carrying
211
+ gtd(check): build.review.carrying → build.review.await-review
212
+ """
213
+
164
214
  Scenario: an unsure verdict makes a finding a note, never a fix or a drop
165
215
  Given judge step "build.quality.triage" answers:
166
216
  """
@@ -1185,6 +1185,44 @@ Feature: the refactor is reviewed on its own before the feature is implemented
1185
1185
  """
1186
1186
  And no commit body contains "refactor.build.review.composing"
1187
1187
 
1188
+ Scenario: a risk the refactor's re-review marks again reaches the human unfixed
1189
+ Given step "refactor.build.review.reviewing" writes ".gtd/REVIEW.md" with:
1190
+ """
1191
+ # Review: abc1234
1192
+
1193
+ <!-- base: 0000000 -->
1194
+
1195
+ ## calc
1196
+
1197
+ - [ ] ./src/calc.ts#1-2 — Risk: drops the sign
1198
+ """
1199
+ When the process is driven
1200
+ Then the process rests at "refactor.build.review.fix.risks.fixing"
1201
+ Given step "refactor.build.review.reviewing" is no longer scripted
1202
+ And step "refactor.build.review.reviewing" replaces the text "Risk: drops the sign" in ".gtd/REVIEW.md" with "Risk: still drops the sign"
1203
+ And step "refactor.build.review.fix.risks.fixing" changes nothing
1204
+ And step "refactor.build.health.check" changes nothing
1205
+ When the process is driven
1206
+ Then the process rests at "refactor.build.review.await-review"
1207
+ And ".gtd/REVIEW.md" contains:
1208
+ """
1209
+ ## Walkthrough
1210
+
1211
+ ### calc
1212
+
1213
+ - [ ] ./src/calc.ts#1-2 — Risk: still drops the sign
1214
+ """
1215
+ And ".gtd/REVIEW.md" contains "## Walkthrough" exactly once
1216
+ And the commit subjects end with:
1217
+ """
1218
+ gtd(agent): refactor.build.review.reviewing → refactor.build.review.fix.risks.fixing
1219
+ gtd(agent): refactor.build.review.fix.risks.fixing → refactor.build.health.check
1220
+ gtd(check): refactor.build.health.check → refactor.build.review.reviewing
1221
+ gtd(agent): refactor.build.review.reviewing → refactor.build.review.ranking
1222
+ gtd(judge): refactor.build.review.ranking → refactor.build.review.carrying
1223
+ gtd(check): refactor.build.review.carrying → refactor.build.review.await-review
1224
+ """
1225
+
1188
1226
  Rule: with a quality lens on both the refactor and the feature
1189
1227
 
1190
1228
  Background:
@@ -179,37 +179,6 @@ Feature: a reviewer walks the human through the feature's code
179
179
  """
180
180
  Then driving the process is refused with "AC-1 ignores floating point — no chunk of its own"
181
181
 
182
- Scenario: a risk the reviewer marks is fixed before the human sees the review
183
- Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
184
- """
185
- # Review: abc1234
186
-
187
- <!-- base: 0000000 -->
188
-
189
- ## Subtract
190
-
191
- - [ ] ./src/calc.ts#2-2 — Risk: overflows on large numbers
192
-
193
- ## AC-1 ignores floating point
194
-
195
- - [ ] ./src/calc.ts#2-2 — returns the raw difference
196
- """
197
- And step "build.review.fix.risks.fixing" changes nothing
198
- And step "build.health.check" changes nothing
199
- When the process is driven
200
- Then the process rests at "build.review.await-review"
201
- And the commit subjects end with:
202
- """
203
- gtd(agent): build.review.reviewing → build.review.ranking
204
- gtd(judge): build.review.ranking → build.review.carrying
205
- gtd(check): build.review.carrying → build.review.fix.risks.fixing
206
- gtd(agent): build.review.fix.risks.fixing → build.health.check
207
- gtd(check): build.health.check → build.review.reviewing
208
- gtd(agent): build.review.reviewing → build.review.ranking
209
- gtd(judge): build.review.ranking → build.review.carrying
210
- gtd(check): build.review.carrying → build.review.await-review
211
- """
212
-
213
182
  Scenario: a standard conflict the reviewer writes itself is refused
214
183
  Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
215
184
  """
@@ -380,6 +380,7 @@ const walkthrough = async (
380
380
  report: t.ComposedReview,
381
381
  lap: t.ReviewRange,
382
382
  carry: string | undefined,
383
+ afterRiskFix: boolean,
383
384
  ): Promise<void> => {
384
385
  const notes = t.titledNotes(report)
385
386
  await reviewing(base, carry, lap, t.walkthroughBlock(notes, report.nitFix))
@@ -393,7 +394,8 @@ const walkthrough = async (
393
394
  notes,
394
395
  changesSince(base).some((c) => isCode(c.path)),
395
396
  )
396
- // Ranked even when it marks risks: the review written after their fix reaches the human with its own.
397
+ // Its risks are fixed and the review written again, so the re-reviewer edits this draft, never gtd's layout.
398
+ if (!afterRiskFix && reviewRisks(written).length > 0) return
397
399
  const full = sectionedReview(
398
400
  await ranked(review, t.withWalkthrough(review, report), new Set(notes.map((n) => n.title))),
399
401
  new Set(reviewChunks(review).map((c) => c.title)),
@@ -432,12 +434,16 @@ export const humanReview = (
432
434
  let nitFix: string | undefined
433
435
  // Every note the human answered in this review, left out of a fresh one.
434
436
  const answered = new Set<string>()
437
+ // Set by a risk fix, so the review written next reaches the human whatever it marks.
438
+ let riskFixed = false
435
439
  return kit.review(base, {
436
440
  file: REVIEW,
437
441
  feedback: feedback.file,
438
442
  isCode,
439
443
  reviewing: (b, carry) => {
440
444
  const fixedAt = nitFix
445
+ const afterRiskFix = riskFixed
446
+ riskFixed = false
441
447
  const report = options.report ?? NO_FINDINGS
442
448
  return options.walkthrough === true
443
449
  ? guarded(
@@ -447,12 +453,16 @@ export const humanReview = (
447
453
  composedOf(b, report, fixedAt, answered),
448
454
  options.lap ?? "first",
449
455
  carry,
456
+ afterRiskFix,
450
457
  ),
451
458
  guard,
452
459
  )()
453
460
  : composing(b, report, carry, fixedAt, answered)
454
461
  },
455
- fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
462
+ fixRisks: guarded((notes) => {
463
+ riskFixed = true
464
+ return fixRisks(notes, fixAccess, rules)
465
+ }, guard),
456
466
  keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
457
467
  // Kept until the human sees a review, so a re-review after a risk fix still names it.
458
468
  awaitReview: (b) => {