@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +28 -17
- package/claude/hooks/assist.ts +1 -1
- package/claude/hooks/register.tsx +1 -1
- package/claude/hooks/ship.ts +963 -143
- package/dist/chunks/{reserved-DeBS4Ymh.mjs → reserved-DoHUII2D.mjs} +206 -121
- package/dist/chunks/{tail-B2SER2Ih.mjs → tail-C6YBG5zh.mjs} +19 -12
- package/dist/testkit.mjs +2 -1
- package/dist/workflow-kit.mjs +51 -21
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +247 -96
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +528 -262
- package/package.json +1 -1
- package/src/testkit/TestRepo.ts +3 -0
- package/src/testkit/index.ts +1 -1
- package/src/testkit/steps.ts +44 -0
- package/src/workflow-kit/review.test.ts +366 -8
- package/src/workflow-kit/review.ts +109 -26
- package/src/workflows/feature/feature.test.ts +313 -1
- package/src/workflows/feature/feature.ts +48 -43
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +54 -24
- package/src/workflows/feature/refactor-review.feature +1706 -0
- package/src/workflows/feature/refactor.test.ts +592 -2
- package/src/workflows/feature/refactor.ts +160 -42
- package/src/workflows/feature/review.test.ts +322 -5
- package/src/workflows/feature/review.ts +77 -10
- package/src/workflows/feature/seam-rejection.feature +4 -0
- package/src/workflows/feature/seam.feature +3 -4
- package/src/workflows/feature/seam.test.ts +196 -1
- package/src/workflows/feature/seam.ts +23 -9
- package/src/workflows/feature/steps.test.ts +54 -2
- package/src/workflows/feature/steps.ts +45 -9
- package/src/workflows/feature/tier.test.ts +4 -0
- package/src/workflows/feature/tier.ts +2 -20
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +97 -0
- package/src/workflows/shared/access.test.ts +31 -1
- package/src/workflows/shared/access.ts +7 -1
- package/src/workflows/shared/fences.ts +19 -0
- package/src/workflows/shared/health.ts +32 -12
- package/src/workflows/shared/options.test.ts +18 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/prose.ts +4 -2
- package/src/workflows/shared/skills.test.ts +20 -1
- package/src/workflows/shared/skills.ts +43 -32
- package/src/workflows/shared/steps.ts +33 -19
- package/src/workflows/shared/tail.test.ts +84 -1
- package/src/workflows/shared/tail.ts +30 -12
- package/src/workflows/shared/text.test.ts +266 -4
- package/src/workflows/shared/text.ts +183 -70
- package/src/workflows/shared/vars.ts +12 -2
package/dist/workflows.mjs
CHANGED
|
@@ -77,6 +77,20 @@ const scope = async (scope, fn) => {
|
|
|
77
77
|
context.popScope();
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* Mark the span `fn` covers as one named slice of the process. Lands no
|
|
82
|
+
* commit, prefixes no step name, opens no memory scope.
|
|
83
|
+
*/
|
|
84
|
+
const slice = async (name, fn) => {
|
|
85
|
+
const context = ctx();
|
|
86
|
+
if (typeof context.enterSlice !== "function") return fn();
|
|
87
|
+
context.enterSlice(name);
|
|
88
|
+
try {
|
|
89
|
+
return await fn();
|
|
90
|
+
} finally {
|
|
91
|
+
context.leaveSlice?.();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
80
94
|
const read = (path) => ctx().read(path);
|
|
81
95
|
/** Every path in the tree matching `pattern` (`*` stays within a segment, `**` crosses them). */
|
|
82
96
|
const glob = (pattern) => ctx().glob(pattern);
|
|
@@ -396,27 +410,6 @@ const judgeDiffShape = async (name, options) => {
|
|
|
396
410
|
};
|
|
397
411
|
};
|
|
398
412
|
const displayPath = (path) => /[\x00-\x1f\x7f-\x9f]/.test(path) ? JSON.stringify(path) : path;
|
|
399
|
-
const seamReport = (shape) => {
|
|
400
|
-
const unreadable = new Set(shape.unreadable);
|
|
401
|
-
return [
|
|
402
|
-
"Seam report: the judge found the diff's shape needs a refactor first. These paths stray outside the allowlist:",
|
|
403
|
-
"",
|
|
404
|
-
...shape.strays.map((path) => `- ${displayPath(path)}${unreadable.has(path) ? " (unreadable — not judged)" : ""}`),
|
|
405
|
-
"",
|
|
406
|
-
"Landing a clean tree with `gtd land` approves this diff exactly as judged; edits are refused. To stop instead, run `gtd abandon`."
|
|
407
|
-
].join("\n");
|
|
408
|
-
};
|
|
409
|
-
/**
|
|
410
|
-
* Rest at an approval gate `name` showing the seam report — only when
|
|
411
|
-
* `shape.choice` is `refactor-first`. `land` and `reject` reach no step.
|
|
412
|
-
*/
|
|
413
|
-
const seamGate = async (name, shape) => {
|
|
414
|
-
if (shape.choice !== "refactor-first") return;
|
|
415
|
-
await human(name, {
|
|
416
|
-
message: seamReport(shape),
|
|
417
|
-
approve: true
|
|
418
|
-
});
|
|
419
|
-
};
|
|
420
413
|
//#endregion
|
|
421
414
|
//#region src/flows/factory.ts
|
|
422
415
|
const FACTORY = Symbol.for("@pmelab/gtd/workflow-factory");
|
|
@@ -16576,14 +16569,28 @@ const stripCodeThreads = (path, text) => {
|
|
|
16576
16569
|
[...(/* @__PURE__ */ new Map([["qa", qaDescriptor], ["review", reviewDescriptor]])).keys()];
|
|
16577
16570
|
//#endregion
|
|
16578
16571
|
//#region src/workflow-kit/review.ts
|
|
16579
|
-
const collect = async (p, capture) => {
|
|
16580
|
-
await p.collecting(capture);
|
|
16572
|
+
const collect = async (p, capture, round, requested) => {
|
|
16573
|
+
await p.collecting(capture, round);
|
|
16581
16574
|
if (read(p.feedback) !== void 0) return "feedback";
|
|
16582
|
-
if (changes().length === 0)
|
|
16575
|
+
if (changes().length === 0) {
|
|
16576
|
+
if (p.refusals.dropped !== void 0 && requested !== void 0 && requested.notes.length + requested.paths.length > 0) return refuse(p.refusals.dropped(requested));
|
|
16577
|
+
return "signoff";
|
|
16578
|
+
}
|
|
16583
16579
|
return refuse(p.refusals.unmatched);
|
|
16584
16580
|
};
|
|
16585
|
-
|
|
16586
|
-
|
|
16581
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16582
|
+
/**
|
|
16583
|
+
* A review with ticks and THREAD footnotes (definition plus markers) removed:
|
|
16584
|
+
* what is left is line notes and one-shot footnotes, which the triage judges.
|
|
16585
|
+
*/
|
|
16586
|
+
const notesOf = (text) => {
|
|
16587
|
+
let out = text;
|
|
16588
|
+
for (const { name } of parseThreads(text)) {
|
|
16589
|
+
const n = escapeRe(name);
|
|
16590
|
+
out = out.replace(new RegExp(`^\\[\\^${n}\\]:[^\\n]*(\\n(?: {2,}|\\t)[^\\n]*|\\n(?=\\s*\\n(?: {2,}|\\t)))*`, "gim"), "").replace(new RegExp(`\\[\\^${n}\\]`, "gi"), "");
|
|
16591
|
+
}
|
|
16592
|
+
return out.replace(/\[x\]/gi, "[ ]").replace(/\s+/g, " ").trim();
|
|
16593
|
+
};
|
|
16587
16594
|
/**
|
|
16588
16595
|
* The human gate and its reply rounds: a question gets an answer inside
|
|
16589
16596
|
* the review file and rests at the gate again, without a lap. Resolves the
|
|
@@ -16601,7 +16608,7 @@ const converse = async (p, base, collectedBefore) => {
|
|
|
16601
16608
|
}
|
|
16602
16609
|
requireThreadsClosed(p.file);
|
|
16603
16610
|
if (!hasThreadFor("agent", p.file)) return collectedAt;
|
|
16604
|
-
await p.collecting(p.captures.edits(head$1()));
|
|
16611
|
+
await p.collecting(p.captures.edits(head$1()), "reply");
|
|
16605
16612
|
collectedAt = head$1();
|
|
16606
16613
|
requireReplies(p.file);
|
|
16607
16614
|
if (!hasThreadFor("human", p.file)) return collectedAt;
|
|
@@ -16630,7 +16637,10 @@ const routeNotes = async (p, notes, r) => {
|
|
|
16630
16637
|
if (deferred.length > 0) await p.defer(deferred);
|
|
16631
16638
|
if (edits.length > 0) {
|
|
16632
16639
|
await r.close();
|
|
16633
|
-
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt)
|
|
16640
|
+
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt), "triaged", {
|
|
16641
|
+
notes: edits,
|
|
16642
|
+
paths: []
|
|
16643
|
+
}));
|
|
16634
16644
|
}
|
|
16635
16645
|
if (nits.length > 0) {
|
|
16636
16646
|
await r.close();
|
|
@@ -16661,34 +16671,47 @@ const settledAfter = (p, collectedAt) => ({
|
|
|
16661
16671
|
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
16662
16672
|
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
16663
16673
|
});
|
|
16674
|
+
const finishNotes = async (p, before, reopened, ctx) => {
|
|
16675
|
+
const notes = reviewNotes(before, read(p.file) ?? "");
|
|
16676
|
+
if (notes.length > 0) return routeNotes(p, notes, ctx);
|
|
16677
|
+
if (reopened) return ctx.unfolded();
|
|
16678
|
+
await ctx.close();
|
|
16679
|
+
return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
|
|
16680
|
+
};
|
|
16681
|
+
const toOutcome = (verdict, rest) => verdict === "signoff" ? { verdict } : {
|
|
16682
|
+
verdict,
|
|
16683
|
+
...rest
|
|
16684
|
+
};
|
|
16685
|
+
const isReopened = (p, r) => r.settled && !r.folded && p.refusals.dropped !== void 0;
|
|
16664
16686
|
const finish = async (p, reviewed, collectedAt) => {
|
|
16665
16687
|
const round = head$1();
|
|
16666
16688
|
const since = changesSince(reviewed);
|
|
16667
16689
|
const edited = since.filter((c) => isCodeEdit(p, c));
|
|
16668
|
-
const baselineText = since.get(p.file)?.before ?? "";
|
|
16669
|
-
const record = read(p.file) ?? "";
|
|
16670
16690
|
const { folded, settled } = afterCollect(p, collectedAt);
|
|
16671
16691
|
const noted = since.some((c) => c.path === p.file);
|
|
16672
16692
|
const close = () => p.close(round);
|
|
16673
|
-
const outcome = (verdict) => verdict
|
|
16674
|
-
verdict,
|
|
16693
|
+
const outcome = (verdict) => toOutcome(verdict, {
|
|
16675
16694
|
base: round,
|
|
16676
16695
|
restoreFrom: reviewed,
|
|
16677
16696
|
edited
|
|
16678
|
-
};
|
|
16697
|
+
});
|
|
16679
16698
|
const unfolded = async () => {
|
|
16680
16699
|
await close();
|
|
16681
16700
|
return outcome(folded ? "feedback" : "signoff");
|
|
16682
16701
|
};
|
|
16683
|
-
const
|
|
16702
|
+
const reopened = isReopened(p, {
|
|
16703
|
+
settled,
|
|
16704
|
+
folded
|
|
16705
|
+
});
|
|
16706
|
+
if (settled && !reopened || edited.length === 0 && !noted) return unfolded();
|
|
16707
|
+
if (edited.length > 0) {
|
|
16684
16708
|
await close();
|
|
16685
|
-
return outcome(await collect(p,
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
return routeNotes(p, notes, {
|
|
16709
|
+
return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
|
|
16710
|
+
notes: [],
|
|
16711
|
+
paths: edited.map((c) => c.path)
|
|
16712
|
+
}));
|
|
16713
|
+
}
|
|
16714
|
+
return finishNotes(p, since.get(p.file)?.before ?? "", reopened, {
|
|
16692
16715
|
round,
|
|
16693
16716
|
close,
|
|
16694
16717
|
outcome,
|
|
@@ -16874,6 +16897,15 @@ const requireFlag = (workflow, registryPath, name, defaultOn) => {
|
|
|
16874
16897
|
};
|
|
16875
16898
|
//#endregion
|
|
16876
16899
|
//#region src/workflows/shared/vars.ts
|
|
16900
|
+
/** The refactor guard's fixed list, and `seamAllow`'s default minus `.gtd/**`. */
|
|
16901
|
+
const testFileGlobs = [
|
|
16902
|
+
"**/*.test.*",
|
|
16903
|
+
"**/*.spec.*",
|
|
16904
|
+
"**/*.feature",
|
|
16905
|
+
"**/tests/**",
|
|
16906
|
+
"**/test/**",
|
|
16907
|
+
"**/__tests__/**"
|
|
16908
|
+
];
|
|
16877
16909
|
const defaultFlagRegistry = "src/flags/index.ts";
|
|
16878
16910
|
/** Process settings. */
|
|
16879
16911
|
const defaults = {
|
|
@@ -16883,10 +16915,11 @@ const defaults = {
|
|
|
16883
16915
|
reviewNoteActionable: "0.7",
|
|
16884
16916
|
reviewSeverityMinP: "0.7",
|
|
16885
16917
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
16886
|
-
seamAllow:
|
|
16918
|
+
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
16887
16919
|
flag_naive_pass: "0",
|
|
16888
16920
|
flag_risk_tier: "0",
|
|
16889
|
-
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
|
|
16921
|
+
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
16922
|
+
refactorQualityReviews: "correctness, ponytail-review, conventions"
|
|
16890
16923
|
};
|
|
16891
16924
|
/** Environment settings. */
|
|
16892
16925
|
const envDefaults = {
|
|
@@ -17115,11 +17148,12 @@ const footnoteRules = `- Leave a footnote anywhere: mark the exact spot with \`[
|
|
|
17115
17148
|
- The same conversation works as \`// H: <question>\` comment lines in a
|
|
17116
17149
|
file changed in this process (\`#\`, \`--\` or \`;\` by language); the phone UI does not show them,
|
|
17117
17150
|
and \`gtd check --open-threads\` lists the open ones`;
|
|
17118
|
-
const
|
|
17151
|
+
const footnoteAnchor = `- A footnote (\`[^name]\` plus its \`[^name]:\` definition) is a comment on
|
|
17119
17152
|
its exact anchor — fold it in as a mandatory concern described
|
|
17120
17153
|
against that anchor's hunk or paragraph, never flattened into a
|
|
17121
17154
|
whole-file remark
|
|
17122
|
-
|
|
17155
|
+
`;
|
|
17156
|
+
const footnoteFoldIn = `${footnoteAnchor}- A one-shot footnote needs no reply: fold it in and DELETE it in this
|
|
17123
17157
|
same turn — marker and definition together. Never re-read a footnote
|
|
17124
17158
|
already acted on. A one-shot footnote that asks a question becomes a
|
|
17125
17159
|
thread instead: rewrite its definition as \`- H: <its body>\` then
|
|
@@ -17504,14 +17538,17 @@ ${fixFeedbackPrompt}
|
|
|
17504
17538
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
17505
17539
|
`;
|
|
17506
17540
|
const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
|
|
17507
|
-
- Change only these paths, nothing else
|
|
17508
|
-
one of them:
|
|
17509
|
-
${strays.map((path) => ` - \`${path}\``).join("\n")}
|
|
17541
|
+
- Change only these paths, nothing else, and never a test file even
|
|
17542
|
+
when it is one of them:
|
|
17543
|
+
${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
17510
17544
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17511
17545
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17512
|
-
const
|
|
17546
|
+
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
17547
|
+
const refactorBuildingPrompt = (pkg, strays, pattern, reference, feedback = false) => `${stateFileRules}
|
|
17513
17548
|
- The package to implement is \`${pkg}\`; never delete it
|
|
17514
|
-
${
|
|
17549
|
+
${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
17550
|
+
last refactor. Act on it, within the rules below
|
|
17551
|
+
` : ""}${refactorRules(strays, pattern)}
|
|
17515
17552
|
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
17516
17553
|
earlier attempt at this change. Read it for ideas; never apply,
|
|
17517
17554
|
cherry-pick or check it out — write a fresh refactor
|
|
@@ -17530,7 +17567,7 @@ verdict — identical, new-failure, or progress — or land (with or
|
|
|
17530
17567
|
without an edit of your own) to accept the conservative default
|
|
17531
17568
|
(retry the fix) with no verdict recorded.
|
|
17532
17569
|
`;
|
|
17533
|
-
const healthDescribePrompt = () => `${stateFileRules}
|
|
17570
|
+
const healthDescribePrompt = (rules) => `${stateFileRules}
|
|
17534
17571
|
- The only state file this turn writes is \`.gtd/ESCALATION.md\`
|
|
17535
17572
|
- Read \`.gtd/FEEDBACK.md\` (this round's failing check output),
|
|
17536
17573
|
earlier rounds' from history (\`git log -p -- .gtd/FEEDBACK.md\` —
|
|
@@ -17540,7 +17577,7 @@ const healthDescribePrompt = () => `${stateFileRules}
|
|
|
17540
17577
|
attempts did not resolve it, and concrete suggested approaches to
|
|
17541
17578
|
solve it
|
|
17542
17579
|
- Never fix the code yourself — this turn only writes the document
|
|
17543
|
-
`;
|
|
17580
|
+
${withRules(rules)}`;
|
|
17544
17581
|
const escalationSystem = () => `${escalationPersona}
|
|
17545
17582
|
|
|
17546
17583
|
${agentConduct}`;
|
|
@@ -17560,21 +17597,21 @@ Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
|
|
|
17560
17597
|
turn, or land untouched to give it one more attempt at the same
|
|
17561
17598
|
analysis.
|
|
17562
17599
|
`;
|
|
17563
|
-
const buildFixPrompt = () => `${stateFileRules}
|
|
17600
|
+
const buildFixPrompt = (rules) => `${stateFileRules}
|
|
17564
17601
|
${fixFeedbackPrompt}
|
|
17565
|
-
- Leave everything uncommitted — do not commit
|
|
17602
|
+
${withRules(rules)}- Leave everything uncommitted — do not commit
|
|
17566
17603
|
`;
|
|
17567
17604
|
const finisherSystem = () => `${finisherPersona}
|
|
17568
17605
|
|
|
17569
17606
|
${agentConduct}`;
|
|
17570
|
-
const buildFixQualityPrompt = () => `${stateFileRules}
|
|
17607
|
+
const buildFixQualityPrompt = (rules) => `${stateFileRules}
|
|
17571
17608
|
- Read \`.gtd/QUALITY.md\` — one \`## \` chunk per finding a quality
|
|
17572
17609
|
dimension wrote. Merge duplicate findings across dimensions
|
|
17573
17610
|
FIRST, then fix every finding in every chunk, blocking or not
|
|
17574
17611
|
- When findings conflict, missing test signal beats line count —
|
|
17575
17612
|
a test is never deleted to satisfy a simplification finding
|
|
17576
17613
|
- Delete \`.gtd/QUALITY.md\` once every finding is resolved
|
|
17577
|
-
- Leave everything else uncommitted and finish your turn
|
|
17614
|
+
${withRules(rules)}- Leave everything else uncommitted and finish your turn
|
|
17578
17615
|
`;
|
|
17579
17616
|
/** Brief for a lens the workflow defines itself; a lens with none is just a skill of that name. */
|
|
17580
17617
|
const correctnessBrief = `- Trace partial-failure and retry paths: what a step that fails
|
|
@@ -17593,18 +17630,18 @@ const conventionsBrief = `- Read every \`AGENTS.md\` and \`CLAUDE.md\` in the re
|
|
|
17593
17630
|
- Every violation of them in the change is a finding — quote the
|
|
17594
17631
|
rule it breaks`;
|
|
17595
17632
|
const specChallengeBrief = `- Find this process's planning documents in history:
|
|
17596
|
-
\`git log <start>..HEAD\` (\`<start>\` is the
|
|
17633
|
+
\`git log <start>..HEAD\` (\`<start>\` is the process start named in parentheses above, NOT the review base) over the steering directory
|
|
17597
17634
|
(\`.gtd/\`), then \`git show\` the last version of the
|
|
17598
17635
|
requirements, architecture and package files
|
|
17599
17636
|
- Flag a spec decision that conflicts with a system invariant — an
|
|
17600
17637
|
existing test, a documented constraint, or a data invariant
|
|
17601
17638
|
other code relies on — naming the decision and the invariant
|
|
17602
17639
|
- With no planning documents in history, write nothing`;
|
|
17603
|
-
const buildQualityReviewingPrompt = (lens, brief) => `${stateFileRules}
|
|
17640
|
+
const buildQualityReviewingPrompt = (lens, brief, base = start()) => `${stateFileRules}
|
|
17604
17641
|
- The only state file this turn writes is \`.gtd/QUALITY.md\` — no
|
|
17605
17642
|
other files for notes or output
|
|
17606
|
-
- Review the whole assembled change, from \`${
|
|
17607
|
-
to the working tree, through this ONE quality lens only —
|
|
17643
|
+
- Review the whole assembled change, from \`${base}\`
|
|
17644
|
+
to the working tree (this process started at \`${start()}\`), through this ONE quality lens only —
|
|
17608
17645
|
\`${lens}\`
|
|
17609
17646
|
- Trace, do not skim: follow the order of external calls against
|
|
17610
17647
|
the resume/retry logic. On a large change, read the touched code
|
|
@@ -17703,11 +17740,12 @@ A footnote works the same way here as a line note:
|
|
|
17703
17740
|
${footnoteRules}
|
|
17704
17741
|
Deleting \`.gtd/REVIEW.md\` is refused.
|
|
17705
17742
|
`;
|
|
17706
|
-
const reviewEditsCapture = (commit) => `This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
17743
|
+
const reviewEditsCapture = (commit, from) => `This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
17707
17744
|
|
|
17708
17745
|
Commit: ${commit}
|
|
17709
|
-
The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are
|
|
17710
|
-
in that commit's other paths. Run: git show ${commit}
|
|
17746
|
+
The human's notes are in .gtd/REVIEW.md at this commit. ${from === void 0 ? `Any hand edits are
|
|
17747
|
+
in that commit's other paths. Run: git show ${commit}` : `Any hand edits landed
|
|
17748
|
+
since ${from}. Run: git diff ${from} ${commit}`}
|
|
17711
17749
|
`;
|
|
17712
17750
|
const reviewNotesCapture = (commit) => `This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
17713
17751
|
|
|
@@ -17732,19 +17770,18 @@ const buildReviewAnswerQuestionsPrompt = (notes) => `${stateFileRules}
|
|
|
17732
17770
|
The question notes are:
|
|
17733
17771
|
|
|
17734
17772
|
${notesCapture(notes)}`;
|
|
17735
|
-
const buildReviewFixNitsPrompt = (notes) => `${stateFileRules}
|
|
17773
|
+
const buildReviewFixNitsPrompt = (notes, rules) => `${stateFileRules}
|
|
17736
17774
|
- Fix every nit below in this one turn, all together
|
|
17737
17775
|
- Leave \`.gtd/REVIEW.md\` untouched
|
|
17738
|
-
- Leave everything uncommitted and finish your turn
|
|
17776
|
+
${withRules(rules)}- Leave everything uncommitted and finish your turn
|
|
17739
17777
|
|
|
17740
17778
|
The nit notes are:
|
|
17741
17779
|
|
|
17742
17780
|
${notesCapture(notes)}`;
|
|
17743
|
-
const buildReviewFixRisksPrompt = (notes) => `${stateFileRules}
|
|
17781
|
+
const buildReviewFixRisksPrompt = (notes, rules) => `${stateFileRules}
|
|
17744
17782
|
- Fix every risk below in this one turn, all together
|
|
17745
|
-
- Where a risk is behavioural, add a test that fails without the fix
|
|
17746
|
-
- Leave
|
|
17747
|
-
- Leave everything uncommitted and finish your turn
|
|
17783
|
+
${rules === void 0 ? "- Where a risk is behavioural, add a test that fails without the fix\n" : ""}- Leave \`.gtd/REVIEW.md\` untouched — the re-review rewrites it
|
|
17784
|
+
${withRules(rules)}- Leave everything uncommitted and finish your turn
|
|
17748
17785
|
|
|
17749
17786
|
The risk notes are:
|
|
17750
17787
|
|
|
@@ -17821,39 +17858,36 @@ Also judge each note's severity, one id per note: \`note-<n>.security\`,
|
|
|
17821
17858
|
\`note-<n>.reversibility\`, \`note-<n>.blastRadius\` and
|
|
17822
17859
|
\`note-<n>.newConcept\`.
|
|
17823
17860
|
`;
|
|
17824
|
-
const
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
label into the concern folded from it
|
|
17845
|
-
- Nit notes were already fixed: never re-raise a fixed nit as a concern
|
|
17846
|
-
- Finish by running \`gtd check review .gtd/REVIEW.md\` and fix
|
|
17847
|
-
what it reports
|
|
17848
|
-
|
|
17849
|
-
The raw review material is:
|
|
17850
|
-
|
|
17851
|
-
${capture}
|
|
17852
|
-
It names a commit. Work from what you already reviewed if you
|
|
17853
|
-
wrote today's review earlier this conversation; otherwise read
|
|
17854
|
-
that commit's diff yourself first.
|
|
17861
|
+
const closedFold = (round, target) => round === "edits" ? `- Fold every hand-edit and every note the human left that round into
|
|
17862
|
+
\`${target}\`
|
|
17863
|
+
` : round === "notes" ? `- Fold each note that asks for a change into \`${target}\`
|
|
17864
|
+
` : `- Fold each \`edit\` note the capture lists, no other note, and every
|
|
17865
|
+
hand-edit, into \`${target}\`
|
|
17866
|
+
`;
|
|
17867
|
+
/** `refactor` retargets the prompt at `.gtd/REFACTOR.md` and fences it to the strays. A concern outside the fence is not folded; if nothing at all is written the landing is refused, but a partly buildable round drops the rest silently. */
|
|
17868
|
+
const buildReviewCollectingPrompt = (capture, refactor) => {
|
|
17869
|
+
const target = refactor ? ".gtd/REFACTOR.md" : ".gtd/REQUIREMENTS.md";
|
|
17870
|
+
const fence = refactor ? `- The next refactor may change only these paths, and never a test file
|
|
17871
|
+
or a flag read:
|
|
17872
|
+
${refactor.strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
17873
|
+
A concern needing any other path, a test file, a new or added flag
|
|
17874
|
+
read, or a change in behaviour (that belongs to the feature, not this
|
|
17875
|
+
refactor) cannot be built: do not fold it
|
|
17876
|
+
` : "";
|
|
17877
|
+
const actionable = refactor ? `The round is actionable if the human left a note on
|
|
17878
|
+
\`.gtd/REVIEW.md\`, added a code comment, or hand-edited code this
|
|
17879
|
+
round (a sketch of what they were reaching for, re-derived from
|
|
17880
|
+
scratch, never final).
|
|
17855
17881
|
|
|
17856
|
-
|
|
17882
|
+
- If actionable and a concern can be built: write \`.gtd/REFACTOR.md\`.
|
|
17883
|
+
Then finish
|
|
17884
|
+
- A round with an \`edit\` note or a hand-edit MUST write
|
|
17885
|
+
\`.gtd/REFACTOR.md\`: landing without it is refused, and the only
|
|
17886
|
+
way out is \`gtd abandon\`. Fold whatever can be built, and name in
|
|
17887
|
+
your final message each concern left out
|
|
17888
|
+
- Finish without writing anything only when no \`edit\` note and no
|
|
17889
|
+
hand-edit asked for a change — changing nothing is the sign-off
|
|
17890
|
+
` : `The round is actionable if any of these hold:
|
|
17857
17891
|
|
|
17858
17892
|
- The human left a note on \`.gtd/REVIEW.md\`. A note is a mandatory
|
|
17859
17893
|
concern below
|
|
@@ -17883,6 +17917,50 @@ actionability, and never dismiss a real note or edit as approval.
|
|
|
17883
17917
|
- If not: finish without writing anything — changing nothing is
|
|
17884
17918
|
the sign-off
|
|
17885
17919
|
`;
|
|
17920
|
+
return `${styleBlock}
|
|
17921
|
+
|
|
17922
|
+
${styleFormatContract}
|
|
17923
|
+
|
|
17924
|
+
You are judging and classifying a round of review feedback${refactor ? " on a\nbehaviour-neutral refactor" : ""}.
|
|
17925
|
+
|
|
17926
|
+
${stateFileRules}
|
|
17927
|
+
${refactor && refactor.round !== "reply" ? footnoteAnchor : `${footnoteFoldIn}${codeThreadReplies()}`}
|
|
17928
|
+
${refactor ? refactor.round === "reply" ? `- This turn replies inside \`.gtd/REVIEW.md\` (thread replies only; the
|
|
17929
|
+
file stays in the tree) — you classify, you do not build. Write
|
|
17930
|
+
exactly one \`- A:\` reply to each thread whose last entry is a
|
|
17931
|
+
\`- H:\` question, then run \`gtd check review .gtd/REVIEW.md\`
|
|
17932
|
+
- Fold a concluded thread into \`${target}\` only when it asks for a
|
|
17933
|
+
change, then delete it from \`.gtd/REVIEW.md\`; a concluded thread
|
|
17934
|
+
that asks for none is just deleted
|
|
17935
|
+
- Fold each one-shot footnote and note that asks for a change into
|
|
17936
|
+
\`${target}\` and delete it in this same turn; praise is just deleted
|
|
17937
|
+
` : `- This turn writes \`${target}\` (the folded concerns) — you classify,
|
|
17938
|
+
you do not build. \`.gtd/REVIEW.md\` is already closed: do not
|
|
17939
|
+
recreate it or reply in it
|
|
17940
|
+
${closedFold(refactor.round, target)}` : `- This turn writes \`${target}\` (the folded concerns) and
|
|
17941
|
+
replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
|
|
17942
|
+
in the tree) — you classify, you do not build
|
|
17943
|
+
- Fold every concluded thread, ticked answer and hand-edit — and each
|
|
17944
|
+
\`edit\` note the capture lists, no other note — into
|
|
17945
|
+
\`${target}\` and delete the folded threads from
|
|
17946
|
+
\`.gtd/REVIEW.md\`; append one \`- A:\` reply to each thread
|
|
17947
|
+
whose last entry is a \`- H:\` question
|
|
17948
|
+
`}- When the capture names an answering commit, carry each answered
|
|
17949
|
+
question into \`${target}\`'s \`## Answered Questions\` as
|
|
17950
|
+
\`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
|
|
17951
|
+
- Keep capture order, and carry each note's \`(tier: reason)\`
|
|
17952
|
+
label into the concern folded from it
|
|
17953
|
+
- Nit notes were already fixed: never re-raise a fixed nit as a concern
|
|
17954
|
+
${fence}${refactor ? "" : "- Finish by running `gtd check review .gtd/REVIEW.md` and fix\n what it reports\n"}
|
|
17955
|
+
The raw review material is:
|
|
17956
|
+
|
|
17957
|
+
${capture}
|
|
17958
|
+
It names a commit. Work from what you already reviewed if you
|
|
17959
|
+
wrote today's review earlier this conversation; otherwise read
|
|
17960
|
+
that commit's diff yourself first.
|
|
17961
|
+
|
|
17962
|
+
${actionable}`;
|
|
17963
|
+
};
|
|
17886
17964
|
const scenariosPrompt = (removed) => `- When a scenario in a \`.feature\` file was added or changed since the
|
|
17887
17965
|
base, the first chunk is \`## Scenarios\`: one pointer per such
|
|
17888
17966
|
scenario, its note saying in plain words what behaviour it now pins
|
|
@@ -17894,7 +17972,12 @@ ${removed.map(({ path, lines }) => lines.map((a) => ` ${path} — ${a.scenari
|
|
|
17894
17972
|
put its \`Weakened:\` note on a pointer of the chunk covering its
|
|
17895
17973
|
removal
|
|
17896
17974
|
`}`;
|
|
17897
|
-
const
|
|
17975
|
+
const lapRange = {
|
|
17976
|
+
first: "it covers everything since the base.",
|
|
17977
|
+
feedback: "it starts at the previous review's boundary, so it covers only what is new since the previous review.",
|
|
17978
|
+
refactor: "it covers the whole refactor since it started, on every round."
|
|
17979
|
+
};
|
|
17980
|
+
const buildReviewReviewingPrompt = (base, lap, carry, removed = []) => `${styleBlock}
|
|
17898
17981
|
|
|
17899
17982
|
${styleFormatContract}
|
|
17900
17983
|
|
|
@@ -17938,8 +18021,7 @@ review the changes:
|
|
|
17938
18021
|
— that parses as a second pointer, not a note
|
|
17939
18022
|
${scenariosPrompt(removed)}No diff is given — read the changes yourself. The range runs
|
|
17940
18023
|
from \`${base}\` to the working tree (committed turns
|
|
17941
|
-
plus anything pending);
|
|
17942
|
-
review's boundary, so it covers only what's new.
|
|
18024
|
+
plus anything pending); ${lapRange[lap]}
|
|
17943
18025
|
|
|
17944
18026
|
${carry === void 0 ? "" : `Carry-over: commit \`${carry}\` answered questions inline in the previous
|
|
17945
18027
|
review (each \`A: \` line under the note it answers). Copy each answered
|
|
@@ -18037,13 +18119,15 @@ re-implement. It changed these files outside the expected set:
|
|
|
18037
18119
|
${listed(rejection.strays)}
|
|
18038
18120
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
18039
18121
|
`}`;
|
|
18040
|
-
const implementBuildingPrompt = (reference, tier) => `${stateFileRules}
|
|
18122
|
+
const implementBuildingPrompt = (reference, tier, amend) => `${stateFileRules}
|
|
18041
18123
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18042
|
-
|
|
18043
|
-
-
|
|
18124
|
+
${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18125
|
+
the re-derived requirements instead of rewriting it
|
|
18126
|
+
` : `- Implement them properly, fitting the codebase's seams and conventions
|
|
18127
|
+
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
18044
18128
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
18045
18129
|
the change fresh
|
|
18046
|
-
${tier === "low" ? "- Add no release flag — this change is low risk tier" : `- Put the new behaviour behind a new release flag, following the
|
|
18130
|
+
`}${tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
18047
18131
|
repository's flag conventions`}
|
|
18048
18132
|
- Write the tests first, then the code that makes them pass
|
|
18049
18133
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
@@ -18077,6 +18161,9 @@ ${listed(strays, unreadable)}
|
|
|
18077
18161
|
See them with \`git show ${reference} -- <path>\` and review by hand. Land a
|
|
18078
18162
|
clean tree to go on to the implementation, which runs next.
|
|
18079
18163
|
`;
|
|
18164
|
+
const refactorDroppedRefusal = (requested) => `gtd land: refactor-feedback: the refactor review asked for a change but .gtd/REFACTOR.md was not written, so nothing can be built from it:
|
|
18165
|
+
${[...requested.notes.map((n) => ` - ${n.id} (${n.anchor}): ${n.text}`), ...requested.paths.map((path) => ` - hand-edited ${path}`)].join("\n")}
|
|
18166
|
+
The only way out is \`gtd abandon\`, which throws away the naive pass, the refactor and its review.`;
|
|
18080
18167
|
//#endregion
|
|
18081
18168
|
//#region src/workflows/shared/steps.ts
|
|
18082
18169
|
const FEEDBACK = ".gtd/FEEDBACK.md";
|
|
@@ -18084,16 +18171,17 @@ const ESCALATION = ".gtd/ESCALATION.md";
|
|
|
18084
18171
|
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
18085
18172
|
const ARCHITECTURE = ".gtd/ARCHITECTURE.md";
|
|
18086
18173
|
const REVIEW = ".gtd/REVIEW.md";
|
|
18174
|
+
const REFACTOR = ".gtd/REFACTOR.md";
|
|
18087
18175
|
const QUALITY = ".gtd/QUALITY.md";
|
|
18088
18176
|
const planner = () => env.plannerModel ?? "";
|
|
18089
18177
|
const coder = () => env.coderModel ?? "";
|
|
18090
|
-
const fixCheck = () => agentWithSkills("fix", buildFixPrompt(), {
|
|
18178
|
+
const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
|
|
18091
18179
|
label: "Fixing the check",
|
|
18092
18180
|
file: FEEDBACK,
|
|
18093
18181
|
model: coder(),
|
|
18094
18182
|
system: finisherSystem()
|
|
18095
18183
|
});
|
|
18096
|
-
const describeEscalation = () => agentWithSkills("health.describe", healthDescribePrompt(), {
|
|
18184
|
+
const describeEscalation = (rules) => agentWithSkills("health.describe", healthDescribePrompt(rules), {
|
|
18097
18185
|
label: "Describing the escalation",
|
|
18098
18186
|
file: FEEDBACK,
|
|
18099
18187
|
model: coder(),
|
|
@@ -18128,14 +18216,14 @@ const builtInLenses = {
|
|
|
18128
18216
|
}
|
|
18129
18217
|
};
|
|
18130
18218
|
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
18131
|
-
const reviewQuality = (lens) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief), {
|
|
18219
|
+
const reviewQuality = (lens, base) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base), {
|
|
18132
18220
|
label: "Reviewing (one quality lens)",
|
|
18133
18221
|
file: QUALITY,
|
|
18134
18222
|
model: planner(),
|
|
18135
18223
|
system: reviewerSystem(),
|
|
18136
18224
|
allowEmpty: true
|
|
18137
18225
|
});
|
|
18138
|
-
const fixQuality = () => agentWithSkills("fix.quality.fixing", buildFixQualityPrompt(), {
|
|
18226
|
+
const fixQuality = (rules) => agentWithSkills("fix.quality.fixing", buildFixQualityPrompt(rules), {
|
|
18139
18227
|
label: "Fixing quality findings",
|
|
18140
18228
|
file: QUALITY,
|
|
18141
18229
|
model: coder(),
|
|
@@ -18143,7 +18231,7 @@ const fixQuality = () => agentWithSkills("fix.quality.fixing", buildFixQualityPr
|
|
|
18143
18231
|
allowEmpty: true
|
|
18144
18232
|
});
|
|
18145
18233
|
/** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
|
|
18146
|
-
const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry, removedAssertions(changesSince(base))), {
|
|
18234
|
+
const reviewing = (base, carry, lap = "first") => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, lap, carry, removedAssertions(changesSince(base))), {
|
|
18147
18235
|
label: "Reviewing",
|
|
18148
18236
|
file: REVIEW,
|
|
18149
18237
|
mode: "review",
|
|
@@ -18152,13 +18240,19 @@ const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildRevi
|
|
|
18152
18240
|
base
|
|
18153
18241
|
});
|
|
18154
18242
|
/** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
|
|
18155
|
-
const fixRisks = (notes
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
|
|
18161
|
-
|
|
18243
|
+
const fixRisks = (notes, access, rules) => {
|
|
18244
|
+
const fixing = (name) => agentWithSkills(name, buildReviewFixRisksPrompt(notes, rules), {
|
|
18245
|
+
label: "Fixing the reviewer's risks",
|
|
18246
|
+
file: REVIEW,
|
|
18247
|
+
model: planner(),
|
|
18248
|
+
system: reviewerSystem(),
|
|
18249
|
+
allowEmpty: true
|
|
18250
|
+
});
|
|
18251
|
+
return access === void 0 ? fixing("review.fix.risks.fixing") : scope({
|
|
18252
|
+
name: "review.fix.risks",
|
|
18253
|
+
access
|
|
18254
|
+
}, () => fixing("fixing"));
|
|
18255
|
+
};
|
|
18162
18256
|
const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => human("review.await-review", {
|
|
18163
18257
|
message,
|
|
18164
18258
|
label: "Awaiting your review",
|
|
@@ -18181,28 +18275,31 @@ const baseline = (name, label = "Checking the baseline") => check(name, env.test
|
|
|
18181
18275
|
report: FEEDBACK,
|
|
18182
18276
|
label
|
|
18183
18277
|
});
|
|
18184
|
-
const escalationSteps = {
|
|
18278
|
+
const escalationSteps = (rules) => ({
|
|
18185
18279
|
limit: 2,
|
|
18186
|
-
describe: describeEscalation,
|
|
18280
|
+
describe: () => describeEscalation(rules),
|
|
18187
18281
|
stop: escalate,
|
|
18188
18282
|
exhausted: escalationExhausted
|
|
18189
|
-
};
|
|
18283
|
+
});
|
|
18190
18284
|
const guarded = (turn, guard) => async (...a) => {
|
|
18191
18285
|
await turn(...a);
|
|
18192
18286
|
guard?.();
|
|
18193
18287
|
};
|
|
18194
|
-
const guardedSteps = (guard, describeGuard) =>
|
|
18195
|
-
|
|
18196
|
-
|
|
18197
|
-
|
|
18198
|
-
|
|
18199
|
-
|
|
18288
|
+
const guardedSteps = (guard, describeGuard, rests = true, rules) => {
|
|
18289
|
+
const steps = escalationSteps(rules);
|
|
18290
|
+
return {
|
|
18291
|
+
limit: steps.limit,
|
|
18292
|
+
describe: guarded(guarded(steps.describe, guard), describeGuard),
|
|
18293
|
+
stop: guarded(steps.stop, rests ? guard : void 0),
|
|
18294
|
+
exhausted: guarded(steps.exhausted, rests ? guard : void 0)
|
|
18295
|
+
};
|
|
18296
|
+
};
|
|
18200
18297
|
/**
|
|
18201
18298
|
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
18202
18299
|
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
18203
18300
|
* on, the human is told the budget is spent instead.
|
|
18204
18301
|
*/
|
|
18205
|
-
const escalation = (count, guard) => escalation$1(count, guardedSteps(guard));
|
|
18302
|
+
const escalation = (count, guard, guardRests = true, rules) => escalation$1(count, guardedSteps(guard, void 0, guardRests, rules));
|
|
18206
18303
|
/** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
|
|
18207
18304
|
const retryQuestion = (comparable) => comparable ? {
|
|
18208
18305
|
id: "verdict",
|
|
@@ -18245,7 +18342,7 @@ const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
|
18245
18342
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
18246
18343
|
sameFailure,
|
|
18247
18344
|
escalations: options.escalations ?? { rounds: 0 },
|
|
18248
|
-
escalation: guardedSteps(options.guard, options.describeGuard)
|
|
18345
|
+
escalation: guardedSteps(options.guard, options.describeGuard, options.guardRests, options.rules)
|
|
18249
18346
|
});
|
|
18250
18347
|
/**
|
|
18251
18348
|
* Hold the process at `<name>.blocked` until the suite is green. `guard` runs
|
|
@@ -37583,6 +37680,7 @@ const ShippedOptions = Struct({
|
|
|
37583
37680
|
refactorFlagPattern: setting,
|
|
37584
37681
|
seamAllow: setting,
|
|
37585
37682
|
qualityReviews: setting,
|
|
37683
|
+
refactorQualityReviews: setting,
|
|
37586
37684
|
testCommand: setting,
|
|
37587
37685
|
plannerModel: setting,
|
|
37588
37686
|
coderModel: setting
|
|
@@ -37635,13 +37733,13 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
|
37635
37733
|
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
37636
37734
|
* has any.
|
|
37637
37735
|
*/
|
|
37638
|
-
const qualityLap = (guard) => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), guarded(reviewQuality, guard), QUALITY);
|
|
37736
|
+
const qualityLap = (guard, lenses, base) => qualityLap$1(lenses ?? lensesOf(vars.qualityReviews ?? ""), guarded((lens) => reviewQuality(lens, base), guard), QUALITY);
|
|
37639
37737
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
37640
|
-
const fixQualityFindings = (escalations, guard) => fixFindings({
|
|
37641
|
-
fix: guarded(fixQuality, guard),
|
|
37738
|
+
const fixQualityFindings = (escalations, guard, guardRests = true, rules) => fixFindings({
|
|
37739
|
+
fix: guarded(() => fixQuality(rules), guard),
|
|
37642
37740
|
findings: QUALITY,
|
|
37643
37741
|
cap: 3,
|
|
37644
|
-
escalate: () => escalation(escalations, guard)
|
|
37742
|
+
escalate: () => escalation(escalations, guard, guardRests, rules)
|
|
37645
37743
|
});
|
|
37646
37744
|
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
37647
37745
|
const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
|
|
@@ -37656,26 +37754,33 @@ const refusePendingHandOff = () => {
|
|
|
37656
37754
|
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
37657
37755
|
* `finish` — the human review, as the calling workflow runs it. `green`: the
|
|
37658
37756
|
* caller just left the full suite green (the package loop), so no run opens it.
|
|
37757
|
+
* `guardRests: false` leaves the escalation's human rests unguarded.
|
|
37659
37758
|
*/
|
|
37660
|
-
const tail = (fixFirst, finish, green = false, guard) => scope("build", async () => {
|
|
37759
|
+
const tail = (fixFirst, finish, green = false, guard, lenses, guardRests = true, lensBase, rules) => scope("build", async () => {
|
|
37661
37760
|
const escalations = { rounds: 0 };
|
|
37662
|
-
const fix = fixCheck;
|
|
37761
|
+
const fix = () => fixCheck(rules);
|
|
37663
37762
|
const repair = async () => {
|
|
37664
37763
|
await guarded(fix, guard)();
|
|
37665
37764
|
await healthy(fix, {
|
|
37666
37765
|
fixesSoFar: 1,
|
|
37667
37766
|
escalations,
|
|
37668
|
-
guard
|
|
37767
|
+
guard,
|
|
37768
|
+
guardRests,
|
|
37769
|
+
rules
|
|
37669
37770
|
});
|
|
37670
37771
|
};
|
|
37671
37772
|
if (fixFirst) await repair();
|
|
37672
37773
|
else if (!green) await healthy(fix, {
|
|
37673
37774
|
escalations,
|
|
37674
|
-
guard
|
|
37775
|
+
guard,
|
|
37776
|
+
guardRests,
|
|
37777
|
+
rules
|
|
37675
37778
|
});
|
|
37676
|
-
if (await qualityLap(guard) === "findings") if (await fixQualityFindings(escalations, guard)) await healthy(fix, {
|
|
37779
|
+
if (await qualityLap(guard, lenses, lensBase) === "findings") if (await fixQualityFindings(escalations, guard, guardRests, rules)) await healthy(fix, {
|
|
37677
37780
|
escalations,
|
|
37678
|
-
guard
|
|
37781
|
+
guard,
|
|
37782
|
+
guardRests,
|
|
37783
|
+
rules
|
|
37679
37784
|
});
|
|
37680
37785
|
else await repair();
|
|
37681
37786
|
return finish(escalations);
|
|
@@ -37687,7 +37792,7 @@ const tail = (fixFirst, finish, green = false, guard) => scope("build", async ()
|
|
|
37687
37792
|
*/
|
|
37688
37793
|
const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
37689
37794
|
file: REVIEW,
|
|
37690
|
-
reviewing: guarded(reviewing, guard),
|
|
37795
|
+
reviewing: guarded((b, carry) => reviewing(b, carry), guard),
|
|
37691
37796
|
fixRisks: guarded(fixRisks, guard),
|
|
37692
37797
|
keepGreen: () => healthy(fixCheck, {
|
|
37693
37798
|
escalations,
|
|
@@ -37704,9 +37809,7 @@ const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
|
37704
37809
|
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
37705
37810
|
const handOffTail = (fixFirst, base, guard) => tail(fixFirst, (escalations) => handOffReview(base, escalations, guard), false, guard);
|
|
37706
37811
|
//#endregion
|
|
37707
|
-
//#region src/workflows/
|
|
37708
|
-
/** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
|
|
37709
|
-
const riskTierOn = () => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1";
|
|
37812
|
+
//#region src/workflows/shared/fences.ts
|
|
37710
37813
|
const markFenced = (text) => {
|
|
37711
37814
|
let fence;
|
|
37712
37815
|
return text.split(/\r?\n/).map((line) => {
|
|
@@ -37720,6 +37823,10 @@ const markFenced = (text) => {
|
|
|
37720
37823
|
};
|
|
37721
37824
|
});
|
|
37722
37825
|
};
|
|
37826
|
+
//#endregion
|
|
37827
|
+
//#region src/workflows/feature/tier.ts
|
|
37828
|
+
/** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
|
|
37829
|
+
const riskTierOn = () => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1";
|
|
37723
37830
|
const sectionLines = (text, title) => {
|
|
37724
37831
|
if (text === void 0) return [];
|
|
37725
37832
|
const heading = new RegExp(`^##[ \\t]+${title}[ \\t]*$`, "i");
|
|
@@ -37809,12 +37916,18 @@ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-q
|
|
|
37809
37916
|
system: reviewerSystem()
|
|
37810
37917
|
});
|
|
37811
37918
|
/** Fix every `nit` note in one turn. */
|
|
37812
|
-
const fixNits = (notes
|
|
37813
|
-
|
|
37814
|
-
|
|
37815
|
-
|
|
37816
|
-
|
|
37817
|
-
|
|
37919
|
+
const fixNits = (notes, access, rules) => {
|
|
37920
|
+
const fixing = (name) => agentWithSkills(name, buildReviewFixNitsPrompt(notes, rules), {
|
|
37921
|
+
label: "Fixing your nits",
|
|
37922
|
+
file: REVIEW,
|
|
37923
|
+
model: planner(),
|
|
37924
|
+
system: reviewerSystem()
|
|
37925
|
+
});
|
|
37926
|
+
return access === void 0 ? fixing("review.fix.nits.fixing") : scope({
|
|
37927
|
+
name: "review.fix.nits",
|
|
37928
|
+
access
|
|
37929
|
+
}, () => fixing("fixing"));
|
|
37930
|
+
};
|
|
37818
37931
|
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
37819
37932
|
const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
|
|
37820
37933
|
label: "Collecting your feedback",
|
|
@@ -37824,6 +37937,17 @@ const collecting = (capture) => agentWithSkills("review.collecting", buildReview
|
|
|
37824
37937
|
system: reviewerSystem(),
|
|
37825
37938
|
allowEmpty: true
|
|
37826
37939
|
});
|
|
37940
|
+
/** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
|
|
37941
|
+
const refactorCollecting = (capture, round, strays) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, {
|
|
37942
|
+
strays,
|
|
37943
|
+
round
|
|
37944
|
+
}), {
|
|
37945
|
+
label: "Collecting your refactor feedback",
|
|
37946
|
+
file: REFACTOR,
|
|
37947
|
+
model: planner(),
|
|
37948
|
+
system: reviewerSystem(),
|
|
37949
|
+
allowEmpty: true
|
|
37950
|
+
});
|
|
37827
37951
|
/** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
|
|
37828
37952
|
const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes, round), {
|
|
37829
37953
|
label: "Deferring could-notes to TODO.md",
|
|
@@ -38041,26 +38165,37 @@ const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
|
38041
38165
|
* another planning lap, questions are answered in place, nits fixed in one
|
|
38042
38166
|
* batch, praise dropped.
|
|
38043
38167
|
*/
|
|
38044
|
-
const review$1 = (base, escalations = { rounds: 0 }) => {
|
|
38168
|
+
const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
|
|
38169
|
+
const { guard, guardRests, fixAccess, rules } = options;
|
|
38170
|
+
const feedback = options.feedback ?? {
|
|
38171
|
+
file: ".gtd/REQUIREMENTS.md",
|
|
38172
|
+
collecting,
|
|
38173
|
+
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"
|
|
38174
|
+
};
|
|
38045
38175
|
const severities = /* @__PURE__ */ new Map();
|
|
38046
38176
|
let deferred = [];
|
|
38047
38177
|
return review$2(base, {
|
|
38048
38178
|
file: REVIEW,
|
|
38049
|
-
feedback:
|
|
38179
|
+
feedback: feedback.file,
|
|
38050
38180
|
isCode: isCode$1,
|
|
38051
38181
|
reviewing: (b, carry) => {
|
|
38052
38182
|
deferred = [];
|
|
38053
|
-
return reviewing(b, carry);
|
|
38183
|
+
return reviewing(b, carry, options.lap);
|
|
38054
38184
|
},
|
|
38055
|
-
fixRisks,
|
|
38056
|
-
keepGreen: () => healthy(fixCheck, {
|
|
38185
|
+
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
38186
|
+
keepGreen: () => healthy(() => fixCheck(rules), {
|
|
38187
|
+
escalations,
|
|
38188
|
+
guard,
|
|
38189
|
+
guardRests,
|
|
38190
|
+
rules
|
|
38191
|
+
}),
|
|
38057
38192
|
awaitReview: (b) => {
|
|
38058
38193
|
const notes = deferred;
|
|
38059
38194
|
deferred = [];
|
|
38060
38195
|
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
|
|
38061
38196
|
},
|
|
38062
38197
|
reviewMissing,
|
|
38063
|
-
collecting,
|
|
38198
|
+
collecting: feedback.collecting,
|
|
38064
38199
|
triage: triage$1(severities),
|
|
38065
38200
|
defer: (notes) => {
|
|
38066
38201
|
deferred = notes.map((n) => ({
|
|
@@ -38072,7 +38207,7 @@ const review$1 = (base, escalations = { rounds: 0 }) => {
|
|
|
38072
38207
|
return deferNotes(deferred, head$1());
|
|
38073
38208
|
},
|
|
38074
38209
|
answerQuestions: answerReviewQuestions,
|
|
38075
|
-
fixNits,
|
|
38210
|
+
fixNits: guarded((notes) => fixNits(notes, fixAccess, rules), guard),
|
|
38076
38211
|
close: closeReview,
|
|
38077
38212
|
captures: {
|
|
38078
38213
|
edits: reviewEditsCapture,
|
|
@@ -38090,21 +38225,44 @@ const review$1 = (base, escalations = { rounds: 0 }) => {
|
|
|
38090
38225
|
},
|
|
38091
38226
|
refusals: {
|
|
38092
38227
|
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
38093
|
-
unmatched:
|
|
38228
|
+
unmatched: feedback.unmatched,
|
|
38229
|
+
...feedback.dropped === void 0 ? {} : { dropped: feedback.dropped }
|
|
38094
38230
|
}
|
|
38095
38231
|
});
|
|
38096
38232
|
};
|
|
38097
38233
|
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
38098
|
-
const buildTail = (fixFirst, base, green = false) => tail(fixFirst, (escalations) => review$1(base, escalations), green);
|
|
38234
|
+
const buildTail = (fixFirst, base, green = false, options = {}) => tail(fixFirst, (escalations) => review$1(base, escalations, options), green, options.guard, options.lenses, options.guardRests, options.lensBase, options.rules);
|
|
38235
|
+
/**
|
|
38236
|
+
* Undo the human's review-round code edits. A path changed since the human
|
|
38237
|
+
* left it is not overwritten; requireRevert names it instead.
|
|
38238
|
+
*/
|
|
38239
|
+
const reUnwind = async (feedback) => {
|
|
38240
|
+
const { base, restoreFrom, edited } = feedback;
|
|
38241
|
+
await run("re-unwind", restoreScript(base, {
|
|
38242
|
+
restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
|
|
38243
|
+
remove: edited.filter((c) => c.status === "added").map((c) => c.path)
|
|
38244
|
+
}, restoreFrom), {
|
|
38245
|
+
label: "Re-unwinding your review edit",
|
|
38246
|
+
file: REVIEW,
|
|
38247
|
+
base
|
|
38248
|
+
});
|
|
38249
|
+
requireRevert(edited, base);
|
|
38250
|
+
};
|
|
38099
38251
|
//#endregion
|
|
38100
38252
|
//#region src/workflows/feature/refactor.ts
|
|
38101
|
-
const REFACTOR_PACKAGE =
|
|
38253
|
+
const REFACTOR_PACKAGE = REFACTOR;
|
|
38102
38254
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
38103
38255
|
const requireWithin = (allowed) => {
|
|
38104
38256
|
const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
|
|
38105
38257
|
if (outside.length === 0) return;
|
|
38106
38258
|
refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
|
|
38107
38259
|
};
|
|
38260
|
+
const isTestFile = (path) => testFileGlobs.some((glob) => matches(path, glob));
|
|
38261
|
+
const requireNoTestFiles = () => {
|
|
38262
|
+
const tests = changes().map((c) => c.path).filter((path) => isCode(path) && isTestFile(path));
|
|
38263
|
+
if (tests.length === 0) return;
|
|
38264
|
+
refuse(`gtd land: refactor-tests: the refactor may not change a test file — revert these:\n${tests.map((path) => ` - ${path}`).join("\n")}`);
|
|
38265
|
+
};
|
|
38108
38266
|
const flagExpression = (pattern) => {
|
|
38109
38267
|
if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
|
|
38110
38268
|
let expression;
|
|
@@ -38146,6 +38304,8 @@ const requireNoNewFlags = (pattern, since) => {
|
|
|
38146
38304
|
if (raised.length === 0) return;
|
|
38147
38305
|
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")}`);
|
|
38148
38306
|
};
|
|
38307
|
+
/** A test-file stray is never refactored: the implement turn still reads it in the naive tip. */
|
|
38308
|
+
const refactorStrays = (shape) => shape.strays.filter((path) => !isTestFile(path));
|
|
38149
38309
|
const packageLines = (strays, pattern, reference) => [
|
|
38150
38310
|
"# Behaviour-neutral refactor",
|
|
38151
38311
|
"",
|
|
@@ -38158,7 +38318,7 @@ const packageLines = (strays, pattern, reference) => [
|
|
|
38158
38318
|
"## Rules",
|
|
38159
38319
|
"",
|
|
38160
38320
|
"- The suite's outcome must not change",
|
|
38161
|
-
"- Change only the paths above
|
|
38321
|
+
"- Change only the paths above, and never a test file",
|
|
38162
38322
|
`- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
|
|
38163
38323
|
"",
|
|
38164
38324
|
"## Reference",
|
|
@@ -38166,49 +38326,125 @@ const packageLines = (strays, pattern, reference) => [
|
|
|
38166
38326
|
reference === void 0 ? "None." : `Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
|
|
38167
38327
|
""
|
|
38168
38328
|
];
|
|
38329
|
+
const demoteHeadings = (text) => {
|
|
38330
|
+
const marked = markFenced(text.trimEnd());
|
|
38331
|
+
const lines = marked.map((m) => m.line);
|
|
38332
|
+
const headings = marked.map((m) => m.fenced ? void 0 : /^(#{1,6}) /.exec(m.line)?.[1]?.length);
|
|
38333
|
+
const shift = 3 - Math.min(...headings.filter((n) => n !== void 0));
|
|
38334
|
+
return lines.map((line, i) => {
|
|
38335
|
+
const depth = headings[i];
|
|
38336
|
+
if (depth === void 0) return line;
|
|
38337
|
+
return "#".repeat(Math.min(6, depth + shift)) + line.slice(depth);
|
|
38338
|
+
});
|
|
38339
|
+
};
|
|
38169
38340
|
const deriveScript = (lines) => `mkdir -p .gtd
|
|
38170
38341
|
printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
|
|
38171
38342
|
`;
|
|
38172
|
-
|
|
38173
|
-
|
|
38343
|
+
/** Undefined for every shape that is not a readable `refactor-first`; throws on a bad flag pattern. */
|
|
38344
|
+
const plan = (shape, options) => {
|
|
38345
|
+
if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return void 0;
|
|
38174
38346
|
const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
|
|
38175
38347
|
const expression = flagExpression(pattern);
|
|
38176
|
-
const
|
|
38348
|
+
const strays = refactorStrays(shape);
|
|
38349
|
+
if (strays.length === 0) return void 0;
|
|
38177
38350
|
const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
|
|
38178
38351
|
if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
|
|
38179
|
-
|
|
38180
|
-
|
|
38181
|
-
|
|
38182
|
-
|
|
38183
|
-
}
|
|
38184
|
-
|
|
38185
|
-
|
|
38186
|
-
|
|
38187
|
-
|
|
38188
|
-
|
|
38189
|
-
|
|
38190
|
-
|
|
38191
|
-
|
|
38192
|
-
|
|
38193
|
-
|
|
38194
|
-
|
|
38352
|
+
return {
|
|
38353
|
+
pattern,
|
|
38354
|
+
strays,
|
|
38355
|
+
reference: options.reference?.trim() ? options.reference : void 0
|
|
38356
|
+
};
|
|
38357
|
+
};
|
|
38358
|
+
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
38359
|
+
requireNoTestFiles();
|
|
38360
|
+
requireWithin(strays);
|
|
38361
|
+
requireNoNewFlags(pattern, entry);
|
|
38362
|
+
};
|
|
38363
|
+
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
38364
|
+
const refactorLap = async (p, guard, feedback) => {
|
|
38365
|
+
const { pattern, strays, reference } = p;
|
|
38366
|
+
await run("deriving", deriveScript([...packageLines(strays, pattern, reference), ...feedback === void 0 ? [] : [
|
|
38367
|
+
"## Review feedback",
|
|
38368
|
+
"",
|
|
38369
|
+
...demoteHeadings(feedback),
|
|
38370
|
+
""
|
|
38371
|
+
]]), {
|
|
38372
|
+
label: "Deriving the refactor package",
|
|
38373
|
+
file: REFACTOR_PACKAGE
|
|
38374
|
+
});
|
|
38375
|
+
await agentWithSkills("building", refactorBuildingPrompt(REFACTOR_PACKAGE, strays, pattern, reference, feedback !== void 0), {
|
|
38376
|
+
label: "Refactoring",
|
|
38377
|
+
model: coder(),
|
|
38378
|
+
system: builderSystem()
|
|
38379
|
+
});
|
|
38380
|
+
guard();
|
|
38381
|
+
await healthy(async () => {
|
|
38382
|
+
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
38383
|
+
label: "Fixing the check",
|
|
38384
|
+
file: FEEDBACK,
|
|
38195
38385
|
model: coder(),
|
|
38196
38386
|
system: builderSystem()
|
|
38197
38387
|
});
|
|
38198
38388
|
guard();
|
|
38199
|
-
|
|
38200
|
-
|
|
38201
|
-
|
|
38202
|
-
|
|
38203
|
-
|
|
38204
|
-
|
|
38205
|
-
|
|
38206
|
-
|
|
38207
|
-
|
|
38208
|
-
|
|
38389
|
+
}, {
|
|
38390
|
+
describeGuard: guard,
|
|
38391
|
+
rules: refactorRules(strays, pattern)
|
|
38392
|
+
});
|
|
38393
|
+
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
38394
|
+
};
|
|
38395
|
+
const strayAccess = (strays) => ({ write: [...strays, ".gtd/**"] });
|
|
38396
|
+
const refactorFirst = async (shape, options = {}) => {
|
|
38397
|
+
const p = plan(shape, options);
|
|
38398
|
+
if (p === void 0) return false;
|
|
38399
|
+
return scope({
|
|
38400
|
+
name: "refactor",
|
|
38401
|
+
access: strayAccess(p.strays)
|
|
38402
|
+
}, async () => {
|
|
38403
|
+
await refactorLap(p, guardOf(p, head$1()));
|
|
38209
38404
|
return true;
|
|
38210
38405
|
});
|
|
38211
38406
|
};
|
|
38407
|
+
/**
|
|
38408
|
+
* `refactorFirst` plus its review, in the one `refactor` scope so the tail's
|
|
38409
|
+
* steps are `refactor.build.*`. A review edit re-runs the refactor, never a
|
|
38410
|
+
* re-plan: the collected notes ride the re-derived package. Every round
|
|
38411
|
+
* reviews the whole refactor since `entry`, since each lap rebuilds over it.
|
|
38412
|
+
*/
|
|
38413
|
+
const reviewedRefactor = async (shape, options = {}) => {
|
|
38414
|
+
const p = plan(shape, options);
|
|
38415
|
+
if (p === void 0) return false;
|
|
38416
|
+
return scope({
|
|
38417
|
+
name: "refactor",
|
|
38418
|
+
access: strayAccess(p.strays)
|
|
38419
|
+
}, async () => {
|
|
38420
|
+
const entry = head$1();
|
|
38421
|
+
const guard = guardOf(p, entry);
|
|
38422
|
+
const feedback = {
|
|
38423
|
+
file: REFACTOR_PACKAGE,
|
|
38424
|
+
collecting: (capture, round) => refactorCollecting(capture, round, p.strays),
|
|
38425
|
+
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",
|
|
38426
|
+
dropped: refactorDroppedRefusal
|
|
38427
|
+
};
|
|
38428
|
+
let collected;
|
|
38429
|
+
for (;;) {
|
|
38430
|
+
await refactorLap(p, guard, collected);
|
|
38431
|
+
const outcome = await buildTail(false, entry, true, {
|
|
38432
|
+
guard,
|
|
38433
|
+
guardRests: false,
|
|
38434
|
+
lenses: lensesOf(vars.refactorQualityReviews ?? ""),
|
|
38435
|
+
feedback,
|
|
38436
|
+
fixAccess: strayAccess(p.strays),
|
|
38437
|
+
lensBase: entry,
|
|
38438
|
+
rules: refactorRules(p.strays, p.pattern),
|
|
38439
|
+
lap: "refactor"
|
|
38440
|
+
});
|
|
38441
|
+
if (outcome.verdict === "signoff") return true;
|
|
38442
|
+
await reUnwind(outcome);
|
|
38443
|
+
collected = read(REFACTOR_PACKAGE);
|
|
38444
|
+
if (collected === void 0) throw new Error(`gtd: review feedback left no ${REFACTOR_PACKAGE}`);
|
|
38445
|
+
}
|
|
38446
|
+
});
|
|
38447
|
+
};
|
|
38212
38448
|
//#endregion
|
|
38213
38449
|
//#region src/workflows/feature/seam.ts
|
|
38214
38450
|
/** Naive-pass retries after a `reject`; the next `reject` rests at `seam.steer`. */
|
|
@@ -38225,8 +38461,8 @@ const naivePass = (rejection) => agentWithSkills("naive", naivePassPrompt(reject
|
|
|
38225
38461
|
model: coder(),
|
|
38226
38462
|
system: builderSystem()
|
|
38227
38463
|
});
|
|
38228
|
-
const implement = (reference, tier) => scope("implement", async () => {
|
|
38229
|
-
await agentWithSkills("building", implementBuildingPrompt(reference, tier), {
|
|
38464
|
+
const implement = (reference, tier, amend) => scope("implement", async () => {
|
|
38465
|
+
await agentWithSkills("building", implementBuildingPrompt(reference, tier, amend), {
|
|
38230
38466
|
label: "Implementing",
|
|
38231
38467
|
model: coder(),
|
|
38232
38468
|
system: builderSystem()
|
|
@@ -38247,13 +38483,10 @@ const decide = () => scope("seam", async () => {
|
|
|
38247
38483
|
const tip = kept("naive");
|
|
38248
38484
|
const shape = await judgeSeam(tip);
|
|
38249
38485
|
if (shape.choice === "land") return { reference: tip.hash };
|
|
38250
|
-
if (shape.choice === "refactor-first") {
|
|
38251
|
-
|
|
38252
|
-
|
|
38253
|
-
|
|
38254
|
-
shape
|
|
38255
|
-
};
|
|
38256
|
-
}
|
|
38486
|
+
if (shape.choice === "refactor-first") return {
|
|
38487
|
+
reference: tip.hash,
|
|
38488
|
+
shape
|
|
38489
|
+
};
|
|
38257
38490
|
rejection = {
|
|
38258
38491
|
strays: shape.strays,
|
|
38259
38492
|
p: shape.judgment?.answers.shape?.p ?? 0
|
|
@@ -38273,14 +38506,32 @@ const decide = () => scope("seam", async () => {
|
|
|
38273
38506
|
retries = 0;
|
|
38274
38507
|
}
|
|
38275
38508
|
});
|
|
38276
|
-
const seamBuild = async (
|
|
38509
|
+
const seamBuild = async (base) => {
|
|
38277
38510
|
const { reference, shape } = await decide();
|
|
38278
|
-
if (shape
|
|
38279
|
-
|
|
38280
|
-
|
|
38281
|
-
|
|
38282
|
-
|
|
38283
|
-
|
|
38511
|
+
if (shape === void 0) return {
|
|
38512
|
+
reference,
|
|
38513
|
+
base
|
|
38514
|
+
};
|
|
38515
|
+
if (shape.unreadable.length > 0) {
|
|
38516
|
+
await scope("seam", () => human("by-hand", {
|
|
38517
|
+
message: seamByHandMessage(shape.strays, shape.unreadable, reference),
|
|
38518
|
+
label: "Reviewing unreadable files by hand",
|
|
38519
|
+
acceptClean: true
|
|
38520
|
+
}));
|
|
38521
|
+
return {
|
|
38522
|
+
reference,
|
|
38523
|
+
base
|
|
38524
|
+
};
|
|
38525
|
+
}
|
|
38526
|
+
if (refactorStrays(shape).length === 0) return {
|
|
38527
|
+
reference,
|
|
38528
|
+
base
|
|
38529
|
+
};
|
|
38530
|
+
await slice("refactor", () => reviewedRefactor(shape, { reference }));
|
|
38531
|
+
return {
|
|
38532
|
+
reference,
|
|
38533
|
+
base: head$1()
|
|
38534
|
+
};
|
|
38284
38535
|
};
|
|
38285
38536
|
//#endregion
|
|
38286
38537
|
//#region src/workflows/feature/feature.ts
|
|
@@ -38289,44 +38540,49 @@ const unwind = () => {
|
|
|
38289
38540
|
const commit = head$1();
|
|
38290
38541
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
38291
38542
|
};
|
|
38292
|
-
/**
|
|
38293
|
-
* Undo the human's review-round code edits. A path changed since the human
|
|
38294
|
-
* left it is not overwritten; requireRevert names it instead.
|
|
38295
|
-
*/
|
|
38296
|
-
const reUnwind = async (feedback) => {
|
|
38297
|
-
const { base, restoreFrom, edited } = feedback;
|
|
38298
|
-
await run("re-unwind", restoreScript(base, {
|
|
38299
|
-
restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
|
|
38300
|
-
remove: edited.filter((c) => c.status === "added").map((c) => c.path)
|
|
38301
|
-
}, restoreFrom), {
|
|
38302
|
-
label: "Re-unwinding your review edit",
|
|
38303
|
-
file: REVIEW,
|
|
38304
|
-
base
|
|
38305
|
-
});
|
|
38306
|
-
requireRevert(edited, base);
|
|
38307
|
-
};
|
|
38308
38543
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
38309
|
-
const planAndBuild = async (firstBase) => {
|
|
38544
|
+
const planAndBuild = async (firstBase, firstLap = "first") => {
|
|
38545
|
+
if (vars.flag_naive_pass === "1") return naivePlanAndBuild(firstBase);
|
|
38310
38546
|
let base = firstBase;
|
|
38547
|
+
let lap = firstLap;
|
|
38311
38548
|
for (;;) {
|
|
38312
38549
|
await design(base);
|
|
38313
|
-
|
|
38314
|
-
|
|
38315
|
-
|
|
38316
|
-
await architecture();
|
|
38317
|
-
await packages();
|
|
38318
|
-
}
|
|
38319
|
-
const outcome = await buildTail(false, base, true);
|
|
38550
|
+
await architecture();
|
|
38551
|
+
await packages();
|
|
38552
|
+
const outcome = await buildTail(false, base, true, lap === "first" ? {} : { lap });
|
|
38320
38553
|
if (outcome.verdict === "signoff") return;
|
|
38321
38554
|
await reUnwind(outcome);
|
|
38322
38555
|
base = outcome.base;
|
|
38556
|
+
lap = "feedback";
|
|
38557
|
+
}
|
|
38558
|
+
};
|
|
38559
|
+
const naivePlanAndBuild = async (firstBase) => {
|
|
38560
|
+
await design(firstBase);
|
|
38561
|
+
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
38562
|
+
const seam = await seamBuild(firstBase);
|
|
38563
|
+
await slice("feature", async () => {
|
|
38564
|
+
await implement(seam.reference, tier);
|
|
38565
|
+
await featureFeedback(await buildTail(false, seam.base, true, { lensBase: seam.base }), seam.base);
|
|
38566
|
+
});
|
|
38567
|
+
};
|
|
38568
|
+
const featureFeedback = async (first, lensBase) => {
|
|
38569
|
+
let outcome = first;
|
|
38570
|
+
while (outcome.verdict === "feedback") {
|
|
38571
|
+
await reUnwind(outcome);
|
|
38572
|
+
await design(outcome.base);
|
|
38573
|
+
await implement(void 0, riskTierOn() ? recordRisk() : void 0, true);
|
|
38574
|
+
outcome = await buildTail(false, outcome.base, true, {
|
|
38575
|
+
lap: "feedback",
|
|
38576
|
+
...lensBase === void 0 ? {} : { lensBase }
|
|
38577
|
+
});
|
|
38323
38578
|
}
|
|
38324
38579
|
};
|
|
38325
|
-
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
|
|
38580
|
+
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
|
|
38326
38581
|
const afterTail = async (outcome) => {
|
|
38327
38582
|
if (outcome.verdict === "signoff") return;
|
|
38583
|
+
if (vars.flag_naive_pass === "1") return featureFeedback(outcome);
|
|
38328
38584
|
await reUnwind(outcome);
|
|
38329
|
-
await planAndBuild(outcome.base);
|
|
38585
|
+
await planAndBuild(outcome.base, "feedback");
|
|
38330
38586
|
};
|
|
38331
38587
|
/** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
|
|
38332
38588
|
const ordinaryStart = async () => {
|
|
@@ -38348,33 +38604,40 @@ const ordinaryStart = async () => {
|
|
|
38348
38604
|
const feature = shipped(ordinaryStart);
|
|
38349
38605
|
//#endregion
|
|
38350
38606
|
//#region src/workflows/shared/skills.ts
|
|
38351
|
-
const skills = (vars) =>
|
|
38352
|
-
|
|
38353
|
-
|
|
38354
|
-
"
|
|
38355
|
-
"
|
|
38356
|
-
"
|
|
38357
|
-
|
|
38358
|
-
|
|
38359
|
-
|
|
38360
|
-
|
|
38361
|
-
|
|
38362
|
-
|
|
38363
|
-
|
|
38364
|
-
|
|
38365
|
-
|
|
38366
|
-
|
|
38367
|
-
|
|
38368
|
-
|
|
38369
|
-
|
|
38370
|
-
|
|
38371
|
-
|
|
38372
|
-
|
|
38373
|
-
|
|
38374
|
-
|
|
38375
|
-
|
|
38376
|
-
|
|
38377
|
-
|
|
38607
|
+
const skills = (vars) => {
|
|
38608
|
+
const build = {
|
|
38609
|
+
build: ["debugging-and-error-recovery"],
|
|
38610
|
+
"build.health": ["debugging-and-error-recovery"],
|
|
38611
|
+
"build.fix.quality": ["incremental-implementation", "code-simplification"],
|
|
38612
|
+
"build.review": ["code-review-and-quality"],
|
|
38613
|
+
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
38614
|
+
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"]
|
|
38615
|
+
};
|
|
38616
|
+
return {
|
|
38617
|
+
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
38618
|
+
architecture: [
|
|
38619
|
+
"api-and-interface-design",
|
|
38620
|
+
"documentation-and-adrs",
|
|
38621
|
+
"ponytail"
|
|
38622
|
+
],
|
|
38623
|
+
"architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
|
|
38624
|
+
"packages.item": ["test-driven-development", "incremental-implementation"],
|
|
38625
|
+
"packages.item.fix.suite": ["debugging-and-error-recovery"],
|
|
38626
|
+
"packages.item.health": ["debugging-and-error-recovery"],
|
|
38627
|
+
notes: ["incremental-implementation", "code-simplification"],
|
|
38628
|
+
...build,
|
|
38629
|
+
...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
|
|
38630
|
+
refactor: ["ponytail"],
|
|
38631
|
+
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
38632
|
+
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
38633
|
+
seam: ["incremental-implementation"],
|
|
38634
|
+
implement: ["test-driven-development", "incremental-implementation"],
|
|
38635
|
+
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
38636
|
+
"implement.health": ["debugging-and-error-recovery"],
|
|
38637
|
+
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens]),
|
|
38638
|
+
...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
|
|
38639
|
+
};
|
|
38640
|
+
};
|
|
38378
38641
|
//#endregion
|
|
38379
38642
|
//#region src/workflows/shared/access.ts
|
|
38380
38643
|
const access = (vars) => ({
|
|
@@ -38385,7 +38648,10 @@ const access = (vars) => ({
|
|
|
38385
38648
|
"build.review.defer": { write: [".gtd/TODO.md"] },
|
|
38386
38649
|
"build.review.fix.nits": {},
|
|
38387
38650
|
"build.review.fix.risks": {},
|
|
38388
|
-
|
|
38651
|
+
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
38652
|
+
"refactor.build.review.defer": { write: [".gtd/TODO.md"] },
|
|
38653
|
+
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
|
|
38654
|
+
...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({ write: [] }))
|
|
38389
38655
|
});
|
|
38390
38656
|
//#endregion
|
|
38391
38657
|
//#region src/workflows/shared/reserved.ts
|
|
@@ -38531,4 +38797,4 @@ var workflows_default = feature;
|
|
|
38531
38797
|
*/
|
|
38532
38798
|
const doors = {};
|
|
38533
38799
|
//#endregion
|
|
38534
|
-
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, refactorFirst, requireDeclaredTests, requireNoNewFlags, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, rollout, rolloutFlow, seamBuild, skills, steering, summary, tail, threads, triage, unwind };
|
|
38800
|
+
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireDeclaredTests, requireNoNewFlags, requireNoTestFiles, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, steering, summary, tail, threads, triage, unwind };
|