@pmelab/gtd 64.3.0 → 65.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 +34 -25
- package/claude/hooks/assist.ts +9 -35
- package/claude/hooks/doors.ts +4 -4
- package/claude/hooks/git.ts +56 -0
- package/claude/hooks/handoff.ts +11 -11
- package/claude/hooks/jobs.ts +8 -8
- package/claude/hooks/pr.ts +8 -8
- package/claude/hooks/register.tsx +16 -78
- package/claude/hooks/tools.ts +7 -20
- package/claude/hooks/wording.ts +3 -3
- package/dist/chunks/{flows-dnIE_qbe.mjs → flows-DGs0W3HI.mjs} +19 -2
- package/dist/chunks/{testkit-BUX-47TD.mjs → testkit-CgrLmpdJ.mjs} +740 -79
- package/dist/chunks/{wire-CaPJ9Tme.mjs → wire-BNS8ciNu.mjs} +5 -3
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +553 -81
- package/dist/gtd.core.mjs +132 -129
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/wire.mjs +1 -1
- package/dist/workflow-kit.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/Cli.ts +4 -4
- package/src/cli/History.test.ts +10 -8
- package/src/cli/Metrics.test.ts +2 -1
- package/src/cli/Program.test.ts +4 -2
- package/src/cli/Program.ts +87 -5
- package/src/edge/Edge.test.ts +37 -16
- package/src/edge/Edge.ts +296 -31
- package/src/emit/GitScript.ts +116 -15
- package/src/emit/index.ts +7 -0
- package/src/flows/config.ts +7 -1
- package/src/flows/index.test.ts +1 -0
- package/src/flows/index.ts +2 -0
- package/src/flows/runtime.ts +32 -1
- package/src/platform/RetainedHistory.ts +3 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Replay.test.ts +9 -0
- package/src/replay/Replay.ts +59 -15
- package/src/replay/Trailers.ts +14 -0
- package/src/replay/index.ts +1 -1
- package/src/step/LandStep.ts +12 -0
- package/src/step/RepoSnapshot.ts +11 -0
- package/src/step/index.ts +1 -0
- package/src/step/planStart.test.ts +4 -3
- package/src/step/planStart.ts +7 -0
- package/src/step/planStep.ts +17 -12
- package/src/step/startBranch.ts +54 -0
- package/src/twins/EmittedScriptRecognizer.ts +117 -0
- package/src/twins/EmittedScripts.test.ts +147 -1
- package/src/twins/InMemRepo.ts +116 -0
- package/src/wire/Demand.ts +22 -10
- package/src/wire/constants.ts +3 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/Workflow.ts +1 -0
- package/src/workflow/compile.ts +2 -0
- package/src/workflow/load.ts +2 -0
- package/claude/hooks/ship.ts +0 -730
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -1981,7 +1981,7 @@ var init_Order = __esmMin((() => {
|
|
|
1981
1981
|
isGreaterThan$1 = (O) => dual(2, (self, that) => O(self, that) === 1);
|
|
1982
1982
|
isLessThanOrEqualTo$1 = (O) => dual(2, (self, that) => O(self, that) !== 1);
|
|
1983
1983
|
isGreaterThanOrEqualTo$1 = (O) => dual(2, (self, that) => O(self, that) !== -1);
|
|
1984
|
-
})), none, some, isNone, isSome, match$3, getOrElse, orElse, fromNullishOr, getOrUndefined$1, map$6, flatMap$4;
|
|
1984
|
+
})), none, some, isNone, isSome, match$3, getOrElse, orElse, fromNullishOr, getOrUndefined$1, getOrThrowWith$1, getOrThrow$1, map$6, flatMap$4;
|
|
1985
1985
|
var init_Option = __esmMin((() => {
|
|
1986
1986
|
init_Function();
|
|
1987
1987
|
init_option();
|
|
@@ -1994,6 +1994,11 @@ var init_Option = __esmMin((() => {
|
|
|
1994
1994
|
orElse = /*#__PURE__*/ dual(2, (self, that) => isNone(self) ? that() : self);
|
|
1995
1995
|
fromNullishOr = (a) => a == null ? none() : some(a);
|
|
1996
1996
|
getOrUndefined$1 = /*#__PURE__*/ getOrElse(constUndefined);
|
|
1997
|
+
getOrThrowWith$1 = /*#__PURE__*/ dual(2, (self, onNone) => {
|
|
1998
|
+
if (isSome(self)) return self.value;
|
|
1999
|
+
throw onNone();
|
|
2000
|
+
});
|
|
2001
|
+
getOrThrow$1 = /*#__PURE__*/ getOrThrowWith$1(() => /* @__PURE__ */ new Error("getOrThrow called on a None"));
|
|
1997
2002
|
map$6 = /*#__PURE__*/ dual(2, (self, f) => isNone(self) ? none() : some(f(self.value)));
|
|
1998
2003
|
flatMap$4 = /*#__PURE__*/ dual(2, (self, f) => isNone(self) ? none() : f(self.value));
|
|
1999
2004
|
}));
|
|
@@ -13724,6 +13729,8 @@ const loopLogPath = gen(function* () {
|
|
|
13724
13729
|
init_dist$1();
|
|
13725
13730
|
/** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
|
|
13726
13731
|
const HISTORY_REF = "refs/worktree/gtd/history";
|
|
13732
|
+
/** Set by an abandon that left `gtd/<b>` for `<b>`: only then does restore recreate `gtd/<b>`. */
|
|
13733
|
+
const RESUME_BRANCH_REF = "refs/worktree/gtd/history-branch";
|
|
13727
13734
|
/** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
|
|
13728
13735
|
const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
|
|
13729
13736
|
const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
|
|
@@ -14632,6 +14639,17 @@ const run$3 = (name, body, options = {}) => ctx().step({
|
|
|
14632
14639
|
body,
|
|
14633
14640
|
options
|
|
14634
14641
|
});
|
|
14642
|
+
/**
|
|
14643
|
+
* Squashes HEAD's tree, with `.gtd/` as the user's branch has it, into one commit on the user's branch
|
|
14644
|
+
* with `message`, and moves that branch there; HEAD stays on `gtd/<b>`.
|
|
14645
|
+
* Resolves to the new commit's hash, or `undefined` when nothing shipped.
|
|
14646
|
+
*/
|
|
14647
|
+
const ship = (name, message, options = {}) => ctx().step({
|
|
14648
|
+
kind: "ship",
|
|
14649
|
+
name,
|
|
14650
|
+
message,
|
|
14651
|
+
options
|
|
14652
|
+
});
|
|
14635
14653
|
/** A judge gate: resolves to what the judge answered, for the flow to decide on. */
|
|
14636
14654
|
const judge = (name, spec) => {
|
|
14637
14655
|
const { questions, evidence, ...options } = spec;
|
|
@@ -15123,7 +15141,8 @@ const CONFIG_KEYS = [
|
|
|
15123
15141
|
"modes",
|
|
15124
15142
|
"ui",
|
|
15125
15143
|
"skills",
|
|
15126
|
-
"access"
|
|
15144
|
+
"access",
|
|
15145
|
+
"historyRemote"
|
|
15127
15146
|
];
|
|
15128
15147
|
const MODE_KEYS = ["format", "validate"];
|
|
15129
15148
|
const UI_KEYS = [
|
|
@@ -15194,6 +15213,10 @@ const configProblems = (value) => {
|
|
|
15194
15213
|
...unknownKeys(value, CONFIG_KEYS, []),
|
|
15195
15214
|
...value.modes === void 0 ? [] : recordOf(value.modes, ["modes"], "modes", modeProblems),
|
|
15196
15215
|
...value.ui === void 0 ? [] : uiProblems(value.ui, ["ui"]),
|
|
15216
|
+
...value.historyRemote === void 0 || typeof value.historyRemote === "string" && value.historyRemote.trim() !== "" ? [] : [{
|
|
15217
|
+
path: ["historyRemote"],
|
|
15218
|
+
message: "must be a non-empty remote name"
|
|
15219
|
+
}],
|
|
15197
15220
|
...value.skills === void 0 ? [] : recordOf(value.skills, ["skills"], "skill-name arrays", (v, path) => isStringArray$1(v) ? [] : [{
|
|
15198
15221
|
path,
|
|
15199
15222
|
message: "must be an array of skill names"
|
|
@@ -15262,6 +15285,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
15262
15285
|
sections: () => sections$1,
|
|
15263
15286
|
settings: () => settings,
|
|
15264
15287
|
settingsOf: () => settingsOf,
|
|
15288
|
+
ship: () => ship,
|
|
15265
15289
|
showScript: () => showScript,
|
|
15266
15290
|
skillsFor: () => skillsFor,
|
|
15267
15291
|
start: () => start,
|
|
@@ -31138,6 +31162,8 @@ const renderFormat = (fmt, ...args) => {
|
|
|
31138
31162
|
const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
|
|
31139
31163
|
/** Where a process keeps its pin: the engine and workflow it runs on, as `gtd bundle` prints them. */
|
|
31140
31164
|
const PIN_PATH = ".gtd/process.gtdpin";
|
|
31165
|
+
/** Where a `run(…, { onFail: "rest" })` body's failure is recorded in the tree: the exit status and the output. */
|
|
31166
|
+
const RUN_FAILED_PATH = ".gtd/run-failed.txt";
|
|
31141
31167
|
// src/wire/Demand.ts
|
|
31142
31168
|
/**
|
|
31143
31169
|
* The beat kind for a rest, in precedence order:
|
|
@@ -31161,7 +31187,7 @@ const beatKindOf = (input) => {
|
|
|
31161
31187
|
* `stalled at "<state>"` stays a stable substring (see
|
|
31162
31188
|
* `tests/integration/features/driver-doc.feature`).
|
|
31163
31189
|
*/
|
|
31164
|
-
const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nTwo ways out:\n - sharpen the step's prompt so the turn has something concrete to author\n - pass allowEmpty: true to the step, so an empty turn completes it and the\n flow decides what follows — finishing, or an escalation step after N\n empty turns\n`;
|
|
31190
|
+
const stallDiagnosis = (state, actor, failure, blocked) => blocked !== void 0 ? `stalled at "${state}": landing this ship step is refused.\n\n${blocked}\n` : failure !== void 0 ? `stalled at "${state}": the script exited non-zero and its failure is recorded in the tree. Change the tree to run the script again; the step advances only on a zero exit.\n\n${failure}` : `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nTwo ways out:\n - sharpen the step's prompt so the turn has something concrete to author\n - pass allowEmpty: true to the step, so an empty turn completes it and the\n flow decides what follows — finishing, or an escalation step after N\n empty turns\n`;
|
|
31165
31191
|
/**
|
|
31166
31192
|
* Assemble one `Demand` — the ONLY place a rest's resolved content becomes
|
|
31167
31193
|
* `Demand.content` (`stallDiagnosis` at `kind === "stalled"`, the rendered
|
|
@@ -31171,7 +31197,7 @@ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${
|
|
|
31171
31197
|
*/
|
|
31172
31198
|
const demandOf = (input) => {
|
|
31173
31199
|
const { rendered, kind, session, validate } = input;
|
|
31174
|
-
const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content;
|
|
31200
|
+
const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor, rendered.failure, rendered.blocked) : rendered.content;
|
|
31175
31201
|
const skills = rendered.skills !== void 0 && rendered.skills.length > 0 ? rendered.skills : void 0;
|
|
31176
31202
|
const access = rendered.access ?? {
|
|
31177
31203
|
read: null,
|
|
@@ -31770,6 +31796,12 @@ const TRAILER_READERS = {
|
|
|
31770
31796
|
"Gtd-Anchor": (value, into) => {
|
|
31771
31797
|
if (HASH_RE.test(value)) into.anchor ??= value;
|
|
31772
31798
|
},
|
|
31799
|
+
"Gtd-Shipped": (value, into) => {
|
|
31800
|
+
if (HASH_RE.test(value)) into.shipped ??= value;
|
|
31801
|
+
},
|
|
31802
|
+
"Gtd-Tip": (value, into) => {
|
|
31803
|
+
if (HASH_RE.test(value)) into.tip ??= value;
|
|
31804
|
+
},
|
|
31773
31805
|
"Gtd-Format": (value, into) => {
|
|
31774
31806
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
31775
31807
|
},
|
|
@@ -31794,6 +31826,8 @@ const parseCommitMessage = (message) => {
|
|
|
31794
31826
|
approved: void 0,
|
|
31795
31827
|
pin: void 0,
|
|
31796
31828
|
anchor: void 0,
|
|
31829
|
+
shipped: void 0,
|
|
31830
|
+
tip: void 0,
|
|
31797
31831
|
judge: [],
|
|
31798
31832
|
marks: [],
|
|
31799
31833
|
vars: {},
|
|
@@ -31835,7 +31869,7 @@ const namesUnpin = (commit) => {
|
|
|
31835
31869
|
const unpin = pinStateIn(commit.parsed.format, "remove");
|
|
31836
31870
|
return unpin !== void 0 && commit.parsed.parsed?.to === unpin;
|
|
31837
31871
|
};
|
|
31838
|
-
const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
31872
|
+
const actorOfKind = (kind) => kind === "run" || kind === "ship" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
31839
31873
|
const memoryScopeOf = (name) => {
|
|
31840
31874
|
const dot = name.lastIndexOf(".");
|
|
31841
31875
|
return dot === -1 ? "" : name.slice(0, dot);
|
|
@@ -31969,9 +32003,12 @@ const KNOWN_OPTIONS = {
|
|
|
31969
32003
|
"acceptClean",
|
|
31970
32004
|
"approve"
|
|
31971
32005
|
]),
|
|
31972
|
-
run: new Set(STEERING_OPTIONS),
|
|
32006
|
+
run: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "onFail"]),
|
|
32007
|
+
ship: /* @__PURE__ */ new Set(["label", "base"]),
|
|
31973
32008
|
judge: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "message"])
|
|
31974
32009
|
};
|
|
32010
|
+
const restsOnFail = (step) => step.request.kind === "run" && step.request.options.onFail === "rest";
|
|
32011
|
+
const onFailFault = (step) => step.request.kind === "run" && step.request.options.onFail !== void 0 && step.request.options.onFail !== "rest" ? `gtd: step "${step.name}": onFail must be "rest" in run() options` : void 0;
|
|
31975
32012
|
const unknownOptions = (step) => {
|
|
31976
32013
|
const unknown = Object.keys(step.request.options).filter((key) => !KNOWN_OPTIONS[step.kind].has(key));
|
|
31977
32014
|
if (unknown.length === 0) return void 0;
|
|
@@ -32082,15 +32119,15 @@ const replay = async (raw) => {
|
|
|
32082
32119
|
return calls;
|
|
32083
32120
|
};
|
|
32084
32121
|
const scoped = (name) => [...scopes.flatMap((s) => s.name === void 0 ? [] : [s.name]), name].join(".");
|
|
32085
|
-
const isAttemptAt = (commit,
|
|
32122
|
+
const isAttemptAt = (commit, step) => commit.parsed.step === void 0 && commit.parsed.parsed !== void 0 && commit.parsed.parsed.from === void 0 && commit.parsed.parsed.to === step.name && (isEmptyDiff(diffTrees(position.tree, commit.tree)) || restsOnFail(step) && commit.tree.read(".gtd/run-failed.txt") !== void 0);
|
|
32086
32123
|
const advance = (next) => {
|
|
32087
32124
|
previousPosition = position;
|
|
32088
32125
|
position = next;
|
|
32089
32126
|
};
|
|
32090
|
-
const replyFor = (request, verdicts) => request.kind === "judge" ? {
|
|
32127
|
+
const replyFor = (request, verdicts, shipped) => request.kind === "judge" ? {
|
|
32091
32128
|
answers: answersFrom(verdicts, request.questions),
|
|
32092
32129
|
truncated: judgeCuts.get(request) ?? []
|
|
32093
|
-
} : void 0;
|
|
32130
|
+
} : request.kind === "ship" ? shipped : void 0;
|
|
32094
32131
|
const judgeCuts = /* @__PURE__ */ new WeakMap();
|
|
32095
32132
|
const resolveScoped = (memory, pick, configured, bundled) => {
|
|
32096
32133
|
const own = /* @__PURE__ */ new Map();
|
|
@@ -32204,7 +32241,7 @@ const replay = async (raw) => {
|
|
|
32204
32241
|
expectedNext = void 0;
|
|
32205
32242
|
anchorPending = false;
|
|
32206
32243
|
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);
|
|
32207
|
-
const error = unknownOptions(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
|
|
32244
|
+
const error = unknownOptions(step) ?? onFailFault(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
|
|
32208
32245
|
return error === void 0 ? void 0 : {
|
|
32209
32246
|
kind: "failed",
|
|
32210
32247
|
message: error
|
|
@@ -32230,7 +32267,7 @@ const replay = async (raw) => {
|
|
|
32230
32267
|
expectNextOf(commit);
|
|
32231
32268
|
skipPins();
|
|
32232
32269
|
noteKept(step, commit);
|
|
32233
|
-
return replyFor(step.request, commit.parsed.judge);
|
|
32270
|
+
return replyFor(step.request, commit.parsed.judge, commit.parsed.shipped);
|
|
32234
32271
|
};
|
|
32235
32272
|
const expectNextOf = (commit) => {
|
|
32236
32273
|
const to = commit.parsed.parsed?.to;
|
|
@@ -32261,10 +32298,14 @@ const replay = async (raw) => {
|
|
|
32261
32298
|
hash: "",
|
|
32262
32299
|
tree: keeping ? position.tree : pending.tree
|
|
32263
32300
|
});
|
|
32264
|
-
return replyFor(step.request, pending.verdicts ?? []);
|
|
32301
|
+
return replyFor(step.request, pending.verdicts ?? [], pending.shipped);
|
|
32265
32302
|
};
|
|
32266
32303
|
const answer = (step) => {
|
|
32267
|
-
while (cursor < commits.length && isAttemptAt(commits[cursor], step
|
|
32304
|
+
while (cursor < commits.length && isAttemptAt(commits[cursor], step)) {
|
|
32305
|
+
if (!isEmptyDiff(diffTrees(position.tree, commits[cursor].tree))) advance({
|
|
32306
|
+
hash: commits[cursor].hash,
|
|
32307
|
+
tree: commits[cursor].tree
|
|
32308
|
+
});
|
|
32268
32309
|
cursor++;
|
|
32269
32310
|
skipPins();
|
|
32270
32311
|
}
|
|
@@ -32277,12 +32318,22 @@ const replay = async (raw) => {
|
|
|
32277
32318
|
landed,
|
|
32278
32319
|
marks
|
|
32279
32320
|
});
|
|
32321
|
+
if (restsOnFail(step) && input.pending.tree.read(".gtd/run-failed.txt") !== void 0) return finish({
|
|
32322
|
+
kind: "rest",
|
|
32323
|
+
rest: step,
|
|
32324
|
+
trace,
|
|
32325
|
+
landed,
|
|
32326
|
+
marks
|
|
32327
|
+
});
|
|
32280
32328
|
return landPending(step, input.pending);
|
|
32281
32329
|
};
|
|
32330
|
+
const bodyFault = (request) => {
|
|
32331
|
+
if (request.kind === "ship" && (typeof request.message !== "string" || request.message.trim() === "")) return `gtd: step "${scoped(request.name)}": a ship() message is a non-empty commit message`;
|
|
32332
|
+
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)`;
|
|
32333
|
+
};
|
|
32282
32334
|
const requestFault = (request) => {
|
|
32283
32335
|
if (scoped(request.name).startsWith("gtd:")) return `gtd: step "${scoped(request.name)}": the "${ENGINE_PREFIX}" prefix is reserved for gtd's own beats — rename the step`;
|
|
32284
|
-
if (request.kind
|
|
32285
|
-
if (request.kind !== "agent") return void 0;
|
|
32336
|
+
if (request.kind !== "agent") return bodyFault(request);
|
|
32286
32337
|
const name = scoped(request.name);
|
|
32287
32338
|
if (request.options.keep === true) {
|
|
32288
32339
|
if (request.options.allowEmpty === true) return `gtd: step "${name}": keep: true cannot be combined with allowEmpty — a turn that changes nothing has nothing to keep`;
|
|
@@ -32464,6 +32515,7 @@ const compileConfig = (value, origin) => {
|
|
|
32464
32515
|
return {
|
|
32465
32516
|
modes,
|
|
32466
32517
|
...config.ui !== void 0 ? { ui: config.ui } : {},
|
|
32518
|
+
...config.historyRemote !== void 0 ? { historyRemote: config.historyRemote } : {},
|
|
32467
32519
|
skills: Object.assign(Object.create(null), config.skills),
|
|
32468
32520
|
access: Object.assign(Object.create(null), config.access),
|
|
32469
32521
|
diagnostics: []
|
|
@@ -32780,6 +32832,7 @@ const load = gen(function* () {
|
|
|
32780
32832
|
return entry === void 0 ? void 0 : resolve(name, entry);
|
|
32781
32833
|
},
|
|
32782
32834
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
32835
|
+
...compiled.historyRemote !== void 0 ? { historyRemote: compiled.historyRemote } : {},
|
|
32783
32836
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
32784
32837
|
fingerprint: pinned?.fingerprint ?? configFingerprint(nodeSyncIo, paths, loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
|
|
32785
32838
|
drift: pinned === void 0 ? [] : changedConfigFiles(pinned.fingerprint, configFingerprint(nodeSyncIo, paths, Object.keys(pinned.fingerprint).map((file) => paths.resolve(host.root, file)), host.root, readFromDisk))
|
|
@@ -33501,8 +33554,9 @@ const shellQuote$1 = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
|
33501
33554
|
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
33502
33555
|
* error, a commit-msg hook) still fails.
|
|
33503
33556
|
*/
|
|
33504
|
-
const commitAllowEmpty = (message, only) =>
|
|
33505
|
-
|
|
33557
|
+
const commitAllowEmpty = (message, only) => commitWordAllowEmpty(shellQuote$1(message), only);
|
|
33558
|
+
/** `commitAllowEmpty` over a message that is already a shell word. */
|
|
33559
|
+
const commitWordAllowEmpty = (m, only) => {
|
|
33506
33560
|
const paths = only === void 0 ? "" : ` -- ${shellQuote$1(only)}`;
|
|
33507
33561
|
return [
|
|
33508
33562
|
`if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
|
|
@@ -33518,6 +33572,28 @@ const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — r
|
|
|
33518
33572
|
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
33519
33573
|
/** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
|
|
33520
33574
|
const commitPath = (path, message) => `git add -A -- ${shellQuote$1(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
|
|
33575
|
+
/**
|
|
33576
|
+
* A `run()` body that records its own failure instead of failing the beat: the
|
|
33577
|
+
* status and the last 50 output lines go to `path`, and the script exits zero
|
|
33578
|
+
* so the turn lands. The file is COMMITTED (and force-pushed with a history
|
|
33579
|
+
* remote), hence the cap.
|
|
33580
|
+
*/
|
|
33581
|
+
const failRestScript = (body, path) => {
|
|
33582
|
+
const p = shellQuote$1(path);
|
|
33583
|
+
const slash = path.lastIndexOf("/");
|
|
33584
|
+
return [
|
|
33585
|
+
`rm -f -- ${p}`,
|
|
33586
|
+
`gtd_run_status=0`,
|
|
33587
|
+
`gtd_run_out="$( {`,
|
|
33588
|
+
body,
|
|
33589
|
+
`} 2>&1 )" || gtd_run_status=$?`,
|
|
33590
|
+
`printf '%s\\n' "$gtd_run_out"`,
|
|
33591
|
+
`if [ "$gtd_run_status" -ne 0 ]; then`,
|
|
33592
|
+
` mkdir -p ${shellQuote$1(path.slice(0, slash))}`,
|
|
33593
|
+
` printf 'exit %s\\n%s\\n' "$gtd_run_status" "$(printf '%s\\n' "$gtd_run_out" | tail -n 50)" >${p}`,
|
|
33594
|
+
`fi`
|
|
33595
|
+
].join("\n");
|
|
33596
|
+
};
|
|
33521
33597
|
/** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
|
|
33522
33598
|
const pinScript = (path) => {
|
|
33523
33599
|
const p = shellQuote$1(path);
|
|
@@ -33581,6 +33657,62 @@ const keepTurn = (refPrefix, message) => {
|
|
|
33581
33657
|
`git reset --hard HEAD`
|
|
33582
33658
|
].join("\n");
|
|
33583
33659
|
};
|
|
33660
|
+
/** `git switch -C <branch> [<start>]`: create or reset the branch, carrying pending changes over. */
|
|
33661
|
+
const switchBranchTo = (branch, start) => `git switch -q -C ${shellQuote$1(branch)}${start === void 0 ? "" : ` ${shellQuote$1(start)}`}`;
|
|
33662
|
+
/** `git switch <branch>`: an existing branch, carrying pending changes over. */
|
|
33663
|
+
const switchTo = (branch) => `git switch -q ${shellQuote$1(branch)}`;
|
|
33664
|
+
/** `switchTo` that, when pending changes block it, stays put and says so: the landing already happened. */
|
|
33665
|
+
const switchBack = (branch) => [
|
|
33666
|
+
`if ! git switch -q ${shellQuote$1(branch)} 2>/dev/null; then`,
|
|
33667
|
+
` printf '%s\\n' ${shellQuote$1(`gtd: pending changes keep HEAD off ${branch}; switch to it once they are committed or stashed`)}`,
|
|
33668
|
+
`fi`
|
|
33669
|
+
].join("\n");
|
|
33670
|
+
const deleteBranch = (branch) => `git branch -q -D ${shellQuote$1(branch)}`;
|
|
33671
|
+
/**
|
|
33672
|
+
* Force-pushes one ref to a remote. A failed push exits non-zero with the retry
|
|
33673
|
+
* on stderr: it runs after the closing commit has landed, so the commit stands.
|
|
33674
|
+
*/
|
|
33675
|
+
const pushRef = (remote, ref) => {
|
|
33676
|
+
const retry = `git push --force ${shellQuote$1(remote)} ${shellQuote$1(`${ref}:${ref}`)}`;
|
|
33677
|
+
return [
|
|
33678
|
+
`if ! ${retry}; then`,
|
|
33679
|
+
` printf '%s\\n' ${shellQuote$1(`gtd: pushing ${ref} failed — retry: ${retry}`)} >&2`,
|
|
33680
|
+
` exit 1`,
|
|
33681
|
+
`fi`
|
|
33682
|
+
].join("\n");
|
|
33683
|
+
};
|
|
33684
|
+
/**
|
|
33685
|
+
* A ship step's landing, in one block because the landing commit's
|
|
33686
|
+
* `Gtd-Shipped` trailer names the squash it makes first. The squash carries
|
|
33687
|
+
* the staged tree with `.gtd/` as `<branch>` has it, built in a scratch index; `<branch>` moves by
|
|
33688
|
+
* compare-and-swap against `parent`, so a branch moved since the decision
|
|
33689
|
+
* fails the script before the landing commits. `commit-tree` ignores
|
|
33690
|
+
* `commit.gpgSign`, so it is read here.
|
|
33691
|
+
*/
|
|
33692
|
+
const shipCommit = (branch, parent, shipMessage, historyRef, message) => [
|
|
33693
|
+
`gtd_ship_parent=${shellQuote$1(parent)}`,
|
|
33694
|
+
`gtd_ship_branch=${shellQuote$1(branch)}`,
|
|
33695
|
+
`gtd_ship_history=${shellQuote$1(historyRef)}`,
|
|
33696
|
+
`gtd_ship_message=${shellQuote$1(shipMessage)}`,
|
|
33697
|
+
`gtd_land_message=${shellQuote$1(message)}`,
|
|
33698
|
+
`git add -A || exit 1`,
|
|
33699
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
33700
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
33701
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
33702
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
33703
|
+
`gtd_index=$(git rev-parse --git-path gtd-ship-index) || exit 1`,
|
|
33704
|
+
`rm -f -- "$gtd_index"`,
|
|
33705
|
+
`GIT_INDEX_FILE="$gtd_index" git read-tree "$gtd_tree" || exit 1`,
|
|
33706
|
+
`GIT_INDEX_FILE="$gtd_index" git rm -r -q --cached --ignore-unmatch -- .gtd || exit 1`,
|
|
33707
|
+
`git ls-tree -r "$gtd_ship_parent" -- .gtd | GIT_INDEX_FILE="$gtd_index" git update-index --index-info || exit 1`,
|
|
33708
|
+
`gtd_ship_tree=$(GIT_INDEX_FILE="$gtd_index" git write-tree) || exit 1`,
|
|
33709
|
+
`rm -f -- "$gtd_index"`,
|
|
33710
|
+
`gtd_ship_trailers=$(printf "Gtd-History: %s\\nGtd-Tip: %s" "$gtd_ship_history" "$gtd_head")`,
|
|
33711
|
+
`gtd_shipped=$(git commit-tree "$gtd_sign" "$gtd_ship_tree" -p "$gtd_ship_parent" -m "$gtd_ship_message" -m "$gtd_ship_trailers") || exit 1`,
|
|
33712
|
+
`git update-ref -m "gtd: ship" "refs/heads/$gtd_ship_branch" "$gtd_shipped" "$gtd_ship_parent" || exit 1`,
|
|
33713
|
+
`gtd_message=$(printf "%s\\nGtd-Shipped: %s" "$gtd_land_message" "$gtd_shipped")`,
|
|
33714
|
+
commitWordAllowEmpty(`"$gtd_message"`)
|
|
33715
|
+
].join("\n");
|
|
33584
33716
|
const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
|
|
33585
33717
|
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
33586
33718
|
const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
|
|
@@ -33609,15 +33741,18 @@ const renderOutcome = (outcome) => {
|
|
|
33609
33741
|
case "note": return printfLine$1("%s\n", [shellQuote$1(outcome.text)]);
|
|
33610
33742
|
}
|
|
33611
33743
|
};
|
|
33612
|
-
const
|
|
33613
|
-
|
|
33614
|
-
|
|
33615
|
-
|
|
33616
|
-
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
33744
|
+
const GIT_WRITES = {
|
|
33745
|
+
commitAll: (w) => commitAll(w.message),
|
|
33746
|
+
commitPath: (w) => commitPath(w.path, w.message),
|
|
33747
|
+
commitClean: (w) => commitClean(w.message),
|
|
33748
|
+
updateRef: (w) => updateRef(w.ref, w.target),
|
|
33749
|
+
keepTurn: (w) => keepTurn(w.ref, w.message),
|
|
33750
|
+
switchBranch: (w) => switchBranchTo(w.branch),
|
|
33751
|
+
switchTo: (w) => switchBack(w.branch),
|
|
33752
|
+
pushRef: (w) => pushRef(w.remote, w.ref),
|
|
33753
|
+
ship: (w) => shipCommit(w.branch, w.parent, w.shipMessage, w.historyRef, w.message)
|
|
33620
33754
|
};
|
|
33755
|
+
const renderGitWrite = (write) => GIT_WRITES[write.kind](write);
|
|
33621
33756
|
/**
|
|
33622
33757
|
* Wraps `command` so a non-zero exit prints `prompt` plus the command's
|
|
33623
33758
|
* captured output before propagating that exit code — mirrors `Emit.ts`'s
|
|
@@ -34010,6 +34145,11 @@ const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot
|
|
|
34010
34145
|
file: snapshot.file
|
|
34011
34146
|
}] : [];
|
|
34012
34147
|
const gitWriteOf = (landing, message) => {
|
|
34148
|
+
if (landing.ship !== void 0) return {
|
|
34149
|
+
kind: "ship",
|
|
34150
|
+
message,
|
|
34151
|
+
...landing.ship
|
|
34152
|
+
};
|
|
34013
34153
|
if (landing.only !== void 0) return {
|
|
34014
34154
|
kind: "commitPath",
|
|
34015
34155
|
path: landing.only,
|
|
@@ -34029,6 +34169,10 @@ const gitWriteOf = (landing, message) => {
|
|
|
34029
34169
|
message
|
|
34030
34170
|
};
|
|
34031
34171
|
};
|
|
34172
|
+
const writeIf = (value, write) => value === void 0 ? [] : [{
|
|
34173
|
+
kind: "gitWrite",
|
|
34174
|
+
write: write(value)
|
|
34175
|
+
}];
|
|
34032
34176
|
const planStep = (snapshot, opts = {}) => {
|
|
34033
34177
|
const landing = snapshot.landing;
|
|
34034
34178
|
if (landing.kind === "refusal") return {
|
|
@@ -34076,6 +34220,7 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
34076
34220
|
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
34077
34221
|
});
|
|
34078
34222
|
const subject = message.split("\n")[0];
|
|
34223
|
+
const historyRef = landing.keepHistory ?? landing.ship?.historyRef;
|
|
34079
34224
|
const outcome = landing.to === snapshot.state ? {
|
|
34080
34225
|
kind: "outcome",
|
|
34081
34226
|
outcome: {
|
|
@@ -34096,20 +34241,37 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
34096
34241
|
to: landing.to,
|
|
34097
34242
|
subject,
|
|
34098
34243
|
steps: [
|
|
34244
|
+
...writeIf(landing.startBranch, (branch) => ({
|
|
34245
|
+
kind: "switchBranch",
|
|
34246
|
+
branch
|
|
34247
|
+
})),
|
|
34099
34248
|
...uncheckStep(snapshot),
|
|
34100
34249
|
{
|
|
34101
34250
|
kind: "gitWrite",
|
|
34102
34251
|
write: gitWriteOf(landing, message)
|
|
34103
34252
|
},
|
|
34104
|
-
...
|
|
34105
|
-
kind: "
|
|
34106
|
-
|
|
34107
|
-
|
|
34108
|
-
|
|
34109
|
-
|
|
34253
|
+
...writeIf(historyRef, (ref) => ({
|
|
34254
|
+
kind: "updateRef",
|
|
34255
|
+
ref,
|
|
34256
|
+
target: "HEAD"
|
|
34257
|
+
})),
|
|
34258
|
+
...writeIf(landing.leaveBranch, (branch) => ({
|
|
34259
|
+
kind: "switchTo",
|
|
34260
|
+
branch
|
|
34261
|
+
})),
|
|
34262
|
+
outcome,
|
|
34263
|
+
...landing.note !== void 0 ? [{
|
|
34264
|
+
kind: "outcome",
|
|
34265
|
+
outcome: {
|
|
34266
|
+
kind: "note",
|
|
34267
|
+
text: landing.note
|
|
34110
34268
|
}
|
|
34111
34269
|
}] : [],
|
|
34112
|
-
|
|
34270
|
+
...writeIf(historyRef !== void 0 ? landing.pushHistory : void 0, (remote) => ({
|
|
34271
|
+
kind: "pushRef",
|
|
34272
|
+
remote,
|
|
34273
|
+
ref: historyRef
|
|
34274
|
+
}))
|
|
34113
34275
|
]
|
|
34114
34276
|
};
|
|
34115
34277
|
};
|
|
@@ -34129,6 +34291,43 @@ const accessRefusal = (changes, write) => {
|
|
|
34129
34291
|
`allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
|
|
34130
34292
|
].join("\n");
|
|
34131
34293
|
};
|
|
34294
|
+
// src/step/startBranch.ts
|
|
34295
|
+
init_dist$1();
|
|
34296
|
+
const GTD_BRANCH_PREFIX = "gtd/";
|
|
34297
|
+
/** `<b>` for a `gtd/<b>` branch name; `undefined` for any other. */
|
|
34298
|
+
const userBranchOf = (branch) => branch.startsWith("gtd/") ? branch.slice(4) : void 0;
|
|
34299
|
+
/**
|
|
34300
|
+
* Where a new process starts: on `gtd/<b>`, so `<b>` never carries its commits.
|
|
34301
|
+
* An existing `gtd/<b>` is reused only when its tip is contained in `<b>` or in
|
|
34302
|
+
* a kept history ref (a squash onto `<b>` never makes it an ancestor).
|
|
34303
|
+
*/
|
|
34304
|
+
const startBranchFor = (commandLabel) => gen(function* () {
|
|
34305
|
+
const git = yield* GitService;
|
|
34306
|
+
const current = yield* git.currentBranch();
|
|
34307
|
+
if (isNone(current) || current.value.startsWith("gtd/")) return { kind: "inPlace" };
|
|
34308
|
+
if (!(yield* git.hasCommits())) return { kind: "inPlace" };
|
|
34309
|
+
const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
|
|
34310
|
+
const existing = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
34311
|
+
if (isSome(existing)) {
|
|
34312
|
+
const head = yield* git.resolveRef("HEAD");
|
|
34313
|
+
let contained = yield* git.isAncestor(existing.value, head);
|
|
34314
|
+
if (!contained) {
|
|
34315
|
+
const kept = yield* git.listRefs(PROCESS_HISTORY_PREFIX);
|
|
34316
|
+
for (const k of kept) if (yield* git.isAncestor(existing.value, k.hash)) {
|
|
34317
|
+
contained = true;
|
|
34318
|
+
break;
|
|
34319
|
+
}
|
|
34320
|
+
}
|
|
34321
|
+
if (!contained) return {
|
|
34322
|
+
kind: "refusal",
|
|
34323
|
+
message: `${commandLabel}: branch ${branch} holds commits that are neither on ${current.value} nor in a kept history ref — merge or delete it first`
|
|
34324
|
+
};
|
|
34325
|
+
}
|
|
34326
|
+
return {
|
|
34327
|
+
kind: "switch",
|
|
34328
|
+
branch
|
|
34329
|
+
};
|
|
34330
|
+
});
|
|
34132
34331
|
// src/step/planStart.ts
|
|
34133
34332
|
init_dist$1();
|
|
34134
34333
|
const refusal = (message) => ({
|
|
@@ -34170,29 +34369,42 @@ const planStart = (current, actor, start) => gen(function* () {
|
|
|
34170
34369
|
const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
|
|
34171
34370
|
if (typeof base === "object") return base;
|
|
34172
34371
|
const subject = formatSubject(actor, firstStep);
|
|
34372
|
+
const message = formatCommitMessage({
|
|
34373
|
+
actor,
|
|
34374
|
+
to: firstStep,
|
|
34375
|
+
workflow: name,
|
|
34376
|
+
...base !== void 0 ? { reviewBase: base } : {},
|
|
34377
|
+
vars
|
|
34378
|
+
});
|
|
34379
|
+
const onBranch = yield* startBranchFor(commandLabel);
|
|
34380
|
+
if (onBranch.kind === "refusal") return refusal(onBranch.message);
|
|
34173
34381
|
return {
|
|
34174
34382
|
kind: "start",
|
|
34175
34383
|
workflow: name,
|
|
34176
34384
|
subject,
|
|
34177
|
-
steps: [
|
|
34178
|
-
kind
|
|
34179
|
-
|
|
34180
|
-
|
|
34181
|
-
|
|
34182
|
-
|
|
34183
|
-
|
|
34184
|
-
|
|
34185
|
-
|
|
34186
|
-
|
|
34187
|
-
|
|
34188
|
-
|
|
34189
|
-
|
|
34190
|
-
|
|
34191
|
-
|
|
34192
|
-
|
|
34193
|
-
|
|
34385
|
+
steps: [
|
|
34386
|
+
...onBranch.kind === "switch" ? [{
|
|
34387
|
+
kind: "gitWrite",
|
|
34388
|
+
write: {
|
|
34389
|
+
kind: "switchBranch",
|
|
34390
|
+
branch: onBranch.branch
|
|
34391
|
+
}
|
|
34392
|
+
}] : [],
|
|
34393
|
+
{
|
|
34394
|
+
kind: "gitWrite",
|
|
34395
|
+
write: {
|
|
34396
|
+
kind: "commitAll",
|
|
34397
|
+
message
|
|
34398
|
+
}
|
|
34399
|
+
},
|
|
34400
|
+
{
|
|
34401
|
+
kind: "outcome",
|
|
34402
|
+
outcome: {
|
|
34403
|
+
kind: "commit",
|
|
34404
|
+
subject
|
|
34405
|
+
}
|
|
34194
34406
|
}
|
|
34195
|
-
|
|
34407
|
+
]
|
|
34196
34408
|
};
|
|
34197
34409
|
});
|
|
34198
34410
|
/**
|
|
@@ -34438,14 +34650,32 @@ const runAtTip = (git, def, tip) => gen(function* () {
|
|
|
34438
34650
|
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
34439
34651
|
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
34440
34652
|
});
|
|
34441
|
-
|
|
34442
|
-
|
|
34443
|
-
|
|
34444
|
-
|
|
34653
|
+
/**
|
|
34654
|
+
* The process HEAD closes or sits inside — `gtd summary`'s run. On `<b>`, where
|
|
34655
|
+
* a finished process leaves HEAD, it is the one `gtd/<b>`'s tip closes, while
|
|
34656
|
+
* `<b>` holds nothing since: its start, or its last ship.
|
|
34657
|
+
*/
|
|
34445
34658
|
const summaryRun = gen(function* () {
|
|
34446
34659
|
const git = yield* GitService;
|
|
34447
34660
|
const def = (yield* (yield* ConfigService).load).workflow;
|
|
34448
|
-
|
|
34661
|
+
const head = (yield* git.subjectHistory(1, 0))[0];
|
|
34662
|
+
const left = head === void 0 ? void 0 : yield* leftProcessTip(git, head);
|
|
34663
|
+
return yield* runAtTip(git, def, left ?? head);
|
|
34664
|
+
});
|
|
34665
|
+
const leftProcessTip = (git, head) => gen(function* () {
|
|
34666
|
+
if (parseCommitMessage(head.subject).parsed !== void 0) return void 0;
|
|
34667
|
+
const current = yield* git.currentBranch();
|
|
34668
|
+
if (isNone(current) || userBranchOf(current.value) !== void 0) return void 0;
|
|
34669
|
+
const tip = yield* git.readRefOption(`refs/heads/${GTD_BRANCH_PREFIX}${current.value}`);
|
|
34670
|
+
if (isNone(tip)) return void 0;
|
|
34671
|
+
const message = yield* git.lastCommitMessage(tip.value);
|
|
34672
|
+
if (parseCommitMessage(message).parsed?.to === void 0) return void 0;
|
|
34673
|
+
const since = parseCommitMessage(yield* git.lastCommitMessage(head.hash)).tip ?? head.hash;
|
|
34674
|
+
if (!(yield* git.isAncestor(since, tip.value))) return void 0;
|
|
34675
|
+
return {
|
|
34676
|
+
hash: tip.value,
|
|
34677
|
+
subject: message.split("\n")[0]
|
|
34678
|
+
};
|
|
34449
34679
|
});
|
|
34450
34680
|
const commitTree = (workspace, hash) => {
|
|
34451
34681
|
let entries;
|
|
@@ -34579,7 +34809,14 @@ const promptDef = (common, request, skills, access) => ({
|
|
|
34579
34809
|
const scriptDef = (common, request) => ({
|
|
34580
34810
|
...common,
|
|
34581
34811
|
kind: "script",
|
|
34582
|
-
content: request.body
|
|
34812
|
+
content: request.options.onFail === "rest" ? failRestScript(request.body, RUN_FAILED_PATH) : request.body,
|
|
34813
|
+
...optional("onFail", request.options.onFail)
|
|
34814
|
+
});
|
|
34815
|
+
const SHIP_CONTENT = noteOutcome("gtd: landing this step ships the process onto your branch");
|
|
34816
|
+
const shipDef = (common) => ({
|
|
34817
|
+
...common,
|
|
34818
|
+
kind: "script",
|
|
34819
|
+
content: SHIP_CONTENT
|
|
34583
34820
|
});
|
|
34584
34821
|
const judgeDef = (common, request, step) => {
|
|
34585
34822
|
const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
|
|
@@ -34603,6 +34840,7 @@ const stepDefOf = (step) => {
|
|
|
34603
34840
|
switch (request.kind) {
|
|
34604
34841
|
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
34605
34842
|
case "run": return scriptDef(common, request);
|
|
34843
|
+
case "ship": return shipDef(common);
|
|
34606
34844
|
case "judge": return judgeDef(common, request, step);
|
|
34607
34845
|
case "human": return messageDef(common, request, step);
|
|
34608
34846
|
}
|
|
@@ -34756,6 +34994,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
34756
34994
|
path: e.path
|
|
34757
34995
|
})) : [];
|
|
34758
34996
|
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`);
|
|
34997
|
+
const shipBlocked = ref === void 0 ? yield* shipBlockedAt(setup, pin, step, changes) : void 0;
|
|
34759
34998
|
return {
|
|
34760
34999
|
def,
|
|
34761
35000
|
workflowName: resolved.name,
|
|
@@ -34774,7 +35013,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
34774
35013
|
memoryResumed: memory.resumed,
|
|
34775
35014
|
hints: hintsOf(stepDef),
|
|
34776
35015
|
cost: costTotals(run.costEntries),
|
|
34777
|
-
setup
|
|
35016
|
+
setup,
|
|
35017
|
+
...shipBlocked !== void 0 ? { shipBlocked } : {}
|
|
34778
35018
|
};
|
|
34779
35019
|
});
|
|
34780
35020
|
const currentRest = restAt(void 0);
|
|
@@ -34903,6 +35143,8 @@ const renderRest = (rest) => succeed$2({
|
|
|
34903
35143
|
kind: rest.stepDef.kind,
|
|
34904
35144
|
content: rest.stepDef.content,
|
|
34905
35145
|
...rest.hints,
|
|
35146
|
+
...failedRunStalled(rest) ? { failure: rest.setup.workspace.readSync(".gtd/run-failed.txt") ?? "" } : {},
|
|
35147
|
+
...rest.shipBlocked !== void 0 ? { blocked: rest.shipBlocked } : {},
|
|
34906
35148
|
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
34907
35149
|
memoryResumed: rest.memoryResumed,
|
|
34908
35150
|
...rest.pin !== void 0 ? { pin: rest.pin } : {}
|
|
@@ -34911,7 +35153,9 @@ const renderRest = (rest) => succeed$2({
|
|
|
34911
35153
|
* Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
|
|
34912
35154
|
* attempt at this very agent rest, and another dispatch would repeat it.
|
|
34913
35155
|
*/
|
|
34914
|
-
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;
|
|
35156
|
+
const stalledAt = (rest) => failedRunStalled(rest) || rest.shipBlocked !== void 0 || 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;
|
|
35157
|
+
/** A `run(…, { onFail: "rest" })` step whose last turn recorded a failure, on a clean tree. */
|
|
35158
|
+
const failedRunStalled = (rest) => rest.changes.length === 0 && rest.stepDef.onFail === "rest" && rest.run.headTurn?.state === rest.state && !rest.run.headTurn.step && rest.setup.workspace.readSync(".gtd/run-failed.txt") !== void 0;
|
|
34915
35159
|
/** 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. */
|
|
34916
35160
|
const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
|
|
34917
35161
|
/** `idle` means exactly one thing: no process underway, clean tree. */
|
|
@@ -35076,7 +35320,7 @@ const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "e
|
|
|
35076
35320
|
* replay with the working tree as the pending turn. The target step is
|
|
35077
35321
|
* computed by replaying, never matched.
|
|
35078
35322
|
*/
|
|
35079
|
-
const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
35323
|
+
const decideLanding = (rest, verdicts, branch, approver, shipped) => gen(function* () {
|
|
35080
35324
|
if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
|
|
35081
35325
|
const unapproved = approvalRefusal(rest, approver);
|
|
35082
35326
|
if (unapproved !== void 0) return unapproved;
|
|
@@ -35089,30 +35333,183 @@ const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
|
35089
35333
|
message
|
|
35090
35334
|
};
|
|
35091
35335
|
}
|
|
35336
|
+
const landing = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef));
|
|
35337
|
+
if (rest.stepDef.onFail === "rest" && landing.read(".gtd/run-failed.txt") !== void 0) return {
|
|
35338
|
+
kind: "attempt",
|
|
35339
|
+
subject: formatSubject(rest.actor, rest.state)
|
|
35340
|
+
};
|
|
35092
35341
|
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
35093
|
-
tree:
|
|
35094
|
-
...verdicts !== void 0 ? { verdicts } : {}
|
|
35342
|
+
tree: landing,
|
|
35343
|
+
...verdicts !== void 0 ? { verdicts } : {},
|
|
35344
|
+
...shipped !== void 0 ? { shipped } : {}
|
|
35095
35345
|
}));
|
|
35096
35346
|
const to = yield* landingTarget(outcome, rest);
|
|
35097
35347
|
if (typeof to !== "string") return to;
|
|
35098
35348
|
return landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
35099
35349
|
});
|
|
35350
|
+
/** The user's branch: `<b>` for a process on `gtd/<b>` while `<b>` still exists; any other branch as is. */
|
|
35351
|
+
const userBranch = (git) => gen(function* () {
|
|
35352
|
+
const current = yield* git.currentBranch();
|
|
35353
|
+
const name = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
35354
|
+
if (name === void 0) return current;
|
|
35355
|
+
const there = yield* git.readRefOption(`refs/heads/${name}`);
|
|
35356
|
+
return isSome(there) ? some(name) : current;
|
|
35357
|
+
});
|
|
35358
|
+
/** `<b>` for a process on `gtd/<b>` while `<b>` exists: where a finishing landing leaves HEAD. */
|
|
35359
|
+
const returnBranch = gen(function* () {
|
|
35360
|
+
const git = yield* GitService;
|
|
35361
|
+
const current = yield* git.currentBranch();
|
|
35362
|
+
const name = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
35363
|
+
if (name === void 0) return void 0;
|
|
35364
|
+
return isSome(yield* git.readRefOption(`refs/heads/${name}`)) ? name : void 0;
|
|
35365
|
+
});
|
|
35366
|
+
/** A starting landing moves onto `gtd/<b>` first. */
|
|
35367
|
+
const withBranchAndPush = (landing) => gen(function* () {
|
|
35368
|
+
if (landing.kind !== "commit") return landing;
|
|
35369
|
+
let out = landing;
|
|
35370
|
+
if (landing.spec.vars !== void 0) {
|
|
35371
|
+
const start = yield* startBranchFor("gtd land");
|
|
35372
|
+
if (start.kind === "refusal") return {
|
|
35373
|
+
kind: "refusal",
|
|
35374
|
+
message: start.message
|
|
35375
|
+
};
|
|
35376
|
+
if (start.kind === "switch") out = {
|
|
35377
|
+
...out,
|
|
35378
|
+
startBranch: start.branch
|
|
35379
|
+
};
|
|
35380
|
+
}
|
|
35381
|
+
return yield* withHistory(out);
|
|
35382
|
+
});
|
|
35383
|
+
/** A closing landing switches back to `<b>`; it and a shipping one push their history ref. */
|
|
35384
|
+
const withHistory = (landing) => gen(function* () {
|
|
35385
|
+
let out = landing;
|
|
35386
|
+
const back = landing.keepHistory === void 0 ? void 0 : yield* returnBranch;
|
|
35387
|
+
if (back !== void 0) out = {
|
|
35388
|
+
...out,
|
|
35389
|
+
leaveBranch: back
|
|
35390
|
+
};
|
|
35391
|
+
if (landing.keepHistory === void 0 && landing.ship === void 0) return out;
|
|
35392
|
+
const remote = (yield* (yield* ConfigService).load).historyRemote;
|
|
35393
|
+
return remote === void 0 ? out : {
|
|
35394
|
+
...out,
|
|
35395
|
+
pushHistory: remote
|
|
35396
|
+
};
|
|
35397
|
+
});
|
|
35100
35398
|
/** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
|
|
35101
|
-
const previewLanding = (rest) => decideLanding(rest, void 0, none(), none()).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$2(void 0)));
|
|
35399
|
+
const previewLanding = (rest) => decideLanding(rest, void 0, none(), none(), rest.step.request.kind === "ship" ? SHIPPED_STAND_IN : void 0).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$2(void 0)));
|
|
35400
|
+
const snapshotOf = (rest, landing) => ({
|
|
35401
|
+
stepDef: rest.stepDef,
|
|
35402
|
+
state: rest.state,
|
|
35403
|
+
actor: rest.actor,
|
|
35404
|
+
changes: rest.changes,
|
|
35405
|
+
file: rest.hints.file,
|
|
35406
|
+
landing
|
|
35407
|
+
});
|
|
35408
|
+
const refusedSnapshot = (rest, message) => snapshotOf(rest, {
|
|
35409
|
+
kind: "refusal",
|
|
35410
|
+
message
|
|
35411
|
+
});
|
|
35412
|
+
const SHIPPED_STAND_IN = "0".repeat(40);
|
|
35413
|
+
const SHIP_EXCLUDED = ".gtd/";
|
|
35414
|
+
const shippedPaths = (before, after) => {
|
|
35415
|
+
const diff = diffTrees(before, after);
|
|
35416
|
+
return [
|
|
35417
|
+
...diff.added,
|
|
35418
|
+
...diff.modified,
|
|
35419
|
+
...diff.deleted
|
|
35420
|
+
].filter((path) => !path.startsWith(SHIP_EXCLUDED));
|
|
35421
|
+
};
|
|
35422
|
+
const noTarget = (why) => ({
|
|
35423
|
+
kind: "skip",
|
|
35424
|
+
note: `gtd: no branch to ship to: ${why}`
|
|
35425
|
+
});
|
|
35426
|
+
/** `<b>` and its tip, for a process on `gtd/<b>`; why there is none otherwise. */
|
|
35427
|
+
const shipTarget = (git) => gen(function* () {
|
|
35428
|
+
const current = yield* git.currentBranch();
|
|
35429
|
+
if (isNone(current)) return noTarget("HEAD is detached");
|
|
35430
|
+
const branch = userBranchOf(current.value);
|
|
35431
|
+
if (branch === void 0) return noTarget(`${current.value} is no gtd/ branch`);
|
|
35432
|
+
if (userBranchOf(branch) !== void 0) return noTarget(`${current.value} records no user branch`);
|
|
35433
|
+
const parent = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
35434
|
+
if (isNone(parent)) return noTarget(`${branch} does not exist`);
|
|
35435
|
+
return {
|
|
35436
|
+
kind: "ship",
|
|
35437
|
+
branch,
|
|
35438
|
+
parent: parent.value
|
|
35439
|
+
};
|
|
35440
|
+
});
|
|
35441
|
+
/**
|
|
35442
|
+
* The paths `<b>` changed outside this process that shipping `tree` would
|
|
35443
|
+
* revert. A path the working tree changed since HEAD counts as brought in:
|
|
35444
|
+
* whether it carries `<b>`'s change is the person's call, not a merge gtd redoes.
|
|
35445
|
+
*/
|
|
35446
|
+
const revertedPaths = (git, workspace, parent, tree, changed) => gen(function* () {
|
|
35447
|
+
const head = yield* git.resolveRef("HEAD");
|
|
35448
|
+
const shipped = parseCommitMessage(yield* git.lastCommitMessage(parent)).tip;
|
|
35449
|
+
if (shipped !== void 0 && (yield* git.isAncestor(shipped, head))) return [];
|
|
35450
|
+
const fork = yield* git.mergeBase(parent, head);
|
|
35451
|
+
const foreign = isNone(fork) ? [...changed] : shippedPaths(commitTree(workspace, fork.value), commitTree(workspace, parent));
|
|
35452
|
+
const committed = commitTree(workspace, head);
|
|
35453
|
+
return foreign.filter((path) => changed.has(path) && tree.read(path) === committed.read(path));
|
|
35454
|
+
});
|
|
35455
|
+
/**
|
|
35456
|
+
* Whether landing a ship step with `tree` would ship, and onto what. The squash
|
|
35457
|
+
* replaces the user's branch's tree with `tree`, so it is blocked while that
|
|
35458
|
+
* branch carries a change `tree` would revert: one made since the last ship
|
|
35459
|
+
* (whose `Gtd-Tip` HEAD descends from), or since it forked from HEAD.
|
|
35460
|
+
*/
|
|
35461
|
+
const decideShip = (workspace, tree) => gen(function* () {
|
|
35462
|
+
const git = yield* GitService;
|
|
35463
|
+
const target = yield* shipTarget(git);
|
|
35464
|
+
if (target.kind !== "ship") return target;
|
|
35465
|
+
const { branch, parent } = target;
|
|
35466
|
+
const changed = new Set(shippedPaths(commitTree(workspace, parent), tree));
|
|
35467
|
+
const reverted = yield* revertedPaths(git, workspace, parent, tree, changed);
|
|
35468
|
+
if (reverted.length > 0) return {
|
|
35469
|
+
kind: "blocked",
|
|
35470
|
+
message: `gtd: ${branch} changed outside this process, and shipping would revert ${reverted.join(", ")} — bring those changes into the working tree (\`git merge --squash --no-commit ${branch}\`, or by hand) and land`
|
|
35471
|
+
};
|
|
35472
|
+
return changed.size === 0 ? {
|
|
35473
|
+
kind: "skip",
|
|
35474
|
+
note: `gtd: nothing to ship: ${branch} already has this tree`
|
|
35475
|
+
} : target;
|
|
35476
|
+
});
|
|
35477
|
+
const shipBlockedAt = (setup, pin, step, changes) => gen(function* () {
|
|
35478
|
+
if (pin !== void 0 || step.request.kind !== "ship" || changes.length > 0) return void 0;
|
|
35479
|
+
const head = yield* (yield* GitService).resolveRef("HEAD");
|
|
35480
|
+
const decision = yield* decideShip(setup.workspace, commitTree(setup.workspace, head));
|
|
35481
|
+
return decision.kind === "blocked" ? decision.message : void 0;
|
|
35482
|
+
});
|
|
35483
|
+
const shipSnapshot = (rest, message, verdicts, branch, approver) => gen(function* () {
|
|
35484
|
+
const tree = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), void 0);
|
|
35485
|
+
const decision = yield* decideShip(rest.setup.workspace, tree);
|
|
35486
|
+
if (decision.kind === "blocked") return refusedSnapshot(rest, decision.message);
|
|
35487
|
+
const shipping = decision.kind === "ship";
|
|
35488
|
+
const decided = yield* decideLanding(rest, verdicts, branch, approver, shipping ? SHIPPED_STAND_IN : void 0);
|
|
35489
|
+
if (decided.kind !== "commit") return snapshotOf(rest, decided);
|
|
35490
|
+
const landing = shipping ? {
|
|
35491
|
+
...decided,
|
|
35492
|
+
ship: {
|
|
35493
|
+
branch: decision.branch,
|
|
35494
|
+
parent: decision.parent,
|
|
35495
|
+
shipMessage: message,
|
|
35496
|
+
historyRef: processHistoryRef(rest.run.startHash)
|
|
35497
|
+
}
|
|
35498
|
+
} : {
|
|
35499
|
+
...decided,
|
|
35500
|
+
note: decision.note
|
|
35501
|
+
};
|
|
35502
|
+
return snapshotOf(rest, yield* withBranchAndPush(landing));
|
|
35503
|
+
});
|
|
35102
35504
|
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
35103
35505
|
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
35104
35506
|
const git = yield* GitService;
|
|
35105
|
-
const branch = yield* git
|
|
35507
|
+
const branch = yield* userBranch(git);
|
|
35106
35508
|
const approver = rest.stepDef.approve === true ? yield* git.authorIdent() : none();
|
|
35107
|
-
const
|
|
35108
|
-
return
|
|
35109
|
-
|
|
35110
|
-
|
|
35111
|
-
actor: rest.actor,
|
|
35112
|
-
changes: rest.changes,
|
|
35113
|
-
file: rest.hints.file,
|
|
35114
|
-
landing
|
|
35115
|
-
};
|
|
35509
|
+
const request = rest.step.request;
|
|
35510
|
+
if (rest.pin === void 0 && request.kind === "ship") return yield* shipSnapshot(rest, request.message, verdicts, branch, approver);
|
|
35511
|
+
const decided = yield* decideLanding(rest, verdicts, branch, approver);
|
|
35512
|
+
return snapshotOf(rest, yield* withBranchAndPush(decided));
|
|
35116
35513
|
});
|
|
35117
35514
|
const humanCommitsOf = (trace, entryCommit) => trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
|
|
35118
35515
|
hash: entry.hash,
|
|
@@ -36248,6 +36645,32 @@ const runStartListCommand = (json, out) => gen(function* () {
|
|
|
36248
36645
|
out.write([...workflows.map((name) => `workflow ${name}\n`), ...doors.map(([n, d]) => `door ${doorSynopsis(n, d)} → ${d.workflow}\n`)].join(""));
|
|
36249
36646
|
});
|
|
36250
36647
|
/**
|
|
36648
|
+
* Back on `<b>` when the process ran on `gtd/<b>` and `<b>` moved only by its
|
|
36649
|
+
* ships (whose `Gtd-Tip` the tip descends from): the rewind stops at the last
|
|
36650
|
+
* ship, so the uncommitted changes are what it left unshipped.
|
|
36651
|
+
*/
|
|
36652
|
+
const abandonTarget = (current, tip, startParent) => gen(function* () {
|
|
36653
|
+
const git = yield* GitService;
|
|
36654
|
+
const userBranch = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
36655
|
+
const userTip = userBranch === void 0 ? none() : yield* git.readRefOption(`refs/heads/${userBranch}`);
|
|
36656
|
+
if (userBranch === void 0 || isNone(userTip)) return {
|
|
36657
|
+
leaves: void 0,
|
|
36658
|
+
rewindTo: startParent
|
|
36659
|
+
};
|
|
36660
|
+
if (userTip.value === startParent) return {
|
|
36661
|
+
leaves: userBranch,
|
|
36662
|
+
rewindTo: startParent
|
|
36663
|
+
};
|
|
36664
|
+
const shipped = parseCommitMessage(yield* git.lastCommitMessage(userTip.value)).tip;
|
|
36665
|
+
return shipped !== void 0 && (yield* git.isAncestor(shipped, tip)) ? {
|
|
36666
|
+
leaves: userBranch,
|
|
36667
|
+
rewindTo: userTip.value
|
|
36668
|
+
} : {
|
|
36669
|
+
leaves: void 0,
|
|
36670
|
+
rewindTo: startParent
|
|
36671
|
+
};
|
|
36672
|
+
});
|
|
36673
|
+
/**
|
|
36251
36674
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
36252
36675
|
* `git reset --mixed` to the process's start commit leaves every turn
|
|
36253
36676
|
* commit's content in the working tree.
|
|
@@ -36262,7 +36685,8 @@ const runStartListCommand = (json, out) => gen(function* () {
|
|
|
36262
36685
|
*/
|
|
36263
36686
|
const runAbandonCommand = (json, out) => gen(function* () {
|
|
36264
36687
|
const git = yield* GitService;
|
|
36265
|
-
const
|
|
36688
|
+
const config = yield* (yield* ConfigService).load;
|
|
36689
|
+
const initial = config.workflow.initial;
|
|
36266
36690
|
const run = yield* currentRun;
|
|
36267
36691
|
if (run.trace.length === 0) {
|
|
36268
36692
|
const required = emitScripts([{
|
|
@@ -36274,6 +36698,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
36274
36698
|
const restState = run.trace[run.trace.length - 1].state;
|
|
36275
36699
|
if (run.startParentHash === EMPTY_TREE) return yield* fail$8(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
|
|
36276
36700
|
const tip = yield* git.resolveRef("HEAD");
|
|
36701
|
+
const historyRef = processHistoryRef(run.startHash);
|
|
36702
|
+
const current = yield* git.currentBranch();
|
|
36703
|
+
const { leaves, rewindTo } = yield* abandonTarget(current, tip, run.startParentHash);
|
|
36704
|
+
const remote = config.historyRemote;
|
|
36277
36705
|
const steps = [
|
|
36278
36706
|
{
|
|
36279
36707
|
kind: "gitWrite",
|
|
@@ -36281,21 +36709,60 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
36281
36709
|
},
|
|
36282
36710
|
{
|
|
36283
36711
|
kind: "gitWrite",
|
|
36284
|
-
command:
|
|
36712
|
+
command: updateRef(historyRef, tip)
|
|
36713
|
+
},
|
|
36714
|
+
{
|
|
36715
|
+
kind: "gitWrite",
|
|
36716
|
+
command: mixedResetTo(rewindTo)
|
|
36285
36717
|
},
|
|
36286
36718
|
{
|
|
36287
36719
|
kind: "gitWrite",
|
|
36288
36720
|
command: unpinScript(PIN_PATH)
|
|
36289
36721
|
},
|
|
36722
|
+
...leaves !== void 0 ? [
|
|
36723
|
+
{
|
|
36724
|
+
kind: "gitWrite",
|
|
36725
|
+
command: switchTo(leaves)
|
|
36726
|
+
},
|
|
36727
|
+
{
|
|
36728
|
+
kind: "gitWrite",
|
|
36729
|
+
command: deleteBranch(getOrThrow$1(current))
|
|
36730
|
+
},
|
|
36731
|
+
{
|
|
36732
|
+
kind: "gitWrite",
|
|
36733
|
+
command: updateRef(RESUME_BRANCH_REF, tip)
|
|
36734
|
+
}
|
|
36735
|
+
] : [{
|
|
36736
|
+
kind: "gitWrite",
|
|
36737
|
+
command: deleteRef(RESUME_BRANCH_REF)
|
|
36738
|
+
}],
|
|
36290
36739
|
{
|
|
36291
36740
|
kind: "outcome",
|
|
36292
|
-
command: abandonedOutcome(restState,
|
|
36293
|
-
}
|
|
36741
|
+
command: abandonedOutcome(restState, rewindTo, initial)
|
|
36742
|
+
},
|
|
36743
|
+
...remote !== void 0 ? [{
|
|
36744
|
+
kind: "gitWrite",
|
|
36745
|
+
command: pushRef(remote, historyRef)
|
|
36746
|
+
}] : []
|
|
36294
36747
|
];
|
|
36295
36748
|
const required = emitScripts(steps).required;
|
|
36296
36749
|
yield* writeScript(json, out, combinedScript(required, ""));
|
|
36297
36750
|
});
|
|
36298
36751
|
/**
|
|
36752
|
+
* `gtd/<b>` when the abandon left it for `<b>` and HEAD is still on `<b>`. A
|
|
36753
|
+
* `gtd/<b>` made since is reset only when the retained tip contains it — the
|
|
36754
|
+
* `-C` switch would orphan anything else.
|
|
36755
|
+
*/
|
|
36756
|
+
const resumeBranch = (git, tip) => gen(function* () {
|
|
36757
|
+
if (isNone(yield* git.readRefOption("refs/worktree/gtd/history-branch"))) return void 0;
|
|
36758
|
+
const current = yield* git.currentBranch();
|
|
36759
|
+
if (isNone(current) || current.value.startsWith("gtd/")) return void 0;
|
|
36760
|
+
const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
|
|
36761
|
+
const existing = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
36762
|
+
if (isSome(existing) && !(yield* git.isAncestor(existing.value, tip))) return yield* fail$8(/* @__PURE__ */ new Error(`gtd restore: branch ${branch} holds commits the retained tip ${tip.slice(0, 7)} does not — merge or delete it first`));
|
|
36763
|
+
return branch;
|
|
36764
|
+
});
|
|
36765
|
+
/**
|
|
36299
36766
|
* `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
|
|
36300
36767
|
*
|
|
36301
36768
|
* Guarded by `restorability` so it never discards work it didn't create:
|
|
@@ -36312,15 +36779,20 @@ const runRestoreCommand = (json, out) => gen(function* () {
|
|
|
36312
36779
|
const check = yield* restorability(git, headHash, tip);
|
|
36313
36780
|
if (!check.ok) return yield* fail$8(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
|
|
36314
36781
|
const after = yield* restAt(tip);
|
|
36782
|
+
const resumeOn = yield* resumeBranch(git, tip);
|
|
36315
36783
|
const steps = [
|
|
36316
36784
|
{
|
|
36317
36785
|
kind: "gitWrite",
|
|
36318
|
-
command: hardResetTo(tip)
|
|
36786
|
+
command: resumeOn === void 0 ? hardResetTo(tip) : switchBranchTo(resumeOn, tip)
|
|
36319
36787
|
},
|
|
36320
36788
|
{
|
|
36321
36789
|
kind: "gitWrite",
|
|
36322
36790
|
command: deleteRef(HISTORY_REF)
|
|
36323
36791
|
},
|
|
36792
|
+
{
|
|
36793
|
+
kind: "gitWrite",
|
|
36794
|
+
command: deleteRef(RESUME_BRANCH_REF)
|
|
36795
|
+
},
|
|
36324
36796
|
{
|
|
36325
36797
|
kind: "outcome",
|
|
36326
36798
|
command: restoredOutcome(tip, after.state)
|
|
@@ -37036,7 +37508,7 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
|
|
|
37036
37508
|
},
|
|
37037
37509
|
group: "Start and stop",
|
|
37038
37510
|
summary: "Start a workflow or door; bare, list them",
|
|
37039
|
-
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
|
|
37511
|
+
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds. The process runs on the branch \`gtd/<b>\` (created, or reused when nothing on it would be lost), never on \`<b>\`; a branch already named \`gtd/…\` and a detached HEAD start in place.
|
|
37040
37512
|
|
|
37041
37513
|
<name> is a door — a named shortcut whose positional args set the workflow's settings — or else a workflow the config exports, which takes no args. A door shadows a workflow of the same name. --var pins a setting either way.
|
|
37042
37514
|
|
|
@@ -37050,7 +37522,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
37050
37522
|
arity: "none",
|
|
37051
37523
|
group: "Start and stop",
|
|
37052
37524
|
summary: "Stop the process, keeping its changes",
|
|
37053
|
-
details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from and keeps everything it produced as uncommitted changes
|
|
37525
|
+
details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from, or to its last ship, and keeps everything it produced as uncommitted changes, switches back to \`<b>\` and deletes \`gtd/<b>\`; it keeps the abandoned tip at \`refs/gtd/history/<entry>\` (pushed to \`historyRemote\` when set). \`gtd restore\` undoes it. With no process underway the script does nothing.
|
|
37054
37526
|
|
|
37055
37527
|
s=$(gtd abandon --json=script) && sh -c "$s"`
|
|
37056
37528
|
},
|
|
@@ -37060,7 +37532,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
37060
37532
|
arity: "none",
|
|
37061
37533
|
group: "Start and stop",
|
|
37062
37534
|
summary: "Bring back the last abandoned process",
|
|
37063
|
-
details: `Bring back the process the last \`gtd abandon\` stopped: the printed script
|
|
37535
|
+
details: `Bring back the process the last \`gtd abandon\` stopped: the printed script puts that process's last commit back, on a recreated \`gtd/<b>\` when the process ran on one. Refuses on a dirty working tree, when nothing was abandoned, or when commits made since would be lost.`
|
|
37064
37536
|
},
|
|
37065
37537
|
{
|
|
37066
37538
|
token: "repin",
|
|
@@ -37078,7 +37550,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
37078
37550
|
arity: "none",
|
|
37079
37551
|
group: "Inspect",
|
|
37080
37552
|
summary: "Print the prompt for the closing message",
|
|
37081
|
-
details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside: its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
|
|
37553
|
+
details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside (back on a user branch, the one its gtd/ branch closes): its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
|
|
37082
37554
|
|
|
37083
37555
|
Refuses when the workflow declares no summary template, or when the process has no commits.`
|
|
37084
37556
|
},
|