@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +14 -8
- package/dist/chunks/{testkit-XQeYCNMn.mjs → testkit-DwypA7cH.mjs} +183 -28
- package/dist/gtd.bundle.mjs +182 -26
- package/dist/gtd.core.mjs +80 -77
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +2 -0
- package/src/cli/Cli.ts +12 -0
- package/src/cli/Launch.ts +1 -0
- package/src/cli/Program.test.ts +9 -7
- package/src/cli/Program.ts +51 -0
- package/src/edge/Edge.test.ts +3 -3
- package/src/edge/Edge.ts +68 -3
- package/src/edge/index.ts +1 -0
- package/src/emit/GitScript.ts +7 -0
- package/src/emit/index.ts +1 -0
- package/src/replay/Format.test.ts +26 -7
- package/src/replay/Format.ts +3 -2
- package/src/replay/Replay.test.ts +87 -0
- package/src/replay/Replay.ts +111 -23
- package/src/replay/Trailers.test.ts +10 -10
- package/src/replay/Trailers.ts +9 -0
- package/src/replay/index.ts +1 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStep.test.ts +4 -4
- package/src/twins/EmittedScriptRecognizer.ts +3 -2
package/README.md
CHANGED
|
@@ -624,10 +624,10 @@ install for any of these but the testkit's: gtd resolves a `gtd.config.ts`'s
|
|
|
624
624
|
`@pmelab/gtd/*` imports to its own copy.
|
|
625
625
|
|
|
626
626
|
The `gtd` bin's commands — `next`, `land`, `start`, `abandon`, `restore`,
|
|
627
|
-
`validate`, `check`, `uncheck`, `guide`, `bundle`, `summary`, `base`,
|
|
628
|
-
`help`, `history list`, `history show`, `history metrics`,
|
|
629
|
-
`history fetch`, `strays`, the tools `ui`, `lsp`, `judge`
|
|
630
|
-
`judge run`) — are listed in the
|
|
627
|
+
`repin`, `validate`, `check`, `uncheck`, `guide`, `bundle`, `summary`, `base`,
|
|
628
|
+
`version`, `help`, `history list`, `history show`, `history metrics`,
|
|
629
|
+
`history judges`, `history fetch`, `strays`, the tools `ui`, `lsp`, `judge`
|
|
630
|
+
(`judge answer`, `judge run`) — are listed in the
|
|
631
631
|
[CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md). Every
|
|
632
632
|
command that prints a script takes `--json=script`, and `--` ends the options.
|
|
633
633
|
|
|
@@ -641,14 +641,20 @@ writing `gtd bundle`'s output to `.gtd/process.gtdpin`, and its last, once the
|
|
|
641
641
|
flow has ended, an unpin beat removing it — the commit that closes the process.
|
|
642
642
|
Both are ordinary `script` beats a driver runs unattended; see
|
|
643
643
|
[Pin and unpin beats](https://github.com/pmelab/gtd/blob/main/docs/driver.md#pin-and-unpin-beats).
|
|
644
|
-
Histories with pin commits are history format 2, which an older gtd
|
|
644
|
+
Histories with pin commits are history format 2 or later, which an older gtd
|
|
645
|
+
refuses.
|
|
645
646
|
|
|
646
647
|
While a process is pinned, the `gtd` on your `PATH` hands every command over to
|
|
647
648
|
the pin, so editing the workflow or upgrading gtd mid-process changes nothing;
|
|
648
649
|
gtd notes on stderr when the live config differs from the pinned one. `ui`,
|
|
649
|
-
`lsp`, `guide`, `bundle`, `help`, `version` and the `history` commands
|
|
650
|
-
run live, and `gtd --live <command>` runs the live engine for any other,
|
|
651
|
-
risk of misreading the process.
|
|
650
|
+
`lsp`, `guide`, `bundle`, `repin`, `help`, `version` and the `history` commands
|
|
651
|
+
always run live, and `gtd --live <command>` runs the live engine for any other,
|
|
652
|
+
at the risk of misreading the process. To fix a workflow mid-process, edit it
|
|
653
|
+
and run `gtd repin`: its script commits a fresh pin from the live engine and
|
|
654
|
+
config, and the process continues on it. Where the edited workflow no longer
|
|
655
|
+
replays the process, the repin continues from the divergence: the commits it
|
|
656
|
+
cannot replay stay as files, and the process rests at the step the edited
|
|
657
|
+
workflow reached (history format 3). A pin in a format your gtd does not know is
|
|
652
658
|
refused: upgrade gtd. See
|
|
653
659
|
[Pinned processes](https://github.com/pmelab/gtd/blob/main/docs/cli.md#pinned-processes).
|
|
654
660
|
|
|
@@ -22810,9 +22810,9 @@ const withoutPath = (tree, hidden) => {
|
|
|
22810
22810
|
const OLDEST_READABLE_FORMAT = 1;
|
|
22811
22811
|
const short$1 = (hash) => hash.slice(0, 7);
|
|
22812
22812
|
const formatFault = (commits) => {
|
|
22813
|
-
const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format >
|
|
22813
|
+
const foreign = commits.find((c) => c.format < OLDEST_READABLE_FORMAT || c.format > 3);
|
|
22814
22814
|
if (foreign === void 0) return void 0;
|
|
22815
|
-
return `gtd: commit ${short$1(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads formats ${OLDEST_READABLE_FORMAT} to
|
|
22815
|
+
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`;
|
|
22816
22816
|
};
|
|
22817
22817
|
//#endregion
|
|
22818
22818
|
//#region src/replay/Trailers.ts
|
|
@@ -22943,6 +22943,9 @@ const TRAILER_READERS = {
|
|
|
22943
22943
|
"Gtd-Pin": (value, into) => {
|
|
22944
22944
|
if (value === "add" || value === "remove") into.pin ??= value;
|
|
22945
22945
|
},
|
|
22946
|
+
"Gtd-Anchor": (value, into) => {
|
|
22947
|
+
if (HASH_RE.test(value)) into.anchor ??= value;
|
|
22948
|
+
},
|
|
22946
22949
|
"Gtd-Format": (value, into) => {
|
|
22947
22950
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
22948
22951
|
},
|
|
@@ -22966,6 +22969,7 @@ const parseCommitMessage = (message) => {
|
|
|
22966
22969
|
kept: void 0,
|
|
22967
22970
|
approved: void 0,
|
|
22968
22971
|
pin: void 0,
|
|
22972
|
+
anchor: void 0,
|
|
22969
22973
|
judge: [],
|
|
22970
22974
|
marks: [],
|
|
22971
22975
|
vars: {},
|
|
@@ -22995,9 +22999,10 @@ const formatCommitMessage = (spec) => {
|
|
|
22995
22999
|
...(spec.marks ?? []).map((m) => `Gtd-Mark: ${m.name}=${m.value}`),
|
|
22996
23000
|
...trailer("Approved", spec.approved && encodeApproval(spec.approved)),
|
|
22997
23001
|
...trailer("Pin", spec.pin),
|
|
23002
|
+
...trailer("Anchor", spec.anchor),
|
|
22998
23003
|
...trailer("Branch", spec.branch),
|
|
22999
23004
|
...trailer("Version", spec.version),
|
|
23000
|
-
`Gtd-Format:
|
|
23005
|
+
`Gtd-Format: 3`
|
|
23001
23006
|
];
|
|
23002
23007
|
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
23003
23008
|
};
|
|
@@ -23091,6 +23096,27 @@ const budgeted = (evidence, budgetBytes) => {
|
|
|
23091
23096
|
};
|
|
23092
23097
|
const sameSkills = (a, b) => (a?.length ?? 0) === (b?.length ?? 0) && (a ?? []).every((skill, i) => skill === b?.[i]);
|
|
23093
23098
|
const samePersona = (a, b) => a.model === b.model && a.system === b.system && sameSkills(a.skills, b.skills) && sameAccess(a.access, b.access);
|
|
23099
|
+
const anchorOf = (parsed) => parsed.pin === "add" ? parsed.anchor : void 0;
|
|
23100
|
+
const isAnchor = (commit) => anchorOf(commit.parsed) !== void 0;
|
|
23101
|
+
/**
|
|
23102
|
+
* The commits every anchor sets aside — those between the commit it names and
|
|
23103
|
+
* itself — or a fault for an anchor naming no commit still read.
|
|
23104
|
+
*/
|
|
23105
|
+
const setAsideBy = (base, all) => {
|
|
23106
|
+
const setAside = /* @__PURE__ */ new Set();
|
|
23107
|
+
if (all.every((c) => anchorOf(c.parsed) === void 0)) return { setAside };
|
|
23108
|
+
let kept = [];
|
|
23109
|
+
for (const commit of all) {
|
|
23110
|
+
const anchor = anchorOf(commit.parsed);
|
|
23111
|
+
if (anchor !== void 0) {
|
|
23112
|
+
const at = anchor === base ? -1 : kept.indexOf(anchor);
|
|
23113
|
+
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` };
|
|
23114
|
+
for (const hash of kept.splice(at + 1)) setAside.add(hash);
|
|
23115
|
+
}
|
|
23116
|
+
kept.push(commit.hash);
|
|
23117
|
+
}
|
|
23118
|
+
return { setAside };
|
|
23119
|
+
};
|
|
23094
23120
|
/**
|
|
23095
23121
|
* Replay one episode: run its entry flow, answering each step from the
|
|
23096
23122
|
* episode's commits in order, until a step has no commit left — the rest.
|
|
@@ -23164,19 +23190,39 @@ const withoutPin = (input) => {
|
|
|
23164
23190
|
};
|
|
23165
23191
|
const replay = async (raw) => {
|
|
23166
23192
|
const input = withoutPin(raw);
|
|
23167
|
-
const
|
|
23193
|
+
const all = input.episode.commits.map((c) => ({
|
|
23168
23194
|
...c,
|
|
23169
23195
|
parsed: parseCommitMessage(c.message)
|
|
23170
23196
|
}));
|
|
23197
|
+
const resolved = setAsideBy(input.episode.base.hash, all);
|
|
23198
|
+
if ("fault" in resolved) return {
|
|
23199
|
+
kind: "failed",
|
|
23200
|
+
message: resolved.fault
|
|
23201
|
+
};
|
|
23202
|
+
const setAside = resolved.setAside;
|
|
23203
|
+
const commits = all.filter((c) => !setAside.has(c.hash));
|
|
23204
|
+
const pinned = (raw.episode.commits.at(-1)?.tree ?? raw.episode.base.tree).paths().includes(PIN_PATH);
|
|
23171
23205
|
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);
|
|
23172
23206
|
let cursor = 0;
|
|
23173
23207
|
let pendingUsed = false;
|
|
23174
23208
|
let position = input.episode.base;
|
|
23175
23209
|
const skipPins = () => {
|
|
23176
|
-
while (cursor < commits.length && commits[cursor].parsed.pin !== void 0)
|
|
23210
|
+
while (cursor < commits.length && commits[cursor].parsed.pin !== void 0) {
|
|
23211
|
+
const commit = commits[cursor];
|
|
23212
|
+
cursor++;
|
|
23213
|
+
if (!isAnchor(commit)) continue;
|
|
23214
|
+
position = commit;
|
|
23215
|
+
const to = commit.parsed.parsed?.to;
|
|
23216
|
+
expectedNext = to === void 0 || to === UNPIN_STATE ? void 0 : {
|
|
23217
|
+
name: to,
|
|
23218
|
+
hash: commit.hash
|
|
23219
|
+
};
|
|
23220
|
+
anchorPending = expectedNext !== void 0;
|
|
23221
|
+
}
|
|
23177
23222
|
};
|
|
23178
23223
|
let previousPosition = input.episode.base;
|
|
23179
23224
|
let expectedNext;
|
|
23225
|
+
let anchorPending = false;
|
|
23180
23226
|
const occurrences = /* @__PURE__ */ new Map();
|
|
23181
23227
|
const personas = /* @__PURE__ */ new Map();
|
|
23182
23228
|
const scopes = [];
|
|
@@ -23319,14 +23365,18 @@ const replay = async (raw) => {
|
|
|
23319
23365
|
}
|
|
23320
23366
|
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`;
|
|
23321
23367
|
};
|
|
23322
|
-
const DIVERGED = "the workflow changed under this process; run `gtd abandon` to start over";
|
|
23368
|
+
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";
|
|
23369
|
+
const diverged = (message, reached) => ({
|
|
23370
|
+
kind: "divergence",
|
|
23371
|
+
message: `${message} — ${DIVERGED}`,
|
|
23372
|
+
at: position.hash,
|
|
23373
|
+
reached: reached?.name
|
|
23374
|
+
});
|
|
23323
23375
|
const blockerAt = (step) => {
|
|
23324
23376
|
const expected = expectedNext;
|
|
23325
23377
|
expectedNext = void 0;
|
|
23326
|
-
|
|
23327
|
-
|
|
23328
|
-
message: `gtd: commit ${short(expected.hash)} names "${expected.name}" as the next step, but replay reached "${step.name}" — ${DIVERGED}`
|
|
23329
|
-
};
|
|
23378
|
+
anchorPending = false;
|
|
23379
|
+
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);
|
|
23330
23380
|
const error = unknownOptions(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
|
|
23331
23381
|
return error === void 0 ? void 0 : {
|
|
23332
23382
|
kind: "failed",
|
|
@@ -23337,33 +23387,30 @@ const replay = async (raw) => {
|
|
|
23337
23387
|
if (step.request.kind !== "human" || step.request.options.approve !== true) return void 0;
|
|
23338
23388
|
const approved = commit.parsed.approved;
|
|
23339
23389
|
if (approved !== void 0 && formatStepId(approved.step) === expected) return void 0;
|
|
23340
|
-
return {
|
|
23341
|
-
kind: "divergence",
|
|
23342
|
-
message: `gtd: commit ${short(commit.hash)} completes approval gate "${expected}" but records no Gtd-Approved for it — ${DIVERGED}`
|
|
23343
|
-
};
|
|
23390
|
+
return diverged(`gtd: commit ${short(commit.hash)} completes approval gate "${expected}" but records no Gtd-Approved for it`, step);
|
|
23344
23391
|
};
|
|
23345
23392
|
const consumeCommit = (step, commit) => {
|
|
23346
23393
|
const recorded = commit.parsed.step;
|
|
23347
23394
|
const expected = formatStepId(step.id);
|
|
23348
23395
|
if (recorded === void 0 || formatStepId(recorded) !== expected) {
|
|
23349
23396
|
const what = recorded === void 0 ? "records no step" : `records step "${formatStepId(recorded)}"`;
|
|
23350
|
-
return finish({
|
|
23351
|
-
kind: "divergence",
|
|
23352
|
-
message: `gtd: commit ${short(commit.hash)} ${what}, but replay expected "${expected}" — ${DIVERGED}`
|
|
23353
|
-
});
|
|
23397
|
+
return finish(diverged(`gtd: commit ${short(commit.hash)} ${what}, but replay expected "${expected}"`, step));
|
|
23354
23398
|
}
|
|
23355
23399
|
const unapproved = missingApproval(step, commit, expected);
|
|
23356
23400
|
if (unapproved !== void 0) return finish(unapproved);
|
|
23357
23401
|
cursor++;
|
|
23358
23402
|
advance(commit);
|
|
23403
|
+
expectNextOf(commit);
|
|
23359
23404
|
skipPins();
|
|
23360
23405
|
noteKept(step, commit);
|
|
23406
|
+
return replyFor(step.request, commit.parsed.judge);
|
|
23407
|
+
};
|
|
23408
|
+
const expectNextOf = (commit) => {
|
|
23361
23409
|
const to = commit.parsed.parsed?.to;
|
|
23362
23410
|
if (to !== void 0 && to !== UNPIN_STATE) expectedNext = {
|
|
23363
23411
|
name: to,
|
|
23364
23412
|
hash: commit.hash
|
|
23365
23413
|
};
|
|
23366
|
-
return replyFor(step.request, commit.parsed.judge);
|
|
23367
23414
|
};
|
|
23368
23415
|
const noteKept = (step, commit) => {
|
|
23369
23416
|
if (commit.parsed.kept === void 0) return;
|
|
@@ -23435,10 +23482,8 @@ const replay = async (raw) => {
|
|
|
23435
23482
|
};
|
|
23436
23483
|
const endOfFlow = (via) => {
|
|
23437
23484
|
skipPins();
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
message: `gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode — ${DIVERGED}`
|
|
23441
|
-
} : {
|
|
23485
|
+
if (anchorPending && cursor === commits.length) return diverged(`gtd: the flow ended, but commit ${short(expectedNext.hash)} continues the process at "${expectedNext.name}"`, void 0);
|
|
23486
|
+
return cursor < commits.length ? diverged(`gtd: the flow ended, but commit ${short(commits[cursor].hash)} continues the episode`, void 0) : {
|
|
23442
23487
|
kind: "ended",
|
|
23443
23488
|
via,
|
|
23444
23489
|
trace,
|
|
@@ -23448,6 +23493,7 @@ const replay = async (raw) => {
|
|
|
23448
23493
|
};
|
|
23449
23494
|
const changesSince = (hash) => {
|
|
23450
23495
|
const tree = treeAt(hash);
|
|
23496
|
+
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()`);
|
|
23451
23497
|
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`);
|
|
23452
23498
|
return changesBetween(tree, position.tree);
|
|
23453
23499
|
};
|
|
@@ -24610,6 +24656,12 @@ const pinScript = (path) => {
|
|
|
24610
24656
|
const slash = path.lastIndexOf("/");
|
|
24611
24657
|
return `${slash === -1 ? "" : `mkdir -p ${shellQuote(path.slice(0, slash))}\n`}gtd bundle >${p} || { rm -f -- ${p}; exit 1; }`;
|
|
24612
24658
|
};
|
|
24659
|
+
/** `gtd repin`'s pin write: a failed `gtd bundle` leaves the committed pin in place. */
|
|
24660
|
+
const repinScript = (path) => {
|
|
24661
|
+
const p = shellQuote(path);
|
|
24662
|
+
const next = shellQuote(`${path}.new`);
|
|
24663
|
+
return `gtd bundle >${next} || { rm -f -- ${next}; exit 1; }\nmv -f -- ${next} ${p}`;
|
|
24664
|
+
};
|
|
24613
24665
|
/** An unpin beat's script, and `gtd abandon`'s removal of the pin. */
|
|
24614
24666
|
const unpinScript = (path) => `rm -f -- ${shellQuote(path)}`;
|
|
24615
24667
|
/**
|
|
@@ -24850,6 +24902,12 @@ const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back
|
|
|
24850
24902
|
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";
|
|
24851
24903
|
/** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
|
|
24852
24904
|
const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
|
|
24905
|
+
/** A landed self-loop/target-change transition. */
|
|
24906
|
+
const transitionOutcome = (from, to) => printfLine("%s %s → %s\n", [
|
|
24907
|
+
"'->'",
|
|
24908
|
+
shellQuote(from),
|
|
24909
|
+
shellQuote(to)
|
|
24910
|
+
]);
|
|
24853
24911
|
/** One already-rendered plain line. */
|
|
24854
24912
|
const noteOutcome = (text) => printfLine("%s\n", [shellQuote(text)]);
|
|
24855
24913
|
/**
|
|
@@ -25366,11 +25424,21 @@ const approvalEntryOf = (hash, message) => message.approved === void 0 ? [] : [{
|
|
|
25366
25424
|
email: message.approved.email,
|
|
25367
25425
|
commit: hash
|
|
25368
25426
|
}];
|
|
25427
|
+
const setAsideIn = (history, location) => {
|
|
25428
|
+
const anchored = setAsideBy(hashAt(history, location.baseIndex) ?? EMPTY_TREE$1, history.slice(location.baseIndex + 1).map((c) => ({
|
|
25429
|
+
hash: c.hash,
|
|
25430
|
+
parsed: parseCommitMessage(c.message)
|
|
25431
|
+
})));
|
|
25432
|
+
return "setAside" in anchored ? anchored.setAside : /* @__PURE__ */ new Set();
|
|
25433
|
+
};
|
|
25369
25434
|
const buildRun = (history, location, closingHash) => {
|
|
25370
25435
|
const allProcessCommits = history.slice(location.processStart);
|
|
25371
25436
|
const allParsed = allProcessCommits.map((c) => parseCommitMessage(c.message));
|
|
25372
25437
|
const processCommits = allProcessCommits.filter((_, i) => allParsed[i].pin !== "add");
|
|
25373
25438
|
const parsed = allParsed.filter((m) => m.pin !== "add");
|
|
25439
|
+
const setAside = setAsideIn(history, location);
|
|
25440
|
+
const answering = parsed.filter((_, i) => !setAside.has(processCommits[i].hash));
|
|
25441
|
+
const answeringCommits = processCommits.filter((c) => !setAside.has(c.hash));
|
|
25374
25442
|
const first = parsed[0];
|
|
25375
25443
|
const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
|
|
25376
25444
|
const head = history[history.length - 1];
|
|
@@ -25382,10 +25450,10 @@ const buildRun = (history, location, closingHash) => {
|
|
|
25382
25450
|
diffBase: first?.reviewBase ?? startParentHash,
|
|
25383
25451
|
trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
|
|
25384
25452
|
costEntries: parsed.flatMap((m, i) => costEntriesOf(processCommits[i].hash, m)),
|
|
25385
|
-
judgeVerdicts:
|
|
25453
|
+
judgeVerdicts: answering.flatMap((m) => m.judge),
|
|
25386
25454
|
pinCommitted: allParsed.some((m) => m.pin === "add"),
|
|
25387
|
-
marks:
|
|
25388
|
-
approvals:
|
|
25455
|
+
marks: answering.flatMap((m) => m.marks),
|
|
25456
|
+
approvals: answeringCommits.flatMap((c, i) => approvalEntryOf(c.hash, answering[i])),
|
|
25389
25457
|
pinnedVars: { ...first?.vars },
|
|
25390
25458
|
headTurn: headTurnOf(head),
|
|
25391
25459
|
closingHash,
|
|
@@ -25814,6 +25882,43 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
25814
25882
|
};
|
|
25815
25883
|
});
|
|
25816
25884
|
const currentRest = restAt(void 0);
|
|
25885
|
+
/**
|
|
25886
|
+
* Where `gtd repin` anchors a pinned process the live workflow no longer
|
|
25887
|
+
* replays: the commit replay last stood on, and the step the live workflow
|
|
25888
|
+
* reaches from there on HEAD's tree — the unpin when the flow ends instead.
|
|
25889
|
+
* `undefined` when there is nothing to anchor: no divergence, or no pin.
|
|
25890
|
+
*/
|
|
25891
|
+
const repinAnchor = gen(function* () {
|
|
25892
|
+
const git = yield* GitService;
|
|
25893
|
+
const config = yield* (yield* ConfigService).load;
|
|
25894
|
+
const workspace = yield* Workspace;
|
|
25895
|
+
const run = yield* computeProcessRun(git, config.workflow);
|
|
25896
|
+
const { def, setup } = yield* setupFor(config, run, workspace, yield* Host);
|
|
25897
|
+
const diverged = yield* promise(() => replayFor(setup));
|
|
25898
|
+
if (diverged.kind !== "divergence" || !headHasPin(workspace, run)) return void 0;
|
|
25899
|
+
const trial = {
|
|
25900
|
+
hash: "",
|
|
25901
|
+
message: formatCommitMessage({
|
|
25902
|
+
actor: "check",
|
|
25903
|
+
from: PIN_STATES.add,
|
|
25904
|
+
to: PIN_STATES.add,
|
|
25905
|
+
pin: "add",
|
|
25906
|
+
anchor: diverged.at
|
|
25907
|
+
}),
|
|
25908
|
+
tree: setup.episode.at(-1)?.tree ?? setup.base.tree
|
|
25909
|
+
};
|
|
25910
|
+
const outcome = yield* promise(() => replayFor({
|
|
25911
|
+
...setup,
|
|
25912
|
+
episode: [...setup.episode, trial]
|
|
25913
|
+
}));
|
|
25914
|
+
if (outcome.kind !== "divergence") return yield* fail$4(replayError(outcome) ?? /* @__PURE__ */ new Error("gtd repin: replay found no step"));
|
|
25915
|
+
const to = outcome.reached ?? PIN_STATES.remove;
|
|
25916
|
+
if (run.workflow === void 0 && to === def.initial) return yield* fail$4(/* @__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`));
|
|
25917
|
+
return {
|
|
25918
|
+
at: diverged.at,
|
|
25919
|
+
to
|
|
25920
|
+
};
|
|
25921
|
+
});
|
|
25817
25922
|
/** The step the process rests at and the trace that reached it — at an unpin, the flow's last step. */
|
|
25818
25923
|
const restingOn = (outcome, pin) => {
|
|
25819
25924
|
if (outcome.kind === "rest") return {
|
|
@@ -27327,6 +27432,44 @@ const runRestoreCommand = (json, out) => gen(function* () {
|
|
|
27327
27432
|
]).required;
|
|
27328
27433
|
yield* writeScript(json, out, combinedScript(required, ""));
|
|
27329
27434
|
});
|
|
27435
|
+
/** The step an ordinary repin rests at — the one the process rests at now. */
|
|
27436
|
+
const repinTarget = gen(function* () {
|
|
27437
|
+
const rest = yield* currentRest;
|
|
27438
|
+
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;
|
|
27439
|
+
if (refusal !== void 0) return yield* fail$4(/* @__PURE__ */ new Error(`gtd repin: ${refusal}`));
|
|
27440
|
+
return rest.step.name;
|
|
27441
|
+
});
|
|
27442
|
+
/**
|
|
27443
|
+
* `gtd repin`: the pin beat's script and landing, on demand. It runs live: a
|
|
27444
|
+
* pinned process the live workflow no longer replays is anchored where it
|
|
27445
|
+
* diverged, and any other failure to read the rest refuses before any pin is
|
|
27446
|
+
* written.
|
|
27447
|
+
*/
|
|
27448
|
+
const runRepinCommand = (json, out) => gen(function* () {
|
|
27449
|
+
const anchor = yield* repinAnchor;
|
|
27450
|
+
const to = anchor?.to ?? (yield* repinTarget);
|
|
27451
|
+
const message = formatCommitMessage({
|
|
27452
|
+
actor: "check",
|
|
27453
|
+
from: PIN_STATES.add,
|
|
27454
|
+
to,
|
|
27455
|
+
pin: "add",
|
|
27456
|
+
...anchor !== void 0 ? { anchor: anchor.at } : {}
|
|
27457
|
+
});
|
|
27458
|
+
yield* writeScript(json, out, combinedScript(emitScripts([
|
|
27459
|
+
{
|
|
27460
|
+
kind: "gitWrite",
|
|
27461
|
+
command: repinScript(PIN_PATH)
|
|
27462
|
+
},
|
|
27463
|
+
{
|
|
27464
|
+
kind: "gitWrite",
|
|
27465
|
+
command: commitPath(PIN_PATH, message)
|
|
27466
|
+
},
|
|
27467
|
+
{
|
|
27468
|
+
kind: "outcome",
|
|
27469
|
+
command: transitionOutcome(PIN_STATES.add, to)
|
|
27470
|
+
}
|
|
27471
|
+
]).required, ""));
|
|
27472
|
+
});
|
|
27330
27473
|
/**
|
|
27331
27474
|
* The mode's own validate command for `file`: a declared `validate:` command
|
|
27332
27475
|
* verbatim; a `"builtin"` validator or no mode at all names the leaf
|
|
@@ -27594,6 +27737,7 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
27594
27737
|
case "start": return runStartCommand(command, json, out);
|
|
27595
27738
|
case "abandon": return runAbandonCommand(json, out);
|
|
27596
27739
|
case "restore": return runRestoreCommand(json, out);
|
|
27740
|
+
case "repin": return runRepinCommand(json, out);
|
|
27597
27741
|
case "next": return runNextCommand(json, out);
|
|
27598
27742
|
case "validate": return runValidateCommand(json, out);
|
|
27599
27743
|
case "check":
|
|
@@ -27722,6 +27866,7 @@ const SCRIPT_KINDS = /* @__PURE__ */ new Set([
|
|
|
27722
27866
|
"start",
|
|
27723
27867
|
"abandon",
|
|
27724
27868
|
"restore",
|
|
27869
|
+
"repin",
|
|
27725
27870
|
"validate",
|
|
27726
27871
|
"historyFetch"
|
|
27727
27872
|
]);
|
|
@@ -27989,6 +28134,16 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
27989
28134
|
summary: "Bring back the last abandoned process",
|
|
27990
28135
|
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.`
|
|
27991
28136
|
},
|
|
28137
|
+
{
|
|
28138
|
+
token: "repin",
|
|
28139
|
+
kind: "repin",
|
|
28140
|
+
arity: "none",
|
|
28141
|
+
group: "Start and stop",
|
|
28142
|
+
summary: "Pin the process to the live gtd and workflow",
|
|
28143
|
+
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.
|
|
28144
|
+
|
|
28145
|
+
s=$(gtd repin --json=script) && sh -c "$s"`
|
|
28146
|
+
},
|
|
27992
28147
|
{
|
|
27993
28148
|
token: "summary",
|
|
27994
28149
|
kind: "summary",
|
|
@@ -29892,9 +30047,9 @@ const recognizeGitBuilders = (repo, block) => {
|
|
|
29892
30047
|
};
|
|
29893
30048
|
/** What the in-memory `gtd bundle` prints: a pin header, and no engine behind it. */
|
|
29894
30049
|
const IN_MEMORY_PIN = "gtdpin 1 gtd in-memory\n";
|
|
29895
|
-
/** A pin or unpin beat's script. */
|
|
30050
|
+
/** A pin or unpin beat's script, or `gtd repin`'s. */
|
|
29896
30051
|
const recognizePinScripts = (repo, block) => {
|
|
29897
|
-
if (block === pinScript(".gtd/process.gtdpin")) {
|
|
30052
|
+
if (block === pinScript(".gtd/process.gtdpin") || block === repinScript(".gtd/process.gtdpin")) {
|
|
29898
30053
|
repo.writeFile(PIN_PATH, IN_MEMORY_PIN);
|
|
29899
30054
|
return { kind: "applied" };
|
|
29900
30055
|
}
|