@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 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
- Four commands drive it:
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
 
@@ -191805,7 +191805,7 @@ const renderDiff = (files) => {
191805
191805
  };
191806
191806
  //#endregion
191807
191807
  //#region src/Git.ts
191808
- const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
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$2)));
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$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
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$1,
192678
- diffBase: EMPTY_TREE$1,
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$1;
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 `refs/gtd/review-head` existing: `git reset --mixed
192912
- * refs/gtd/review-head` restores HEAD/index exactly, leaving only the
192913
- * reviewer's own edits dirty (captured by the resting state's own `on`
192914
- * patterns like any other pending change). The pure machine therefore never
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
- * `refs/gtd/review-head` (the base to `refs/gtd/review-base`), then `git
192919
- * reset --mixed <base>`. It re-arms after read-only commands (`gtd next` /
192920
- * `gtd status`) and refused invocations too, so the editor's diff view stays
192921
- * consistent no matter which command the loop last ran.
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
- const REVIEW_HEAD_REF = "refs/gtd/review-head";
192928
- const REVIEW_BASE_REF = "refs/gtd/review-base";
192929
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
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 `refs/gtd/review-head` existence, NOT on any config or machine
192951
- * state, so it always runs first and the machine never sees the window.
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 savedHead = yield* git.readRefOption(REVIEW_HEAD_REF);
192962
- if (isNone(savedHead)) return { closed: false };
192963
- const base = yield* git.readRefOption(REVIEW_BASE_REF);
192964
- if (isSome(base) && base.value !== EMPTY_TREE) {
192965
- 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 `git reset --mixed refs/gtd/review-head && git update-ref -d refs/gtd/review-head && git update-ref -d refs/gtd/review-base`"));
192966
- }
192967
- yield* git.mixedResetTo(REVIEW_HEAD_REF);
192968
- yield* git.deleteRef(REVIEW_HEAD_REF);
192969
- yield* git.deleteRef(REVIEW_BASE_REF);
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 it before starting a review`));
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 it before starting a fix`));
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.
@@ -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 under
453
- `refs/gtd/review-base`) for the window's lifetime. See
454
- [STATES.md §11](../STATES.md) for the full lifecycle.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
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": {