@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as moveScript, D as revertScript, E as restoreScript, H as installContext, K as markedOptions, O as showScript, S as checkScript, T as removeScript, Y as protocolMismatch, i as configure, k as writeScript$1, n as configProblems, o as isFactoryWorkflow, p as numeric, s as answered, t as flows_exports, x as applyScript } from "./flows-
|
|
2
|
-
import {
|
|
1
|
+
import { C as moveScript, D as revertScript, E as restoreScript, H as installContext, K as markedOptions, O as showScript, S as checkScript, T as removeScript, Y as protocolMismatch, i as configure, k as writeScript$1, n as configProblems, o as isFactoryWorkflow, p as numeric, s as answered, t as flows_exports, x as applyScript } from "./flows-DGs0W3HI.mjs";
|
|
2
|
+
import { A as RUN_FAILED_PATH, N as renderFormat, O as ACCESS_REFUSAL, a as renderScriptJson, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, k as PIN_PATH, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, y as demandOf } from "./wire-BNS8ciNu.mjs";
|
|
3
3
|
import { F as headingSectionBodies, I as formatFinding, a as sections, c as openThreads, d as formatFromSpec, i as sectionBodies, l as steeringFormatFor, o as threads, r as openQuestions, s as BUILT_IN_MODE_NAMES, t as parseCodeThreads, u as unansweredQuestions } from "./steering-DEh_MS40.mjs";
|
|
4
4
|
import { createRequire, isBuiltin } from "node:module";
|
|
5
5
|
import * as Crypto from "node:crypto";
|
|
@@ -3306,6 +3306,44 @@ const fromNullishOr = (a) => a == null ? none() : some(a);
|
|
|
3306
3306
|
*/
|
|
3307
3307
|
const getOrUndefined$1 = /*#__PURE__*/ getOrElse(constUndefined);
|
|
3308
3308
|
/**
|
|
3309
|
+
* Extracts the value from a `Some`, or throws a default `Error` for `None`.
|
|
3310
|
+
*
|
|
3311
|
+
* **When to use**
|
|
3312
|
+
*
|
|
3313
|
+
* Use when you need quick fail-fast unwrapping of an `Option` and a generic
|
|
3314
|
+
* error is acceptable.
|
|
3315
|
+
*
|
|
3316
|
+
* **Details**
|
|
3317
|
+
*
|
|
3318
|
+
* - `Some` → returns the inner value
|
|
3319
|
+
* - `None` → throws `new Error("getOrThrow called on a None")`
|
|
3320
|
+
*
|
|
3321
|
+
* **Example** (Throwing a default error)
|
|
3322
|
+
*
|
|
3323
|
+
* ```ts import.meta.vitest
|
|
3324
|
+
* import { Option, Result } from "effect"
|
|
3325
|
+
*
|
|
3326
|
+
* Option.getOrThrow(Option.some(1)) // => 1
|
|
3327
|
+
*
|
|
3328
|
+
* const failure = Result.try({
|
|
3329
|
+
* try: () => Option.getOrThrow(Option.none()),
|
|
3330
|
+
* catch: (error) => (error as Error).message
|
|
3331
|
+
* })
|
|
3332
|
+
* Result.getFailure(failure).pipe(Option.getOrElse(() => "no error")) // => "getOrThrow called on a None"
|
|
3333
|
+
* ```
|
|
3334
|
+
*
|
|
3335
|
+
* @see {@link getOrThrowWith} for a custom error
|
|
3336
|
+
* @see {@link getOrElse} for a non-throwing alternative
|
|
3337
|
+
*
|
|
3338
|
+
* @stability stable
|
|
3339
|
+
* @category converting
|
|
3340
|
+
* @since 2.0.0
|
|
3341
|
+
*/
|
|
3342
|
+
const getOrThrow$1 = /*#__PURE__*/ (/* @__PURE__ */ dual(2, (self, onNone) => {
|
|
3343
|
+
if (isSome(self)) return self.value;
|
|
3344
|
+
throw onNone();
|
|
3345
|
+
}))(() => /* @__PURE__ */ new Error("getOrThrow called on a None"));
|
|
3346
|
+
/**
|
|
3309
3347
|
* Transforms the value inside a `Some` using the provided function, leaving
|
|
3310
3348
|
* `None` unchanged.
|
|
3311
3349
|
*
|
|
@@ -22479,6 +22517,8 @@ const loopLogPath = gen(function* () {
|
|
|
22479
22517
|
// src/platform/RetainedHistory.ts
|
|
22480
22518
|
/** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
|
|
22481
22519
|
const HISTORY_REF = "refs/worktree/gtd/history";
|
|
22520
|
+
/** Set by an abandon that left `gtd/<b>` for `<b>`: only then does restore recreate `gtd/<b>`. */
|
|
22521
|
+
const RESUME_BRANCH_REF = "refs/worktree/gtd/history-branch";
|
|
22482
22522
|
/** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
|
|
22483
22523
|
const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
|
|
22484
22524
|
const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
|
|
@@ -23514,6 +23554,12 @@ const TRAILER_READERS = {
|
|
|
23514
23554
|
"Gtd-Anchor": (value, into) => {
|
|
23515
23555
|
if (HASH_RE.test(value)) into.anchor ??= value;
|
|
23516
23556
|
},
|
|
23557
|
+
"Gtd-Shipped": (value, into) => {
|
|
23558
|
+
if (HASH_RE.test(value)) into.shipped ??= value;
|
|
23559
|
+
},
|
|
23560
|
+
"Gtd-Tip": (value, into) => {
|
|
23561
|
+
if (HASH_RE.test(value)) into.tip ??= value;
|
|
23562
|
+
},
|
|
23517
23563
|
"Gtd-Format": (value, into) => {
|
|
23518
23564
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
23519
23565
|
},
|
|
@@ -23538,6 +23584,8 @@ const parseCommitMessage = (message) => {
|
|
|
23538
23584
|
approved: void 0,
|
|
23539
23585
|
pin: void 0,
|
|
23540
23586
|
anchor: void 0,
|
|
23587
|
+
shipped: void 0,
|
|
23588
|
+
tip: void 0,
|
|
23541
23589
|
judge: [],
|
|
23542
23590
|
marks: [],
|
|
23543
23591
|
vars: {},
|
|
@@ -23579,7 +23627,7 @@ const namesUnpin = (commit) => {
|
|
|
23579
23627
|
const unpin = pinStateIn(commit.parsed.format, "remove");
|
|
23580
23628
|
return unpin !== void 0 && commit.parsed.parsed?.to === unpin;
|
|
23581
23629
|
};
|
|
23582
|
-
const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
23630
|
+
const actorOfKind = (kind) => kind === "run" || kind === "ship" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
23583
23631
|
const memoryScopeOf = (name) => {
|
|
23584
23632
|
const dot = name.lastIndexOf(".");
|
|
23585
23633
|
return dot === -1 ? "" : name.slice(0, dot);
|
|
@@ -23713,9 +23761,12 @@ const KNOWN_OPTIONS = {
|
|
|
23713
23761
|
"acceptClean",
|
|
23714
23762
|
"approve"
|
|
23715
23763
|
]),
|
|
23716
|
-
run: new Set(STEERING_OPTIONS),
|
|
23764
|
+
run: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "onFail"]),
|
|
23765
|
+
ship: /* @__PURE__ */ new Set(["label", "base"]),
|
|
23717
23766
|
judge: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "message"])
|
|
23718
23767
|
};
|
|
23768
|
+
const restsOnFail = (step) => step.request.kind === "run" && step.request.options.onFail === "rest";
|
|
23769
|
+
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;
|
|
23719
23770
|
const unknownOptions = (step) => {
|
|
23720
23771
|
const unknown = Object.keys(step.request.options).filter((key) => !KNOWN_OPTIONS[step.kind].has(key));
|
|
23721
23772
|
if (unknown.length === 0) return void 0;
|
|
@@ -23826,15 +23877,15 @@ const replay = async (raw) => {
|
|
|
23826
23877
|
return calls;
|
|
23827
23878
|
};
|
|
23828
23879
|
const scoped = (name) => [...scopes.flatMap((s) => s.name === void 0 ? [] : [s.name]), name].join(".");
|
|
23829
|
-
const isAttemptAt = (commit,
|
|
23880
|
+
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$1(position.tree, commit.tree)) || restsOnFail(step) && commit.tree.read(".gtd/run-failed.txt") !== void 0);
|
|
23830
23881
|
const advance = (next) => {
|
|
23831
23882
|
previousPosition = position;
|
|
23832
23883
|
position = next;
|
|
23833
23884
|
};
|
|
23834
|
-
const replyFor = (request, verdicts) => request.kind === "judge" ? {
|
|
23885
|
+
const replyFor = (request, verdicts, shipped) => request.kind === "judge" ? {
|
|
23835
23886
|
answers: answersFrom(verdicts, request.questions),
|
|
23836
23887
|
truncated: judgeCuts.get(request) ?? []
|
|
23837
|
-
} : void 0;
|
|
23888
|
+
} : request.kind === "ship" ? shipped : void 0;
|
|
23838
23889
|
const judgeCuts = /* @__PURE__ */ new WeakMap();
|
|
23839
23890
|
const resolveScoped = (memory, pick, configured, bundled) => {
|
|
23840
23891
|
const own = /* @__PURE__ */ new Map();
|
|
@@ -23948,7 +23999,7 @@ const replay = async (raw) => {
|
|
|
23948
23999
|
expectedNext = void 0;
|
|
23949
24000
|
anchorPending = false;
|
|
23950
24001
|
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);
|
|
23951
|
-
const error = unknownOptions(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
|
|
24002
|
+
const error = unknownOptions(step) ?? onFailFault(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
|
|
23952
24003
|
return error === void 0 ? void 0 : {
|
|
23953
24004
|
kind: "failed",
|
|
23954
24005
|
message: error
|
|
@@ -23974,7 +24025,7 @@ const replay = async (raw) => {
|
|
|
23974
24025
|
expectNextOf(commit);
|
|
23975
24026
|
skipPins();
|
|
23976
24027
|
noteKept(step, commit);
|
|
23977
|
-
return replyFor(step.request, commit.parsed.judge);
|
|
24028
|
+
return replyFor(step.request, commit.parsed.judge, commit.parsed.shipped);
|
|
23978
24029
|
};
|
|
23979
24030
|
const expectNextOf = (commit) => {
|
|
23980
24031
|
const to = commit.parsed.parsed?.to;
|
|
@@ -24005,10 +24056,14 @@ const replay = async (raw) => {
|
|
|
24005
24056
|
hash: "",
|
|
24006
24057
|
tree: keeping ? position.tree : pending.tree
|
|
24007
24058
|
});
|
|
24008
|
-
return replyFor(step.request, pending.verdicts ?? []);
|
|
24059
|
+
return replyFor(step.request, pending.verdicts ?? [], pending.shipped);
|
|
24009
24060
|
};
|
|
24010
24061
|
const answer = (step) => {
|
|
24011
|
-
while (cursor < commits.length && isAttemptAt(commits[cursor], step
|
|
24062
|
+
while (cursor < commits.length && isAttemptAt(commits[cursor], step)) {
|
|
24063
|
+
if (!isEmptyDiff(diffTrees$1(position.tree, commits[cursor].tree))) advance({
|
|
24064
|
+
hash: commits[cursor].hash,
|
|
24065
|
+
tree: commits[cursor].tree
|
|
24066
|
+
});
|
|
24012
24067
|
cursor++;
|
|
24013
24068
|
skipPins();
|
|
24014
24069
|
}
|
|
@@ -24021,12 +24076,22 @@ const replay = async (raw) => {
|
|
|
24021
24076
|
landed,
|
|
24022
24077
|
marks
|
|
24023
24078
|
});
|
|
24079
|
+
if (restsOnFail(step) && input.pending.tree.read(".gtd/run-failed.txt") !== void 0) return finish({
|
|
24080
|
+
kind: "rest",
|
|
24081
|
+
rest: step,
|
|
24082
|
+
trace,
|
|
24083
|
+
landed,
|
|
24084
|
+
marks
|
|
24085
|
+
});
|
|
24024
24086
|
return landPending(step, input.pending);
|
|
24025
24087
|
};
|
|
24088
|
+
const bodyFault = (request) => {
|
|
24089
|
+
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`;
|
|
24090
|
+
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)`;
|
|
24091
|
+
};
|
|
24026
24092
|
const requestFault = (request) => {
|
|
24027
24093
|
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`;
|
|
24028
|
-
if (request.kind
|
|
24029
|
-
if (request.kind !== "agent") return void 0;
|
|
24094
|
+
if (request.kind !== "agent") return bodyFault(request);
|
|
24030
24095
|
const name = scoped(request.name);
|
|
24031
24096
|
if (request.options.keep === true) {
|
|
24032
24097
|
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`;
|
|
@@ -24199,6 +24264,7 @@ const compileConfig = (value, origin) => {
|
|
|
24199
24264
|
return {
|
|
24200
24265
|
modes,
|
|
24201
24266
|
...config.ui !== void 0 ? { ui: config.ui } : {},
|
|
24267
|
+
...config.historyRemote !== void 0 ? { historyRemote: config.historyRemote } : {},
|
|
24202
24268
|
skills: Object.assign(Object.create(null), config.skills),
|
|
24203
24269
|
access: Object.assign(Object.create(null), config.access),
|
|
24204
24270
|
diagnostics: []
|
|
@@ -24512,6 +24578,7 @@ const load = gen(function* () {
|
|
|
24512
24578
|
return entry === void 0 ? void 0 : resolve(name, entry);
|
|
24513
24579
|
},
|
|
24514
24580
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
24581
|
+
...compiled.historyRemote !== void 0 ? { historyRemote: compiled.historyRemote } : {},
|
|
24515
24582
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
24516
24583
|
fingerprint: pinned?.fingerprint ?? configFingerprint(nodeSyncIo, paths, loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
|
|
24517
24584
|
drift: pinned === void 0 ? [] : changedConfigFiles(pinned.fingerprint, configFingerprint(nodeSyncIo, paths, Object.keys(pinned.fingerprint).map((file) => paths.resolve(host.root, file)), host.root, readFromDisk))
|
|
@@ -25212,8 +25279,9 @@ const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
|
25212
25279
|
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
25213
25280
|
* error, a commit-msg hook) still fails.
|
|
25214
25281
|
*/
|
|
25215
|
-
const commitAllowEmpty = (message, only) =>
|
|
25216
|
-
|
|
25282
|
+
const commitAllowEmpty = (message, only) => commitWordAllowEmpty(shellQuote(message), only);
|
|
25283
|
+
/** `commitAllowEmpty` over a message that is already a shell word. */
|
|
25284
|
+
const commitWordAllowEmpty = (m, only) => {
|
|
25217
25285
|
const paths = only === void 0 ? "" : ` -- ${shellQuote(only)}`;
|
|
25218
25286
|
return [
|
|
25219
25287
|
`if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
|
|
@@ -25229,6 +25297,28 @@ const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — r
|
|
|
25229
25297
|
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
25230
25298
|
/** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
|
|
25231
25299
|
const commitPath = (path, message) => `git add -A -- ${shellQuote(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
|
|
25300
|
+
/**
|
|
25301
|
+
* A `run()` body that records its own failure instead of failing the beat: the
|
|
25302
|
+
* status and the last 50 output lines go to `path`, and the script exits zero
|
|
25303
|
+
* so the turn lands. The file is COMMITTED (and force-pushed with a history
|
|
25304
|
+
* remote), hence the cap.
|
|
25305
|
+
*/
|
|
25306
|
+
const failRestScript = (body, path) => {
|
|
25307
|
+
const p = shellQuote(path);
|
|
25308
|
+
const slash = path.lastIndexOf("/");
|
|
25309
|
+
return [
|
|
25310
|
+
`rm -f -- ${p}`,
|
|
25311
|
+
`gtd_run_status=0`,
|
|
25312
|
+
`gtd_run_out="$( {`,
|
|
25313
|
+
body,
|
|
25314
|
+
`} 2>&1 )" || gtd_run_status=$?`,
|
|
25315
|
+
`printf '%s\\n' "$gtd_run_out"`,
|
|
25316
|
+
`if [ "$gtd_run_status" -ne 0 ]; then`,
|
|
25317
|
+
` mkdir -p ${shellQuote(path.slice(0, slash))}`,
|
|
25318
|
+
` printf 'exit %s\\n%s\\n' "$gtd_run_status" "$(printf '%s\\n' "$gtd_run_out" | tail -n 50)" >${p}`,
|
|
25319
|
+
`fi`
|
|
25320
|
+
].join("\n");
|
|
25321
|
+
};
|
|
25232
25322
|
/** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
|
|
25233
25323
|
const pinScript = (path) => {
|
|
25234
25324
|
const p = shellQuote(path);
|
|
@@ -25304,6 +25394,62 @@ const keepTurn = (refPrefix, message) => {
|
|
|
25304
25394
|
`git reset --hard HEAD`
|
|
25305
25395
|
].join("\n");
|
|
25306
25396
|
};
|
|
25397
|
+
/** `git switch -C <branch> [<start>]`: create or reset the branch, carrying pending changes over. */
|
|
25398
|
+
const switchBranchTo = (branch, start) => `git switch -q -C ${shellQuote(branch)}${start === void 0 ? "" : ` ${shellQuote(start)}`}`;
|
|
25399
|
+
/** `git switch <branch>`: an existing branch, carrying pending changes over. */
|
|
25400
|
+
const switchTo = (branch) => `git switch -q ${shellQuote(branch)}`;
|
|
25401
|
+
/** `switchTo` that, when pending changes block it, stays put and says so: the landing already happened. */
|
|
25402
|
+
const switchBack = (branch) => [
|
|
25403
|
+
`if ! git switch -q ${shellQuote(branch)} 2>/dev/null; then`,
|
|
25404
|
+
` printf '%s\\n' ${shellQuote(`gtd: pending changes keep HEAD off ${branch}; switch to it once they are committed or stashed`)}`,
|
|
25405
|
+
`fi`
|
|
25406
|
+
].join("\n");
|
|
25407
|
+
const deleteBranch = (branch) => `git branch -q -D ${shellQuote(branch)}`;
|
|
25408
|
+
/**
|
|
25409
|
+
* Force-pushes one ref to a remote. A failed push exits non-zero with the retry
|
|
25410
|
+
* on stderr: it runs after the closing commit has landed, so the commit stands.
|
|
25411
|
+
*/
|
|
25412
|
+
const pushRef = (remote, ref) => {
|
|
25413
|
+
const retry = `git push --force ${shellQuote(remote)} ${shellQuote(`${ref}:${ref}`)}`;
|
|
25414
|
+
return [
|
|
25415
|
+
`if ! ${retry}; then`,
|
|
25416
|
+
` printf '%s\\n' ${shellQuote(`gtd: pushing ${ref} failed — retry: ${retry}`)} >&2`,
|
|
25417
|
+
` exit 1`,
|
|
25418
|
+
`fi`
|
|
25419
|
+
].join("\n");
|
|
25420
|
+
};
|
|
25421
|
+
/**
|
|
25422
|
+
* A ship step's landing, in one block because the landing commit's
|
|
25423
|
+
* `Gtd-Shipped` trailer names the squash it makes first. The squash carries
|
|
25424
|
+
* the staged tree with `.gtd/` as `<branch>` has it, built in a scratch index; `<branch>` moves by
|
|
25425
|
+
* compare-and-swap against `parent`, so a branch moved since the decision
|
|
25426
|
+
* fails the script before the landing commits. `commit-tree` ignores
|
|
25427
|
+
* `commit.gpgSign`, so it is read here.
|
|
25428
|
+
*/
|
|
25429
|
+
const shipCommit = (branch, parent, shipMessage, historyRef, message) => [
|
|
25430
|
+
`gtd_ship_parent=${shellQuote(parent)}`,
|
|
25431
|
+
`gtd_ship_branch=${shellQuote(branch)}`,
|
|
25432
|
+
`gtd_ship_history=${shellQuote(historyRef)}`,
|
|
25433
|
+
`gtd_ship_message=${shellQuote(shipMessage)}`,
|
|
25434
|
+
`gtd_land_message=${shellQuote(message)}`,
|
|
25435
|
+
`git add -A || exit 1`,
|
|
25436
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
25437
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
25438
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
25439
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
25440
|
+
`gtd_index=$(git rev-parse --git-path gtd-ship-index) || exit 1`,
|
|
25441
|
+
`rm -f -- "$gtd_index"`,
|
|
25442
|
+
`GIT_INDEX_FILE="$gtd_index" git read-tree "$gtd_tree" || exit 1`,
|
|
25443
|
+
`GIT_INDEX_FILE="$gtd_index" git rm -r -q --cached --ignore-unmatch -- .gtd || exit 1`,
|
|
25444
|
+
`git ls-tree -r "$gtd_ship_parent" -- .gtd | GIT_INDEX_FILE="$gtd_index" git update-index --index-info || exit 1`,
|
|
25445
|
+
`gtd_ship_tree=$(GIT_INDEX_FILE="$gtd_index" git write-tree) || exit 1`,
|
|
25446
|
+
`rm -f -- "$gtd_index"`,
|
|
25447
|
+
`gtd_ship_trailers=$(printf "Gtd-History: %s\\nGtd-Tip: %s" "$gtd_ship_history" "$gtd_head")`,
|
|
25448
|
+
`gtd_shipped=$(git commit-tree "$gtd_sign" "$gtd_ship_tree" -p "$gtd_ship_parent" -m "$gtd_ship_message" -m "$gtd_ship_trailers") || exit 1`,
|
|
25449
|
+
`git update-ref -m "gtd: ship" "refs/heads/$gtd_ship_branch" "$gtd_shipped" "$gtd_ship_parent" || exit 1`,
|
|
25450
|
+
`gtd_message=$(printf "%s\\nGtd-Shipped: %s" "$gtd_land_message" "$gtd_shipped")`,
|
|
25451
|
+
commitWordAllowEmpty(`"$gtd_message"`)
|
|
25452
|
+
].join("\n");
|
|
25307
25453
|
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
25308
25454
|
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
25309
25455
|
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
@@ -25332,15 +25478,18 @@ const renderOutcome = (outcome) => {
|
|
|
25332
25478
|
case "note": return printfLine$1("%s\n", [shellQuote(outcome.text)]);
|
|
25333
25479
|
}
|
|
25334
25480
|
};
|
|
25335
|
-
const
|
|
25336
|
-
|
|
25337
|
-
|
|
25338
|
-
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
|
|
25481
|
+
const GIT_WRITES = {
|
|
25482
|
+
commitAll: (w) => commitAll(w.message),
|
|
25483
|
+
commitPath: (w) => commitPath(w.path, w.message),
|
|
25484
|
+
commitClean: (w) => commitClean(w.message),
|
|
25485
|
+
updateRef: (w) => updateRef(w.ref, w.target),
|
|
25486
|
+
keepTurn: (w) => keepTurn(w.ref, w.message),
|
|
25487
|
+
switchBranch: (w) => switchBranchTo(w.branch),
|
|
25488
|
+
switchTo: (w) => switchBack(w.branch),
|
|
25489
|
+
pushRef: (w) => pushRef(w.remote, w.ref),
|
|
25490
|
+
ship: (w) => shipCommit(w.branch, w.parent, w.shipMessage, w.historyRef, w.message)
|
|
25343
25491
|
};
|
|
25492
|
+
const renderGitWrite = (write) => GIT_WRITES[write.kind](write);
|
|
25344
25493
|
/**
|
|
25345
25494
|
* Wraps `command` so a non-zero exit prints `prompt` plus the command's
|
|
25346
25495
|
* captured output before propagating that exit code — mirrors `Emit.ts`'s
|
|
@@ -25731,6 +25880,11 @@ const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot
|
|
|
25731
25880
|
file: snapshot.file
|
|
25732
25881
|
}] : [];
|
|
25733
25882
|
const gitWriteOf = (landing, message) => {
|
|
25883
|
+
if (landing.ship !== void 0) return {
|
|
25884
|
+
kind: "ship",
|
|
25885
|
+
message,
|
|
25886
|
+
...landing.ship
|
|
25887
|
+
};
|
|
25734
25888
|
if (landing.only !== void 0) return {
|
|
25735
25889
|
kind: "commitPath",
|
|
25736
25890
|
path: landing.only,
|
|
@@ -25750,6 +25904,10 @@ const gitWriteOf = (landing, message) => {
|
|
|
25750
25904
|
message
|
|
25751
25905
|
};
|
|
25752
25906
|
};
|
|
25907
|
+
const writeIf = (value, write) => value === void 0 ? [] : [{
|
|
25908
|
+
kind: "gitWrite",
|
|
25909
|
+
write: write(value)
|
|
25910
|
+
}];
|
|
25753
25911
|
const planStep = (snapshot, opts = {}) => {
|
|
25754
25912
|
const landing = snapshot.landing;
|
|
25755
25913
|
if (landing.kind === "refusal") return {
|
|
@@ -25797,6 +25955,7 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
25797
25955
|
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
25798
25956
|
});
|
|
25799
25957
|
const subject = message.split("\n")[0];
|
|
25958
|
+
const historyRef = landing.keepHistory ?? landing.ship?.historyRef;
|
|
25800
25959
|
const outcome = landing.to === snapshot.state ? {
|
|
25801
25960
|
kind: "outcome",
|
|
25802
25961
|
outcome: {
|
|
@@ -25817,20 +25976,37 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
25817
25976
|
to: landing.to,
|
|
25818
25977
|
subject,
|
|
25819
25978
|
steps: [
|
|
25979
|
+
...writeIf(landing.startBranch, (branch) => ({
|
|
25980
|
+
kind: "switchBranch",
|
|
25981
|
+
branch
|
|
25982
|
+
})),
|
|
25820
25983
|
...uncheckStep(snapshot),
|
|
25821
25984
|
{
|
|
25822
25985
|
kind: "gitWrite",
|
|
25823
25986
|
write: gitWriteOf(landing, message)
|
|
25824
25987
|
},
|
|
25825
|
-
...
|
|
25826
|
-
kind: "
|
|
25827
|
-
|
|
25828
|
-
|
|
25829
|
-
|
|
25830
|
-
|
|
25988
|
+
...writeIf(historyRef, (ref) => ({
|
|
25989
|
+
kind: "updateRef",
|
|
25990
|
+
ref,
|
|
25991
|
+
target: "HEAD"
|
|
25992
|
+
})),
|
|
25993
|
+
...writeIf(landing.leaveBranch, (branch) => ({
|
|
25994
|
+
kind: "switchTo",
|
|
25995
|
+
branch
|
|
25996
|
+
})),
|
|
25997
|
+
outcome,
|
|
25998
|
+
...landing.note !== void 0 ? [{
|
|
25999
|
+
kind: "outcome",
|
|
26000
|
+
outcome: {
|
|
26001
|
+
kind: "note",
|
|
26002
|
+
text: landing.note
|
|
25831
26003
|
}
|
|
25832
26004
|
}] : [],
|
|
25833
|
-
|
|
26005
|
+
...writeIf(historyRef !== void 0 ? landing.pushHistory : void 0, (remote) => ({
|
|
26006
|
+
kind: "pushRef",
|
|
26007
|
+
remote,
|
|
26008
|
+
ref: historyRef
|
|
26009
|
+
}))
|
|
25834
26010
|
]
|
|
25835
26011
|
};
|
|
25836
26012
|
};
|
|
@@ -25850,6 +26026,42 @@ const accessRefusal = (changes, write) => {
|
|
|
25850
26026
|
`allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
|
|
25851
26027
|
].join("\n");
|
|
25852
26028
|
};
|
|
26029
|
+
// src/step/startBranch.ts
|
|
26030
|
+
const GTD_BRANCH_PREFIX = "gtd/";
|
|
26031
|
+
/** `<b>` for a `gtd/<b>` branch name; `undefined` for any other. */
|
|
26032
|
+
const userBranchOf = (branch) => branch.startsWith("gtd/") ? branch.slice(4) : void 0;
|
|
26033
|
+
/**
|
|
26034
|
+
* Where a new process starts: on `gtd/<b>`, so `<b>` never carries its commits.
|
|
26035
|
+
* An existing `gtd/<b>` is reused only when its tip is contained in `<b>` or in
|
|
26036
|
+
* a kept history ref (a squash onto `<b>` never makes it an ancestor).
|
|
26037
|
+
*/
|
|
26038
|
+
const startBranchFor = (commandLabel) => gen(function* () {
|
|
26039
|
+
const git = yield* GitService;
|
|
26040
|
+
const current = yield* git.currentBranch();
|
|
26041
|
+
if (isNone(current) || current.value.startsWith("gtd/")) return { kind: "inPlace" };
|
|
26042
|
+
if (!(yield* git.hasCommits())) return { kind: "inPlace" };
|
|
26043
|
+
const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
|
|
26044
|
+
const existing = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
26045
|
+
if (isSome(existing)) {
|
|
26046
|
+
const head = yield* git.resolveRef("HEAD");
|
|
26047
|
+
let contained = yield* git.isAncestor(existing.value, head);
|
|
26048
|
+
if (!contained) {
|
|
26049
|
+
const kept = yield* git.listRefs(PROCESS_HISTORY_PREFIX);
|
|
26050
|
+
for (const k of kept) if (yield* git.isAncestor(existing.value, k.hash)) {
|
|
26051
|
+
contained = true;
|
|
26052
|
+
break;
|
|
26053
|
+
}
|
|
26054
|
+
}
|
|
26055
|
+
if (!contained) return {
|
|
26056
|
+
kind: "refusal",
|
|
26057
|
+
message: `${commandLabel}: branch ${branch} holds commits that are neither on ${current.value} nor in a kept history ref — merge or delete it first`
|
|
26058
|
+
};
|
|
26059
|
+
}
|
|
26060
|
+
return {
|
|
26061
|
+
kind: "switch",
|
|
26062
|
+
branch
|
|
26063
|
+
};
|
|
26064
|
+
});
|
|
25853
26065
|
// src/step/planStart.ts
|
|
25854
26066
|
const refusal = (message) => ({
|
|
25855
26067
|
kind: "refusal",
|
|
@@ -25890,29 +26102,42 @@ const planStart = (current, actor, start) => gen(function* () {
|
|
|
25890
26102
|
const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
|
|
25891
26103
|
if (typeof base === "object") return base;
|
|
25892
26104
|
const subject = formatSubject(actor, firstStep);
|
|
26105
|
+
const message = formatCommitMessage({
|
|
26106
|
+
actor,
|
|
26107
|
+
to: firstStep,
|
|
26108
|
+
workflow: name,
|
|
26109
|
+
...base !== void 0 ? { reviewBase: base } : {},
|
|
26110
|
+
vars
|
|
26111
|
+
});
|
|
26112
|
+
const onBranch = yield* startBranchFor(commandLabel);
|
|
26113
|
+
if (onBranch.kind === "refusal") return refusal(onBranch.message);
|
|
25893
26114
|
return {
|
|
25894
26115
|
kind: "start",
|
|
25895
26116
|
workflow: name,
|
|
25896
26117
|
subject,
|
|
25897
|
-
steps: [
|
|
25898
|
-
kind
|
|
25899
|
-
|
|
25900
|
-
|
|
25901
|
-
|
|
25902
|
-
|
|
25903
|
-
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
25907
|
-
|
|
25908
|
-
|
|
25909
|
-
|
|
25910
|
-
|
|
25911
|
-
|
|
25912
|
-
|
|
25913
|
-
|
|
26118
|
+
steps: [
|
|
26119
|
+
...onBranch.kind === "switch" ? [{
|
|
26120
|
+
kind: "gitWrite",
|
|
26121
|
+
write: {
|
|
26122
|
+
kind: "switchBranch",
|
|
26123
|
+
branch: onBranch.branch
|
|
26124
|
+
}
|
|
26125
|
+
}] : [],
|
|
26126
|
+
{
|
|
26127
|
+
kind: "gitWrite",
|
|
26128
|
+
write: {
|
|
26129
|
+
kind: "commitAll",
|
|
26130
|
+
message
|
|
26131
|
+
}
|
|
26132
|
+
},
|
|
26133
|
+
{
|
|
26134
|
+
kind: "outcome",
|
|
26135
|
+
outcome: {
|
|
26136
|
+
kind: "commit",
|
|
26137
|
+
subject
|
|
26138
|
+
}
|
|
25914
26139
|
}
|
|
25915
|
-
|
|
26140
|
+
]
|
|
25916
26141
|
};
|
|
25917
26142
|
});
|
|
25918
26143
|
/**
|
|
@@ -26156,14 +26381,32 @@ const runAtTip = (git, def, tip) => gen(function* () {
|
|
|
26156
26381
|
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
26157
26382
|
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
26158
26383
|
});
|
|
26159
|
-
|
|
26160
|
-
|
|
26161
|
-
|
|
26162
|
-
|
|
26384
|
+
/**
|
|
26385
|
+
* The process HEAD closes or sits inside — `gtd summary`'s run. On `<b>`, where
|
|
26386
|
+
* a finished process leaves HEAD, it is the one `gtd/<b>`'s tip closes, while
|
|
26387
|
+
* `<b>` holds nothing since: its start, or its last ship.
|
|
26388
|
+
*/
|
|
26163
26389
|
const summaryRun = gen(function* () {
|
|
26164
26390
|
const git = yield* GitService;
|
|
26165
26391
|
const def = (yield* (yield* ConfigService).load).workflow;
|
|
26166
|
-
|
|
26392
|
+
const head = (yield* git.subjectHistory(1, 0))[0];
|
|
26393
|
+
const left = head === void 0 ? void 0 : yield* leftProcessTip(git, head);
|
|
26394
|
+
return yield* runAtTip(git, def, left ?? head);
|
|
26395
|
+
});
|
|
26396
|
+
const leftProcessTip = (git, head) => gen(function* () {
|
|
26397
|
+
if (parseCommitMessage(head.subject).parsed !== void 0) return void 0;
|
|
26398
|
+
const current = yield* git.currentBranch();
|
|
26399
|
+
if (isNone(current) || userBranchOf(current.value) !== void 0) return void 0;
|
|
26400
|
+
const tip = yield* git.readRefOption(`refs/heads/${GTD_BRANCH_PREFIX}${current.value}`);
|
|
26401
|
+
if (isNone(tip)) return void 0;
|
|
26402
|
+
const message = yield* git.lastCommitMessage(tip.value);
|
|
26403
|
+
if (parseCommitMessage(message).parsed?.to === void 0) return void 0;
|
|
26404
|
+
const since = parseCommitMessage(yield* git.lastCommitMessage(head.hash)).tip ?? head.hash;
|
|
26405
|
+
if (!(yield* git.isAncestor(since, tip.value))) return void 0;
|
|
26406
|
+
return {
|
|
26407
|
+
hash: tip.value,
|
|
26408
|
+
subject: message.split("\n")[0]
|
|
26409
|
+
};
|
|
26167
26410
|
});
|
|
26168
26411
|
const commitTree = (workspace, hash) => {
|
|
26169
26412
|
let entries;
|
|
@@ -26297,7 +26540,14 @@ const promptDef = (common, request, skills, access) => ({
|
|
|
26297
26540
|
const scriptDef = (common, request) => ({
|
|
26298
26541
|
...common,
|
|
26299
26542
|
kind: "script",
|
|
26300
|
-
content: request.body
|
|
26543
|
+
content: request.options.onFail === "rest" ? failRestScript(request.body, RUN_FAILED_PATH) : request.body,
|
|
26544
|
+
...optional("onFail", request.options.onFail)
|
|
26545
|
+
});
|
|
26546
|
+
const SHIP_CONTENT = noteOutcome("gtd: landing this step ships the process onto your branch");
|
|
26547
|
+
const shipDef = (common) => ({
|
|
26548
|
+
...common,
|
|
26549
|
+
kind: "script",
|
|
26550
|
+
content: SHIP_CONTENT
|
|
26301
26551
|
});
|
|
26302
26552
|
const judgeDef = (common, request, step) => {
|
|
26303
26553
|
const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
|
|
@@ -26321,6 +26571,7 @@ const stepDefOf = (step) => {
|
|
|
26321
26571
|
switch (request.kind) {
|
|
26322
26572
|
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
26323
26573
|
case "run": return scriptDef(common, request);
|
|
26574
|
+
case "ship": return shipDef(common);
|
|
26324
26575
|
case "judge": return judgeDef(common, request, step);
|
|
26325
26576
|
case "human": return messageDef(common, request, step);
|
|
26326
26577
|
}
|
|
@@ -26474,6 +26725,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
26474
26725
|
path: e.path
|
|
26475
26726
|
})) : [];
|
|
26476
26727
|
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`);
|
|
26728
|
+
const shipBlocked = ref === void 0 ? yield* shipBlockedAt(setup, pin, step, changes) : void 0;
|
|
26477
26729
|
return {
|
|
26478
26730
|
def,
|
|
26479
26731
|
workflowName: resolved.name,
|
|
@@ -26492,7 +26744,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
26492
26744
|
memoryResumed: memory.resumed,
|
|
26493
26745
|
hints: hintsOf(stepDef),
|
|
26494
26746
|
cost: costTotals(run.costEntries),
|
|
26495
|
-
setup
|
|
26747
|
+
setup,
|
|
26748
|
+
...shipBlocked !== void 0 ? { shipBlocked } : {}
|
|
26496
26749
|
};
|
|
26497
26750
|
});
|
|
26498
26751
|
const currentRest = restAt(void 0);
|
|
@@ -26621,6 +26874,8 @@ const renderRest = (rest) => succeed$1({
|
|
|
26621
26874
|
kind: rest.stepDef.kind,
|
|
26622
26875
|
content: rest.stepDef.content,
|
|
26623
26876
|
...rest.hints,
|
|
26877
|
+
...failedRunStalled(rest) ? { failure: rest.setup.workspace.readSync(".gtd/run-failed.txt") ?? "" } : {},
|
|
26878
|
+
...rest.shipBlocked !== void 0 ? { blocked: rest.shipBlocked } : {},
|
|
26624
26879
|
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
26625
26880
|
memoryResumed: rest.memoryResumed,
|
|
26626
26881
|
...rest.pin !== void 0 ? { pin: rest.pin } : {}
|
|
@@ -26629,7 +26884,9 @@ const renderRest = (rest) => succeed$1({
|
|
|
26629
26884
|
* Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
|
|
26630
26885
|
* attempt at this very agent rest, and another dispatch would repeat it.
|
|
26631
26886
|
*/
|
|
26632
|
-
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;
|
|
26887
|
+
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;
|
|
26888
|
+
/** A `run(…, { onFail: "rest" })` step whose last turn recorded a failure, on a clean tree. */
|
|
26889
|
+
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;
|
|
26633
26890
|
/** 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. */
|
|
26634
26891
|
const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
|
|
26635
26892
|
/** `idle` means exactly one thing: no process underway, clean tree. */
|
|
@@ -26794,7 +27051,7 @@ const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "e
|
|
|
26794
27051
|
* replay with the working tree as the pending turn. The target step is
|
|
26795
27052
|
* computed by replaying, never matched.
|
|
26796
27053
|
*/
|
|
26797
|
-
const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
27054
|
+
const decideLanding = (rest, verdicts, branch, approver, shipped) => gen(function* () {
|
|
26798
27055
|
if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
|
|
26799
27056
|
const unapproved = approvalRefusal(rest, approver);
|
|
26800
27057
|
if (unapproved !== void 0) return unapproved;
|
|
@@ -26807,30 +27064,183 @@ const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
|
26807
27064
|
message
|
|
26808
27065
|
};
|
|
26809
27066
|
}
|
|
27067
|
+
const landing = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef));
|
|
27068
|
+
if (rest.stepDef.onFail === "rest" && landing.read(".gtd/run-failed.txt") !== void 0) return {
|
|
27069
|
+
kind: "attempt",
|
|
27070
|
+
subject: formatSubject(rest.actor, rest.state)
|
|
27071
|
+
};
|
|
26810
27072
|
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
26811
|
-
tree:
|
|
26812
|
-
...verdicts !== void 0 ? { verdicts } : {}
|
|
27073
|
+
tree: landing,
|
|
27074
|
+
...verdicts !== void 0 ? { verdicts } : {},
|
|
27075
|
+
...shipped !== void 0 ? { shipped } : {}
|
|
26813
27076
|
}));
|
|
26814
27077
|
const to = yield* landingTarget(outcome, rest);
|
|
26815
27078
|
if (typeof to !== "string") return to;
|
|
26816
27079
|
return landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
26817
27080
|
});
|
|
27081
|
+
/** The user's branch: `<b>` for a process on `gtd/<b>` while `<b>` still exists; any other branch as is. */
|
|
27082
|
+
const userBranch = (git) => gen(function* () {
|
|
27083
|
+
const current = yield* git.currentBranch();
|
|
27084
|
+
const name = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
27085
|
+
if (name === void 0) return current;
|
|
27086
|
+
const there = yield* git.readRefOption(`refs/heads/${name}`);
|
|
27087
|
+
return isSome(there) ? some(name) : current;
|
|
27088
|
+
});
|
|
27089
|
+
/** `<b>` for a process on `gtd/<b>` while `<b>` exists: where a finishing landing leaves HEAD. */
|
|
27090
|
+
const returnBranch = gen(function* () {
|
|
27091
|
+
const git = yield* GitService;
|
|
27092
|
+
const current = yield* git.currentBranch();
|
|
27093
|
+
const name = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
27094
|
+
if (name === void 0) return void 0;
|
|
27095
|
+
return isSome(yield* git.readRefOption(`refs/heads/${name}`)) ? name : void 0;
|
|
27096
|
+
});
|
|
27097
|
+
/** A starting landing moves onto `gtd/<b>` first. */
|
|
27098
|
+
const withBranchAndPush = (landing) => gen(function* () {
|
|
27099
|
+
if (landing.kind !== "commit") return landing;
|
|
27100
|
+
let out = landing;
|
|
27101
|
+
if (landing.spec.vars !== void 0) {
|
|
27102
|
+
const start = yield* startBranchFor("gtd land");
|
|
27103
|
+
if (start.kind === "refusal") return {
|
|
27104
|
+
kind: "refusal",
|
|
27105
|
+
message: start.message
|
|
27106
|
+
};
|
|
27107
|
+
if (start.kind === "switch") out = {
|
|
27108
|
+
...out,
|
|
27109
|
+
startBranch: start.branch
|
|
27110
|
+
};
|
|
27111
|
+
}
|
|
27112
|
+
return yield* withHistory(out);
|
|
27113
|
+
});
|
|
27114
|
+
/** A closing landing switches back to `<b>`; it and a shipping one push their history ref. */
|
|
27115
|
+
const withHistory = (landing) => gen(function* () {
|
|
27116
|
+
let out = landing;
|
|
27117
|
+
const back = landing.keepHistory === void 0 ? void 0 : yield* returnBranch;
|
|
27118
|
+
if (back !== void 0) out = {
|
|
27119
|
+
...out,
|
|
27120
|
+
leaveBranch: back
|
|
27121
|
+
};
|
|
27122
|
+
if (landing.keepHistory === void 0 && landing.ship === void 0) return out;
|
|
27123
|
+
const remote = (yield* (yield* ConfigService).load).historyRemote;
|
|
27124
|
+
return remote === void 0 ? out : {
|
|
27125
|
+
...out,
|
|
27126
|
+
pushHistory: remote
|
|
27127
|
+
};
|
|
27128
|
+
});
|
|
26818
27129
|
/** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
|
|
26819
|
-
const previewLanding = (rest) => decideLanding(rest, void 0, none(), none()).pipe(map((landing) => landing.kind === "commit" ? landing.to : void 0), catch_(() => succeed$1(void 0)));
|
|
27130
|
+
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$1(void 0)));
|
|
27131
|
+
const snapshotOf = (rest, landing) => ({
|
|
27132
|
+
stepDef: rest.stepDef,
|
|
27133
|
+
state: rest.state,
|
|
27134
|
+
actor: rest.actor,
|
|
27135
|
+
changes: rest.changes,
|
|
27136
|
+
file: rest.hints.file,
|
|
27137
|
+
landing
|
|
27138
|
+
});
|
|
27139
|
+
const refusedSnapshot = (rest, message) => snapshotOf(rest, {
|
|
27140
|
+
kind: "refusal",
|
|
27141
|
+
message
|
|
27142
|
+
});
|
|
27143
|
+
const SHIPPED_STAND_IN = "0".repeat(40);
|
|
27144
|
+
const SHIP_EXCLUDED = ".gtd/";
|
|
27145
|
+
const shippedPaths = (before, after) => {
|
|
27146
|
+
const diff = diffTrees$1(before, after);
|
|
27147
|
+
return [
|
|
27148
|
+
...diff.added,
|
|
27149
|
+
...diff.modified,
|
|
27150
|
+
...diff.deleted
|
|
27151
|
+
].filter((path) => !path.startsWith(SHIP_EXCLUDED));
|
|
27152
|
+
};
|
|
27153
|
+
const noTarget = (why) => ({
|
|
27154
|
+
kind: "skip",
|
|
27155
|
+
note: `gtd: no branch to ship to: ${why}`
|
|
27156
|
+
});
|
|
27157
|
+
/** `<b>` and its tip, for a process on `gtd/<b>`; why there is none otherwise. */
|
|
27158
|
+
const shipTarget = (git) => gen(function* () {
|
|
27159
|
+
const current = yield* git.currentBranch();
|
|
27160
|
+
if (isNone(current)) return noTarget("HEAD is detached");
|
|
27161
|
+
const branch = userBranchOf(current.value);
|
|
27162
|
+
if (branch === void 0) return noTarget(`${current.value} is no gtd/ branch`);
|
|
27163
|
+
if (userBranchOf(branch) !== void 0) return noTarget(`${current.value} records no user branch`);
|
|
27164
|
+
const parent = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
27165
|
+
if (isNone(parent)) return noTarget(`${branch} does not exist`);
|
|
27166
|
+
return {
|
|
27167
|
+
kind: "ship",
|
|
27168
|
+
branch,
|
|
27169
|
+
parent: parent.value
|
|
27170
|
+
};
|
|
27171
|
+
});
|
|
27172
|
+
/**
|
|
27173
|
+
* The paths `<b>` changed outside this process that shipping `tree` would
|
|
27174
|
+
* revert. A path the working tree changed since HEAD counts as brought in:
|
|
27175
|
+
* whether it carries `<b>`'s change is the person's call, not a merge gtd redoes.
|
|
27176
|
+
*/
|
|
27177
|
+
const revertedPaths = (git, workspace, parent, tree, changed) => gen(function* () {
|
|
27178
|
+
const head = yield* git.resolveRef("HEAD");
|
|
27179
|
+
const shipped = parseCommitMessage(yield* git.lastCommitMessage(parent)).tip;
|
|
27180
|
+
if (shipped !== void 0 && (yield* git.isAncestor(shipped, head))) return [];
|
|
27181
|
+
const fork = yield* git.mergeBase(parent, head);
|
|
27182
|
+
const foreign = isNone(fork) ? [...changed] : shippedPaths(commitTree(workspace, fork.value), commitTree(workspace, parent));
|
|
27183
|
+
const committed = commitTree(workspace, head);
|
|
27184
|
+
return foreign.filter((path) => changed.has(path) && tree.read(path) === committed.read(path));
|
|
27185
|
+
});
|
|
27186
|
+
/**
|
|
27187
|
+
* Whether landing a ship step with `tree` would ship, and onto what. The squash
|
|
27188
|
+
* replaces the user's branch's tree with `tree`, so it is blocked while that
|
|
27189
|
+
* branch carries a change `tree` would revert: one made since the last ship
|
|
27190
|
+
* (whose `Gtd-Tip` HEAD descends from), or since it forked from HEAD.
|
|
27191
|
+
*/
|
|
27192
|
+
const decideShip = (workspace, tree) => gen(function* () {
|
|
27193
|
+
const git = yield* GitService;
|
|
27194
|
+
const target = yield* shipTarget(git);
|
|
27195
|
+
if (target.kind !== "ship") return target;
|
|
27196
|
+
const { branch, parent } = target;
|
|
27197
|
+
const changed = new Set(shippedPaths(commitTree(workspace, parent), tree));
|
|
27198
|
+
const reverted = yield* revertedPaths(git, workspace, parent, tree, changed);
|
|
27199
|
+
if (reverted.length > 0) return {
|
|
27200
|
+
kind: "blocked",
|
|
27201
|
+
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`
|
|
27202
|
+
};
|
|
27203
|
+
return changed.size === 0 ? {
|
|
27204
|
+
kind: "skip",
|
|
27205
|
+
note: `gtd: nothing to ship: ${branch} already has this tree`
|
|
27206
|
+
} : target;
|
|
27207
|
+
});
|
|
27208
|
+
const shipBlockedAt = (setup, pin, step, changes) => gen(function* () {
|
|
27209
|
+
if (pin !== void 0 || step.request.kind !== "ship" || changes.length > 0) return void 0;
|
|
27210
|
+
const head = yield* (yield* GitService).resolveRef("HEAD");
|
|
27211
|
+
const decision = yield* decideShip(setup.workspace, commitTree(setup.workspace, head));
|
|
27212
|
+
return decision.kind === "blocked" ? decision.message : void 0;
|
|
27213
|
+
});
|
|
27214
|
+
const shipSnapshot = (rest, message, verdicts, branch, approver) => gen(function* () {
|
|
27215
|
+
const tree = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), void 0);
|
|
27216
|
+
const decision = yield* decideShip(rest.setup.workspace, tree);
|
|
27217
|
+
if (decision.kind === "blocked") return refusedSnapshot(rest, decision.message);
|
|
27218
|
+
const shipping = decision.kind === "ship";
|
|
27219
|
+
const decided = yield* decideLanding(rest, verdicts, branch, approver, shipping ? SHIPPED_STAND_IN : void 0);
|
|
27220
|
+
if (decided.kind !== "commit") return snapshotOf(rest, decided);
|
|
27221
|
+
const landing = shipping ? {
|
|
27222
|
+
...decided,
|
|
27223
|
+
ship: {
|
|
27224
|
+
branch: decision.branch,
|
|
27225
|
+
parent: decision.parent,
|
|
27226
|
+
shipMessage: message,
|
|
27227
|
+
historyRef: processHistoryRef(rest.run.startHash)
|
|
27228
|
+
}
|
|
27229
|
+
} : {
|
|
27230
|
+
...decided,
|
|
27231
|
+
note: decision.note
|
|
27232
|
+
};
|
|
27233
|
+
return snapshotOf(rest, yield* withBranchAndPush(landing));
|
|
27234
|
+
});
|
|
26820
27235
|
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
26821
27236
|
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
26822
27237
|
const git = yield* GitService;
|
|
26823
|
-
const branch = yield* git
|
|
27238
|
+
const branch = yield* userBranch(git);
|
|
26824
27239
|
const approver = rest.stepDef.approve === true ? yield* git.authorIdent() : none();
|
|
26825
|
-
const
|
|
26826
|
-
return
|
|
26827
|
-
|
|
26828
|
-
|
|
26829
|
-
actor: rest.actor,
|
|
26830
|
-
changes: rest.changes,
|
|
26831
|
-
file: rest.hints.file,
|
|
26832
|
-
landing
|
|
26833
|
-
};
|
|
27240
|
+
const request = rest.step.request;
|
|
27241
|
+
if (rest.pin === void 0 && request.kind === "ship") return yield* shipSnapshot(rest, request.message, verdicts, branch, approver);
|
|
27242
|
+
const decided = yield* decideLanding(rest, verdicts, branch, approver);
|
|
27243
|
+
return snapshotOf(rest, yield* withBranchAndPush(decided));
|
|
26834
27244
|
});
|
|
26835
27245
|
const humanCommitsOf = (trace, entryCommit) => trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
|
|
26836
27246
|
hash: entry.hash,
|
|
@@ -27957,6 +28367,32 @@ const runStartListCommand = (json, out) => gen(function* () {
|
|
|
27957
28367
|
out.write([...workflows.map((name) => `workflow ${name}\n`), ...doors.map(([n, d]) => `door ${doorSynopsis(n, d)} → ${d.workflow}\n`)].join(""));
|
|
27958
28368
|
});
|
|
27959
28369
|
/**
|
|
28370
|
+
* Back on `<b>` when the process ran on `gtd/<b>` and `<b>` moved only by its
|
|
28371
|
+
* ships (whose `Gtd-Tip` the tip descends from): the rewind stops at the last
|
|
28372
|
+
* ship, so the uncommitted changes are what it left unshipped.
|
|
28373
|
+
*/
|
|
28374
|
+
const abandonTarget = (current, tip, startParent) => gen(function* () {
|
|
28375
|
+
const git = yield* GitService;
|
|
28376
|
+
const userBranch = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
28377
|
+
const userTip = userBranch === void 0 ? none() : yield* git.readRefOption(`refs/heads/${userBranch}`);
|
|
28378
|
+
if (userBranch === void 0 || isNone(userTip)) return {
|
|
28379
|
+
leaves: void 0,
|
|
28380
|
+
rewindTo: startParent
|
|
28381
|
+
};
|
|
28382
|
+
if (userTip.value === startParent) return {
|
|
28383
|
+
leaves: userBranch,
|
|
28384
|
+
rewindTo: startParent
|
|
28385
|
+
};
|
|
28386
|
+
const shipped = parseCommitMessage(yield* git.lastCommitMessage(userTip.value)).tip;
|
|
28387
|
+
return shipped !== void 0 && (yield* git.isAncestor(shipped, tip)) ? {
|
|
28388
|
+
leaves: userBranch,
|
|
28389
|
+
rewindTo: userTip.value
|
|
28390
|
+
} : {
|
|
28391
|
+
leaves: void 0,
|
|
28392
|
+
rewindTo: startParent
|
|
28393
|
+
};
|
|
28394
|
+
});
|
|
28395
|
+
/**
|
|
27960
28396
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
27961
28397
|
* `git reset --mixed` to the process's start commit leaves every turn
|
|
27962
28398
|
* commit's content in the working tree.
|
|
@@ -27971,7 +28407,8 @@ const runStartListCommand = (json, out) => gen(function* () {
|
|
|
27971
28407
|
*/
|
|
27972
28408
|
const runAbandonCommand = (json, out) => gen(function* () {
|
|
27973
28409
|
const git = yield* GitService;
|
|
27974
|
-
const
|
|
28410
|
+
const config = yield* (yield* ConfigService).load;
|
|
28411
|
+
const initial = config.workflow.initial;
|
|
27975
28412
|
const run = yield* currentRun;
|
|
27976
28413
|
if (run.trace.length === 0) {
|
|
27977
28414
|
const required = emitScripts([{
|
|
@@ -27983,6 +28420,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
27983
28420
|
const restState = run.trace[run.trace.length - 1].state;
|
|
27984
28421
|
if (run.startParentHash === EMPTY_TREE) return yield* fail$4(/* @__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`));
|
|
27985
28422
|
const tip = yield* git.resolveRef("HEAD");
|
|
28423
|
+
const historyRef = processHistoryRef(run.startHash);
|
|
28424
|
+
const current = yield* git.currentBranch();
|
|
28425
|
+
const { leaves, rewindTo } = yield* abandonTarget(current, tip, run.startParentHash);
|
|
28426
|
+
const remote = config.historyRemote;
|
|
27986
28427
|
const steps = [
|
|
27987
28428
|
{
|
|
27988
28429
|
kind: "gitWrite",
|
|
@@ -27990,21 +28431,60 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
27990
28431
|
},
|
|
27991
28432
|
{
|
|
27992
28433
|
kind: "gitWrite",
|
|
27993
|
-
command:
|
|
28434
|
+
command: updateRef(historyRef, tip)
|
|
28435
|
+
},
|
|
28436
|
+
{
|
|
28437
|
+
kind: "gitWrite",
|
|
28438
|
+
command: mixedResetTo(rewindTo)
|
|
27994
28439
|
},
|
|
27995
28440
|
{
|
|
27996
28441
|
kind: "gitWrite",
|
|
27997
28442
|
command: unpinScript(PIN_PATH)
|
|
27998
28443
|
},
|
|
28444
|
+
...leaves !== void 0 ? [
|
|
28445
|
+
{
|
|
28446
|
+
kind: "gitWrite",
|
|
28447
|
+
command: switchTo(leaves)
|
|
28448
|
+
},
|
|
28449
|
+
{
|
|
28450
|
+
kind: "gitWrite",
|
|
28451
|
+
command: deleteBranch(getOrThrow$1(current))
|
|
28452
|
+
},
|
|
28453
|
+
{
|
|
28454
|
+
kind: "gitWrite",
|
|
28455
|
+
command: updateRef(RESUME_BRANCH_REF, tip)
|
|
28456
|
+
}
|
|
28457
|
+
] : [{
|
|
28458
|
+
kind: "gitWrite",
|
|
28459
|
+
command: deleteRef(RESUME_BRANCH_REF)
|
|
28460
|
+
}],
|
|
27999
28461
|
{
|
|
28000
28462
|
kind: "outcome",
|
|
28001
|
-
command: abandonedOutcome(restState,
|
|
28002
|
-
}
|
|
28463
|
+
command: abandonedOutcome(restState, rewindTo, initial)
|
|
28464
|
+
},
|
|
28465
|
+
...remote !== void 0 ? [{
|
|
28466
|
+
kind: "gitWrite",
|
|
28467
|
+
command: pushRef(remote, historyRef)
|
|
28468
|
+
}] : []
|
|
28003
28469
|
];
|
|
28004
28470
|
const required = emitScripts(steps).required;
|
|
28005
28471
|
yield* writeScript(json, out, combinedScript(required, ""));
|
|
28006
28472
|
});
|
|
28007
28473
|
/**
|
|
28474
|
+
* `gtd/<b>` when the abandon left it for `<b>` and HEAD is still on `<b>`. A
|
|
28475
|
+
* `gtd/<b>` made since is reset only when the retained tip contains it — the
|
|
28476
|
+
* `-C` switch would orphan anything else.
|
|
28477
|
+
*/
|
|
28478
|
+
const resumeBranch = (git, tip) => gen(function* () {
|
|
28479
|
+
if (isNone(yield* git.readRefOption("refs/worktree/gtd/history-branch"))) return void 0;
|
|
28480
|
+
const current = yield* git.currentBranch();
|
|
28481
|
+
if (isNone(current) || current.value.startsWith("gtd/")) return void 0;
|
|
28482
|
+
const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
|
|
28483
|
+
const existing = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
28484
|
+
if (isSome(existing) && !(yield* git.isAncestor(existing.value, tip))) return yield* fail$4(/* @__PURE__ */ new Error(`gtd restore: branch ${branch} holds commits the retained tip ${tip.slice(0, 7)} does not — merge or delete it first`));
|
|
28485
|
+
return branch;
|
|
28486
|
+
});
|
|
28487
|
+
/**
|
|
28008
28488
|
* `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
|
|
28009
28489
|
*
|
|
28010
28490
|
* Guarded by `restorability` so it never discards work it didn't create:
|
|
@@ -28021,15 +28501,20 @@ const runRestoreCommand = (json, out) => gen(function* () {
|
|
|
28021
28501
|
const check = yield* restorability(git, headHash, tip);
|
|
28022
28502
|
if (!check.ok) return yield* fail$4(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
|
|
28023
28503
|
const after = yield* restAt(tip);
|
|
28504
|
+
const resumeOn = yield* resumeBranch(git, tip);
|
|
28024
28505
|
const steps = [
|
|
28025
28506
|
{
|
|
28026
28507
|
kind: "gitWrite",
|
|
28027
|
-
command: hardResetTo(tip)
|
|
28508
|
+
command: resumeOn === void 0 ? hardResetTo(tip) : switchBranchTo(resumeOn, tip)
|
|
28028
28509
|
},
|
|
28029
28510
|
{
|
|
28030
28511
|
kind: "gitWrite",
|
|
28031
28512
|
command: deleteRef(HISTORY_REF)
|
|
28032
28513
|
},
|
|
28514
|
+
{
|
|
28515
|
+
kind: "gitWrite",
|
|
28516
|
+
command: deleteRef(RESUME_BRANCH_REF)
|
|
28517
|
+
},
|
|
28033
28518
|
{
|
|
28034
28519
|
kind: "outcome",
|
|
28035
28520
|
command: restoredOutcome(tip, after.state)
|
|
@@ -28744,7 +29229,7 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
|
|
|
28744
29229
|
},
|
|
28745
29230
|
group: "Start and stop",
|
|
28746
29231
|
summary: "Start a workflow or door; bare, list them",
|
|
28747
|
-
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
|
|
29232
|
+
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.
|
|
28748
29233
|
|
|
28749
29234
|
<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.
|
|
28750
29235
|
|
|
@@ -28758,7 +29243,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
28758
29243
|
arity: "none",
|
|
28759
29244
|
group: "Start and stop",
|
|
28760
29245
|
summary: "Stop the process, keeping its changes",
|
|
28761
|
-
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
|
|
29246
|
+
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.
|
|
28762
29247
|
|
|
28763
29248
|
s=$(gtd abandon --json=script) && sh -c "$s"`
|
|
28764
29249
|
},
|
|
@@ -28768,7 +29253,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
28768
29253
|
arity: "none",
|
|
28769
29254
|
group: "Start and stop",
|
|
28770
29255
|
summary: "Bring back the last abandoned process",
|
|
28771
|
-
details: `Bring back the process the last \`gtd abandon\` stopped: the printed script
|
|
29256
|
+
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.`
|
|
28772
29257
|
},
|
|
28773
29258
|
{
|
|
28774
29259
|
token: "repin",
|
|
@@ -28786,7 +29271,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
28786
29271
|
arity: "none",
|
|
28787
29272
|
group: "Inspect",
|
|
28788
29273
|
summary: "Print the prompt for the closing message",
|
|
28789
|
-
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.
|
|
29274
|
+
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.
|
|
28790
29275
|
|
|
28791
29276
|
Refuses when the workflow declares no summary template, or when the process has no commits.`
|
|
28792
29277
|
},
|
|
@@ -29589,6 +30074,71 @@ var InMemRepo = class InMemRepo {
|
|
|
29589
30074
|
this.worktree = new Map(tree);
|
|
29590
30075
|
this.index = new Map(tree);
|
|
29591
30076
|
}
|
|
30077
|
+
/** `git switch -C <name> [<start>]`: pending changes ride along only when the tip's tree is unchanged. */
|
|
30078
|
+
switchBranchTo(name, start) {
|
|
30079
|
+
const hash = start === void 0 ? this.head : this.resolveRef(start);
|
|
30080
|
+
if (hash === null) throw new Error(`invalid reference: ${start ?? "HEAD"}`);
|
|
30081
|
+
this.moveToBranch(name, hash, true);
|
|
30082
|
+
}
|
|
30083
|
+
/** `git switch <name>`: a pending change rides along when its path is the same in both trees. */
|
|
30084
|
+
switchTo(name) {
|
|
30085
|
+
const hash = this.branches.get(name);
|
|
30086
|
+
if (hash === void 0) throw new Error(`invalid reference: ${name}`);
|
|
30087
|
+
if (hash === this.head) return this.moveToBranch(name, hash, false);
|
|
30088
|
+
const from = this.headTree();
|
|
30089
|
+
const to = this.getCommit(hash)?.files ?? /* @__PURE__ */ new Map();
|
|
30090
|
+
const pending = [.../* @__PURE__ */ new Set([
|
|
30091
|
+
...from.keys(),
|
|
30092
|
+
...this.index.keys(),
|
|
30093
|
+
...this.worktree.keys()
|
|
30094
|
+
])].filter((path) => this.index.get(path) !== from.get(path) || this.worktree.get(path) !== from.get(path));
|
|
30095
|
+
if (pending.some((path) => to.get(path) !== from.get(path))) throw new Error(`cannot switch to '${name}': local changes would be overwritten`);
|
|
30096
|
+
const carry = (side) => {
|
|
30097
|
+
const out = new Map(to);
|
|
30098
|
+
for (const path of pending) {
|
|
30099
|
+
const content = side.get(path);
|
|
30100
|
+
if (content === void 0) out.delete(path);
|
|
30101
|
+
else out.set(path, content);
|
|
30102
|
+
}
|
|
30103
|
+
return out;
|
|
30104
|
+
};
|
|
30105
|
+
this.worktree = carry(this.worktree);
|
|
30106
|
+
this.index = carry(this.index);
|
|
30107
|
+
this.head = hash;
|
|
30108
|
+
this.currentBranch = name;
|
|
30109
|
+
}
|
|
30110
|
+
moveToBranch(name, hash, reset) {
|
|
30111
|
+
if (hash !== this.head) {
|
|
30112
|
+
if (this.statusPorcelain().trim() !== "") throw new Error(`cannot switch to '${name}': the working tree has uncommitted changes`);
|
|
30113
|
+
const tree = this.getCommit(hash)?.files ?? /* @__PURE__ */ new Map();
|
|
30114
|
+
this.head = hash;
|
|
30115
|
+
this.worktree = new Map(tree);
|
|
30116
|
+
this.index = new Map(tree);
|
|
30117
|
+
}
|
|
30118
|
+
if (reset || !this.branches.has(name)) this.branches.set(name, hash);
|
|
30119
|
+
this.currentBranch = name;
|
|
30120
|
+
}
|
|
30121
|
+
deleteBranch(name) {
|
|
30122
|
+
if (name === this.currentBranch) throw new Error(`cannot delete branch '${name}' checked out`);
|
|
30123
|
+
if (!this.branches.delete(name)) throw new Error(`branch '${name}' not found`);
|
|
30124
|
+
}
|
|
30125
|
+
remotes = /* @__PURE__ */ new Map();
|
|
30126
|
+
/** Test setup: a remote that accepts pushes. */
|
|
30127
|
+
addRemote(name) {
|
|
30128
|
+
this.remotes.set(name, /* @__PURE__ */ new Map());
|
|
30129
|
+
}
|
|
30130
|
+
/** What a remote holds after pushes: ref → hash. */
|
|
30131
|
+
remoteRefs(name) {
|
|
30132
|
+
return this.remotes.get(name) ?? /* @__PURE__ */ new Map();
|
|
30133
|
+
}
|
|
30134
|
+
/** `git push --force <remote> <ref>:<ref>`; returns git's complaint, or `undefined`. */
|
|
30135
|
+
pushRef(remote, ref) {
|
|
30136
|
+
const there = this.remotes.get(remote);
|
|
30137
|
+
if (there === void 0) return `fatal: '${remote}' does not appear to be a git repository`;
|
|
30138
|
+
const hash = this.resolveRef(ref);
|
|
30139
|
+
if (hash === null) return `error: src refspec ${ref} does not match any`;
|
|
30140
|
+
there.set(ref, hash);
|
|
30141
|
+
}
|
|
29592
30142
|
/** Refs under `prefix`, newest tip first; the commit sequence number stands in for the committer date. */
|
|
29593
30143
|
listRefs(prefix) {
|
|
29594
30144
|
return [...this.refs.entries()].filter(([ref]) => ref.startsWith(prefix)).map(([ref, hash]) => ({
|
|
@@ -29821,6 +30371,27 @@ var InMemRepo = class InMemRepo {
|
|
|
29821
30371
|
this.index = new Map(tree);
|
|
29822
30372
|
this.resetHard();
|
|
29823
30373
|
}
|
|
30374
|
+
/** Twin of `GitScript.shipCommit`: the staged tree, with `.gtd/` as `branch` has it, onto `branch`, then the landing commit naming it. */
|
|
30375
|
+
shipCommit(branch, parent, shipMessage, historyRef, message) {
|
|
30376
|
+
this.stageAll();
|
|
30377
|
+
if (this.head === null) return "fatal: Needed a single revision";
|
|
30378
|
+
if (this.signingFails) return "error: gpg failed to sign the data";
|
|
30379
|
+
const kept = [...this.commits.get(parent)?.files ?? []];
|
|
30380
|
+
const tree = new Map([...[...this.index].filter(([path]) => !path.startsWith(".gtd/")), ...kept.filter(([path]) => path.startsWith(".gtd/"))]);
|
|
30381
|
+
const shipped = `${shipMessage}\n\nGtd-History: ${historyRef}\nGtd-Tip: ${this.head}\n`;
|
|
30382
|
+
const hash = makeHash(shipped, parent, tree);
|
|
30383
|
+
if (this.branches.get(branch) !== parent) return `fatal: update_ref failed for ref 'refs/heads/${branch}': cannot lock ref 'refs/heads/${branch}': is at ${this.branches.get(branch) ?? "nothing"} but expected ${parent}`;
|
|
30384
|
+
this.commits.set(hash, {
|
|
30385
|
+
hash,
|
|
30386
|
+
message: shipped,
|
|
30387
|
+
files: tree,
|
|
30388
|
+
parent,
|
|
30389
|
+
seq: this.nextSeq++,
|
|
30390
|
+
signed: this.signingEnabled
|
|
30391
|
+
});
|
|
30392
|
+
this.branches.set(branch, hash);
|
|
30393
|
+
return this.commitAsIs(`${message}\nGtd-Shipped: ${hash}`);
|
|
30394
|
+
}
|
|
29824
30395
|
/** Discard every pending change, tracked or untracked: stage everything, then hard-reset (which drops the freshly-staged untracked paths too). */
|
|
29825
30396
|
discardPending() {
|
|
29826
30397
|
this.index = new Map(this.worktree);
|
|
@@ -30610,6 +31181,18 @@ const recognizeKeepTurn = (repo, block) => {
|
|
|
30610
31181
|
status: 1
|
|
30611
31182
|
};
|
|
30612
31183
|
};
|
|
31184
|
+
const attempt = (apply) => {
|
|
31185
|
+
try {
|
|
31186
|
+
apply();
|
|
31187
|
+
return { kind: "applied" };
|
|
31188
|
+
} catch (e) {
|
|
31189
|
+
return {
|
|
31190
|
+
kind: "failed",
|
|
31191
|
+
error: `fatal: ${e instanceof Error ? e.message : String(e)}`,
|
|
31192
|
+
status: 128
|
|
31193
|
+
};
|
|
31194
|
+
}
|
|
31195
|
+
};
|
|
30613
31196
|
const recognizeGitBuilders = (repo, block) => {
|
|
30614
31197
|
if (block === discardPending()) {
|
|
30615
31198
|
repo.discardPending();
|
|
@@ -30687,6 +31270,58 @@ const recognizeGitBuilders = (repo, block) => {
|
|
|
30687
31270
|
}
|
|
30688
31271
|
return;
|
|
30689
31272
|
}
|
|
31273
|
+
if (block.startsWith("git switch -q -C ")) {
|
|
31274
|
+
const [branch, start] = extractQuotedTokens(block);
|
|
31275
|
+
if (branch !== void 0 && switchBranchTo(branch, start) === block) return attempt(() => repo.switchBranchTo(branch, start));
|
|
31276
|
+
return;
|
|
31277
|
+
}
|
|
31278
|
+
if (block.startsWith("if ! git switch -q ")) {
|
|
31279
|
+
const [branch, , note] = extractQuotedTokens(block);
|
|
31280
|
+
if (branch !== void 0 && switchBack(branch) === block) try {
|
|
31281
|
+
repo.switchTo(branch);
|
|
31282
|
+
return { kind: "applied" };
|
|
31283
|
+
} catch {
|
|
31284
|
+
return {
|
|
31285
|
+
kind: "noop",
|
|
31286
|
+
output: `${note}\n`
|
|
31287
|
+
};
|
|
31288
|
+
}
|
|
31289
|
+
return;
|
|
31290
|
+
}
|
|
31291
|
+
if (block.startsWith("git switch -q ")) {
|
|
31292
|
+
const [branch] = extractQuotedTokens(block);
|
|
31293
|
+
if (branch !== void 0 && switchTo(branch) === block) return attempt(() => repo.switchTo(branch));
|
|
31294
|
+
return;
|
|
31295
|
+
}
|
|
31296
|
+
if (block.startsWith("git branch -q -D ")) {
|
|
31297
|
+
const [branch] = extractQuotedTokens(block);
|
|
31298
|
+
if (branch !== void 0 && deleteBranch(branch) === block) return attempt(() => repo.deleteBranch(branch));
|
|
31299
|
+
return;
|
|
31300
|
+
}
|
|
31301
|
+
if (block.startsWith("if ! git push --force ")) {
|
|
31302
|
+
const [remote, refspec] = extractQuotedTokens(block);
|
|
31303
|
+
const ref = refspec?.split(":")[0];
|
|
31304
|
+
if (remote !== void 0 && ref !== void 0 && pushRef(remote, ref) === block) {
|
|
31305
|
+
const failure = repo.pushRef(remote, ref);
|
|
31306
|
+
if (failure === void 0) return { kind: "applied" };
|
|
31307
|
+
return {
|
|
31308
|
+
kind: "failed",
|
|
31309
|
+
error: `${failure}\n${extractQuotedTokens(block)[3]}`,
|
|
31310
|
+
status: 1
|
|
31311
|
+
};
|
|
31312
|
+
}
|
|
31313
|
+
return;
|
|
31314
|
+
}
|
|
31315
|
+
if (block.startsWith("gtd_ship_parent=")) {
|
|
31316
|
+
const [parent, branch, history, shipMessage, message] = extractQuotedTokens(block);
|
|
31317
|
+
if (parent === void 0 || branch === void 0 || history === void 0 || shipMessage === void 0 || message === void 0 || shipCommit(branch, parent, shipMessage, history, message) !== block) return;
|
|
31318
|
+
const failure = repo.shipCommit(branch, parent, shipMessage, history, message);
|
|
31319
|
+
return failure === void 0 ? { kind: "applied" } : {
|
|
31320
|
+
kind: "failed",
|
|
31321
|
+
error: failure,
|
|
31322
|
+
status: 1
|
|
31323
|
+
};
|
|
31324
|
+
}
|
|
30690
31325
|
if (block.startsWith("git update-ref -d ")) {
|
|
30691
31326
|
const [ref] = extractQuotedTokens(block);
|
|
30692
31327
|
if (ref !== void 0 && deleteRef(ref) === block) {
|
|
@@ -30793,6 +31428,31 @@ const recognizeModeCommand = (repo, commands, block) => {
|
|
|
30793
31428
|
} : scripted;
|
|
30794
31429
|
return recognizeScriptedCommand(repo, /* @__PURE__ */ new Map([[command, bound]]), command);
|
|
30795
31430
|
};
|
|
31431
|
+
const FAIL_REST_MIDDLE = "\n} 2>&1 )\" || gtd_run_status=$?\n";
|
|
31432
|
+
/** A `run(…, { onFail: "rest" })` body: the inner script's failure is recorded in the tree and the block itself succeeds. */
|
|
31433
|
+
const recognizeFailRest = (repo, commands, block) => {
|
|
31434
|
+
if (!block.startsWith("rm -f -- '") || !block.includes("gtd_run_out=")) return void 0;
|
|
31435
|
+
const [path] = extractQuotedTokens(block);
|
|
31436
|
+
const headerEnd = block.indexOf("gtd_run_out=\"$( {\n");
|
|
31437
|
+
const bodyEnd = block.indexOf(FAIL_REST_MIDDLE);
|
|
31438
|
+
if (path === void 0 || headerEnd === -1 || bodyEnd === -1) return void 0;
|
|
31439
|
+
const body = block.slice(headerEnd + 18, bodyEnd);
|
|
31440
|
+
if (failRestScript(body, path) !== block) return void 0;
|
|
31441
|
+
if (repo.readFile(path) !== void 0) repo.deleteFile(path);
|
|
31442
|
+
const result = applyEmittedScript(repo, commands, body);
|
|
31443
|
+
if (!result.ok && result.error?.startsWith(UNRECOGNIZED_PREFIX)) return {
|
|
31444
|
+
kind: "failed",
|
|
31445
|
+
error: `${result.output}${result.error}`
|
|
31446
|
+
};
|
|
31447
|
+
if (!result.ok) {
|
|
31448
|
+
const lines = result.output.replace(/\n+$/, "").split("\n");
|
|
31449
|
+
repo.writeFile(path, `exit ${result.status}\n${lines.slice(-50).join("\n")}\n`);
|
|
31450
|
+
}
|
|
31451
|
+
return {
|
|
31452
|
+
kind: "noop",
|
|
31453
|
+
output: result.output
|
|
31454
|
+
};
|
|
31455
|
+
};
|
|
30796
31456
|
const FAILURE_PROMPT_HEADER = "gtd_validate_status=0\ngtd_validate_out=\"$( {\n";
|
|
30797
31457
|
const FAILURE_PROMPT_MIDDLE = "\n} 2>&1 )\" || gtd_validate_status=$?\nif [ \"$gtd_validate_status\" -ne 0 ]; then\n printf '%s\\n\\n%s\\n' ";
|
|
30798
31458
|
const FAILURE_PROMPT_FOOTER = " \"$gtd_validate_out\"\n exit \"$gtd_validate_status\"\nfi";
|
|
@@ -31083,6 +31743,7 @@ const recognizersFor = (repo, commands, git) => [
|
|
|
31083
31743
|
(block) => git ? recognizeGitBuilders(repo, block) : void 0,
|
|
31084
31744
|
(block) => recognizeWriteLines(repo, block),
|
|
31085
31745
|
(block) => recognizePinScripts(repo, block),
|
|
31746
|
+
(block) => recognizeFailRest(repo, commands, block),
|
|
31086
31747
|
(block) => recognizeFailurePromptWrapper(repo, commands, block),
|
|
31087
31748
|
(block) => recognizePresentationSubshell(repo, commands, block, git),
|
|
31088
31749
|
(block) => recognizeModeContradictionSkipNotice(block),
|