@pmelab/gtd 2.1.0 → 2.2.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 +33 -0
- package/dist/gtd.bundle.mjs +95 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,12 @@ to cwd, so it refuses with a clear error if invoked from a subdirectory.
|
|
|
116
116
|
`--jsn`) is rejected with a usage error rather than silently ignored, so a
|
|
117
117
|
mistyped flag can never degrade a JSON caller to plain-text mode.
|
|
118
118
|
|
|
119
|
+
One nuance to "(no mutation)": `next` and `status` never author commits or
|
|
120
|
+
change workflow state, but while a human review is pending they do maintain the
|
|
121
|
+
review checkout window (closing it to read state, re-arming it on the way out —
|
|
122
|
+
see [Human review gate](#human-review-gate)), which transiently moves HEAD and
|
|
123
|
+
the index. The working tree is never touched.
|
|
124
|
+
|
|
119
125
|
### `gtd step` / `gtd step-agent`
|
|
120
126
|
|
|
121
127
|
Both drive the **same fixpoint loop** — gather → resolve → perform the returned
|
|
@@ -547,12 +553,39 @@ Once `.gtd/` is fully closed, the machine writes `.gtd/REVIEW.md` and rests at
|
|
|
547
553
|
plus routing `gtd: done`.
|
|
548
554
|
- Flipping only `- [ ]` → `- [x]` checkboxes in `.gtd/REVIEW.md` — checkbox-only
|
|
549
555
|
edits are also treated as clean approval.
|
|
556
|
+
- Deleting `.gtd/REVIEW.md` outright.
|
|
550
557
|
|
|
551
558
|
Any **substantive** edit — to `.gtd/REVIEW.md` prose, or to the reviewed code
|
|
552
559
|
itself — is feedback: `gtd(human): review` plus routing `gtd: review feedback`,
|
|
553
560
|
`.gtd/REVIEW.md` removed, and `gtd next` re-emits a grilling prompt to the agent
|
|
554
561
|
that inlines the human's finding.
|
|
555
562
|
|
|
563
|
+
**The review diff lives in your editor.** While the gate is pending, gtd holds
|
|
564
|
+
open a _review checkout window_: it saves the real head to
|
|
565
|
+
`refs/gtd/review-head`, then rewinds HEAD and the index to the review base with
|
|
566
|
+
`git reset --mixed`, leaving the working tree untouched. Every editor's standard
|
|
567
|
+
git integration now shows the entire reviewable diff as ordinary uncommitted
|
|
568
|
+
changes — SCM panel, gutter marks, per-file diffs. Review it there:
|
|
569
|
+
|
|
570
|
+
- **Edit** anything (code or `.gtd/REVIEW.md` prose) → feedback.
|
|
571
|
+
- **Discard a hunk** in the editor → that reversion IS the feedback: the agent
|
|
572
|
+
is re-grilled with it.
|
|
573
|
+
- **Delete a surfaced file** → reject-this-file feedback.
|
|
574
|
+
- Touch nothing (or tick checkboxes / delete `.gtd/REVIEW.md`) → approval.
|
|
575
|
+
|
|
576
|
+
Any gtd invocation closes the window first (restoring HEAD/index exactly, so
|
|
577
|
+
only your own edits remain dirty — they land as their own separate
|
|
578
|
+
`gtd(human): review` commit, never mixed into the reviewed work), and
|
|
579
|
+
`gtd next`/`gtd status` re-arm it on their way out. The mechanics are
|
|
580
|
+
crash-safe; details and invariants in STATES.md ("The review checkout window").
|
|
581
|
+
|
|
582
|
+
Caveats while a review is pending: don't push (the branch tip rests at the
|
|
583
|
+
review base — the real head is safe under `refs/gtd/review-head`); commits you
|
|
584
|
+
make manually survive as working-tree content and become review feedback, but
|
|
585
|
+
their commit message is discarded; linked `git worktree` checkouts are
|
|
586
|
+
unsupported. If you switch branches mid-review, gtd refuses to touch the foreign
|
|
587
|
+
branch and prints the manual recovery command.
|
|
588
|
+
|
|
556
589
|
### Squash
|
|
557
590
|
|
|
558
591
|
With `squash: true` (the default), `gtd: done` is **not** a rest — the same
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -367862,6 +367862,10 @@ var makeGitImpl = (executor, root2) => {
|
|
|
367862
367862
|
(hash3) => /^[0-9a-f]{40}$/.test(hash3) ? Effect_exports.succeed(hash3) : Effect_exports.fail(new Error(`Invalid ref: ${ref}`))
|
|
367863
367863
|
)
|
|
367864
367864
|
),
|
|
367865
|
+
readRefOption: (ref) => exec2("git", "rev-parse", "--verify", "--quiet", ref).pipe(
|
|
367866
|
+
Effect_exports.map((s5) => Option_exports.some(s5.trim())),
|
|
367867
|
+
Effect_exports.catchAll(() => Effect_exports.succeed(Option_exports.none()))
|
|
367868
|
+
),
|
|
367865
367869
|
topLevel: () => exec2("git", "rev-parse", "--show-toplevel").pipe(Effect_exports.map((s5) => s5.trim())),
|
|
367866
367870
|
resolveDefaultBranch: () => exec2("git", "rev-parse", "--abbrev-ref", "origin/HEAD").pipe(
|
|
367867
367871
|
Effect_exports.map((s5) => s5.trim()),
|
|
@@ -367995,7 +367999,20 @@ var makeGitImpl = (executor, root2) => {
|
|
|
367995
367999
|
)
|
|
367996
368000
|
);
|
|
367997
368001
|
}).pipe(Effect_exports.asVoid),
|
|
367998
|
-
softResetTo: (ref) => exec2("git", "reset", "--soft", ref).pipe(Effect_exports.asVoid)
|
|
368002
|
+
softResetTo: (ref) => exec2("git", "reset", "--soft", ref).pipe(Effect_exports.asVoid),
|
|
368003
|
+
updateRef: (ref, hash3) => exec2("git", "update-ref", ref, hash3).pipe(Effect_exports.asVoid),
|
|
368004
|
+
deleteRef: (ref) => exec2("git", "update-ref", "-d", ref).pipe(
|
|
368005
|
+
Effect_exports.asVoid,
|
|
368006
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
368007
|
+
),
|
|
368008
|
+
mixedResetTo: (ref) => exec2("git", "reset", "--mixed", ref).pipe(Effect_exports.asVoid),
|
|
368009
|
+
restoreStagedFrom: (source2, paths) => paths.length === 0 ? Effect_exports.void : exec2("git", "restore", "--staged", `--source=${source2}`, "--", ...paths).pipe(
|
|
368010
|
+
Effect_exports.asVoid,
|
|
368011
|
+
// `git restore` errors when a pathspec matches nothing at either
|
|
368012
|
+
// side — for an optional dir like `.gtd/` that's a no-op, not a failure.
|
|
368013
|
+
Effect_exports.catchAll(() => Effect_exports.void)
|
|
368014
|
+
),
|
|
368015
|
+
addIntentToAdd: () => exec2("git", "add", "--intent-to-add", ".").pipe(Effect_exports.asVoid)
|
|
367999
368016
|
};
|
|
368000
368017
|
};
|
|
368001
368018
|
var makeLiveEffect = Effect_exports.gen(function* () {
|
|
@@ -389493,6 +389510,78 @@ var buildPrompt = (result, resolveModel = builtinResolveModel, output = "plain")
|
|
|
389493
389510
|
return raw.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
389494
389511
|
};
|
|
389495
389512
|
|
|
389513
|
+
// src/ReviewWindow.ts
|
|
389514
|
+
var REVIEW_HEAD_REF = "refs/gtd/review-head";
|
|
389515
|
+
var REVIEW_BASE_REF = "refs/gtd/review-base";
|
|
389516
|
+
var AWAITING_REVIEW_SUBJECT = ROUTING_SUBJECT["awaiting-review"];
|
|
389517
|
+
var subjectOf2 = (message) => message.split("\n")[0] ?? "";
|
|
389518
|
+
var reviewWindowBase = (history) => {
|
|
389519
|
+
const beforeHead = history.slice(0, -1);
|
|
389520
|
+
let lastDoneIdx = -1;
|
|
389521
|
+
for (let i = 0; i < beforeHead.length; i++) {
|
|
389522
|
+
if (subjectOf2(beforeHead[i].message) === ROUTING_SUBJECT.done) lastDoneIdx = i;
|
|
389523
|
+
}
|
|
389524
|
+
const currentCycle = beforeHead.slice(lastDoneIdx + 1);
|
|
389525
|
+
let anchor;
|
|
389526
|
+
let lastAwaitingReview;
|
|
389527
|
+
let firstGrilling;
|
|
389528
|
+
for (const c7 of currentCycle) {
|
|
389529
|
+
const parsed = parseSubject(subjectOf2(c7.message));
|
|
389530
|
+
if (parsed.kind === "routing" && parsed.phase === "reviewing" && parsed.param !== void 0) {
|
|
389531
|
+
anchor = parsed.param;
|
|
389532
|
+
}
|
|
389533
|
+
if (parsed.kind === "routing" && parsed.phase === "awaiting-review") {
|
|
389534
|
+
lastAwaitingReview = c7.hash;
|
|
389535
|
+
}
|
|
389536
|
+
if (parsed.kind === "turn" && parsed.gate === "grilling" && firstGrilling === void 0) {
|
|
389537
|
+
firstGrilling = c7.hash;
|
|
389538
|
+
}
|
|
389539
|
+
}
|
|
389540
|
+
return anchor ?? lastAwaitingReview ?? firstGrilling;
|
|
389541
|
+
};
|
|
389542
|
+
var closeReviewWindow = Effect_exports.gen(
|
|
389543
|
+
function* () {
|
|
389544
|
+
const git = yield* GitService;
|
|
389545
|
+
const savedHead = yield* git.readRefOption(REVIEW_HEAD_REF);
|
|
389546
|
+
if (Option_exports.isNone(savedHead)) return { closed: false };
|
|
389547
|
+
const base = yield* git.readRefOption(REVIEW_BASE_REF);
|
|
389548
|
+
if (Option_exports.isSome(base)) {
|
|
389549
|
+
const onReviewedBranch = yield* git.isAncestor(base.value, "HEAD");
|
|
389550
|
+
if (!onReviewedBranch) {
|
|
389551
|
+
return yield* Effect_exports.fail(
|
|
389552
|
+
new Error(
|
|
389553
|
+
"a gtd review checkout window is open but HEAD has moved off the reviewed branch \u2014 return to it, or restore manually with `git reset --mixed refs/gtd/review-head && git update-ref -d refs/gtd/review-head && git update-ref -d refs/gtd/review-base`"
|
|
389554
|
+
)
|
|
389555
|
+
);
|
|
389556
|
+
}
|
|
389557
|
+
}
|
|
389558
|
+
yield* git.mixedResetTo(REVIEW_HEAD_REF);
|
|
389559
|
+
yield* git.deleteRef(REVIEW_HEAD_REF);
|
|
389560
|
+
yield* git.deleteRef(REVIEW_BASE_REF);
|
|
389561
|
+
return { closed: true };
|
|
389562
|
+
}
|
|
389563
|
+
);
|
|
389564
|
+
var openReviewWindow = Effect_exports.gen(
|
|
389565
|
+
function* () {
|
|
389566
|
+
const git = yield* GitService;
|
|
389567
|
+
const hasCommits = yield* git.hasCommits();
|
|
389568
|
+
if (!hasCommits) return { opened: false };
|
|
389569
|
+
const subject = yield* git.lastCommitSubject();
|
|
389570
|
+
if (subject !== AWAITING_REVIEW_SUBJECT) return { opened: false };
|
|
389571
|
+
const history = yield* git.commitHistory();
|
|
389572
|
+
const base = reviewWindowBase(history);
|
|
389573
|
+
if (base === void 0) return { opened: false };
|
|
389574
|
+
const headHash = yield* git.resolveRef("HEAD");
|
|
389575
|
+
if (base === headHash) return { opened: false };
|
|
389576
|
+
yield* git.updateRef(REVIEW_BASE_REF, base);
|
|
389577
|
+
yield* git.updateRef(REVIEW_HEAD_REF, headHash);
|
|
389578
|
+
yield* git.mixedResetTo(base);
|
|
389579
|
+
yield* git.restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"]);
|
|
389580
|
+
yield* git.addIntentToAdd();
|
|
389581
|
+
return { opened: true };
|
|
389582
|
+
}
|
|
389583
|
+
);
|
|
389584
|
+
|
|
389496
389585
|
// src/State.ts
|
|
389497
389586
|
var edgeActionHandlers = {
|
|
389498
389587
|
captureTurn: (a5) => `capture the ${a5.actor} turn as "gtd(${a5.actor}): ${a5.gate}"`,
|
|
@@ -389807,9 +389896,13 @@ function makeProgram(opts = {}) {
|
|
|
389807
389896
|
const git = yield* GitService;
|
|
389808
389897
|
const fs9 = yield* FileSystem_exports.FileSystem;
|
|
389809
389898
|
yield* assertRunningFromRepoRoot(git, fs9);
|
|
389899
|
+
yield* closeReviewWindow;
|
|
389810
389900
|
yield* (yield* ConfigInit).ensure;
|
|
389811
389901
|
const config2 = yield* ConfigService;
|
|
389812
|
-
yield* dispatchKnownSubcommand(sub, argv, git, config2, json2, write4)
|
|
389902
|
+
yield* dispatchKnownSubcommand(sub, argv, git, config2, json2, write4).pipe(
|
|
389903
|
+
Effect_exports.tap(() => openReviewWindow),
|
|
389904
|
+
Effect_exports.tapError(() => openReviewWindow.pipe(Effect_exports.ignore))
|
|
389905
|
+
);
|
|
389813
389906
|
}).pipe(
|
|
389814
389907
|
json2 ? Effect_exports.catchAll(
|
|
389815
389908
|
(error) => Effect_exports.sync(
|