@pmelab/gtd 64.3.0 → 65.0.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.
Files changed (58) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +34 -25
  3. package/claude/hooks/assist.ts +9 -35
  4. package/claude/hooks/doors.ts +4 -4
  5. package/claude/hooks/git.ts +56 -0
  6. package/claude/hooks/handoff.ts +11 -11
  7. package/claude/hooks/jobs.ts +8 -8
  8. package/claude/hooks/pr.ts +8 -8
  9. package/claude/hooks/register.tsx +16 -78
  10. package/claude/hooks/tools.ts +7 -20
  11. package/claude/hooks/wording.ts +3 -3
  12. package/dist/chunks/{flows-dnIE_qbe.mjs → flows-DGs0W3HI.mjs} +19 -2
  13. package/dist/chunks/{testkit-BUX-47TD.mjs → testkit-CgrLmpdJ.mjs} +740 -79
  14. package/dist/chunks/{wire-CaPJ9Tme.mjs → wire-BNS8ciNu.mjs} +5 -3
  15. package/dist/flows.mjs +1 -1
  16. package/dist/gtd.bundle.mjs +553 -81
  17. package/dist/gtd.core.mjs +132 -129
  18. package/dist/testkit/gherkin.mjs +1 -1
  19. package/dist/testkit.mjs +2 -2
  20. package/dist/wire.mjs +1 -1
  21. package/dist/workflow-kit.mjs +1 -1
  22. package/package.json +1 -1
  23. package/src/cli/Cli.ts +4 -4
  24. package/src/cli/History.test.ts +10 -8
  25. package/src/cli/Metrics.test.ts +2 -1
  26. package/src/cli/Program.test.ts +4 -2
  27. package/src/cli/Program.ts +87 -5
  28. package/src/edge/Edge.test.ts +37 -16
  29. package/src/edge/Edge.ts +296 -31
  30. package/src/emit/GitScript.ts +116 -15
  31. package/src/emit/index.ts +7 -0
  32. package/src/flows/config.ts +7 -1
  33. package/src/flows/index.test.ts +1 -0
  34. package/src/flows/index.ts +2 -0
  35. package/src/flows/runtime.ts +32 -1
  36. package/src/platform/RetainedHistory.ts +3 -0
  37. package/src/platform/index.ts +1 -0
  38. package/src/replay/Replay.test.ts +9 -0
  39. package/src/replay/Replay.ts +59 -15
  40. package/src/replay/Trailers.ts +14 -0
  41. package/src/replay/index.ts +1 -1
  42. package/src/step/LandStep.ts +12 -0
  43. package/src/step/RepoSnapshot.ts +11 -0
  44. package/src/step/index.ts +1 -0
  45. package/src/step/planStart.test.ts +4 -3
  46. package/src/step/planStart.ts +7 -0
  47. package/src/step/planStep.ts +17 -12
  48. package/src/step/startBranch.ts +54 -0
  49. package/src/twins/EmittedScriptRecognizer.ts +117 -0
  50. package/src/twins/EmittedScripts.test.ts +147 -1
  51. package/src/twins/InMemRepo.ts +116 -0
  52. package/src/wire/Demand.ts +22 -10
  53. package/src/wire/constants.ts +3 -0
  54. package/src/wire/types.ts +4 -0
  55. package/src/workflow/Workflow.ts +1 -0
  56. package/src/workflow/compile.ts +2 -0
  57. package/src/workflow/load.ts +2 -0
  58. package/claude/hooks/ship.ts +0 -730
@@ -1981,7 +1981,7 @@ var init_Order = __esmMin((() => {
1981
1981
  isGreaterThan$1 = (O) => dual(2, (self, that) => O(self, that) === 1);
1982
1982
  isLessThanOrEqualTo$1 = (O) => dual(2, (self, that) => O(self, that) !== 1);
1983
1983
  isGreaterThanOrEqualTo$1 = (O) => dual(2, (self, that) => O(self, that) !== -1);
1984
- })), none, some, isNone, isSome, match$3, getOrElse, orElse, fromNullishOr, getOrUndefined$1, map$6, flatMap$4;
1984
+ })), none, some, isNone, isSome, match$3, getOrElse, orElse, fromNullishOr, getOrUndefined$1, getOrThrowWith$1, getOrThrow$1, map$6, flatMap$4;
1985
1985
  var init_Option = __esmMin((() => {
1986
1986
  init_Function();
1987
1987
  init_option();
@@ -1994,6 +1994,11 @@ var init_Option = __esmMin((() => {
1994
1994
  orElse = /*#__PURE__*/ dual(2, (self, that) => isNone(self) ? that() : self);
1995
1995
  fromNullishOr = (a) => a == null ? none() : some(a);
1996
1996
  getOrUndefined$1 = /*#__PURE__*/ getOrElse(constUndefined);
1997
+ getOrThrowWith$1 = /*#__PURE__*/ dual(2, (self, onNone) => {
1998
+ if (isSome(self)) return self.value;
1999
+ throw onNone();
2000
+ });
2001
+ getOrThrow$1 = /*#__PURE__*/ getOrThrowWith$1(() => /* @__PURE__ */ new Error("getOrThrow called on a None"));
1997
2002
  map$6 = /*#__PURE__*/ dual(2, (self, f) => isNone(self) ? none() : some(f(self.value)));
1998
2003
  flatMap$4 = /*#__PURE__*/ dual(2, (self, f) => isNone(self) ? none() : f(self.value));
1999
2004
  }));
@@ -13724,6 +13729,8 @@ const loopLogPath = gen(function* () {
13724
13729
  init_dist$1();
13725
13730
  /** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
13726
13731
  const HISTORY_REF = "refs/worktree/gtd/history";
13732
+ /** Set by an abandon that left `gtd/<b>` for `<b>`: only then does restore recreate `gtd/<b>`. */
13733
+ const RESUME_BRANCH_REF = "refs/worktree/gtd/history-branch";
13727
13734
  /** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
13728
13735
  const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
13729
13736
  const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
@@ -14632,6 +14639,17 @@ const run$3 = (name, body, options = {}) => ctx().step({
14632
14639
  body,
14633
14640
  options
14634
14641
  });
14642
+ /**
14643
+ * Squashes HEAD's tree, with `.gtd/` as the user's branch has it, into one commit on the user's branch
14644
+ * with `message`, and moves that branch there; HEAD stays on `gtd/<b>`.
14645
+ * Resolves to the new commit's hash, or `undefined` when nothing shipped.
14646
+ */
14647
+ const ship = (name, message, options = {}) => ctx().step({
14648
+ kind: "ship",
14649
+ name,
14650
+ message,
14651
+ options
14652
+ });
14635
14653
  /** A judge gate: resolves to what the judge answered, for the flow to decide on. */
14636
14654
  const judge = (name, spec) => {
14637
14655
  const { questions, evidence, ...options } = spec;
@@ -15123,7 +15141,8 @@ const CONFIG_KEYS = [
15123
15141
  "modes",
15124
15142
  "ui",
15125
15143
  "skills",
15126
- "access"
15144
+ "access",
15145
+ "historyRemote"
15127
15146
  ];
15128
15147
  const MODE_KEYS = ["format", "validate"];
15129
15148
  const UI_KEYS = [
@@ -15194,6 +15213,10 @@ const configProblems = (value) => {
15194
15213
  ...unknownKeys(value, CONFIG_KEYS, []),
15195
15214
  ...value.modes === void 0 ? [] : recordOf(value.modes, ["modes"], "modes", modeProblems),
15196
15215
  ...value.ui === void 0 ? [] : uiProblems(value.ui, ["ui"]),
15216
+ ...value.historyRemote === void 0 || typeof value.historyRemote === "string" && value.historyRemote.trim() !== "" ? [] : [{
15217
+ path: ["historyRemote"],
15218
+ message: "must be a non-empty remote name"
15219
+ }],
15197
15220
  ...value.skills === void 0 ? [] : recordOf(value.skills, ["skills"], "skill-name arrays", (v, path) => isStringArray$1(v) ? [] : [{
15198
15221
  path,
15199
15222
  message: "must be an array of skill names"
@@ -15262,6 +15285,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
15262
15285
  sections: () => sections$1,
15263
15286
  settings: () => settings,
15264
15287
  settingsOf: () => settingsOf,
15288
+ ship: () => ship,
15265
15289
  showScript: () => showScript,
15266
15290
  skillsFor: () => skillsFor,
15267
15291
  start: () => start,
@@ -31138,6 +31162,8 @@ const renderFormat = (fmt, ...args) => {
31138
31162
  const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
31139
31163
  /** Where a process keeps its pin: the engine and workflow it runs on, as `gtd bundle` prints them. */
31140
31164
  const PIN_PATH = ".gtd/process.gtdpin";
31165
+ /** Where a `run(…, { onFail: "rest" })` body's failure is recorded in the tree: the exit status and the output. */
31166
+ const RUN_FAILED_PATH = ".gtd/run-failed.txt";
31141
31167
  // src/wire/Demand.ts
31142
31168
  /**
31143
31169
  * The beat kind for a rest, in precedence order:
@@ -31161,7 +31187,7 @@ const beatKindOf = (input) => {
31161
31187
  * `stalled at "<state>"` stays a stable substring (see
31162
31188
  * `tests/integration/features/driver-doc.feature`).
31163
31189
  */
31164
- const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nTwo ways out:\n - sharpen the step's prompt so the turn has something concrete to author\n - pass allowEmpty: true to the step, so an empty turn completes it and the\n flow decides what follows — finishing, or an escalation step after N\n empty turns\n`;
31190
+ const stallDiagnosis = (state, actor, failure, blocked) => blocked !== void 0 ? `stalled at "${state}": landing this ship step is refused.\n\n${blocked}\n` : failure !== void 0 ? `stalled at "${state}": the script exited non-zero and its failure is recorded in the tree. Change the tree to run the script again; the step advances only on a zero exit.\n\n${failure}` : `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nTwo ways out:\n - sharpen the step's prompt so the turn has something concrete to author\n - pass allowEmpty: true to the step, so an empty turn completes it and the\n flow decides what follows — finishing, or an escalation step after N\n empty turns\n`;
31165
31191
  /**
31166
31192
  * Assemble one `Demand` — the ONLY place a rest's resolved content becomes
31167
31193
  * `Demand.content` (`stallDiagnosis` at `kind === "stalled"`, the rendered
@@ -31171,7 +31197,7 @@ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${
31171
31197
  */
31172
31198
  const demandOf = (input) => {
31173
31199
  const { rendered, kind, session, validate } = input;
31174
- const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content;
31200
+ const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor, rendered.failure, rendered.blocked) : rendered.content;
31175
31201
  const skills = rendered.skills !== void 0 && rendered.skills.length > 0 ? rendered.skills : void 0;
31176
31202
  const access = rendered.access ?? {
31177
31203
  read: null,
@@ -31770,6 +31796,12 @@ const TRAILER_READERS = {
31770
31796
  "Gtd-Anchor": (value, into) => {
31771
31797
  if (HASH_RE.test(value)) into.anchor ??= value;
31772
31798
  },
31799
+ "Gtd-Shipped": (value, into) => {
31800
+ if (HASH_RE.test(value)) into.shipped ??= value;
31801
+ },
31802
+ "Gtd-Tip": (value, into) => {
31803
+ if (HASH_RE.test(value)) into.tip ??= value;
31804
+ },
31773
31805
  "Gtd-Format": (value, into) => {
31774
31806
  if (FORMAT_RE.test(value)) into.format ??= Number(value);
31775
31807
  },
@@ -31794,6 +31826,8 @@ const parseCommitMessage = (message) => {
31794
31826
  approved: void 0,
31795
31827
  pin: void 0,
31796
31828
  anchor: void 0,
31829
+ shipped: void 0,
31830
+ tip: void 0,
31797
31831
  judge: [],
31798
31832
  marks: [],
31799
31833
  vars: {},
@@ -31835,7 +31869,7 @@ const namesUnpin = (commit) => {
31835
31869
  const unpin = pinStateIn(commit.parsed.format, "remove");
31836
31870
  return unpin !== void 0 && commit.parsed.parsed?.to === unpin;
31837
31871
  };
31838
- const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
31872
+ const actorOfKind = (kind) => kind === "run" || kind === "ship" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
31839
31873
  const memoryScopeOf = (name) => {
31840
31874
  const dot = name.lastIndexOf(".");
31841
31875
  return dot === -1 ? "" : name.slice(0, dot);
@@ -31969,9 +32003,12 @@ const KNOWN_OPTIONS = {
31969
32003
  "acceptClean",
31970
32004
  "approve"
31971
32005
  ]),
31972
- run: new Set(STEERING_OPTIONS),
32006
+ run: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "onFail"]),
32007
+ ship: /* @__PURE__ */ new Set(["label", "base"]),
31973
32008
  judge: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "message"])
31974
32009
  };
32010
+ const restsOnFail = (step) => step.request.kind === "run" && step.request.options.onFail === "rest";
32011
+ const onFailFault = (step) => step.request.kind === "run" && step.request.options.onFail !== void 0 && step.request.options.onFail !== "rest" ? `gtd: step "${step.name}": onFail must be "rest" in run() options` : void 0;
31975
32012
  const unknownOptions = (step) => {
31976
32013
  const unknown = Object.keys(step.request.options).filter((key) => !KNOWN_OPTIONS[step.kind].has(key));
31977
32014
  if (unknown.length === 0) return void 0;
@@ -32082,15 +32119,15 @@ const replay = async (raw) => {
32082
32119
  return calls;
32083
32120
  };
32084
32121
  const scoped = (name) => [...scopes.flatMap((s) => s.name === void 0 ? [] : [s.name]), name].join(".");
32085
- const isAttemptAt = (commit, name) => commit.parsed.step === void 0 && commit.parsed.parsed !== void 0 && commit.parsed.parsed.from === void 0 && commit.parsed.parsed.to === name && isEmptyDiff(diffTrees(position.tree, commit.tree));
32122
+ const isAttemptAt = (commit, step) => commit.parsed.step === void 0 && commit.parsed.parsed !== void 0 && commit.parsed.parsed.from === void 0 && commit.parsed.parsed.to === step.name && (isEmptyDiff(diffTrees(position.tree, commit.tree)) || restsOnFail(step) && commit.tree.read(".gtd/run-failed.txt") !== void 0);
32086
32123
  const advance = (next) => {
32087
32124
  previousPosition = position;
32088
32125
  position = next;
32089
32126
  };
32090
- const replyFor = (request, verdicts) => request.kind === "judge" ? {
32127
+ const replyFor = (request, verdicts, shipped) => request.kind === "judge" ? {
32091
32128
  answers: answersFrom(verdicts, request.questions),
32092
32129
  truncated: judgeCuts.get(request) ?? []
32093
- } : void 0;
32130
+ } : request.kind === "ship" ? shipped : void 0;
32094
32131
  const judgeCuts = /* @__PURE__ */ new WeakMap();
32095
32132
  const resolveScoped = (memory, pick, configured, bundled) => {
32096
32133
  const own = /* @__PURE__ */ new Map();
@@ -32204,7 +32241,7 @@ const replay = async (raw) => {
32204
32241
  expectedNext = void 0;
32205
32242
  anchorPending = false;
32206
32243
  if (expected !== void 0 && expected.name !== step.name) return diverged(`gtd: commit ${short(expected.hash)} names "${expected.name}" as the next step, but replay reached "${step.name}"`, step);
32207
- const error = unknownOptions(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
32244
+ const error = unknownOptions(step) ?? onFailFault(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
32208
32245
  return error === void 0 ? void 0 : {
32209
32246
  kind: "failed",
32210
32247
  message: error
@@ -32230,7 +32267,7 @@ const replay = async (raw) => {
32230
32267
  expectNextOf(commit);
32231
32268
  skipPins();
32232
32269
  noteKept(step, commit);
32233
- return replyFor(step.request, commit.parsed.judge);
32270
+ return replyFor(step.request, commit.parsed.judge, commit.parsed.shipped);
32234
32271
  };
32235
32272
  const expectNextOf = (commit) => {
32236
32273
  const to = commit.parsed.parsed?.to;
@@ -32261,10 +32298,14 @@ const replay = async (raw) => {
32261
32298
  hash: "",
32262
32299
  tree: keeping ? position.tree : pending.tree
32263
32300
  });
32264
- return replyFor(step.request, pending.verdicts ?? []);
32301
+ return replyFor(step.request, pending.verdicts ?? [], pending.shipped);
32265
32302
  };
32266
32303
  const answer = (step) => {
32267
- while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) {
32304
+ while (cursor < commits.length && isAttemptAt(commits[cursor], step)) {
32305
+ if (!isEmptyDiff(diffTrees(position.tree, commits[cursor].tree))) advance({
32306
+ hash: commits[cursor].hash,
32307
+ tree: commits[cursor].tree
32308
+ });
32268
32309
  cursor++;
32269
32310
  skipPins();
32270
32311
  }
@@ -32277,12 +32318,22 @@ const replay = async (raw) => {
32277
32318
  landed,
32278
32319
  marks
32279
32320
  });
32321
+ if (restsOnFail(step) && input.pending.tree.read(".gtd/run-failed.txt") !== void 0) return finish({
32322
+ kind: "rest",
32323
+ rest: step,
32324
+ trace,
32325
+ landed,
32326
+ marks
32327
+ });
32280
32328
  return landPending(step, input.pending);
32281
32329
  };
32330
+ const bodyFault = (request) => {
32331
+ if (request.kind === "ship" && (typeof request.message !== "string" || request.message.trim() === "")) return `gtd: step "${scoped(request.name)}": a ship() message is a non-empty commit message`;
32332
+ if (request.kind === "run" && typeof request.body !== "string") return `gtd: step "${scoped(request.name)}": a run() body is a shell script string — decide in flow code, then render the script (see checkScript and friends)`;
32333
+ };
32282
32334
  const requestFault = (request) => {
32283
32335
  if (scoped(request.name).startsWith("gtd:")) return `gtd: step "${scoped(request.name)}": the "${ENGINE_PREFIX}" prefix is reserved for gtd's own beats — rename the step`;
32284
- if (request.kind === "run" && typeof request.body !== "string") return `gtd: step "${scoped(request.name)}": a run() body is a shell script string — decide in flow code, then render the script (see checkScript and friends)`;
32285
- if (request.kind !== "agent") return void 0;
32336
+ if (request.kind !== "agent") return bodyFault(request);
32286
32337
  const name = scoped(request.name);
32287
32338
  if (request.options.keep === true) {
32288
32339
  if (request.options.allowEmpty === true) return `gtd: step "${name}": keep: true cannot be combined with allowEmpty — a turn that changes nothing has nothing to keep`;
@@ -32464,6 +32515,7 @@ const compileConfig = (value, origin) => {
32464
32515
  return {
32465
32516
  modes,
32466
32517
  ...config.ui !== void 0 ? { ui: config.ui } : {},
32518
+ ...config.historyRemote !== void 0 ? { historyRemote: config.historyRemote } : {},
32467
32519
  skills: Object.assign(Object.create(null), config.skills),
32468
32520
  access: Object.assign(Object.create(null), config.access),
32469
32521
  diagnostics: []
@@ -32780,6 +32832,7 @@ const load = gen(function* () {
32780
32832
  return entry === void 0 ? void 0 : resolve(name, entry);
32781
32833
  },
32782
32834
  ...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
32835
+ ...compiled.historyRemote !== void 0 ? { historyRemote: compiled.historyRemote } : {},
32783
32836
  warnings: diagnostics.filter((d) => d.severity === "warning"),
32784
32837
  fingerprint: pinned?.fingerprint ?? configFingerprint(nodeSyncIo, paths, loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
32785
32838
  drift: pinned === void 0 ? [] : changedConfigFiles(pinned.fingerprint, configFingerprint(nodeSyncIo, paths, Object.keys(pinned.fingerprint).map((file) => paths.resolve(host.root, file)), host.root, readFromDisk))
@@ -33501,8 +33554,9 @@ const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
33501
33554
  * output rather than the exit code, so a genuinely rejected commit (a lint
33502
33555
  * error, a commit-msg hook) still fails.
33503
33556
  */
33504
- const commitAllowEmpty = (message, only) => {
33505
- const m = shellQuote$1(message);
33557
+ const commitAllowEmpty = (message, only) => commitWordAllowEmpty(shellQuote$1(message), only);
33558
+ /** `commitAllowEmpty` over a message that is already a shell word. */
33559
+ const commitWordAllowEmpty = (m, only) => {
33506
33560
  const paths = only === void 0 ? "" : ` -- ${shellQuote$1(only)}`;
33507
33561
  return [
33508
33562
  `if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
@@ -33518,6 +33572,28 @@ const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — r
33518
33572
  const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
33519
33573
  /** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
33520
33574
  const commitPath = (path, message) => `git add -A -- ${shellQuote$1(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
33575
+ /**
33576
+ * A `run()` body that records its own failure instead of failing the beat: the
33577
+ * status and the last 50 output lines go to `path`, and the script exits zero
33578
+ * so the turn lands. The file is COMMITTED (and force-pushed with a history
33579
+ * remote), hence the cap.
33580
+ */
33581
+ const failRestScript = (body, path) => {
33582
+ const p = shellQuote$1(path);
33583
+ const slash = path.lastIndexOf("/");
33584
+ return [
33585
+ `rm -f -- ${p}`,
33586
+ `gtd_run_status=0`,
33587
+ `gtd_run_out="$( {`,
33588
+ body,
33589
+ `} 2>&1 )" || gtd_run_status=$?`,
33590
+ `printf '%s\\n' "$gtd_run_out"`,
33591
+ `if [ "$gtd_run_status" -ne 0 ]; then`,
33592
+ ` mkdir -p ${shellQuote$1(path.slice(0, slash))}`,
33593
+ ` printf 'exit %s\\n%s\\n' "$gtd_run_status" "$(printf '%s\\n' "$gtd_run_out" | tail -n 50)" >${p}`,
33594
+ `fi`
33595
+ ].join("\n");
33596
+ };
33521
33597
  /** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
33522
33598
  const pinScript = (path) => {
33523
33599
  const p = shellQuote$1(path);
@@ -33581,6 +33657,62 @@ const keepTurn = (refPrefix, message) => {
33581
33657
  `git reset --hard HEAD`
33582
33658
  ].join("\n");
33583
33659
  };
33660
+ /** `git switch -C <branch> [<start>]`: create or reset the branch, carrying pending changes over. */
33661
+ const switchBranchTo = (branch, start) => `git switch -q -C ${shellQuote$1(branch)}${start === void 0 ? "" : ` ${shellQuote$1(start)}`}`;
33662
+ /** `git switch <branch>`: an existing branch, carrying pending changes over. */
33663
+ const switchTo = (branch) => `git switch -q ${shellQuote$1(branch)}`;
33664
+ /** `switchTo` that, when pending changes block it, stays put and says so: the landing already happened. */
33665
+ const switchBack = (branch) => [
33666
+ `if ! git switch -q ${shellQuote$1(branch)} 2>/dev/null; then`,
33667
+ ` printf '%s\\n' ${shellQuote$1(`gtd: pending changes keep HEAD off ${branch}; switch to it once they are committed or stashed`)}`,
33668
+ `fi`
33669
+ ].join("\n");
33670
+ const deleteBranch = (branch) => `git branch -q -D ${shellQuote$1(branch)}`;
33671
+ /**
33672
+ * Force-pushes one ref to a remote. A failed push exits non-zero with the retry
33673
+ * on stderr: it runs after the closing commit has landed, so the commit stands.
33674
+ */
33675
+ const pushRef = (remote, ref) => {
33676
+ const retry = `git push --force ${shellQuote$1(remote)} ${shellQuote$1(`${ref}:${ref}`)}`;
33677
+ return [
33678
+ `if ! ${retry}; then`,
33679
+ ` printf '%s\\n' ${shellQuote$1(`gtd: pushing ${ref} failed — retry: ${retry}`)} >&2`,
33680
+ ` exit 1`,
33681
+ `fi`
33682
+ ].join("\n");
33683
+ };
33684
+ /**
33685
+ * A ship step's landing, in one block because the landing commit's
33686
+ * `Gtd-Shipped` trailer names the squash it makes first. The squash carries
33687
+ * the staged tree with `.gtd/` as `<branch>` has it, built in a scratch index; `<branch>` moves by
33688
+ * compare-and-swap against `parent`, so a branch moved since the decision
33689
+ * fails the script before the landing commits. `commit-tree` ignores
33690
+ * `commit.gpgSign`, so it is read here.
33691
+ */
33692
+ const shipCommit = (branch, parent, shipMessage, historyRef, message) => [
33693
+ `gtd_ship_parent=${shellQuote$1(parent)}`,
33694
+ `gtd_ship_branch=${shellQuote$1(branch)}`,
33695
+ `gtd_ship_history=${shellQuote$1(historyRef)}`,
33696
+ `gtd_ship_message=${shellQuote$1(shipMessage)}`,
33697
+ `gtd_land_message=${shellQuote$1(message)}`,
33698
+ `git add -A || exit 1`,
33699
+ `gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
33700
+ `if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
33701
+ `gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
33702
+ `gtd_tree=$(git write-tree) || exit 1`,
33703
+ `gtd_index=$(git rev-parse --git-path gtd-ship-index) || exit 1`,
33704
+ `rm -f -- "$gtd_index"`,
33705
+ `GIT_INDEX_FILE="$gtd_index" git read-tree "$gtd_tree" || exit 1`,
33706
+ `GIT_INDEX_FILE="$gtd_index" git rm -r -q --cached --ignore-unmatch -- .gtd || exit 1`,
33707
+ `git ls-tree -r "$gtd_ship_parent" -- .gtd | GIT_INDEX_FILE="$gtd_index" git update-index --index-info || exit 1`,
33708
+ `gtd_ship_tree=$(GIT_INDEX_FILE="$gtd_index" git write-tree) || exit 1`,
33709
+ `rm -f -- "$gtd_index"`,
33710
+ `gtd_ship_trailers=$(printf "Gtd-History: %s\\nGtd-Tip: %s" "$gtd_ship_history" "$gtd_head")`,
33711
+ `gtd_shipped=$(git commit-tree "$gtd_sign" "$gtd_ship_tree" -p "$gtd_ship_parent" -m "$gtd_ship_message" -m "$gtd_ship_trailers") || exit 1`,
33712
+ `git update-ref -m "gtd: ship" "refs/heads/$gtd_ship_branch" "$gtd_shipped" "$gtd_ship_parent" || exit 1`,
33713
+ `gtd_message=$(printf "%s\\nGtd-Shipped: %s" "$gtd_land_message" "$gtd_shipped")`,
33714
+ commitWordAllowEmpty(`"$gtd_message"`)
33715
+ ].join("\n");
33584
33716
  const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
33585
33717
  /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
33586
33718
  const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
@@ -33609,15 +33741,18 @@ const renderOutcome = (outcome) => {
33609
33741
  case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
33610
33742
  }
33611
33743
  };
33612
- const renderGitWrite = (write) => {
33613
- switch (write.kind) {
33614
- case "commitAll": return commitAll(write.message);
33615
- case "commitPath": return commitPath(write.path, write.message);
33616
- case "commitClean": return commitClean(write.message);
33617
- case "updateRef": return updateRef(write.ref, write.target);
33618
- case "keepTurn": return keepTurn(write.ref, write.message);
33619
- }
33744
+ const GIT_WRITES = {
33745
+ commitAll: (w) => commitAll(w.message),
33746
+ commitPath: (w) => commitPath(w.path, w.message),
33747
+ commitClean: (w) => commitClean(w.message),
33748
+ updateRef: (w) => updateRef(w.ref, w.target),
33749
+ keepTurn: (w) => keepTurn(w.ref, w.message),
33750
+ switchBranch: (w) => switchBranchTo(w.branch),
33751
+ switchTo: (w) => switchBack(w.branch),
33752
+ pushRef: (w) => pushRef(w.remote, w.ref),
33753
+ ship: (w) => shipCommit(w.branch, w.parent, w.shipMessage, w.historyRef, w.message)
33620
33754
  };
33755
+ const renderGitWrite = (write) => GIT_WRITES[write.kind](write);
33621
33756
  /**
33622
33757
  * Wraps `command` so a non-zero exit prints `prompt` plus the command's
33623
33758
  * captured output before propagating that exit code — mirrors `Emit.ts`'s
@@ -34010,6 +34145,11 @@ const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot
34010
34145
  file: snapshot.file
34011
34146
  }] : [];
34012
34147
  const gitWriteOf = (landing, message) => {
34148
+ if (landing.ship !== void 0) return {
34149
+ kind: "ship",
34150
+ message,
34151
+ ...landing.ship
34152
+ };
34013
34153
  if (landing.only !== void 0) return {
34014
34154
  kind: "commitPath",
34015
34155
  path: landing.only,
@@ -34029,6 +34169,10 @@ const gitWriteOf = (landing, message) => {
34029
34169
  message
34030
34170
  };
34031
34171
  };
34172
+ const writeIf = (value, write) => value === void 0 ? [] : [{
34173
+ kind: "gitWrite",
34174
+ write: write(value)
34175
+ }];
34032
34176
  const planStep = (snapshot, opts = {}) => {
34033
34177
  const landing = snapshot.landing;
34034
34178
  if (landing.kind === "refusal") return {
@@ -34076,6 +34220,7 @@ const planStep = (snapshot, opts = {}) => {
34076
34220
  ...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
34077
34221
  });
34078
34222
  const subject = message.split("\n")[0];
34223
+ const historyRef = landing.keepHistory ?? landing.ship?.historyRef;
34079
34224
  const outcome = landing.to === snapshot.state ? {
34080
34225
  kind: "outcome",
34081
34226
  outcome: {
@@ -34096,20 +34241,37 @@ const planStep = (snapshot, opts = {}) => {
34096
34241
  to: landing.to,
34097
34242
  subject,
34098
34243
  steps: [
34244
+ ...writeIf(landing.startBranch, (branch) => ({
34245
+ kind: "switchBranch",
34246
+ branch
34247
+ })),
34099
34248
  ...uncheckStep(snapshot),
34100
34249
  {
34101
34250
  kind: "gitWrite",
34102
34251
  write: gitWriteOf(landing, message)
34103
34252
  },
34104
- ...landing.keepHistory !== void 0 ? [{
34105
- kind: "gitWrite",
34106
- write: {
34107
- kind: "updateRef",
34108
- ref: landing.keepHistory,
34109
- target: "HEAD"
34253
+ ...writeIf(historyRef, (ref) => ({
34254
+ kind: "updateRef",
34255
+ ref,
34256
+ target: "HEAD"
34257
+ })),
34258
+ ...writeIf(landing.leaveBranch, (branch) => ({
34259
+ kind: "switchTo",
34260
+ branch
34261
+ })),
34262
+ outcome,
34263
+ ...landing.note !== void 0 ? [{
34264
+ kind: "outcome",
34265
+ outcome: {
34266
+ kind: "note",
34267
+ text: landing.note
34110
34268
  }
34111
34269
  }] : [],
34112
- outcome
34270
+ ...writeIf(historyRef !== void 0 ? landing.pushHistory : void 0, (remote) => ({
34271
+ kind: "pushRef",
34272
+ remote,
34273
+ ref: historyRef
34274
+ }))
34113
34275
  ]
34114
34276
  };
34115
34277
  };
@@ -34129,6 +34291,43 @@ const accessRefusal = (changes, write) => {
34129
34291
  `allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
34130
34292
  ].join("\n");
34131
34293
  };
34294
+ // src/step/startBranch.ts
34295
+ init_dist$1();
34296
+ const GTD_BRANCH_PREFIX = "gtd/";
34297
+ /** `<b>` for a `gtd/<b>` branch name; `undefined` for any other. */
34298
+ const userBranchOf = (branch) => branch.startsWith("gtd/") ? branch.slice(4) : void 0;
34299
+ /**
34300
+ * Where a new process starts: on `gtd/<b>`, so `<b>` never carries its commits.
34301
+ * An existing `gtd/<b>` is reused only when its tip is contained in `<b>` or in
34302
+ * a kept history ref (a squash onto `<b>` never makes it an ancestor).
34303
+ */
34304
+ const startBranchFor = (commandLabel) => gen(function* () {
34305
+ const git = yield* GitService;
34306
+ const current = yield* git.currentBranch();
34307
+ if (isNone(current) || current.value.startsWith("gtd/")) return { kind: "inPlace" };
34308
+ if (!(yield* git.hasCommits())) return { kind: "inPlace" };
34309
+ const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
34310
+ const existing = yield* git.readRefOption(`refs/heads/${branch}`);
34311
+ if (isSome(existing)) {
34312
+ const head = yield* git.resolveRef("HEAD");
34313
+ let contained = yield* git.isAncestor(existing.value, head);
34314
+ if (!contained) {
34315
+ const kept = yield* git.listRefs(PROCESS_HISTORY_PREFIX);
34316
+ for (const k of kept) if (yield* git.isAncestor(existing.value, k.hash)) {
34317
+ contained = true;
34318
+ break;
34319
+ }
34320
+ }
34321
+ if (!contained) return {
34322
+ kind: "refusal",
34323
+ message: `${commandLabel}: branch ${branch} holds commits that are neither on ${current.value} nor in a kept history ref — merge or delete it first`
34324
+ };
34325
+ }
34326
+ return {
34327
+ kind: "switch",
34328
+ branch
34329
+ };
34330
+ });
34132
34331
  // src/step/planStart.ts
34133
34332
  init_dist$1();
34134
34333
  const refusal = (message) => ({
@@ -34170,29 +34369,42 @@ const planStart = (current, actor, start) => gen(function* () {
34170
34369
  const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
34171
34370
  if (typeof base === "object") return base;
34172
34371
  const subject = formatSubject(actor, firstStep);
34372
+ const message = formatCommitMessage({
34373
+ actor,
34374
+ to: firstStep,
34375
+ workflow: name,
34376
+ ...base !== void 0 ? { reviewBase: base } : {},
34377
+ vars
34378
+ });
34379
+ const onBranch = yield* startBranchFor(commandLabel);
34380
+ if (onBranch.kind === "refusal") return refusal(onBranch.message);
34173
34381
  return {
34174
34382
  kind: "start",
34175
34383
  workflow: name,
34176
34384
  subject,
34177
- steps: [{
34178
- kind: "gitWrite",
34179
- write: {
34180
- kind: "commitAll",
34181
- message: formatCommitMessage({
34182
- actor,
34183
- to: firstStep,
34184
- workflow: name,
34185
- ...base !== void 0 ? { reviewBase: base } : {},
34186
- vars
34187
- })
34188
- }
34189
- }, {
34190
- kind: "outcome",
34191
- outcome: {
34192
- kind: "commit",
34193
- subject
34385
+ steps: [
34386
+ ...onBranch.kind === "switch" ? [{
34387
+ kind: "gitWrite",
34388
+ write: {
34389
+ kind: "switchBranch",
34390
+ branch: onBranch.branch
34391
+ }
34392
+ }] : [],
34393
+ {
34394
+ kind: "gitWrite",
34395
+ write: {
34396
+ kind: "commitAll",
34397
+ message
34398
+ }
34399
+ },
34400
+ {
34401
+ kind: "outcome",
34402
+ outcome: {
34403
+ kind: "commit",
34404
+ subject
34405
+ }
34194
34406
  }
34195
- }]
34407
+ ]
34196
34408
  };
34197
34409
  });
34198
34410
  /**
@@ -34438,14 +34650,32 @@ const runAtTip = (git, def, tip) => gen(function* () {
34438
34650
  if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
34439
34651
  return yield* runOf([...before, ...closing], { ...location }, tip.hash);
34440
34652
  });
34441
- const runAtHead = (git, def) => gen(function* () {
34442
- return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
34443
- });
34444
- /** The process HEAD closes or sits inside — `gtd summary`'s run. */
34653
+ /**
34654
+ * The process HEAD closes or sits inside — `gtd summary`'s run. On `<b>`, where
34655
+ * a finished process leaves HEAD, it is the one `gtd/<b>`'s tip closes, while
34656
+ * `<b>` holds nothing since: its start, or its last ship.
34657
+ */
34445
34658
  const summaryRun = gen(function* () {
34446
34659
  const git = yield* GitService;
34447
34660
  const def = (yield* (yield* ConfigService).load).workflow;
34448
- return yield* runAtHead(git, def);
34661
+ const head = (yield* git.subjectHistory(1, 0))[0];
34662
+ const left = head === void 0 ? void 0 : yield* leftProcessTip(git, head);
34663
+ return yield* runAtTip(git, def, left ?? head);
34664
+ });
34665
+ const leftProcessTip = (git, head) => gen(function* () {
34666
+ if (parseCommitMessage(head.subject).parsed !== void 0) return void 0;
34667
+ const current = yield* git.currentBranch();
34668
+ if (isNone(current) || userBranchOf(current.value) !== void 0) return void 0;
34669
+ const tip = yield* git.readRefOption(`refs/heads/${GTD_BRANCH_PREFIX}${current.value}`);
34670
+ if (isNone(tip)) return void 0;
34671
+ const message = yield* git.lastCommitMessage(tip.value);
34672
+ if (parseCommitMessage(message).parsed?.to === void 0) return void 0;
34673
+ const since = parseCommitMessage(yield* git.lastCommitMessage(head.hash)).tip ?? head.hash;
34674
+ if (!(yield* git.isAncestor(since, tip.value))) return void 0;
34675
+ return {
34676
+ hash: tip.value,
34677
+ subject: message.split("\n")[0]
34678
+ };
34449
34679
  });
34450
34680
  const commitTree = (workspace, hash) => {
34451
34681
  let entries;
@@ -34579,7 +34809,14 @@ const promptDef = (common, request, skills, access) => ({
34579
34809
  const scriptDef = (common, request) => ({
34580
34810
  ...common,
34581
34811
  kind: "script",
34582
- content: request.body
34812
+ content: request.options.onFail === "rest" ? failRestScript(request.body, RUN_FAILED_PATH) : request.body,
34813
+ ...optional("onFail", request.options.onFail)
34814
+ });
34815
+ const SHIP_CONTENT = noteOutcome("gtd: landing this step ships the process onto your branch");
34816
+ const shipDef = (common) => ({
34817
+ ...common,
34818
+ kind: "script",
34819
+ content: SHIP_CONTENT
34583
34820
  });
34584
34821
  const judgeDef = (common, request, step) => {
34585
34822
  const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
@@ -34603,6 +34840,7 @@ const stepDefOf = (step) => {
34603
34840
  switch (request.kind) {
34604
34841
  case "agent": return promptDef(common, request, step.skills, step.access);
34605
34842
  case "run": return scriptDef(common, request);
34843
+ case "ship": return shipDef(common);
34606
34844
  case "judge": return judgeDef(common, request, step);
34607
34845
  case "human": return messageDef(common, request, step);
34608
34846
  }
@@ -34756,6 +34994,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
34756
34994
  path: e.path
34757
34995
  })) : [];
34758
34996
  if (ref === void 0 && config.drift.length > 0) yield* (yield* Narrator).warn(`gtd: notice: the live config differs from the pinned one (${config.drift.join(", ")}); \`gtd repin\` to switch`);
34997
+ const shipBlocked = ref === void 0 ? yield* shipBlockedAt(setup, pin, step, changes) : void 0;
34759
34998
  return {
34760
34999
  def,
34761
35000
  workflowName: resolved.name,
@@ -34774,7 +35013,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
34774
35013
  memoryResumed: memory.resumed,
34775
35014
  hints: hintsOf(stepDef),
34776
35015
  cost: costTotals(run.costEntries),
34777
- setup
35016
+ setup,
35017
+ ...shipBlocked !== void 0 ? { shipBlocked } : {}
34778
35018
  };
34779
35019
  });
34780
35020
  const currentRest = restAt(void 0);
@@ -34903,6 +35143,8 @@ const renderRest = (rest) => succeed$2({
34903
35143
  kind: rest.stepDef.kind,
34904
35144
  content: rest.stepDef.content,
34905
35145
  ...rest.hints,
35146
+ ...failedRunStalled(rest) ? { failure: rest.setup.workspace.readSync(".gtd/run-failed.txt") ?? "" } : {},
35147
+ ...rest.shipBlocked !== void 0 ? { blocked: rest.shipBlocked } : {},
34906
35148
  ...rest.memory !== void 0 ? { memory: rest.memory } : {},
34907
35149
  memoryResumed: rest.memoryResumed,
34908
35150
  ...rest.pin !== void 0 ? { pin: rest.pin } : {}
@@ -34911,7 +35153,9 @@ const renderRest = (rest) => succeed$2({
34911
35153
  * Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
34912
35154
  * attempt at this very agent rest, and another dispatch would repeat it.
34913
35155
  */
34914
- const stalledAt = (rest) => rest.changes.length === 0 && rest.stepDef.kind === "prompt" && rest.stepDef.allowEmpty !== true && rest.run.headTurn?.state === rest.state && rest.run.headTurn.actor === rest.actor && !rest.run.headTurn.step && rest.run.headTurn.empty;
35156
+ const stalledAt = (rest) => failedRunStalled(rest) || rest.shipBlocked !== void 0 || rest.changes.length === 0 && rest.stepDef.kind === "prompt" && rest.stepDef.allowEmpty !== true && rest.run.headTurn?.state === rest.state && rest.run.headTurn.actor === rest.actor && !rest.run.headTurn.step && rest.run.headTurn.empty;
35157
+ /** A `run(…, { onFail: "rest" })` step whose last turn recorded a failure, on a clean tree. */
35158
+ const failedRunStalled = (rest) => rest.changes.length === 0 && rest.stepDef.onFail === "rest" && rest.run.headTurn?.state === rest.state && !rest.run.headTurn.step && rest.setup.workspace.readSync(".gtd/run-failed.txt") !== void 0;
34915
35159
  /** No process is underway: the default workflow's first step, first visit — a dirty tree there is a turn not yet landed, not a process. */
34916
35160
  const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
34917
35161
  /** `idle` means exactly one thing: no process underway, clean tree. */
@@ -35076,7 +35320,7 @@ const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "e
35076
35320
  * replay with the working tree as the pending turn. The target step is
35077
35321
  * computed by replaying, never matched.
35078
35322
  */
35079
- const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
35323
+ const decideLanding = (rest, verdicts, branch, approver, shipped) => gen(function* () {
35080
35324
  if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
35081
35325
  const unapproved = approvalRefusal(rest, approver);
35082
35326
  if (unapproved !== void 0) return unapproved;
@@ -35089,30 +35333,183 @@ const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
35089
35333
  message
35090
35334
  };
35091
35335
  }
35336
+ const landing = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef));
35337
+ if (rest.stepDef.onFail === "rest" && landing.read(".gtd/run-failed.txt") !== void 0) return {
35338
+ kind: "attempt",
35339
+ subject: formatSubject(rest.actor, rest.state)
35340
+ };
35092
35341
  const outcome = yield* promise(() => replayFor(rest.setup, {
35093
- tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
35094
- ...verdicts !== void 0 ? { verdicts } : {}
35342
+ tree: landing,
35343
+ ...verdicts !== void 0 ? { verdicts } : {},
35344
+ ...shipped !== void 0 ? { shipped } : {}
35095
35345
  }));
35096
35346
  const to = yield* landingTarget(outcome, rest);
35097
35347
  if (typeof to !== "string") return to;
35098
35348
  return landingTo(rest, to, branch, approver, outcomeMarks(outcome));
35099
35349
  });
35350
+ /** The user's branch: `<b>` for a process on `gtd/<b>` while `<b>` still exists; any other branch as is. */
35351
+ const userBranch = (git) => gen(function* () {
35352
+ const current = yield* git.currentBranch();
35353
+ const name = isSome(current) ? userBranchOf(current.value) : void 0;
35354
+ if (name === void 0) return current;
35355
+ const there = yield* git.readRefOption(`refs/heads/${name}`);
35356
+ return isSome(there) ? some(name) : current;
35357
+ });
35358
+ /** `<b>` for a process on `gtd/<b>` while `<b>` exists: where a finishing landing leaves HEAD. */
35359
+ const returnBranch = gen(function* () {
35360
+ const git = yield* GitService;
35361
+ const current = yield* git.currentBranch();
35362
+ const name = isSome(current) ? userBranchOf(current.value) : void 0;
35363
+ if (name === void 0) return void 0;
35364
+ return isSome(yield* git.readRefOption(`refs/heads/${name}`)) ? name : void 0;
35365
+ });
35366
+ /** A starting landing moves onto `gtd/<b>` first. */
35367
+ const withBranchAndPush = (landing) => gen(function* () {
35368
+ if (landing.kind !== "commit") return landing;
35369
+ let out = landing;
35370
+ if (landing.spec.vars !== void 0) {
35371
+ const start = yield* startBranchFor("gtd land");
35372
+ if (start.kind === "refusal") return {
35373
+ kind: "refusal",
35374
+ message: start.message
35375
+ };
35376
+ if (start.kind === "switch") out = {
35377
+ ...out,
35378
+ startBranch: start.branch
35379
+ };
35380
+ }
35381
+ return yield* withHistory(out);
35382
+ });
35383
+ /** A closing landing switches back to `<b>`; it and a shipping one push their history ref. */
35384
+ const withHistory = (landing) => gen(function* () {
35385
+ let out = landing;
35386
+ const back = landing.keepHistory === void 0 ? void 0 : yield* returnBranch;
35387
+ if (back !== void 0) out = {
35388
+ ...out,
35389
+ leaveBranch: back
35390
+ };
35391
+ if (landing.keepHistory === void 0 && landing.ship === void 0) return out;
35392
+ const remote = (yield* (yield* ConfigService).load).historyRemote;
35393
+ return remote === void 0 ? out : {
35394
+ ...out,
35395
+ pushHistory: remote
35396
+ };
35397
+ });
35100
35398
  /** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
35101
- const previewLanding = (rest) => decideLanding(rest, void 0, none(), none()).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$2(void 0)));
35399
+ const previewLanding = (rest) => decideLanding(rest, void 0, none(), none(), rest.step.request.kind === "ship" ? SHIPPED_STAND_IN : void 0).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$2(void 0)));
35400
+ const snapshotOf = (rest, landing) => ({
35401
+ stepDef: rest.stepDef,
35402
+ state: rest.state,
35403
+ actor: rest.actor,
35404
+ changes: rest.changes,
35405
+ file: rest.hints.file,
35406
+ landing
35407
+ });
35408
+ const refusedSnapshot = (rest, message) => snapshotOf(rest, {
35409
+ kind: "refusal",
35410
+ message
35411
+ });
35412
+ const SHIPPED_STAND_IN = "0".repeat(40);
35413
+ const SHIP_EXCLUDED = ".gtd/";
35414
+ const shippedPaths = (before, after) => {
35415
+ const diff = diffTrees(before, after);
35416
+ return [
35417
+ ...diff.added,
35418
+ ...diff.modified,
35419
+ ...diff.deleted
35420
+ ].filter((path) => !path.startsWith(SHIP_EXCLUDED));
35421
+ };
35422
+ const noTarget = (why) => ({
35423
+ kind: "skip",
35424
+ note: `gtd: no branch to ship to: ${why}`
35425
+ });
35426
+ /** `<b>` and its tip, for a process on `gtd/<b>`; why there is none otherwise. */
35427
+ const shipTarget = (git) => gen(function* () {
35428
+ const current = yield* git.currentBranch();
35429
+ if (isNone(current)) return noTarget("HEAD is detached");
35430
+ const branch = userBranchOf(current.value);
35431
+ if (branch === void 0) return noTarget(`${current.value} is no gtd/ branch`);
35432
+ if (userBranchOf(branch) !== void 0) return noTarget(`${current.value} records no user branch`);
35433
+ const parent = yield* git.readRefOption(`refs/heads/${branch}`);
35434
+ if (isNone(parent)) return noTarget(`${branch} does not exist`);
35435
+ return {
35436
+ kind: "ship",
35437
+ branch,
35438
+ parent: parent.value
35439
+ };
35440
+ });
35441
+ /**
35442
+ * The paths `<b>` changed outside this process that shipping `tree` would
35443
+ * revert. A path the working tree changed since HEAD counts as brought in:
35444
+ * whether it carries `<b>`'s change is the person's call, not a merge gtd redoes.
35445
+ */
35446
+ const revertedPaths = (git, workspace, parent, tree, changed) => gen(function* () {
35447
+ const head = yield* git.resolveRef("HEAD");
35448
+ const shipped = parseCommitMessage(yield* git.lastCommitMessage(parent)).tip;
35449
+ if (shipped !== void 0 && (yield* git.isAncestor(shipped, head))) return [];
35450
+ const fork = yield* git.mergeBase(parent, head);
35451
+ const foreign = isNone(fork) ? [...changed] : shippedPaths(commitTree(workspace, fork.value), commitTree(workspace, parent));
35452
+ const committed = commitTree(workspace, head);
35453
+ return foreign.filter((path) => changed.has(path) && tree.read(path) === committed.read(path));
35454
+ });
35455
+ /**
35456
+ * Whether landing a ship step with `tree` would ship, and onto what. The squash
35457
+ * replaces the user's branch's tree with `tree`, so it is blocked while that
35458
+ * branch carries a change `tree` would revert: one made since the last ship
35459
+ * (whose `Gtd-Tip` HEAD descends from), or since it forked from HEAD.
35460
+ */
35461
+ const decideShip = (workspace, tree) => gen(function* () {
35462
+ const git = yield* GitService;
35463
+ const target = yield* shipTarget(git);
35464
+ if (target.kind !== "ship") return target;
35465
+ const { branch, parent } = target;
35466
+ const changed = new Set(shippedPaths(commitTree(workspace, parent), tree));
35467
+ const reverted = yield* revertedPaths(git, workspace, parent, tree, changed);
35468
+ if (reverted.length > 0) return {
35469
+ kind: "blocked",
35470
+ message: `gtd: ${branch} changed outside this process, and shipping would revert ${reverted.join(", ")} — bring those changes into the working tree (\`git merge --squash --no-commit ${branch}\`, or by hand) and land`
35471
+ };
35472
+ return changed.size === 0 ? {
35473
+ kind: "skip",
35474
+ note: `gtd: nothing to ship: ${branch} already has this tree`
35475
+ } : target;
35476
+ });
35477
+ const shipBlockedAt = (setup, pin, step, changes) => gen(function* () {
35478
+ if (pin !== void 0 || step.request.kind !== "ship" || changes.length > 0) return void 0;
35479
+ const head = yield* (yield* GitService).resolveRef("HEAD");
35480
+ const decision = yield* decideShip(setup.workspace, commitTree(setup.workspace, head));
35481
+ return decision.kind === "blocked" ? decision.message : void 0;
35482
+ });
35483
+ const shipSnapshot = (rest, message, verdicts, branch, approver) => gen(function* () {
35484
+ const tree = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), void 0);
35485
+ const decision = yield* decideShip(rest.setup.workspace, tree);
35486
+ if (decision.kind === "blocked") return refusedSnapshot(rest, decision.message);
35487
+ const shipping = decision.kind === "ship";
35488
+ const decided = yield* decideLanding(rest, verdicts, branch, approver, shipping ? SHIPPED_STAND_IN : void 0);
35489
+ if (decided.kind !== "commit") return snapshotOf(rest, decided);
35490
+ const landing = shipping ? {
35491
+ ...decided,
35492
+ ship: {
35493
+ branch: decision.branch,
35494
+ parent: decision.parent,
35495
+ shipMessage: message,
35496
+ historyRef: processHistoryRef(rest.run.startHash)
35497
+ }
35498
+ } : {
35499
+ ...decided,
35500
+ note: decision.note
35501
+ };
35502
+ return snapshotOf(rest, yield* withBranchAndPush(landing));
35503
+ });
35102
35504
  /** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
35103
35505
  const snapshotFromRest = (rest, verdicts) => gen(function* () {
35104
35506
  const git = yield* GitService;
35105
- const branch = yield* git.currentBranch();
35507
+ const branch = yield* userBranch(git);
35106
35508
  const approver = rest.stepDef.approve === true ? yield* git.authorIdent() : none();
35107
- const landing = yield* decideLanding(rest, verdicts, branch, approver);
35108
- return {
35109
- stepDef: rest.stepDef,
35110
- state: rest.state,
35111
- actor: rest.actor,
35112
- changes: rest.changes,
35113
- file: rest.hints.file,
35114
- landing
35115
- };
35509
+ const request = rest.step.request;
35510
+ if (rest.pin === void 0 && request.kind === "ship") return yield* shipSnapshot(rest, request.message, verdicts, branch, approver);
35511
+ const decided = yield* decideLanding(rest, verdicts, branch, approver);
35512
+ return snapshotOf(rest, yield* withBranchAndPush(decided));
35116
35513
  });
35117
35514
  const humanCommitsOf = (trace, entryCommit) => trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
35118
35515
  hash: entry.hash,
@@ -36248,6 +36645,32 @@ const runStartListCommand = (json, out) => gen(function* () {
36248
36645
  out.write([...workflows.map((name) => `workflow ${name}\n`), ...doors.map(([n, d]) => `door ${doorSynopsis(n, d)} → ${d.workflow}\n`)].join(""));
36249
36646
  });
36250
36647
  /**
36648
+ * Back on `<b>` when the process ran on `gtd/<b>` and `<b>` moved only by its
36649
+ * ships (whose `Gtd-Tip` the tip descends from): the rewind stops at the last
36650
+ * ship, so the uncommitted changes are what it left unshipped.
36651
+ */
36652
+ const abandonTarget = (current, tip, startParent) => gen(function* () {
36653
+ const git = yield* GitService;
36654
+ const userBranch = isSome(current) ? userBranchOf(current.value) : void 0;
36655
+ const userTip = userBranch === void 0 ? none() : yield* git.readRefOption(`refs/heads/${userBranch}`);
36656
+ if (userBranch === void 0 || isNone(userTip)) return {
36657
+ leaves: void 0,
36658
+ rewindTo: startParent
36659
+ };
36660
+ if (userTip.value === startParent) return {
36661
+ leaves: userBranch,
36662
+ rewindTo: startParent
36663
+ };
36664
+ const shipped = parseCommitMessage(yield* git.lastCommitMessage(userTip.value)).tip;
36665
+ return shipped !== void 0 && (yield* git.isAncestor(shipped, tip)) ? {
36666
+ leaves: userBranch,
36667
+ rewindTo: userTip.value
36668
+ } : {
36669
+ leaves: void 0,
36670
+ rewindTo: startParent
36671
+ };
36672
+ });
36673
+ /**
36251
36674
  * Ends the process underway without completing it. Nothing is discarded: a
36252
36675
  * `git reset --mixed` to the process's start commit leaves every turn
36253
36676
  * commit's content in the working tree.
@@ -36262,7 +36685,8 @@ const runStartListCommand = (json, out) => gen(function* () {
36262
36685
  */
36263
36686
  const runAbandonCommand = (json, out) => gen(function* () {
36264
36687
  const git = yield* GitService;
36265
- const initial = (yield* (yield* ConfigService).load).workflow.initial;
36688
+ const config = yield* (yield* ConfigService).load;
36689
+ const initial = config.workflow.initial;
36266
36690
  const run = yield* currentRun;
36267
36691
  if (run.trace.length === 0) {
36268
36692
  const required = emitScripts([{
@@ -36274,6 +36698,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
36274
36698
  const restState = run.trace[run.trace.length - 1].state;
36275
36699
  if (run.startParentHash === EMPTY_TREE) return yield* fail$8(/* @__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`));
36276
36700
  const tip = yield* git.resolveRef("HEAD");
36701
+ const historyRef = processHistoryRef(run.startHash);
36702
+ const current = yield* git.currentBranch();
36703
+ const { leaves, rewindTo } = yield* abandonTarget(current, tip, run.startParentHash);
36704
+ const remote = config.historyRemote;
36277
36705
  const steps = [
36278
36706
  {
36279
36707
  kind: "gitWrite",
@@ -36281,21 +36709,60 @@ const runAbandonCommand = (json, out) => gen(function* () {
36281
36709
  },
36282
36710
  {
36283
36711
  kind: "gitWrite",
36284
- command: mixedResetTo(run.startParentHash)
36712
+ command: updateRef(historyRef, tip)
36713
+ },
36714
+ {
36715
+ kind: "gitWrite",
36716
+ command: mixedResetTo(rewindTo)
36285
36717
  },
36286
36718
  {
36287
36719
  kind: "gitWrite",
36288
36720
  command: unpinScript(PIN_PATH)
36289
36721
  },
36722
+ ...leaves !== void 0 ? [
36723
+ {
36724
+ kind: "gitWrite",
36725
+ command: switchTo(leaves)
36726
+ },
36727
+ {
36728
+ kind: "gitWrite",
36729
+ command: deleteBranch(getOrThrow$1(current))
36730
+ },
36731
+ {
36732
+ kind: "gitWrite",
36733
+ command: updateRef(RESUME_BRANCH_REF, tip)
36734
+ }
36735
+ ] : [{
36736
+ kind: "gitWrite",
36737
+ command: deleteRef(RESUME_BRANCH_REF)
36738
+ }],
36290
36739
  {
36291
36740
  kind: "outcome",
36292
- command: abandonedOutcome(restState, run.startParentHash, initial)
36293
- }
36741
+ command: abandonedOutcome(restState, rewindTo, initial)
36742
+ },
36743
+ ...remote !== void 0 ? [{
36744
+ kind: "gitWrite",
36745
+ command: pushRef(remote, historyRef)
36746
+ }] : []
36294
36747
  ];
36295
36748
  const required = emitScripts(steps).required;
36296
36749
  yield* writeScript(json, out, combinedScript(required, ""));
36297
36750
  });
36298
36751
  /**
36752
+ * `gtd/<b>` when the abandon left it for `<b>` and HEAD is still on `<b>`. A
36753
+ * `gtd/<b>` made since is reset only when the retained tip contains it — the
36754
+ * `-C` switch would orphan anything else.
36755
+ */
36756
+ const resumeBranch = (git, tip) => gen(function* () {
36757
+ if (isNone(yield* git.readRefOption("refs/worktree/gtd/history-branch"))) return void 0;
36758
+ const current = yield* git.currentBranch();
36759
+ if (isNone(current) || current.value.startsWith("gtd/")) return void 0;
36760
+ const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
36761
+ const existing = yield* git.readRefOption(`refs/heads/${branch}`);
36762
+ if (isSome(existing) && !(yield* git.isAncestor(existing.value, tip))) return yield* fail$8(/* @__PURE__ */ new Error(`gtd restore: branch ${branch} holds commits the retained tip ${tip.slice(0, 7)} does not — merge or delete it first`));
36763
+ return branch;
36764
+ });
36765
+ /**
36299
36766
  * `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
36300
36767
  *
36301
36768
  * Guarded by `restorability` so it never discards work it didn't create:
@@ -36312,15 +36779,20 @@ const runRestoreCommand = (json, out) => gen(function* () {
36312
36779
  const check = yield* restorability(git, headHash, tip);
36313
36780
  if (!check.ok) return yield* fail$8(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
36314
36781
  const after = yield* restAt(tip);
36782
+ const resumeOn = yield* resumeBranch(git, tip);
36315
36783
  const steps = [
36316
36784
  {
36317
36785
  kind: "gitWrite",
36318
- command: hardResetTo(tip)
36786
+ command: resumeOn === void 0 ? hardResetTo(tip) : switchBranchTo(resumeOn, tip)
36319
36787
  },
36320
36788
  {
36321
36789
  kind: "gitWrite",
36322
36790
  command: deleteRef(HISTORY_REF)
36323
36791
  },
36792
+ {
36793
+ kind: "gitWrite",
36794
+ command: deleteRef(RESUME_BRANCH_REF)
36795
+ },
36324
36796
  {
36325
36797
  kind: "outcome",
36326
36798
  command: restoredOutcome(tip, after.state)
@@ -37036,7 +37508,7 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
37036
37508
  },
37037
37509
  group: "Start and stop",
37038
37510
  summary: "Start a workflow or door; bare, list them",
37039
- details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
37511
+ details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds. The process runs on the branch \`gtd/<b>\` (created, or reused when nothing on it would be lost), never on \`<b>\`; a branch already named \`gtd/…\` and a detached HEAD start in place.
37040
37512
 
37041
37513
  <name> is a door — a named shortcut whose positional args set the workflow's settings — or else a workflow the config exports, which takes no args. A door shadows a workflow of the same name. --var pins a setting either way.
37042
37514
 
@@ -37050,7 +37522,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
37050
37522
  arity: "none",
37051
37523
  group: "Start and stop",
37052
37524
  summary: "Stop the process, keeping its changes",
37053
- details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from and keeps everything it produced as uncommitted changes; \`gtd restore\` undoes it. With no process underway the script does nothing.
37525
+ details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from, or to its last ship, and keeps everything it produced as uncommitted changes, switches back to \`<b>\` and deletes \`gtd/<b>\`; it keeps the abandoned tip at \`refs/gtd/history/<entry>\` (pushed to \`historyRemote\` when set). \`gtd restore\` undoes it. With no process underway the script does nothing.
37054
37526
 
37055
37527
  s=$(gtd abandon --json=script) && sh -c "$s"`
37056
37528
  },
@@ -37060,7 +37532,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
37060
37532
  arity: "none",
37061
37533
  group: "Start and stop",
37062
37534
  summary: "Bring back the last abandoned process",
37063
- details: `Bring back the process the last \`gtd abandon\` stopped: the printed script resets HEAD to that process's last commit. Refuses on a dirty working tree, when nothing was abandoned, or when commits made since would be lost.`
37535
+ details: `Bring back the process the last \`gtd abandon\` stopped: the printed script puts that process's last commit back, on a recreated \`gtd/<b>\` when the process ran on one. Refuses on a dirty working tree, when nothing was abandoned, or when commits made since would be lost.`
37064
37536
  },
37065
37537
  {
37066
37538
  token: "repin",
@@ -37078,7 +37550,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
37078
37550
  arity: "none",
37079
37551
  group: "Inspect",
37080
37552
  summary: "Print the prompt for the closing message",
37081
- details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside: its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
37553
+ details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside (back on a user branch, the one its gtd/ branch closes): its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
37082
37554
 
37083
37555
  Refuses when the workflow declares no summary template, or when the process has no commits.`
37084
37556
  },