@pmelab/gtd 9.14.1 → 10.1.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 CHANGED
@@ -108,8 +108,8 @@ commit everything with this message: gtd(human): idle → unwind
108
108
  (run `gtd land --json=script | sh` to get the landing script)
109
109
  ```
110
110
 
111
- You could run that commit by hand. Take the script instead — it adds the
112
- precondition checks and retries you would otherwise have to remember:
111
+ You could run that commit by hand. Take the script instead — it spells out the
112
+ exact message and reports what landed:
113
113
 
114
114
  ```bash
115
115
  gtd land --json=script | sh
@@ -256,14 +256,19 @@ coding agent:
256
256
  ```
257
257
 
258
258
  It prints a self-contained briefing that teaches the agent to set the project
259
- up, build two commands for itself in whatever shell or runtime it runs, and ask
259
+ up, build four commands for itself in whatever shell or runtime it runs, and ask
260
260
  you what you want before it starts driving. You get one prompt to paste, not a
261
- state name to choose. The two commands: a **loop command** that drives beats
262
- until the process rests, and an **edit command** that opens the steering file
263
- the process is waiting on right now — falling back to `.gtd/TODO.md` when the
264
- resting state declares none. That fallback is also how you begin: on a clean
265
- repository the edit command opens the empty `.gtd/TODO.md`, and whatever you
266
- write there is the first sketch the whole process gets planned from.
261
+ state name to choose. The four commands: **`gtd-build`** drives beats until the
262
+ process rests; **`gtd-edit`** opens the steering file the process is waiting on
263
+ right now — falling back to `.gtd/TODO.md` when the resting state declares none;
264
+ **`gtd-review <commitish>`** starts a review round over that commitish and
265
+ drives it; **`gtd-fix`** enters the fix process and drives it. The
266
+ `.gtd/TODO.md` fallback is also how you begin: on a clean repository the edit
267
+ command opens the empty `.gtd/TODO.md`, and whatever you write there is the
268
+ first sketch the whole process gets planned from. As a final step, if it finds
269
+ an LSP-capable editor, the briefing also offers to wire up live diagnostics and
270
+ review actions in it — asking first and naming the exact file, and merging
271
+ rather than overwriting your editor's config.
267
272
 
268
273
  ### The workflow it ships with
269
274
 
@@ -6514,14 +6514,6 @@ const Equivalence$3 = (self, that) => matchWith(self, that, {
6514
6514
  * @since 2.0.0
6515
6515
  * @category math
6516
6516
  */
6517
- const times = /*#__PURE__*/ dual(2, (self, times) => match$7(self, {
6518
- onMillis: (millis) => make$42(millis * times),
6519
- onNanos: (nanos) => make$42(nanos * BigInt(times))
6520
- }));
6521
- /**
6522
- * @since 2.0.0
6523
- * @category math
6524
- */
6525
6517
  const sum = /*#__PURE__*/ dual(2, (self, that) => matchWith(self, that, {
6526
6518
  onMillis: (self, that) => make$42(self + that),
6527
6519
  onNanos: (self, that) => make$42(self + that)
@@ -8120,7 +8112,7 @@ const catchAll$1 = /*#__PURE__*/ dual(2, (self, f) => matchEffect$1(self, {
8120
8112
  onFailure: f,
8121
8113
  onSuccess: succeed$8
8122
8114
  }));
8123
- const catchIf$1 = /*#__PURE__*/ dual(3, (self, predicate, f) => catchAllCause$1(self, (cause) => {
8115
+ const catchIf = /*#__PURE__*/ dual(3, (self, predicate, f) => catchAllCause$1(self, (cause) => {
8124
8116
  const either = failureOrCause$1(cause);
8125
8117
  switch (either._tag) {
8126
8118
  case "Left": return predicate(either.left) ? f(either.left) : failCause$7(cause);
@@ -9869,10 +9861,6 @@ const defaultServicesWith = (f) => withFiberRuntime$1((fiber) => f(fiber.current
9869
9861
  const clockWith = (f) => defaultServicesWith((services) => f(services.unsafeMap.get(clockTag.key)));
9870
9862
  /** @internal */
9871
9863
  const currentTimeMillis$1 = /*#__PURE__*/ clockWith((clock) => clock.currentTimeMillis);
9872
- /** @internal */
9873
- const randomWith = (f) => defaultServicesWith((services) => f(services.unsafeMap.get(randomTag.key)));
9874
- /** @internal */
9875
- const next$1 = /*#__PURE__*/ randomWith((random) => random.next);
9876
9864
  //#endregion
9877
9865
  //#region node_modules/effect/dist/esm/Boolean.js
9878
9866
  /**
@@ -11367,7 +11355,7 @@ const catchTag$1 = /*#__PURE__*/ dual((args) => isEffect$1(args[0]), (self, ...a
11367
11355
  for (let i = 0; i < args.length - 1; i++) if (args[i] === tag) return true;
11368
11356
  return false;
11369
11357
  };
11370
- return catchIf$1(self, predicate, f);
11358
+ return catchIf(self, predicate, f);
11371
11359
  });
11372
11360
  const diffFiberRefs = (self) => summarized(self, fiberRefs, diff$2);
11373
11361
  const diffFiberRefsAndRuntimeFlags = (self) => summarized(self, zip$3(fiberRefs, runtimeFlags), ([refs, flags], [refsNew, flagsNew]) => [diff$2(refs, refsNew), diff$3(flags, flagsNew)]);
@@ -11879,7 +11867,7 @@ const fromIterable = (iterable) => {
11879
11867
  return new MetricBoundariesImpl(pipe(iterable, appendAll$2(of$1(Number.POSITIVE_INFINITY)), dedupe));
11880
11868
  };
11881
11869
  /** @internal */
11882
- const exponential$2 = (options) => pipe(makeBy(options.count - 1, (i) => options.start * Math.pow(options.factor, i)), unsafeFromArray, fromIterable);
11870
+ const exponential = (options) => pipe(makeBy(options.count - 1, (i) => options.start * Math.pow(options.factor, i)), unsafeFromArray, fromIterable);
11883
11871
  /** @internal */
11884
11872
  const MetricKeyTypeTypeId = /*#__PURE__*/ Symbol.for("effect/MetricKeyType");
11885
11873
  /** @internal */
@@ -12953,7 +12941,7 @@ const fiberSuccesses = /*#__PURE__*/ counter("effect_fiber_successes", { increme
12953
12941
  /** @internal */
12954
12942
  const fiberFailures = /*#__PURE__*/ counter("effect_fiber_failures", { incremental: true });
12955
12943
  /** @internal */
12956
- const fiberLifetimes = /*#__PURE__*/ tagged$1(/*#__PURE__*/ histogram("effect_fiber_lifetimes", /*#__PURE__*/ exponential$2({
12944
+ const fiberLifetimes = /*#__PURE__*/ tagged$1(/*#__PURE__*/ histogram("effect_fiber_lifetimes", /*#__PURE__*/ exponential({
12957
12945
  start: .5,
12958
12946
  factor: 2,
12959
12947
  count: 35
@@ -14396,7 +14384,7 @@ const isEmpty$3 = (self) => {
14396
14384
  return self.startMillis >= self.endMillis;
14397
14385
  };
14398
14386
  /** @internal */
14399
- const intersect$5 = /*#__PURE__*/ dual(2, (self, that) => {
14387
+ const intersect$4 = /*#__PURE__*/ dual(2, (self, that) => {
14400
14388
  const start = Math.max(self.startMillis, that.startMillis);
14401
14389
  const end = Math.min(self.endMillis, that.endMillis);
14402
14390
  return make$23(start, end);
@@ -14449,7 +14437,7 @@ const isEmpty$2 = isEmpty$3;
14449
14437
  * @since 2.0.0
14450
14438
  * @category ordering
14451
14439
  */
14452
- const intersect$4 = intersect$5;
14440
+ const intersect$3 = intersect$4;
14453
14441
  /**
14454
14442
  * Calculates the size of the `Interval` as the `Duration` from the start of the
14455
14443
  * interval to the end of the interval.
@@ -14476,14 +14464,14 @@ const make$21 = (intervals) => {
14476
14464
  };
14477
14465
  };
14478
14466
  /** @internal */
14479
- const intersect$3 = /*#__PURE__*/ dual(2, (self, that) => intersectLoop(self.intervals, that.intervals, empty$24()));
14467
+ const intersect$2 = /*#__PURE__*/ dual(2, (self, that) => intersectLoop(self.intervals, that.intervals, empty$24()));
14480
14468
  /** @internal */
14481
14469
  const intersectLoop = (_left, _right, _acc) => {
14482
14470
  let left = _left;
14483
14471
  let right = _right;
14484
14472
  let acc = _acc;
14485
14473
  while (isNonEmpty$4(left) && isNonEmpty$4(right)) {
14486
- const interval = pipe(headNonEmpty(left), intersect$4(headNonEmpty(right)));
14474
+ const interval = pipe(headNonEmpty(left), intersect$3(headNonEmpty(right)));
14487
14475
  const intervals = isEmpty$2(interval) ? acc : pipe(acc, prepend$1(interval));
14488
14476
  if (pipe(headNonEmpty(left), lessThan$3(headNonEmpty(right)))) left = tailNonEmpty(left);
14489
14477
  else right = tailNonEmpty(right);
@@ -14520,7 +14508,7 @@ const make$20 = make$21;
14520
14508
  * @since 2.0.0
14521
14509
  * @category utils
14522
14510
  */
14523
- const intersect$2 = intersect$3;
14511
+ const intersect$1 = intersect$2;
14524
14512
  /**
14525
14513
  * The start of the earliest interval in the specified `Intervals`.
14526
14514
  *
@@ -15886,15 +15874,6 @@ const uncapitalize = (self) => {
15886
15874
  * @since 2.0.0
15887
15875
  */
15888
15876
  const isNonEmpty$1 = (self) => self.length > 0;
15889
- //#endregion
15890
- //#region node_modules/effect/dist/esm/Random.js
15891
- /**
15892
- * Returns the next numeric value from the pseudo-random number generator.
15893
- *
15894
- * @since 2.0.0
15895
- * @category constructors
15896
- */
15897
- const next = next$1;
15898
15877
  /** @internal */
15899
15878
  const ScheduleTypeId = /*#__PURE__*/ Symbol.for("effect/Schedule");
15900
15879
  /** @internal */
@@ -16021,18 +16000,9 @@ const checkEffect = /*#__PURE__*/ dual(2, (self, test) => makeWithState(self.ini
16021
16000
  ]);
16022
16001
  })));
16023
16002
  /** @internal */
16024
- const delayedEffect = /*#__PURE__*/ dual(2, (self, f) => modifyDelayEffect(self, (_, delay) => f(delay)));
16025
- /** @internal */
16026
- const delayedSchedule = (schedule) => addDelay(schedule, (x) => x);
16027
- /** @internal */
16028
16003
  const driver = (self) => pipe(make$30([none$4(), self.initial]), map$8((ref) => new ScheduleDriverImpl(self, ref)));
16029
16004
  /** @internal */
16030
- const exponential$1 = (baseInput, factor = 2) => {
16031
- const base = decode(baseInput);
16032
- return delayedSchedule(map$5(forever, (i) => times(base, Math.pow(factor, i))));
16033
- };
16034
- /** @internal */
16035
- const intersect$1 = /*#__PURE__*/ dual(2, (self, that) => intersectWith(self, that, intersect$2));
16005
+ const intersect = /*#__PURE__*/ dual(2, (self, that) => intersectWith(self, that, intersect$1));
16036
16006
  /** @internal */
16037
16007
  const intersectWith = /*#__PURE__*/ dual(3, (self, that, f) => makeWithState([self.initial, that.initial], (now, input, state) => pipe(zipWith$2(self.step(now, input, state[0]), that.step(now, input, state[1]), (a, b) => [a, b]), flatMap$7(([[lState, out, lDecision], [rState, out2, rDecision]]) => {
16038
16008
  if (isContinue(lDecision) && isContinue(rDecision)) return intersectWithLoop(self, that, input, lState, out, lDecision.intervals, rState, out2, rDecision.intervals, f);
@@ -16068,23 +16038,6 @@ const intersectWithLoop = (self, that, input, lState, out, lInterval, rState, ou
16068
16038
  });
16069
16039
  };
16070
16040
  /** @internal */
16071
- const jittered$1 = (self) => jitteredWith(self, {
16072
- min: .8,
16073
- max: 1.2
16074
- });
16075
- /** @internal */
16076
- const jitteredWith = /*#__PURE__*/ dual(2, (self, options) => {
16077
- const { max, min } = Object.assign({
16078
- min: .8,
16079
- max: 1.2
16080
- }, options);
16081
- return delayedEffect(self, (duration) => map$8(next, (random) => {
16082
- const d = toMillis(duration);
16083
- const jittered = d * min * (1 - random) + d * max * random;
16084
- return millis(jittered);
16085
- }));
16086
- });
16087
- /** @internal */
16088
16041
  const map$5 = /*#__PURE__*/ dual(2, (self, f) => mapEffect(self, (out) => sync$2(() => f(out))));
16089
16042
  /** @internal */
16090
16043
  const mapEffect = /*#__PURE__*/ dual(2, (self, f) => makeWithState(self.initial, (now, input, state) => flatMap$7(self.step(now, input, state), ([state, out, decision]) => map$8(f(out), (out2) => [
@@ -16122,7 +16075,7 @@ const passthrough = (self) => makeWithState(self.initial, (now, input, state) =>
16122
16075
  decision
16123
16076
  ])));
16124
16077
  /** @internal */
16125
- const recurs$1 = (n) => whileOutput(forever, (out) => out < n);
16078
+ const recurs = (n) => whileOutput(forever, (out) => out < n);
16126
16079
  /** @internal */
16127
16080
  const spaced$1 = (duration) => addDelay(forever, () => duration);
16128
16081
  /** @internal */
@@ -16171,7 +16124,7 @@ const repeat_combined = /*#__PURE__*/ dual(2, (self, options) => {
16171
16124
  if (typeof applied === "boolean") return succeed$8(applied);
16172
16125
  return scheduleDefectWrap(applied);
16173
16126
  }) : withWhile;
16174
- const withTimes = options.times ? intersect$1(withUntil, recurs$1(options.times)).pipe(map$5((intersectionPair) => intersectionPair[0])) : withUntil;
16127
+ const withTimes = options.times ? intersect(withUntil, recurs(options.times)).pipe(map$5((intersectionPair) => intersectionPair[0])) : withUntil;
16175
16128
  return scheduleDefectRefail(repeat_Effect(self, withTimes));
16176
16129
  });
16177
16130
  /** @internal */
@@ -16207,7 +16160,7 @@ const fromRetryOptions = (options) => {
16207
16160
  if (typeof applied === "boolean") return succeed$8(applied);
16208
16161
  return scheduleDefectWrap(applied);
16209
16162
  }) : withWhile;
16210
- return options.times !== void 0 ? intersect$1(withUntil, recurs$1(options.times)) : withUntil;
16163
+ return options.times !== void 0 ? intersect(withUntil, recurs(options.times)) : withUntil;
16211
16164
  };
16212
16165
  /** @internal */
16213
16166
  const retryOrElse_Effect = /*#__PURE__*/ dual(3, (self, policy, orElse) => flatMap$7(driver(policy), (driver) => retryOrElse_EffectLoop(provideServiceEffect(self, CurrentIterationMetadata, get$4(driver.iterationMeta)), driver, (e, out) => provideServiceEffect(orElse(e, out), CurrentIterationMetadata, get$4(driver.iterationMeta)))));
@@ -17449,59 +17402,6 @@ const catchAll = catchAll$1;
17449
17402
  */
17450
17403
  const catchAllCause = catchAllCause$1;
17451
17404
  /**
17452
- * Recovers from specific errors based on a predicate.
17453
- *
17454
- * **When to Use**
17455
- *
17456
- * `catchIf` works similarly to {@link catchSome}, but it allows you to
17457
- * recover from errors by providing a predicate function. If the predicate
17458
- * matches the error, the recovery effect is applied. This function doesn't
17459
- * alter the error type, so the resulting effect still carries the original
17460
- * error type unless a user-defined type guard is used to narrow the type.
17461
- *
17462
- * **Example** (Catching Specific Errors with a Predicate)
17463
- *
17464
- * ```ts
17465
- * import { Effect, Random } from "effect"
17466
- *
17467
- * class HttpError {
17468
- * readonly _tag = "HttpError"
17469
- * }
17470
- *
17471
- * class ValidationError {
17472
- * readonly _tag = "ValidationError"
17473
- * }
17474
- *
17475
- * // ┌─── Effect<string, HttpError | ValidationError, never>
17476
- * // ▼
17477
- * const program = Effect.gen(function* () {
17478
- * const n1 = yield* Random.next
17479
- * const n2 = yield* Random.next
17480
- * if (n1 < 0.5) {
17481
- * yield* Effect.fail(new HttpError())
17482
- * }
17483
- * if (n2 < 0.5) {
17484
- * yield* Effect.fail(new ValidationError())
17485
- * }
17486
- * return "some result"
17487
- * })
17488
- *
17489
- * // ┌─── Effect<string, ValidationError, never>
17490
- * // ▼
17491
- * const recovered = program.pipe(
17492
- * Effect.catchIf(
17493
- * // Only handle HttpError errors
17494
- * (error) => error._tag === "HttpError",
17495
- * () => Effect.succeed("Recovering from HttpError")
17496
- * )
17497
- * )
17498
- * ```
17499
- *
17500
- * @since 2.0.0
17501
- * @category Error handling
17502
- */
17503
- const catchIf = catchIf$1;
17504
- /**
17505
17405
  * Catches and handles specific errors by their `_tag` field, which is used as a
17506
17406
  * discriminator.
17507
17407
  *
@@ -22675,79 +22575,6 @@ const updateContext = updateContext$1;
22675
22575
  //#endregion
22676
22576
  //#region node_modules/effect/dist/esm/Schedule.js
22677
22577
  /**
22678
- * Creates a schedule that recurs indefinitely with exponentially increasing
22679
- * delays.
22680
- *
22681
- * **Details**
22682
- *
22683
- * This schedule starts with an initial delay of `base` and increases the delay
22684
- * exponentially on each repetition using the formula `base * factor^n`, where
22685
- * `n` is the number of times the schedule has executed so far. If no `factor`
22686
- * is provided, it defaults to `2`, causing the delay to double after each
22687
- * execution.
22688
- *
22689
- * @since 2.0.0
22690
- * @category Constructors
22691
- */
22692
- const exponential = exponential$1;
22693
- /**
22694
- * Combines two schedules, continuing only if both schedules want to continue,
22695
- * using the longer delay.
22696
- *
22697
- * **Details**
22698
- *
22699
- * This function takes two schedules and creates a new schedule that only
22700
- * continues execution if both schedules allow it. The interval between
22701
- * recurrences is determined by the longer delay between the two schedules.
22702
- *
22703
- * The output of the resulting schedule is a tuple containing the outputs of
22704
- * both schedules. The input type is the intersection of both schedules' input
22705
- * types.
22706
- *
22707
- * This is useful when coordinating multiple scheduling conditions where
22708
- * execution should proceed only when both schedules permit it.
22709
- *
22710
- * @see {@link intersectWith} If you need to use a custom merge function.
22711
- *
22712
- * @since 2.0.0
22713
- * @category Composition
22714
- */
22715
- const intersect = intersect$1;
22716
- /**
22717
- * Returns a new schedule that randomly adjusts the interval size within a
22718
- * range.
22719
- *
22720
- * **Details**
22721
- *
22722
- * This function modifies a schedule so that its delay between executions is
22723
- * randomly varied within a range. By default, the delay is adjusted between
22724
- * `80%` (`0.8 * interval`) and `120%` (`1.2 * interval`) of the original
22725
- * interval size.
22726
- *
22727
- * This is useful for adding randomness to repeated executions, reducing
22728
- * contention in distributed systems, and avoiding synchronized execution
22729
- * patterns that can cause bottlenecks.
22730
- *
22731
- * @see {@link jitteredWith} If you need to specify custom min/max values.
22732
- *
22733
- * @since 2.0.0
22734
- * @category Timing & Delay
22735
- */
22736
- const jittered = jittered$1;
22737
- /**
22738
- * A schedule that recurs a fixed number of times before terminating.
22739
- *
22740
- * **Details**
22741
- *
22742
- * This schedule will continue executing until it has been stepped `n` times,
22743
- * after which it will stop. The output of the schedule is the current count of
22744
- * recurrences.
22745
- *
22746
- * @category Constructors
22747
- * @since 2.0.0
22748
- */
22749
- const recurs = recurs$1;
22750
- /**
22751
22578
  * Returns a schedule that recurs continuously, with each repetition
22752
22579
  * spaced by the specified `duration` from the last run.
22753
22580
  *
@@ -192377,7 +192204,7 @@ const ERRORS_MD_PATHS = /* @__PURE__ */ new Set([".gtd/ERRORS.md", "ERRORS.md"])
192377
192204
  * there. An unresolvable `ref` (an empty repo) yields an empty map, so every
192378
192205
  * candidate reads as "not at `ref`".
192379
192206
  */
192380
- const blobsAtRef = (exec, ref, paths) => exec("git", "ls-tree", "-r", "-z", ref, "--", ...paths).pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")), map$4((out) => {
192207
+ const blobsAtRef = (exec, ref, paths) => exec("git", "ls-tree", "-r", "-z", ref, "--", ...paths).pipe(catchAll(() => succeed$3("")), map$4((out) => {
192381
192208
  const blobs = /* @__PURE__ */ new Map();
192382
192209
  for (const entry of splitNul(out)) {
192383
192210
  const tab = entry.indexOf(" ");
@@ -192403,14 +192230,14 @@ const blobsAtRef = (exec, ref, paths) => exec("git", "ls-tree", "-r", "-z", ref,
192403
192230
  * A symlink is the one residual inexactness: hash-object hashes the target's
192404
192231
  * content rather than the link text git stores, and reads as `M`, never `D`.
192405
192232
  */
192406
- const hashObjects = (exec, paths) => exec("git", "hash-object", "--", ...paths).pipe(map$4((out) => new Map(out.trim().split("\n").map((oid, i) => [paths[i] ?? "", oid.trim()]))), catchIf((e) => !isIndexLockError(e), () => succeed$3(/* @__PURE__ */ new Map())));
192233
+ const hashObjects = (exec, paths) => exec("git", "hash-object", "--", ...paths).pipe(map$4((out) => new Map(out.trim().split("\n").map((oid, i) => [paths[i] ?? "", oid.trim()]))), catchAll(() => succeed$3(/* @__PURE__ */ new Map())));
192407
192234
  /**
192408
192235
  * Classify every untracked path against `ref`'s tree, by CONTENT: not at
192409
192236
  * `ref` → `A`; different bytes → `M`; identical bytes → no entry. The latter
192410
- * two exist because "untracked" does not mean "new" — the review window's
192411
- * `git reset --mixed <base>` leaves every file the reviewed range ADDED
192412
- * untracked while it's still present at the window's saved head. "Different
192413
- * bytes" goes through `hashObjects`'s clean filters, matching how git itself
192237
+ * two exist because "untracked" does not mean "new": against a `ref` older
192238
+ * than HEAD, a path can be absent from the index yet already present in
192239
+ * `ref`'s tree with the same or different bytes. "Different bytes" goes
192240
+ * through `hashObjects`'s clean filters, matching how git itself
192414
192241
  * decides it, so an untouched `text=auto` file is never over-reported `M`.
192415
192242
  */
192416
192243
  const classifyUntracked = (exec, ref, untrackedPaths) => gen(function* () {
@@ -192430,34 +192257,12 @@ const classifyUntracked = (exec, ref, untrackedPaths) => gen(function* () {
192430
192257
  });
192431
192258
  });
192432
192259
  /**
192433
- * True when `error` is git's `index.lock` contention failure. gtd shares one
192434
- * worktree index with every git-aware tool the reviewer runs, each of which
192435
- * writes the index to refresh its stat cache whenever the review window's
192436
- * `git reset --mixed` wakes it — a pure "couldn't start" failure, so the
192437
- * losing command is safe to retry verbatim.
192438
- */
192439
- const isIndexLockError = (error) => /index\.lock[\s\S]*File exists|Another git process seems to be running/i.test(error.message);
192440
- const withIndexLockRetry = (eff) => retry(eff, {
192441
- schedule: intersect(recurs(6), exponential(millis(10), 2)).pipe(jittered),
192442
- while: (error) => isIndexLockError(error)
192443
- });
192444
- /**
192445
- * Wrap every operation of a `GitOperations` implementation in
192446
- * `withIndexLockRetry`. The ONE place the retry is applied — both
192447
- * `GitService.Live` and the in-memory test layer build their service through
192448
- * this. Never feed a `strictGitOperations` Proxy through this: `Object.keys`
192449
- * on the Proxy only sees the overrides, not the full port.
192450
- */
192451
- const withIndexLockRetries = (ops) => Object.fromEntries(Object.entries(ops).map(([name, fn]) => [name, (...args) => withIndexLockRetry(fn(...args))]));
192452
- /**
192453
192260
  * Run a command and return its stdout — FAILING on a non-zero exit code with
192454
192261
  * the command line and stderr in the error message. `Command.string` alone
192455
192262
  * silently ignores exit codes, which used to make gtd report success on
192456
192263
  * rejected commits and lose files whose quoted paths broke a swallowed
192457
192264
  * `git add`. Callers that expect a probe to fail handle it with an explicit
192458
- * `catchAll`; `index.lock` contention is retried transparently
192459
- * (`withIndexLockRetries`, applied once to the whole port) before any
192460
- * caller's `catchAll` sees it.
192265
+ * `catchAll`.
192461
192266
  */
192462
192267
  const run = (root, ...args) => scoped$3(gen(function* () {
192463
192268
  const process = yield* (yield* CommandExecutor).start(make$8(...args).pipe(workingDirectory(root), stdout("pipe"), stderr("pipe")));
@@ -192486,7 +192291,7 @@ const makeGitImpl = (executor, root) => {
192486
192291
  readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
192487
192292
  changedPaths: (base) => gen(function* () {
192488
192293
  const ref = base ?? "HEAD";
192489
- const nameStatusOut = yield* exec("git", "diff", "--name-status", "-z", ref).pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
192294
+ const nameStatusOut = yield* exec("git", "diff", "--name-status", "-z", ref).pipe(catchAll(() => succeed$3("")));
192490
192295
  const trackedPaths = parseNameStatus(splitNul(nameStatusOut));
192491
192296
  const untrackedRaw = yield* exec("git", "ls-files", "--others", "--exclude-standard", "-z");
192492
192297
  const untrackedPaths = splitNul(untrackedRaw);
@@ -192554,7 +192359,7 @@ const makeLiveEffect = gen(function* () {
192554
192359
  return makeGitImpl(executor, root);
192555
192360
  });
192556
192361
  var GitService = class GitService extends Tag("GitService")() {
192557
- static Live = effect$1(GitService, makeLiveEffect.pipe(map$4(withIndexLockRetries)));
192362
+ static Live = effect$1(GitService, makeLiveEffect);
192558
192363
  };
192559
192364
  //#endregion
192560
192365
  //#region src/RepoFiles.ts
@@ -193200,146 +193005,13 @@ const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote
193200
193005
  /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
193201
193006
  const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
193202
193007
  //#endregion
193203
- //#region src/OutcomeScript.ts
193204
- /** The plain-text twin of `printfLine`: substitutes `args` into `fmt`'s `%s` placeholders, in order. */
193205
- const renderFormat = (fmt, ...args) => {
193206
- let i = 0;
193207
- return fmt.replace(/%s/g, () => args[i++] ?? "");
193208
- };
193209
- /**
193210
- * A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
193211
- * tokens (a variable reference like `"$from"`), never re-quoted here: a value
193212
- * reaches the script as a printf ARGUMENT, never interpolated into the format
193213
- * itself, so a subject or state name containing `%` can never be read as a
193214
- * conversion spec.
193215
- */
193216
- const printfLine = (fmt, args) => `printf ${shellQuote(fmt)} ${args.join(" ")}`;
193217
- const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
193218
- const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back at %s (\"%s\"), resting at \"%s\".\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";
193219
- const FMT_RESTORED = "restored the retained history — HEAD is back at %s (\"%s\"), resting at \"%s\". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n";
193220
- /** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
193221
- const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
193222
- /**
193223
- * The bash preamble every outcome-carrying script includes, ONE block (no
193224
- * blank lines) so `assembleScript`'s blank-line-joined sections stay intact.
193225
- * The `${TERM:-}` form (never a bare `$TERM`) is deliberate: `TERM` is
193226
- * legitimately unset in plenty of real shells, and a bare reference would
193227
- * trip `set -u` in the embedding script.
193228
- */
193229
- const OUTCOME_PREAMBLE = [
193230
- "# gtd: human-facing outcome rendering (see src/OutcomeScript.ts)",
193231
- "if [ -t 1 ] && [ -z \"${NO_COLOR:-}\" ] && [ \"${TERM:-}\" != dumb ]; then",
193232
- " gtd_c_reset=$(printf '\\033[0m'); gtd_c_bold=$(printf '\\033[1m'); gtd_c_dim=$(printf '\\033[2m')",
193233
- " gtd_c_cyan=$(printf '\\033[36m'); gtd_c_green=$(printf '\\033[32m')",
193234
- " gtd_m_trans=\"➡️\"; gtd_m_commit=\"✅\"; gtd_m_ellipsis=\"…\"",
193235
- "else",
193236
- " gtd_c_reset=\"\"; gtd_c_bold=\"\"; gtd_c_dim=\"\"",
193237
- " gtd_c_cyan=\"\"; gtd_c_green=\"\"",
193238
- " gtd_m_trans=\"->\"; gtd_m_commit=\"[commit]\"; gtd_m_ellipsis=\"...\"",
193239
- "fi",
193240
- "gtd_files() {",
193241
- " gtd_files_sha=\"$1\"",
193242
- " gtd_files_shown=0",
193243
- " gtd_files_total=0",
193244
- " gtd_files_list=\"$(git diff-tree --no-commit-id --name-only -r --root \"$gtd_files_sha\" 2>/dev/null || true)\"",
193245
- " [ -z \"$gtd_files_list\" ] && { unset gtd_files_sha gtd_files_f gtd_files_shown gtd_files_total gtd_files_list; return 0; }",
193246
- " while IFS= read -r gtd_files_f; do [ -n \"$gtd_files_f\" ] && gtd_files_total=$((gtd_files_total + 1)); done <<EOF",
193247
- "$gtd_files_list",
193248
- "EOF",
193249
- " while IFS= read -r gtd_files_f; do",
193250
- " [ -z \"$gtd_files_f\" ] && continue",
193251
- " if [ \"$gtd_files_shown\" -ge 3 ]; then",
193252
- " printf ' %s%s (%d more)%s\\n' \"$gtd_c_dim\" \"$gtd_m_ellipsis\" \"$((gtd_files_total - 3))\" \"$gtd_c_reset\"",
193253
- " break",
193254
- " fi",
193255
- " printf ' %s%s%s\\n' \"$gtd_c_dim\" \"$gtd_files_f\" \"$gtd_c_reset\"",
193256
- " gtd_files_shown=$((gtd_files_shown + 1))",
193257
- " done <<EOF",
193258
- "$gtd_files_list",
193259
- "EOF",
193260
- " unset gtd_files_sha gtd_files_f gtd_files_shown gtd_files_total gtd_files_list",
193261
- "}",
193262
- "gtd_report_transition() {",
193263
- " gtd_rt_from=\"$1\"",
193264
- " gtd_rt_to=\"$2\"",
193265
- " printf '%s %s%s → %s%s\\n' \"$gtd_m_trans\" \"${gtd_c_bold}${gtd_c_cyan}\" \"$gtd_rt_from\" \"$gtd_rt_to\" \"$gtd_c_reset\"",
193266
- " gtd_files HEAD",
193267
- " unset gtd_rt_from gtd_rt_to",
193268
- "}",
193269
- "gtd_report_commit() {",
193270
- " gtd_rc_subject=\"$1\"",
193271
- " printf '%s %s%s%s\\n' \"$gtd_m_commit\" \"$gtd_c_green\" \"$gtd_rc_subject\" \"$gtd_c_reset\"",
193272
- " gtd_files HEAD",
193273
- " unset gtd_rc_subject",
193274
- "}",
193275
- "gtd_report_note() {",
193276
- " printf '%s\\n' \"$1\"",
193277
- "}",
193278
- "gtd_report_abandoned() {",
193279
- " gtd_ra_from=\"$1\"",
193280
- " gtd_ra_head=\"$2\"",
193281
- " gtd_ra_state=\"$3\"",
193282
- " gtd_ra_short=\"$(git rev-parse --short \"$gtd_ra_head\")\"",
193283
- " gtd_ra_subject=\"$(git log -1 --format=%s \"$gtd_ra_head\")\"",
193284
- ` ${printfLine(FMT_ABANDONED, [
193285
- "\"$gtd_ra_from\"",
193286
- "\"$gtd_ra_short\"",
193287
- "\"$gtd_ra_subject\"",
193288
- "\"$gtd_ra_state\""
193289
- ])}`,
193290
- " unset gtd_ra_from gtd_ra_head gtd_ra_state gtd_ra_short gtd_ra_subject",
193291
- "}",
193292
- "gtd_report_restored() {",
193293
- " gtd_rr_to=\"$1\"",
193294
- " gtd_rr_state=\"$2\"",
193295
- " gtd_rr_short=\"$(git rev-parse --short \"$gtd_rr_to\")\"",
193296
- " gtd_rr_subject=\"$(git log -1 --format=%s \"$gtd_rr_to\")\"",
193297
- ` ${printfLine(FMT_RESTORED, [
193298
- "\"$gtd_rr_short\"",
193299
- "\"$gtd_rr_subject\"",
193300
- "\"$gtd_rr_state\""
193301
- ])}`,
193302
- " unset gtd_rr_to gtd_rr_state gtd_rr_short gtd_rr_subject",
193303
- "}"
193304
- ].join("\n");
193305
- /** `gtd_report_transition <from> <to>` — a landed self-loop/target-change transition, with its changed-file rows. */
193306
- const transitionOutcome = (from, to) => `gtd_report_transition ${shellQuote(from)} ${shellQuote(to)}`;
193307
- /** `gtd_report_commit <subject>` — a bare capture (a self-loop commit), with its changed-file rows. */
193308
- const commitOutcome = (subject) => `gtd_report_commit ${shellQuote(subject)}`;
193309
- /** `gtd_report_note <text>` — one already-rendered plain line, no marker. */
193310
- const noteOutcome = (text) => `gtd_report_note ${shellQuote(text)}`;
193311
- /** `gtd_report_abandoned <from> <head> <state>` — resolves the post-hoc short hash/subject from `head` (a commitish) in-script. */
193312
- const abandonedOutcome = (from, head, state) => `gtd_report_abandoned ${shellQuote(from)} ${shellQuote(head)} ${shellQuote(state)}`;
193313
- /** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders, printed via `gtd_report_note`. */
193314
- const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
193315
- /** `gtd_report_restored <to> <state>` — resolves the post-hoc short hash/subject from `to` (a commitish) in-script. */
193316
- const restoredOutcome = (to, state) => `gtd_report_restored ${shellQuote(to)} ${shellQuote(state)}`;
193317
- //#endregion
193318
193008
  //#region src/Emit.ts
193319
193009
  /**
193320
- * Closes the time-of-check/time-of-use gap between the CLI resolving
193321
- * `expectedHead` at decide time and the script running later in an unrelated
193322
- * process. A plain `[ ... ] || { ...; exit 1; }`, not `if`/`fi`, so it stays
193323
- * safe under `set -e` (an OR list's left side failing never trips `-e` on its
193324
- * own). Exported so `src/testing/EmittedScriptRecognizer.ts` can re-derive
193325
- * and string-compare this exact shape.
193326
- *
193327
- * The probe is `--verify --quiet`, not bare `git rev-parse HEAD` — load-
193328
- * bearing: against an unborn HEAD (no commits), the bare form prints the
193329
- * literal string `"HEAD"` and exits 128, so it could never match an
193330
- * `expectedHead === ""` comparison. `--verify --quiet` reads an unborn HEAD
193331
- * back as an empty string instead.
193332
- */
193333
- const headAssertion = (expectedHead) => {
193334
- const q = shellQuote(expectedHead);
193335
- return `[ "$(git rev-parse --verify --quiet HEAD 2>/dev/null)" = ${q} ] || { printf 'gtd: repository changed since this script was generated (expected HEAD %s) — re-run gtd\\n' ${q} >&2; exit 1; }`;
193336
- };
193337
- /**
193338
193010
  * `resolveValidateScript`'s guard: when the declared steering file is absent
193339
193011
  * (e.g. before the producing agent has written it at all), there's nothing
193340
193012
  * to format or validate, so the script exits 0 cleanly rather than running
193341
- * the mode's commands against a missing file. An OR list, not `if`, for the
193342
- * same `set -eu` safety `headAssertion` documents.
193013
+ * the mode's commands against a missing file. An OR list, not `if`: an OR
193014
+ * list's left side failing never trips `set -e` on its own.
193343
193015
  */
193344
193016
  const fileExistsGuard = (file) => `[ -f ${shellQuote(file)} ] || exit 0`;
193345
193017
  /**
@@ -193360,66 +193032,15 @@ const failurePromptWrapper = (command, prompt) => {
193360
193032
  `fi`
193361
193033
  ].join("\n");
193362
193034
  };
193363
- /**
193364
- * `gtd_retry` `eval`s one already-`shellQuote`d command, mirroring
193365
- * `src/Git.ts`'s `withIndexLockRetry`: retry only on the same two
193366
- * `isIndexLockError` substrings, jittered exponential backoff (~10ms
193367
- * doubling, 6 total attempts), propagating any other failure immediately.
193368
- * `awk` (not the shell — no fractional `sleep`, and no `$RANDOM` under POSIX
193369
- * `sh`/`dash`) computes both the fractional-second delay and its own jitter
193370
- * deterministically, so no external entropy source is needed. POSIX `sh` has
193371
- * no `local`, so every variable is `gtd_`-prefixed and `unset` before each
193372
- * `return` to avoid leaking into the rest of the assembled script.
193373
- */
193374
- const RETRY_HELPER = [
193375
- `gtd_retry() {`,
193376
- ` gtd_cmd=$1`,
193377
- ` gtd_attempt=1`,
193378
- ` gtd_delay_ms=10`,
193379
- ` while true; do`,
193380
- ` if gtd_out=$(eval "$gtd_cmd" 2>&1); then`,
193381
- ` [ -n "$gtd_out" ] && printf '%s\\n' "$gtd_out"`,
193382
- ` unset gtd_cmd gtd_attempt gtd_delay_ms gtd_out gtd_total_ms`,
193383
- ` return 0`,
193384
- ` fi`,
193385
- ` case "$gtd_out" in`,
193386
- ` *"index.lock"*|*"Another git process seems to be running"*) ;;`,
193387
- ` *)`,
193388
- ` printf '%s\\n' "$gtd_out" >&2`,
193389
- ` unset gtd_cmd gtd_attempt gtd_delay_ms gtd_out gtd_total_ms`,
193390
- ` return 1`,
193391
- ` ;;`,
193392
- ` esac`,
193393
- ` if [ "$gtd_attempt" -ge 6 ]; then`,
193394
- ` printf '%s\\n' "$gtd_out" >&2`,
193395
- ` unset gtd_cmd gtd_attempt gtd_delay_ms gtd_out gtd_total_ms`,
193396
- ` return 1`,
193397
- ` fi`,
193398
- ` gtd_total_ms=$(awk -v attempt="$gtd_attempt" -v ms="$gtd_delay_ms" 'BEGIN { jitter = (attempt * 2654435761) % ms + 1; printf "%.3f", (ms + jitter) / 1000 }')`,
193399
- ` sleep "$gtd_total_ms"`,
193400
- ` gtd_delay_ms=$(( gtd_delay_ms * 2 ))`,
193401
- ` gtd_attempt=$(( gtd_attempt + 1 ))`,
193402
- ` done`,
193403
- `}`
193404
- ].join("\n");
193405
193035
  const renderStep = (step) => {
193406
- if (step.kind === "gitWrite") return `gtd_retry ${shellQuote(step.command)}`;
193407
193036
  if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
193408
193037
  return step.command;
193409
193038
  };
193410
- const assembleScript = (preconditions, steps) => {
193411
- if (steps.length === 0) return "";
193412
- const sections = ["set -eu"];
193413
- if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
193414
- if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
193415
- if (steps.some((step) => step.kind === "outcome")) sections.push(OUTCOME_PREAMBLE);
193416
- sections.push(...steps.map(renderStep));
193417
- return sections.join("\n\n");
193418
- };
193039
+ const assembleScript = (steps) => steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n");
193419
193040
  /** Build both halves from already-rendered command strings; an empty (or omitted) array produces the empty-string result, so a driver checking `if [ -n "$required" ]` never has to special-case a bare preamble. */
193420
- const emitScripts = (preconditions, required = [], optional = []) => ({
193421
- required: assembleScript(preconditions, required),
193422
- optional: assembleScript(preconditions, optional)
193041
+ const emitScripts = (required = [], optional = []) => ({
193042
+ required: assembleScript(required),
193043
+ optional: assembleScript(optional)
193423
193044
  });
193424
193045
  /** Exported so `src/testing/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
193425
193046
  const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn";
@@ -193444,6 +193065,66 @@ const combinedScript = (required, optional) => {
193444
193065
  ].join("\n");
193445
193066
  };
193446
193067
  //#endregion
193068
+ //#region src/OutcomeScript.ts
193069
+ /** The plain-text twin of `printfLine`: substitutes `args` into `fmt`'s `%s` placeholders, in order. */
193070
+ const renderFormat = (fmt, ...args) => {
193071
+ let i = 0;
193072
+ return fmt.replace(/%s/g, () => args[i++] ?? "");
193073
+ };
193074
+ /**
193075
+ * A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
193076
+ * tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
193077
+ * re-quoted here: a value reaches the script as a printf ARGUMENT, never
193078
+ * interpolated into the format itself, so a subject or state name containing
193079
+ * `%` can never be read as a conversion spec. Markers like `->` are arguments
193080
+ * too, keeping every format string free of a leading `-` that a shell's
193081
+ * `printf` could mistake for an option. A real newline in `fmt` is emitted as
193082
+ * the two-character `\n` escape `printf` itself expands, keeping every emitted
193083
+ * statement on one line.
193084
+ */
193085
+ const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
193086
+ /**
193087
+ * Marks an emitted block as print-only. Load-bearing beyond documentation:
193088
+ * `src/testing/EmittedScriptRecognizer.ts` recognizes an outcome block by
193089
+ * this line, and a bare `printf` would be ambiguous with a workflow
193090
+ * `script:` command that happens to start the same way.
193091
+ */
193092
+ const OUTCOME_MARKER = "# gtd: outcome (print-only)";
193093
+ const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
193094
+ const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back at %s (\"%s\"), resting at \"%s\".\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";
193095
+ const FMT_RESTORED = "restored the retained history — HEAD is back at %s (\"%s\"), resting at \"%s\". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n";
193096
+ /** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
193097
+ const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
193098
+ /** A landed self-loop/target-change transition. */
193099
+ const transitionOutcome = (from, to) => printfLine("%s %s → %s\n", [
193100
+ "'->'",
193101
+ shellQuote(from),
193102
+ shellQuote(to)
193103
+ ]);
193104
+ /** A bare capture (a self-loop commit). */
193105
+ const commitOutcome = (subject) => printfLine("%s %s\n", ["'[commit]'", shellQuote(subject)]);
193106
+ /** One already-rendered plain line. */
193107
+ const noteOutcome = (text) => printfLine("%s\n", [shellQuote(text)]);
193108
+ /**
193109
+ * Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
193110
+ * — this line runs AFTER the reset that made `head` the new HEAD, so neither
193111
+ * value is knowable when gtd generates it.
193112
+ */
193113
+ const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
193114
+ shellQuote(from),
193115
+ `"$(git rev-parse --short ${shellQuote(head)})"`,
193116
+ `"$(git log -1 --format=%s ${shellQuote(head)})"`,
193117
+ shellQuote(state)
193118
+ ]);
193119
+ /** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
193120
+ const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
193121
+ /** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
193122
+ const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
193123
+ `"$(git rev-parse --short ${shellQuote(to)})"`,
193124
+ `"$(git log -1 --format=%s ${shellQuote(to)})"`,
193125
+ shellQuote(state)
193126
+ ]);
193127
+ //#endregion
193447
193128
  //#region src/Edge.ts
193448
193129
  const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
193449
193130
  const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
@@ -193608,10 +193289,7 @@ const walkProcessBoundary = (history, initialState, includeClosingBoundary) => {
193608
193289
  * would pool across them). HEAD itself being such a boundary yields an EMPTY
193609
193290
  * run (`trace: []`).
193610
193291
  *
193611
- * `head` overrides the literal `HEAD` the walk would otherwise end at:
193612
- * `restAt`'s window-aware branch passes the saved-head hash here while a
193613
- * review window is open, so the trace still covers commits a real `git log`
193614
- * would miss with HEAD rewound to the review base.
193292
+ * `head` overrides the literal `HEAD` the walk would otherwise end at.
193615
193293
  *
193616
193294
  * `includeClosingBoundary` (default `false`) is `summaryRun`'s one-flag
193617
193295
  * difference from `currentRun`'s ordinary walk: when set AND the walk's very
@@ -193979,15 +193657,8 @@ const renderDecision = (rest, decision, cost, model) => {
193979
193657
  command: commitAll(withCostTrailer(decision.subject, cost, model))
193980
193658
  }, commitDecisionOutcome(decision)];
193981
193659
  };
193982
- /**
193983
- * The `EmitPreconditions` a step's/entry's assembled scripts assert against:
193984
- * `expectedHead` is the resolved HEAD hash the `Rest` snapshot was taken at.
193985
- */
193986
- const buildPreconditions = (rest) => ({ expectedHead: rest.context.currentCommit });
193987
193660
  /** The `EmittedScripts` a `"commit"` `StepPlan` carries alongside `perform` — built from `renderDecision`'s output. */
193988
- const buildStepScripts = (rest, decision, cost, model) => {
193989
- return emitScripts(buildPreconditions(rest), renderDecision(rest, decision, cost, model));
193990
- };
193661
+ const buildStepScripts = (rest, decision, cost, model) => emitScripts(renderDecision(rest, decision, cost, model));
193991
193662
  /**
193992
193663
  * A no-op is TERMINAL only at a `script` rest: gtd rendered the script, the
193993
193664
  * driver ran it, it left nothing any pattern claims, and re-running it can't
@@ -194095,17 +193766,16 @@ const planEntry = (rest, actor, entry) => gen(function* () {
194095
193766
  base = resolvedBase.right;
194096
193767
  }
194097
193768
  const subject = stateSubject(actor, entryState);
194098
- const message = withEntryTrailers(subject, {
194099
- ...base !== void 0 ? { base } : {},
194100
- vars: varOverrides
194101
- });
194102
193769
  return {
194103
193770
  kind: "entry",
194104
193771
  state: entryState,
194105
193772
  subject,
194106
- scripts: emitScripts(buildPreconditions(rest), [{
193773
+ scripts: emitScripts([{
194107
193774
  kind: "gitWrite",
194108
- command: commitAll(message)
193775
+ command: commitAll(withEntryTrailers(subject, {
193776
+ ...base !== void 0 ? { base } : {},
193777
+ vars: varOverrides
193778
+ }))
194109
193779
  }, {
194110
193780
  kind: "outcome",
194111
193781
  command: commitOutcome(subject)
@@ -205944,7 +205614,7 @@ const landFields = (input) => ({
205944
205614
  const renderLandJson = (fields) => JSON.stringify(fields) + "\n";
205945
205615
  const FMT_NOOP = "nothing to do at \"%s\"\n";
205946
205616
  const FMT_LAND_PROSE = "commit everything with this message: %s\n";
205947
- /** `nothing to do at "<state>"` — a no-op step's plain-text line, and the text a print-only script's `gtd_report_note` carries. */
205617
+ /** `nothing to do at "<state>"` — a no-op step's plain-text line, and the text a print-only script's own `printf` carries. */
205948
205618
  const noopText = (state) => renderFormat(FMT_NOOP, state);
205949
205619
  /** `commit everything with this message: <subject>` — plain `gtd land`'s own stdout at a pending diff (no script); `--json` keeps emitting the script itself, unaffected. */
205950
205620
  const landProseText = (subject) => renderFormat(FMT_LAND_PROSE, subject);
@@ -206235,6 +205905,24 @@ f="$(gtd next --json=file)"
206235
205905
  f="\${f:-.gtd/TODO.md}"
206236
205906
  mkdir -p "$(dirname "$f")"
206237
205907
  exec "\${EDITOR:-vi}" "$f"`;
205908
+ const REVIEW_COMMAND = `#!/usr/bin/env sh
205909
+ set -eu
205910
+ GTD_BUILD=~/.local/bin/gtd-build
205911
+ if [ -z "\${1:-}" ]; then
205912
+ echo "usage: gtd-review <commitish>" >&2
205913
+ exit 2
205914
+ fi
205915
+ cd "$(git rev-parse --show-toplevel)"
205916
+ script="$(gtd --entry review-gate.check --var reviewBase="$1")"
205917
+ sh -c "$script"
205918
+ exec "$GTD_BUILD"`;
205919
+ const FIX_COMMAND = `#!/usr/bin/env sh
205920
+ set -eu
205921
+ GTD_BUILD=~/.local/bin/gtd-build
205922
+ cd "$(git rev-parse --show-toplevel)"
205923
+ script="$(gtd --entry fix-precheck)"
205924
+ sh -c "$script"
205925
+ exec "$GTD_BUILD"`;
206238
205926
  const HEADER = () => `gtd ${GTD_VERSION$1} — driver protocol\n\ngtd decides and prints; the driver executes. gtd never runs git, never runs\nyour agent, never runs a check script, and never runs a steering mode's own\nformat:/validate: command either — that pair is text in the script gtd next\n--json's validate field (or gtd validate) prints, for the driver to run\nitself ahead of gtd land.\n`;
206239
205927
  const BEAT_PROTOCOL = `
206240
205928
  ## The beat protocol
@@ -206412,14 +206100,14 @@ since moved refuses loudly instead of corrupting anything. Scripts are
206412
206100
  re-runnable and assert their own HEAD precondition — a driver needs no retry
206413
206101
  logic beyond "ask gtd again".
206414
206102
  `;
206415
- const referenceImplementation = () => `
206416
- ## Building the user's driver: interview first, then adapt
206103
+ const commandSuite = () => `
206104
+ ## Building the user's command suite: interview first, then adapt
206417
206105
 
206418
- The obligations above are the contract; the sh block below is one WORKED
206419
- EXAMPLE of it. Do not copy it blindly, and do not guess the user's setup —
206420
- INVESTIGATE first, then INTERVIEW, then build a driver shaped by both. Hold
206421
- ONE conversation, not two: the setup steps below come first, because a repo
206422
- that is not set up yet cannot be driven, and the driver-shape questions
206106
+ The obligations above are the contract; the four command bodies below are one
206107
+ WORKED EXAMPLE of it. Do not copy them blindly, and do not guess the user's
206108
+ setup — INVESTIGATE first, then INTERVIEW, then build a suite shaped by both.
206109
+ Hold ONE conversation, not four: the setup steps below come first, because a
206110
+ repo that is not set up yet cannot be driven, and the suite-shape questions
206423
206111
  follow in the same numbered list.
206424
206112
 
206425
206113
  1. **Investigate the repository and ask the user what they want before
@@ -206438,29 +206126,59 @@ follow in the same numbered list.
206438
206126
  uncommitted config is a pending change, so starting a process without
206439
206127
  committing it first turns the config into the very first thing the
206440
206128
  process plans against. Commit it, THEN start driving.
206441
- 4. **Which agent should run the turns?** Whatever coding-agent CLI they
206442
- already use (default: \`claude\`). Map \`session.id\`/\`session.resume\`
206443
- onto THAT agent's continuation mechanism (obligation 5); an agent with
206444
- no session concept just ignores them — memory is an optimization, every
206445
- prompt is self-contained.
206446
- 5. **Under which permission model?** Fully autonomous turns (e.g.
206129
+ 4. **Which agent should run the turns?** Probe \`PATH\` for the known
206130
+ coding-agent CLIs — \`claude\`, \`codex\`, \`gemini\`, \`cursor-agent\`,
206131
+ \`aider\`, \`opencode\`, \`amp\` — show the user which are actually
206132
+ installed, and ask which one to drive with (default: \`claude\`). Accept
206133
+ a name that was not on the list; the probe is a convenience, not a
206134
+ restriction. The chosen CLI's own flags then replace the \`claude\` lines
206135
+ in the reference body below — its session flags (obligation 5) and its
206136
+ permission model (question 5).
206137
+ 5. **Under which permission model, and should the workflow's model hints be
206138
+ honored at all?** Fully autonomous turns (e.g.
206447
206139
  \`--dangerously-skip-permissions\`), a sandbox, or the agent's default
206448
- prompting — their risk tolerance, their call. Also whether the workflow's
206449
- own \`model\` hints should be honored (default: yes).
206450
- 6. **How do they want to invoke it?** A command on PATH (default:
206451
- \`~/.local/bin/gtd-loop\`), a project task-runner entry, a CI job step —
206452
- or no artifact at all: YOU drive the beats yourself, following the
206453
- obligations directly. Pick the runtime to match: bash, their language of
206454
- choice, anything that reads \`--json=<path>\` and spawns subprocesses.
206140
+ prompting — their risk tolerance, their call. Also ask whether the
206141
+ workflow's own \`model\` hints (resolved in question 6 below) should be
206142
+ honored (default: yes) — answering no means dropping \`--model\` entirely
206143
+ from every prompt line and writing no model exports, so every turn runs
206144
+ on the chosen CLI's own default tier.
206145
+ 6. **If model hints are honored, resolve them to real model identifiers.**
206146
+ The bundled workflow ships \`plannerModel: smart\` and \`coderModel: base\`.
206147
+ **Those are opaque hints, not model names** — gtd never interprets them,
206148
+ and passing \`--model smart\` to a real CLI fails on the first prompt
206149
+ beat. Derive the chosen CLI's own available models at install time — its
206150
+ \`--help\`, its docs, its own config — never a hardcoded table (a vendor
206151
+ renames a model and a baked-in list goes stale the day it ships). **When
206152
+ the heavier/cheaper mapping is not obvious, ask**: list the CLI's models
206153
+ and let the user pick one per tier — the heavier tier for
206154
+ \`plannerModel\` (triage, design, review), the cheaper tier for
206155
+ \`coderModel\` (build, fix). Write the resolved names as
206156
+ \`GTD_PLANNERMODEL\`/\`GTD_CODERMODEL\` exports at the top of \`gtd-build\`,
206157
+ wrapped in \`# gtd-install: model exports\` / \`# gtd-install: end\`
206158
+ markers — NOT into \`.gtdrc\`. They stay per machine and are never
206159
+ committed, so one person's model choice binds nobody else on the repo.
206160
+ Two consequences follow, both worth stating to the user: \`gtd-review\`
206161
+ and \`gtd-fix\` inherit these exports only because they \`exec\`
206162
+ \`gtd-build\` — anything that drives beats without going through
206163
+ \`gtd-build\` sees the raw \`smart\`/\`base\` hints and fails; and \`GTD_*\` is
206164
+ the highest-precedence config layer, so these exports silently win over
206165
+ a \`plannerModel\`/\`coderModel\` a teammate later commits to \`.gtdrc\`.
206455
206166
  7. **What should happen at the boundaries?** Where the log goes (obligation
206456
206167
  4), and whether halting at a human gate should do anything richer than
206457
206168
  print — desktop notification, terminal-multiplexer status, editor focus —
206458
206169
  which belongs in their wrapper, never in gtd.
206459
- 8. **Which editor should the edit command spawn?** Default \`$EDITOR\`, never
206460
- a hardcoded editor.
206461
- 9. **What should the edit command be named?** Suggest
206462
- \`~/.local/bin/gtd-edit\`, matching the loop command's own default
206463
- (\`~/.local/bin/gtd-loop\`) — the interview can override either name.
206170
+ 8. **How do they want to invoke it?** A command on PATH (default:
206171
+ \`~/.local/bin/gtd-build\`), a project task-runner entry, a CI job step —
206172
+ or no artifact at all: YOU drive the beats yourself, following the
206173
+ obligations directly. Pick the runtime to match: bash, their language of
206174
+ choice, anything that reads \`--json=<path>\` and spawns subprocesses.
206175
+ 9. **Where should the suite live, which editor should \`gtd-edit\` spawn, and
206176
+ does anything need renaming?** Suggest \`~/.local/bin\` for all four —
206177
+ \`gtd-build\`, \`gtd-edit\`, \`gtd-review\`, \`gtd-fix\` — and \`$EDITOR\` (never
206178
+ a hardcoded editor) for the one that opens files. The interview can
206179
+ rename any of the four; whatever name is chosen for \`gtd-build\` is the
206180
+ RESOLVED path baked into \`GTD_BUILD\` in the other two bodies below —
206181
+ never the literal string \`gtd-build\`.
206464
206182
 
206465
206183
  Then build it, and verify safely before the first real drive:
206466
206184
  \`gtd next --json=<path>\`/\`gtd next\` are both pure reads — read/parse them,
@@ -206468,20 +206186,20 @@ check your kind dispatch against the table above, call them as often as you
206468
206186
  like. Nothing happens until you run an emitted script.
206469
206187
 
206470
206188
  The reference rendering in sh (no \`jq\`, no JSON parser at all — each
206471
- \`--json=<path>\` call prints its one value directly; the \`claude\` lines are
206472
- what answers 1–2 replace):
206189
+ \`--json=<path>\` call prints its one value directly). Two of the four bodies
206190
+ below share one convention: \`GTD_BUILD\` is set once, at the top, to the
206191
+ suite's resolved \`gtd-build\` path.
206192
+
206193
+ ### \`gtd-build\` — the driver loop
206194
+
206195
+ The \`claude\` lines are what interview answers 4 and 6 replace. Default path:
206196
+ \`~/.local/bin/gtd-build\`.
206473
206197
 
206474
206198
  \`\`\`bash
206475
206199
  ${MINIMAL_DRIVER}
206476
206200
  \`\`\`
206477
- `;
206478
- const editCommand = () => `
206479
- ## Building the user's edit command
206480
206201
 
206481
- The loop command above drives beats; it never opens a file for the human. Add
206482
- a second artifact to the same interview (questions 8–9 above): an edit
206483
- command that opens the steering file the process is resting at right now,
206484
- suggested at \`~/.local/bin/gtd-edit\`.
206202
+ ### \`gtd-edit\` — open the steering file the process is resting at
206485
206203
 
206486
206204
  \`gtd next --json=file\` already reports the file for every state that
206487
206205
  declares one — the edit command needs no new engine surface, just a read and
@@ -206501,12 +206219,115 @@ the first sketch the whole process gets planned from. The same command does
206501
206219
  the same thing at every rest, whether or not a state declares a \`file:\`.
206502
206220
 
206503
206221
  The edit command stops when the editor exits — it never drives the loop. It
206504
- opens a file and returns; the human runs the loop command themselves when
206505
- they are ready.
206222
+ opens a file and returns; the human runs \`gtd-build\` themselves when they
206223
+ are ready. Default path: \`~/.local/bin/gtd-edit\`.
206506
206224
 
206507
206225
  \`\`\`bash
206508
206226
  ${EDIT_COMMAND}
206509
206227
  \`\`\`
206228
+
206229
+ ### \`gtd-review <commitish>\` — start a review round and drive it
206230
+
206231
+ Runs \`gtd --entry review-gate.check --var reviewBase=<commitish>\`, captured
206232
+ by command substitution — never a pipe: a pipeline reports only its LAST
206233
+ command's exit status, so \`gtd --entry ... | sh\` under \`set -e\` would sail
206234
+ past a refusal and \`exec\` a loop over a process that was never started. It
206235
+ runs the captured script, then \`exec\`s the suite's RESOLVED \`gtd-build\`
206236
+ path — never the literal string \`gtd-build\`. This is one invocation that
206237
+ carries the review all the way to its next human gate, not just a starter:
206238
+ \`gtd-review\` on a RED baseline hands off to \`gtd-build\`, which halts at the
206239
+ blocked gate and prints it, rather than starting the review. Refuses with
206240
+ \`usage: gtd-review <commitish>\` on stderr and exit \`2\` when the commitish is
206241
+ missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-review\`.
206242
+
206243
+ \`\`\`bash
206244
+ ${REVIEW_COMMAND}
206245
+ \`\`\`
206246
+
206247
+ ### \`gtd-fix\` — enter the fix process and drive it
206248
+
206249
+ Runs \`gtd --entry fix-precheck\`, captured the same way, then \`exec\`s the
206250
+ suite's RESOLVED \`gtd-build\` path. \`gtd-fix\` on a GREEN suite is a no-op
206251
+ straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
206252
+ Default path: \`~/.local/bin/gtd-fix\`.
206253
+
206254
+ \`\`\`bash
206255
+ ${FIX_COMMAND}
206256
+ \`\`\`
206257
+ ${REINSTALL}`;
206258
+ const REINSTALL = `
206259
+ ## Re-installing: detect and adapt, don't blindly overwrite
206260
+
206261
+ A second install on a machine that already has a suite is not a fresh
206262
+ install — read each of the four suite paths before writing anything
206263
+ (\`gtd-build\`, \`gtd-edit\`, \`gtd-review\`, \`gtd-fix\`), and branch per path:
206264
+
206265
+ - Absent — install it, as in a fresh install.
206266
+ - Present and content-equal to what this gtd version would emit — say so,
206267
+ change nothing, and skip that command's interview questions entirely.
206268
+ - Present and different — show the difference, name the likely cause (a gtd
206269
+ upgrade, or the user's own edit), and ask before overwriting. Never
206270
+ silently replace a file the user may have customised.
206271
+ - Unreadable, or present as a directory instead of a file — report it and
206272
+ ask about it, the same as a differing file. Never overwritten.
206273
+
206274
+ Drift is detected by comparing CONTENT, never by parsing a version out of
206275
+ the installed file — an installed command carries no version marker to
206276
+ parse.
206277
+
206278
+ \`gtd-build\` needs one exemption before that comparison: the region between
206279
+ \`# gtd-install: model exports\` and \`# gtd-install: end\` gets stripped from
206280
+ the installed file first. A \`gtd-build\` differing only inside those
206281
+ markers is unchanged, and re-asks nothing — the resolved model names are
206282
+ per machine, so without this exemption every single re-install would
206283
+ report drift on the one command everybody has.
206284
+
206285
+ This check scopes to exactly the four suite paths above. \`gtd-loop\` is
206286
+ outside it entirely — never read, never diffed, never touched by it in any
206287
+ way — an existing \`gtd-loop\` survives untouched beside the new
206288
+ \`gtd-build\`, and cleaning it up is the human's own call.
206289
+ `;
206290
+ const EDITOR_INTEGRATION = `
206291
+ ## Editor integration: offer it, don't bury it
206292
+
206293
+ \`gtd lsp\` runs a language server, started by \`gtd lsp\` itself, speaking LSP
206294
+ over \`stdio\`, applying to files under \`.gtd/\`. That is the whole integration
206295
+ contract, identical for every editor — this briefing names no specific editor
206296
+ and carries no per-editor config recipe.
206297
+
206298
+ Find the user's editor from their OWN shell configuration, never a guess:
206299
+ check \`$EDITOR\`/\`$VISUAL\` first, then look for an alias or export in their
206300
+ shell rc files (\`.zshrc\`, \`.bashrc\`, \`.config/fish/config.fish\`). Ask the
206301
+ user outright when that turns up nothing or turns up more than one. When the
206302
+ shell configuration names no editor and the user names none either, say
206303
+ nothing and move on — don't pitch editor integration to somebody with no
206304
+ editor to integrate.
206305
+
206306
+ Once an editor is identified, look ITS OWN LSP configuration format up
206307
+ yourself — there is no bundled recipe for it. If it is LSP-capable, offer to
206308
+ wire \`gtd lsp\` up with a brief explanation (three or four lines, not a copy
206309
+ of \`docs/setup.md\`): live diagnostics on \`.gtd/\` steering files, an outline
206310
+ of what is left to review, click-to-check review hunks, pick-an-option
206311
+ actions on open questions, and a \`gtd.openSteeringFile\` command that jumps
206312
+ to the current state's file. Two facts belong in that offer because both
206313
+ bite on a fresh repo: \`gtd lsp\` never creates \`.gtd/\`, so
206314
+ \`gtd.openSteeringFile\` may point at a path that does not exist yet before
206315
+ the first sketch; and \`gtd lsp\` needs no repository root — it is one of the
206316
+ commands that skips the root guard.
206317
+
206318
+ On yes, edit the editor's own config file yourself — never print a snippet
206319
+ and walk away. Integration works without the human touching anything. Three
206320
+ guards apply to that write, because it lands outside the repository in a
206321
+ file the user shares across every project:
206322
+
206323
+ - **Ask first, per editor, naming the exact file** about to change. Silence
206324
+ is not consent for a machine-wide config.
206325
+ - **Merge, never overwrite.** Read the existing config, add only the gtd
206326
+ language-server entry, and leave every other key byte-identical.
206327
+ - **Skip and report when the entry is already present** — a second install
206328
+ must not append a duplicate server registration. A malformed existing
206329
+ config (unparseable JSON, a TOML syntax error) is a stop-and-report, not a
206330
+ rewrite: name the file that could not be parsed and leave it untouched.
206510
206331
  `;
206511
206332
  const PREREQUISITES = `
206512
206333
  ## Prerequisites and portability
@@ -206523,7 +206344,7 @@ const PREREQUISITES = `
206523
206344
  Any runtime works: the contract is \`--json\`/\`--json=<path>\` in, subprocesses
206524
206345
  out.
206525
206346
  `;
206526
- const renderBriefing = () => HEADER() + BEAT_PROTOCOL + JSON_FIELD_REFERENCE + DRIVER_OBLIGATIONS + RECOVERY + referenceImplementation() + editCommand() + PREREQUISITES;
206347
+ const renderBriefing = () => HEADER() + BEAT_PROTOCOL + JSON_FIELD_REFERENCE + DRIVER_OBLIGATIONS + RECOVERY + commandSuite() + EDITOR_INTEGRATION + PREREQUISITES;
206527
206348
  //#endregion
206528
206349
  //#region src/Select.ts
206529
206350
  /**
@@ -206654,7 +206475,6 @@ const runInitCommand = (out) => gen(function* () {
206654
206475
  */
206655
206476
  const normalizeScriptNewline = (script) => script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`;
206656
206477
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
206657
- const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
206658
206478
  /** True for a `"commit"` decision that is an ATTEMPT (`PatternMachine.StepCommit.attempt`) — the one flag `enforceStepGuards` bypasses its own steps for (see its call site). */
206659
206479
  const isAttemptDecision = (decision) => decision.kind === "commit" && decision.attempt === true;
206660
206480
  /** The subject the required script will produce. */
@@ -206665,7 +206485,7 @@ const previewSubject = (decision) => succeed$3(decision.subject);
206665
206485
  * landing script — that's a driver contract now (`gtd next --json`'s
206666
206486
  * `validate` field), not a gtd guarantee (see `resolveSelfValidateCommand`).
206667
206487
  */
206668
- const buildRequiredScript = (rest, decision, cost, model) => succeed$3(emitScripts(headPreconditions(rest.context.currentCommit), renderDecision(rest, decision, cost, model)).required);
206488
+ const buildRequiredScript = (rest, decision, cost, model) => succeed$3(emitScripts(renderDecision(rest, decision, cost, model)).required);
206669
206489
  /**
206670
206490
  * Decide the one resulting transition (a commit) for `gtd land`
206671
206491
  * without performing it, authenticating as `rest.actor`. Refusals fail the
@@ -206677,7 +206497,7 @@ const planLanding = (opts = {}) => gen(function* () {
206677
206497
  const plan = yield* planStep(rest, opts);
206678
206498
  if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
206679
206499
  if (plan.kind === "noop") {
206680
- const required = emitScripts({}, [{
206500
+ const required = emitScripts([{
206681
206501
  kind: "outcome",
206682
206502
  command: noteOutcome(noopText(plan.state))
206683
206503
  }]).required;
@@ -206814,7 +206634,7 @@ const runAbandonCommand = (out) => gen(function* () {
206814
206634
  const initial = initialStateOf(def);
206815
206635
  const run = yield* currentRun;
206816
206636
  if (run.trace.length === 0) {
206817
- const required = emitScripts({}, [{
206637
+ const required = emitScripts([{
206818
206638
  kind: "outcome",
206819
206639
  command: abandonNoopOutcome(initial)
206820
206640
  }]).required;
@@ -206823,11 +206643,10 @@ const runAbandonCommand = (out) => gen(function* () {
206823
206643
  }
206824
206644
  const restState = run.trace[run.trace.length - 1].state;
206825
206645
  if (run.startParentHash === EMPTY_TREE) return yield* fail$2(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
206826
- const tip = yield* git.resolveRef("HEAD");
206827
- const steps = [
206646
+ const required = emitScripts([
206828
206647
  {
206829
206648
  kind: "gitWrite",
206830
- command: updateRef(HISTORY_REF, tip)
206649
+ command: updateRef(HISTORY_REF, yield* git.resolveRef("HEAD"))
206831
206650
  },
206832
206651
  {
206833
206652
  kind: "gitWrite",
@@ -206837,8 +206656,7 @@ const runAbandonCommand = (out) => gen(function* () {
206837
206656
  kind: "outcome",
206838
206657
  command: abandonedOutcome(restState, run.startParentHash, initial)
206839
206658
  }
206840
- ];
206841
- const required = emitScripts(headPreconditions(tip), steps).required;
206659
+ ]).required;
206842
206660
  out.write(combinedScript(required, ""));
206843
206661
  });
206844
206662
  /**
@@ -206858,7 +206676,7 @@ const runRestoreCommand = (out) => gen(function* () {
206858
206676
  const check = yield* restorability(git, headHash, tip);
206859
206677
  if (!check.ok) return yield* fail$2(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
206860
206678
  const after = yield* restAt(tip);
206861
- const steps = [
206679
+ const required = emitScripts([
206862
206680
  {
206863
206681
  kind: "gitWrite",
206864
206682
  command: hardResetTo(tip)
@@ -206871,8 +206689,7 @@ const runRestoreCommand = (out) => gen(function* () {
206871
206689
  kind: "outcome",
206872
206690
  command: restoredOutcome(tip, after.state)
206873
206691
  }
206874
- ];
206875
- const required = emitScripts(headPreconditions(headHash), steps).required;
206692
+ ]).required;
206876
206693
  out.write(combinedScript(required, ""));
206877
206694
  });
206878
206695
  /**
@@ -206996,18 +206813,17 @@ const resolveValidateScript = (rest) => gen(function* () {
206996
206813
  if (file === void 0 || mode === void 0) return void 0;
206997
206814
  const resolved = resolveSteeringMode(rest.def, mode);
206998
206815
  if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
206999
- const steps = [
207000
- ...yield* modeContradictionSteps(resolved, mode, rest.context),
207001
- {
207002
- kind: "command",
207003
- command: fileExistsGuard(file)
207004
- },
207005
- ...yield* renderSteeringModeCommandSteps(resolved, file, rest.context)
207006
- ];
207007
206816
  return {
207008
206817
  file,
207009
206818
  mode,
207010
- script: emitScripts(headPreconditions(rest.context.currentCommit), steps).required
206819
+ script: emitScripts([
206820
+ ...yield* modeContradictionSteps(resolved, mode, rest.context),
206821
+ {
206822
+ kind: "command",
206823
+ command: fileExistsGuard(file)
206824
+ },
206825
+ ...yield* renderSteeringModeCommandSteps(resolved, file, rest.context)
206826
+ ]).required
207011
206827
  };
207012
206828
  });
207013
206829
  /** The driver-facing `BeatKind` for a currently-resolved rest, the one computation `gatherBeatFields` reads — so a driver's `kind` field can never drift from what it assembled. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "9.14.1",
3
+ "version": "10.1.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": {