@pmelab/gtd 28.1.0 → 29.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +28 -17
  3. package/claude/hooks/assist.ts +1 -1
  4. package/claude/hooks/register.tsx +1 -1
  5. package/claude/hooks/ship.ts +963 -143
  6. package/dist/chunks/{reserved-DeBS4Ymh.mjs → reserved-DoHUII2D.mjs} +206 -121
  7. package/dist/chunks/{tail-B2SER2Ih.mjs → tail-C6YBG5zh.mjs} +19 -12
  8. package/dist/testkit.mjs +2 -1
  9. package/dist/workflow-kit.mjs +51 -21
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +247 -96
  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 +528 -262
  16. package/package.json +1 -1
  17. package/src/testkit/TestRepo.ts +3 -0
  18. package/src/testkit/index.ts +1 -1
  19. package/src/testkit/steps.ts +44 -0
  20. package/src/workflow-kit/review.test.ts +366 -8
  21. package/src/workflow-kit/review.ts +109 -26
  22. package/src/workflows/feature/feature.test.ts +313 -1
  23. package/src/workflows/feature/feature.ts +48 -43
  24. package/src/workflows/feature/one-step-land.feature +4 -0
  25. package/src/workflows/feature/refactor-first.feature +54 -24
  26. package/src/workflows/feature/refactor-review.feature +1706 -0
  27. package/src/workflows/feature/refactor.test.ts +592 -2
  28. package/src/workflows/feature/refactor.ts +160 -42
  29. package/src/workflows/feature/review.test.ts +322 -5
  30. package/src/workflows/feature/review.ts +77 -10
  31. package/src/workflows/feature/seam-rejection.feature +4 -0
  32. package/src/workflows/feature/seam.feature +3 -4
  33. package/src/workflows/feature/seam.test.ts +196 -1
  34. package/src/workflows/feature/seam.ts +23 -9
  35. package/src/workflows/feature/steps.test.ts +54 -2
  36. package/src/workflows/feature/steps.ts +45 -9
  37. package/src/workflows/feature/tier.test.ts +4 -0
  38. package/src/workflows/feature/tier.ts +2 -20
  39. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +97 -0
  40. package/src/workflows/shared/access.test.ts +31 -1
  41. package/src/workflows/shared/access.ts +7 -1
  42. package/src/workflows/shared/fences.ts +19 -0
  43. package/src/workflows/shared/health.ts +32 -12
  44. package/src/workflows/shared/options.test.ts +18 -0
  45. package/src/workflows/shared/options.ts +1 -0
  46. package/src/workflows/shared/prose.ts +4 -2
  47. package/src/workflows/shared/skills.test.ts +20 -1
  48. package/src/workflows/shared/skills.ts +43 -32
  49. package/src/workflows/shared/steps.ts +33 -19
  50. package/src/workflows/shared/tail.test.ts +84 -1
  51. package/src/workflows/shared/tail.ts +30 -12
  52. package/src/workflows/shared/text.test.ts +266 -4
  53. package/src/workflows/shared/text.ts +183 -70
  54. package/src/workflows/shared/vars.ts +12 -2
@@ -1,10 +1,8 @@
1
- import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, seamGate, start, vars } from "../flows.mjs";
2
- import { review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as reviewEditsCapture, A as architectureGateAnswerMessage, B as designTriagePrompt, C as reviewMissing, D as architectSystem, E as agentWithSkills, F as buildReviewTriageMessage, G as implementBuildingPrompt, I as builderSystem, J as packagesItemBuildingPrompt, K as implementFixPrompt, L as deferNotesScript, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, O as architectureAuthorPrompt, P as buildReviewFixNitsPrompt, Q as reviewEditNotesCapture, R as designGateAnswerMessage, S as planner, T as reviewing, W as idleMessage, X as refactorBuildingPrompt, Y as packagesItemFixSuitePrompt, Z as refactorFixPrompt, _ as awaitReview, at as startGateBlockedMessage, ct as defaults, d as healthy, f as ARCHITECTURE, g as REVIEW, h as REQUIREMENTS, i as skills, it as seamSteerMessage, j as buildReviewAnswerQuestionsPrompt, k as architectureDecomposePrompt, l as gate, lt as envDefaults, n as summary, nt as reviewerSystem, o as shipped, ot as unwindFailedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as seamByHandMessage, st as unwindFailure, t as steering, tt as reviewNotesCapture, v as coder, x as fixRisks, y as fixCheck, z as designSystem } from "../chunks/reserved-DeBS4Ymh.mjs";
4
- import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-B2SER2Ih.mjs";
5
- //#region src/workflows/feature/tier.ts
6
- /** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
7
- const riskTierOn = () => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1";
1
+ import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
2
+ import { lensesOf, review as review$1 } from "../workflow-kit.mjs";
3
+ import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-DoHUII2D.mjs";
4
+ import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-C6YBG5zh.mjs";
5
+ //#region src/workflows/shared/fences.ts
8
6
  const markFenced = (text) => {
9
7
  let fence;
10
8
  return text.split(/\r?\n/).map((line) => {
@@ -18,6 +16,10 @@ const markFenced = (text) => {
18
16
  };
19
17
  });
20
18
  };
19
+ //#endregion
20
+ //#region src/workflows/feature/tier.ts
21
+ /** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
22
+ const riskTierOn = () => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1";
21
23
  const sectionLines = (text, title) => {
22
24
  if (text === void 0) return [];
23
25
  const heading = new RegExp(`^##[ \\t]+${title}[ \\t]*$`, "i");
@@ -107,12 +109,18 @@ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-q
107
109
  system: reviewerSystem()
108
110
  });
109
111
  /** Fix every `nit` note in one turn. */
110
- const fixNits = (notes) => agentWithSkills("review.fix.nits.fixing", buildReviewFixNitsPrompt(notes), {
111
- label: "Fixing your nits",
112
- file: REVIEW,
113
- model: planner(),
114
- system: reviewerSystem()
115
- });
112
+ const fixNits = (notes, access, rules) => {
113
+ const fixing = (name) => agentWithSkills(name, buildReviewFixNitsPrompt(notes, rules), {
114
+ label: "Fixing your nits",
115
+ file: REVIEW,
116
+ model: planner(),
117
+ system: reviewerSystem()
118
+ });
119
+ return access === void 0 ? fixing("review.fix.nits.fixing") : scope({
120
+ name: "review.fix.nits",
121
+ access
122
+ }, () => fixing("fixing"));
123
+ };
116
124
  /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
117
125
  const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
118
126
  label: "Collecting your feedback",
@@ -122,6 +130,17 @@ const collecting = (capture) => agentWithSkills("review.collecting", buildReview
122
130
  system: reviewerSystem(),
123
131
  allowEmpty: true
124
132
  });
133
+ /** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
134
+ const refactorCollecting = (capture, round, strays) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, {
135
+ strays,
136
+ round
137
+ }), {
138
+ label: "Collecting your refactor feedback",
139
+ file: REFACTOR,
140
+ model: planner(),
141
+ system: reviewerSystem(),
142
+ allowEmpty: true
143
+ });
125
144
  /** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
126
145
  const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes, round), {
127
146
  label: "Deferring could-notes to TODO.md",
@@ -339,26 +358,37 @@ const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
339
358
  * another planning lap, questions are answered in place, nits fixed in one
340
359
  * batch, praise dropped.
341
360
  */
342
- const review = (base, escalations = { rounds: 0 }) => {
361
+ const review = (base, escalations = { rounds: 0 }, options = {}) => {
362
+ const { guard, guardRests, fixAccess, rules } = options;
363
+ const feedback = options.feedback ?? {
364
+ file: ".gtd/REQUIREMENTS.md",
365
+ collecting,
366
+ unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
367
+ };
343
368
  const severities = /* @__PURE__ */ new Map();
344
369
  let deferred = [];
345
370
  return review$1(base, {
346
371
  file: REVIEW,
347
- feedback: REQUIREMENTS,
372
+ feedback: feedback.file,
348
373
  isCode: isCode$1,
349
374
  reviewing: (b, carry) => {
350
375
  deferred = [];
351
- return reviewing(b, carry);
376
+ return reviewing(b, carry, options.lap);
352
377
  },
353
- fixRisks,
354
- keepGreen: () => healthy(fixCheck, { escalations }),
378
+ fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
379
+ keepGreen: () => healthy(() => fixCheck(rules), {
380
+ escalations,
381
+ guard,
382
+ guardRests,
383
+ rules
384
+ }),
355
385
  awaitReview: (b) => {
356
386
  const notes = deferred;
357
387
  deferred = [];
358
388
  return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
359
389
  },
360
390
  reviewMissing,
361
- collecting,
391
+ collecting: feedback.collecting,
362
392
  triage: triage$1(severities),
363
393
  defer: (notes) => {
364
394
  deferred = notes.map((n) => ({
@@ -370,7 +400,7 @@ const review = (base, escalations = { rounds: 0 }) => {
370
400
  return deferNotes(deferred, head());
371
401
  },
372
402
  answerQuestions: answerReviewQuestions,
373
- fixNits,
403
+ fixNits: guarded((notes) => fixNits(notes, fixAccess, rules), guard),
374
404
  close: closeReview,
375
405
  captures: {
376
406
  edits: reviewEditsCapture,
@@ -388,21 +418,44 @@ const review = (base, escalations = { rounds: 0 }) => {
388
418
  },
389
419
  refusals: {
390
420
  deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
391
- unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
421
+ unmatched: feedback.unmatched,
422
+ ...feedback.dropped === void 0 ? {} : { dropped: feedback.dropped }
392
423
  }
393
424
  });
394
425
  };
395
426
  /** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
396
- const buildTail = (fixFirst, base, green = false) => tail(fixFirst, (escalations) => review(base, escalations), green);
427
+ const buildTail = (fixFirst, base, green = false, options = {}) => tail(fixFirst, (escalations) => review(base, escalations, options), green, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
428
+ /**
429
+ * Undo the human's review-round code edits. A path changed since the human
430
+ * left it is not overwritten; requireRevert names it instead.
431
+ */
432
+ const reUnwind = async (feedback) => {
433
+ const { base, restoreFrom, edited } = feedback;
434
+ await run("re-unwind", restoreScript(base, {
435
+ restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
436
+ remove: edited.filter((c) => c.status === "added").map((c) => c.path)
437
+ }, restoreFrom), {
438
+ label: "Re-unwinding your review edit",
439
+ file: REVIEW,
440
+ base
441
+ });
442
+ requireRevert(edited, base);
443
+ };
397
444
  //#endregion
398
445
  //#region src/workflows/feature/refactor.ts
399
- const REFACTOR_PACKAGE = ".gtd/REFACTOR.md";
446
+ const REFACTOR_PACKAGE = REFACTOR;
400
447
  const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
401
448
  const requireWithin = (allowed) => {
402
449
  const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
403
450
  if (outside.length === 0) return;
404
451
  refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
405
452
  };
453
+ const isTestFile = (path) => testFileGlobs.some((glob) => matches(path, glob));
454
+ const requireNoTestFiles = () => {
455
+ const tests = changes().map((c) => c.path).filter((path) => isCode(path) && isTestFile(path));
456
+ if (tests.length === 0) return;
457
+ refuse(`gtd land: refactor-tests: the refactor may not change a test file — revert these:\n${tests.map((path) => ` - ${path}`).join("\n")}`);
458
+ };
406
459
  const flagExpression = (pattern) => {
407
460
  if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
408
461
  let expression;
@@ -444,6 +497,8 @@ const requireNoNewFlags = (pattern, since) => {
444
497
  if (raised.length === 0) return;
445
498
  refuse(`gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`);
446
499
  };
500
+ /** A test-file stray is never refactored: the implement turn still reads it in the naive tip. */
501
+ const refactorStrays = (shape) => shape.strays.filter((path) => !isTestFile(path));
447
502
  const packageLines = (strays, pattern, reference) => [
448
503
  "# Behaviour-neutral refactor",
449
504
  "",
@@ -456,7 +511,7 @@ const packageLines = (strays, pattern, reference) => [
456
511
  "## Rules",
457
512
  "",
458
513
  "- The suite's outcome must not change",
459
- "- Change only the paths above; a test file only when it is one of them",
514
+ "- Change only the paths above, and never a test file",
460
515
  `- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
461
516
  "",
462
517
  "## Reference",
@@ -464,49 +519,125 @@ const packageLines = (strays, pattern, reference) => [
464
519
  reference === void 0 ? "None." : `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
465
520
  ""
466
521
  ];
522
+ const demoteHeadings = (text) => {
523
+ const marked = markFenced(text.trimEnd());
524
+ const lines = marked.map((m) => m.line);
525
+ const headings = marked.map((m) => m.fenced ? void 0 : /^(#{1,6}) /.exec(m.line)?.[1]?.length);
526
+ const shift = 3 - Math.min(...headings.filter((n) => n !== void 0));
527
+ return lines.map((line, i) => {
528
+ const depth = headings[i];
529
+ if (depth === void 0) return line;
530
+ return "#".repeat(Math.min(6, depth + shift)) + line.slice(depth);
531
+ });
532
+ };
467
533
  const deriveScript = (lines) => `mkdir -p .gtd
468
534
  printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
469
535
  `;
470
- const refactorFirst = async (shape, options = {}) => {
471
- if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return false;
536
+ /** Undefined for every shape that is not a readable `refactor-first`; throws on a bad flag pattern. */
537
+ const plan = (shape, options) => {
538
+ if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return void 0;
472
539
  const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
473
540
  const expression = flagExpression(pattern);
474
- const { strays } = shape;
541
+ const strays = refactorStrays(shape);
542
+ if (strays.length === 0) return void 0;
475
543
  const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
476
544
  if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
477
- const reference = options.reference?.trim() ? options.reference : void 0;
478
- return scope({
479
- name: "refactor",
480
- access: { write: [...strays, ".gtd/**"] }
481
- }, async () => {
482
- await run("deriving", deriveScript(packageLines(strays, pattern, reference)), {
483
- label: "Deriving the refactor package",
484
- file: REFACTOR_PACKAGE
485
- });
486
- const from = head();
487
- const guard = () => {
488
- requireWithin(strays);
489
- requireNoNewFlags(pattern, from);
490
- };
491
- await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference), {
492
- label: "Refactoring",
545
+ return {
546
+ pattern,
547
+ strays,
548
+ reference: options.reference?.trim() ? options.reference : void 0
549
+ };
550
+ };
551
+ const guardOf = ({ pattern, strays }, entry) => () => {
552
+ requireNoTestFiles();
553
+ requireWithin(strays);
554
+ requireNoNewFlags(pattern, entry);
555
+ };
556
+ /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
557
+ const refactorLap = async (p, guard, feedback) => {
558
+ const { pattern, strays, reference } = p;
559
+ await run("deriving", deriveScript([...packageLines(strays, pattern, reference), ...feedback === void 0 ? [] : [
560
+ "## Review feedback",
561
+ "",
562
+ ...demoteHeadings(feedback),
563
+ ""
564
+ ]]), {
565
+ label: "Deriving the refactor package",
566
+ file: REFACTOR_PACKAGE
567
+ });
568
+ await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference, feedback !== void 0), {
569
+ label: "Refactoring",
570
+ model: coder(),
571
+ system: builderSystem()
572
+ });
573
+ guard();
574
+ await healthy(async () => {
575
+ await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
576
+ label: "Fixing the check",
577
+ file: FEEDBACK,
493
578
  model: coder(),
494
579
  system: builderSystem()
495
580
  });
496
581
  guard();
497
- await healthy(async () => {
498
- await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
499
- label: "Fixing the check",
500
- file: FEEDBACK,
501
- model: coder(),
502
- system: builderSystem()
503
- });
504
- guard();
505
- }, { describeGuard: guard });
506
- await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
582
+ }, {
583
+ describeGuard: guard,
584
+ rules: refactorRules(strays, pattern)
585
+ });
586
+ await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
587
+ };
588
+ const strayAccess = (strays) => ({ write: [...strays, ".gtd/**"] });
589
+ const refactorFirst = async (shape, options = {}) => {
590
+ const p = plan(shape, options);
591
+ if (p === void 0) return false;
592
+ return scope({
593
+ name: "refactor",
594
+ access: strayAccess(p.strays)
595
+ }, async () => {
596
+ await refactorLap(p, guardOf(p, head()));
507
597
  return true;
508
598
  });
509
599
  };
600
+ /**
601
+ * `refactorFirst` plus its review, in the one `refactor` scope so the tail's
602
+ * steps are `refactor.build.*`. A review edit re-runs the refactor, never a
603
+ * re-plan: the collected notes ride the re-derived package. Every round
604
+ * reviews the whole refactor since `entry`, since each lap rebuilds over it.
605
+ */
606
+ const reviewedRefactor = async (shape, options = {}) => {
607
+ const p = plan(shape, options);
608
+ if (p === void 0) return false;
609
+ return scope({
610
+ name: "refactor",
611
+ access: strayAccess(p.strays)
612
+ }, async () => {
613
+ const entry = head();
614
+ const guard = guardOf(p, entry);
615
+ const feedback = {
616
+ file: REFACTOR_PACKAGE,
617
+ collecting: (capture, round) => refactorCollecting(capture, round, p.strays),
618
+ 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",
619
+ dropped: refactorDroppedRefusal
620
+ };
621
+ let collected;
622
+ for (;;) {
623
+ await refactorLap(p, guard, collected);
624
+ const outcome = await buildTail(false, entry, true, {
625
+ guard,
626
+ guardRests: false,
627
+ lenses: lensesOf(vars.refactorQualityReviews ?? ""),
628
+ feedback,
629
+ fixAccess: strayAccess(p.strays),
630
+ lensBase: entry,
631
+ rules: refactorRules(p.strays, p.pattern),
632
+ lap: "refactor"
633
+ });
634
+ if (outcome.verdict === "signoff") return true;
635
+ await reUnwind(outcome);
636
+ collected = read(REFACTOR_PACKAGE);
637
+ if (collected === void 0) throw new Error(`gtd: review feedback left no ${REFACTOR_PACKAGE}`);
638
+ }
639
+ });
640
+ };
510
641
  //#endregion
511
642
  //#region src/workflows/feature/seam.ts
512
643
  /** Naive-pass retries after a `reject`; the next `reject` rests at `seam.steer`. */
@@ -523,8 +654,8 @@ const naivePass = (rejection) => agentWithSkills("naive", naivePassPrompt(reject
523
654
  model: coder(),
524
655
  system: builderSystem()
525
656
  });
526
- const implement = (reference, tier) => scope("implement", async () => {
527
- await agentWithSkills("building", implementBuildingPrompt(reference, tier), {
657
+ const implement = (reference, tier, amend) => scope("implement", async () => {
658
+ await agentWithSkills("building", implementBuildingPrompt(reference, tier, amend), {
528
659
  label: "Implementing",
529
660
  model: coder(),
530
661
  system: builderSystem()
@@ -545,13 +676,10 @@ const decide = () => scope("seam", async () => {
545
676
  const tip = kept("naive");
546
677
  const shape = await judgeSeam(tip);
547
678
  if (shape.choice === "land") return { reference: tip.hash };
548
- if (shape.choice === "refactor-first") {
549
- await seamGate("approve", shape);
550
- return {
551
- reference: tip.hash,
552
- shape
553
- };
554
- }
679
+ if (shape.choice === "refactor-first") return {
680
+ reference: tip.hash,
681
+ shape
682
+ };
555
683
  rejection = {
556
684
  strays: shape.strays,
557
685
  p: shape.judgment?.answers.shape?.p ?? 0
@@ -571,14 +699,32 @@ const decide = () => scope("seam", async () => {
571
699
  retries = 0;
572
700
  }
573
701
  });
574
- const seamBuild = async (tier) => {
702
+ const seamBuild = async (base) => {
575
703
  const { reference, shape } = await decide();
576
- if (shape !== void 0 && !await refactorFirst(shape, { reference })) await scope("seam", () => human("by-hand", {
577
- message: seamByHandMessage(shape.strays, shape.unreadable, reference),
578
- label: "Reviewing unreadable files by hand",
579
- acceptClean: true
580
- }));
581
- await implement(reference, tier);
704
+ if (shape === void 0) return {
705
+ reference,
706
+ base
707
+ };
708
+ if (shape.unreadable.length > 0) {
709
+ await scope("seam", () => human("by-hand", {
710
+ message: seamByHandMessage(shape.strays, shape.unreadable, reference),
711
+ label: "Reviewing unreadable files by hand",
712
+ acceptClean: true
713
+ }));
714
+ return {
715
+ reference,
716
+ base
717
+ };
718
+ }
719
+ if (refactorStrays(shape).length === 0) return {
720
+ reference,
721
+ base
722
+ };
723
+ await slice("refactor", () => reviewedRefactor(shape, { reference }));
724
+ return {
725
+ reference,
726
+ base: head()
727
+ };
582
728
  };
583
729
  //#endregion
584
730
  //#region src/workflows/feature/feature.ts
@@ -587,44 +733,49 @@ const unwind = () => {
587
733
  const commit = head();
588
734
  return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
589
735
  };
590
- /**
591
- * Undo the human's review-round code edits. A path changed since the human
592
- * left it is not overwritten; requireRevert names it instead.
593
- */
594
- const reUnwind = async (feedback) => {
595
- const { base, restoreFrom, edited } = feedback;
596
- await run("re-unwind", restoreScript(base, {
597
- restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
598
- remove: edited.filter((c) => c.status === "added").map((c) => c.path)
599
- }, restoreFrom), {
600
- label: "Re-unwinding your review edit",
601
- file: REVIEW,
602
- base
603
- });
604
- requireRevert(edited, base);
605
- };
606
736
  /** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
607
- const planAndBuild = async (firstBase) => {
737
+ const planAndBuild = async (firstBase, firstLap = "first") => {
738
+ if (vars.flag_naive_pass === "1") return naivePlanAndBuild(firstBase);
608
739
  let base = firstBase;
740
+ let lap = firstLap;
609
741
  for (;;) {
610
742
  await design(base);
611
- const tier = riskTierOn() ? recordRisk() : void 0;
612
- if (vars.flag_naive_pass === "1") await seamBuild(tier);
613
- else {
614
- await architecture();
615
- await packages();
616
- }
617
- const outcome = await buildTail(false, base, true);
743
+ await architecture();
744
+ await packages();
745
+ const outcome = await buildTail(false, base, true, lap === "first" ? {} : { lap });
618
746
  if (outcome.verdict === "signoff") return;
619
747
  await reUnwind(outcome);
620
748
  base = outcome.base;
749
+ lap = "feedback";
750
+ }
751
+ };
752
+ const naivePlanAndBuild = async (firstBase) => {
753
+ await design(firstBase);
754
+ const tier = riskTierOn() ? recordRisk() : void 0;
755
+ const seam = await seamBuild(firstBase);
756
+ await slice("feature", async () => {
757
+ await implement(seam.reference, tier);
758
+ await featureFeedback(await buildTail(false, seam.base, true, { lensBase: seam.base }), seam.base);
759
+ });
760
+ };
761
+ const featureFeedback = async (first, lensBase) => {
762
+ let outcome = first;
763
+ while (outcome.verdict === "feedback") {
764
+ await reUnwind(outcome);
765
+ await design(outcome.base);
766
+ await implement(void 0, riskTierOn() ? recordRisk() : void 0, true);
767
+ outcome = await buildTail(false, outcome.base, true, {
768
+ lap: "feedback",
769
+ ...lensBase === void 0 ? {} : { lensBase }
770
+ });
621
771
  }
622
772
  };
623
- /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
773
+ /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
624
774
  const afterTail = async (outcome) => {
625
775
  if (outcome.verdict === "signoff") return;
776
+ if (vars.flag_naive_pass === "1") return featureFeedback(outcome);
626
777
  await reUnwind(outcome);
627
- await planAndBuild(outcome.base);
778
+ await planAndBuild(outcome.base, "feedback");
628
779
  };
629
780
  /** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
630
781
  const ordinaryStart = async () => {
@@ -648,4 +799,4 @@ const feature = shipped(ordinaryStart);
648
799
  //#region src/workflows/feature/index.ts
649
800
  var feature_default = feature;
650
801
  //#endregion
651
- export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, implement, judgeSeam, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, refactorFirst, requireDeclaredTests, requireNoNewFlags, requireWithin, seamBuild, skills, steering, summary, triage, unwind };
802
+ export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, implement, judgeSeam, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireDeclaredTests, requireNoNewFlags, requireNoTestFiles, requireWithin, reviewedRefactor, seamBuild, skills, steering, summary, triage, unwind };
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { E as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, ct as defaults, g as REVIEW, i as skills, lt as envDefaults, n as summary, o as shipped, r as access, s as baseline, t as steering, v as coder } from "../chunks/reserved-DeBS4Ymh.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-B2SER2Ih.mjs";
3
+ import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-DoHUII2D.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C6YBG5zh.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,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { ct as defaults, et as reviewGateBlockedMessage, i as skills, l as gate, lt as envDefaults, n as summary, o as shipped, r as access, t as steering } from "../chunks/reserved-DeBS4Ymh.mjs";
3
- import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-B2SER2Ih.mjs";
2
+ import { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-DoHUII2D.mjs";
3
+ import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C6YBG5zh.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { a as flagShipped, ct as defaults, ft as flipProblems, i as skills, l as gate, lt as envDefaults, mt as requireFlag, n as summary, p as FEEDBACK, r as access, t as steering, ut as flagMentions, v as coder } from "../chunks/reserved-DeBS4Ymh.mjs";
2
+ import { a as flagShipped, dt as defaults, ft as envDefaults, gt as flipProblems, i as skills, l as gate, mt as flagMentions, n as summary, p as FEEDBACK, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-DoHUII2D.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.";