@pmelab/gtd 60.1.0 → 62.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 (61) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +16 -0
  3. package/dist/chunks/{testkit-BhTey1Do.mjs → testkit-XQeYCNMn.mjs} +1670 -1470
  4. package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
  5. package/dist/gtd.bundle.mjs +991 -627
  6. package/dist/gtd.core.mjs +117 -117
  7. package/dist/testkit/gherkin.mjs +1 -1
  8. package/dist/testkit.mjs +1 -1
  9. package/dist/wire.mjs +2 -2
  10. package/package.json +1 -1
  11. package/src/cli/Cli.ts +6 -1
  12. package/src/cli/History.test.ts +10 -1
  13. package/src/cli/Launch.test.ts +52 -0
  14. package/src/cli/Launch.ts +143 -0
  15. package/src/cli/Metrics.test.ts +10 -1
  16. package/src/cli/Program.test.ts +30 -20
  17. package/src/cli/Program.ts +4 -2
  18. package/src/cli/Strays.ts +3 -2
  19. package/src/cli/index.ts +1 -0
  20. package/src/edge/Edge.test.ts +79 -26
  21. package/src/edge/Edge.ts +178 -99
  22. package/src/emit/GitScript.ts +21 -3
  23. package/src/emit/index.ts +3 -0
  24. package/src/gherkin/messages.test.ts +1 -0
  25. package/src/gherkin/module-config.feature +1 -0
  26. package/src/gherkin/more-steps.feature +1 -0
  27. package/src/gherkin/testkit.feature +2 -1
  28. package/src/install/Install.ts +9 -2
  29. package/src/platform/PinFile.ts +61 -0
  30. package/src/platform/index.ts +1 -0
  31. package/src/replay/Format.test.ts +35 -8
  32. package/src/replay/Format.ts +7 -4
  33. package/src/replay/Replay.test.ts +54 -0
  34. package/src/replay/Replay.ts +61 -17
  35. package/src/replay/Trailers.test.ts +11 -48
  36. package/src/replay/Trailers.ts +36 -42
  37. package/src/replay/Tree.ts +11 -0
  38. package/src/replay/index.ts +3 -0
  39. package/src/step/LandStep.ts +1 -0
  40. package/src/step/RepoSnapshot.ts +2 -0
  41. package/src/step/planStart.test.ts +2 -3
  42. package/src/step/planStart.ts +1 -5
  43. package/src/step/planStep.test.ts +4 -4
  44. package/src/step/planStep.ts +10 -8
  45. package/src/testkit/TestRepo.test.ts +4 -1
  46. package/src/testkit/TestRepo.ts +3 -1
  47. package/src/twins/EmittedScriptRecognizer.ts +32 -1
  48. package/src/twins/GitTiers.ts +23 -0
  49. package/src/twins/InMemRepo.ts +14 -0
  50. package/src/twins/PinBeats.ts +33 -0
  51. package/src/twins/PinFile.test.ts +107 -0
  52. package/src/twins/PinFile.ts +12 -0
  53. package/src/twins/index.ts +2 -0
  54. package/src/wire/BeatDocument.ts +26 -5
  55. package/src/wire/Demand.ts +16 -6
  56. package/src/wire/constants.ts +3 -0
  57. package/src/wire/types.ts +4 -0
  58. package/src/workflow/fingerprint.ts +2 -2
  59. package/src/workflow/index.ts +2 -2
  60. package/src/workflow/load.ts +14 -1
  61. package/src/workflow/pin.ts +15 -7
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __createRequire } from 'module';
3
3
  const require = __createRequire(import.meta.url);
4
- import { createRequire, isBuiltin } from "node:module";
4
+ import { createRequire, isBuiltin, registerHooks } from "node:module";
5
5
  import * as OS from "node:os";
6
6
  import { homedir, networkInterfaces, tmpdir } from "node:os";
7
7
  import * as NodeChildProcess from "node:child_process";
@@ -14,7 +14,7 @@ import * as NFS from "node:fs";
14
14
  import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
15
15
  import * as Path from "node:path";
16
16
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
- import { gzipSync } from "node:zlib";
17
+ import { gunzipSync, gzipSync } from "node:zlib";
18
18
  import * as NodeUrl from "node:url";
19
19
  import { fileURLToPath, pathToFileURL } from "node:url";
20
20
  import { stripVTControlCharacters } from "node:util";
@@ -12054,6 +12054,57 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
12054
12054
  };
12055
12055
  });
12056
12056
  //#endregion
12057
+ //#region src/platform/PinFile.ts
12058
+ const livePinFile = (cwd, path) => () => {
12059
+ const file = join(cwd, path);
12060
+ if (existsSync(file)) try {
12061
+ return {
12062
+ path: file,
12063
+ bytes: readFileSync(file)
12064
+ };
12065
+ } catch {
12066
+ return {
12067
+ path: file,
12068
+ bytes: /* @__PURE__ */ new Uint8Array()
12069
+ };
12070
+ }
12071
+ try {
12072
+ return {
12073
+ path: file,
12074
+ bytes: execFileSync("git", ["show", `HEAD:${path}`], {
12075
+ cwd,
12076
+ stdio: [
12077
+ "ignore",
12078
+ "pipe",
12079
+ "ignore"
12080
+ ]
12081
+ })
12082
+ };
12083
+ } catch {
12084
+ return;
12085
+ }
12086
+ };
12087
+ /**
12088
+ * Runs a pin's module as this process's main program. Node refuses an unknown
12089
+ * extension, so a load hook serves the unpacked source under the pin's own
12090
+ * URL — `import.meta.url` stays a file in the repository.
12091
+ */
12092
+ const runPinModule = async (path, source) => {
12093
+ const url = pathToFileURL(path).href;
12094
+ registerHooks({
12095
+ resolve: (specifier, context, nextResolve) => specifier === url ? {
12096
+ url,
12097
+ shortCircuit: true
12098
+ } : nextResolve(specifier, context),
12099
+ load: (requested, context, nextLoad) => requested === url ? {
12100
+ format: "module",
12101
+ source,
12102
+ shortCircuit: true
12103
+ } : nextLoad(requested, context)
12104
+ });
12105
+ await import(url);
12106
+ };
12107
+ //#endregion
12057
12108
  //#region src/flags/index.ts
12058
12109
  const FLAGS$1 = [];
12059
12110
  const BOOL = {
@@ -12326,7 +12377,13 @@ The \`kind\` field (\`--json=kind\`) selects what to do:
12326
12377
  Land it immediately, no display needed.
12327
12378
  - \`script\` — run \`--json=content\` with \`sh -c\`, appending its output to
12328
12379
  \`--json=log\`, and ignore its exit code — the outcome lives in the tree,
12329
- not in the exit code.
12380
+ not in the exit code. Two script beats carry a \`pin\` mark: the **pin
12381
+ beat** right after a process starts (\`pin: "add"\`, its script writes
12382
+ \`gtd bundle\`'s output to \`.gtd/process.gtdpin\`) and the **unpin beat**
12383
+ once the flow has ended (\`pin: "remove"\`, its script deletes that file).
12384
+ Both run unattended, like any script beat. A driver may write or delete the
12385
+ file its own way instead of running the script; either way it then lands,
12386
+ and the unpin's landing is the commit that closes the process.
12330
12387
  - \`prompt\` — send plain \`gtd next\`'s own output to your agent CLI over
12331
12388
  stdin (it's the bare content at this kind, so no separate \`--json=content\`
12332
12389
  read is needed), using the accompanying \`session.id\`/\`session.resume\`/
@@ -12369,7 +12426,8 @@ skills, \`skills\` (an array of the skill names, already trimmed and split —
12369
12426
  \`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
12370
12427
  document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
12371
12428
  \`cost\`, \`costByModel\`. Always \`approvals\` (an array of
12372
- \`{step, name, email, commit}\`, \`[]\` when none).
12429
+ \`{step, name, email, commit}\`, \`[]\` when none). Only at a pin or unpin
12430
+ beat: \`pin\` (\`"add"\` or \`"remove"\`).
12373
12431
 
12374
12432
  \`gtd land\` fields, in fixed order: \`schema\`, \`script\` (the POSIX sh to run), \`settled\`
12375
12433
  (true for a no-op at a \`script\` rest — stop immediately, nothing more to
@@ -28942,6 +29000,8 @@ const renderFormat = (fmt, ...args) => {
28942
29000
  };
28943
29001
  /** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
28944
29002
  const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
29003
+ /** Where a process keeps its pin: the engine and workflow it runs on, as `gtd bundle` prints them. */
29004
+ const PIN_PATH = ".gtd/process.gtdpin";
28945
29005
  //#endregion
28946
29006
  //#region src/wire/Demand.ts
28947
29007
  /**
@@ -28982,14 +29042,20 @@ const demandOf = (input) => {
28982
29042
  read: null,
28983
29043
  write: null
28984
29044
  };
28985
- return kind === "prompt" ? {
29045
+ if (kind === "prompt") return {
28986
29046
  kind,
28987
29047
  content,
28988
29048
  session,
28989
29049
  validate,
28990
29050
  skills,
28991
29051
  access
28992
- } : {
29052
+ };
29053
+ if (kind === "script" && rendered.pin !== void 0) return {
29054
+ kind,
29055
+ content,
29056
+ pin: rendered.pin
29057
+ };
29058
+ return {
28993
29059
  kind,
28994
29060
  content
28995
29061
  };
@@ -29042,33 +29108,39 @@ const assertNeverDemand = (demand) => {
29042
29108
  * falling through to "no session."
29043
29109
  */
29044
29110
  const dispatchFieldsOf = (demand) => {
29111
+ const none = {
29112
+ session: void 0,
29113
+ validate: void 0,
29114
+ skills: void 0,
29115
+ access: void 0,
29116
+ pin: void 0
29117
+ };
29045
29118
  switch (demand.kind) {
29046
29119
  case "prompt": return {
29120
+ ...none,
29047
29121
  session: demand.session,
29048
29122
  validate: demand.validate,
29049
29123
  skills: demand.skills,
29050
29124
  access: demand.access
29051
29125
  };
29126
+ case "script": return {
29127
+ ...none,
29128
+ pin: demand.pin
29129
+ };
29052
29130
  case "capture":
29053
29131
  case "message":
29054
- case "script":
29055
- case "stalled": return {
29056
- session: void 0,
29057
- validate: void 0,
29058
- skills: void 0,
29059
- access: void 0
29060
- };
29132
+ case "stalled": return none;
29061
29133
  default: return assertNeverDemand(demand);
29062
29134
  }
29063
29135
  };
29064
29136
  /**
29065
- * Flatten a `Demand` plus its `BeatStatus` into the single 25-key document
29137
+ * Flatten a `Demand` plus its `BeatStatus` into the single 26-key document
29066
29138
  * `gtd next --json` emits — the ONLY place the two are joined. `cost`/
29067
29139
  * `costByModel` are omitted together, exactly when no cost was recorded
29068
29140
  * (`cost <= 0`).
29069
29141
  */
29070
29142
  const beatDocument = (demand, status) => {
29071
- const { session, validate, skills, access } = dispatchFieldsOf(demand);
29143
+ const { session, validate, skills, access, pin } = dispatchFieldsOf(demand);
29072
29144
  const hasCost = status.cost > 0;
29073
29145
  return {
29074
29146
  schema: 1,
@@ -29080,6 +29152,7 @@ const beatDocument = (demand, status) => {
29080
29152
  model: status.model,
29081
29153
  system: status.system,
29082
29154
  validate,
29155
+ pin,
29083
29156
  log: status.log,
29084
29157
  state: status.state,
29085
29158
  actor: status.actor,
@@ -29417,11 +29490,20 @@ const diffTrees = (before, after) => {
29417
29490
  };
29418
29491
  };
29419
29492
  const isEmptyDiff = (diff) => diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0;
29493
+ const withoutPath = (tree, hidden) => {
29494
+ let paths;
29495
+ return {
29496
+ paths: () => paths ??= tree.paths().filter((path) => path !== hidden),
29497
+ read: (path) => path === hidden ? void 0 : tree.read(path),
29498
+ ...tree.id !== void 0 ? { id: (path) => path === hidden ? void 0 : tree.id(path) } : {}
29499
+ };
29500
+ };
29501
+ const OLDEST_READABLE_FORMAT = 1;
29420
29502
  const short$1 = (hash) => hash.slice(0, 7);
29421
29503
  const formatFault = (commits) => {
29422
- const foreign = commits.find((c) => c.format !== 1);
29504
+ const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format > 2);
29423
29505
  if (foreign === void 0) return void 0;
29424
- return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads format 1 — run a gtd release that reads format ${foreign.format} to continue this process`;
29506
+ return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads formats ${OLDEST_READABLE_FORMAT} to 2 — run a gtd release that reads format ${foreign.format} to continue this process`;
29425
29507
  };
29426
29508
  //#endregion
29427
29509
  //#region src/replay/Trailers.ts
@@ -29458,6 +29540,11 @@ const parseStepId = (raw) => {
29458
29540
  occurrence: Number(match[2])
29459
29541
  };
29460
29542
  };
29543
+ /** What a pin and an unpin beat rest at, and their commits name — neither is a step of the workflow. */
29544
+ const PIN_STATES = {
29545
+ add: "pin",
29546
+ remove: "unpin"
29547
+ };
29461
29548
  const isVerdict = (value) => {
29462
29549
  if (typeof value !== "object" || value === null) return false;
29463
29550
  const v = value;
@@ -29477,13 +29564,7 @@ const parseJson = (raw) => {
29477
29564
  const TRAILER_RE = /^(Gtd-[A-Za-z-]+):[ \t]*(.*?)[ \t]*$/gm;
29478
29565
  const COST_RE = /^([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+))?$/;
29479
29566
  const VAR_RE = /^([^=\s]+)=(.*)$/;
29480
- const CONFIG_RE = /^([0-9a-f]{16}) (.+)$/;
29481
29567
  const CONTROL_CHARACTER$1 = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
29482
- const encodePath = (path) => path.trim() !== path || path.startsWith("\"") || CONTROL_CHARACTER$1.test(path) ? JSON.stringify(path) : path;
29483
- const decodePath = (raw) => {
29484
- const path = raw.startsWith("\"") ? parseJson(raw) : raw;
29485
- return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
29486
- };
29487
29568
  const decodeApproval = (raw) => {
29488
29569
  const parsed = parseJson(raw);
29489
29570
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
@@ -29535,11 +29616,6 @@ const TRAILER_READERS = {
29535
29616
  const kv = VAR_RE.exec(value);
29536
29617
  if (kv !== null) into.vars[kv[1]] = kv[2];
29537
29618
  },
29538
- "Gtd-Config": (value, into) => {
29539
- const entry = CONFIG_RE.exec(value);
29540
- const path = entry === null ? void 0 : decodePath(entry[2]);
29541
- if (path !== void 0) into.config[path] = entry[1];
29542
- },
29543
29619
  "Gtd-Review-Base": (value, into) => {
29544
29620
  into.reviewBase ??= value.split(/\s/)[0];
29545
29621
  },
@@ -29555,6 +29631,9 @@ const TRAILER_READERS = {
29555
29631
  "Gtd-Approved": (value, into) => {
29556
29632
  into.approved ??= decodeApproval(value);
29557
29633
  },
29634
+ "Gtd-Pin": (value, into) => {
29635
+ if (value === "add" || value === "remove") into.pin ??= value;
29636
+ },
29558
29637
  "Gtd-Format": (value, into) => {
29559
29638
  if (FORMAT_RE.test(value)) into.format ??= Number(value);
29560
29639
  },
@@ -29577,10 +29656,10 @@ const parseCommitMessage = (message) => {
29577
29656
  version: void 0,
29578
29657
  kept: void 0,
29579
29658
  approved: void 0,
29659
+ pin: void 0,
29580
29660
  judge: [],
29581
29661
  marks: [],
29582
29662
  vars: {},
29583
- config: {},
29584
29663
  cost: [],
29585
29664
  format: void 0
29586
29665
  };
@@ -29594,24 +29673,28 @@ const parseCommitMessage = (message) => {
29594
29673
  };
29595
29674
  const sortedEntries = (record = {}) => Object.entries(record).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
29596
29675
  /** Trailer order is fixed so the same spec always yields the same bytes. */
29676
+ const trailer = (name, value) => value === void 0 ? [] : [`Gtd-${name}: ${value}`];
29677
+ const costValue = (cost) => cost === void 0 ? void 0 : `${cost.cost}${cost.model !== void 0 ? ` ${cost.model}` : ""}`;
29597
29678
  const formatCommitMessage = (spec) => {
29598
- const lines = [];
29599
- if (spec.step !== void 0) lines.push(`Gtd-Step: ${formatStepId(spec.step)}`);
29600
- if (spec.workflow !== void 0) lines.push(`Gtd-Workflow: ${spec.workflow}`);
29601
- if (spec.reviewBase !== void 0) lines.push(`Gtd-Review-Base: ${spec.reviewBase}`);
29602
- for (const [name, value] of sortedEntries(spec.vars)) lines.push(`Gtd-Var: ${name}=${value}`);
29603
- for (const [path, hash] of sortedEntries(spec.config)) lines.push(`Gtd-Config: ${hash} ${encodePath(path)}`);
29604
- if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
29605
- for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`);
29606
- for (const m of spec.marks ?? []) lines.push(`Gtd-Mark: ${m.name}=${m.value}`);
29607
- if (spec.approved !== void 0) lines.push(`Gtd-Approved: ${encodeApproval(spec.approved)}`);
29608
- if (spec.branch !== void 0) lines.push(`Gtd-Branch: ${spec.branch}`);
29609
- if (spec.version !== void 0) lines.push(`Gtd-Version: ${spec.version}`);
29610
- lines.push(`Gtd-Format: 1`);
29679
+ const lines = [
29680
+ ...trailer("Step", spec.step && formatStepId(spec.step)),
29681
+ ...trailer("Workflow", spec.workflow),
29682
+ ...trailer("Review-Base", spec.reviewBase),
29683
+ ...sortedEntries(spec.vars).map(([name, value]) => `Gtd-Var: ${name}=${value}`),
29684
+ ...trailer("Cost", costValue(spec.cost)),
29685
+ ...(spec.judge ?? []).map((verdict) => `Gtd-Judge: ${JSON.stringify(verdict)}`),
29686
+ ...(spec.marks ?? []).map((m) => `Gtd-Mark: ${m.name}=${m.value}`),
29687
+ ...trailer("Approved", spec.approved && encodeApproval(spec.approved)),
29688
+ ...trailer("Pin", spec.pin),
29689
+ ...trailer("Branch", spec.branch),
29690
+ ...trailer("Version", spec.version),
29691
+ `Gtd-Format: 2`
29692
+ ];
29611
29693
  return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
29612
29694
  };
29613
29695
  //#endregion
29614
29696
  //#region src/replay/Replay.ts
29697
+ const UNPIN_STATE = PIN_STATES.remove;
29615
29698
  const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
29616
29699
  const memoryScopeOf = (name) => {
29617
29700
  const dot = name.lastIndexOf(".");
@@ -29745,7 +29828,33 @@ const modeWithoutFile = (step) => {
29745
29828
  if (options.mode === void 0 || options.file !== void 0) return void 0;
29746
29829
  return `gtd: step "${step.name}": mode requires file in ${step.kind}() options — name the steering file the mode checks`;
29747
29830
  };
29748
- const replay = async (input) => {
29831
+ const withoutPin = (input) => {
29832
+ const hide = (tree) => withoutPath(tree, PIN_PATH);
29833
+ return {
29834
+ ...input,
29835
+ episode: {
29836
+ base: {
29837
+ ...input.episode.base,
29838
+ tree: hide(input.episode.base.tree)
29839
+ },
29840
+ commits: input.episode.commits.map((c) => ({
29841
+ ...c,
29842
+ tree: hide(c.tree),
29843
+ ...c.kept !== void 0 ? { kept: {
29844
+ ...c.kept,
29845
+ tree: hide(c.kept.tree)
29846
+ } } : {}
29847
+ }))
29848
+ },
29849
+ ...input.startTree !== void 0 ? { startTree: hide(input.startTree) } : {},
29850
+ ...input.pending !== void 0 ? { pending: {
29851
+ ...input.pending,
29852
+ tree: hide(input.pending.tree)
29853
+ } } : {}
29854
+ };
29855
+ };
29856
+ const replay = async (raw) => {
29857
+ const input = withoutPin(raw);
29749
29858
  const commits = input.episode.commits.map((c) => ({
29750
29859
  ...c,
29751
29860
  parsed: parseCommitMessage(c.message)
@@ -29754,6 +29863,9 @@ const replay = async (input) => {
29754
29863
  let cursor = 0;
29755
29864
  let pendingUsed = false;
29756
29865
  let position = input.episode.base;
29866
+ const skipPins = () => {
29867
+ while (cursor < commits.length && commits[cursor].parsed.pin !== void 0) cursor++;
29868
+ };
29757
29869
  let previousPosition = input.episode.base;
29758
29870
  let expectedNext;
29759
29871
  const occurrences = /* @__PURE__ */ new Map();
@@ -29935,19 +30047,24 @@ const replay = async (input) => {
29935
30047
  if (unapproved !== void 0) return finish(unapproved);
29936
30048
  cursor++;
29937
30049
  advance(commit);
29938
- if (commit.parsed.kept !== void 0) if (commit.kept === void 0) keptTips.delete(step.name);
29939
- else keptTips.set(step.name, {
29940
- hash: commit.kept.hash,
29941
- before: commit.tree,
29942
- after: commit.kept.tree
29943
- });
30050
+ skipPins();
30051
+ noteKept(step, commit);
29944
30052
  const to = commit.parsed.parsed?.to;
29945
- if (to !== void 0) expectedNext = {
30053
+ if (to !== void 0 && to !== UNPIN_STATE) expectedNext = {
29946
30054
  name: to,
29947
30055
  hash: commit.hash
29948
30056
  };
29949
30057
  return replyFor(step.request, commit.parsed.judge);
29950
30058
  };
30059
+ const noteKept = (step, commit) => {
30060
+ if (commit.parsed.kept === void 0) return;
30061
+ if (commit.kept === void 0) keptTips.delete(step.name);
30062
+ else keptTips.set(step.name, {
30063
+ hash: commit.kept.hash,
30064
+ before: commit.tree,
30065
+ after: commit.kept.tree
30066
+ });
30067
+ };
29951
30068
  const landPending = (step, pending) => {
29952
30069
  pendingUsed = true;
29953
30070
  landed = step;
@@ -29964,7 +30081,10 @@ const replay = async (input) => {
29964
30081
  return replyFor(step.request, pending.verdicts ?? []);
29965
30082
  };
29966
30083
  const answer = (step) => {
29967
- while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) cursor++;
30084
+ while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) {
30085
+ cursor++;
30086
+ skipPins();
30087
+ }
29968
30088
  const commit = commits[cursor];
29969
30089
  if (commit !== void 0) return consumeCommit(step, commit);
29970
30090
  if (input.pending === void 0 || pendingUsed) return finish({
@@ -29977,6 +30097,7 @@ const replay = async (input) => {
29977
30097
  return landPending(step, input.pending);
29978
30098
  };
29979
30099
  const requestFault = (request) => {
30100
+ if (Object.values(PIN_STATES).includes(scoped(request.name))) return `gtd: step "${scoped(request.name)}": the name is reserved for the pin and unpin beats — rename the step`;
29980
30101
  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)`;
29981
30102
  if (request.kind !== "agent") return void 0;
29982
30103
  const name = scoped(request.name);
@@ -30003,15 +30124,18 @@ const replay = async (input) => {
30003
30124
  const blocker = blockerAt(step);
30004
30125
  return blocker === void 0 ? answer(step) : finish(blocker);
30005
30126
  };
30006
- const endOfFlow = (via) => cursor < commits.length ? {
30007
- kind: "divergence",
30008
- message: `gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode — ${DIVERGED}`
30009
- } : {
30010
- kind: "ended",
30011
- via,
30012
- trace,
30013
- landed,
30014
- marks
30127
+ const endOfFlow = (via) => {
30128
+ skipPins();
30129
+ return cursor < commits.length ? {
30130
+ kind: "divergence",
30131
+ message: `gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode — ${DIVERGED}`
30132
+ } : {
30133
+ kind: "ended",
30134
+ via,
30135
+ trace,
30136
+ landed,
30137
+ marks
30138
+ };
30015
30139
  };
30016
30140
  const changesSince = (hash) => {
30017
30141
  const tree = treeAt(hash);
@@ -30074,10 +30198,11 @@ const replay = async (input) => {
30074
30198
  skillsFor: (localName) => resolveSkills(memoryScopeOf(scoped(localName))) ?? [],
30075
30199
  accessFor: (localName, file) => foldedAccessFor(resolveAccess(memoryScopeOf(scoped(localName))), file),
30076
30200
  head: () => {
30077
- const rest = commits.slice(cursor);
30201
+ const rest = commits.slice(cursor).filter((c) => c.parsed.pin === void 0);
30078
30202
  return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
30079
30203
  }
30080
30204
  };
30205
+ skipPins();
30081
30206
  installContext(context);
30082
30207
  try {
30083
30208
  const flowDone = input.flow({}).then(() => {
@@ -30477,7 +30602,8 @@ const load = gen(function* () {
30477
30602
  },
30478
30603
  ...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
30479
30604
  warnings: diagnostics.filter((d) => d.severity === "warning"),
30480
- fingerprint: pinned?.fingerprint ?? configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk)
30605
+ fingerprint: pinned?.fingerprint ?? configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
30606
+ drift: pinned === void 0 ? [] : changedConfigFiles(pinned.fingerprint, configFingerprint(Object.keys(pinned.fingerprint).map((file) => resolve(host.root, file)), host.root, readFromDisk))
30481
30607
  };
30482
30608
  });
30483
30609
  const readFromDisk = (path) => {
@@ -31131,8 +31257,6 @@ const knownModes = (def) => Object.keys(def.modes);
31131
31257
  //#endregion
31132
31258
  //#region src/workflow/pin.ts
31133
31259
  init_dist$1();
31134
- /** Bumped when a launcher could no longer run an older pin the same way. */
31135
- const PIN_FORMAT = 1;
31136
31260
  /**
31137
31261
  * The engine a pin carries, as ES module source. Live, the `gtd.core.mjs` the
31138
31262
  * running gtd ships beside its own bundle.
@@ -31158,7 +31282,505 @@ const pin = (module, root, version) => gen(function* () {
31158
31282
  try: () => pinnedWorkflowSource(module, root, version),
31159
31283
  catch: (e) => new GtdError(`gtd bundle: ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
31160
31284
  })) + core.replace(/^#!.*\n/, "");
31161
- return Buffer.concat([Buffer.from(`gtdpin ${PIN_FORMAT} gtd ${version}\n`), gzipSync(source, { level: 9 })]);
31285
+ return Buffer.concat([Buffer.from(`gtdpin 1 gtd ${version}\n`), gzipSync(source, { level: 9 })]);
31286
+ });
31287
+ /** A pin's header line, read without unpacking it; undefined when the bytes are no pin. */
31288
+ const pinHeader = (bytes) => {
31289
+ const end = bytes.indexOf(10);
31290
+ const header = end < 0 ? null : /^gtdpin (\d+) gtd (\S+)$/.exec(Buffer.from(bytes.subarray(0, end)).toString());
31291
+ return header === null ? void 0 : {
31292
+ format: Number(header[1]),
31293
+ version: header[2]
31294
+ };
31295
+ };
31296
+ /** A pin's header and module source. */
31297
+ const readPin = (bytes) => {
31298
+ const header = pinHeader(bytes);
31299
+ if (header === void 0) throw new Error("not a gtd pin");
31300
+ return {
31301
+ ...header,
31302
+ source: gunzipSync(bytes.subarray(bytes.indexOf(10) + 1)).toString("utf8")
31303
+ };
31304
+ };
31305
+ //#endregion
31306
+ //#region src/emit/GitScript.ts
31307
+ const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
31308
+ /**
31309
+ * `--allow-empty` because gtd's workflow commits may be empty on purpose.
31310
+ * Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
31311
+ * "empty git commit" rejection message, discriminated from the captured
31312
+ * output rather than the exit code, so a genuinely rejected commit (a lint
31313
+ * error, a commit-msg hook) still fails.
31314
+ */
31315
+ const commitAllowEmpty = (message, only) => {
31316
+ const m = shellQuote$1(message);
31317
+ const paths = only === void 0 ? "" : ` -- ${shellQuote$1(only)}`;
31318
+ return [
31319
+ `if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
31320
+ ` case "$out" in`,
31321
+ ` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m}${paths} || exit 1 ;;`,
31322
+ ` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
31323
+ ` esac`,
31324
+ `fi`
31325
+ ].join("\n");
31326
+ };
31327
+ const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — revert the edits and run `gtd land` again to approve";
31328
+ /** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
31329
+ const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
31330
+ /** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
31331
+ const commitPath = (path, message) => `git add -A -- ${shellQuote$1(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
31332
+ /** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
31333
+ const pinScript = (path) => {
31334
+ const p = shellQuote$1(path);
31335
+ const slash = path.lastIndexOf("/");
31336
+ return `${slash === -1 ? "" : `mkdir -p ${shellQuote$1(path.slice(0, slash))}\n`}gtd bundle >${p} || { rm -f -- ${p}; exit 1; }`;
31337
+ };
31338
+ /** An unpin beat's script, and `gtd abandon`'s removal of the pin. */
31339
+ const unpinScript = (path) => `rm -f -- ${shellQuote$1(path)}`;
31340
+ /**
31341
+ * An approval records exactly the judged tree: HEAD is pinned before the
31342
+ * clean check and the commit is built over its tree with `commit-tree`, so no
31343
+ * hook runs and none can add to it. A HEAD that moved fails the guarded
31344
+ * `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
31345
+ */
31346
+ const commitClean = (message) => [
31347
+ `gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
31348
+ `if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
31349
+ ` printf '%s\\n' ${shellQuote$1(CHANGED_SINCE_LAND)} >&2`,
31350
+ ` exit 1`,
31351
+ `fi`,
31352
+ `gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
31353
+ `if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
31354
+ `gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
31355
+ `git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
31356
+ ].join("\n");
31357
+ const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
31358
+ const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
31359
+ /**
31360
+ * POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
31361
+ * tree, so no hook runs on it and its tree is the pre-turn tree by
31362
+ * construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
31363
+ * `-S`, read (and failing closed) before any object or ref is written; the
31364
+ * index is already staged by then, so an early exit leaves the turn staged.
31365
+ * The kept ref is deleted only after `reset -q` succeeds: it is then the
31366
+ * turn's only handle.
31367
+ * The branch moves before the final hard reset, so if that reset fails the
31368
+ * marker has landed and the turn is still in the working tree (kept ref
31369
+ * present); every other failure leaves the branch untouched.
31370
+ */
31371
+ const keepTurn = (refPrefix, message) => {
31372
+ const keptRef = `${shellQuote$1(`${refPrefix}/`)}"$gtd_kept"`;
31373
+ return [
31374
+ `git add -A || exit 1`,
31375
+ `gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
31376
+ `if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
31377
+ `gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
31378
+ `gtd_tree=$(git write-tree) || exit 1`,
31379
+ `gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
31380
+ `git update-ref ${keptRef} "$gtd_kept" || exit 1`,
31381
+ `gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
31382
+ `if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
31383
+ ` git reset -q && git update-ref -d ${keptRef}`,
31384
+ ` exit 1`,
31385
+ `fi`,
31386
+ `git reset --hard HEAD`
31387
+ ].join("\n");
31388
+ };
31389
+ const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
31390
+ /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
31391
+ const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
31392
+ /**
31393
+ * No `+` on the refspec: a local ref the remote's tip does not descend from
31394
+ * fails the fetch instead of being overwritten (a fast-forward still applies).
31395
+ */
31396
+ const fetchHistoryRefs = (remote) => [
31397
+ `set -eu`,
31398
+ `before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
31399
+ `git fetch ${shellQuote$1(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
31400
+ `after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
31401
+ `printf '%s history refs arrived\\n' "$((after - before))"`
31402
+ ].join("\n");
31403
+ /** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/twins/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
31404
+ const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
31405
+ const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
31406
+ const renderOutcome = (outcome) => {
31407
+ switch (outcome.kind) {
31408
+ case "transition": return printfLine$1("%s %s → %s\n", [
31409
+ "'->'",
31410
+ shellQuote$1(outcome.from),
31411
+ shellQuote$1(outcome.to)
31412
+ ]);
31413
+ case "commit": return printfLine$1("%s %s\n", ["'[commit]'", shellQuote$1(outcome.subject)]);
31414
+ case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
31415
+ }
31416
+ };
31417
+ const renderGitWrite = (write) => {
31418
+ switch (write.kind) {
31419
+ case "commitAll": return commitAll(write.message);
31420
+ case "commitPath": return commitPath(write.path, write.message);
31421
+ case "commitClean": return commitClean(write.message);
31422
+ case "updateRef": return updateRef(write.ref, write.target);
31423
+ case "keepTurn": return keepTurn(write.ref, write.message);
31424
+ }
31425
+ };
31426
+ /**
31427
+ * Wraps `command` so a non-zero exit prints `prompt` plus the command's
31428
+ * captured output before propagating that exit code — mirrors `Emit.ts`'s
31429
+ * `failurePromptWrapper` (same reverse-import concern as `renderOutcome`
31430
+ * above: `Emit.ts` imports `shellQuote` from here already).
31431
+ */
31432
+ const failurePromptWrapper$1 = (command, prompt) => {
31433
+ return [
31434
+ `gtd_validate_status=0`,
31435
+ `gtd_validate_out="$( {`,
31436
+ command,
31437
+ `} 2>&1 )" || gtd_validate_status=$?`,
31438
+ `if [ "$gtd_validate_status" -ne 0 ]; then`,
31439
+ ` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
31440
+ ` exit "$gtd_validate_status"`,
31441
+ `fi`
31442
+ ].join("\n");
31443
+ };
31444
+ const renderLandStep = (step) => {
31445
+ if (step.kind === "gitWrite") return renderGitWrite(step.write);
31446
+ if (step.kind === "outcome") return renderOutcome(step.outcome);
31447
+ if (step.kind === "uncheck") return `gtd uncheck ${shellQuote$1(step.file)}`;
31448
+ return step.onFailure !== void 0 ? failurePromptWrapper$1(step.command, step.onFailure) : step.command;
31449
+ };
31450
+ const ScriptSurface = { render: (steps) => {
31451
+ const rendered = steps.map(renderLandStep);
31452
+ return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
31453
+ } };
31454
+ //#endregion
31455
+ //#region src/emit/Emit.ts
31456
+ /** Exits 0 when the declared steering file is absent — nothing to format or validate yet. An OR list, not `if`: its left side failing never trips `set -e`. */
31457
+ const fileExistsGuard = (file) => `[ -f ${shellQuote$1(file)} ] || exit 0`;
31458
+ /** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
31459
+ const withFileVar$1 = (command, file) => `export GTD_FILE=${shellQuote$1(file)}\n${command}`;
31460
+ const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
31461
+ const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
31462
+ const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
31463
+ /** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
31464
+ const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote$1(file));
31465
+ /**
31466
+ * The leading word of a rendered `format:`/`validate:` command, when the
31467
+ * command has exactly one unambiguous binary to probe — `undefined` for
31468
+ * anything a shell metacharacter could hide a second command behind. A missing
31469
+ * guard degrades to a raw exit; a wrong guard would refuse a working command.
31470
+ */
31471
+ const extractLeadingBinary = (command) => {
31472
+ if (command.includes("\n")) return void 0;
31473
+ if (SHELL_METACHARACTER_RE.test(command.replace(FILE_VAR_RE, ""))) return void 0;
31474
+ const match = SIMPLE_LEADING_WORD_RE.exec(command);
31475
+ return match === null ? void 0 : match[0];
31476
+ };
31477
+ /**
31478
+ * Names a typo'd or uninstalled binary and the `$PATH` it was looked up on.
31479
+ * `$PATH` is a shell word the DRIVER's shell expands at run time, so gtd never
31480
+ * resolves it in-process. Exits 127, matching bash's "command not found".
31481
+ */
31482
+ const binaryGuard = (binary, mode, key) => {
31483
+ const messageQ = shellQuote$1(`gtd: mode "${mode}": "${key}" command not found: ${binary}`);
31484
+ return [
31485
+ `command -v ${shellQuote$1(binary)} >/dev/null 2>&1 || {`,
31486
+ ` printf '%s\\n%s\\n' ${messageQ} '$PATH: '"$PATH"`,
31487
+ ` exit 127`,
31488
+ `}`
31489
+ ].join("\n");
31490
+ };
31491
+ /**
31492
+ * Prints `prompt` plus the captured output on a non-zero exit, then propagates
31493
+ * it. The `{ … }` group inlines a multi-line `command` verbatim; the
31494
+ * assignment sits left of `||` so `set -e` never trips on the failure itself.
31495
+ */
31496
+ const failurePromptWrapper = (command, prompt) => {
31497
+ return [
31498
+ `gtd_validate_status=0`,
31499
+ `gtd_validate_out="$( {`,
31500
+ command,
31501
+ `} 2>&1 )" || gtd_validate_status=$?`,
31502
+ `if [ "$gtd_validate_status" -ne 0 ]; then`,
31503
+ ` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
31504
+ ` exit "$gtd_validate_status"`,
31505
+ `fi`
31506
+ ].join("\n");
31507
+ };
31508
+ const renderStep = (step) => {
31509
+ if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
31510
+ return step.command;
31511
+ };
31512
+ const assembleScript = (steps) => steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n");
31513
+ /** 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. */
31514
+ const emitScripts = (required = [], optional = []) => ({
31515
+ required: assembleScript(required),
31516
+ optional: assembleScript(optional)
31517
+ });
31518
+ /** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
31519
+ const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
31520
+ const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
31521
+ const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
31522
+ /**
31523
+ * A write command's single pasteable script (`gtd abandon | sh`,
31524
+ * `gtd land --json=script`'s own field). `optional`, when non-empty, is
31525
+ * wrapped in a subshell whose failure is swallowed — presentation-only, so it
31526
+ * must never turn a landed turn into a non-zero exit.
31527
+ */
31528
+ const combinedScript = (required, optional) => {
31529
+ if (required.length === 0) return "";
31530
+ if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
31531
+ return [
31532
+ DID_NOT_RUN_COMMENT,
31533
+ "",
31534
+ required,
31535
+ "",
31536
+ PRESENTATION_ONLY_COMMENT,
31537
+ `(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
31538
+ ].join("\n");
31539
+ };
31540
+ //#endregion
31541
+ //#region src/emit/OutcomeScript.ts
31542
+ /**
31543
+ * A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
31544
+ * tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
31545
+ * re-quoted here: a value reaches the script as a printf ARGUMENT, never
31546
+ * interpolated into the format itself, so a subject or state name containing
31547
+ * `%` can never be read as a conversion spec. Markers like `->` are arguments
31548
+ * too, keeping every format string free of a leading `-` that a shell's
31549
+ * `printf` could mistake for an option. A real newline in `fmt` is emitted as
31550
+ * the two-character `\n` escape `printf` itself expands, keeping every emitted
31551
+ * statement on one line.
31552
+ */
31553
+ const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
31554
+ /**
31555
+ * Marks an emitted block as print-only. Load-bearing beyond documentation:
31556
+ * `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
31557
+ * this line, and a bare `printf` would be ambiguous with a workflow
31558
+ * `script:` command that happens to start the same way.
31559
+ */
31560
+ const OUTCOME_MARKER = "# gtd: outcome (print-only)";
31561
+ const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
31562
+ 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";
31563
+ 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";
31564
+ /** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
31565
+ const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
31566
+ /** One already-rendered plain line. */
31567
+ const noteOutcome = (text) => printfLine("%s\n", [shellQuote$1(text)]);
31568
+ /**
31569
+ * Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
31570
+ * — this line runs AFTER the reset that made `head` the new HEAD, so neither
31571
+ * value is knowable when gtd generates it.
31572
+ */
31573
+ const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
31574
+ shellQuote$1(from),
31575
+ `"$(git rev-parse --short ${shellQuote$1(head)})"`,
31576
+ `"$(git log -1 --format=%s ${shellQuote$1(head)})"`,
31577
+ shellQuote$1(state)
31578
+ ]);
31579
+ /** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
31580
+ const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
31581
+ /** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
31582
+ const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
31583
+ `"$(git rev-parse --short ${shellQuote$1(to)})"`,
31584
+ `"$(git log -1 --format=%s ${shellQuote$1(to)})"`,
31585
+ shellQuote$1(state)
31586
+ ]);
31587
+ //#endregion
31588
+ //#region src/emit/ModeContradiction.ts
31589
+ /**
31590
+ * Written for two readers at once, since gtd validate's exit code carries no
31591
+ * second meaning to distinguish them: the agent (this is a CONFIGURATION BUG,
31592
+ * not the steering file — stop, don't edit it, or a fix-retry loop will
31593
+ * thrash on the document) and the human (which mode, and the exact rendered
31594
+ * `format:` command, to fix `gtd.config.ts` without reproducing the failure).
31595
+ */
31596
+ const contradictionMessage = (mode, formatCommand) => `gtd: mode "${mode}"'s format: command breaks its own validator — this is a CONFIGURATION BUG, not a problem with the steering file. Do NOT edit the steering file — stop and end your turn now.\nThe rendered format: command that ran:\n${formatCommand}\nWhat it turned mode "${mode}"'s own canonical sample into:`;
31597
+ /**
31598
+ * Formats a copy of the mode's own canonical sample and re-validates it with
31599
+ * `gtd check <mode>`, failing loudly when the round-trip breaks it — detects a
31600
+ * mode whose `format:` contradicts its own `validate:` without ever touching
31601
+ * the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
31602
+ * heredoc, since no delimiter chosen ahead of time can collide with the
31603
+ * sample's own bytes. One bash fragment with no blank lines (so
31604
+ * `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
31605
+ * to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
31606
+ * still fires at a first-write beat where the real steering file doesn't
31607
+ * exist yet.
31608
+ */
31609
+ const buildModeContradictionCheck = (inputs) => {
31610
+ const { mode, samplePath, sample, formatCommand } = inputs;
31611
+ const pathQ = shellQuote$1(samplePath);
31612
+ const messageQ = shellQuote$1(contradictionMessage(mode, formatCommand));
31613
+ return [
31614
+ `printf '%s' ${shellQuote$1(sample)} > ${pathQ}`,
31615
+ formatCommand,
31616
+ `gtd check ${mode} ${pathQ} >/dev/null 2>&1 || {`,
31617
+ ` printf '%s\\n' ${messageQ} >&2`,
31618
+ ` cat ${pathQ} >&2`,
31619
+ ` rm -f ${pathQ}`,
31620
+ ` exit 1`,
31621
+ `}`,
31622
+ `rm -f ${pathQ}`
31623
+ ].join("\n");
31624
+ };
31625
+ /**
31626
+ * Skip notice for a mode whose validator is EXTERNAL (a user `validate:`
31627
+ * command, not one of the two built-in modes) — there's no in-process parser
31628
+ * to round-trip a sample through. Printed to stderr since silence would read
31629
+ * as a clean bill of health.
31630
+ */
31631
+ const modeContradictionSkipNotice = (mode) => {
31632
+ return `printf '%s\\n' ${shellQuote$1(`gtd: mode "${mode}" has an external validate: command — skipping the format/validate contradiction check`)} >&2`;
31633
+ };
31634
+ //#endregion
31635
+ //#region src/emit/SteeringMode.ts
31636
+ init_dist$1();
31637
+ init_steering();
31638
+ /**
31639
+ * `format` alone (no live/external verdict) when there's no validator at
31640
+ * all; a live in-process validator for a `"builtin"` validate OR a command
31641
+ * that is exactly the format's own seeded string; `externalValidate` for any
31642
+ * other command. Each arm returns directly — a flat sequence, not nested
31643
+ * ternaries — so the four outcomes stay each their own case to read, not a
31644
+ * boolean expression to re-derive.
31645
+ */
31646
+ const capabilitiesFor = (mode, format, validate) => {
31647
+ if (format === void 0) return validate?.kind === "command" ? { externalValidate: true } : {};
31648
+ if (validate?.kind === "builtin") return {
31649
+ format,
31650
+ liveValidate: format.validate
31651
+ };
31652
+ if (validate === void 0) return { format };
31653
+ if (isSeededValidateCommand(mode, validate.command)) return {
31654
+ format,
31655
+ liveValidate: format.validate
31656
+ };
31657
+ return {
31658
+ format,
31659
+ externalValidate: true
31660
+ };
31661
+ };
31662
+ /**
31663
+ * Resolve a `mode:` name against `def.modes` plus the built-in registry
31664
+ * (`src/workflow/SteeringFormats.ts`) — half by half: a declared `format:`/`validate:`
31665
+ * wins, and an undeclared `validate:` falls back to the built-in format's own
31666
+ * parser when the name is registered. `format` is set from the registry
31667
+ * ALONE, independent of which half of `validate` wins — a declared validator
31668
+ * overrides validation, not the format identity.
31669
+ *
31670
+ * `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
31671
+ * against the registry alone, with no workflow definition in hand (e.g. the
31672
+ * basename dispatch for a `REVIEW.md` no config maps).
31673
+ *
31674
+ * The `"unknown"` arm carries the whole refusal message, naming what IS known
31675
+ * when `def` is given — this is the one place that message is built, so it
31676
+ * can never drift from what actually resolved.
31677
+ */
31678
+ const resolveMode = (def, state, mode) => {
31679
+ const declared = def?.modes?.[mode];
31680
+ const format = steeringFormatFor(mode);
31681
+ if (declared === void 0 && format === void 0) return {
31682
+ kind: "unknown",
31683
+ message: def !== void 0 ? `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})` : `mode "${mode}" is not a built-in steering format`
31684
+ };
31685
+ const validate = declared?.validate !== void 0 ? {
31686
+ kind: "command",
31687
+ command: declared.validate
31688
+ } : format !== void 0 ? {
31689
+ kind: "builtin",
31690
+ format
31691
+ } : void 0;
31692
+ return {
31693
+ kind: "resolved",
31694
+ mode,
31695
+ ...format !== void 0 ? { format } : {},
31696
+ ...declared?.format !== void 0 ? { formatCommand: declared.format } : {},
31697
+ ...validate !== void 0 ? { validate } : {},
31698
+ capabilities: capabilitiesFor(mode, format, validate)
31699
+ };
31700
+ };
31701
+ /** `gtd next`'s fix-retry instruction, printed ahead of the failing command's captured output. */
31702
+ const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
31703
+ /**
31704
+ * Pushes a `binaryGuard` step ahead of `command` when its own leading word
31705
+ * is unambiguous — BEFORE, not wrapped around: a guard that fails must stop
31706
+ * the script on its own `exit 127`, without touching `failurePromptWrapper`'s
31707
+ * captured-output path. Each command guards on its OWN leading word — a
31708
+ * guard naming the wrong binary is worse than none.
31709
+ */
31710
+ const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
31711
+ const binary = extractLeadingBinary(command);
31712
+ if (binary !== void 0) steps.push({
31713
+ kind: "command",
31714
+ command: binaryGuard(binary, mode, key)
31715
+ });
31716
+ steps.push({
31717
+ kind: "command",
31718
+ command: withFileVar$1(command, file),
31719
+ ...onFailure !== void 0 ? { onFailure } : {}
31720
+ });
31721
+ };
31722
+ /**
31723
+ * `resolved`'s `format:`/`validate:` as `EmitStep[]` — format first,
31724
+ * validate last, the last one wrapped with `onFailure` when the validator is
31725
+ * a command (a built-in's own in-process parser has no shell step to wrap:
31726
+ * `gtd check`'s seeded command carries that instead). Each command gets its
31727
+ * own `binaryGuard` immediately ahead of it.
31728
+ */
31729
+ const modeCommandSteps = (resolved, file) => {
31730
+ const steps = [];
31731
+ if (resolved.formatCommand !== void 0) pushGuardedCommand(steps, resolved.mode, "format", resolved.formatCommand, file, void 0);
31732
+ if (resolved.validate?.kind === "command") pushGuardedCommand(steps, resolved.mode, "validate", resolved.validate.command, file, fixPromptInstruction(file));
31733
+ return steps;
31734
+ };
31735
+ /** `<Host.scratchDir>/gtd-mode-sample-<mode>-<pid>.md` — an absolute literal baked in at emit time, never a shell variable. `<pid>` avoids collisions between concurrent `gtd` processes. Never a `/tmp` literal or `mktemp` (`tests/tooling/no-tmp-assumption.test.ts` scans for both). */
31736
+ const scratchSamplePath = (scratchDir, mode) => join(scratchDir, `gtd-mode-sample-${mode}-${process.pid}.md`);
31737
+ /**
31738
+ * The contradiction round-trip/skip-notice steps for `resolved`'s `mode:`.
31739
+ * No `formatCommand` means nothing to round-trip: empty. Otherwise: a live
31740
+ * built-in validator runs the round-trip against that format's own canonical
31741
+ * sample; an external validator prints a one-line skip notice instead, since
31742
+ * silence there would read as a clean bill of health; a format-only mode with
31743
+ * neither has nothing to round-trip either.
31744
+ */
31745
+ const modeContradictionSteps = (resolved) => gen(function* () {
31746
+ if (resolved.formatCommand === void 0) return [];
31747
+ const { capabilities } = resolved;
31748
+ if (capabilities.format !== void 0 && capabilities.liveValidate !== void 0) {
31749
+ const { scratchDir } = yield* Host;
31750
+ const samplePath = scratchSamplePath(scratchDir, resolved.mode);
31751
+ const formatCommand = withFileVar$1(resolved.formatCommand, samplePath);
31752
+ return [{
31753
+ kind: "command",
31754
+ command: buildModeContradictionCheck({
31755
+ mode: resolved.mode,
31756
+ samplePath,
31757
+ sample: capabilities.format.sample,
31758
+ formatCommand
31759
+ })
31760
+ }];
31761
+ }
31762
+ if (capabilities.externalValidate === true) return [{
31763
+ kind: "command",
31764
+ command: modeContradictionSkipNotice(resolved.mode)
31765
+ }];
31766
+ return [];
31767
+ });
31768
+ /**
31769
+ * Renders `resolved`'s complete validate script for `file`: the mode's
31770
+ * format/validate contradiction check, a guard for a not-yet-written file,
31771
+ * then the format/validate commands themselves (format first). gtd never
31772
+ * runs this script itself — `resolveMode`+`validateScriptFor` only render
31773
+ * text for a driver to execute (`gtd renders; the driver executes`).
31774
+ */
31775
+ const validateScriptFor = (resolved, file) => gen(function* () {
31776
+ return { script: emitScripts([
31777
+ ...yield* modeContradictionSteps(resolved),
31778
+ {
31779
+ kind: "command",
31780
+ command: fileExistsGuard(file)
31781
+ },
31782
+ ...modeCommandSteps(resolved, file)
31783
+ ]).required };
31162
31784
  });
31163
31785
  //#endregion
31164
31786
  //#region src/step/planStep.ts
@@ -31171,6 +31793,26 @@ const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot
31171
31793
  kind: "uncheck",
31172
31794
  file: snapshot.file
31173
31795
  }] : [];
31796
+ const gitWriteOf = (landing, message) => {
31797
+ if (landing.only !== void 0) return {
31798
+ kind: "commitPath",
31799
+ path: landing.only,
31800
+ message
31801
+ };
31802
+ if (landing.keepTurn !== void 0) return {
31803
+ kind: "keepTurn",
31804
+ ref: landing.keepTurn,
31805
+ message
31806
+ };
31807
+ if (landing.spec.approved !== void 0) return {
31808
+ kind: "commitClean",
31809
+ message
31810
+ };
31811
+ return {
31812
+ kind: "commitAll",
31813
+ message
31814
+ };
31815
+ };
31174
31816
  const planStep = (snapshot, opts = {}) => {
31175
31817
  const landing = snapshot.landing;
31176
31818
  if (landing.kind === "refusal") return {
@@ -31241,17 +31883,7 @@ const planStep = (snapshot, opts = {}) => {
31241
31883
  ...uncheckStep(snapshot),
31242
31884
  {
31243
31885
  kind: "gitWrite",
31244
- write: landing.keepTurn !== void 0 ? {
31245
- kind: "keepTurn",
31246
- ref: landing.keepTurn,
31247
- message
31248
- } : landing.spec.approved !== void 0 ? {
31249
- kind: "commitClean",
31250
- message
31251
- } : {
31252
- kind: "commitAll",
31253
- message
31254
- }
31886
+ write: gitWriteOf(landing, message)
31255
31887
  },
31256
31888
  ...landing.keepHistory !== void 0 ? [{
31257
31889
  kind: "gitWrite",
@@ -31299,8 +31931,7 @@ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) =>
31299
31931
  * `gtd start <name>`: start a brand NEW process on a workflow, writing an
31300
31932
  * opening commit that names the workflow's first step and carries a
31301
31933
  * `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
31302
- * a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
31303
- * and a `Gtd-Config:` trailer per config file.
31934
+ * and a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base.
31304
31935
  * All validation is a REFUSAL, not an Effect failure.
31305
31936
  */
31306
31937
  const planStart = (current, actor, start) => gen(function* () {
@@ -31338,8 +31969,7 @@ const planStart = (current, actor, start) => gen(function* () {
31338
31969
  to: firstStep,
31339
31970
  workflow: name,
31340
31971
  ...base !== void 0 ? { reviewBase: base } : {},
31341
- vars,
31342
- config: start.config
31972
+ vars
31343
31973
  })
31344
31974
  }
31345
31975
  }, {
@@ -31382,6 +32012,8 @@ const ACTORS = /* @__PURE__ */ new Set([
31382
32012
  "check",
31383
32013
  "judge"
31384
32014
  ]);
32015
+ const isOpening = ({ parsed, step }) => parsed !== void 0 && step === void 0 && parsed.from === void 0 && parsed.actor === "human";
32016
+ const boundsEpisode = (def, { parsed, pin }) => pin !== "add" && (parsed === void 0 || !ACTORS.has(parsed.actor) || parsed.to === def.initial);
31385
32017
  /**
31386
32018
  * Where the episode HEAD belongs to begins, read off subjects alone. Walking
31387
32019
  * back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
@@ -31394,14 +32026,13 @@ const ACTORS = /* @__PURE__ */ new Set([
31394
32026
  const locateEpisode = (def, history) => {
31395
32027
  for (let i = history.length - 1; i >= 0; i--) {
31396
32028
  const message = parseCommitMessage(history[i].message);
31397
- const subject = message.parsed;
31398
- if (subject !== void 0 && message.step === void 0 && subject.from === void 0 && subject.actor === "human") return {
32029
+ if (isOpening(message)) return {
31399
32030
  workflow: message.workflow,
31400
32031
  legacyOpening: message.workflow === void 0,
31401
32032
  baseIndex: i,
31402
32033
  processStart: i
31403
32034
  };
31404
- if (subject === void 0 || !ACTORS.has(subject.actor) || subject.to === def.initial) return {
32035
+ if (boundsEpisode(def, message)) return {
31405
32036
  workflow: void 0,
31406
32037
  legacyOpening: false,
31407
32038
  baseIndex: i,
@@ -31419,7 +32050,7 @@ const headTurnOf = (head) => {
31419
32050
  if (head === void 0) return void 0;
31420
32051
  const message = parseCommitMessage(head.message);
31421
32052
  const subject = message.parsed;
31422
- if (subject === void 0 || !ACTORS.has(subject.actor)) return void 0;
32053
+ if (subject === void 0 || !ACTORS.has(subject.actor) || message.pin !== void 0) return void 0;
31423
32054
  return {
31424
32055
  state: subject.to,
31425
32056
  actor: subject.actor,
@@ -31447,15 +32078,6 @@ const runOf = (history, location, closingHash = void 0) => {
31447
32078
  const fault = historyFormatFault(history, location);
31448
32079
  return fault === void 0 ? succeed$1(buildRun(history, location, closingHash)) : fail$8(new Error(fault));
31449
32080
  };
31450
- const pinnedConfigOf = (history) => {
31451
- for (let i = history.length - 1; i >= 0; i--) {
31452
- const files = parseCommitMessage(history[i].message).config;
31453
- if (Object.keys(files).length > 0) return {
31454
- commit: history[i].hash,
31455
- files
31456
- };
31457
- }
31458
- };
31459
32081
  const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
31460
32082
  step: formatStepId(message.approved.step),
31461
32083
  name: message.approved.name,
@@ -31463,8 +32085,10 @@ const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
31463
32085
  commit: hash
31464
32086
  }];
31465
32087
  const buildRun = (history, location, closingHash) => {
31466
- const processCommits = history.slice(location.processStart);
31467
- const parsed = processCommits.map((c) => parseCommitMessage(c.message));
32088
+ const allProcessCommits = history.slice(location.processStart);
32089
+ const allParsed = allProcessCommits.map((c) => parseCommitMessage(c.message));
32090
+ const processCommits = allProcessCommits.filter((_, i) => allParsed[i].pin !== "add");
32091
+ const parsed = allParsed.filter((m) => m.pin !== "add");
31468
32092
  const first = parsed[0];
31469
32093
  const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
31470
32094
  const head = history[history.length - 1];
@@ -31477,10 +32101,10 @@ const buildRun = (history, location, closingHash) => {
31477
32101
  trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
31478
32102
  costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
31479
32103
  judgeVerdicts: parsed.flatMap((m) => m.judge),
32104
+ pinCommitted: allParsed.some((m) => m.pin === "add"),
31480
32105
  marks: parsed.flatMap((m) => m.marks),
31481
32106
  approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
31482
32107
  pinnedVars: { ...first?.vars },
31483
- pinnedConfig: pinnedConfigOf(history.slice(Math.max(location.baseIndex, 0))),
31484
32108
  headTurn: headTurnOf(head),
31485
32109
  closingHash,
31486
32110
  episode: {
@@ -31516,6 +32140,7 @@ const subjectIsBoundary = (def, subject) => {
31516
32140
  const parsed = parseCommitMessage(subject).parsed;
31517
32141
  if (parsed === void 0 || !ACTORS.has(parsed.actor)) return true;
31518
32142
  if (parsed.actor === "human" && parsed.from === void 0) return false;
32143
+ if (parsed.actor === "check" && parsed.from === PIN_STATES.add) return false;
31519
32144
  return parsed.to === def.initial;
31520
32145
  };
31521
32146
  /** The last page index (walking newest→oldest, same direction as `locateEpisode`) whose subject is a boundary, or `undefined` if none in `page`. */
@@ -31569,18 +32194,6 @@ const findBoundaryBase = (git, def, head) => gen(function* () {
31569
32194
  });
31570
32195
  const historyUpTo = (git, def, head) => flatMap(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
31571
32196
  const computeProcessRun = (git, def, head) => flatMap(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
31572
- const changedSinceStart = (pinned, current, workspace) => {
31573
- if (pinned === void 0) return [];
31574
- return changedConfigFiles(pinned.files, current).filter((path) => {
31575
- const committed = workspace.readCommittedSync(path, pinned.commit);
31576
- return current[path] === void 0 || committed === void 0 || contentHash(committed) !== current[path];
31577
- });
31578
- };
31579
- const warnConfigChanges = (run, current, workspace) => gen(function* () {
31580
- const changed = changedSinceStart(run.pinnedConfig, current, workspace);
31581
- if (changed.length === 0) return;
31582
- yield* (yield* Narrator).warn(`gtd: warning: the config changed since this process started (${changed.join(", ")}) — the rest of the process replays under the changed config`);
31583
- });
31584
32197
  /** The run alone, never replaying — `gtd abandon` must work even when replay would refuse, short of a history it cannot read. */
31585
32198
  const currentRun = gen(function* () {
31586
32199
  const git = yield* GitService;
@@ -31884,31 +32497,32 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
31884
32497
  const run = yield* computeProcessRun(git, config.workflow, ref);
31885
32498
  const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
31886
32499
  const outcome = yield* promise(() => replayFor(setup));
31887
- const error = replayError(outcome);
31888
- if (error !== void 0 || outcome.kind !== "rest") return yield* fail$8(error ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
31889
- const step = outcome.rest;
31890
- yield* (yield* Narrator).narrate(`rest resolved: ${step.name} (awaits ${step.actor})`);
31891
- const stepDef = stepDefOf(step);
32500
+ const pinned = headHasPin(workspace, run);
32501
+ const pin = pinBeatAt(outcome, run, def, pinned);
32502
+ const resting = restingOn(outcome, pin);
32503
+ if (resting === void 0) return yield* fail$8(replayError(outcome) ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
32504
+ const { reached, step } = resting;
32505
+ const { stepDef, state, memory } = restFacts(pin, step, reached.trace, run);
32506
+ yield* (yield* Narrator).narrate(`rest resolved: ${state} (awaits ${stepDef.actor})`);
31892
32507
  if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$8(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
31893
32508
  const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
31894
32509
  status: normalizeStatus(e.status),
31895
32510
  path: e.path
31896
32511
  })) : [];
31897
- const memory = memoryOf(outcome.trace, run);
31898
- const configNow = config.fingerprint;
31899
- if (ref === void 0) yield* warnConfigChanges(run, configNow, workspace);
32512
+ 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`);
31900
32513
  return {
31901
32514
  def,
31902
32515
  workflowName: resolved.name,
31903
- state: step.name,
32516
+ state,
31904
32517
  stepDef,
31905
- actor: step.actor,
32518
+ actor: stepDef.actor,
32519
+ pin,
32520
+ pinned,
31906
32521
  step,
31907
- trace: outcome.trace,
32522
+ trace: reached.trace,
31908
32523
  run,
31909
32524
  vars,
31910
32525
  env,
31911
- config: configNow,
31912
32526
  changes,
31913
32527
  memory: memory.key,
31914
32528
  memoryResumed: memory.resumed,
@@ -31918,6 +32532,52 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
31918
32532
  };
31919
32533
  });
31920
32534
  const currentRest = restAt(void 0);
32535
+ /** The step the process rests at and the trace that reached it — at an unpin, the flow's last step. */
32536
+ const restingOn = (outcome, pin) => {
32537
+ if (outcome.kind === "rest") return {
32538
+ reached: outcome,
32539
+ step: outcome.rest
32540
+ };
32541
+ if (outcome.kind !== "ended" || pin !== "remove") return void 0;
32542
+ const step = outcome.trace.at(-1);
32543
+ return step === void 0 ? void 0 : {
32544
+ reached: outcome,
32545
+ step
32546
+ };
32547
+ };
32548
+ /** A pin or unpin beat has its own script, no step's, and no agent memory. */
32549
+ const restFacts = (pin, step, trace, run) => pin === void 0 ? {
32550
+ stepDef: stepDefOf(step),
32551
+ state: step.name,
32552
+ memory: memoryOf(trace, run)
32553
+ } : {
32554
+ stepDef: pinStepDef(pin),
32555
+ state: PIN_STATES[pin],
32556
+ memory: {
32557
+ key: void 0,
32558
+ resumed: false
32559
+ }
32560
+ };
32561
+ const headHasPin = (workspace, run) => {
32562
+ const head = run.episode.commits.at(-1)?.hash ?? run.episode.base;
32563
+ return head !== void 0 && workspace.treeSync(head).has(".gtd/process.gtdpin");
32564
+ };
32565
+ const underwayAt = (run, step, def) => run.workflow !== void 0 || step.name !== def.initial || step.id.occurrence !== 1;
32566
+ /**
32567
+ * A started process pins first — again, should a turn have deleted the pin;
32568
+ * a pinned flow that ended unpins before the closing commit.
32569
+ */
32570
+ const pinBeatAt = (outcome, run, def, pinned) => {
32571
+ if (flowEnded(outcome, run, def)) return pinned || run.pinCommitted ? "remove" : void 0;
32572
+ if (outcome.kind !== "rest" || pinned) return void 0;
32573
+ return underwayAt(run, outcome.rest, def) ? "add" : void 0;
32574
+ };
32575
+ const flowEnded = (outcome, run, def) => outcome.kind === "ended" || outcome.kind === "rest" && run.workflow === void 0 && outcome.rest.name === def.initial && underwayAt(run, outcome.rest, def);
32576
+ const pinStepDef = (pin) => ({
32577
+ actor: "check",
32578
+ kind: "script",
32579
+ content: pin === "add" ? pinScript(PIN_PATH) : unpinScript(PIN_PATH)
32580
+ });
31921
32581
  /**
31922
32582
  * Whether `gtd start <name>` can start a process here: the workflow must
31923
32583
  * reach a step from the tree the opening commit would capture. That step is
@@ -31961,7 +32621,8 @@ const renderRest = (rest) => succeed$1({
31961
32621
  content: rest.stepDef.content,
31962
32622
  ...rest.hints,
31963
32623
  ...rest.memory !== void 0 ? { memory: rest.memory } : {},
31964
- memoryResumed: rest.memoryResumed
32624
+ memoryResumed: rest.memoryResumed,
32625
+ ...rest.pin !== void 0 ? { pin: rest.pin } : {}
31965
32626
  });
31966
32627
  /**
31967
32628
  * Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
@@ -31969,7 +32630,7 @@ const renderRest = (rest) => succeed$1({
31969
32630
  */
31970
32631
  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;
31971
32632
  /** 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. */
31972
- const noProcessUnderway = (rest) => rest.run.workflow === void 0 && rest.state === rest.def.initial && rest.step.id.occurrence === 1;
32633
+ const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
31973
32634
  /** `idle` means exactly one thing: no process underway, clean tree. */
31974
32635
  const restIsIdle = (rest) => noProcessUnderway(rest) && rest.changes.length === 0;
31975
32636
  const isHumanReviewGate = (def) => def.actor === "human" && def.mode === "review";
@@ -32059,14 +32720,14 @@ const landingTo = (rest, to, branch, approver, marks) => {
32059
32720
  const starts = !underway && to !== rest.def.initial;
32060
32721
  const refused = starts ? multilineRefusal(rest.vars) : void 0;
32061
32722
  if (refused !== void 0) return refused;
32062
- const finishes = underway && to === rest.def.initial;
32723
+ const { finishes, landsAt } = endingOf(rest, to, underway);
32063
32724
  return {
32064
32725
  kind: "commit",
32065
- to,
32726
+ to: landsAt,
32066
32727
  spec: {
32067
32728
  actor: rest.actor,
32068
32729
  from: rest.state,
32069
- to,
32730
+ to: landsAt,
32070
32731
  step: rest.step.id,
32071
32732
  ...marks.length > 0 && { marks },
32072
32733
  ...approvedTrailer(rest, approver),
@@ -32077,10 +32738,55 @@ const landingTo = (rest, to, branch, approver, marks) => {
32077
32738
  ...keptTurn(rest)
32078
32739
  };
32079
32740
  };
32080
- const startTrailers = (rest, starts) => starts ? {
32081
- vars: rest.vars,
32082
- config: rest.config
32083
- } : {};
32741
+ const endingOf = (rest, to, underway) => {
32742
+ const ends = underway && to === rest.def.initial;
32743
+ return {
32744
+ finishes: ends && !rest.pinned,
32745
+ landsAt: ends && rest.pinned ? PIN_STATES.remove : to
32746
+ };
32747
+ };
32748
+ /** A pin or unpin commit carries the pin alone; the unpin commit closes the process. */
32749
+ const pinLanding = (rest, pin, branch) => {
32750
+ const change = rest.changes.find((c) => c.path === PIN_PATH)?.status;
32751
+ return pin === "add" ? pinCommit(rest, change) : unpinCommit(rest, change, branch);
32752
+ };
32753
+ const pinCommit = (rest, change) => {
32754
+ if (change === void 0 || change === "D") return pinRefusal("add");
32755
+ const to = rest.step.name;
32756
+ return {
32757
+ kind: "commit",
32758
+ to,
32759
+ spec: {
32760
+ actor: rest.actor,
32761
+ from: rest.state,
32762
+ to,
32763
+ pin: "add"
32764
+ },
32765
+ only: PIN_PATH
32766
+ };
32767
+ };
32768
+ const unpinCommit = (rest, change, branch) => {
32769
+ if (change !== "D" && rest.pinned) return pinRefusal("remove");
32770
+ const to = rest.def.initial;
32771
+ return {
32772
+ kind: "commit",
32773
+ to,
32774
+ spec: {
32775
+ actor: rest.actor,
32776
+ from: rest.state,
32777
+ to,
32778
+ pin: "remove",
32779
+ ...closingTrailers(branch)
32780
+ },
32781
+ keepHistory: processHistoryRef(rest.run.startHash),
32782
+ ...rest.pinned ? { only: PIN_PATH } : {}
32783
+ };
32784
+ };
32785
+ const pinRefusal = (pin) => ({
32786
+ kind: "refusal",
32787
+ message: pin === "add" ? `gtd land: ${PIN_PATH} is missing — run the pin beat's script (\`gtd next --json=content\`); if its \`gtd bundle\` fails, fix what it reports, or run \`gtd abandon\`` : `gtd land: ${PIN_PATH} is still there — run the unpin beat's script (\`gtd next --json=content\`), then land`
32788
+ });
32789
+ const startTrailers = (rest, starts) => starts ? { vars: rest.vars } : {};
32084
32790
  const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "ended" ? outcome.marks : [];
32085
32791
  /**
32086
32792
  * What landing the pending turn at `rest` does — a pure decision from a
@@ -32088,6 +32794,7 @@ const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "e
32088
32794
  * computed by replaying, never matched.
32089
32795
  */
32090
32796
  const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
32797
+ if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
32091
32798
  const unapproved = approvalRefusal(rest, approver);
32092
32799
  if (unapproved !== void 0) return unapproved;
32093
32800
  const early = cleanLanding(rest);
@@ -32150,16 +32857,20 @@ const summaryFor = (run) => gen(function* () {
32150
32857
  if (summary === void 0 || run.trace.length === 0) return { prompt: void 0 };
32151
32858
  return { prompt: summary(summaryInput(run, settingsFor(resolved, run.pinnedVars, host.env, {}, {}))) };
32152
32859
  });
32153
- /** Repository-relative paths of the config files today's config and the process's own record name; one outside the repository cannot be read at a ref, so it is left out. */
32154
- const inRepoConfigPaths = (root, files, recorded) => {
32155
- const paths = new Set(Object.keys(recorded));
32860
+ /**
32861
+ * Repository-relative paths of today's config files; one outside the
32862
+ * repository cannot be read at a ref, so it is left out. A file only the old
32863
+ * config read needs no check: whichever file imported it differs too.
32864
+ */
32865
+ const inRepoConfigPaths = (root, files) => {
32866
+ const paths = /* @__PURE__ */ new Set();
32156
32867
  for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
32157
32868
  return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
32158
32869
  };
32159
32870
  const configRefusal = (command, tip, paths, reason = "A history ref replays under today's config, so it is unreadable after the config changes") => /* @__PURE__ */ new Error(`${command}: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. ${reason}`);
32160
- const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
32871
+ const differingConfigPaths = (git, workspace, root, config, hash) => gen(function* () {
32161
32872
  const differing = [];
32162
- for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint), run.pinnedConfig?.files ?? {})) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
32873
+ for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint))) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
32163
32874
  return differing;
32164
32875
  });
32165
32876
  const notClosedError = (hash, outcome) => outcome.kind === "rest" ? /* @__PURE__ */ new Error(`gtd history: ${hash.slice(0, 7)} does not close a process under today's config`) : replayError(outcome) ?? /* @__PURE__ */ new Error("gtd history: replay failed");
@@ -32216,7 +32927,7 @@ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
32216
32927
  const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
32217
32928
  const versionWarning = versionWarningFor(recorded);
32218
32929
  const withWarning = (err) => versionWarning === void 0 ? err : /* @__PURE__ */ new Error(`${err.message}\n${versionWarning}`);
32219
- const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
32930
+ const differing = yield* differingConfigPaths(git, workspace, host.root, config, hash);
32220
32931
  if (differing.length > 0) return yield* fail$8(withWarning(configRefusal(how.command, hash, differing, how.configReason)));
32221
32932
  const { setup } = yield* setupFor(config, run, workspace, host);
32222
32933
  const outcome = yield* promise(() => replayFor(setup));
@@ -32228,474 +32939,6 @@ const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
32228
32939
  };
32229
32940
  });
32230
32941
  //#endregion
32231
- //#region src/emit/GitScript.ts
32232
- const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
32233
- /**
32234
- * `--allow-empty` because gtd's workflow commits may be empty on purpose.
32235
- * Mirrors `Git.ts`'s retry: only re-tries without hooks on the specific
32236
- * "empty git commit" rejection message, discriminated from the captured
32237
- * output rather than the exit code, so a genuinely rejected commit (a lint
32238
- * error, a commit-msg hook) still fails.
32239
- */
32240
- const commitAllowEmpty = (message) => {
32241
- const m = shellQuote$1(message);
32242
- return [
32243
- `if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
32244
- ` case "$out" in`,
32245
- ` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} || exit 1 ;;`,
32246
- ` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
32247
- ` esac`,
32248
- `fi`
32249
- ].join("\n");
32250
- };
32251
- const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — revert the edits and run `gtd land` again to approve";
32252
- /** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
32253
- const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
32254
- /**
32255
- * An approval records exactly the judged tree: HEAD is pinned before the
32256
- * clean check and the commit is built over its tree with `commit-tree`, so no
32257
- * hook runs and none can add to it. A HEAD that moved fails the guarded
32258
- * `update-ref`. `commit-tree` ignores `commit.gpgSign`, so it is read here.
32259
- */
32260
- const commitClean = (message) => [
32261
- `gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
32262
- `if [ -n "$(git status --porcelain --untracked-files=all --ignore-submodules=none)" ]; then`,
32263
- ` printf '%s\\n' ${shellQuote$1(CHANGED_SINCE_LAND)} >&2`,
32264
- ` exit 1`,
32265
- `fi`,
32266
- `gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
32267
- `if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
32268
- `gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
32269
- `git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head" || exit 1`
32270
- ].join("\n");
32271
- const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
32272
- const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
32273
- /**
32274
- * POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
32275
- * tree, so no hook runs on it and its tree is the pre-turn tree by
32276
- * construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
32277
- * `-S`, read (and failing closed) before any object or ref is written; the
32278
- * index is already staged by then, so an early exit leaves the turn staged.
32279
- * The kept ref is deleted only after `reset -q` succeeds: it is then the
32280
- * turn's only handle.
32281
- * The branch moves before the final hard reset, so if that reset fails the
32282
- * marker has landed and the turn is still in the working tree (kept ref
32283
- * present); every other failure leaves the branch untouched.
32284
- */
32285
- const keepTurn = (refPrefix, message) => {
32286
- const keptRef = `${shellQuote$1(`${refPrefix}/`)}"$gtd_kept"`;
32287
- return [
32288
- `git add -A || exit 1`,
32289
- `gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
32290
- `if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
32291
- `gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
32292
- `gtd_tree=$(git write-tree) || exit 1`,
32293
- `gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
32294
- `git update-ref ${keptRef} "$gtd_kept" || exit 1`,
32295
- `gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
32296
- `if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref -m ${shellQuote$1(`commit: ${message}`)} HEAD "$gtd_tip" "$gtd_head"; then`,
32297
- ` git reset -q && git update-ref -d ${keptRef}`,
32298
- ` exit 1`,
32299
- `fi`,
32300
- `git reset --hard HEAD`
32301
- ].join("\n");
32302
- };
32303
- const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
32304
- /** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
32305
- const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
32306
- /**
32307
- * No `+` on the refspec: a local ref the remote's tip does not descend from
32308
- * fails the fetch instead of being overwritten (a fast-forward still applies).
32309
- */
32310
- const fetchHistoryRefs = (remote) => [
32311
- `set -eu`,
32312
- `before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
32313
- `git fetch ${shellQuote$1(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
32314
- `after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
32315
- `printf '%s history refs arrived\\n' "$((after - before))"`
32316
- ].join("\n");
32317
- /** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/twins/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
32318
- const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
32319
- const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
32320
- const renderOutcome = (outcome) => {
32321
- switch (outcome.kind) {
32322
- case "transition": return printfLine$1("%s %s → %s\n", [
32323
- "'->'",
32324
- shellQuote$1(outcome.from),
32325
- shellQuote$1(outcome.to)
32326
- ]);
32327
- case "commit": return printfLine$1("%s %s\n", ["'[commit]'", shellQuote$1(outcome.subject)]);
32328
- case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
32329
- }
32330
- };
32331
- const renderGitWrite = (write) => {
32332
- switch (write.kind) {
32333
- case "commitAll": return commitAll(write.message);
32334
- case "commitClean": return commitClean(write.message);
32335
- case "updateRef": return updateRef(write.ref, write.target);
32336
- case "keepTurn": return keepTurn(write.ref, write.message);
32337
- }
32338
- };
32339
- /**
32340
- * Wraps `command` so a non-zero exit prints `prompt` plus the command's
32341
- * captured output before propagating that exit code — mirrors `Emit.ts`'s
32342
- * `failurePromptWrapper` (same reverse-import concern as `renderOutcome`
32343
- * above: `Emit.ts` imports `shellQuote` from here already).
32344
- */
32345
- const failurePromptWrapper$1 = (command, prompt) => {
32346
- return [
32347
- `gtd_validate_status=0`,
32348
- `gtd_validate_out="$( {`,
32349
- command,
32350
- `} 2>&1 )" || gtd_validate_status=$?`,
32351
- `if [ "$gtd_validate_status" -ne 0 ]; then`,
32352
- ` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
32353
- ` exit "$gtd_validate_status"`,
32354
- `fi`
32355
- ].join("\n");
32356
- };
32357
- const renderLandStep = (step) => {
32358
- if (step.kind === "gitWrite") return renderGitWrite(step.write);
32359
- if (step.kind === "outcome") return renderOutcome(step.outcome);
32360
- if (step.kind === "uncheck") return `gtd uncheck ${shellQuote$1(step.file)}`;
32361
- return step.onFailure !== void 0 ? failurePromptWrapper$1(step.command, step.onFailure) : step.command;
32362
- };
32363
- const ScriptSurface = { render: (steps) => {
32364
- const rendered = steps.map(renderLandStep);
32365
- return rendered.length === 0 ? "" : ["set -eu", ...rendered].join("\n\n");
32366
- } };
32367
- //#endregion
32368
- //#region src/emit/Emit.ts
32369
- /** Exits 0 when the declared steering file is absent — nothing to format or validate yet. An OR list, not `if`: its left side failing never trips `set -e`. */
32370
- const fileExistsGuard = (file) => `[ -f ${shellQuote$1(file)} ] || exit 0`;
32371
- /** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
32372
- const withFileVar$1 = (command, file) => `export GTD_FILE=${shellQuote$1(file)}\n${command}`;
32373
- const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/;
32374
- const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/;
32375
- const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g;
32376
- /** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
32377
- const commandForFile = (command, file) => command.replace(FILE_VAR_RE, shellQuote$1(file));
32378
- /**
32379
- * The leading word of a rendered `format:`/`validate:` command, when the
32380
- * command has exactly one unambiguous binary to probe — `undefined` for
32381
- * anything a shell metacharacter could hide a second command behind. A missing
32382
- * guard degrades to a raw exit; a wrong guard would refuse a working command.
32383
- */
32384
- const extractLeadingBinary = (command) => {
32385
- if (command.includes("\n")) return void 0;
32386
- if (SHELL_METACHARACTER_RE.test(command.replace(FILE_VAR_RE, ""))) return void 0;
32387
- const match = SIMPLE_LEADING_WORD_RE.exec(command);
32388
- return match === null ? void 0 : match[0];
32389
- };
32390
- /**
32391
- * Names a typo'd or uninstalled binary and the `$PATH` it was looked up on.
32392
- * `$PATH` is a shell word the DRIVER's shell expands at run time, so gtd never
32393
- * resolves it in-process. Exits 127, matching bash's "command not found".
32394
- */
32395
- const binaryGuard = (binary, mode, key) => {
32396
- const messageQ = shellQuote$1(`gtd: mode "${mode}": "${key}" command not found: ${binary}`);
32397
- return [
32398
- `command -v ${shellQuote$1(binary)} >/dev/null 2>&1 || {`,
32399
- ` printf '%s\\n%s\\n' ${messageQ} '$PATH: '"$PATH"`,
32400
- ` exit 127`,
32401
- `}`
32402
- ].join("\n");
32403
- };
32404
- /**
32405
- * Prints `prompt` plus the captured output on a non-zero exit, then propagates
32406
- * it. The `{ … }` group inlines a multi-line `command` verbatim; the
32407
- * assignment sits left of `||` so `set -e` never trips on the failure itself.
32408
- */
32409
- const failurePromptWrapper = (command, prompt) => {
32410
- return [
32411
- `gtd_validate_status=0`,
32412
- `gtd_validate_out="$( {`,
32413
- command,
32414
- `} 2>&1 )" || gtd_validate_status=$?`,
32415
- `if [ "$gtd_validate_status" -ne 0 ]; then`,
32416
- ` printf '%s\\n\\n%s\\n' ${shellQuote$1(prompt)} "$gtd_validate_out"`,
32417
- ` exit "$gtd_validate_status"`,
32418
- `fi`
32419
- ].join("\n");
32420
- };
32421
- const renderStep = (step) => {
32422
- if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
32423
- return step.command;
32424
- };
32425
- const assembleScript = (steps) => steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n");
32426
- /** 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. */
32427
- const emitScripts = (required = [], optional = []) => ({
32428
- required: assembleScript(required),
32429
- optional: assembleScript(optional)
32430
- });
32431
- /** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
32432
- const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
32433
- const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
32434
- const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
32435
- /**
32436
- * A write command's single pasteable script (`gtd abandon | sh`,
32437
- * `gtd land --json=script`'s own field). `optional`, when non-empty, is
32438
- * wrapped in a subshell whose failure is swallowed — presentation-only, so it
32439
- * must never turn a landed turn into a non-zero exit.
32440
- */
32441
- const combinedScript = (required, optional) => {
32442
- if (required.length === 0) return "";
32443
- if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
32444
- return [
32445
- DID_NOT_RUN_COMMENT,
32446
- "",
32447
- required,
32448
- "",
32449
- PRESENTATION_ONLY_COMMENT,
32450
- `(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
32451
- ].join("\n");
32452
- };
32453
- //#endregion
32454
- //#region src/emit/OutcomeScript.ts
32455
- /**
32456
- * A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
32457
- * tokens (a `shellQuote`d literal, a `"$(git …)"` substitution), never
32458
- * re-quoted here: a value reaches the script as a printf ARGUMENT, never
32459
- * interpolated into the format itself, so a subject or state name containing
32460
- * `%` can never be read as a conversion spec. Markers like `->` are arguments
32461
- * too, keeping every format string free of a leading `-` that a shell's
32462
- * `printf` could mistake for an option. A real newline in `fmt` is emitted as
32463
- * the two-character `\n` escape `printf` itself expands, keeping every emitted
32464
- * statement on one line.
32465
- */
32466
- const printfLine = (fmt, args) => `${OUTCOME_MARKER}\nprintf ${shellQuote$1(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
32467
- /**
32468
- * Marks an emitted block as print-only. Load-bearing beyond documentation:
32469
- * `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
32470
- * this line, and a bare `printf` would be ambiguous with a workflow
32471
- * `script:` command that happens to start the same way.
32472
- */
32473
- const OUTCOME_MARKER = "# gtd: outcome (print-only)";
32474
- const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
32475
- 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";
32476
- 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";
32477
- /** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
32478
- const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
32479
- /** One already-rendered plain line. */
32480
- const noteOutcome = (text) => printfLine("%s\n", [shellQuote$1(text)]);
32481
- /**
32482
- * Resolves the post-hoc short hash/subject from `head` (a commitish) in-script
32483
- * — this line runs AFTER the reset that made `head` the new HEAD, so neither
32484
- * value is knowable when gtd generates it.
32485
- */
32486
- const abandonedOutcome = (from, head, state) => printfLine(FMT_ABANDONED, [
32487
- shellQuote$1(from),
32488
- `"$(git rev-parse --short ${shellQuote$1(head)})"`,
32489
- `"$(git log -1 --format=%s ${shellQuote$1(head)})"`,
32490
- shellQuote$1(state)
32491
- ]);
32492
- /** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders. */
32493
- const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
32494
- /** Resolves the post-hoc short hash/subject from `to` (a commitish) in-script, for the same reason `abandonedOutcome` does. */
32495
- const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
32496
- `"$(git rev-parse --short ${shellQuote$1(to)})"`,
32497
- `"$(git log -1 --format=%s ${shellQuote$1(to)})"`,
32498
- shellQuote$1(state)
32499
- ]);
32500
- //#endregion
32501
- //#region src/emit/ModeContradiction.ts
32502
- /**
32503
- * Written for two readers at once, since gtd validate's exit code carries no
32504
- * second meaning to distinguish them: the agent (this is a CONFIGURATION BUG,
32505
- * not the steering file — stop, don't edit it, or a fix-retry loop will
32506
- * thrash on the document) and the human (which mode, and the exact rendered
32507
- * `format:` command, to fix `gtd.config.ts` without reproducing the failure).
32508
- */
32509
- const contradictionMessage = (mode, formatCommand) => `gtd: mode "${mode}"'s format: command breaks its own validator — this is a CONFIGURATION BUG, not a problem with the steering file. Do NOT edit the steering file — stop and end your turn now.\nThe rendered format: command that ran:\n${formatCommand}\nWhat it turned mode "${mode}"'s own canonical sample into:`;
32510
- /**
32511
- * Formats a copy of the mode's own canonical sample and re-validates it with
32512
- * `gtd check <mode>`, failing loudly when the round-trip breaks it — detects a
32513
- * mode whose `format:` contradicts its own `validate:` without ever touching
32514
- * the real steering file. `printf '%s'` over a `shellQuote`d sample, never a
32515
- * heredoc, since no delimiter chosen ahead of time can collide with the
32516
- * sample's own bytes. One bash fragment with no blank lines (so
32517
- * `src/emit/Emit.ts`'s `assembleScript` never splits it from a sibling step), meant
32518
- * to run BEFORE `[ -f <file> ] || exit 0` in the emitted validate script so it
32519
- * still fires at a first-write beat where the real steering file doesn't
32520
- * exist yet.
32521
- */
32522
- const buildModeContradictionCheck = (inputs) => {
32523
- const { mode, samplePath, sample, formatCommand } = inputs;
32524
- const pathQ = shellQuote$1(samplePath);
32525
- const messageQ = shellQuote$1(contradictionMessage(mode, formatCommand));
32526
- return [
32527
- `printf '%s' ${shellQuote$1(sample)} > ${pathQ}`,
32528
- formatCommand,
32529
- `gtd check ${mode} ${pathQ} >/dev/null 2>&1 || {`,
32530
- ` printf '%s\\n' ${messageQ} >&2`,
32531
- ` cat ${pathQ} >&2`,
32532
- ` rm -f ${pathQ}`,
32533
- ` exit 1`,
32534
- `}`,
32535
- `rm -f ${pathQ}`
32536
- ].join("\n");
32537
- };
32538
- /**
32539
- * Skip notice for a mode whose validator is EXTERNAL (a user `validate:`
32540
- * command, not one of the two built-in modes) — there's no in-process parser
32541
- * to round-trip a sample through. Printed to stderr since silence would read
32542
- * as a clean bill of health.
32543
- */
32544
- const modeContradictionSkipNotice = (mode) => {
32545
- return `printf '%s\\n' ${shellQuote$1(`gtd: mode "${mode}" has an external validate: command — skipping the format/validate contradiction check`)} >&2`;
32546
- };
32547
- //#endregion
32548
- //#region src/emit/SteeringMode.ts
32549
- init_dist$1();
32550
- init_steering();
32551
- /**
32552
- * `format` alone (no live/external verdict) when there's no validator at
32553
- * all; a live in-process validator for a `"builtin"` validate OR a command
32554
- * that is exactly the format's own seeded string; `externalValidate` for any
32555
- * other command. Each arm returns directly — a flat sequence, not nested
32556
- * ternaries — so the four outcomes stay each their own case to read, not a
32557
- * boolean expression to re-derive.
32558
- */
32559
- const capabilitiesFor = (mode, format, validate) => {
32560
- if (format === void 0) return validate?.kind === "command" ? { externalValidate: true } : {};
32561
- if (validate?.kind === "builtin") return {
32562
- format,
32563
- liveValidate: format.validate
32564
- };
32565
- if (validate === void 0) return { format };
32566
- if (isSeededValidateCommand(mode, validate.command)) return {
32567
- format,
32568
- liveValidate: format.validate
32569
- };
32570
- return {
32571
- format,
32572
- externalValidate: true
32573
- };
32574
- };
32575
- /**
32576
- * Resolve a `mode:` name against `def.modes` plus the built-in registry
32577
- * (`src/workflow/SteeringFormats.ts`) — half by half: a declared `format:`/`validate:`
32578
- * wins, and an undeclared `validate:` falls back to the built-in format's own
32579
- * parser when the name is registered. `format` is set from the registry
32580
- * ALONE, independent of which half of `validate` wins — a declared validator
32581
- * overrides validation, not the format identity.
32582
- *
32583
- * `def: undefined` is the LSP's built-in-only fallback — resolving `mode`
32584
- * against the registry alone, with no workflow definition in hand (e.g. the
32585
- * basename dispatch for a `REVIEW.md` no config maps).
32586
- *
32587
- * The `"unknown"` arm carries the whole refusal message, naming what IS known
32588
- * when `def` is given — this is the one place that message is built, so it
32589
- * can never drift from what actually resolved.
32590
- */
32591
- const resolveMode = (def, state, mode) => {
32592
- const declared = def?.modes?.[mode];
32593
- const format = steeringFormatFor(mode);
32594
- if (declared === void 0 && format === void 0) return {
32595
- kind: "unknown",
32596
- message: def !== void 0 ? `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})` : `mode "${mode}" is not a built-in steering format`
32597
- };
32598
- const validate = declared?.validate !== void 0 ? {
32599
- kind: "command",
32600
- command: declared.validate
32601
- } : format !== void 0 ? {
32602
- kind: "builtin",
32603
- format
32604
- } : void 0;
32605
- return {
32606
- kind: "resolved",
32607
- mode,
32608
- ...format !== void 0 ? { format } : {},
32609
- ...declared?.format !== void 0 ? { formatCommand: declared.format } : {},
32610
- ...validate !== void 0 ? { validate } : {},
32611
- capabilities: capabilitiesFor(mode, format, validate)
32612
- };
32613
- };
32614
- /** `gtd next`'s fix-retry instruction, printed ahead of the failing command's captured output. */
32615
- const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
32616
- /**
32617
- * Pushes a `binaryGuard` step ahead of `command` when its own leading word
32618
- * is unambiguous — BEFORE, not wrapped around: a guard that fails must stop
32619
- * the script on its own `exit 127`, without touching `failurePromptWrapper`'s
32620
- * captured-output path. Each command guards on its OWN leading word — a
32621
- * guard naming the wrong binary is worse than none.
32622
- */
32623
- const pushGuardedCommand = (steps, mode, key, command, file, onFailure) => {
32624
- const binary = extractLeadingBinary(command);
32625
- if (binary !== void 0) steps.push({
32626
- kind: "command",
32627
- command: binaryGuard(binary, mode, key)
32628
- });
32629
- steps.push({
32630
- kind: "command",
32631
- command: withFileVar$1(command, file),
32632
- ...onFailure !== void 0 ? { onFailure } : {}
32633
- });
32634
- };
32635
- /**
32636
- * `resolved`'s `format:`/`validate:` as `EmitStep[]` — format first,
32637
- * validate last, the last one wrapped with `onFailure` when the validator is
32638
- * a command (a built-in's own in-process parser has no shell step to wrap:
32639
- * `gtd check`'s seeded command carries that instead). Each command gets its
32640
- * own `binaryGuard` immediately ahead of it.
32641
- */
32642
- const modeCommandSteps = (resolved, file) => {
32643
- const steps = [];
32644
- if (resolved.formatCommand !== void 0) pushGuardedCommand(steps, resolved.mode, "format", resolved.formatCommand, file, void 0);
32645
- if (resolved.validate?.kind === "command") pushGuardedCommand(steps, resolved.mode, "validate", resolved.validate.command, file, fixPromptInstruction(file));
32646
- return steps;
32647
- };
32648
- /** `<Host.scratchDir>/gtd-mode-sample-<mode>-<pid>.md` — an absolute literal baked in at emit time, never a shell variable. `<pid>` avoids collisions between concurrent `gtd` processes. Never a `/tmp` literal or `mktemp` (`tests/tooling/no-tmp-assumption.test.ts` scans for both). */
32649
- const scratchSamplePath = (scratchDir, mode) => join(scratchDir, `gtd-mode-sample-${mode}-${process.pid}.md`);
32650
- /**
32651
- * The contradiction round-trip/skip-notice steps for `resolved`'s `mode:`.
32652
- * No `formatCommand` means nothing to round-trip: empty. Otherwise: a live
32653
- * built-in validator runs the round-trip against that format's own canonical
32654
- * sample; an external validator prints a one-line skip notice instead, since
32655
- * silence there would read as a clean bill of health; a format-only mode with
32656
- * neither has nothing to round-trip either.
32657
- */
32658
- const modeContradictionSteps = (resolved) => gen(function* () {
32659
- if (resolved.formatCommand === void 0) return [];
32660
- const { capabilities } = resolved;
32661
- if (capabilities.format !== void 0 && capabilities.liveValidate !== void 0) {
32662
- const { scratchDir } = yield* Host;
32663
- const samplePath = scratchSamplePath(scratchDir, resolved.mode);
32664
- const formatCommand = withFileVar$1(resolved.formatCommand, samplePath);
32665
- return [{
32666
- kind: "command",
32667
- command: buildModeContradictionCheck({
32668
- mode: resolved.mode,
32669
- samplePath,
32670
- sample: capabilities.format.sample,
32671
- formatCommand
32672
- })
32673
- }];
32674
- }
32675
- if (capabilities.externalValidate === true) return [{
32676
- kind: "command",
32677
- command: modeContradictionSkipNotice(resolved.mode)
32678
- }];
32679
- return [];
32680
- });
32681
- /**
32682
- * Renders `resolved`'s complete validate script for `file`: the mode's
32683
- * format/validate contradiction check, a guard for a not-yet-written file,
32684
- * then the format/validate commands themselves (format first). gtd never
32685
- * runs this script itself — `resolveMode`+`validateScriptFor` only render
32686
- * text for a driver to execute (`gtd renders; the driver executes`).
32687
- */
32688
- const validateScriptFor = (resolved, file) => gen(function* () {
32689
- return { script: emitScripts([
32690
- ...yield* modeContradictionSteps(resolved),
32691
- {
32692
- kind: "command",
32693
- command: fileExistsGuard(file)
32694
- },
32695
- ...modeCommandSteps(resolved, file)
32696
- ]).required };
32697
- });
32698
- //#endregion
32699
32942
  //#region src/cli/ToolCommands.ts
32700
32943
  init_dist$1();
32701
32944
  /**
@@ -32753,7 +32996,7 @@ const runStraysCommand = (command, json, out) => gen(function* () {
32753
32996
  const narrator = yield* Narrator;
32754
32997
  const base = yield* resolveSide(git, command.base);
32755
32998
  const tip = yield* resolveSide(git, command.tip);
32756
- const touched = yield* git.diffPaths(base, tip);
32999
+ const touched = (yield* git.diffPaths(base, tip)).filter((path) => path !== PIN_PATH);
32757
33000
  const strays = strayPaths(touched, command.globs);
32758
33001
  const total = touched.length;
32759
33002
  if (json.kind === "select") yield* writeSelection(out, strays, json.path).pipe(catchIf((error) => error instanceof SelectorUsageError && /^\d+$/.test(json.path), () => void_));
@@ -33655,8 +33898,7 @@ const startWorkflow = (workflow, varOverrides, json, out, commandLabel) => gen(f
33655
33898
  workflow: resolved,
33656
33899
  first: started,
33657
33900
  commandLabel,
33658
- vars: varOverrides,
33659
- config: rest.config
33901
+ vars: varOverrides
33660
33902
  });
33661
33903
  if (plan.kind === "refusal") return yield* fail$8(new Error(plan.message));
33662
33904
  yield* writeScript(json, out, listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
@@ -33768,6 +34010,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
33768
34010
  kind: "gitWrite",
33769
34011
  command: mixedResetTo(run.startParentHash)
33770
34012
  },
34013
+ {
34014
+ kind: "gitWrite",
34015
+ command: unpinScript(PIN_PATH)
34016
+ },
33771
34017
  {
33772
34018
  kind: "outcome",
33773
34019
  command: abandonedOutcome(restState, run.startParentHash, initial)
@@ -33925,7 +34171,7 @@ const runCodeThreadsCheckCommand = () => gen(function* () {
33925
34171
  if (rest.run.trace.length === 0) return;
33926
34172
  const git = yield* GitService;
33927
34173
  const workspace = yield* Workspace;
33928
- const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D").flatMap(({ path }) => {
34174
+ const lines = (yield* git.changedPaths(rest.run.diffBase)).filter((c) => c.status !== "D" && c.path !== ".gtd/process.gtdpin").flatMap(({ path }) => {
33929
34175
  const { threads, findings } = parseCodeThreads(path, workspace.atPath(path) ?? "");
33930
34176
  return [...threads.filter((t) => t.waitingOn === "human").map((t) => `${path}:${t.line + 1}: ${t.entries[0].text}`), ...findings.map((f) => `${path}:${(f.line ?? 0) + 1}: ${f.message}`)];
33931
34177
  });
@@ -34655,6 +34901,7 @@ const flagHeader = (row) => {
34655
34901
  };
34656
34902
  const GLOBAL_OPTIONS = [
34657
34903
  ["-v, --verbose", "Narrate on stderr what gtd resolved"],
34904
+ ["--live", "Run this gtd, not the process's pin; it may misread the process"],
34658
34905
  ["-h, --help", "Print help; gtd <command> --help for one command"],
34659
34906
  ["-V, --version", "Print the version"],
34660
34907
  ["--", "End the options: every word after it is an argument"]
@@ -34666,7 +34913,7 @@ const EXTRA_TOPICS = [[
34666
34913
  ], [
34667
34914
  "version",
34668
34915
  "Print the version",
34669
- "Print gtd's version."
34916
+ "Print gtd's version, and the version of a pinned process's gtd."
34670
34917
  ]];
34671
34918
  const EXAMPLES = ` $ gtd start list what can start
34672
34919
  $ gtd start <name> start a process
@@ -35164,6 +35411,108 @@ const runCli = (argv, io) => {
35164
35411
  }), provide(io.layers(plan.verbose)), sandbox, catch_((cause) => hasInterruptsOnly(cause) ? failCause$1(cause) : report(io, plan.json.kind !== "off")(cause)));
35165
35412
  };
35166
35413
  //#endregion
35414
+ //#region src/cli/Launch.ts
35415
+ /** What the core a pin carries lacks, or what must read the live state: these never hand over. */
35416
+ const LIVE_KINDS = /* @__PURE__ */ new Set([
35417
+ "ui",
35418
+ "lsp",
35419
+ "guide",
35420
+ "bundle",
35421
+ "historyList",
35422
+ "historyShow",
35423
+ "historyMetrics",
35424
+ "historyJudges",
35425
+ "historyFetch"
35426
+ ]);
35427
+ const LIVE_FLAG = "--live";
35428
+ const optionWords = (argv) => {
35429
+ const end = argv.indexOf("--", 2);
35430
+ return argv.slice(2, end < 0 ? void 0 : end);
35431
+ };
35432
+ const withoutLiveFlag = (argv) => {
35433
+ const end = argv.indexOf("--", 2);
35434
+ return argv.filter((word, i) => word !== LIVE_FLAG || i < 2 || end >= 0 && i > end);
35435
+ };
35436
+ /**
35437
+ * Whether this gtd runs the command or hands it to the process's pin. A pin
35438
+ * this gtd cannot run is refused, never guessed at; a usage error the live
35439
+ * parser finds is still the pin's to judge, since its engine may read
35440
+ * another CLI.
35441
+ */
35442
+ const launchFor = (argv, pinFile) => {
35443
+ if (globalThis[PIN_KEY] !== void 0) return {
35444
+ kind: "live",
35445
+ argv
35446
+ };
35447
+ const live = optionWords(argv).includes(LIVE_FLAG);
35448
+ const rest = withoutLiveFlag(argv);
35449
+ if (live) return {
35450
+ kind: "live",
35451
+ argv: rest
35452
+ };
35453
+ const plan = parseArgv(rest);
35454
+ const version = plan.kind === "output" && plan.stdout === `${GTD_VERSION}\n`;
35455
+ if (plan.kind === "output" && !version) return {
35456
+ kind: "live",
35457
+ argv: rest
35458
+ };
35459
+ if (plan.kind === "command" && LIVE_KINDS.has(plan.command.kind)) return {
35460
+ kind: "live",
35461
+ argv: rest
35462
+ };
35463
+ const found = pinFile();
35464
+ if (found === void 0) return {
35465
+ kind: "live",
35466
+ argv: rest
35467
+ };
35468
+ const header = pinHeader(found.bytes);
35469
+ if (version) return {
35470
+ kind: "output",
35471
+ stdout: header === void 0 ? plan.stdout : `${GTD_VERSION} (this process is pinned to gtd ${header.version})\n`
35472
+ };
35473
+ if (header === void 0 || header.format !== 1) {
35474
+ const what = header === void 0 ? "is no pin this gtd can read" : `is pin format ${header.format}, made by gtd ${header.version}`;
35475
+ return {
35476
+ kind: "refuse",
35477
+ message: `gtd: ${found.path} ${what}; gtd ${GTD_VERSION} runs pin format 1 — upgrade gtd (\`gtd --live\` runs this gtd instead, and may misread the process)`
35478
+ };
35479
+ }
35480
+ return {
35481
+ kind: "pin",
35482
+ argv: rest,
35483
+ path: found.path,
35484
+ version: header.version,
35485
+ source: () => readPin(found.bytes).source
35486
+ };
35487
+ };
35488
+ const isJsonFlag = (word) => word === "--json" || word.startsWith("--json=");
35489
+ const refusalText = (argv, message) => (optionWords(argv).some(isJsonFlag) ? `${JSON.stringify({
35490
+ state: "error",
35491
+ prompt: message
35492
+ })}\n` : "") + `${message}\n`;
35493
+ const runPin = async (launch, launcher) => {
35494
+ const words = optionWords(launch.argv);
35495
+ if (words.includes("--verbose") || words.includes("-v")) launcher.stderr(`launcher: running the pin ${launch.path} (gtd ${launch.version})\n`);
35496
+ try {
35497
+ await launcher.pin(launch);
35498
+ } catch (error) {
35499
+ const reason = error instanceof Error ? error.message : String(error);
35500
+ launcher.stderr(`gtd: could not run the pin ${launch.path}: ${reason}\n`);
35501
+ launcher.exit(1);
35502
+ }
35503
+ };
35504
+ const runLauncher = async (argv, pinFile, launcher) => {
35505
+ const launch = launchFor(argv, pinFile);
35506
+ switch (launch.kind) {
35507
+ case "live": return launcher.live(launch.argv);
35508
+ case "output": return launcher.stdout(launch.stdout);
35509
+ case "refuse":
35510
+ launcher.stderr(refusalText(argv, launch.message));
35511
+ return launcher.exit(1);
35512
+ case "pin": return runPin(launch, launcher);
35513
+ }
35514
+ };
35515
+ //#endregion
35167
35516
  //#region node_modules/@trpc/server/dist/codes-DagpWZLc.mjs
35168
35517
  /**
35169
35518
  * Ensures there are no duplicate keys when building a procedure.
@@ -51190,25 +51539,40 @@ const liveToolCommands = effect(ToolCommands, gen(function* () {
51190
51539
  //#endregion
51191
51540
  //#region src/main.ts
51192
51541
  init_dist$1();
51193
- let deathSignal;
51194
- process.once("SIGINT", () => {
51195
- deathSignal = "SIGINT";
51196
- });
51197
- process.once("SIGTERM", () => {
51198
- deathSignal ??= "SIGTERM";
51199
- });
51200
- process.once("SIGHUP", () => {
51201
- deathSignal = "SIGHUP";
51202
- process.kill(process.pid, "SIGTERM");
51542
+ const runLive = (argv) => {
51543
+ let deathSignal;
51544
+ process.once("SIGINT", () => {
51545
+ deathSignal = "SIGINT";
51546
+ });
51547
+ process.once("SIGTERM", () => {
51548
+ deathSignal ??= "SIGTERM";
51549
+ });
51550
+ process.once("SIGHUP", () => {
51551
+ deathSignal = "SIGHUP";
51552
+ process.kill(process.pid, "SIGTERM");
51553
+ });
51554
+ runMain(runCli(argv, nodeCliIo(liveToolCommands)), { teardown: (exit, onExit) => {
51555
+ if (deathSignal === void 0) {
51556
+ defaultTeardown(exit, onExit);
51557
+ return;
51558
+ }
51559
+ const signal = deathSignal;
51560
+ process.removeAllListeners(signal);
51561
+ process.kill(process.pid, signal);
51562
+ } });
51563
+ };
51564
+ await runLauncher(process.argv, livePinFile(process.cwd(), PIN_PATH), {
51565
+ stdout: (chunk) => {
51566
+ process.stdout.write(chunk);
51567
+ },
51568
+ stderr: (chunk) => {
51569
+ process.stderr.write(chunk);
51570
+ },
51571
+ exit: (code) => {
51572
+ process.exitCode = code;
51573
+ },
51574
+ live: runLive,
51575
+ pin: (launch) => runPinModule(launch.path, launch.source())
51203
51576
  });
51204
- runMain(runCli(process.argv, nodeCliIo(liveToolCommands)), { teardown: (exit, onExit) => {
51205
- if (deathSignal === void 0) {
51206
- defaultTeardown(exit, onExit);
51207
- return;
51208
- }
51209
- const signal = deathSignal;
51210
- process.removeAllListeners(signal);
51211
- process.kill(process.pid, signal);
51212
- } });
51213
51577
  //#endregion
51214
51578
  export {};