@pmelab/gtd 1.8.0 → 1.9.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/README.md +63 -22
- package/dist/gtd.bundle.mjs +75 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,14 @@ No config file, no setup subcommand.
|
|
|
50
50
|
|
|
51
51
|
Run `gtd` from your repository's working directory — it prints the next prompt
|
|
52
52
|
to stdout. It takes **no ref argument** — the review base is always
|
|
53
|
-
auto-computed.
|
|
53
|
+
auto-computed. The one exception is `gtd review <target>`: an explicit,
|
|
54
|
+
on-demand human review against a chosen base (see
|
|
55
|
+
[Review subcommand](#review-subcommand) below).
|
|
56
|
+
|
|
57
|
+
On an idle tree outside a process (no steering files, nothing reviewable), `gtd`
|
|
58
|
+
now runs a **health check** instead of stopping immediately — `gtd review` is
|
|
59
|
+
the only way to start an ad-hoc review when the automatic review base yields no
|
|
60
|
+
diff.
|
|
54
61
|
|
|
55
62
|
## JSON output mode
|
|
56
63
|
|
|
@@ -181,7 +188,8 @@ The complete set:
|
|
|
181
188
|
`gtd: new task` · `gtd: grilling` · `gtd: grilled` · `gtd: planning` ·
|
|
182
189
|
`gtd: building` · `gtd: errors` · `gtd: feedback` · `gtd: fixing` ·
|
|
183
190
|
`gtd: package done` · `gtd: awaiting review` · `gtd: done` · `gtd: health-check`
|
|
184
|
-
· `gtd: health-fix` — plus the hand-made `gtd: transport`
|
|
191
|
+
· `gtd: health-fix` · `gtd: reviewing` — plus the hand-made `gtd: transport`
|
|
192
|
+
(see below).
|
|
185
193
|
|
|
186
194
|
The last commit subject is bucketed two ways:
|
|
187
195
|
|
|
@@ -494,9 +502,9 @@ loop before the next one starts.
|
|
|
494
502
|
(`gtd: done`) → **Squashing** → **Idle**. The Squashing agent authors a
|
|
495
503
|
conventional-commits message from the full process diff and squashes all
|
|
496
504
|
intermediate `gtd: *` commits into one with `git reset --soft <base>` +
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
505
|
+
`git commit`, then **gtd STOPs**. The base is the parent of the current
|
|
506
|
+
cycle's start marker **nearest to HEAD** (the last `gtd: new task`; for
|
|
507
|
+
legacy cycles the last contiguous `gtd: grilling` run), and on a feature
|
|
500
508
|
branch it never reaches below the merge-base with the default branch — stray
|
|
501
509
|
markers left behind by older squashes can never drag the squash into
|
|
502
510
|
previously shipped features. Post-squash review does not fire automatically —
|
|
@@ -505,20 +513,13 @@ loop before the next one starts.
|
|
|
505
513
|
has no unrelated code dirty — a lone untracked `SQUASH_MSG.md` is tolerated
|
|
506
514
|
and deleted before the squash commit. If unrelated code is dirty at
|
|
507
515
|
`gtd: done`, gtd routes to **New Feature** instead. Set `squash: false` in
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
> > > > > > > straight to Idle. Checking off REVIEW.md checkboxes (`- [ ]` →
|
|
516
|
-
> > > > > > > `- [x]`) also counts as approval and routes to **Done** — they
|
|
517
|
-
> > > > > > > are navigation aids, not feedback. Only **non-checkbox** edits
|
|
518
|
-
> > > > > > > (code changes, inline comments, textual annotations in
|
|
519
|
-
> > > > > > > REVIEW.md) trigger **Accept Review**, which seeds a fresh
|
|
520
|
-
> > > > > > > `TODO.md` from your feedback, discards your code edits, removes
|
|
521
|
-
> > > > > > > `REVIEW.md`, and re-enters Grilling — the loop starts over.
|
|
516
|
+
`.gtdrc` to skip squashing and go straight to Idle. Checking off REVIEW.md
|
|
517
|
+
checkboxes (`- [ ]` → `- [x]`) also counts as approval and routes to **Done**
|
|
518
|
+
— they are navigation aids, not feedback. Only **non-checkbox** edits (code
|
|
519
|
+
changes, inline comments, textual annotations in REVIEW.md) trigger **Accept
|
|
520
|
+
Review**, which seeds a fresh `TODO.md` from your feedback, discards your
|
|
521
|
+
code edits, removes `REVIEW.md`, and re-enters Grilling — the loop starts
|
|
522
|
+
over.
|
|
522
523
|
|
|
523
524
|
## Configuration
|
|
524
525
|
|
|
@@ -738,10 +739,50 @@ When there are genuinely no open questions left, the agent writes the sentinel
|
|
|
738
739
|
line `no open questions — run gtd to plan` and leaves **no** markers — a clean
|
|
739
740
|
tree with no markers is what advances the plan to **Grilled** and decomposition.
|
|
740
741
|
|
|
741
|
-
##
|
|
742
|
+
## Subcommands
|
|
743
|
+
|
|
744
|
+
gtd ships two subcommands: `format` and `review`.
|
|
745
|
+
|
|
746
|
+
## Review subcommand
|
|
747
|
+
|
|
748
|
+
```bash
|
|
749
|
+
gtd review <target>
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
Starts an explicit, on-demand human review of the diff between HEAD and
|
|
753
|
+
`merge-base(<target>, HEAD)`. Use this when the automatic review base yields no
|
|
754
|
+
diff (idle tree outside a process) or when you want to review against a specific
|
|
755
|
+
base regardless of workflow state.
|
|
756
|
+
|
|
757
|
+
### Flow
|
|
758
|
+
|
|
759
|
+
1. `gtd review <target>` computes the diff HEAD adds over
|
|
760
|
+
`merge-base(<target>, HEAD)`.
|
|
761
|
+
2. The edge writes an empty anchor commit `gtd: reviewing` (no content — just
|
|
762
|
+
the marker).
|
|
763
|
+
3. The **Clean** state writes `REVIEW.md` with the computed diff and emits the
|
|
764
|
+
normal review prompt. `--json` is accepted and enables auto-advance mode
|
|
765
|
+
(same behavior as the default command).
|
|
766
|
+
4. The normal loop then drives: **Await Review** → **Done** → **Squashing**,
|
|
767
|
+
collapsing back to the `gtd: reviewing` anchor commit.
|
|
768
|
+
|
|
769
|
+
### Error handling
|
|
770
|
+
|
|
771
|
+
All errors exit with **code 1** and write a message to **stderr**:
|
|
772
|
+
|
|
773
|
+
- **Missing target** — `gtd review` with no argument:
|
|
774
|
+
`gtd review: missing target argument`
|
|
775
|
+
- **Extra arguments** — `gtd review main extra`:
|
|
776
|
+
`gtd review: too many arguments — expected one target, got: …`
|
|
777
|
+
- **Unresolvable ref** — the target cannot be resolved by git:
|
|
778
|
+
`gtd review: cannot resolve ref '<target>': <error message>`
|
|
779
|
+
- **Empty diff** — the merge-base diff between `<target>` and HEAD is empty
|
|
780
|
+
(nothing to review):
|
|
781
|
+
`gtd review: nothing to review (<target> diff is empty after filtering)`
|
|
782
|
+
|
|
783
|
+
## Format subcommand
|
|
742
784
|
|
|
743
|
-
gtd
|
|
744
|
-
markdown file in place:
|
|
785
|
+
`gtd format` formats a markdown file in place:
|
|
745
786
|
|
|
746
787
|
```bash
|
|
747
788
|
gtd format <file>
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -368477,8 +368477,8 @@ var fixing_default = '<%~ include("@header") %>\n\n<%~ include("@context", { con
|
|
|
368477
368477
|
// src/prompts/agentic-review.md
|
|
368478
368478
|
var agentic_review_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nSpawn a **reviewing subagent** using model `<%= it.model %>`. The package\'s task spec\nfiles and its cumulative diff are found below. The subagent must:\n\n1. **Read the task spec files** \u2014 they define the acceptance criteria. Every\n requirement must be checked.\n2. **Read the package diff** \u2014 examine every hunk to determine whether the\n implementation satisfies the spec.\n3. **Always write `FEEDBACK.md`** in the repo root:\n - **Fully satisfies the spec** \u2192 write an **empty** `FEEDBACK.md` (zero bytes\n or whitespace only). This empty file is the **approval** signal.\n - **Does not fully satisfy** \u2192 write concrete, actionable findings anchored\n to specific file and symbol names, grouped under short headings, so the fix\n agent can act without re-reading the diff.\n4. **Do not edit source files and do not commit** \u2014 the reviewer only writes\n `FEEDBACK.md`, left uncommitted.\n<% if (it.context.packages[0]) { %>\n<%~ include("@package", { pkg: it.context.packages[0], fenceFor: it.fenceFor }) %>\n<% } %><% if (it.context.refDiff && it.context.refDiff.trim()) { %>\n<%~ include("@diff", { heading: "Package diff", diff: it.context.refDiff, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<%~ include(it.tail) %>\n';
|
|
368479
368479
|
|
|
368480
|
-
// src/prompts/
|
|
368481
|
-
var
|
|
368480
|
+
// src/prompts/review.md
|
|
368481
|
+
var review_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nSpawn a **planning-model subagent** using model `<%= it.model %>` to author a\n`REVIEW.md` file that will help a human to review the changes. It must:\n\n1. **Read the diff inlined below** \u2014 extract the changed hunks with their file\n paths.\n2. **Group hunks semantically** \u2014 cluster hunks that serve the same logical\n concern (the same feature, refactor, or fix), even across files. Aim for the\n fewest chunks that keep the review navigable.\n3. **Write `REVIEW.md`** in the repo root in this format:\n\n ```markdown\n # Review: <short-hash>\n\n <!-- base: <full-hash> -->\n\n ## <Chunk Title>\n\n <What this chunk changes and why>\n\n - [ ] ./path/to/file.ts#42\n - [ ] ./path/to/file.ts#99\n\n ## <Another Chunk Title>\n\n <Explanation>\n\n - [ ] ./path/to/another.ts#1\n ```\n\n - `<short-hash>` is the first 7 characters of the review base SHA;\n `<full-hash>` is the full SHA. Both are read from the `Review base:` line /\n diff label in the prompt context.\n - Chunk titles are short imperative phrases (\u2264 6 words).\n - Explanations describe _what_ changed and _why_, not just where.\n - File pointers are relative, prefixed with `./`; the line numbers (`#42`)\n are creation-time hints that will drift \u2014 not authoritative.\n - Checkboxes (`- [ ]`) signal approval \u2014 ticking them (with no other edits)\n counts as approving the review (`gtd: done`). Only non-checkbox edits to\n `REVIEW.md` (or any code edits) are treated as a change-request.\n - The user checks off or edits items in place as they work through the\n review; there is no separate Resolved section.\n\n4. Leave `REVIEW.md` **uncommitted**, the outer loop will process it.\n<% if (it.context.refDiff && it.context.refDiff.trim()) { %>\n<%= it.context.reviewBase !== undefined ? "\\nReview base: " + it.context.reviewBase + "\\n" : "" %>\n<%~ include("@diff", { heading: "Changes to review (`git diff " + (it.context.reviewBase !== undefined ? it.context.reviewBase : "<base>") + " HEAD`)", diff: it.context.refDiff, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<%~ include(it.tail) %>\n';
|
|
368482
368482
|
|
|
368483
368483
|
// src/prompts/squashing.md
|
|
368484
368484
|
var squashing_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nThe process is **approved and done**. Your job is to author a clean\nconventional-commits squash message and hand it off to the harness.\n\n### Step 1 \u2014 Extract decisions from grilling rounds\n\nScan the git history of recent `gtd: ...` commits. Look for changes to `TODO.md`\n\u2014 specifically the `## Captured input (grilling)` sections and any edits to plan\ntext. Extract **key decisions, trade-offs, and design choices** made during\ngrilling rounds. These will appear in the commit body so the history is\nself-documenting.\n\n### Step 2 \u2014 Draft the commit message\n\nDraft ONE conventional-commits message:\n\n```\ntype(scope): subject\n\nbody (explain the why \u2014 motivation, trade-offs, key decisions from grilling)\n```\n\n- **type**: `feat` / `fix` / `refactor` / `chore` / `docs` / `test`\n- **subject**: imperative mood, \u2264 72 characters, lowercase after the colon\n- **body**: include the important decisions / trade-offs from grilling sessions.\n Omit if there were no meaningful decisions to capture.\n\n### Step 3 \u2014 Write SQUASH_MSG.md and hand off\n\nWrite the commit message (plain text, no markdown wrapper) to `SQUASH_MSG.md` in\nthe repo root, then leave it uncommitted \u2014 the harness handles the squash commit\non the next cycle once it sees `SQUASH_MSG.md`.\n\n**Do not run `git reset --soft` or `git commit` yourself** \u2014 the harness handles\nthe squash commit.\n<% if (it.context.squashDiff && it.context.squashDiff.trim()) { %>\n<%= it.context.squashBase !== undefined ? "\\nSquash base: " + it.context.squashBase + "\\n" : "" %>\n<%~ include("@diff", { heading: "Full-process diff (`git diff " + (it.context.squashBase !== undefined ? it.context.squashBase : "<squashBase>") + " HEAD`)", diff: it.context.squashDiff, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<%~ include(it.tail) %>\n';
|
|
@@ -368532,7 +368532,7 @@ eta.loadTemplate("@decompose", decompose_default);
|
|
|
368532
368532
|
eta.loadTemplate("@building", building_default);
|
|
368533
368533
|
eta.loadTemplate("@fixing", fixing_default);
|
|
368534
368534
|
eta.loadTemplate("@agentic-review", agentic_review_default);
|
|
368535
|
-
eta.loadTemplate("@
|
|
368535
|
+
eta.loadTemplate("@review", review_default);
|
|
368536
368536
|
eta.loadTemplate("@squashing", squashing_default);
|
|
368537
368537
|
eta.loadTemplate("@escalate", escalate_default);
|
|
368538
368538
|
eta.loadTemplate("@idle", idle_default);
|
|
@@ -368544,7 +368544,7 @@ var STATE_TEMPLATE = {
|
|
|
368544
368544
|
building: "@building",
|
|
368545
368545
|
fixing: "@fixing",
|
|
368546
368546
|
"agentic-review": "@agentic-review",
|
|
368547
|
-
clean: "@
|
|
368547
|
+
clean: "@review",
|
|
368548
368548
|
squashing: "@squashing",
|
|
368549
368549
|
escalate: "@escalate",
|
|
368550
368550
|
idle: "@idle",
|
|
@@ -385730,7 +385730,7 @@ function clean(original, cloned) {
|
|
|
385730
385730
|
}
|
|
385731
385731
|
return cloned;
|
|
385732
385732
|
}
|
|
385733
|
-
var
|
|
385733
|
+
var clean_default = clean;
|
|
385734
385734
|
function printFrontMatter({ node }) {
|
|
385735
385735
|
return node.raw;
|
|
385736
385736
|
}
|
|
@@ -385835,7 +385835,7 @@ function normalizePrinter(printer2) {
|
|
|
385835
385835
|
if (originalCleanFunction && frontMatterSupport.massageAstNode) {
|
|
385836
385836
|
massageAstNode = new Proxy(originalCleanFunction, {
|
|
385837
385837
|
apply(target, thisArgument, argumentsList) {
|
|
385838
|
-
|
|
385838
|
+
clean_default(...argumentsList);
|
|
385839
385839
|
return Reflect.apply(target, thisArgument, argumentsList);
|
|
385840
385840
|
}
|
|
385841
385841
|
});
|
|
@@ -388279,6 +388279,7 @@ var PACKAGE_DONE_SUBJECT = "gtd: package done";
|
|
|
388279
388279
|
var AWAITING_REVIEW_SUBJECT = "gtd: awaiting review";
|
|
388280
388280
|
var DONE_SUBJECT = "gtd: done";
|
|
388281
388281
|
var REVIEW_FEEDBACK_SUBJECT = "gtd: review feedback";
|
|
388282
|
+
var REVIEWING_SUBJECT = "gtd: reviewing";
|
|
388282
388283
|
var SQUASH_MSG_FILE = "SQUASH_MSG.md";
|
|
388283
388284
|
var STEERING_FILES = [
|
|
388284
388285
|
TODO_FILE,
|
|
@@ -388577,7 +388578,7 @@ var gatherEvents = () => (
|
|
|
388577
388578
|
const squashCycle = squashHistory.slice(prevDoneIdx + 1, lastDoneIdxForSquash + 1);
|
|
388578
388579
|
let startIdx = -1;
|
|
388579
388580
|
for (let i = squashCycle.length - 1; i >= 0; i--) {
|
|
388580
|
-
if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT) {
|
|
388581
|
+
if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT || subjectOf(squashCycle[i]) === REVIEWING_SUBJECT) {
|
|
388581
388582
|
startIdx = i;
|
|
388582
388583
|
break;
|
|
388583
388584
|
}
|
|
@@ -388684,6 +388685,15 @@ var captureAndRevert = (git, subject, onRegen) => Effect_exports.gen(function* (
|
|
|
388684
388685
|
yield* git.revertNoCommit("HEAD");
|
|
388685
388686
|
return captured;
|
|
388686
388687
|
});
|
|
388688
|
+
var reviewAgainst = (target) => Effect_exports.gen(function* () {
|
|
388689
|
+
const git = yield* GitService;
|
|
388690
|
+
const targetHash = yield* git.resolveRef(target);
|
|
388691
|
+
const mergeBase = yield* git.mergeBase(target, "HEAD");
|
|
388692
|
+
const mergeBaseHash = Option_exports.isNone(mergeBase) || mergeBase.value === targetHash ? targetHash : mergeBase.value;
|
|
388693
|
+
const refDiff = yield* git.diffRef(mergeBaseHash, WORKFLOW_FILE_EXCLUDES);
|
|
388694
|
+
if (refDiff.trim().length === 0) return void 0;
|
|
388695
|
+
return { reviewBase: mergeBaseHash, refDiff };
|
|
388696
|
+
});
|
|
388687
388697
|
var perform = (action) => (
|
|
388688
388698
|
// fallow-ignore-next-line complexity
|
|
388689
388699
|
Effect_exports.gen(function* () {
|
|
@@ -389198,6 +389208,20 @@ var resolve4 = (events) => {
|
|
|
389198
389208
|
if (p4.todoExists) return resolveGrilling(p4, counters, head7);
|
|
389199
389209
|
return resolveCleanOrIdle(p4, counters, head7) ?? corrupt();
|
|
389200
389210
|
};
|
|
389211
|
+
var cleanResult = (args2) => ({
|
|
389212
|
+
state: "clean",
|
|
389213
|
+
autoAdvance: args2.autoAdvance,
|
|
389214
|
+
context: {
|
|
389215
|
+
...buildContext(
|
|
389216
|
+
{
|
|
389217
|
+
...DEFAULT_PAYLOAD,
|
|
389218
|
+
reviewBase: args2.reviewBase,
|
|
389219
|
+
refDiff: args2.refDiff
|
|
389220
|
+
},
|
|
389221
|
+
{ testFixCount: 0, reviewFixCount: 0, healthFixCount: 0 }
|
|
389222
|
+
)
|
|
389223
|
+
}
|
|
389224
|
+
});
|
|
389201
389225
|
|
|
389202
389226
|
// src/State.ts
|
|
389203
389227
|
var EDGE_ONLY_STATES2 = /* @__PURE__ */ new Set([
|
|
@@ -389257,10 +389281,6 @@ function makeProgram(opts = {}) {
|
|
|
389257
389281
|
yield* formatFile(args2[0]);
|
|
389258
389282
|
return;
|
|
389259
389283
|
}
|
|
389260
|
-
if (sub !== void 0) {
|
|
389261
|
-
return yield* Effect_exports.fail(new Error(`unknown command '${sub}'`));
|
|
389262
|
-
}
|
|
389263
|
-
const config2 = yield* ConfigService;
|
|
389264
389284
|
const git = yield* GitService;
|
|
389265
389285
|
const fs9 = yield* FileSystem_exports.FileSystem;
|
|
389266
389286
|
const topLevel = yield* git.topLevel();
|
|
@@ -389273,6 +389293,50 @@ function makeProgram(opts = {}) {
|
|
|
389273
389293
|
)
|
|
389274
389294
|
);
|
|
389275
389295
|
}
|
|
389296
|
+
const config2 = yield* ConfigService;
|
|
389297
|
+
if (sub === "review") {
|
|
389298
|
+
const args2 = argv.slice(3).filter((a5) => a5.length > 0 && !a5.startsWith("--"));
|
|
389299
|
+
if (args2.length === 0) {
|
|
389300
|
+
return yield* Effect_exports.fail(new Error("gtd review: missing target argument"));
|
|
389301
|
+
}
|
|
389302
|
+
if (args2.length > 1) {
|
|
389303
|
+
return yield* Effect_exports.fail(
|
|
389304
|
+
new Error(
|
|
389305
|
+
`gtd review: too many arguments \u2014 expected one target, got: ${args2.join(", ")}`
|
|
389306
|
+
)
|
|
389307
|
+
);
|
|
389308
|
+
}
|
|
389309
|
+
const target = args2[0];
|
|
389310
|
+
const reviewResult = yield* reviewAgainst(target).pipe(
|
|
389311
|
+
Effect_exports.catchAll(
|
|
389312
|
+
(error) => Effect_exports.fail(new Error(`gtd review: cannot resolve ref '${target}': ${error.message}`))
|
|
389313
|
+
)
|
|
389314
|
+
);
|
|
389315
|
+
if (reviewResult === void 0) {
|
|
389316
|
+
return yield* Effect_exports.fail(
|
|
389317
|
+
new Error(`gtd review: nothing to review (${target} diff is empty after filtering)`)
|
|
389318
|
+
);
|
|
389319
|
+
}
|
|
389320
|
+
const { reviewBase, refDiff } = reviewResult;
|
|
389321
|
+
yield* git.commitAllWithPrefix(REVIEWING_SUBJECT);
|
|
389322
|
+
const result = cleanResult({ reviewBase, refDiff, autoAdvance: json2 });
|
|
389323
|
+
const builtPrompt = buildPrompt(result, config2.resolveModel, json2 ? "json" : "plain");
|
|
389324
|
+
if (json2) {
|
|
389325
|
+
write4(
|
|
389326
|
+
JSON.stringify({
|
|
389327
|
+
state: result.state,
|
|
389328
|
+
autoAdvance: result.autoAdvance,
|
|
389329
|
+
prompt: builtPrompt
|
|
389330
|
+
}) + "\n"
|
|
389331
|
+
);
|
|
389332
|
+
} else {
|
|
389333
|
+
write4(builtPrompt);
|
|
389334
|
+
}
|
|
389335
|
+
return;
|
|
389336
|
+
}
|
|
389337
|
+
if (sub !== void 0) {
|
|
389338
|
+
return yield* Effect_exports.fail(new Error(`unknown command '${sub}'`));
|
|
389339
|
+
}
|
|
389276
389340
|
let hops = 0;
|
|
389277
389341
|
while (true) {
|
|
389278
389342
|
hops += 1;
|