@pmelab/gtd 42.0.0 → 43.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 +79 -35
- package/claude/hooks/sketch.ts +1 -1
- package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-COEMFEpy.mjs} +35 -46
- package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
- package/dist/chunks/{tail-D2o2h5cc.mjs → tail-C_xn8U7U.mjs} +1 -1
- package/dist/gtd.bundle.mjs +488 -30
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +447 -27
- package/dist/web/main.js +42 -13
- package/dist/workflow-kit.mjs +61 -25
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +31 -42
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +217 -121
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +33 -1
- package/src/cli/Cli.ts +70 -6
- package/src/cli/History.ts +15 -0
- package/src/cli/Judges.test.ts +373 -0
- package/src/cli/Judges.ts +624 -0
- package/src/cli/Metrics.ts +3 -16
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +9 -0
- package/src/steering/index.ts +12 -1
- package/src/steering/review.test.ts +107 -0
- package/src/steering/review.ts +159 -9
- package/src/testkit/EmittedScriptRecognizer.ts +0 -14
- package/src/testkit/steps.ts +4 -0
- package/src/workflow-kit/review.test.ts +170 -26
- package/src/workflow-kit/review.ts +95 -19
- package/src/workflows/feature/lens-review.feature +16 -4
- package/src/workflows/feature/review-answers.feature +179 -0
- package/src/workflows/feature/review.test.ts +5 -53
- package/src/workflows/feature/review.ts +31 -42
- package/src/workflows/feature/steps.test.ts +0 -12
- package/src/workflows/feature/steps.ts +3 -10
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
- package/src/workflows/shared/access.test.ts +2 -11
- package/src/workflows/shared/access.ts +0 -2
- package/src/workflows/shared/text.test.ts +40 -89
- package/src/workflows/shared/text.ts +52 -54
package/README.md
CHANGED
|
@@ -339,10 +339,10 @@ Then, in any repository:
|
|
|
339
339
|
```
|
|
340
340
|
|
|
341
341
|
That starts a process from your requirements and drives it until it needs you.
|
|
342
|
-
The requirements go to the top of `.gtd/TODO.md
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
The requirements go to the top of `.gtd/TODO.md`, above whatever it already
|
|
343
|
+
holds. A long, multi-line requirements paste still starts a process; if Claude
|
|
344
|
+
Code never expanded the paste, gtd says it could not read it, and you add the
|
|
345
|
+
requirements to the top of `.gtd/TODO.md` and run `/gtd`. `/gtd fix`,
|
|
346
346
|
`/gtd review [base]`, `/gtd rollout <flag>` and `/gtd cleanup <flag>` (where the
|
|
347
347
|
config exports them) take the doors described below; text whose word count does
|
|
348
348
|
not fit a door's args (bare `/gtd rollout` included) starts a process from that
|
|
@@ -522,37 +522,41 @@ noted in steps 3 and 4 below.
|
|
|
522
522
|
|
|
523
523
|
4. **You review.** After the last slice's quality lap gtd writes the review
|
|
524
524
|
document itself (`build.review.composing`; no reviewer turn runs): one `## `
|
|
525
|
-
chunk per design note the lap left,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
525
|
+
chunk per design note the lap left, each ending in three tick boxes — **fix
|
|
526
|
+
as suggested**, **won't fix**, or **your own answer** written over
|
|
527
|
+
`_your answer_` — then, for reference, no tick boxes, no answer needed — what
|
|
528
|
+
was **fixed automatically** (with the fixing commit) and what was **dropped
|
|
529
|
+
as noise**. On `refactor-first` the refactor slice gets one of its own first,
|
|
530
|
+
before the feature is built; see [the naive pass](#the-naive-pass). Review
|
|
531
|
+
the diff yourself (`gtd base` prints its base). **Every note needs exactly
|
|
532
|
+
one tick before `gtd land` accepts the review** (`answer-completeness`; an
|
|
533
|
+
open `- H:` thread waiting on the agent is the one exception). Fix as
|
|
534
|
+
suggested joins the nit batch below, unjudged; won't fix drops the note and
|
|
535
|
+
records it as `Gtd-Mark: wontfix=<title>` on the gate's commit; your own
|
|
536
|
+
answer is judged like a note. A note you answered is left out of the fresh
|
|
537
|
+
review after a nit fix; a round that also answered a question restores the
|
|
538
|
+
review instead, your ticks kept. Land with every note answered and nothing
|
|
539
|
+
else changed to approve, or write what is wrong — disagreeing with a fix or a
|
|
540
|
+
drop means writing a note of your own. Approving ends the process (at the
|
|
541
|
+
refactor review, it moves on to the feature); feedback is judged note by
|
|
542
|
+
note, each as `edit`, `question`, `nit` or `praise`. An `edit` sends the
|
|
543
|
+
process back to step 2 for re-derived requirements — the built criteria keep
|
|
544
|
+
their IDs (reworded in place when their behaviour changes) and new behaviour
|
|
545
|
+
gets new ones — then the implementation is amended to meet them, with no new
|
|
546
|
+
naive pass or seam judge, every criterion's coverage is judged again, the
|
|
547
|
+
quality lap runs, and you review again. At the refactor review an `edit`
|
|
548
|
+
re-derives the refactor instead. A `question` is answered inline under your
|
|
549
|
+
note and the process stops at the review again — no new plan. A `nit` is
|
|
550
|
+
fixed in one batch, the suite must go green (a red one gets fix turns first),
|
|
551
|
+
then a fresh review of the change stops at the review again. `praise` is
|
|
552
|
+
dropped; a round of only praise signs off. When a round mixes them, questions
|
|
553
|
+
are answered and nits fixed first, then the edits are planned — risk: the
|
|
554
|
+
planning lap may redo nit fixes it touches.
|
|
547
555
|
|
|
548
556
|
Each `edit` note also gets a severity tier from four judge questions
|
|
549
|
-
(security, reversibility, blast radius, new concept).
|
|
550
|
-
|
|
551
|
-
a note forces `critical` and skips the judge.
|
|
552
|
-
committed `.gtd/TODO.md`; it never starts a process on its own, and it ships
|
|
553
|
-
in the branch/PR unless you delete it. Landing clean at the gate after a
|
|
554
|
-
deferral signs off without the deferred notes; the gate lists them. To pull
|
|
555
|
-
one in, edit its line in `.gtd/TODO.md` when you start a process.
|
|
557
|
+
(security, reversibility, blast radius, new concept). Every tier is
|
|
558
|
+
re-planned this round, `critical` first, then `should`, then `could`; nothing
|
|
559
|
+
is deferred. A leading `!` on a note forces `critical` and skips the judge.
|
|
556
560
|
`reviewSeverityMinP` sets the confidence floor (see
|
|
557
561
|
[configuration](docs/configuration.md)). A hand-edit to code always plans a
|
|
558
562
|
lap. Only a confident non-`edit` verdict skips the replan — a note whose
|
|
@@ -740,6 +744,7 @@ full trail with `gtd history fetch | sh`, then `gtd history show <ref>`.
|
|
|
740
744
|
gtd history list # one line per kept process
|
|
741
745
|
gtd history show <ref> # replay its steps, judge verdicts and approvals
|
|
742
746
|
gtd history metrics [ref...] # how the kept processes went, per process and in total
|
|
747
|
+
gtd history judges [ref...] # how often each judge's verdicts were overturned
|
|
743
748
|
gtd history fetch | sh # pull every kept ref from origin
|
|
744
749
|
```
|
|
745
750
|
|
|
@@ -772,6 +777,45 @@ process kept from that branch. A rate is `null` without a decided slice. Each
|
|
|
772
777
|
process names its `workflow` (`null` for the default one); the total spans every
|
|
773
778
|
ref measured, so name the refs to leave another workflow's processes out.
|
|
774
779
|
|
|
780
|
+
`gtd history judges` calibrates three of the bundled `feature` workflow's judges
|
|
781
|
+
from the `Gtd-Judge` verdicts the kept refs recorded. It never replays and asks
|
|
782
|
+
no model. A verdict is **overturned** when later history contradicts it:
|
|
783
|
+
|
|
784
|
+
- **retry** (`health.judge`) — an `identical` verdict, when the next fix turn's
|
|
785
|
+
check went green anyway (no outcome when a person edited code in between)
|
|
786
|
+
- **triage** (`quality.triage`) — a `mechanical` or `noise` verdict the review
|
|
787
|
+
listed under _Fixed automatically_ or _Dropped as noise_, when the human
|
|
788
|
+
edited that finding's line there or wrote under it
|
|
789
|
+
- **clear** (`design.clear`) — a `yes` that skipped design (landed on
|
|
790
|
+
`design.normalise`), when a later human design step in the same process
|
|
791
|
+
changed the acceptance criteria
|
|
792
|
+
|
|
793
|
+
Any other verdict has no outcome. The report counts verdicts, scored verdicts
|
|
794
|
+
and overturns per judge and per `p` bucket (`0.8-0.9: 1/2` — one of two
|
|
795
|
+
overturned). `--var <threshold>=<value>` re-decides every past verdict under
|
|
796
|
+
another threshold and prints the decisions that would change, against the
|
|
797
|
+
threshold each process recorded (a process that recorded none is left out):
|
|
798
|
+
|
|
799
|
+
```sh
|
|
800
|
+
gtd history judges --var qualityTriageMinP=0.9
|
|
801
|
+
# what-if qualityTriageMinP=0.9: 7 noise drops become notes (2 overturned)
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
The thresholds are `judgeIdenticalMinP` (retry) and `qualityTriageMinP`
|
|
805
|
+
(triage). The judge's evidence-budget cut is not recorded, so a cut verdict is
|
|
806
|
+
scored and re-decides as if it was whole. A retry verdict on an empty report is
|
|
807
|
+
not scored, as the flow drops it. `--json` prints
|
|
808
|
+
`{ processes, total, whatIf }`, each process and the total with its `judges`.
|
|
809
|
+
`--jsonl` prints one line per verdict instead — `process`, `judge`, `commit`,
|
|
810
|
+
`step`, `question`, `state` (the evidence the tree still shows: the finding, the
|
|
811
|
+
two check reports, the sketch without its numbered criteria), `answer`, `p` and
|
|
812
|
+
`outcome` (`overturned`, `held` or `null`) — ready to fine-tune a local model
|
|
813
|
+
on:
|
|
814
|
+
|
|
815
|
+
```sh
|
|
816
|
+
gtd history judges --jsonl > judges.jsonl
|
|
817
|
+
```
|
|
818
|
+
|
|
775
819
|
`gtd history fetch` only prints a fetch script; pipe it into `sh` to run it.
|
|
776
820
|
Beyond that one ship push, refs are not pushed or fetched by default. The ref
|
|
777
821
|
holds every step commit, including steering files and code a later step removed,
|
|
@@ -828,8 +872,8 @@ install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
|
|
|
828
872
|
The `gtd` bin's commands — `next`, `land`, `abandon`, `restore`, `validate`,
|
|
829
873
|
`check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `slices`,
|
|
830
874
|
`version`, `help`, `history list`, `history show`, `history metrics`,
|
|
831
|
-
`history fetch`, `strays`, and the tools `ui`, `lsp`, `loop`,
|
|
832
|
-
`judge` (`judge answer`, `judge run`) — are listed in the
|
|
875
|
+
`history judges`, `history fetch`, `strays`, and the tools `ui`, `lsp`, `loop`,
|
|
876
|
+
`context-hook`, `judge` (`judge answer`, `judge run`) — are listed in the
|
|
833
877
|
[CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
|
|
834
878
|
`gtd slices` lists the spans a flow's `slice()` calls mark. It refuses (exit 1)
|
|
835
879
|
when HEAD neither closes nor sits inside a process, or when today's config
|
package/claude/hooks/sketch.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// New requirements go above what the steering file already holds, untouched:
|
|
2
|
-
//
|
|
2
|
+
// anything already there that the start diff alters reads as the human's edit.
|
|
3
3
|
export function withRequirements(existing: string, requirements: string): string {
|
|
4
4
|
const head = `${requirements.trimEnd()}\n`
|
|
5
5
|
return existing.trim() ? `${head}\n${existing}` : head
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, mark, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
|
|
2
|
+
import { d as ANSWER_FIX, f as ANSWER_LEAD, p as ANSWER_WONT_FIX, v as FREE_TEXT_PLACEHOLDER } from "./steering-D4LUQ-av.mjs";
|
|
2
3
|
import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
|
|
3
4
|
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-C6fLlQzx.mjs";
|
|
4
5
|
//#region src/workflows/shared/registry.ts
|
|
@@ -437,9 +438,9 @@ const SUMMARY_TITLES = [
|
|
|
437
438
|
const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
|
|
438
439
|
const short = (hash) => hash.slice(0, 7);
|
|
439
440
|
/**
|
|
440
|
-
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note,
|
|
441
|
-
*
|
|
442
|
-
* matched to their chunk by title.
|
|
441
|
+
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
442
|
+
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
443
|
+
* notes and answers are matched to their chunk by title.
|
|
443
444
|
*/
|
|
444
445
|
const composedReview = (base, report) => {
|
|
445
446
|
const used = new Set(SUMMARY_TITLES);
|
|
@@ -450,11 +451,16 @@ const composedReview = (base, report) => {
|
|
|
450
451
|
return name;
|
|
451
452
|
};
|
|
452
453
|
const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
|
|
454
|
+
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
|
|
455
|
+
const notes = report.notes.map((n) => ({
|
|
456
|
+
...n,
|
|
457
|
+
title: unique(n.title)
|
|
458
|
+
})).filter((n) => report.answered?.has(n.title) !== true);
|
|
453
459
|
const weakened = (report.weakened ?? []).flatMap(({ path, lines }) => lines.map((a) => `- ${path} — ${a.scenario} — ${a.line}`));
|
|
454
460
|
const chunks = [
|
|
455
|
-
...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits were
|
|
461
|
+
...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits and fix-as-suggested notes were worked on in ${short(report.nitFix)}; the notes you answered are left out — check the diff.`)],
|
|
456
462
|
...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
|
|
457
|
-
...
|
|
463
|
+
...notes.map((n) => chunk(n.title, `${n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._"}\n\n${answerBoxes}`)),
|
|
458
464
|
...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
|
|
459
465
|
...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
|
|
460
466
|
];
|
|
@@ -537,10 +543,6 @@ ${stateFileRules}
|
|
|
537
543
|
${footnoteFoldIn}${codeThreadReplies()}
|
|
538
544
|
- The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
|
|
539
545
|
— no other files for notes or output
|
|
540
|
-
- Lines of a \`## Deferred review notes\` section of \`.gtd/TODO.md\`
|
|
541
|
-
that the start diff does not touch are parked by review, never folded;
|
|
542
|
-
the human's edits to such a line (or its removal) are input like any
|
|
543
|
-
other part of the start diff
|
|
544
546
|
- \`.gtd/TODO.md\` is the likely home of the sketch that started
|
|
545
547
|
this process — the human's input, folded into the concerns
|
|
546
548
|
below like any other part of the start diff; never a state
|
|
@@ -833,8 +835,12 @@ const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds what t
|
|
|
833
835
|
per finding that needs your judgement — it questions a criterion or the
|
|
834
836
|
approach, or the fixer could not resolve it — then, for reference only, what
|
|
835
837
|
was **fixed automatically** (with the fixing commit) and what was **dropped
|
|
836
|
-
as noise**.
|
|
837
|
-
|
|
838
|
+
as noise**. Each finding chunk ends in three boxes — tick exactly one:
|
|
839
|
+
\`fix as suggested\` (fixed in the nit batch below), \`won't fix\` (dropped,
|
|
840
|
+
recorded in the commit), or write your own answer over \`_your answer_\` and
|
|
841
|
+
tick it (judged like a note). Every landing here is refused until each
|
|
842
|
+
finding holds exactly one tick. The fixed and noise lists need no answer;
|
|
843
|
+
disagreeing with a fix or a drop means writing a note of your own.
|
|
838
844
|
|
|
839
845
|
Review the diff yourself, with whatever tool you like — gtd checks
|
|
840
846
|
nothing out and touches no ref; \`gtd base\` prints this same hash any
|
|
@@ -844,8 +850,8 @@ working tree:
|
|
|
844
850
|
git diff ${base}
|
|
845
851
|
When you've been through the whole diff, run \`gtd land\`:
|
|
846
852
|
|
|
847
|
-
- **Sign off** — leave no comment — no note
|
|
848
|
-
\`.gtd/REVIEW.md\`, no code edit — to close the process.
|
|
853
|
+
- **Sign off** — answer every finding, leave no other comment — no note
|
|
854
|
+
in \`.gtd/REVIEW.md\`, no code edit — to close the process.
|
|
849
855
|
Every turn commit stays on the
|
|
850
856
|
branch; run \`gtd summary\` afterward for a closing-message
|
|
851
857
|
prompt.
|
|
@@ -992,27 +998,6 @@ const wrapItem = (item) => {
|
|
|
992
998
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
993
999
|
return [...out, line];
|
|
994
1000
|
};
|
|
995
|
-
/** Appends one section of `notes` to `.gtd/TODO.md`, wrapped the way oxfmt writes it; an existing file is only ever appended to. */
|
|
996
|
-
const deferNotesScript = (notes, round) => {
|
|
997
|
-
return `mkdir -p .gtd
|
|
998
|
-
if [ -s .gtd/TODO.md ]; then
|
|
999
|
-
[ -z "$(tail -c1 .gtd/TODO.md)" ] || printf '\\n' >> .gtd/TODO.md
|
|
1000
|
-
printf '\\n' >> .gtd/TODO.md
|
|
1001
|
-
fi
|
|
1002
|
-
printf '%s\\n' ${[
|
|
1003
|
-
`## Deferred review notes (${round.slice(0, 7)})`,
|
|
1004
|
-
"",
|
|
1005
|
-
...notes.flatMap((n) => wrapItem(`- ${noteHead(n)}: ${n.text.replace(/\s+/g, " ")}`))
|
|
1006
|
-
].map(quote).join(" ")} >> .gtd/TODO.md
|
|
1007
|
-
`;
|
|
1008
|
-
};
|
|
1009
|
-
/** The gate right after a deferral: landing clean signs off without these notes. */
|
|
1010
|
-
const buildReviewAwaitAfterDeferMessage = (base, deferred) => `${buildReviewAwaitReviewMessage(base)}
|
|
1011
|
-
Deferred to \`.gtd/TODO.md\` just now, NOT acted on in this process — landing
|
|
1012
|
-
clean signs off without them:
|
|
1013
|
-
|
|
1014
|
-
${deferred.map((n) => `- ${noteHead(n)}: ${n.text.replace(/\s*\n\s*/g, " ")}`).join("\n")}
|
|
1015
|
-
`;
|
|
1016
1001
|
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
1017
1002
|
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
1018
1003
|
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
@@ -1022,16 +1007,18 @@ default; a skipped judgment never dismisses a note).
|
|
|
1022
1007
|
|
|
1023
1008
|
Also judge each note's severity, one id per note: \`note-<n>.security\`,
|
|
1024
1009
|
\`note-<n>.reversibility\`, \`note-<n>.blastRadius\` and
|
|
1025
|
-
\`note-<n>.newConcept\`.
|
|
1010
|
+
\`note-<n>.newConcept\`. An own answer under a finding's boxes is judged
|
|
1011
|
+
the same way, keyed \`answer-<n>\`.
|
|
1026
1012
|
`;
|
|
1027
1013
|
const closedFold = (round, target) => round === "edits" ? `- Fold every hand-edit and every note the human left that round into
|
|
1028
|
-
\`${target}\`
|
|
1014
|
+
\`${target}\` — a note ticked \`fix as suggested\` or answered in its
|
|
1015
|
+
own words included; one ticked \`won't fix\` is dropped
|
|
1029
1016
|
` : round === "notes" ? `- Fold each note that asks for a change into \`${target}\`
|
|
1030
1017
|
` : `- Fold each \`edit\` note the capture lists, no other note, and every
|
|
1031
1018
|
hand-edit, into \`${target}\`
|
|
1032
1019
|
`;
|
|
1033
1020
|
/** `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. */
|
|
1034
|
-
const buildReviewCollectingPrompt = (capture, refactor) => {
|
|
1021
|
+
const buildReviewCollectingPrompt = (capture, refactor, round) => {
|
|
1035
1022
|
const target = refactor ? ".gtd/REFACTOR.md" : ".gtd/REQUIREMENTS.md";
|
|
1036
1023
|
const fence = refactor ? `- The next refactor may change only these paths, plus an existing test
|
|
1037
1024
|
file's call sites, and never a flag read:
|
|
@@ -1106,12 +1093,16 @@ ${refactor ? refactor.round === "reply" ? `- This turn replies inside \`.gtd/REV
|
|
|
1106
1093
|
${closedFold(refactor.round, target)}` : `- This turn writes \`${target}\` (the folded concerns) and
|
|
1107
1094
|
replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
|
|
1108
1095
|
in the tree) — you classify, you do not build
|
|
1109
|
-
- Fold every concluded thread
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1096
|
+
- Fold every concluded thread and hand-edit — and each \`edit\` note
|
|
1097
|
+
the capture lists, no other note — into \`${target}\` and
|
|
1098
|
+
delete the folded threads from \`.gtd/REVIEW.md\`; append one \`- A:\`
|
|
1099
|
+
reply to each thread whose last entry is a \`- H:\` question
|
|
1100
|
+
${round === "edits" ? `- A hand-edit round routes no answer box itself: fold each note
|
|
1101
|
+
ticked \`fix as suggested\` or answered in its own words, too; drop one
|
|
1102
|
+
ticked \`won't fix\`
|
|
1103
|
+
` : `- Never fold a note's ticked answer box the capture does not list: gtd
|
|
1104
|
+
routes those itself
|
|
1105
|
+
`}`}- When the capture names an answering commit, carry each answered
|
|
1115
1106
|
question into \`${target}\`'s \`## Answered Questions\` as
|
|
1116
1107
|
\`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
|
|
1117
1108
|
- Keep capture order, and carry each note's \`(tier: reason)\`
|
|
@@ -1829,11 +1820,9 @@ const skills = (vars) => {
|
|
|
1829
1820
|
const access = (vars) => ({
|
|
1830
1821
|
design: { write: [] },
|
|
1831
1822
|
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
1832
|
-
"build.review.defer": { write: [".gtd/TODO.md"] },
|
|
1833
1823
|
"build.review.fix.nits": {},
|
|
1834
1824
|
"build.review.fix.risks": {},
|
|
1835
1825
|
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
1836
|
-
"refactor.build.review.defer": { write: [".gtd/TODO.md"] },
|
|
1837
1826
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
|
|
1838
1827
|
...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({ write: [] }))
|
|
1839
1828
|
});
|
|
@@ -1845,4 +1834,4 @@ const steering = {
|
|
|
1845
1834
|
[REVIEW]: "review"
|
|
1846
1835
|
};
|
|
1847
1836
|
//#endregion
|
|
1848
|
-
export {
|
|
1837
|
+
export { naivePassPrompt as $, agentWithSkills as A, coverageQuestion as B, fixCheck as C, envDefaults as Ct, reviewMissing as D, flipProblems as Dt, planner as E, flagReferences as Et, builderSystem as F, designTriagePrompt as G, criterionLine as H, clearCriteriaMessage as I, handOffAwaitReviewMessage as J, finisherSystem as K, clearCriteriaQuestion as L, buildReviewCollectingPrompt as M, buildReviewFixNitsPrompt as N, reviewQuality as O, registryEntry as Ot, buildReviewTriageMessage as P, naiveApplyFailure as Q, composedReview as R, coder as S, defaults as St, fixRisks as T, flagMentions as Tt, designGateAnswerMessage as U, coveringPrompt as V, designSystem as W, implementBuildingPrompt as X, idleMessage as Y, implementFixPrompt as Z, REFACTOR_NOTE as _, slicingPrompt as _t, flagModeOn as a, refactorBuildingPrompt as at, SLICES as b, unwindFailedMessage as bt, baseline as c, refactorNotePrompt as ct, guarded as d, reviewEditsCapture as dt, normaliseCriteriaScript as et, healthy as f, reviewGateBlockedMessage as ft, REFACTOR as g, seamSteerMessage as gt, QUALITY as h, seamByHandMessage as ht, skills as i, qualityUnresolvedMessage as it, buildReviewAnswerQuestionsPrompt as j, reviewing as k, requireFlag as kt, escalation as l, refactorRules as lt, HANDOFF as m, reviewerSystem as mt, summary as n, qualityFindingsFile as nt, flagShipped as o, refactorDroppedRefusal as ot, FEEDBACK as p, reviewNotesCapture as pt, fixReviewNotesPrompt as q, access as r, qualityTriageMessage as rt, shipped as s, refactorFixPrompt as st, steering as t, oversizedMessage as tt, gate as u, reviewEditNotesCapture as ut, REQUIREMENTS as v, startGateBlockedMessage as vt, fixQuality as w, testFileGlobs as wt, awaitReview as x, unwindFailure as xt, REVIEW as y, uncoveredMessage as yt, coverageMessage as z };
|
|
@@ -13349,8 +13349,8 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
|
|
|
13349
13349
|
const PREFIX_RE$1 = /^(H|A):\s*/;
|
|
13350
13350
|
const lineOf = (node, which) => toLspPosition(node.position[which]).line;
|
|
13351
13351
|
/** The item's own blocks joined — never the item node's span, which can run past its last line into the next bullet's marker. */
|
|
13352
|
-
const itemText = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
13353
|
-
const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText(content, item));
|
|
13352
|
+
const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
|
|
13353
|
+
const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText$1(content, item));
|
|
13354
13354
|
/** A thread is a definition whose body OPENS with a list whose first item carries a prefix — anything else stays a one-shot footnote. */
|
|
13355
13355
|
const isThreadBody = (content, children) => {
|
|
13356
13356
|
const first = children[0];
|
|
@@ -13370,7 +13370,7 @@ const entryOf = (content, item, previous, report) => {
|
|
|
13370
13370
|
return;
|
|
13371
13371
|
}
|
|
13372
13372
|
const author = match[1] === "H" ? "me" : "agent";
|
|
13373
|
-
const text = itemText(content, item).slice(match[0].length).trim();
|
|
13373
|
+
const text = itemText$1(content, item).slice(match[0].length).trim();
|
|
13374
13374
|
if (text === "") report(`empty "${match[1]}:" entry — write your own words there`, start, end);
|
|
13375
13375
|
if (previous === void 0 && author === "agent") report(`the agent never starts a thread — the first entry must be "H:"`, start, end);
|
|
13376
13376
|
else if (previous === author) report(`two consecutive "${match[1]}:" entries — entries must alternate`, start, end);
|
|
@@ -15086,6 +15086,9 @@ const qaDescriptor = {
|
|
|
15086
15086
|
};
|
|
15087
15087
|
//#endregion
|
|
15088
15088
|
//#region src/steering/review.ts
|
|
15089
|
+
const ANSWER_LEAD = "Tick exactly one:";
|
|
15090
|
+
const ANSWER_FIX = "fix as suggested";
|
|
15091
|
+
const ANSWER_WONT_FIX = "won't fix";
|
|
15089
15092
|
/**
|
|
15090
15093
|
* The canonical sample. Every detail here is load-bearing for the formatter
|
|
15091
15094
|
* round-trip: both footnote bodies exceed 80 characters so a reflow actually
|
|
@@ -15352,16 +15355,42 @@ const parseHunk = (content, lines, title, item) => {
|
|
|
15352
15355
|
errors: [hunkSecondPointerFinding(content, lines, item, token, sourceLine, title, file), hunkLineWithoutRangeFinding(content, item, token, sourceLine, title, file)].filter((e) => e !== void 0)
|
|
15353
15356
|
};
|
|
15354
15357
|
};
|
|
15358
|
+
const itemText = (content, item) => item.children.map((c) => sourceText(content, c)).join(" ").replace(/^\[[ xX]\]\s*/, "").trim();
|
|
15359
|
+
/**
|
|
15360
|
+
* The task list right after the `Tick exactly one:` paragraph, with that
|
|
15361
|
+
* paragraph's index. Read by position, never by label, so an edited label or
|
|
15362
|
+
* a free text opening with `./path` is still an answer, not a pointer. Any
|
|
15363
|
+
* count but three boxes is kept, and reads as unanswered.
|
|
15364
|
+
*/
|
|
15365
|
+
const answerListOf = (content, body) => {
|
|
15366
|
+
const lead = body.findIndex((n, i) => n.type === "paragraph" && sourceText(content, n) === "Tick exactly one:" && body[i + 1]?.type === "list" && body[i + 1].children.every((item) => item.checked != null));
|
|
15367
|
+
if (lead === -1) return void 0;
|
|
15368
|
+
const list = body[lead + 1];
|
|
15369
|
+
const last = list.children.length - 1;
|
|
15370
|
+
return {
|
|
15371
|
+
lead,
|
|
15372
|
+
list,
|
|
15373
|
+
options: list.children.map((item, i) => {
|
|
15374
|
+
const text = itemText(content, item);
|
|
15375
|
+
return {
|
|
15376
|
+
checked: item.checked === true,
|
|
15377
|
+
text: i === last && text.toLowerCase() === "_your answer_".toLowerCase() ? "" : text
|
|
15378
|
+
};
|
|
15379
|
+
})
|
|
15380
|
+
};
|
|
15381
|
+
};
|
|
15355
15382
|
/** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
|
|
15356
15383
|
const parseChunkBody = (content, lines, title, body) => {
|
|
15357
|
-
const
|
|
15358
|
-
const
|
|
15384
|
+
const answer = answerListOf(content, body);
|
|
15385
|
+
const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
|
|
15386
|
+
const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
|
|
15387
|
+
const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
|
|
15359
15388
|
const descriptionNodes = blockNodesOfRun(content, leadingRun);
|
|
15360
15389
|
const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
|
|
15361
15390
|
const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
|
|
15362
15391
|
const files = [];
|
|
15363
15392
|
const errors = [];
|
|
15364
|
-
for (const item of body.flatMap((n) => taskItems(n))) {
|
|
15393
|
+
for (const item of body.filter((n) => !isAnswer(n)).flatMap((n) => taskItems(n))) {
|
|
15365
15394
|
const parsed = parseHunk(content, lines, title, item);
|
|
15366
15395
|
if (!parsed) continue;
|
|
15367
15396
|
files.push(parsed.file);
|
|
@@ -15372,7 +15401,9 @@ const parseChunkBody = (content, lines, title, body) => {
|
|
|
15372
15401
|
descriptionNodes,
|
|
15373
15402
|
descriptionInline,
|
|
15374
15403
|
files,
|
|
15375
|
-
errors
|
|
15404
|
+
errors,
|
|
15405
|
+
...answer === void 0 ? {} : { answers: answer.options },
|
|
15406
|
+
boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
|
|
15376
15407
|
};
|
|
15377
15408
|
};
|
|
15378
15409
|
/** Splits the tree's top-level nodes into consecutive `##` chunk blocks — a `## ` line found only inside a fence never becomes a heading node at all, so it's never mistaken for a chunk. */
|
|
@@ -15408,9 +15439,9 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15408
15439
|
const errors = [];
|
|
15409
15440
|
for (const { heading, headingLine, body } of splitChunks(tree)) {
|
|
15410
15441
|
const title = headingText$1(content, heading);
|
|
15411
|
-
const { description, descriptionNodes, descriptionInline, files, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
15442
|
+
const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
|
|
15412
15443
|
errors.push(...bodyErrors);
|
|
15413
|
-
if (files.length === 0 &&
|
|
15444
|
+
if (files.length === 0 && boxed) errors.push({
|
|
15414
15445
|
message: `Chunk "${title}" has no file pointers`,
|
|
15415
15446
|
line: headingLine,
|
|
15416
15447
|
range: nodeRange(heading)
|
|
@@ -15421,7 +15452,8 @@ const parseChangesets = (tree, content, lines) => {
|
|
|
15421
15452
|
descriptionNodes,
|
|
15422
15453
|
descriptionInline,
|
|
15423
15454
|
files,
|
|
15424
|
-
headingLine
|
|
15455
|
+
headingLine,
|
|
15456
|
+
...answers === void 0 ? {} : { answers }
|
|
15425
15457
|
});
|
|
15426
15458
|
}
|
|
15427
15459
|
if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
|
|
@@ -15502,7 +15534,9 @@ const isHunkPointerItem = (content, item) => {
|
|
|
15502
15534
|
* clear, so a caller can skip the write and leave the mtime untouched.
|
|
15503
15535
|
*/
|
|
15504
15536
|
const reviewClearTicks = (content) => {
|
|
15505
|
-
const
|
|
15537
|
+
const tree = parseMarkdown(content);
|
|
15538
|
+
const answers = new Set(splitChunks(tree).flatMap(({ body }) => answerListOf(content, body)?.list.children ?? []));
|
|
15539
|
+
const offsets = taskItems(tree).filter((item) => item.checked === true && !answers.has(item) && isHunkPointerItem(content, item)).map((item) => checkboxOffset(content, item)).filter((o) => o !== void 0).sort((a, b) => a - b);
|
|
15506
15540
|
if (offsets.length === 0) return content;
|
|
15507
15541
|
let result = "";
|
|
15508
15542
|
let cursor = 0;
|
|
@@ -15936,6 +15970,55 @@ const reviewRisks = (content) => parseReviewDoc(content).changesets.flatMap((chu
|
|
|
15936
15970
|
id: `risk-${i + 1}`,
|
|
15937
15971
|
...r
|
|
15938
15972
|
}));
|
|
15973
|
+
const CHOICES = [
|
|
15974
|
+
"fix",
|
|
15975
|
+
"wontfix",
|
|
15976
|
+
"answer"
|
|
15977
|
+
];
|
|
15978
|
+
const decisionOf = (chunk) => {
|
|
15979
|
+
if (chunk.answers?.length !== 3) return void 0;
|
|
15980
|
+
const ticked = chunk.answers.flatMap((o, i) => o.checked ? [i] : []);
|
|
15981
|
+
if (ticked.length !== 1) return void 0;
|
|
15982
|
+
const i = ticked[0];
|
|
15983
|
+
const text = i === 2 ? chunk.answers[2].text : "";
|
|
15984
|
+
if (i === 2 && text === "") return void 0;
|
|
15985
|
+
return {
|
|
15986
|
+
anchor: chunk.title,
|
|
15987
|
+
body: chunk.description,
|
|
15988
|
+
choice: CHOICES[i],
|
|
15989
|
+
text
|
|
15990
|
+
};
|
|
15991
|
+
};
|
|
15992
|
+
/**
|
|
15993
|
+
* Notes whose answer boxes do not hold exactly one tick (a ticked free-text
|
|
15994
|
+
* slot filled in), with their 1-based heading line and why; and, given the
|
|
15995
|
+
* file `before` the edit, every note whose boxes were deleted.
|
|
15996
|
+
*/
|
|
15997
|
+
const unansweredNotes = (content, before) => {
|
|
15998
|
+
const now = parseReviewDoc(content).changesets;
|
|
15999
|
+
const boxed = new Set(now.filter((c) => c.answers !== void 0).map((c) => c.title));
|
|
16000
|
+
const removed = (before === void 0 ? [] : parseReviewDoc(before).changesets).filter((c) => c.answers !== void 0 && !boxed.has(c.title)).map((c) => ({
|
|
16001
|
+
title: c.title,
|
|
16002
|
+
line: 0,
|
|
16003
|
+
reason: "its answer boxes were deleted — restore them"
|
|
16004
|
+
}));
|
|
16005
|
+
return [...now.filter((c) => c.answers !== void 0 && decisionOf(c) === void 0).map((c) => ({
|
|
16006
|
+
title: c.title,
|
|
16007
|
+
line: c.headingLine + 1,
|
|
16008
|
+
reason: c.answers.length === 3 ? "tick exactly one box" : `it has ${c.answers.length} boxes — restore the three gtd wrote`
|
|
16009
|
+
})), ...removed];
|
|
16010
|
+
};
|
|
16011
|
+
/** Every answer in `after` that `before` did not already hold for the same note, in document order. */
|
|
16012
|
+
const reviewDecisions = (before, after) => {
|
|
16013
|
+
const prior = new Map(parseReviewDoc(before).changesets.map((c) => [c.title, c]));
|
|
16014
|
+
return parseReviewDoc(after).changesets.flatMap((chunk) => {
|
|
16015
|
+
const decision = decisionOf(chunk);
|
|
16016
|
+
if (decision === void 0) return [];
|
|
16017
|
+
const was = prior.get(chunk.title);
|
|
16018
|
+
const old = was === void 0 ? void 0 : decisionOf(was);
|
|
16019
|
+
return old?.choice === decision.choice && old.text === decision.text ? [] : [decision];
|
|
16020
|
+
});
|
|
16021
|
+
};
|
|
15939
16022
|
//#endregion
|
|
15940
16023
|
//#region src/steering/Edits.ts
|
|
15941
16024
|
/** Splices `edits` into `content`, sorted last-to-first so an earlier range's offset is never invalidated by a later edit. Positions are 0-based line/character over `\n`-split lines. */
|
|
@@ -16304,4 +16387,4 @@ const viewOf = (format, content) => ({
|
|
|
16304
16387
|
/** Every OPEN thread — last entry is the agent's — as `{ name, line, firstMe }` (0-based line). `[]` for a format with no thread concept. */
|
|
16305
16388
|
const openThreads = (format, content) => format.openThreads?.(content) ?? [];
|
|
16306
16389
|
//#endregion
|
|
16307
|
-
export {
|
|
16390
|
+
export { parseFootnotes as C, headingSectionBodies as D, getParseCount as E, headingSections as O, openThreadFindings as S, parseThreadsWithFindings as T, unansweredNotes as _, unansweredQuestions as a, FOOTNOTE_ACTION_TITLE as b, stripCodeThreads as c, ANSWER_FIX as d, ANSWER_LEAD as f, reviewRisks as g, reviewNotes as h, steeringFormatOrFreeForm as i, formatFinding as k, freeFormFormat as l, reviewDecisions as m, openThreads as n, viewOf as o, ANSWER_WONT_FIX as p, steeringFormatFor as r, parseCodeThreads as s, BUILT_IN_MODE_NAMES as t, applySteeringEdits as u, FREE_TEXT_PLACEHOLDER as v, parseThreads as w, THREAD_REPLY_ACTION_TITLE as x, isAnswered as y };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, head, judge, mark, numeric, read, refuse, removeScript, run, scope, sectionBodies, showScript, vars, writeScript } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import { C as fixCheck, D as reviewMissing,
|
|
3
|
+
import { C as fixCheck, D as reviewMissing, J as handOffAwaitReviewMessage, O as reviewQuality, T as fixRisks, d as guarded, f as healthy, h as QUALITY, k as reviewing, l as escalation, nt as qualityFindingsFile, rt as qualityTriageMessage, w as fixQuality, x as awaitReview, y as REVIEW } from "./reserved-COEMFEpy.mjs";
|
|
4
4
|
//#region src/workflows/shared/lenses.ts
|
|
5
5
|
const NO_FINDINGS = {
|
|
6
6
|
notes: [],
|