@pmelab/gtd 62.0.0 → 63.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.
@@ -29501,9 +29501,9 @@ const withoutPath = (tree, hidden) => {
29501
29501
  const OLDEST_READABLE_FORMAT = 1;
29502
29502
  const short$1 = (hash) => hash.slice(0, 7);
29503
29503
  const formatFault = (commits) => {
29504
- const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format > 2);
29504
+ const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format > 3);
29505
29505
  if (foreign === void 0) return void 0;
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`;
29506
+ return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads formats ${OLDEST_READABLE_FORMAT} to 3 — run a gtd release that reads format ${foreign.format} to continue this process`;
29507
29507
  };
29508
29508
  //#endregion
29509
29509
  //#region src/replay/Trailers.ts
@@ -29634,6 +29634,9 @@ const TRAILER_READERS = {
29634
29634
  "Gtd-Pin": (value, into) => {
29635
29635
  if (value === "add" || value === "remove") into.pin ??= value;
29636
29636
  },
29637
+ "Gtd-Anchor": (value, into) => {
29638
+ if (HASH_RE.test(value)) into.anchor ??= value;
29639
+ },
29637
29640
  "Gtd-Format": (value, into) => {
29638
29641
  if (FORMAT_RE.test(value)) into.format ??= Number(value);
29639
29642
  },
@@ -29657,6 +29660,7 @@ const parseCommitMessage = (message) => {
29657
29660
  kept: void 0,
29658
29661
  approved: void 0,
29659
29662
  pin: void 0,
29663
+ anchor: void 0,
29660
29664
  judge: [],
29661
29665
  marks: [],
29662
29666
  vars: {},
@@ -29686,9 +29690,10 @@ const formatCommitMessage = (spec) => {
29686
29690
  ...(spec.marks ?? []).map((m) => `Gtd-Mark: ${m.name}=${m.value}`),
29687
29691
  ...trailer("Approved", spec.approved && encodeApproval(spec.approved)),
29688
29692
  ...trailer("Pin", spec.pin),
29693
+ ...trailer("Anchor", spec.anchor),
29689
29694
  ...trailer("Branch", spec.branch),
29690
29695
  ...trailer("Version", spec.version),
29691
- `Gtd-Format: 2`
29696
+ `Gtd-Format: 3`
29692
29697
  ];
29693
29698
  return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
29694
29699
  };
@@ -29782,6 +29787,27 @@ const budgeted = (evidence, budgetBytes) => {
29782
29787
  };
29783
29788
  const sameSkills = (a, b) => (a?.length ?? 0) === (b?.length ?? 0) && (a ?? []).every((skill, i) => skill === b?.[i]);
29784
29789
  const samePersona = (a, b) => a.model === b.model && a.system === b.system && sameSkills(a.skills, b.skills) && sameAccess(a.access, b.access);
29790
+ const anchorOf = (parsed) => parsed.pin === "add" ? parsed.anchor : void 0;
29791
+ const isAnchor = (commit) => anchorOf(commit.parsed) !== void 0;
29792
+ /**
29793
+ * The commits every anchor sets aside — those between the commit it names and
29794
+ * itself — or a fault for an anchor naming no commit still read.
29795
+ */
29796
+ const setAsideBy = (base, all) => {
29797
+ const setAside = /* @__PURE__ */ new Set();
29798
+ if (all.every((c) => anchorOf(c.parsed) === void 0)) return { setAside };
29799
+ let kept = [];
29800
+ for (const commit of all) {
29801
+ const anchor = anchorOf(commit.parsed);
29802
+ if (anchor !== void 0) {
29803
+ const at = anchor === base ? -1 : kept.indexOf(anchor);
29804
+ if (anchor !== base && at === -1) return { fault: `gtd: commit ${short(commit.hash)} anchors the process at ${short(anchor)}, which is no commit replay reads in this episode` };
29805
+ for (const hash of kept.splice(at + 1)) setAside.add(hash);
29806
+ }
29807
+ kept.push(commit.hash);
29808
+ }
29809
+ return { setAside };
29810
+ };
29785
29811
  /**
29786
29812
  * Replay one episode: run its entry flow, answering each step from the
29787
29813
  * episode's commits in order, until a step has no commit left — the rest.
@@ -29855,19 +29881,39 @@ const withoutPin = (input) => {
29855
29881
  };
29856
29882
  const replay = async (raw) => {
29857
29883
  const input = withoutPin(raw);
29858
- const commits = input.episode.commits.map((c) => ({
29884
+ const all = input.episode.commits.map((c) => ({
29859
29885
  ...c,
29860
29886
  parsed: parseCommitMessage(c.message)
29861
29887
  }));
29888
+ const resolved = setAsideBy(input.episode.base.hash, all);
29889
+ if ("fault" in resolved) return {
29890
+ kind: "failed",
29891
+ message: resolved.fault
29892
+ };
29893
+ const setAside = resolved.setAside;
29894
+ const commits = all.filter((c) => !setAside.has(c.hash));
29895
+ const pinned = (raw.episode.commits.at(-1)?.tree ?? raw.episode.base.tree).paths().includes(PIN_PATH);
29862
29896
  const treeAt = (hash) => hash === input.episode.base.hash ? input.episode.base.tree : commits.find((c) => c.hash === hash)?.tree ?? (hash === input.start ? input.startTree : void 0);
29863
29897
  let cursor = 0;
29864
29898
  let pendingUsed = false;
29865
29899
  let position = input.episode.base;
29866
29900
  const skipPins = () => {
29867
- while (cursor < commits.length && commits[cursor].parsed.pin !== void 0) cursor++;
29901
+ while (cursor < commits.length && commits[cursor].parsed.pin !== void 0) {
29902
+ const commit = commits[cursor];
29903
+ cursor++;
29904
+ if (!isAnchor(commit)) continue;
29905
+ position = commit;
29906
+ const to = commit.parsed.parsed?.to;
29907
+ expectedNext = to === void 0 || to === UNPIN_STATE ? void 0 : {
29908
+ name: to,
29909
+ hash: commit.hash
29910
+ };
29911
+ anchorPending = expectedNext !== void 0;
29912
+ }
29868
29913
  };
29869
29914
  let previousPosition = input.episode.base;
29870
29915
  let expectedNext;
29916
+ let anchorPending = false;
29871
29917
  const occurrences = /* @__PURE__ */ new Map();
29872
29918
  const personas = /* @__PURE__ */ new Map();
29873
29919
  const scopes = [];
@@ -30010,14 +30056,18 @@ const replay = async (raw) => {
30010
30056
  }
30011
30057
  return samePersona(seen, identity) ? void 0 : `gtd: "${step.name}" runs with a different model, system prompt, skills or file access than an earlier agent step in its conversation in memory scope "${step.memoryScope || "root"}" — a conversation has one identity`;
30012
30058
  };
30013
- const DIVERGED = "the workflow changed under this process; run `gtd abandon` to start over";
30059
+ const DIVERGED = pinned ? "the workflow changed under this process; run `gtd repin` to continue under it from there, or `gtd abandon` to start over" : "the workflow changed under this process; run `gtd abandon` to start over";
30060
+ const diverged = (message, reached) => ({
30061
+ kind: "divergence",
30062
+ message: `${message} — ${DIVERGED}`,
30063
+ at: position.hash,
30064
+ reached: reached?.name
30065
+ });
30014
30066
  const blockerAt = (step) => {
30015
30067
  const expected = expectedNext;
30016
30068
  expectedNext = void 0;
30017
- if (expected !== void 0 && expected.name !== step.name) return {
30018
- kind: "divergence",
30019
- message: `gtd: commit ${short(expected.hash)} names "${expected.name}" as the next step, but replay reached "${step.name}" — ${DIVERGED}`
30020
- };
30069
+ anchorPending = false;
30070
+ if (expected !== void 0 && expected.name !== step.name) return diverged(`gtd: commit ${short(expected.hash)} names "${expected.name}" as the next step, but replay reached "${step.name}"`, step);
30021
30071
  const error = unknownOptions(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
30022
30072
  return error === void 0 ? void 0 : {
30023
30073
  kind: "failed",
@@ -30028,33 +30078,30 @@ const replay = async (raw) => {
30028
30078
  if (step.request.kind !== "human" || step.request.options.approve !== true) return void 0;
30029
30079
  const approved = commit.parsed.approved;
30030
30080
  if (approved !== void 0 && formatStepId(approved.step) === expected) return void 0;
30031
- return {
30032
- kind: "divergence",
30033
- message: `gtd: commit ${short(commit.hash)} completes approval gate "${expected}" but records no Gtd-Approved for it — ${DIVERGED}`
30034
- };
30081
+ return diverged(`gtd: commit ${short(commit.hash)} completes approval gate "${expected}" but records no Gtd-Approved for it`, step);
30035
30082
  };
30036
30083
  const consumeCommit = (step, commit) => {
30037
30084
  const recorded = commit.parsed.step;
30038
30085
  const expected = formatStepId(step.id);
30039
30086
  if (recorded === void 0 || formatStepId(recorded) !== expected) {
30040
30087
  const what = recorded === void 0 ? "records no step" : `records step "${formatStepId(recorded)}"`;
30041
- return finish({
30042
- kind: "divergence",
30043
- message: `gtd: commit ${short(commit.hash)} ${what}, but replay expected "${expected}" — ${DIVERGED}`
30044
- });
30088
+ return finish(diverged(`gtd: commit ${short(commit.hash)} ${what}, but replay expected "${expected}"`, step));
30045
30089
  }
30046
30090
  const unapproved = missingApproval(step, commit, expected);
30047
30091
  if (unapproved !== void 0) return finish(unapproved);
30048
30092
  cursor++;
30049
30093
  advance(commit);
30094
+ expectNextOf(commit);
30050
30095
  skipPins();
30051
30096
  noteKept(step, commit);
30097
+ return replyFor(step.request, commit.parsed.judge);
30098
+ };
30099
+ const expectNextOf = (commit) => {
30052
30100
  const to = commit.parsed.parsed?.to;
30053
30101
  if (to !== void 0 && to !== UNPIN_STATE) expectedNext = {
30054
30102
  name: to,
30055
30103
  hash: commit.hash
30056
30104
  };
30057
- return replyFor(step.request, commit.parsed.judge);
30058
30105
  };
30059
30106
  const noteKept = (step, commit) => {
30060
30107
  if (commit.parsed.kept === void 0) return;
@@ -30126,10 +30173,8 @@ const replay = async (raw) => {
30126
30173
  };
30127
30174
  const endOfFlow = (via) => {
30128
30175
  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
- } : {
30176
+ if (anchorPending && cursor === commits.length) return diverged(`gtd: the flow ended, but commit ${short(expectedNext.hash)} continues the process at "${expectedNext.name}"`, void 0);
30177
+ return cursor < commits.length ? diverged(`gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode`, void 0) : {
30133
30178
  kind: "ended",
30134
30179
  via,
30135
30180
  trace,
@@ -30139,6 +30184,7 @@ const replay = async (raw) => {
30139
30184
  };
30140
30185
  const changesSince = (hash) => {
30141
30186
  const tree = treeAt(hash);
30187
+ if (setAside.has(hash)) throw new Error(`gtd: changesSince(${hash}): a repin set ${short(hash)} aside — pass a hash this run read from head() or start()`);
30142
30188
  if (tree === void 0) throw new Error(`gtd: changesSince(${hash}): ${hash} is not the episode base or one of its commits — pass a hash this run read from head() or start(), not one captured earlier, read from state, or from another branch`);
30143
30189
  return changesBetween(tree, position.tree);
30144
30190
  };
@@ -31335,6 +31381,12 @@ const pinScript = (path) => {
31335
31381
  const slash = path.lastIndexOf("/");
31336
31382
  return `${slash === -1 ? "" : `mkdir -p ${shellQuote$1(path.slice(0, slash))}\n`}gtd bundle >${p} || { rm -f -- ${p}; exit 1; }`;
31337
31383
  };
31384
+ /** `gtd repin`'s pin write: a failed `gtd bundle` leaves the committed pin in place. */
31385
+ const repinScript = (path) => {
31386
+ const p = shellQuote$1(path);
31387
+ const next = shellQuote$1(`${path}.new`);
31388
+ return `gtd bundle >${next} || { rm -f -- ${next}; exit 1; }\nmv -f -- ${next} ${p}`;
31389
+ };
31338
31390
  /** An unpin beat's script, and `gtd abandon`'s removal of the pin. */
31339
31391
  const unpinScript = (path) => `rm -f -- ${shellQuote$1(path)}`;
31340
31392
  /**
@@ -31563,6 +31615,12 @@ const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back
31563
31615
  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
31616
  /** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
31565
31617
  const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
31618
+ /** A landed self-loop/target-change transition. */
31619
+ const transitionOutcome = (from, to) => printfLine("%s %s → %s\n", [
31620
+ "'->'",
31621
+ shellQuote$1(from),
31622
+ shellQuote$1(to)
31623
+ ]);
31566
31624
  /** One already-rendered plain line. */
31567
31625
  const noteOutcome = (text) => printfLine("%s\n", [shellQuote$1(text)]);
31568
31626
  /**
@@ -32084,11 +32142,21 @@ const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
32084
32142
  email: message.approved.email,
32085
32143
  commit: hash
32086
32144
  }];
32145
+ const setAsideIn = (history, location) => {
32146
+ const anchored = setAsideBy(hashAt(history, location.baseIndex) ?? EMPTY_TREE$1, history.slice(location.baseIndex + 1).map((c) => ({
32147
+ hash: c.hash,
32148
+ parsed: parseCommitMessage(c.message)
32149
+ })));
32150
+ return "setAside" in anchored ? anchored.setAside : /* @__PURE__ */ new Set();
32151
+ };
32087
32152
  const buildRun = (history, location, closingHash) => {
32088
32153
  const allProcessCommits = history.slice(location.processStart);
32089
32154
  const allParsed = allProcessCommits.map((c) => parseCommitMessage(c.message));
32090
32155
  const processCommits = allProcessCommits.filter((_, i) => allParsed[i].pin !== "add");
32091
32156
  const parsed = allParsed.filter((m) => m.pin !== "add");
32157
+ const setAside = setAsideIn(history, location);
32158
+ const answering = parsed.filter((_, i) => !setAside.has(processCommits[i].hash));
32159
+ const answeringCommits = processCommits.filter((c) => !setAside.has(c.hash));
32092
32160
  const first = parsed[0];
32093
32161
  const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
32094
32162
  const head = history[history.length - 1];
@@ -32100,10 +32168,10 @@ const buildRun = (history, location, closingHash) => {
32100
32168
  diffBase: first?.reviewBase ?? startParentHash,
32101
32169
  trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
32102
32170
  costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
32103
- judgeVerdicts: parsed.flatMap((m) => m.judge),
32171
+ judgeVerdicts: answering.flatMap((m) => m.judge),
32104
32172
  pinCommitted: allParsed.some((m) => m.pin === "add"),
32105
- marks: parsed.flatMap((m) => m.marks),
32106
- approvals: processCommits.flatMap((c, i) => approvalEntryOf(c.hash, parsed[i])),
32173
+ marks: answering.flatMap((m) => m.marks),
32174
+ approvals: answeringCommits.flatMap((c, i) => approvalEntryOf(c.hash, answering[i])),
32107
32175
  pinnedVars: { ...first?.vars },
32108
32176
  headTurn: headTurnOf(head),
32109
32177
  closingHash,
@@ -32532,6 +32600,43 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
32532
32600
  };
32533
32601
  });
32534
32602
  const currentRest = restAt(void 0);
32603
+ /**
32604
+ * Where `gtd repin` anchors a pinned process the live workflow no longer
32605
+ * replays: the commit replay last stood on, and the step the live workflow
32606
+ * reaches from there on HEAD's tree — the unpin when the flow ends instead.
32607
+ * `undefined` when there is nothing to anchor: no divergence, or no pin.
32608
+ */
32609
+ const repinAnchor = gen(function* () {
32610
+ const git = yield* GitService;
32611
+ const config = yield* (yield* ConfigService).load;
32612
+ const workspace = yield* Workspace;
32613
+ const run = yield* computeProcessRun(git, config.workflow);
32614
+ const { def, setup } = yield* setupFor(config, run, workspace, yield* Host);
32615
+ const diverged = yield* promise(() => replayFor(setup));
32616
+ if (diverged.kind !== "divergence" || !headHasPin(workspace, run)) return void 0;
32617
+ const trial = {
32618
+ hash: "",
32619
+ message: formatCommitMessage({
32620
+ actor: "check",
32621
+ from: PIN_STATES.add,
32622
+ to: PIN_STATES.add,
32623
+ pin: "add",
32624
+ anchor: diverged.at
32625
+ }),
32626
+ tree: setup.episode.at(-1)?.tree ?? setup.base.tree
32627
+ };
32628
+ const outcome = yield* promise(() => replayFor({
32629
+ ...setup,
32630
+ episode: [...setup.episode, trial]
32631
+ }));
32632
+ if (outcome.kind !== "divergence") return yield* fail$8(replayError(outcome) ?? /* @__PURE__ */ new Error("gtd repin: replay found no step"));
32633
+ const to = outcome.reached ?? PIN_STATES.remove;
32634
+ if (run.workflow === void 0 && to === def.initial) return yield* fail$8(/* @__PURE__ */ new Error(`gtd repin: the live workflow continues this process at "${to}", the default workflow's first step, where a finished process waits — run \`gtd abandon\` to start over`));
32635
+ return {
32636
+ at: diverged.at,
32637
+ to
32638
+ };
32639
+ });
32535
32640
  /** The step the process rests at and the trace that reached it — at an unpin, the flow's last step. */
32536
32641
  const restingOn = (outcome, pin) => {
32537
32642
  if (outcome.kind === "rest") return {
@@ -34054,6 +34159,44 @@ const runRestoreCommand = (json, out) => gen(function* () {
34054
34159
  ]).required;
34055
34160
  yield* writeScript(json, out, combinedScript(required, ""));
34056
34161
  });
34162
+ /** The step an ordinary repin rests at — the one the process rests at now. */
34163
+ const repinTarget = gen(function* () {
34164
+ const rest = yield* currentRest;
34165
+ const refusal = rest.pin === "add" ? "the next beat pins the process already — run it (`gtd next --json=content`), then land" : rest.pin === "remove" ? "the flow has ended — its unpin beat is next, so there is nothing to repin" : noProcessUnderway(rest) ? "no gtd process is underway — nothing to repin" : void 0;
34166
+ if (refusal !== void 0) return yield* fail$8(/* @__PURE__ */ new Error(`gtd repin: ${refusal}`));
34167
+ return rest.step.name;
34168
+ });
34169
+ /**
34170
+ * `gtd repin`: the pin beat's script and landing, on demand. It runs live: a
34171
+ * pinned process the live workflow no longer replays is anchored where it
34172
+ * diverged, and any other failure to read the rest refuses before any pin is
34173
+ * written.
34174
+ */
34175
+ const runRepinCommand = (json, out) => gen(function* () {
34176
+ const anchor = yield* repinAnchor;
34177
+ const to = anchor?.to ?? (yield* repinTarget);
34178
+ const message = formatCommitMessage({
34179
+ actor: "check",
34180
+ from: PIN_STATES.add,
34181
+ to,
34182
+ pin: "add",
34183
+ ...anchor !== void 0 ? { anchor: anchor.at } : {}
34184
+ });
34185
+ yield* writeScript(json, out, combinedScript(emitScripts([
34186
+ {
34187
+ kind: "gitWrite",
34188
+ command: repinScript(PIN_PATH)
34189
+ },
34190
+ {
34191
+ kind: "gitWrite",
34192
+ command: commitPath(PIN_PATH, message)
34193
+ },
34194
+ {
34195
+ kind: "outcome",
34196
+ command: transitionOutcome(PIN_STATES.add, to)
34197
+ }
34198
+ ]).required, ""));
34199
+ });
34057
34200
  /**
34058
34201
  * The mode's own validate command for `file`: a declared `validate:` command
34059
34202
  * verbatim; a `"builtin"` validator or no mode at all names the leaf
@@ -34321,6 +34464,7 @@ const dispatchVoidCommand = (command, json, out) => {
34321
34464
  case "start": return runStartCommand(command, json, out);
34322
34465
  case "abandon": return runAbandonCommand(json, out);
34323
34466
  case "restore": return runRestoreCommand(json, out);
34467
+ case "repin": return runRepinCommand(json, out);
34324
34468
  case "next": return runNextCommand(json, out);
34325
34469
  case "validate": return runValidateCommand(json, out);
34326
34470
  case "check":
@@ -34450,6 +34594,7 @@ const SCRIPT_KINDS = /* @__PURE__ */ new Set([
34450
34594
  "start",
34451
34595
  "abandon",
34452
34596
  "restore",
34597
+ "repin",
34453
34598
  "validate",
34454
34599
  "historyFetch"
34455
34600
  ]);
@@ -34717,6 +34862,16 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
34717
34862
  summary: "Bring back the last abandoned process",
34718
34863
  details: `Bring back the process the last \`gtd abandon\` stopped: the printed script resets HEAD to that process's last commit. Refuses on a dirty working tree, when nothing was abandoned, or when commits made since would be lost.`
34719
34864
  },
34865
+ {
34866
+ token: "repin",
34867
+ kind: "repin",
34868
+ arity: "none",
34869
+ group: "Start and stop",
34870
+ summary: "Pin the process to the live gtd and workflow",
34871
+ details: `Switch the process underway to the live engine and workflow, the way out when the workflow needs a fix mid-process: the printed script runs the pin beat again, writing \`gtd bundle\`'s output to .gtd/process.gtdpin and committing that file alone. Always runs on the gtd on PATH, never on the pin. When the live workflow no longer replays the process, the commit continues from the divergence: a Gtd-Anchor trailer names the last commit it replays, the later commits stay as files only, and the process rests at the step the live workflow reached there. Refuses when no process is underway, when the next beat is a pin or unpin beat already, when an unpinned process diverges, or when the live workflow would continue at the default workflow's first step.
34872
+
34873
+ s=$(gtd repin --json=script) && sh -c "$s"`
34874
+ },
34720
34875
  {
34721
34876
  token: "summary",
34722
34877
  kind: "summary",
@@ -35418,6 +35573,7 @@ const LIVE_KINDS = /* @__PURE__ */ new Set([
35418
35573
  "lsp",
35419
35574
  "guide",
35420
35575
  "bundle",
35576
+ "repin",
35421
35577
  "historyList",
35422
35578
  "historyShow",
35423
35579
  "historyMetrics",