@pmelab/gtd 25.0.0 → 26.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.
Files changed (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +7 -0
  3. package/dist/flows.mjs +15 -1
  4. package/dist/gtd.bundle.mjs +161 -20
  5. package/dist/testkit.mjs +311 -57
  6. package/package.json +2 -1
  7. package/src/cli/Cli.test.ts +28 -3
  8. package/src/cli/Cli.ts +12 -1
  9. package/src/cli/History.test.ts +2 -2
  10. package/src/cli/Program.test.ts +10 -12
  11. package/src/edge/Edge.test.ts +113 -16
  12. package/src/edge/Edge.ts +22 -6
  13. package/src/emit/GitScript.test.ts +65 -2
  14. package/src/emit/GitScript.ts +34 -1
  15. package/src/emit/index.ts +1 -0
  16. package/src/flags/index.test.ts +67 -0
  17. package/src/flags/index.ts +38 -0
  18. package/src/flows/runtime.test.ts +33 -0
  19. package/src/flows/runtime.ts +31 -0
  20. package/src/platform/RetainedHistory.test.ts +9 -0
  21. package/src/platform/RetainedHistory.ts +3 -0
  22. package/src/platform/Workspace.test.ts +1 -1
  23. package/src/platform/index.ts +1 -0
  24. package/src/replay/RefName.test.ts +40 -0
  25. package/src/replay/RefName.ts +19 -0
  26. package/src/replay/Replay.test.ts +193 -2
  27. package/src/replay/Replay.ts +51 -6
  28. package/src/replay/Trailers.test.ts +26 -0
  29. package/src/replay/Trailers.ts +8 -0
  30. package/src/replay/index.ts +1 -0
  31. package/src/step/LandStep.ts +1 -0
  32. package/src/step/RepoSnapshot.ts +2 -0
  33. package/src/step/planStart.test.ts +5 -5
  34. package/src/step/planStep.test.ts +53 -0
  35. package/src/step/planStep.ts +7 -1
  36. package/src/testkit/EmittedScriptRecognizer.test.ts +57 -29
  37. package/src/testkit/EmittedScriptRecognizer.ts +26 -4
  38. package/src/testkit/EmittedScripts.test.ts +182 -0
  39. package/src/testkit/FakeGitOperations.ts +8 -2
  40. package/src/testkit/GitTiers.ts +103 -24
  41. package/src/testkit/InMemRepo.ts +57 -12
  42. package/src/testkit/TestRepo.ts +2 -2
  43. package/src/workflow/Workflow.ts +1 -0
  44. package/src/workflow/discovery.test.ts +1 -1
  45. package/src/workflow/vars.test.ts +16 -0
  46. package/src/workflow/vars.ts +4 -1
  47. package/src/workflows/shared/text.fixture.ts +1 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "25.0.0",
3
+ "version": "26.1.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -28,6 +28,8 @@ Or run without installing (prefix every `gtd` below with `npx`) — see
28
28
  for the settings most projects tune. A process setting's value is committed to
29
29
  Git history; keep secrets in an environment setting (see
30
30
  [Settings](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#settings)).
31
+ `GTD_FLAG_<NAME>` variables toggle experimental features — see
32
+ [Feature flags](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#feature-flags).
31
33
 
32
34
  Also install all three skill sources the bundled workflow names in its prompts —
33
35
  none is optional. See
@@ -574,6 +576,11 @@ git config --add remote.origin.fetch 'refs/gtd/history/*:refs/gtd/history/*'
574
576
  The second line makes every plain `git fetch` include them. It has no leading
575
577
  `+`, so a conflicting local ref fails instead of being overwritten.
576
578
 
579
+ A step declared with `keep: true` files its turn under `refs/gtd/kept/` instead
580
+ of on the branch (see [Kept tips](docs/configuration.md#kept-tips)). Those refs
581
+ are not process history: `gtd history` does not list them, and the
582
+ `refs/gtd/history/*` refspecs above neither push nor fetch them.
583
+
577
584
  ## Entrypoints
578
585
 
579
586
  gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
package/dist/flows.mjs CHANGED
@@ -109,6 +109,19 @@ const changesSince = (hash, pattern) => {
109
109
  const all = context.changesSince(hash);
110
110
  return toChanges(pattern === void 0 ? all : all.filter((c) => context.matches(c.path, pattern)));
111
111
  };
112
+ /**
113
+ * The latest kept tip of step `name` (scoped from here, same as `agent()`) in
114
+ * this episode, or `undefined` when none.
115
+ */
116
+ const kept = (name) => {
117
+ const context = ctx();
118
+ if (typeof context.kept !== "function") throw new Error("gtd: kept() needs a newer gtd — upgrade the gtd that runs it");
119
+ const tip = context.kept(name);
120
+ return tip === void 0 ? void 0 : {
121
+ hash: tip.hash,
122
+ changes: toChanges(tip.changes)
123
+ };
124
+ };
112
125
  /** The commit the process stands on at this point of the flow. */
113
126
  const head = () => ctx().head();
114
127
  /** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
@@ -502,6 +515,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
502
515
  installContext: () => installContext,
503
516
  isFactoryWorkflow: () => isFactoryWorkflow,
504
517
  judge: () => judge,
518
+ kept: () => kept,
505
519
  moveScript: () => moveScript,
506
520
  numeric: () => numeric,
507
521
  openQuestions: () => openQuestions,
@@ -530,4 +544,4 @@ var flows_exports = /* @__PURE__ */ __exportAll({
530
544
  wrote: () => wrote
531
545
  });
532
546
  //#endregion
533
- export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
547
+ export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, kept, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
@@ -20943,6 +20943,8 @@ const HISTORY_REF = "refs/worktree/gtd/history";
20943
20943
  /** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
20944
20944
  const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
20945
20945
  const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
20946
+ /** Directory of a step's turns set aside by `keep: true`, each at `<this>/<kept hash>`; own namespace, so history list/show/fetch never see it. */
20947
+ const keptRef = (entry, step) => `refs/gtd/kept/${entry}/${step}`;
20946
20948
  const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
20947
20949
  /**
20948
20950
  * Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
@@ -20957,6 +20959,26 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
20957
20959
  };
20958
20960
  });
20959
20961
  //#endregion
20962
+ //#region src/flags/index.ts
20963
+ const FLAGS$1 = [];
20964
+ const BOOL = {
20965
+ "": false,
20966
+ "0": false,
20967
+ false: false,
20968
+ "1": true,
20969
+ true: true
20970
+ };
20971
+ const resolveFlags = (env, registry = FLAGS$1) => {
20972
+ const out = {};
20973
+ for (const { name } of registry) {
20974
+ const variable = `GTD_FLAG_${name.toUpperCase()}`;
20975
+ const key = (env[variable] ?? "").toLowerCase();
20976
+ if (!Object.hasOwn(BOOL, key)) throw new Error(`${variable} must be 1, 0, true or false`);
20977
+ out[name] = BOOL[key];
20978
+ }
20979
+ return out;
20980
+ };
20981
+ //#endregion
20960
20982
  //#region src/install/Version.ts
20961
20983
  const _require = createRequire(import.meta.url);
20962
20984
  const findPackageJson = (fromUrl) => {
@@ -21695,6 +21717,19 @@ const changesSince = (hash, pattern) => {
21695
21717
  const all = context.changesSince(hash);
21696
21718
  return toChanges(pattern === void 0 ? all : all.filter((c) => context.matches(c.path, pattern)));
21697
21719
  };
21720
+ /**
21721
+ * The latest kept tip of step `name` (scoped from here, same as `agent()`) in
21722
+ * this episode, or `undefined` when none.
21723
+ */
21724
+ const kept = (name) => {
21725
+ const context = ctx();
21726
+ if (typeof context.kept !== "function") throw new Error("gtd: kept() needs a newer gtd — upgrade the gtd that runs it");
21727
+ const tip = context.kept(name);
21728
+ return tip === void 0 ? void 0 : {
21729
+ hash: tip.hash,
21730
+ changes: toChanges(tip.changes)
21731
+ };
21732
+ };
21698
21733
  /** The commit the process stands on at this point of the flow. */
21699
21734
  const head = () => ctx().head();
21700
21735
  /** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
@@ -22088,6 +22123,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
22088
22123
  installContext: () => installContext,
22089
22124
  isFactoryWorkflow: () => isFactoryWorkflow,
22090
22125
  judge: () => judge,
22126
+ kept: () => kept,
22091
22127
  moveScript: () => moveScript,
22092
22128
  numeric: () => numeric,
22093
22129
  openQuestions: () => openQuestions,
@@ -37471,8 +37507,24 @@ var init_wire = __esmMin((() => {
37471
37507
  init_Glob();
37472
37508
  }));
37473
37509
  //#endregion
37474
- //#region src/replay/Tree.ts
37510
+ //#region src/replay/RefName.ts
37475
37511
  init_wire();
37512
+ const FORBIDDEN = /[\u0000- \u007f~^:?*[\\]/;
37513
+ /** Why `name` cannot be (part of) a git ref name, or `undefined` when it can. */
37514
+ const refNameFault = (name) => {
37515
+ if (name === "") return "it is empty";
37516
+ if (FORBIDDEN.test(name)) return "it contains a space, control character or one of ~^:?*[\\";
37517
+ if (name.includes("..")) return "it contains \"..\"";
37518
+ if (name.includes("@{")) return "it contains \"@{\"";
37519
+ if (name.endsWith(".")) return "it ends with \".\"";
37520
+ for (const part of name.split("/")) {
37521
+ if (part === "") return "it has an empty \"/\" component";
37522
+ if (part.startsWith(".")) return "a component starts with \".\"";
37523
+ if (part.endsWith(".lock")) return "a component ends with \".lock\"";
37524
+ }
37525
+ };
37526
+ //#endregion
37527
+ //#region src/replay/Tree.ts
37476
37528
  const treeFromRecord = (files) => ({
37477
37529
  paths: () => Object.keys(files).sort(),
37478
37530
  read: (path) => Object.hasOwn(files, path) ? files[path] : void 0
@@ -37566,6 +37618,7 @@ const decodePath = (raw) => {
37566
37618
  return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
37567
37619
  };
37568
37620
  const FORMAT_RE = /^[1-9][0-9]*$/;
37621
+ const HASH_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
37569
37622
  const TRAILER_READERS = {
37570
37623
  "Gtd-Step": (value, into) => {
37571
37624
  into.step ??= parseStepId(value);
@@ -37595,6 +37648,9 @@ const TRAILER_READERS = {
37595
37648
  "Gtd-Version": (value, into) => {
37596
37649
  if (value !== "") into.version ??= value;
37597
37650
  },
37651
+ "Gtd-Kept": (value, into) => {
37652
+ if (HASH_RE.test(value)) into.kept ??= value;
37653
+ },
37598
37654
  "Gtd-Format": (value, into) => {
37599
37655
  if (FORMAT_RE.test(value)) into.format ??= Number(value);
37600
37656
  },
@@ -37615,6 +37671,7 @@ const parseCommitMessage = (message) => {
37615
37671
  reviewBase: void 0,
37616
37672
  branch: void 0,
37617
37673
  version: void 0,
37674
+ kept: void 0,
37618
37675
  judge: [],
37619
37676
  vars: {},
37620
37677
  config: {},
@@ -37729,7 +37786,8 @@ const KNOWN_OPTIONS = {
37729
37786
  ...STEERING_OPTIONS,
37730
37787
  "model",
37731
37788
  "system",
37732
- "allowEmpty"
37789
+ "allowEmpty",
37790
+ "keep"
37733
37791
  ]),
37734
37792
  human: /* @__PURE__ */ new Set([
37735
37793
  ...STEERING_OPTIONS,
@@ -37761,6 +37819,7 @@ const replay = async (input) => {
37761
37819
  const scopeCallIds = [];
37762
37820
  let scopeCallCount = 0;
37763
37821
  const trace = [];
37822
+ const keptTips = /* @__PURE__ */ new Map();
37764
37823
  let landed;
37765
37824
  let outcome;
37766
37825
  let signal;
@@ -37920,6 +37979,12 @@ const replay = async (input) => {
37920
37979
  }
37921
37980
  cursor++;
37922
37981
  advance(commit);
37982
+ if (commit.parsed.kept !== void 0) if (commit.kept === void 0) keptTips.delete(step.name);
37983
+ else keptTips.set(step.name, {
37984
+ hash: commit.kept.hash,
37985
+ before: commit.tree,
37986
+ after: commit.kept.tree
37987
+ });
37923
37988
  const to = commit.parsed.parsed?.to;
37924
37989
  if (to !== void 0) expectedNext = {
37925
37990
  name: to,
@@ -37927,6 +37992,21 @@ const replay = async (input) => {
37927
37992
  };
37928
37993
  return replyFor(step.request, commit.parsed.judge);
37929
37994
  };
37995
+ const landPending = (step, pending) => {
37996
+ pendingUsed = true;
37997
+ landed = step;
37998
+ const keeping = step.request.kind === "agent" && step.request.options.keep === true;
37999
+ if (keeping) keptTips.set(step.name, {
38000
+ hash: "",
38001
+ before: position.tree,
38002
+ after: pending.tree
38003
+ });
38004
+ advance({
38005
+ hash: "",
38006
+ tree: keeping ? position.tree : pending.tree
38007
+ });
38008
+ return replyFor(step.request, pending.verdicts ?? []);
38009
+ };
37930
38010
  const answer = (step) => {
37931
38011
  while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) cursor++;
37932
38012
  const commit = commits[cursor];
@@ -37937,17 +38017,18 @@ const replay = async (input) => {
37937
38017
  trace,
37938
38018
  landed
37939
38019
  });
37940
- pendingUsed = true;
37941
- landed = step;
37942
- advance({
37943
- hash: "",
37944
- tree: input.pending.tree
37945
- });
37946
- return replyFor(step.request, input.pending.verdicts ?? []);
38020
+ return landPending(step, input.pending);
37947
38021
  };
37948
38022
  const requestFault = (request) => {
37949
38023
  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)`;
37950
- return request.kind === "agent" ? scopeAccessFault() : void 0;
38024
+ if (request.kind !== "agent") return void 0;
38025
+ const name = scoped(request.name);
38026
+ if (request.options.keep === true) {
38027
+ 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`;
38028
+ const refFault = name.includes("/") ? "it contains \"/\"" : refNameFault(name);
38029
+ if (refFault !== void 0) return `gtd: step "${name}": keep: true files the turn under a git ref named after the step, but ${refFault}`;
38030
+ }
38031
+ return scopeAccessFault();
37951
38032
  };
37952
38033
  const handleStep = async (request) => {
37953
38034
  if (outcome !== void 0) throw new Stop();
@@ -38030,6 +38111,13 @@ const replay = async (input) => {
38030
38111
  faults: findings.filter((f) => f.message.startsWith(`Code thread at ${c.path}:${t.line + 1}: `)).map((f) => f.message)
38031
38112
  }));
38032
38113
  }),
38114
+ kept: (name) => {
38115
+ const tip = keptTips.get(scoped(name));
38116
+ return tip === void 0 ? void 0 : {
38117
+ hash: tip.hash,
38118
+ changes: changesBetween(tip.before, tip.after)
38119
+ };
38120
+ },
38033
38121
  vars: input.vars,
38034
38122
  env: input.env,
38035
38123
  start: () => input.start,
@@ -38131,7 +38219,9 @@ const resolveVars = (workflowVars, entryVars, env) => {
38131
38219
  ...entryVars
38132
38220
  };
38133
38221
  for (const name of Object.keys(merged)) {
38134
- const value = env[PREFIX + name.toUpperCase()];
38222
+ const key = PREFIX + name.toUpperCase();
38223
+ if (key.startsWith("GTD_FLAG_")) continue;
38224
+ const value = env[key];
38135
38225
  if (value !== void 0) merged[name] = value;
38136
38226
  }
38137
38227
  return merged;
@@ -38997,7 +39087,11 @@ const planStep = (snapshot, opts = {}) => {
38997
39087
  ...uncheckStep(snapshot),
38998
39088
  {
38999
39089
  kind: "gitWrite",
39000
- write: {
39090
+ write: landing.keepTurn !== void 0 ? {
39091
+ kind: "keepTurn",
39092
+ ref: landing.keepTurn,
39093
+ message
39094
+ } : {
39001
39095
  kind: "commitAll",
39002
39096
  message
39003
39097
  }
@@ -39477,6 +39571,7 @@ const promptDef = (common, request, skills, access) => ({
39477
39571
  ...optional$1("model", request.options.model),
39478
39572
  ...optional$1("system", request.options.system),
39479
39573
  ...optional$1("allowEmpty", request.options.allowEmpty),
39574
+ ...optional$1("keep", request.options.keep),
39480
39575
  ...optional$1("skills", skills),
39481
39576
  ...optional$1("access", access)
39482
39577
  });
@@ -39591,11 +39686,21 @@ const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(func
39591
39686
  hash: run.episode.base,
39592
39687
  tree: commitTree(workspace, run.episode.base)
39593
39688
  };
39594
- const episode = run.episode.commits.map((c) => ({
39595
- hash: c.hash,
39596
- message: c.message,
39597
- tree: commitTree(workspace, c.hash)
39598
- }));
39689
+ const git = yield* GitService;
39690
+ const episode = [];
39691
+ for (const c of run.episode.commits) {
39692
+ const tip = parseCommitMessage(c.message).kept;
39693
+ const exists = tip !== void 0 && isSome(yield* git.readRefOption(`${tip}^{commit}`));
39694
+ episode.push({
39695
+ hash: c.hash,
39696
+ message: c.message,
39697
+ tree: commitTree(workspace, c.hash),
39698
+ ...exists ? { kept: {
39699
+ hash: tip,
39700
+ tree: commitTree(workspace, tip)
39701
+ } } : {}
39702
+ });
39703
+ }
39599
39704
  let pendingMemo;
39600
39705
  return {
39601
39706
  resolved,
@@ -39771,6 +39876,7 @@ const closingTrailers = (branch) => ({
39771
39876
  ...isSome(branch) ? { branch: branch.value } : {}
39772
39877
  });
39773
39878
  const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
39879
+ const keptTurn = (rest) => rest.stepDef.keep === true && rest.changes.length > 0 ? { keepTurn: keptRef(rest.run.startHash, rest.state) } : {};
39774
39880
  const landingTo = (rest, to, branch) => {
39775
39881
  if (isSettledNoop(rest, to)) return {
39776
39882
  kind: "noop",
@@ -39795,7 +39901,8 @@ const landingTo = (rest, to, branch) => {
39795
39901
  } : {},
39796
39902
  ...finishes ? closingTrailers(branch) : {}
39797
39903
  },
39798
- ...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {}
39904
+ ...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {},
39905
+ ...keptTurn(rest)
39799
39906
  };
39800
39907
  };
39801
39908
  /**
@@ -39932,7 +40039,7 @@ const commitAllowEmpty = (message) => {
39932
40039
  return [
39933
40040
  `if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
39934
40041
  ` case "$out" in`,
39935
- ` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} ;;`,
40042
+ ` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} || exit 1 ;;`,
39936
40043
  ` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
39937
40044
  ` esac`,
39938
40045
  `fi`
@@ -39942,6 +40049,36 @@ const commitAllowEmpty = (message) => {
39942
40049
  const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
39943
40050
  const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
39944
40051
  const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
40052
+ /**
40053
+ * POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
40054
+ * tree, so no hook runs on it and its tree is the pre-turn tree by
40055
+ * construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
40056
+ * `-S`, read (and failing closed) before any object or ref is written; the
40057
+ * index is already staged by then, so an early exit leaves the turn staged.
40058
+ * The kept ref is deleted only after `reset -q` succeeds: it is then the
40059
+ * turn's only handle.
40060
+ * The branch moves before the final hard reset, so if that reset fails the
40061
+ * marker has landed and the turn is still in the working tree (kept ref
40062
+ * present); every other failure leaves the branch untouched.
40063
+ */
40064
+ const keepTurn = (refPrefix, message) => {
40065
+ const keptRef = `${shellQuote$1(`${refPrefix}/`)}"$gtd_kept"`;
40066
+ return [
40067
+ `git add -A || exit 1`,
40068
+ `gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
40069
+ `if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
40070
+ `gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
40071
+ `gtd_tree=$(git write-tree) || exit 1`,
40072
+ `gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
40073
+ `git update-ref ${keptRef} "$gtd_kept" || exit 1`,
40074
+ `gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
40075
+ `if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref HEAD "$gtd_tip" "$gtd_head"; then`,
40076
+ ` git reset -q && git update-ref -d ${keptRef}`,
40077
+ ` exit 1`,
40078
+ `fi`,
40079
+ `git reset --hard HEAD`
40080
+ ].join("\n");
40081
+ };
39945
40082
  const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
39946
40083
  /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
39947
40084
  const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
@@ -39974,6 +40111,7 @@ const renderGitWrite = (write) => {
39974
40111
  switch (write.kind) {
39975
40112
  case "commitAll": return commitAll(write.message);
39976
40113
  case "updateRef": return updateRef(write.ref, write.target);
40114
+ case "keepTurn": return keepTurn(write.ref, write.message);
39977
40115
  }
39978
40116
  };
39979
40117
  /**
@@ -42051,7 +42189,10 @@ const runCli = (argv, io) => {
42051
42189
  io.exit(2);
42052
42190
  });
42053
42191
  const out = bufferedArtifactOut(io);
42054
- return runCommand(plan.command, plan.json, out).pipe(map$4(() => {
42192
+ return flatMap$4(Host, ({ env }) => try_({
42193
+ try: () => resolveFlags(env, io.flags),
42194
+ catch: (e) => new GtdError(e.message)
42195
+ })).pipe(flatMap$4(() => runCommand(plan.command, plan.json, out)), map$4(() => {
42055
42196
  out.flush();
42056
42197
  io.exit(0);
42057
42198
  }), provide$1(io.layers(plan.verbose)), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json.kind !== "off")(cause)));