@pmelab/gtd 62.1.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
  };
@@ -32096,11 +32142,21 @@ const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
32096
32142
  email: message.approved.email,
32097
32143
  commit: hash
32098
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
+ };
32099
32152
  const buildRun = (history, location, closingHash) => {
32100
32153
  const allProcessCommits = history.slice(location.processStart);
32101
32154
  const allParsed = allProcessCommits.map((c) => parseCommitMessage(c.message));
32102
32155
  const processCommits = allProcessCommits.filter((_, i) => allParsed[i].pin !== "add");
32103
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));
32104
32160
  const first = parsed[0];
32105
32161
  const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
32106
32162
  const head = history[history.length - 1];
@@ -32112,10 +32168,10 @@ const buildRun = (history, location, closingHash) => {
32112
32168
  diffBase: first?.reviewBase ?? startParentHash,
32113
32169
  trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
32114
32170
  costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
32115
- judgeVerdicts: parsed.flatMap((m) => m.judge),
32171
+ judgeVerdicts: answering.flatMap((m) => m.judge),
32116
32172
  pinCommitted: allParsed.some((m) => m.pin === "add"),
32117
- marks: parsed.flatMap((m) => m.marks),
32118
- 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])),
32119
32175
  pinnedVars: { ...first?.vars },
32120
32176
  headTurn: headTurnOf(head),
32121
32177
  closingHash,
@@ -32544,6 +32600,43 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
32544
32600
  };
32545
32601
  });
32546
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
+ });
32547
32640
  /** The step the process rests at and the trace that reached it — at an unpin, the flow's last step. */
32548
32641
  const restingOn = (outcome, pin) => {
32549
32642
  if (outcome.kind === "rest") return {
@@ -34066,20 +34159,28 @@ const runRestoreCommand = (json, out) => gen(function* () {
34066
34159
  ]).required;
34067
34160
  yield* writeScript(json, out, combinedScript(required, ""));
34068
34161
  });
34069
- /**
34070
- * `gtd repin`: the pin beat's script and landing, on demand. It runs live,
34071
- * so reading the rest first refuses a live workflow that cannot replay the
34072
- * process before any pin is written.
34073
- */
34074
- const runRepinCommand = (json, out) => gen(function* () {
34162
+ /** The step an ordinary repin rests at — the one the process rests at now. */
34163
+ const repinTarget = gen(function* () {
34075
34164
  const rest = yield* currentRest;
34076
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;
34077
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);
34078
34178
  const message = formatCommitMessage({
34079
34179
  actor: "check",
34080
34180
  from: PIN_STATES.add,
34081
- to: rest.step.name,
34082
- pin: "add"
34181
+ to,
34182
+ pin: "add",
34183
+ ...anchor !== void 0 ? { anchor: anchor.at } : {}
34083
34184
  });
34084
34185
  yield* writeScript(json, out, combinedScript(emitScripts([
34085
34186
  {
@@ -34092,7 +34193,7 @@ const runRepinCommand = (json, out) => gen(function* () {
34092
34193
  },
34093
34194
  {
34094
34195
  kind: "outcome",
34095
- command: transitionOutcome(PIN_STATES.add, rest.step.name)
34196
+ command: transitionOutcome(PIN_STATES.add, to)
34096
34197
  }
34097
34198
  ]).required, ""));
34098
34199
  });
@@ -34767,7 +34868,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
34767
34868
  arity: "none",
34768
34869
  group: "Start and stop",
34769
34870
  summary: "Pin the process to the live gtd and workflow",
34770
- 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. Refuses when no process is underway, when the next beat is a pin or unpin beat already, or when the live workflow does not replay the process.
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.
34771
34872
 
34772
34873
  s=$(gtd repin --json=script) && sh -c "$s"`
34773
34874
  },