@pmelab/gtd 7.1.0 → 7.1.1
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 +5 -2
- package/dist/gtd.bundle.mjs +141 -32
- package/docs/cli.md +73 -2
- package/docs/configuration.md +4 -3
- package/docs/loop.md +13 -0
- package/docs/upgrading.md +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,8 +76,8 @@ over — a `workflow:` is the whole definition). See
|
|
|
76
76
|
|
|
77
77
|
gtd is a small **pattern machine**: named states, each awaiting one actor and
|
|
78
78
|
carrying one piece of content (a script, a prompt, a message, or a squash commit
|
|
79
|
-
template), with an ordered set of change-patterns routing to the next state.
|
|
80
|
-
|
|
79
|
+
template), with an ordered set of change-patterns routing to the next state. A
|
|
80
|
+
handful of commands drive it:
|
|
81
81
|
|
|
82
82
|
- **`gtd step <actor>`** — authenticate as `<actor>` and perform the one
|
|
83
83
|
transition the pending changes match.
|
|
@@ -91,6 +91,9 @@ Four commands drive it:
|
|
|
91
91
|
- **`gtd fix`** — start a brand new process that goes straight into repairing
|
|
92
92
|
the current failing tests, then runs the shared review/squash tail (a no-op if
|
|
93
93
|
the suite is already green).
|
|
94
|
+
- **`gtd abandon`** — end the process underway without completing it: rewind to
|
|
95
|
+
the commit it started from, keeping everything it produced as uncommitted
|
|
96
|
+
changes (nothing is discarded).
|
|
94
97
|
- **`gtd visualize`** — serve an interactive diagram of the active workflow on a
|
|
95
98
|
local web server (the main flow, its sub-machines, and per-state details).
|
|
96
99
|
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -191805,7 +191805,7 @@ const renderDiff = (files) => {
|
|
|
191805
191805
|
};
|
|
191806
191806
|
//#endregion
|
|
191807
191807
|
//#region src/Git.ts
|
|
191808
|
-
const EMPTY_TREE$
|
|
191808
|
+
const EMPTY_TREE$3 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
191809
191809
|
/**
|
|
191810
191810
|
* Parse `git diff --name-status` output into `{ path, status }` pairs.
|
|
191811
191811
|
* Status codes: A (added), D (deleted), M (modified), R (renamed), C (copied), etc.
|
|
@@ -191936,7 +191936,7 @@ const makeGitImpl = (executor, root) => {
|
|
|
191936
191936
|
readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
|
|
191937
191937
|
commitDiff: (hash, exclude = []) => gen(function* () {
|
|
191938
191938
|
yield* exec("git", "rev-parse", "--verify", hash);
|
|
191939
|
-
const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$
|
|
191939
|
+
const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$3)));
|
|
191940
191940
|
const nameStatusOut = yield* exec("git", "diff", "--name-status", parent, hash).pipe(catchAll(() => succeed$3("")));
|
|
191941
191941
|
const allPaths = parseNameStatus(nameStatusOut);
|
|
191942
191942
|
const filtered = applyExcludes(allPaths, exclude);
|
|
@@ -192578,7 +192578,7 @@ const renderModeCommand = (template, context) => eta.renderString(template, cont
|
|
|
192578
192578
|
* one commit (or one squash). A caller that wants several transitions issues
|
|
192579
192579
|
* several invocations.
|
|
192580
192580
|
*/
|
|
192581
|
-
const EMPTY_TREE$
|
|
192581
|
+
const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
192582
192582
|
const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
|
|
192583
192583
|
const COST_TRAILER_PREFIX = "Gtd-Cost: ";
|
|
192584
192584
|
const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
|
|
@@ -192674,8 +192674,8 @@ const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entri
|
|
|
192674
192674
|
const computeProcessRun = (git, def) => gen(function* () {
|
|
192675
192675
|
if (!(yield* git.hasCommits())) return {
|
|
192676
192676
|
startHash: "",
|
|
192677
|
-
startParentHash: EMPTY_TREE$
|
|
192678
|
-
diffBase: EMPTY_TREE$
|
|
192677
|
+
startParentHash: EMPTY_TREE$2,
|
|
192678
|
+
diffBase: EMPTY_TREE$2,
|
|
192679
192679
|
trace: [],
|
|
192680
192680
|
costEntries: []
|
|
192681
192681
|
};
|
|
@@ -192691,7 +192691,7 @@ const computeProcessRun = (git, def) => gen(function* () {
|
|
|
192691
192691
|
const processCommits = history.slice(startIdx);
|
|
192692
192692
|
const trace = processCommits.map((h) => parseStateSubject(subjectOf$1(h.message)).state);
|
|
192693
192693
|
const costEntries = parseCostTrailers(processCommits.map((h) => h.message));
|
|
192694
|
-
const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$
|
|
192694
|
+
const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$2;
|
|
192695
192695
|
return {
|
|
192696
192696
|
startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
|
|
192697
192697
|
startParentHash,
|
|
@@ -192908,25 +192908,45 @@ const executeDecision = (git, run, decision, context, cost, model) => gen(functi
|
|
|
192908
192908
|
* every state subcommand:
|
|
192909
192909
|
*
|
|
192910
192910
|
* - `closeReviewWindow` runs BEFORE anything reads or mutates state, keyed
|
|
192911
|
-
* solely on `
|
|
192912
|
-
*
|
|
192913
|
-
*
|
|
192914
|
-
*
|
|
192915
|
-
* sees the window.
|
|
192911
|
+
* solely on `REVIEW_HEAD_REF` existing: `git reset --mixed <that ref>`
|
|
192912
|
+
* restores HEAD/index exactly, leaving only the reviewer's own edits dirty
|
|
192913
|
+
* (captured by the resting state's own `on` patterns like any other pending
|
|
192914
|
+
* change). The pure machine therefore never sees the window.
|
|
192916
192915
|
* - `openReviewWindow` runs AFTER the subcommand finishes and self-guards on
|
|
192917
192916
|
* the resolved rest declaring `reviewWindow: true`: it saves HEAD to
|
|
192918
|
-
* `
|
|
192919
|
-
*
|
|
192920
|
-
*
|
|
192921
|
-
*
|
|
192917
|
+
* `REVIEW_HEAD_REF` (the base to `REVIEW_BASE_REF`), then `git reset --mixed
|
|
192918
|
+
* <base>`. It re-arms after read-only commands (`gtd next` / `gtd status`)
|
|
192919
|
+
* and refused invocations too, so the editor's diff view stays consistent no
|
|
192920
|
+
* matter which command the loop last ran.
|
|
192922
192921
|
*
|
|
192923
192922
|
* Every open/close step is idempotent under re-entry, so a crash at any point
|
|
192924
192923
|
* is recovered by the next invocation's close (the saved ref also keeps the
|
|
192925
192924
|
* real head GC-reachable for the window's whole lifetime).
|
|
192926
192925
|
*/
|
|
192927
|
-
|
|
192928
|
-
|
|
192929
|
-
|
|
192926
|
+
/**
|
|
192927
|
+
* The window's two state refs live in git's PER-WORKTREE `refs/worktree/*`
|
|
192928
|
+
* namespace, so linked worktrees sharing one `.git` (`git worktree add`) each
|
|
192929
|
+
* get their OWN window: a review resting in one worktree is invisible to — and
|
|
192930
|
+
* un-clobberable by — every other one, and git drops the refs with the
|
|
192931
|
+
* worktree.
|
|
192932
|
+
*
|
|
192933
|
+
* gtd ≤ 7.1 kept them in the SHARED `refs/gtd/*` namespace (the legacy names
|
|
192934
|
+
* below), which made a second worktree's very first gtd invocation close the
|
|
192935
|
+
* FIRST worktree's window: its `git reset --mixed` moved the second worktree's
|
|
192936
|
+
* branch onto the other worktree's saved head, and every later invocation
|
|
192937
|
+
* refused with "a process is already underway" (issue #118).
|
|
192938
|
+
*/
|
|
192939
|
+
const REVIEW_HEAD_REF = "refs/worktree/gtd/review-head";
|
|
192940
|
+
const REVIEW_BASE_REF = "refs/worktree/gtd/review-base";
|
|
192941
|
+
/**
|
|
192942
|
+
* The pre-7.2 SHARED refs. gtd never writes them any more; `closeReviewWindow`
|
|
192943
|
+
* only reads them to finish a window an older gtd left open across the upgrade
|
|
192944
|
+
* — and only when HEAD is still contained in the saved head, since a shared ref
|
|
192945
|
+
* may belong to a sibling worktree (see `openWindowRefs`).
|
|
192946
|
+
*/
|
|
192947
|
+
const LEGACY_REVIEW_HEAD_REF = "refs/gtd/review-head";
|
|
192948
|
+
const LEGACY_REVIEW_BASE_REF = "refs/gtd/review-base";
|
|
192949
|
+
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
192930
192950
|
const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
|
|
192931
192951
|
/**
|
|
192932
192952
|
* The diff base while the window is open: the hash of the most-recent
|
|
@@ -192945,28 +192965,63 @@ const reviewBaseHash = (git, def, run) => gen(function* () {
|
|
|
192945
192965
|
}
|
|
192946
192966
|
return base;
|
|
192947
192967
|
});
|
|
192968
|
+
/** The manual-recovery incantation every close refusal ends with. */
|
|
192969
|
+
const manualRecovery = (refs) => `\`git reset --mixed ${refs.headRef} && git update-ref -d ${refs.headRef} && git update-ref -d ${refs.baseRef}\``;
|
|
192970
|
+
/**
|
|
192971
|
+
* Which ref pair (if any) records an open window: this worktree's own
|
|
192972
|
+
* `refs/worktree/gtd/*` pair first, falling back to the pre-7.2 shared
|
|
192973
|
+
* `refs/gtd/*` pair so an upgrade mid-window still closes cleanly.
|
|
192974
|
+
*/
|
|
192975
|
+
const openWindowRefs = (git) => gen(function* () {
|
|
192976
|
+
const scoped = yield* git.readRefOption(REVIEW_HEAD_REF);
|
|
192977
|
+
if (isSome(scoped)) return some({
|
|
192978
|
+
headRef: REVIEW_HEAD_REF,
|
|
192979
|
+
baseRef: REVIEW_BASE_REF,
|
|
192980
|
+
headHash: scoped.value,
|
|
192981
|
+
legacy: false
|
|
192982
|
+
});
|
|
192983
|
+
const legacy = yield* git.readRefOption(LEGACY_REVIEW_HEAD_REF);
|
|
192984
|
+
if (isNone(legacy)) return none$4();
|
|
192985
|
+
return some({
|
|
192986
|
+
headRef: LEGACY_REVIEW_HEAD_REF,
|
|
192987
|
+
baseRef: LEGACY_REVIEW_BASE_REF,
|
|
192988
|
+
headHash: legacy.value,
|
|
192989
|
+
legacy: true
|
|
192990
|
+
});
|
|
192991
|
+
});
|
|
192948
192992
|
/**
|
|
192949
192993
|
* Restore the real head if a review checkout window is open; no-op otherwise.
|
|
192950
|
-
* Keyed on
|
|
192951
|
-
* state, so it always runs first and the machine never sees
|
|
192994
|
+
* Keyed on the saved-head ref's existence (see `openWindowRefs`), NOT on any
|
|
192995
|
+
* config or machine state, so it always runs first and the machine never sees
|
|
192996
|
+
* the window.
|
|
192952
192997
|
*
|
|
192953
192998
|
* Fails loudly — refs left in place — when HEAD is no longer on the reviewed
|
|
192954
192999
|
* branch (the base is not an ancestor of HEAD, e.g. after a branch switch): a
|
|
192955
193000
|
* mixed reset there would rewrite the wrong branch's tip. Manual commits on
|
|
192956
193001
|
* top of the base pass the guard: the reset keeps their content in the working
|
|
192957
193002
|
* tree, where the next turn's capture picks it up as review feedback.
|
|
193003
|
+
*
|
|
193004
|
+
* A window found under the LEGACY shared refs gets one extra guard: a shared
|
|
193005
|
+
* ref may have been written by a sibling worktree, so the close only proceeds
|
|
193006
|
+
* when HEAD is contained in the saved head — the shape a window this worktree
|
|
193007
|
+
* opened itself has. Anything else refuses with the manual recovery rather
|
|
193008
|
+
* than resetting this branch onto another worktree's work (issue #118).
|
|
192958
193009
|
*/
|
|
192959
193010
|
const closeReviewWindow = gen(function* () {
|
|
192960
193011
|
const git = yield* GitService;
|
|
192961
|
-
const
|
|
192962
|
-
if (isNone(
|
|
192963
|
-
const
|
|
192964
|
-
|
|
192965
|
-
|
|
192966
|
-
|
|
192967
|
-
|
|
192968
|
-
|
|
192969
|
-
|
|
193012
|
+
const open = yield* openWindowRefs(git);
|
|
193013
|
+
if (isNone(open)) return { closed: false };
|
|
193014
|
+
const refs = open.value;
|
|
193015
|
+
const base = yield* git.readRefOption(refs.baseRef);
|
|
193016
|
+
if (isSome(base) && base.value !== EMPTY_TREE$1) {
|
|
193017
|
+
if (!(yield* git.isAncestor(base.value, "HEAD"))) return yield* fail$2(/* @__PURE__ */ new Error(`a gtd review checkout window is open but HEAD has moved off the reviewed branch — return to it, or restore manually with ${manualRecovery(refs)}`));
|
|
193018
|
+
}
|
|
193019
|
+
if (refs.legacy) {
|
|
193020
|
+
if (!(yield* git.isAncestor("HEAD", refs.headHash))) return yield* fail$2(/* @__PURE__ */ new Error(`a review checkout window is recorded under the shared ${LEGACY_REVIEW_HEAD_REF} (written by gtd 7.1 or older) but it does not belong to this worktree — HEAD is not contained in it. gtd now scopes the window per worktree (${REVIEW_HEAD_REF}); in the worktree that owns it, run ${manualRecovery(refs)}`));
|
|
193021
|
+
}
|
|
193022
|
+
yield* git.mixedResetTo(refs.headRef);
|
|
193023
|
+
yield* git.deleteRef(refs.headRef);
|
|
193024
|
+
yield* git.deleteRef(refs.baseRef);
|
|
192970
193025
|
return { closed: true };
|
|
192971
193026
|
});
|
|
192972
193027
|
/**
|
|
@@ -192996,7 +193051,7 @@ const openReviewWindow = gen(function* () {
|
|
|
192996
193051
|
const run = yield* computeProcessRun(git, def);
|
|
192997
193052
|
const base = (yield* reviewBaseHash(git, def, run)) ?? run.diffBase;
|
|
192998
193053
|
const headHash = yield* git.resolveRef("HEAD");
|
|
192999
|
-
if (base === EMPTY_TREE || base === headHash) return { opened: false };
|
|
193054
|
+
if (base === EMPTY_TREE$1 || base === headHash) return { opened: false };
|
|
193000
193055
|
yield* git.updateRef(REVIEW_BASE_REF, base);
|
|
193001
193056
|
yield* git.updateRef(REVIEW_HEAD_REF, headHash);
|
|
193002
193057
|
yield* git.mixedResetTo(base);
|
|
@@ -205112,6 +205167,11 @@ Commands:
|
|
|
205112
205167
|
state (fixEntry: true) that goes straight into repairing the
|
|
205113
205168
|
current failing tests. Requires a clean tree resting at the
|
|
205114
205169
|
workflow's initial state
|
|
205170
|
+
abandon End the process currently underway without completing it:
|
|
205171
|
+
close any open review checkout window, then rewind HEAD to
|
|
205172
|
+
the commit the process started from, keeping everything it
|
|
205173
|
+
produced as uncommitted changes. A no-op when no process is
|
|
205174
|
+
underway
|
|
205115
205175
|
next Print the resolved rest's rendered script/prompt/message
|
|
205116
205176
|
(no mutation)
|
|
205117
205177
|
status Print the resolved rest's state/actor and which declared
|
|
@@ -205375,7 +205435,7 @@ const runReviewCommand = (argv, json, write) => gen(function* () {
|
|
|
205375
205435
|
const commitish = args[0];
|
|
205376
205436
|
const git = yield* GitService;
|
|
205377
205437
|
const rest = yield* resolveRest();
|
|
205378
|
-
if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd review: a process is already underway (resting at "${rest.state}") — finish or abandon
|
|
205438
|
+
if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd review: a process is already underway (resting at "${rest.state}") — finish it, or run \`gtd abandon\`, before starting a review`));
|
|
205379
205439
|
if ((yield* pendingChanges(git)).length > 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd review: the working tree must be clean before starting a review"));
|
|
205380
205440
|
const entryState = reviewEntryStateOf(rest.def);
|
|
205381
205441
|
if (entryState === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd review: the active workflow declares no review entry state"));
|
|
@@ -205412,7 +205472,7 @@ const runFixCommand = (argv, json, write) => gen(function* () {
|
|
|
205412
205472
|
yield* rejectExtraArgs("fix", argv);
|
|
205413
205473
|
const git = yield* GitService;
|
|
205414
205474
|
const rest = yield* resolveRest();
|
|
205415
|
-
if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd fix: a process is already underway (resting at "${rest.state}") — finish or abandon
|
|
205475
|
+
if (rest.state !== initialStateOf(rest.def)) return yield* fail$2(/* @__PURE__ */ new Error(`gtd fix: a process is already underway (resting at "${rest.state}") — finish it, or run \`gtd abandon\`, before starting a fix`));
|
|
205416
205476
|
if ((yield* pendingChanges(git)).length > 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd fix: the working tree must be clean before starting a fix"));
|
|
205417
205477
|
const entryState = fixEntryStateOf(rest.def);
|
|
205418
205478
|
if (entryState === void 0) return yield* fail$2(/* @__PURE__ */ new Error("gtd fix: the active workflow declares no fix entry state"));
|
|
@@ -205424,6 +205484,53 @@ const runFixCommand = (argv, json, write) => gen(function* () {
|
|
|
205424
205484
|
}) + "\n");
|
|
205425
205485
|
else write(`committed: ${subject}\n`);
|
|
205426
205486
|
});
|
|
205487
|
+
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
205488
|
+
/**
|
|
205489
|
+
* `gtd abandon`: end the process currently underway WITHOUT completing it,
|
|
205490
|
+
* returning the machine to the workflow's initial state — the recovery path out
|
|
205491
|
+
* of a process nobody is going to finish (the refusals of `gtd review`/`gtd fix`
|
|
205492
|
+
* name it: "finish or abandon it before starting a review").
|
|
205493
|
+
*
|
|
205494
|
+
* NOTHING is discarded. The shared bracket in `makeProgram` has already closed
|
|
205495
|
+
* any open review checkout window (so HEAD is the real head), and abandon then
|
|
205496
|
+
* `git reset --mixed`es HEAD to the commit the process started from
|
|
205497
|
+
* (`computeProcessRun`'s `startParentHash` — the same boundary a squash resets
|
|
205498
|
+
* to). Every turn commit the process wrote is dropped, and everything they
|
|
205499
|
+
* carried — the code, the `.gtd/` steering files — stays in the working tree as
|
|
205500
|
+
* uncommitted changes for the human to keep, re-commit, or discard with
|
|
205501
|
+
* ordinary git.
|
|
205502
|
+
*
|
|
205503
|
+
* Idempotent: resting at the initial state (a plain non-gtd branch, or a
|
|
205504
|
+
* just-squashed cycle) is a no-op SUCCESS, not a refusal — a recovery command
|
|
205505
|
+
* that fails when there is nothing to recover is a worse tool. The one refusal
|
|
205506
|
+
* is a process whose first commit is the repository's own root commit: there is
|
|
205507
|
+
* no earlier commit to rewind to.
|
|
205508
|
+
*/
|
|
205509
|
+
const runAbandonCommand = (argv, json, write) => gen(function* () {
|
|
205510
|
+
yield* rejectExtraArgs("abandon", argv);
|
|
205511
|
+
const git = yield* GitService;
|
|
205512
|
+
const rest = yield* resolveRest();
|
|
205513
|
+
const initial = initialStateOf(rest.def);
|
|
205514
|
+
if (rest.state === initial) {
|
|
205515
|
+
if (json) write(JSON.stringify({
|
|
205516
|
+
state: initial,
|
|
205517
|
+
abandoned: false
|
|
205518
|
+
}) + "\n");
|
|
205519
|
+
else write(`no gtd process is underway (resting at "${initial}") — nothing to abandon\n`);
|
|
205520
|
+
return;
|
|
205521
|
+
}
|
|
205522
|
+
const run = yield* computeProcessRun(git, rest.def);
|
|
205523
|
+
if (run.startParentHash === EMPTY_TREE) return yield* fail$2(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${rest.state}") starts at the repository's first commit — there is no earlier commit to rewind to`));
|
|
205524
|
+
yield* git.mixedResetTo(run.startParentHash);
|
|
205525
|
+
const subject = yield* git.lastCommitSubject();
|
|
205526
|
+
if (json) write(JSON.stringify({
|
|
205527
|
+
state: initial,
|
|
205528
|
+
abandoned: true,
|
|
205529
|
+
from: rest.state,
|
|
205530
|
+
head: run.startParentHash
|
|
205531
|
+
}) + "\n");
|
|
205532
|
+
else write(`abandoned the process resting at "${rest.state}" — HEAD is back at ${run.startParentHash.slice(0, 7)} ("${subject}"), resting at "${initial}".\nEverything the process produced is kept as uncommitted changes (\`git status\`); discard them with \`git checkout -- . && git clean -fd .gtd\` for a clean tree.\n`);
|
|
205533
|
+
});
|
|
205427
205534
|
/**
|
|
205428
205535
|
* The self-validation instruction gtd APPENDS to a `prompt` rest that declares
|
|
205429
205536
|
* both `file:` and `mode:` — i.e. a state whose actor hands over a steering
|
|
@@ -205852,6 +205959,7 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
205852
205959
|
"step",
|
|
205853
205960
|
"review",
|
|
205854
205961
|
"fix",
|
|
205962
|
+
"abandon",
|
|
205855
205963
|
"next",
|
|
205856
205964
|
"status",
|
|
205857
205965
|
"validate"
|
|
@@ -205923,6 +206031,7 @@ const dispatchKnownSubcommand = (sub, argv, json, write, cost, model) => {
|
|
|
205923
206031
|
case "step": return runStepCommand(argv, json, write, cost, model);
|
|
205924
206032
|
case "review": return runReviewCommand(argv, json, write);
|
|
205925
206033
|
case "fix": return runFixCommand(argv, json, write);
|
|
206034
|
+
case "abandon": return runAbandonCommand(argv, json, write);
|
|
205926
206035
|
case "next": return runNextCommand(json, write);
|
|
205927
206036
|
case "status": return runStatusCommand(argv, json, write);
|
|
205928
206037
|
case "validate": return runValidateCommand(argv, json, write);
|
package/docs/cli.md
CHANGED
|
@@ -25,6 +25,11 @@ Commands:
|
|
|
25
25
|
state (fixEntry: true) that goes straight into repairing the
|
|
26
26
|
current failing tests. Requires a clean tree resting at the
|
|
27
27
|
workflow's initial state
|
|
28
|
+
abandon End the process currently underway without completing it:
|
|
29
|
+
close any open review checkout window, then rewind HEAD to
|
|
30
|
+
the commit the process started from, keeping everything it
|
|
31
|
+
produced as uncommitted changes. A no-op when no process is
|
|
32
|
+
underway
|
|
28
33
|
next Print the resolved rest's rendered script/prompt/message
|
|
29
34
|
(no mutation)
|
|
30
35
|
status Print the resolved rest's state/actor and which declared
|
|
@@ -167,7 +172,8 @@ written):
|
|
|
167
172
|
|
|
168
173
|
- the machine currently resting at the workflow's **initial state** (a plain
|
|
169
174
|
non-gtd branch resolves there via the inert-subject rule — the normal case; a
|
|
170
|
-
process already underway refuses
|
|
175
|
+
process already underway refuses — finish it, or
|
|
176
|
+
[`gtd abandon`](#gtd-abandon---json) it);
|
|
171
177
|
- a **clean working tree**;
|
|
172
178
|
- the active workflow declaring a **`reviewEntry: true`** state (otherwise:
|
|
173
179
|
`gtd review: the active workflow declares no review entry state`);
|
|
@@ -213,7 +219,8 @@ Requires, in order (any failure is a plain refusal — exit non-zero, nothing
|
|
|
213
219
|
written):
|
|
214
220
|
|
|
215
221
|
- the machine currently resting at the workflow's **initial state** (a process
|
|
216
|
-
already underway refuses)
|
|
222
|
+
already underway refuses — finish it, or [`gtd abandon`](#gtd-abandon---json)
|
|
223
|
+
it);
|
|
217
224
|
- a **clean working tree**;
|
|
218
225
|
- the active workflow declaring a **`fixEntry: true`** state (otherwise:
|
|
219
226
|
`gtd fix: the active workflow declares no fix entry state`).
|
|
@@ -232,6 +239,62 @@ unified template the fix-entry state is `fix-check`: a red suite drops into the
|
|
|
232
239
|
shared `fixing` loop and out through the review + squash tail; a green suite is
|
|
233
240
|
a no-op back to `idle`.
|
|
234
241
|
|
|
242
|
+
## `gtd abandon [--json]`
|
|
243
|
+
|
|
244
|
+
Ends the process currently underway **without completing it** — the way out of a
|
|
245
|
+
process nobody is going to finish, and the command `gtd review`/`gtd fix` name
|
|
246
|
+
when they refuse ("finish it, or run `gtd abandon`, before starting a review").
|
|
247
|
+
A workflow's own exit is its squash finale
|
|
248
|
+
([STATES.md §8](../STATES.md#8-the-squash-lifecycle)); this is the human's, and
|
|
249
|
+
it targets the same boundary:
|
|
250
|
+
|
|
251
|
+
1. any open **review checkout window** is closed first (the bracket every state
|
|
252
|
+
subcommand runs — see
|
|
253
|
+
[STATES.md §11](../STATES.md#11-the-review-checkout-window)), so the rewind
|
|
254
|
+
starts from the real head, not the rewound base;
|
|
255
|
+
2. HEAD is `git reset --mixed`ed to the commit the process started from (its
|
|
256
|
+
start parent — the same target a squash resets to).
|
|
257
|
+
|
|
258
|
+
**Nothing is discarded.** Every turn commit the process wrote is dropped, and
|
|
259
|
+
everything they carried — the code, the `.gtd/` steering files — stays in the
|
|
260
|
+
working tree as uncommitted changes for you to keep, re-commit, or throw away
|
|
261
|
+
with ordinary git:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
abandoned the process resting at "await-review" — HEAD is back at 1a2b3c4
|
|
265
|
+
("feat: add calculator"), resting at "idle".
|
|
266
|
+
Everything the process produced is kept as uncommitted changes (`git status`);
|
|
267
|
+
discard them with `git checkout -- . && git clean -fd .gtd` for a clean tree.
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Takes no positional argument (extra arguments are a usage error). Resting at the
|
|
271
|
+
workflow's initial state is a **no-op success**, not a refusal — a recovery
|
|
272
|
+
command that fails when there is nothing to recover is a worse tool:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
no gtd process is underway (resting at "idle") — nothing to abandon
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The one refusal is a process whose first commit is the repository's own root
|
|
279
|
+
commit: there is no earlier commit to rewind to, so its commits have to be
|
|
280
|
+
removed by hand.
|
|
281
|
+
|
|
282
|
+
`--json` emits `{state, abandoned, from, head}` — `state` is the initial state
|
|
283
|
+
the machine returns to, `abandoned` whether anything was rewound, and (only when
|
|
284
|
+
it was) `from` the state the abandoned process rested at plus `head` the full
|
|
285
|
+
hash HEAD now points at:
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"state": "idle",
|
|
290
|
+
"abandoned": true,
|
|
291
|
+
"from": "await-review",
|
|
292
|
+
"head": "1a2b3c4…"
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The no-op reports `{"state": "idle", "abandoned": false}` and still exits 0.
|
|
297
|
+
|
|
235
298
|
## `gtd next [--json]`
|
|
236
299
|
|
|
237
300
|
Pure emitter of the resolved rest's rendered content — it **never mutates** the
|
|
@@ -495,3 +558,11 @@ the plain-text one.
|
|
|
495
558
|
`--help`/`--version` (and the `help`/`version` subcommands), `lsp`, and
|
|
496
559
|
`visualize` skip this guard entirely (`visualize` still reads the `.gtdrc`
|
|
497
560
|
workflow, but needs no git state).
|
|
561
|
+
- **Linked worktrees are independent.** N `git worktree` worktrees of one
|
|
562
|
+
repository (sharing a single `.git`) each run their own gtd process: state is
|
|
563
|
+
derived from that worktree's own HEAD, and the review checkout window's refs
|
|
564
|
+
live in git's per-worktree `refs/worktree/gtd/*` namespace, so a review
|
|
565
|
+
resting in one worktree neither blocks nor rewrites any other
|
|
566
|
+
([STATES.md §11](../STATES.md#11-the-review-checkout-window)). This holds from
|
|
567
|
+
7.2 on; gtd ≤ 7.1 kept the window under the shared `refs/gtd/*` refs, where
|
|
568
|
+
the first worktree to open one clobbered every sibling's branch.
|
package/docs/configuration.md
CHANGED
|
@@ -449,9 +449,10 @@ The unified template enables `reviewWindow: true` on `await-review` and marks
|
|
|
449
449
|
`review-deciding` `reviewBase: true`, so the first review covers the whole cycle
|
|
450
450
|
but each subsequent feedback round's window narrows to `last-review..HEAD`. The
|
|
451
451
|
pure engine never observes an open window — it is opened/closed entirely at the
|
|
452
|
-
edge; the real head is preserved under `refs/gtd/review-head` (the base
|
|
453
|
-
`refs/gtd/review-base`) for the window's lifetime
|
|
454
|
-
|
|
452
|
+
edge; the real head is preserved under `refs/worktree/gtd/review-head` (the base
|
|
453
|
+
under `refs/worktree/gtd/review-base`) for the window's lifetime — git's
|
|
454
|
+
per-worktree ref namespace, so linked worktrees of one repository each get their
|
|
455
|
+
own window. See [STATES.md §11](../STATES.md) for the full lifecycle.
|
|
455
456
|
|
|
456
457
|
### `reviewEntry:` — the review entry point (`gtd review <commitish>`)
|
|
457
458
|
|
package/docs/loop.md
CHANGED
|
@@ -34,6 +34,19 @@ capture for the human as its opening move (see below), so re-launching the loop
|
|
|
34
34
|
is the only thing the human ever does — at a gate they edit and re-run, and the
|
|
35
35
|
loop picks their change up.
|
|
36
36
|
|
|
37
|
+
**A `"message"` rest is a real handoff, not a slow beat.** No driver signs off
|
|
38
|
+
for the human: in the bundled workflow the loop runs the whole cycle unattended
|
|
39
|
+
up to `await-review` and then STOPS there with the review checkout window open
|
|
40
|
+
(the reviewable diff surfaced as uncommitted changes — see
|
|
41
|
+
[STATES.md §11](../STATES.md#11-the-review-checkout-window)). Advancing takes a
|
|
42
|
+
human edit to `.gtd/REVIEW.md`: ticking every box with **no** comment signs off
|
|
43
|
+
into the squash finale, while any comment (a note, or a code edit of your own)
|
|
44
|
+
is feedback that routes into another build + re-review round. Re-launching the
|
|
45
|
+
loop after that edit picks the decision up. Until then the loop halts on every
|
|
46
|
+
run — that is the contract, not a stall. A review nobody is going to finish is
|
|
47
|
+
ended with [`gtd abandon`](cli.md#gtd-abandon---json), which drops the process's
|
|
48
|
+
commits and keeps their content as uncommitted changes.
|
|
49
|
+
|
|
37
50
|
```bash
|
|
38
51
|
gtd next --json # ask who's up and what they should do
|
|
39
52
|
```
|
package/docs/upgrading.md
CHANGED
|
@@ -159,6 +159,42 @@ loudly at load time rather than silently misinterpreting.
|
|
|
159
159
|
or agent harness, upgrading the `gtd` binary also means re-copying that skill
|
|
160
160
|
from this release.
|
|
161
161
|
|
|
162
|
+
## 7.2: the review checkout window is now per worktree
|
|
163
|
+
|
|
164
|
+
The review checkout window's two refs moved from the SHARED `refs/gtd/*`
|
|
165
|
+
namespace to git's PER-WORKTREE `refs/worktree/*` one:
|
|
166
|
+
|
|
167
|
+
| before (≤ 7.1) | from 7.2 |
|
|
168
|
+
| ---------------------- | ------------------------------- |
|
|
169
|
+
| `refs/gtd/review-head` | `refs/worktree/gtd/review-head` |
|
|
170
|
+
| `refs/gtd/review-base` | `refs/worktree/gtd/review-base` |
|
|
171
|
+
|
|
172
|
+
Nothing in a `.gtdrc` or a workflow definition references those names, so **no
|
|
173
|
+
config change is needed**. What changes is behaviour in a repository checked out
|
|
174
|
+
as several linked worktrees (`git worktree add`, one `.git` shared between
|
|
175
|
+
them): each worktree now has its own window, so a review resting in one no
|
|
176
|
+
longer "closes" from another — which, on the shared refs, mixed-reset that other
|
|
177
|
+
worktree's branch onto the reviewing worktree's saved head and left every
|
|
178
|
+
sibling refusing with "a process is already underway".
|
|
179
|
+
|
|
180
|
+
A window an OLDER gtd left open across the upgrade still closes from the legacy
|
|
181
|
+
refs, so an in-flight review in a single-worktree repo needs no attention. In a
|
|
182
|
+
multi-worktree repo gtd cannot tell which worktree a shared ref belongs to, so
|
|
183
|
+
it closes only when HEAD is contained in the saved head and otherwise refuses
|
|
184
|
+
with the exact recovery commands — run them in the worktree that owns the
|
|
185
|
+
window:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
git reset --mixed refs/gtd/review-head
|
|
189
|
+
git update-ref -d refs/gtd/review-head
|
|
190
|
+
git update-ref -d refs/gtd/review-base
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
7.2 also adds [`gtd abandon`](cli.md#gtd-abandon---json), the supported way out
|
|
194
|
+
of a process that will never be finished (it closes the window, then rewinds
|
|
195
|
+
HEAD to the process's start parent, keeping everything the process produced as
|
|
196
|
+
uncommitted changes) — no hand-editing of refs required.
|
|
197
|
+
|
|
162
198
|
## Prior breaking change: the v1 → v2 label grammar (historical)
|
|
163
199
|
|
|
164
200
|
v2 replaced v1's undifferentiated turn labels with six labels carrying the
|