@pmelab/gtd 2.0.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 +56 -0
- package/bin/gtd-loop +58 -0
- package/dist/gtd.bundle.mjs +95 -2
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -67,6 +67,9 @@ gtd next --json # ask who's up and what they should do
|
|
|
67
67
|
See [The reference loop driver](#the-reference-loop-driver) for a full script
|
|
68
68
|
implementing this protocol, and [`skills/loop/SKILL.md`](skills/loop/SKILL.md)
|
|
69
69
|
for the agent-facing instructions that follow the same pinned contract.
|
|
70
|
+
`gtd-loop`, installed alongside `gtd` (see below), is the packaged, ready-to-run
|
|
71
|
+
implementation of that same script for anyone who doesn't want to drive the loop
|
|
72
|
+
by hand.
|
|
70
73
|
|
|
71
74
|
## Installation
|
|
72
75
|
|
|
@@ -113,6 +116,12 @@ to cwd, so it refuses with a clear error if invoked from a subdirectory.
|
|
|
113
116
|
`--jsn`) is rejected with a usage error rather than silently ignored, so a
|
|
114
117
|
mistyped flag can never degrade a JSON caller to plain-text mode.
|
|
115
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
|
+
|
|
116
125
|
### `gtd step` / `gtd step-agent`
|
|
117
126
|
|
|
118
127
|
Both drive the **same fixpoint loop** — gather → resolve → perform the returned
|
|
@@ -397,6 +406,26 @@ side can drive — agent rests and agent-driven checkpoints — reports
|
|
|
397
406
|
cycles, a force-approved package close) chain without human involvement until an
|
|
398
407
|
actual human gate is hit.
|
|
399
408
|
|
|
409
|
+
`bin/gtd-loop`, installed as the `gtd-loop` binary, is the packaged
|
|
410
|
+
implementation of this exact script — kept in sync with it the same way
|
|
411
|
+
`skills/loop/SKILL.md` is. It additionally attempts `gtd step` (not just
|
|
412
|
+
`gtd step-agent`) every iteration, so a plain rerun after you've edited a file
|
|
413
|
+
at a human gate (no commit needed) picks up your edit and keeps going, and it
|
|
414
|
+
halts with a diagnostic if the same state and prompt repeat with no progress
|
|
415
|
+
(see `skills/loop/SKILL.md`'s "Stall detection").
|
|
416
|
+
|
|
417
|
+
### Using a different agent
|
|
418
|
+
|
|
419
|
+
`gtd-loop` defaults to
|
|
420
|
+
`claude -p "$GTD_LOOP_PROMPT" --dangerously-skip-permissions`, but the agent
|
|
421
|
+
invocation is swappable: set `GTD_LOOP_AGENT_CMD` to any shell command, and it
|
|
422
|
+
runs with the prompt available as `$GTD_LOOP_PROMPT` in its environment. For
|
|
423
|
+
example, to drive a different agent CLI:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
GTD_LOOP_AGENT_CMD='my-agent-cli --prompt "$GTD_LOOP_PROMPT"' gtd-loop
|
|
427
|
+
```
|
|
428
|
+
|
|
400
429
|
## States & subjects overview
|
|
401
430
|
|
|
402
431
|
`resolve()` lands on exactly one of **16 states**: `grilling`, `grilled`,
|
|
@@ -524,12 +553,39 @@ Once `.gtd/` is fully closed, the machine writes `.gtd/REVIEW.md` and rests at
|
|
|
524
553
|
plus routing `gtd: done`.
|
|
525
554
|
- Flipping only `- [ ]` → `- [x]` checkboxes in `.gtd/REVIEW.md` — checkbox-only
|
|
526
555
|
edits are also treated as clean approval.
|
|
556
|
+
- Deleting `.gtd/REVIEW.md` outright.
|
|
527
557
|
|
|
528
558
|
Any **substantive** edit — to `.gtd/REVIEW.md` prose, or to the reviewed code
|
|
529
559
|
itself — is feedback: `gtd(human): review` plus routing `gtd: review feedback`,
|
|
530
560
|
`.gtd/REVIEW.md` removed, and `gtd next` re-emits a grilling prompt to the agent
|
|
531
561
|
that inlines the human's finding.
|
|
532
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
|
+
|
|
533
589
|
### Squash
|
|
534
590
|
|
|
535
591
|
With `squash: true` (the default), `gtd: done` is **not** a rest — the same
|
package/bin/gtd-loop
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Drives the two-beat protocol (README.md "The reference loop driver") to a
|
|
5
|
+
# human gate. Both mutators are attempted every iteration, refusal-tolerant:
|
|
6
|
+
# whichever actor is actually awaited claims the turn (captures the human's
|
|
7
|
+
# uncommitted edit after a halt, or the agent's own edit from the previous
|
|
8
|
+
# iteration), the other refuses harmlessly.
|
|
9
|
+
prev_state=""
|
|
10
|
+
prev_prompt=""
|
|
11
|
+
|
|
12
|
+
while true; do
|
|
13
|
+
gtd step --json >/dev/null 2>&1 || true
|
|
14
|
+
gtd step-agent --json >/dev/null 2>&1 || true
|
|
15
|
+
|
|
16
|
+
if ! next_json="$(gtd next --json 2>&1)"; then
|
|
17
|
+
echo "gtd-loop: could not determine the next step:" >&2
|
|
18
|
+
echo "$next_json" >&2
|
|
19
|
+
echo "Run \`gtd status\` to inspect the repo." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
actor="$(jq -r .actor <<<"$next_json")"
|
|
24
|
+
pending="$(jq -r .pending <<<"$next_json")"
|
|
25
|
+
state="$(jq -r .state <<<"$next_json")"
|
|
26
|
+
prompt="$(jq -r .prompt <<<"$next_json")"
|
|
27
|
+
|
|
28
|
+
if [[ "$actor" != "agent" ]]; then
|
|
29
|
+
echo "--- Your turn ($state) ---"
|
|
30
|
+
gtd next
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if [[ "$pending" == "true" ]]; then
|
|
35
|
+
prev_state=""
|
|
36
|
+
prev_prompt=""
|
|
37
|
+
continue
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Stall detection (skills/loop/SKILL.md): the same agent-owned state and
|
|
41
|
+
# prompt repeating means the last dispatch made no progress — stop rather
|
|
42
|
+
# than spin on it.
|
|
43
|
+
if [[ "$state" == "$prev_state" && "$prompt" == "$prev_prompt" ]]; then
|
|
44
|
+
echo "gtd-loop: no progress at '$state' — the agent's last turn changed nothing. Stopping to avoid spinning." >&2
|
|
45
|
+
gtd next >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
prev_state="$state"
|
|
49
|
+
prev_prompt="$prompt"
|
|
50
|
+
|
|
51
|
+
# Swappable agent adapter: GTD_LOOP_AGENT_CMD lets any coding agent CLI
|
|
52
|
+
# stand in for the default, receiving the prompt via $GTD_LOOP_PROMPT.
|
|
53
|
+
cmd="${GTD_LOOP_AGENT_CMD:-}"
|
|
54
|
+
if [[ -z "$cmd" ]]; then
|
|
55
|
+
cmd='claude -p "$GTD_LOOP_PROMPT" --dangerously-skip-permissions'
|
|
56
|
+
fi
|
|
57
|
+
GTD_LOOP_PROMPT="$prompt" bash -c "$cmd"
|
|
58
|
+
done
|
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(
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
7
|
-
"gtd": "dist/gtd.bundle.mjs"
|
|
7
|
+
"gtd": "dist/gtd.bundle.mjs",
|
|
8
|
+
"gtd-loop": "bin/gtd-loop"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist/",
|
|
12
|
+
"bin/",
|
|
11
13
|
"README.md",
|
|
12
14
|
"LICENSE",
|
|
13
15
|
"schema.json"
|