@pmelab/gtd 64.3.0 → 66.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 +35 -25
- package/claude/hooks/assist.ts +9 -35
- package/claude/hooks/doors.ts +4 -4
- package/claude/hooks/drive.ts +10 -1
- 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 +86 -26
- package/claude/hooks/register.tsx +35 -82
- package/claude/hooks/tools.ts +7 -20
- package/claude/hooks/wording.ts +3 -3
- package/dist/chunks/{flows-dnIE_qbe.mjs → flows-CmQplvh1.mjs} +146 -35
- package/dist/chunks/{steering-DEh_MS40.mjs → steering-CMrePUiY.mjs} +115 -17
- package/dist/chunks/{testkit-BUX-47TD.mjs → testkit-BJhAAVGt.mjs} +905 -99
- package/dist/chunks/{wire-CaPJ9Tme.mjs → wire-xTbzFc26.mjs} +12 -6
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +965 -163
- package/dist/gtd.core.mjs +150 -134
- package/dist/steering.mjs +1 -1
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +185 -18
- package/dist/wire.mjs +1 -1
- package/dist/workflow-kit.mjs +9 -3
- package/package.json +1 -1
- package/src/cli/Cli.ts +5 -5
- 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 +105 -7
- package/src/edge/Edge.test.ts +37 -16
- package/src/edge/Edge.ts +380 -33
- package/src/emit/GitScript.ts +116 -15
- package/src/emit/index.ts +7 -0
- package/src/flows/config.ts +7 -1
- package/src/flows/helpers.ts +36 -18
- package/src/flows/index.test.ts +4 -0
- package/src/flows/index.ts +7 -0
- package/src/flows/runtime.ts +153 -15
- package/src/install/Install.ts +9 -2
- package/src/platform/Git.test.ts +7 -11
- package/src/platform/NodeSync.ts +2 -0
- package/src/platform/RetainedHistory.ts +3 -0
- package/src/platform/SyncIo.ts +2 -0
- package/src/platform/Workspace.test.ts +37 -0
- package/src/platform/Workspace.ts +36 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Replay.test.ts +9 -0
- package/src/replay/Replay.ts +92 -16
- package/src/replay/Trailers.ts +17 -0
- package/src/replay/Tree.ts +5 -0
- package/src/replay/index.ts +1 -1
- package/src/steering/FORMATS.md +3 -1
- package/src/steering/SteeringFormat.ts +13 -0
- package/src/steering/fixtures/review/sectioned.md +1 -1
- package/src/steering/fixtures/review/sectioned.read.json +0 -1
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +181 -5
- package/src/steering/review.ts +160 -18
- package/src/step/LandStep.ts +12 -0
- package/src/step/RepoSnapshot.ts +15 -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 +22 -17
- package/src/step/startBranch.ts +54 -0
- package/src/twins/EmittedScriptRecognizer.ts +118 -1
- package/src/twins/EmittedScripts.test.ts +147 -1
- package/src/twins/InMemRepo.ts +140 -3
- package/src/twins/Layers.ts +1 -0
- package/src/twins/SyncIo.test.ts +19 -4
- package/src/twins/SyncIo.ts +13 -1
- package/src/wire/BeatDocument.test.ts +15 -0
- package/src/wire/BeatDocument.ts +8 -2
- package/src/wire/Demand.ts +26 -12
- package/src/wire/constants.ts +3 -0
- package/src/wire/golden/1/beat-capture.json +1 -0
- package/src/wire/golden/1/beat-message.json +1 -0
- package/src/wire/golden/1/beat-prompt.json +1 -0
- package/src/wire/golden/1/beat-script.json +1 -0
- package/src/wire/golden/1/beat-stalled.json +1 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/Workflow.ts +3 -0
- package/src/workflow/compile.ts +2 -0
- package/src/workflow/load.ts +2 -0
- package/src/workflow-kit/index.test.ts +1 -0
- package/src/workflow-kit/index.ts +1 -0
- package/src/workflow-kit/scenarios.ts +8 -0
- package/claude/hooks/ship.ts +0 -730
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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-
|
|
1
|
+
import { A as revertScript, E as moveScript, G as installContext, M as writeScript$1, O as removeScript, Q as protocolMismatch, T as checkScript, Y as markedOptions, i as configure, j as showScript, k as restoreScript, n as configProblems, o as isFactoryWorkflow, p as numeric, s as answered, t as flows_exports, w as applyScript } from "./flows-CmQplvh1.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-xTbzFc26.mjs";
|
|
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-CMrePUiY.mjs";
|
|
4
4
|
import { createRequire, isBuiltin } from "node:module";
|
|
5
5
|
import * as Crypto from "node:crypto";
|
|
6
6
|
import { createHash } 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
|
*
|
|
@@ -20316,7 +20354,8 @@ const orAbsent = (read) => {
|
|
|
20316
20354
|
};
|
|
20317
20355
|
const infoOf = (stats) => ({
|
|
20318
20356
|
type: stats.isSymbolicLink() ? "symlink" : stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other",
|
|
20319
|
-
uid: process.platform === "win32" ? void 0 : stats.uid
|
|
20357
|
+
uid: process.platform === "win32" ? void 0 : stats.uid,
|
|
20358
|
+
...process.platform === "win32" ? {} : { executable: (stats.mode & 64) !== 0 }
|
|
20320
20359
|
});
|
|
20321
20360
|
const nodeSyncIo = {
|
|
20322
20361
|
readFile: (path) => orAbsent(() => readFileSync(path)),
|
|
@@ -20786,6 +20825,15 @@ const applyRawDiff = (tree, entries) => {
|
|
|
20786
20825
|
tree.set(entry.path, entry.newId);
|
|
20787
20826
|
}
|
|
20788
20827
|
};
|
|
20828
|
+
/** `git ls-tree -r -z --full-tree <ref>`'s output, parsed into `path → mode`. */
|
|
20829
|
+
const parseLsTreeModes = (out) => {
|
|
20830
|
+
const modes = /* @__PURE__ */ new Map();
|
|
20831
|
+
for (const record of out.split("\0")) {
|
|
20832
|
+
const tab = record.indexOf(" ");
|
|
20833
|
+
if (tab !== -1) modes.set(record.slice(tab + 1), record.slice(0, tab).split(" ")[0]);
|
|
20834
|
+
}
|
|
20835
|
+
return modes;
|
|
20836
|
+
};
|
|
20789
20837
|
/** `git ls-tree -r -z --full-tree <ref>`'s output, parsed into `path → blob id`. */
|
|
20790
20838
|
const parseLsTree = (out) => {
|
|
20791
20839
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -21018,6 +21066,34 @@ const makeWorkspaceOps = ({ root, pid }, git, commonDir, paths, io) => {
|
|
|
21018
21066
|
}
|
|
21019
21067
|
return new Map([...entries].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
21020
21068
|
};
|
|
21069
|
+
const modeSync = (ref) => {
|
|
21070
|
+
const modes = parseLsTreeModes(gitSync([
|
|
21071
|
+
"ls-tree",
|
|
21072
|
+
"-r",
|
|
21073
|
+
"-z",
|
|
21074
|
+
"--full-tree",
|
|
21075
|
+
ref ?? "HEAD"
|
|
21076
|
+
]));
|
|
21077
|
+
if (ref !== void 0) return modes;
|
|
21078
|
+
const status = gitSync([
|
|
21079
|
+
"status",
|
|
21080
|
+
"--porcelain=v1",
|
|
21081
|
+
"-z",
|
|
21082
|
+
"--untracked-files=all",
|
|
21083
|
+
"--no-renames",
|
|
21084
|
+
"--ignore-submodules=none"
|
|
21085
|
+
]);
|
|
21086
|
+
for (const record of status.split("\0")) {
|
|
21087
|
+
if (record.length < 4) continue;
|
|
21088
|
+
const path = record.slice(3).replace(/\/$/, "");
|
|
21089
|
+
const stat = io.lstat(paths.join(root, path));
|
|
21090
|
+
if (stat === void 0) modes.delete(path);
|
|
21091
|
+
else if (stat.type === "symlink") modes.set(path, "120000");
|
|
21092
|
+
else if (stat.type === "directory") modes.set(path, "160000");
|
|
21093
|
+
else modes.set(path, stat.executable === true ? "100755" : "100644");
|
|
21094
|
+
}
|
|
21095
|
+
return modes;
|
|
21096
|
+
};
|
|
21021
21097
|
/** A submodule's (or embedded repository's) checked-out commit — what `git add` records for it. */
|
|
21022
21098
|
const gitlinkOf = (path) => {
|
|
21023
21099
|
try {
|
|
@@ -21035,6 +21111,7 @@ const makeWorkspaceOps = ({ root, pid }, git, commonDir, paths, io) => {
|
|
|
21035
21111
|
return {
|
|
21036
21112
|
treeSync,
|
|
21037
21113
|
worktreeSync,
|
|
21114
|
+
modeSync,
|
|
21038
21115
|
episodeTrees,
|
|
21039
21116
|
readSync,
|
|
21040
21117
|
read: (path) => try_({
|
|
@@ -22479,6 +22556,8 @@ const loopLogPath = gen(function* () {
|
|
|
22479
22556
|
// src/platform/RetainedHistory.ts
|
|
22480
22557
|
/** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
|
|
22481
22558
|
const HISTORY_REF = "refs/worktree/gtd/history";
|
|
22559
|
+
/** Set by an abandon that left `gtd/<b>` for `<b>`: only then does restore recreate `gtd/<b>`. */
|
|
22560
|
+
const RESUME_BRANCH_REF = "refs/worktree/gtd/history-branch";
|
|
22482
22561
|
/** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
|
|
22483
22562
|
const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
|
|
22484
22563
|
const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
|
|
@@ -22690,6 +22769,7 @@ while :; do
|
|
|
22690
22769
|
model="$(gtd next --json=model)"
|
|
22691
22770
|
system="$(gtd next --json=system)"
|
|
22692
22771
|
validate="$(gtd next --json=validate)"
|
|
22772
|
+
fresh="$(gtd next --json=fresh)"
|
|
22693
22773
|
log="$(gtd next --json=log)"
|
|
22694
22774
|
# \`model\` is a hint, not a model name — gtd-workflows' default ships
|
|
22695
22775
|
# \`smart\` and \`base\`, which \`claude --model\` rejects. Resolve them
|
|
@@ -22732,8 +22812,14 @@ while :; do
|
|
|
22732
22812
|
then first=--resume second=--session-id
|
|
22733
22813
|
else first=--session-id second=--resume
|
|
22734
22814
|
fi
|
|
22815
|
+
# A validator retry resumes with the correction alone; a session that
|
|
22816
|
+
# cannot resume starts over with \`fresh\`, the original prompt plus it.
|
|
22817
|
+
second_prompt="$log.prompt"
|
|
22818
|
+
if [ "$second" = --session-id ] && [ -n "\${fresh:-}" ]
|
|
22819
|
+
then printf '%s\\n' "$fresh" >"$log.fresh" && second_prompt="$log.fresh"
|
|
22820
|
+
fi
|
|
22735
22821
|
turn "$first" "$session_id" <"$log.prompt" ||
|
|
22736
|
-
{ [ "$ran" = false ] && turn "$second" "$session_id" <"$
|
|
22822
|
+
{ [ "$ran" = false ] && turn "$second" "$session_id" <"$second_prompt"; } || {
|
|
22737
22823
|
echo "agent turn failed — see $log" >&2
|
|
22738
22824
|
exit 1
|
|
22739
22825
|
}
|
|
@@ -22747,7 +22833,7 @@ while :; do
|
|
|
22747
22833
|
exit 1
|
|
22748
22834
|
}
|
|
22749
22835
|
done
|
|
22750
|
-
rm -f "$log.prompt" ;;
|
|
22836
|
+
rm -f "$log.prompt" "$log.fresh" ;;
|
|
22751
22837
|
esac
|
|
22752
22838
|
|
|
22753
22839
|
# Read every value the landing needs BEFORE piping the script to \`sh\`:
|
|
@@ -23339,6 +23425,9 @@ const treeFromRecord = (files) => ({
|
|
|
23339
23425
|
});
|
|
23340
23426
|
const diffTrees$1 = (before, after) => {
|
|
23341
23427
|
const same = (path) => {
|
|
23428
|
+
if (before.mode !== void 0 && after.mode !== void 0) {
|
|
23429
|
+
if (before.mode(path) !== after.mode(path)) return false;
|
|
23430
|
+
}
|
|
23342
23431
|
const a = before.id?.(path);
|
|
23343
23432
|
const b = after.id?.(path);
|
|
23344
23433
|
return a !== void 0 && b !== void 0 ? a === b : before.read(path) === after.read(path);
|
|
@@ -23514,6 +23603,12 @@ const TRAILER_READERS = {
|
|
|
23514
23603
|
"Gtd-Anchor": (value, into) => {
|
|
23515
23604
|
if (HASH_RE.test(value)) into.anchor ??= value;
|
|
23516
23605
|
},
|
|
23606
|
+
"Gtd-Shipped": (value, into) => {
|
|
23607
|
+
if (HASH_RE.test(value)) into.shipped ??= value;
|
|
23608
|
+
},
|
|
23609
|
+
"Gtd-Tip": (value, into) => {
|
|
23610
|
+
if (HASH_RE.test(value)) into.tip ??= value;
|
|
23611
|
+
},
|
|
23517
23612
|
"Gtd-Format": (value, into) => {
|
|
23518
23613
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
23519
23614
|
},
|
|
@@ -23538,6 +23633,8 @@ const parseCommitMessage = (message) => {
|
|
|
23538
23633
|
approved: void 0,
|
|
23539
23634
|
pin: void 0,
|
|
23540
23635
|
anchor: void 0,
|
|
23636
|
+
shipped: void 0,
|
|
23637
|
+
tip: void 0,
|
|
23541
23638
|
judge: [],
|
|
23542
23639
|
marks: [],
|
|
23543
23640
|
vars: {},
|
|
@@ -23570,7 +23667,8 @@ const formatCommitMessage = (spec) => {
|
|
|
23570
23667
|
...trailer("Anchor", spec.anchor),
|
|
23571
23668
|
...trailer("Branch", spec.branch),
|
|
23572
23669
|
...trailer("Version", spec.version),
|
|
23573
|
-
`Gtd-Format: 4
|
|
23670
|
+
`Gtd-Format: 4`,
|
|
23671
|
+
...trailer("Shipped", spec.shipped)
|
|
23574
23672
|
];
|
|
23575
23673
|
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
23576
23674
|
};
|
|
@@ -23579,7 +23677,7 @@ const namesUnpin = (commit) => {
|
|
|
23579
23677
|
const unpin = pinStateIn(commit.parsed.format, "remove");
|
|
23580
23678
|
return unpin !== void 0 && commit.parsed.parsed?.to === unpin;
|
|
23581
23679
|
};
|
|
23582
|
-
const actorOfKind = (kind) => kind === "run" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
23680
|
+
const actorOfKind = (kind) => kind === "run" || kind === "ship" ? "check" : kind === "agent" ? "agent" : kind === "human" ? "human" : "judge";
|
|
23583
23681
|
const memoryScopeOf = (name) => {
|
|
23584
23682
|
const dot = name.lastIndexOf(".");
|
|
23585
23683
|
return dot === -1 ? "" : name.slice(0, dot);
|
|
@@ -23705,7 +23803,11 @@ const KNOWN_OPTIONS = {
|
|
|
23705
23803
|
"model",
|
|
23706
23804
|
"system",
|
|
23707
23805
|
"allowEmpty",
|
|
23708
|
-
"keep"
|
|
23806
|
+
"keep",
|
|
23807
|
+
"validate",
|
|
23808
|
+
"cap",
|
|
23809
|
+
"exhausted",
|
|
23810
|
+
"correction"
|
|
23709
23811
|
]),
|
|
23710
23812
|
human: /* @__PURE__ */ new Set([
|
|
23711
23813
|
...STEERING_OPTIONS,
|
|
@@ -23713,9 +23815,24 @@ const KNOWN_OPTIONS = {
|
|
|
23713
23815
|
"acceptClean",
|
|
23714
23816
|
"approve"
|
|
23715
23817
|
]),
|
|
23716
|
-
run: new Set(STEERING_OPTIONS),
|
|
23818
|
+
run: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "onFail"]),
|
|
23819
|
+
ship: /* @__PURE__ */ new Set(["label", "base"]),
|
|
23717
23820
|
judge: /* @__PURE__ */ new Set([...STEERING_OPTIONS, "message"])
|
|
23718
23821
|
};
|
|
23822
|
+
const validatorOptionFault = (name, options) => {
|
|
23823
|
+
const { validate, cap, exhausted, correction } = options;
|
|
23824
|
+
const prefix = `gtd: step "${name}": `;
|
|
23825
|
+
const notFunction = Object.entries({
|
|
23826
|
+
validate,
|
|
23827
|
+
exhausted,
|
|
23828
|
+
correction
|
|
23829
|
+
}).find(([, value]) => value !== void 0 && typeof value !== "function");
|
|
23830
|
+
if (notFunction !== void 0) return `${prefix}${notFunction[0]} must be a function in agent() options`;
|
|
23831
|
+
if (cap !== void 0 && !(Number.isInteger(cap) && cap > 0)) return `${prefix}cap must be a positive integer in agent() options`;
|
|
23832
|
+
if (validate !== void 0 && options.keep === true) return `${prefix}validate cannot be combined with keep: true — a kept turn is set aside, so there is no landed result to validate`;
|
|
23833
|
+
};
|
|
23834
|
+
const restsOnFail = (step) => step.request.kind === "run" && step.request.options.onFail === "rest";
|
|
23835
|
+
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
23836
|
const unknownOptions = (step) => {
|
|
23720
23837
|
const unknown = Object.keys(step.request.options).filter((key) => !KNOWN_OPTIONS[step.kind].has(key));
|
|
23721
23838
|
if (unknown.length === 0) return void 0;
|
|
@@ -23826,15 +23943,15 @@ const replay = async (raw) => {
|
|
|
23826
23943
|
return calls;
|
|
23827
23944
|
};
|
|
23828
23945
|
const scoped = (name) => [...scopes.flatMap((s) => s.name === void 0 ? [] : [s.name]), name].join(".");
|
|
23829
|
-
const isAttemptAt = (commit,
|
|
23946
|
+
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
23947
|
const advance = (next) => {
|
|
23831
23948
|
previousPosition = position;
|
|
23832
23949
|
position = next;
|
|
23833
23950
|
};
|
|
23834
|
-
const replyFor = (request, verdicts) => request.kind === "judge" ? {
|
|
23951
|
+
const replyFor = (request, verdicts, shipped) => request.kind === "judge" ? {
|
|
23835
23952
|
answers: answersFrom(verdicts, request.questions),
|
|
23836
23953
|
truncated: judgeCuts.get(request) ?? []
|
|
23837
|
-
} : void 0;
|
|
23954
|
+
} : request.kind === "ship" ? shipped : void 0;
|
|
23838
23955
|
const judgeCuts = /* @__PURE__ */ new WeakMap();
|
|
23839
23956
|
const resolveScoped = (memory, pick, configured, bundled) => {
|
|
23840
23957
|
const own = /* @__PURE__ */ new Map();
|
|
@@ -23948,7 +24065,7 @@ const replay = async (raw) => {
|
|
|
23948
24065
|
expectedNext = void 0;
|
|
23949
24066
|
anchorPending = false;
|
|
23950
24067
|
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);
|
|
24068
|
+
const error = unknownOptions(step) ?? onFailFault(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
|
|
23952
24069
|
return error === void 0 ? void 0 : {
|
|
23953
24070
|
kind: "failed",
|
|
23954
24071
|
message: error
|
|
@@ -23974,7 +24091,7 @@ const replay = async (raw) => {
|
|
|
23974
24091
|
expectNextOf(commit);
|
|
23975
24092
|
skipPins();
|
|
23976
24093
|
noteKept(step, commit);
|
|
23977
|
-
return replyFor(step.request, commit.parsed.judge);
|
|
24094
|
+
return replyFor(step.request, commit.parsed.judge, commit.parsed.shipped);
|
|
23978
24095
|
};
|
|
23979
24096
|
const expectNextOf = (commit) => {
|
|
23980
24097
|
const to = commit.parsed.parsed?.to;
|
|
@@ -24005,10 +24122,14 @@ const replay = async (raw) => {
|
|
|
24005
24122
|
hash: "",
|
|
24006
24123
|
tree: keeping ? position.tree : pending.tree
|
|
24007
24124
|
});
|
|
24008
|
-
return replyFor(step.request, pending.verdicts ?? []);
|
|
24125
|
+
return replyFor(step.request, pending.verdicts ?? [], pending.shipped);
|
|
24009
24126
|
};
|
|
24010
24127
|
const answer = (step) => {
|
|
24011
|
-
while (cursor < commits.length && isAttemptAt(commits[cursor], step
|
|
24128
|
+
while (cursor < commits.length && isAttemptAt(commits[cursor], step)) {
|
|
24129
|
+
if (!isEmptyDiff(diffTrees$1(position.tree, commits[cursor].tree))) advance({
|
|
24130
|
+
hash: commits[cursor].hash,
|
|
24131
|
+
tree: commits[cursor].tree
|
|
24132
|
+
});
|
|
24012
24133
|
cursor++;
|
|
24013
24134
|
skipPins();
|
|
24014
24135
|
}
|
|
@@ -24021,13 +24142,25 @@ const replay = async (raw) => {
|
|
|
24021
24142
|
landed,
|
|
24022
24143
|
marks
|
|
24023
24144
|
});
|
|
24145
|
+
if (restsOnFail(step) && input.pending.tree.read(".gtd/run-failed.txt") !== void 0) return finish({
|
|
24146
|
+
kind: "rest",
|
|
24147
|
+
rest: step,
|
|
24148
|
+
trace,
|
|
24149
|
+
landed,
|
|
24150
|
+
marks
|
|
24151
|
+
});
|
|
24024
24152
|
return landPending(step, input.pending);
|
|
24025
24153
|
};
|
|
24154
|
+
const bodyFault = (request) => {
|
|
24155
|
+
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`;
|
|
24156
|
+
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)`;
|
|
24157
|
+
};
|
|
24026
24158
|
const requestFault = (request) => {
|
|
24027
24159
|
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;
|
|
24160
|
+
if (request.kind !== "agent") return bodyFault(request);
|
|
24030
24161
|
const name = scoped(request.name);
|
|
24162
|
+
const optionFault = validatorOptionFault(name, request.options);
|
|
24163
|
+
if (optionFault !== void 0) return optionFault;
|
|
24031
24164
|
if (request.options.keep === true) {
|
|
24032
24165
|
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`;
|
|
24033
24166
|
const refFault = name.includes("/") ? "it contains \"/\"" : refNameFault(name);
|
|
@@ -24199,6 +24332,7 @@ const compileConfig = (value, origin) => {
|
|
|
24199
24332
|
return {
|
|
24200
24333
|
modes,
|
|
24201
24334
|
...config.ui !== void 0 ? { ui: config.ui } : {},
|
|
24335
|
+
...config.historyRemote !== void 0 ? { historyRemote: config.historyRemote } : {},
|
|
24202
24336
|
skills: Object.assign(Object.create(null), config.skills),
|
|
24203
24337
|
access: Object.assign(Object.create(null), config.access),
|
|
24204
24338
|
diagnostics: []
|
|
@@ -24512,6 +24646,7 @@ const load = gen(function* () {
|
|
|
24512
24646
|
return entry === void 0 ? void 0 : resolve(name, entry);
|
|
24513
24647
|
},
|
|
24514
24648
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
24649
|
+
...compiled.historyRemote !== void 0 ? { historyRemote: compiled.historyRemote } : {},
|
|
24515
24650
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
24516
24651
|
fingerprint: pinned?.fingerprint ?? configFingerprint(nodeSyncIo, paths, loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
|
|
24517
24652
|
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 +25347,9 @@ const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
|
25212
25347
|
* output rather than the exit code, so a genuinely rejected commit (a lint
|
|
25213
25348
|
* error, a commit-msg hook) still fails.
|
|
25214
25349
|
*/
|
|
25215
|
-
const commitAllowEmpty = (message, only) =>
|
|
25216
|
-
|
|
25350
|
+
const commitAllowEmpty = (message, only) => commitWordAllowEmpty(shellQuote(message), only);
|
|
25351
|
+
/** `commitAllowEmpty` over a message that is already a shell word. */
|
|
25352
|
+
const commitWordAllowEmpty = (m, only) => {
|
|
25217
25353
|
const paths = only === void 0 ? "" : ` -- ${shellQuote(only)}`;
|
|
25218
25354
|
return [
|
|
25219
25355
|
`if ! out=$(git commit --allow-empty -m ${m}${paths} 2>&1); then`,
|
|
@@ -25229,6 +25365,28 @@ const CHANGED_SINCE_LAND = "gtd: the working tree changed since `gtd land` — r
|
|
|
25229
25365
|
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
25230
25366
|
/** Commits `path` alone, as the worktree has it; every other pending change stays pending. */
|
|
25231
25367
|
const commitPath = (path, message) => `git add -A -- ${shellQuote(path)} || exit 1\n${commitAllowEmpty(message, path)}`;
|
|
25368
|
+
/**
|
|
25369
|
+
* A `run()` body that records its own failure instead of failing the beat: the
|
|
25370
|
+
* status and the last 50 output lines go to `path`, and the script exits zero
|
|
25371
|
+
* so the turn lands. The file is COMMITTED (and force-pushed with a history
|
|
25372
|
+
* remote), hence the cap.
|
|
25373
|
+
*/
|
|
25374
|
+
const failRestScript = (body, path) => {
|
|
25375
|
+
const p = shellQuote(path);
|
|
25376
|
+
const slash = path.lastIndexOf("/");
|
|
25377
|
+
return [
|
|
25378
|
+
`rm -f -- ${p}`,
|
|
25379
|
+
`gtd_run_status=0`,
|
|
25380
|
+
`gtd_run_out="$( {`,
|
|
25381
|
+
body,
|
|
25382
|
+
`} 2>&1 )" || gtd_run_status=$?`,
|
|
25383
|
+
`printf '%s\\n' "$gtd_run_out"`,
|
|
25384
|
+
`if [ "$gtd_run_status" -ne 0 ]; then`,
|
|
25385
|
+
` mkdir -p ${shellQuote(path.slice(0, slash))}`,
|
|
25386
|
+
` printf 'exit %s\\n%s\\n' "$gtd_run_status" "$(printf '%s\\n' "$gtd_run_out" | tail -n 50)" >${p}`,
|
|
25387
|
+
`fi`
|
|
25388
|
+
].join("\n");
|
|
25389
|
+
};
|
|
25232
25390
|
/** A pin beat's script. A failed `gtd bundle` leaves no half-written pin behind. */
|
|
25233
25391
|
const pinScript = (path) => {
|
|
25234
25392
|
const p = shellQuote(path);
|
|
@@ -25304,6 +25462,62 @@ const keepTurn = (refPrefix, message) => {
|
|
|
25304
25462
|
`git reset --hard HEAD`
|
|
25305
25463
|
].join("\n");
|
|
25306
25464
|
};
|
|
25465
|
+
/** `git switch -C <branch> [<start>]`: create or reset the branch, carrying pending changes over. */
|
|
25466
|
+
const switchBranchTo = (branch, start) => `git switch -q -C ${shellQuote(branch)}${start === void 0 ? "" : ` ${shellQuote(start)}`}`;
|
|
25467
|
+
/** `git switch <branch>`: an existing branch, carrying pending changes over. */
|
|
25468
|
+
const switchTo = (branch) => `git switch -q ${shellQuote(branch)}`;
|
|
25469
|
+
/** `switchTo` that, when pending changes block it, stays put and says so: the landing already happened. */
|
|
25470
|
+
const switchBack = (branch) => [
|
|
25471
|
+
`if ! git switch -q ${shellQuote(branch)} 2>/dev/null; then`,
|
|
25472
|
+
` printf '%s\\n' ${shellQuote(`gtd: pending changes keep HEAD off ${branch}; switch to it once they are committed or stashed`)}`,
|
|
25473
|
+
`fi`
|
|
25474
|
+
].join("\n");
|
|
25475
|
+
const deleteBranch = (branch) => `git branch -q -D ${shellQuote(branch)}`;
|
|
25476
|
+
/**
|
|
25477
|
+
* Force-pushes one ref to a remote. A failed push exits non-zero with the retry
|
|
25478
|
+
* on stderr: it runs after the closing commit has landed, so the commit stands.
|
|
25479
|
+
*/
|
|
25480
|
+
const pushRef = (remote, ref) => {
|
|
25481
|
+
const retry = `git push --force ${shellQuote(remote)} ${shellQuote(`${ref}:${ref}`)}`;
|
|
25482
|
+
return [
|
|
25483
|
+
`if ! ${retry}; then`,
|
|
25484
|
+
` printf '%s\\n' ${shellQuote(`gtd: pushing ${ref} failed — retry: ${retry}`)} >&2`,
|
|
25485
|
+
` exit 1`,
|
|
25486
|
+
`fi`
|
|
25487
|
+
].join("\n");
|
|
25488
|
+
};
|
|
25489
|
+
/**
|
|
25490
|
+
* A ship step's landing, in one block because the landing commit's
|
|
25491
|
+
* `Gtd-Shipped` trailer names the squash it makes first. The squash carries
|
|
25492
|
+
* the staged tree with `.gtd/` as `<branch>` has it, built in a scratch index; `<branch>` moves by
|
|
25493
|
+
* compare-and-swap against `parent`, so a branch moved since the decision
|
|
25494
|
+
* fails the script before the landing commits. `commit-tree` ignores
|
|
25495
|
+
* `commit.gpgSign`, so it is read here.
|
|
25496
|
+
*/
|
|
25497
|
+
const shipCommit = (branch, parent, shipMessage, historyRef, message) => [
|
|
25498
|
+
`gtd_ship_parent=${shellQuote(parent)}`,
|
|
25499
|
+
`gtd_ship_branch=${shellQuote(branch)}`,
|
|
25500
|
+
`gtd_ship_history=${shellQuote(historyRef)}`,
|
|
25501
|
+
`gtd_ship_message=${shellQuote(shipMessage)}`,
|
|
25502
|
+
`gtd_land_message=${shellQuote(message)}`,
|
|
25503
|
+
`git add -A || exit 1`,
|
|
25504
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
25505
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
25506
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
25507
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
25508
|
+
`gtd_index=$(git rev-parse --git-path gtd-ship-index) || exit 1`,
|
|
25509
|
+
`rm -f -- "$gtd_index"`,
|
|
25510
|
+
`GIT_INDEX_FILE="$gtd_index" git read-tree "$gtd_tree" || exit 1`,
|
|
25511
|
+
`GIT_INDEX_FILE="$gtd_index" git rm -r -q --cached --ignore-unmatch -- .gtd || exit 1`,
|
|
25512
|
+
`git ls-tree -r "$gtd_ship_parent" -- .gtd | GIT_INDEX_FILE="$gtd_index" git update-index --index-info || exit 1`,
|
|
25513
|
+
`gtd_ship_tree=$(GIT_INDEX_FILE="$gtd_index" git write-tree) || exit 1`,
|
|
25514
|
+
`rm -f -- "$gtd_index"`,
|
|
25515
|
+
`gtd_ship_trailers=$(printf "Gtd-History: %s\\nGtd-Tip: %s" "$gtd_ship_history" "$gtd_head")`,
|
|
25516
|
+
`gtd_shipped=$(git commit-tree "$gtd_sign" "$gtd_ship_tree" -p "$gtd_ship_parent" -m "$gtd_ship_message" -m "$gtd_ship_trailers") || exit 1`,
|
|
25517
|
+
`git update-ref -m "gtd: ship" "refs/heads/$gtd_ship_branch" "$gtd_shipped" "$gtd_ship_parent" || exit 1`,
|
|
25518
|
+
`gtd_message=$(printf "%s\\nGtd-Shipped: %s" "$gtd_land_message" "$gtd_shipped")`,
|
|
25519
|
+
commitWordAllowEmpty(`"$gtd_message"`)
|
|
25520
|
+
].join("\n");
|
|
25307
25521
|
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
25308
25522
|
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
25309
25523
|
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
@@ -25332,15 +25546,18 @@ const renderOutcome = (outcome) => {
|
|
|
25332
25546
|
case "note": return printfLine$1("%s\n", [shellQuote(outcome.text)]);
|
|
25333
25547
|
}
|
|
25334
25548
|
};
|
|
25335
|
-
const
|
|
25336
|
-
|
|
25337
|
-
|
|
25338
|
-
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
|
|
25549
|
+
const GIT_WRITES = {
|
|
25550
|
+
commitAll: (w) => commitAll(w.message),
|
|
25551
|
+
commitPath: (w) => commitPath(w.path, w.message),
|
|
25552
|
+
commitClean: (w) => commitClean(w.message),
|
|
25553
|
+
updateRef: (w) => updateRef(w.ref, w.target),
|
|
25554
|
+
keepTurn: (w) => keepTurn(w.ref, w.message),
|
|
25555
|
+
switchBranch: (w) => switchBranchTo(w.branch),
|
|
25556
|
+
switchTo: (w) => switchBack(w.branch),
|
|
25557
|
+
pushRef: (w) => pushRef(w.remote, w.ref),
|
|
25558
|
+
ship: (w) => shipCommit(w.branch, w.parent, w.shipMessage, w.historyRef, w.message)
|
|
25343
25559
|
};
|
|
25560
|
+
const renderGitWrite = (write) => GIT_WRITES[write.kind](write);
|
|
25344
25561
|
/**
|
|
25345
25562
|
* Wraps `command` so a non-zero exit prints `prompt` plus the command's
|
|
25346
25563
|
* captured output before propagating that exit code — mirrors `Emit.ts`'s
|
|
@@ -25722,15 +25939,19 @@ const validateScriptFor = (resolved, file) => gen(function* () {
|
|
|
25722
25939
|
});
|
|
25723
25940
|
// src/step/planStep.ts
|
|
25724
25941
|
/**
|
|
25725
|
-
*
|
|
25726
|
-
*
|
|
25727
|
-
* up — a tick is read-progress, never sign-off.
|
|
25942
|
+
* Clear ticks at the human review gate, a tick is read-progress. Keep them when
|
|
25943
|
+
* the next step is an agent turn: the agent reads them.
|
|
25728
25944
|
*/
|
|
25729
|
-
const uncheckStep = (snapshot) => snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
25945
|
+
const uncheckStep = (snapshot, keepTicks) => !keepTicks && snapshot.stepDef.actor === "human" && snapshot.stepDef.mode === "review" && snapshot.file !== void 0 ? [{
|
|
25730
25946
|
kind: "uncheck",
|
|
25731
25947
|
file: snapshot.file
|
|
25732
25948
|
}] : [];
|
|
25733
25949
|
const gitWriteOf = (landing, message) => {
|
|
25950
|
+
if (landing.ship !== void 0) return {
|
|
25951
|
+
kind: "ship",
|
|
25952
|
+
message,
|
|
25953
|
+
...landing.ship
|
|
25954
|
+
};
|
|
25734
25955
|
if (landing.only !== void 0) return {
|
|
25735
25956
|
kind: "commitPath",
|
|
25736
25957
|
path: landing.only,
|
|
@@ -25750,6 +25971,10 @@ const gitWriteOf = (landing, message) => {
|
|
|
25750
25971
|
message
|
|
25751
25972
|
};
|
|
25752
25973
|
};
|
|
25974
|
+
const writeIf = (value, write) => value === void 0 ? [] : [{
|
|
25975
|
+
kind: "gitWrite",
|
|
25976
|
+
write: write(value)
|
|
25977
|
+
}];
|
|
25753
25978
|
const planStep = (snapshot, opts = {}) => {
|
|
25754
25979
|
const landing = snapshot.landing;
|
|
25755
25980
|
if (landing.kind === "refusal") return {
|
|
@@ -25797,6 +26022,7 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
25797
26022
|
...opts.judge !== void 0 && opts.judge.length > 0 ? { judge: opts.judge } : {}
|
|
25798
26023
|
});
|
|
25799
26024
|
const subject = message.split("\n")[0];
|
|
26025
|
+
const historyRef = landing.keepHistory ?? landing.ship?.historyRef ?? landing.recordedHistory;
|
|
25800
26026
|
const outcome = landing.to === snapshot.state ? {
|
|
25801
26027
|
kind: "outcome",
|
|
25802
26028
|
outcome: {
|
|
@@ -25817,20 +26043,37 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
25817
26043
|
to: landing.to,
|
|
25818
26044
|
subject,
|
|
25819
26045
|
steps: [
|
|
25820
|
-
...
|
|
26046
|
+
...writeIf(landing.startBranch, (branch) => ({
|
|
26047
|
+
kind: "switchBranch",
|
|
26048
|
+
branch
|
|
26049
|
+
})),
|
|
26050
|
+
...uncheckStep(snapshot, landing.keepTicks === true),
|
|
25821
26051
|
{
|
|
25822
26052
|
kind: "gitWrite",
|
|
25823
26053
|
write: gitWriteOf(landing, message)
|
|
25824
26054
|
},
|
|
25825
|
-
...
|
|
25826
|
-
kind: "
|
|
25827
|
-
|
|
25828
|
-
|
|
25829
|
-
|
|
25830
|
-
|
|
26055
|
+
...writeIf(historyRef, (ref) => ({
|
|
26056
|
+
kind: "updateRef",
|
|
26057
|
+
ref,
|
|
26058
|
+
target: "HEAD"
|
|
26059
|
+
})),
|
|
26060
|
+
...writeIf(landing.leaveBranch, (branch) => ({
|
|
26061
|
+
kind: "switchTo",
|
|
26062
|
+
branch
|
|
26063
|
+
})),
|
|
26064
|
+
outcome,
|
|
26065
|
+
...landing.note !== void 0 ? [{
|
|
26066
|
+
kind: "outcome",
|
|
26067
|
+
outcome: {
|
|
26068
|
+
kind: "note",
|
|
26069
|
+
text: landing.note
|
|
25831
26070
|
}
|
|
25832
26071
|
}] : [],
|
|
25833
|
-
|
|
26072
|
+
...writeIf(historyRef !== void 0 ? landing.pushHistory : void 0, (remote) => ({
|
|
26073
|
+
kind: "pushRef",
|
|
26074
|
+
remote,
|
|
26075
|
+
ref: historyRef
|
|
26076
|
+
}))
|
|
25834
26077
|
]
|
|
25835
26078
|
};
|
|
25836
26079
|
};
|
|
@@ -25850,6 +26093,42 @@ const accessRefusal = (changes, write) => {
|
|
|
25850
26093
|
`allowed: ${write.length === 0 ? "(nothing)" : write.join(", ")}`
|
|
25851
26094
|
].join("\n");
|
|
25852
26095
|
};
|
|
26096
|
+
// src/step/startBranch.ts
|
|
26097
|
+
const GTD_BRANCH_PREFIX = "gtd/";
|
|
26098
|
+
/** `<b>` for a `gtd/<b>` branch name; `undefined` for any other. */
|
|
26099
|
+
const userBranchOf = (branch) => branch.startsWith("gtd/") ? branch.slice(4) : void 0;
|
|
26100
|
+
/**
|
|
26101
|
+
* Where a new process starts: on `gtd/<b>`, so `<b>` never carries its commits.
|
|
26102
|
+
* An existing `gtd/<b>` is reused only when its tip is contained in `<b>` or in
|
|
26103
|
+
* a kept history ref (a squash onto `<b>` never makes it an ancestor).
|
|
26104
|
+
*/
|
|
26105
|
+
const startBranchFor = (commandLabel) => gen(function* () {
|
|
26106
|
+
const git = yield* GitService;
|
|
26107
|
+
const current = yield* git.currentBranch();
|
|
26108
|
+
if (isNone(current) || current.value.startsWith("gtd/")) return { kind: "inPlace" };
|
|
26109
|
+
if (!(yield* git.hasCommits())) return { kind: "inPlace" };
|
|
26110
|
+
const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
|
|
26111
|
+
const existing = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
26112
|
+
if (isSome(existing)) {
|
|
26113
|
+
const head = yield* git.resolveRef("HEAD");
|
|
26114
|
+
let contained = yield* git.isAncestor(existing.value, head);
|
|
26115
|
+
if (!contained) {
|
|
26116
|
+
const kept = yield* git.listRefs(PROCESS_HISTORY_PREFIX);
|
|
26117
|
+
for (const k of kept) if (yield* git.isAncestor(existing.value, k.hash)) {
|
|
26118
|
+
contained = true;
|
|
26119
|
+
break;
|
|
26120
|
+
}
|
|
26121
|
+
}
|
|
26122
|
+
if (!contained) return {
|
|
26123
|
+
kind: "refusal",
|
|
26124
|
+
message: `${commandLabel}: branch ${branch} holds commits that are neither on ${current.value} nor in a kept history ref — merge or delete it first`
|
|
26125
|
+
};
|
|
26126
|
+
}
|
|
26127
|
+
return {
|
|
26128
|
+
kind: "switch",
|
|
26129
|
+
branch
|
|
26130
|
+
};
|
|
26131
|
+
});
|
|
25853
26132
|
// src/step/planStart.ts
|
|
25854
26133
|
const refusal = (message) => ({
|
|
25855
26134
|
kind: "refusal",
|
|
@@ -25890,29 +26169,42 @@ const planStart = (current, actor, start) => gen(function* () {
|
|
|
25890
26169
|
const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
|
|
25891
26170
|
if (typeof base === "object") return base;
|
|
25892
26171
|
const subject = formatSubject(actor, firstStep);
|
|
26172
|
+
const message = formatCommitMessage({
|
|
26173
|
+
actor,
|
|
26174
|
+
to: firstStep,
|
|
26175
|
+
workflow: name,
|
|
26176
|
+
...base !== void 0 ? { reviewBase: base } : {},
|
|
26177
|
+
vars
|
|
26178
|
+
});
|
|
26179
|
+
const onBranch = yield* startBranchFor(commandLabel);
|
|
26180
|
+
if (onBranch.kind === "refusal") return refusal(onBranch.message);
|
|
25893
26181
|
return {
|
|
25894
26182
|
kind: "start",
|
|
25895
26183
|
workflow: name,
|
|
25896
26184
|
subject,
|
|
25897
|
-
steps: [
|
|
25898
|
-
kind
|
|
25899
|
-
|
|
25900
|
-
|
|
25901
|
-
|
|
25902
|
-
|
|
25903
|
-
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
25907
|
-
|
|
25908
|
-
|
|
25909
|
-
|
|
25910
|
-
|
|
25911
|
-
|
|
25912
|
-
|
|
25913
|
-
|
|
26185
|
+
steps: [
|
|
26186
|
+
...onBranch.kind === "switch" ? [{
|
|
26187
|
+
kind: "gitWrite",
|
|
26188
|
+
write: {
|
|
26189
|
+
kind: "switchBranch",
|
|
26190
|
+
branch: onBranch.branch
|
|
26191
|
+
}
|
|
26192
|
+
}] : [],
|
|
26193
|
+
{
|
|
26194
|
+
kind: "gitWrite",
|
|
26195
|
+
write: {
|
|
26196
|
+
kind: "commitAll",
|
|
26197
|
+
message
|
|
26198
|
+
}
|
|
26199
|
+
},
|
|
26200
|
+
{
|
|
26201
|
+
kind: "outcome",
|
|
26202
|
+
outcome: {
|
|
26203
|
+
kind: "commit",
|
|
26204
|
+
subject
|
|
26205
|
+
}
|
|
25914
26206
|
}
|
|
25915
|
-
|
|
26207
|
+
]
|
|
25916
26208
|
};
|
|
25917
26209
|
});
|
|
25918
26210
|
/**
|
|
@@ -26156,14 +26448,32 @@ const runAtTip = (git, def, tip) => gen(function* () {
|
|
|
26156
26448
|
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
26157
26449
|
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
26158
26450
|
});
|
|
26159
|
-
|
|
26160
|
-
|
|
26161
|
-
|
|
26162
|
-
|
|
26451
|
+
/**
|
|
26452
|
+
* The process HEAD closes or sits inside — `gtd summary`'s run. On `<b>`, where
|
|
26453
|
+
* a finished process leaves HEAD, it is the one `gtd/<b>`'s tip closes, while
|
|
26454
|
+
* `<b>` holds nothing since: its start, or its last ship.
|
|
26455
|
+
*/
|
|
26163
26456
|
const summaryRun = gen(function* () {
|
|
26164
26457
|
const git = yield* GitService;
|
|
26165
26458
|
const def = (yield* (yield* ConfigService).load).workflow;
|
|
26166
|
-
|
|
26459
|
+
const head = (yield* git.subjectHistory(1, 0))[0];
|
|
26460
|
+
const left = head === void 0 ? void 0 : yield* leftProcessTip(git, head);
|
|
26461
|
+
return yield* runAtTip(git, def, left ?? head);
|
|
26462
|
+
});
|
|
26463
|
+
const leftProcessTip = (git, head) => gen(function* () {
|
|
26464
|
+
if (parseCommitMessage(head.subject).parsed !== void 0) return void 0;
|
|
26465
|
+
const current = yield* git.currentBranch();
|
|
26466
|
+
if (isNone(current) || userBranchOf(current.value) !== void 0) return void 0;
|
|
26467
|
+
const tip = yield* git.readRefOption(`refs/heads/${GTD_BRANCH_PREFIX}${current.value}`);
|
|
26468
|
+
if (isNone(tip)) return void 0;
|
|
26469
|
+
const message = yield* git.lastCommitMessage(tip.value);
|
|
26470
|
+
if (parseCommitMessage(message).parsed?.to === void 0) return void 0;
|
|
26471
|
+
const since = parseCommitMessage(yield* git.lastCommitMessage(head.hash)).tip ?? head.hash;
|
|
26472
|
+
if (!(yield* git.isAncestor(since, tip.value))) return void 0;
|
|
26473
|
+
return {
|
|
26474
|
+
hash: tip.value,
|
|
26475
|
+
subject: message.split("\n")[0]
|
|
26476
|
+
};
|
|
26167
26477
|
});
|
|
26168
26478
|
const commitTree = (workspace, hash) => {
|
|
26169
26479
|
let entries;
|
|
@@ -26291,13 +26601,21 @@ const promptDef = (common, request, skills, access) => ({
|
|
|
26291
26601
|
...optional("system", request.options.system),
|
|
26292
26602
|
...optional("allowEmpty", request.options.allowEmpty),
|
|
26293
26603
|
...optional("keep", request.options.keep),
|
|
26604
|
+
...optional("original", request.original),
|
|
26294
26605
|
...optional("skills", skills),
|
|
26295
26606
|
...optional("access", access)
|
|
26296
26607
|
});
|
|
26297
26608
|
const scriptDef = (common, request) => ({
|
|
26298
26609
|
...common,
|
|
26299
26610
|
kind: "script",
|
|
26300
|
-
content: request.body
|
|
26611
|
+
content: request.options.onFail === "rest" ? failRestScript(request.body, RUN_FAILED_PATH) : request.body,
|
|
26612
|
+
...optional("onFail", request.options.onFail)
|
|
26613
|
+
});
|
|
26614
|
+
const SHIP_CONTENT = noteOutcome("gtd: landing this step ships the process onto your branch");
|
|
26615
|
+
const shipDef = (common) => ({
|
|
26616
|
+
...common,
|
|
26617
|
+
kind: "script",
|
|
26618
|
+
content: SHIP_CONTENT
|
|
26301
26619
|
});
|
|
26302
26620
|
const judgeDef = (common, request, step) => {
|
|
26303
26621
|
const message = request.options.message ?? DEFAULT_JUDGE_MESSAGE;
|
|
@@ -26321,6 +26639,7 @@ const stepDefOf = (step) => {
|
|
|
26321
26639
|
switch (request.kind) {
|
|
26322
26640
|
case "agent": return promptDef(common, request, step.skills, step.access);
|
|
26323
26641
|
case "run": return scriptDef(common, request);
|
|
26642
|
+
case "ship": return shipDef(common);
|
|
26324
26643
|
case "judge": return judgeDef(common, request, step);
|
|
26325
26644
|
case "human": return messageDef(common, request, step);
|
|
26326
26645
|
}
|
|
@@ -26474,6 +26793,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
26474
26793
|
path: e.path
|
|
26475
26794
|
})) : [];
|
|
26476
26795
|
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`);
|
|
26796
|
+
const shipBlocked = ref === void 0 ? yield* shipBlockedAt(setup, pin, step, changes) : void 0;
|
|
26477
26797
|
return {
|
|
26478
26798
|
def,
|
|
26479
26799
|
workflowName: resolved.name,
|
|
@@ -26492,7 +26812,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
26492
26812
|
memoryResumed: memory.resumed,
|
|
26493
26813
|
hints: hintsOf(stepDef),
|
|
26494
26814
|
cost: costTotals(run.costEntries),
|
|
26495
|
-
setup
|
|
26815
|
+
setup,
|
|
26816
|
+
...shipBlocked !== void 0 ? { shipBlocked } : {}
|
|
26496
26817
|
};
|
|
26497
26818
|
});
|
|
26498
26819
|
const currentRest = restAt(void 0);
|
|
@@ -26621,15 +26942,20 @@ const renderRest = (rest) => succeed$1({
|
|
|
26621
26942
|
kind: rest.stepDef.kind,
|
|
26622
26943
|
content: rest.stepDef.content,
|
|
26623
26944
|
...rest.hints,
|
|
26945
|
+
...failedRunStalled(rest) ? { failure: rest.setup.workspace.readSync(".gtd/run-failed.txt") ?? "" } : {},
|
|
26946
|
+
...rest.shipBlocked !== void 0 ? { blocked: rest.shipBlocked } : {},
|
|
26624
26947
|
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
26625
26948
|
memoryResumed: rest.memoryResumed,
|
|
26949
|
+
...rest.stepDef.original !== void 0 ? { original: rest.stepDef.original } : {},
|
|
26626
26950
|
...rest.pin !== void 0 ? { pin: rest.pin } : {}
|
|
26627
26951
|
});
|
|
26628
26952
|
/**
|
|
26629
26953
|
* Derived, restart-proof stall detection: the tree is clean, HEAD is an empty
|
|
26630
26954
|
* attempt at this very agent rest, and another dispatch would repeat it.
|
|
26631
26955
|
*/
|
|
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;
|
|
26956
|
+
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;
|
|
26957
|
+
/** A `run(…, { onFail: "rest" })` step whose last turn recorded a failure, on a clean tree. */
|
|
26958
|
+
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
26959
|
/** 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
26960
|
const noProcessUnderway = (rest) => rest.pin === void 0 && !underwayAt(rest.run, rest.step, rest.def);
|
|
26635
26961
|
/** `idle` means exactly one thing: no process underway, clean tree. */
|
|
@@ -26667,7 +26993,7 @@ const cleanLanding = (rest) => {
|
|
|
26667
26993
|
settled: false
|
|
26668
26994
|
};
|
|
26669
26995
|
};
|
|
26670
|
-
/** At the human review gate the pending tree lands with every tick cleared
|
|
26996
|
+
/** At the human review gate the pending tree lands with every tick cleared; an agent-bound landing keeps them only if the unrewritten tree replays to the same step. */
|
|
26671
26997
|
const reviewGateRewrite = (def) => {
|
|
26672
26998
|
const reviewFormat = steeringFormatFor("review");
|
|
26673
26999
|
if (!isHumanReviewGate(def) || def.file === void 0 || reviewFormat === void 0) return;
|
|
@@ -26676,6 +27002,15 @@ const reviewGateRewrite = (def) => {
|
|
|
26676
27002
|
apply: (content) => reviewFormat.clearTicks(content)
|
|
26677
27003
|
};
|
|
26678
27004
|
};
|
|
27005
|
+
/** Whether the pending review file differs from HEAD's once both have their ticks cleared. */
|
|
27006
|
+
const reviewEditedBeyondTicks = (rest) => {
|
|
27007
|
+
const rewrite = reviewGateRewrite(rest.stepDef);
|
|
27008
|
+
if (rewrite === void 0) return false;
|
|
27009
|
+
const pending = rest.setup.workspace.readSync(rewrite.path);
|
|
27010
|
+
const committed = rest.setup.workspace.readCommittedSync(rewrite.path);
|
|
27011
|
+
if (pending === void 0 || committed === void 0) return true;
|
|
27012
|
+
return rewrite.apply(pending) !== rewrite.apply(committed);
|
|
27013
|
+
};
|
|
26679
27014
|
const multilineRefusal = (vars) => {
|
|
26680
27015
|
const multiline = multilineSetting(vars);
|
|
26681
27016
|
return multiline === void 0 ? void 0 : {
|
|
@@ -26794,7 +27129,7 @@ const outcomeMarks = (outcome) => outcome.kind === "rest" || outcome.kind === "e
|
|
|
26794
27129
|
* replay with the working tree as the pending turn. The target step is
|
|
26795
27130
|
* computed by replaying, never matched.
|
|
26796
27131
|
*/
|
|
26797
|
-
const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
27132
|
+
const decideLanding = (rest, verdicts, branch, approver, shipped) => gen(function* () {
|
|
26798
27133
|
if (rest.pin !== void 0) return pinLanding(rest, rest.pin, branch);
|
|
26799
27134
|
const unapproved = approvalRefusal(rest, approver);
|
|
26800
27135
|
if (unapproved !== void 0) return unapproved;
|
|
@@ -26807,30 +27142,218 @@ const decideLanding = (rest, verdicts, branch, approver) => gen(function* () {
|
|
|
26807
27142
|
message
|
|
26808
27143
|
};
|
|
26809
27144
|
}
|
|
27145
|
+
const landing = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef));
|
|
27146
|
+
if (rest.stepDef.onFail === "rest" && landing.read(".gtd/run-failed.txt") !== void 0) return {
|
|
27147
|
+
kind: "attempt",
|
|
27148
|
+
subject: formatSubject(rest.actor, rest.state)
|
|
27149
|
+
};
|
|
26810
27150
|
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
26811
|
-
tree:
|
|
26812
|
-
...verdicts !== void 0 ? { verdicts } : {}
|
|
27151
|
+
tree: landing,
|
|
27152
|
+
...verdicts !== void 0 ? { verdicts } : {},
|
|
27153
|
+
...shipped !== void 0 ? { shipped } : {}
|
|
26813
27154
|
}));
|
|
26814
27155
|
const to = yield* landingTarget(outcome, rest);
|
|
26815
27156
|
if (typeof to !== "string") return to;
|
|
26816
|
-
|
|
27157
|
+
const landed = landingTo(rest, to, branch, approver, outcomeMarks(outcome));
|
|
27158
|
+
if (landed.kind !== "commit" || !isHumanReviewGate(rest.stepDef) || outcome.kind !== "rest" || outcome.rest.request.kind !== "agent" || !reviewEditedBeyondTicks(rest)) return landed;
|
|
27159
|
+
const ticked = yield* promise(() => replayFor(rest.setup, {
|
|
27160
|
+
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), void 0),
|
|
27161
|
+
...verdicts !== void 0 ? { verdicts } : {},
|
|
27162
|
+
...shipped !== void 0 ? { shipped } : {}
|
|
27163
|
+
}));
|
|
27164
|
+
return ticked.kind === "rest" && ticked.rest.name === outcome.rest.name ? {
|
|
27165
|
+
...landed,
|
|
27166
|
+
keepTicks: true
|
|
27167
|
+
} : landed;
|
|
27168
|
+
});
|
|
27169
|
+
/** The user's branch: `<b>` for a process on `gtd/<b>` while `<b>` still exists; any other branch as is. */
|
|
27170
|
+
const userBranch = (git) => gen(function* () {
|
|
27171
|
+
const current = yield* git.currentBranch();
|
|
27172
|
+
const name = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
27173
|
+
if (name === void 0) return current;
|
|
27174
|
+
const there = yield* git.readRefOption(`refs/heads/${name}`);
|
|
27175
|
+
return isSome(there) ? some(name) : current;
|
|
27176
|
+
});
|
|
27177
|
+
/** `<b>` for a process on `gtd/<b>` while `<b>` exists: where a finishing landing leaves HEAD. */
|
|
27178
|
+
const returnBranch = gen(function* () {
|
|
27179
|
+
const git = yield* GitService;
|
|
27180
|
+
const current = yield* git.currentBranch();
|
|
27181
|
+
const name = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
27182
|
+
if (name === void 0) return void 0;
|
|
27183
|
+
return isSome(yield* git.readRefOption(`refs/heads/${name}`)) ? name : void 0;
|
|
27184
|
+
});
|
|
27185
|
+
/** A starting landing moves onto `gtd/<b>` first. */
|
|
27186
|
+
const withBranchAndPush = (landing) => gen(function* () {
|
|
27187
|
+
if (landing.kind !== "commit") return landing;
|
|
27188
|
+
let out = landing;
|
|
27189
|
+
if (landing.spec.vars !== void 0) {
|
|
27190
|
+
const start = yield* startBranchFor("gtd land");
|
|
27191
|
+
if (start.kind === "refusal") return {
|
|
27192
|
+
kind: "refusal",
|
|
27193
|
+
message: start.message
|
|
27194
|
+
};
|
|
27195
|
+
if (start.kind === "switch") out = {
|
|
27196
|
+
...out,
|
|
27197
|
+
startBranch: start.branch
|
|
27198
|
+
};
|
|
27199
|
+
}
|
|
27200
|
+
return yield* withHistory(out);
|
|
27201
|
+
});
|
|
27202
|
+
/** A closing landing switches back to `<b>`; it and a shipping one push their history ref. */
|
|
27203
|
+
const withHistory = (landing) => gen(function* () {
|
|
27204
|
+
let out = landing;
|
|
27205
|
+
const back = landing.keepHistory === void 0 ? void 0 : yield* returnBranch;
|
|
27206
|
+
if (back !== void 0) out = {
|
|
27207
|
+
...out,
|
|
27208
|
+
leaveBranch: back
|
|
27209
|
+
};
|
|
27210
|
+
if (landing.keepHistory === void 0 && landing.ship === void 0 && landing.recordedHistory === void 0) return out;
|
|
27211
|
+
const remote = (yield* (yield* ConfigService).load).historyRemote;
|
|
27212
|
+
return remote === void 0 ? out : {
|
|
27213
|
+
...out,
|
|
27214
|
+
pushHistory: remote
|
|
27215
|
+
};
|
|
26817
27216
|
});
|
|
26818
27217
|
/** 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)));
|
|
27218
|
+
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)));
|
|
27219
|
+
const snapshotOf = (rest, landing) => ({
|
|
27220
|
+
stepDef: rest.stepDef,
|
|
27221
|
+
state: rest.state,
|
|
27222
|
+
actor: rest.actor,
|
|
27223
|
+
changes: rest.changes,
|
|
27224
|
+
file: rest.hints.file,
|
|
27225
|
+
landing
|
|
27226
|
+
});
|
|
27227
|
+
const refusedSnapshot = (rest, message) => snapshotOf(rest, {
|
|
27228
|
+
kind: "refusal",
|
|
27229
|
+
message
|
|
27230
|
+
});
|
|
27231
|
+
const SHIPPED_STAND_IN = "0".repeat(40);
|
|
27232
|
+
const SHIP_EXCLUDED = ".gtd/";
|
|
27233
|
+
/** `tree` with file modes read from `ref` (the working tree when `undefined`), for the ship decision only: reading them costs a git call. */
|
|
27234
|
+
const withModes = (workspace, tree, ref) => {
|
|
27235
|
+
let modes;
|
|
27236
|
+
return {
|
|
27237
|
+
...tree,
|
|
27238
|
+
mode: (path) => (modes ??= workspace.modeSync(ref)).get(path)
|
|
27239
|
+
};
|
|
27240
|
+
};
|
|
27241
|
+
const shippedPaths = (before, after) => {
|
|
27242
|
+
const diff = diffTrees$1(before, after);
|
|
27243
|
+
return [
|
|
27244
|
+
...diff.added,
|
|
27245
|
+
...diff.modified,
|
|
27246
|
+
...diff.deleted
|
|
27247
|
+
].filter((path) => !path.startsWith(SHIP_EXCLUDED));
|
|
27248
|
+
};
|
|
27249
|
+
const noTarget = (why) => ({
|
|
27250
|
+
kind: "skip",
|
|
27251
|
+
note: `gtd: no branch to ship to: ${why}`
|
|
27252
|
+
});
|
|
27253
|
+
/** `<b>` and its tip, for a process on `gtd/<b>`; why there is none otherwise. */
|
|
27254
|
+
const shipTarget = (git) => gen(function* () {
|
|
27255
|
+
const current = yield* git.currentBranch();
|
|
27256
|
+
if (isNone(current)) return noTarget("HEAD is detached");
|
|
27257
|
+
const branch = userBranchOf(current.value);
|
|
27258
|
+
if (branch === void 0) return noTarget(`${current.value} is no gtd/ branch`);
|
|
27259
|
+
if (userBranchOf(branch) !== void 0) return noTarget(`${current.value} records no user branch`);
|
|
27260
|
+
const parent = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
27261
|
+
if (isNone(parent)) return noTarget(`${branch} does not exist`);
|
|
27262
|
+
return {
|
|
27263
|
+
kind: "ship",
|
|
27264
|
+
branch,
|
|
27265
|
+
parent: parent.value
|
|
27266
|
+
};
|
|
27267
|
+
});
|
|
27268
|
+
/**
|
|
27269
|
+
* The paths `<b>` changed outside this process that shipping `tree` would
|
|
27270
|
+
* revert. A path the working tree changed since HEAD counts as brought in:
|
|
27271
|
+
* whether it carries `<b>`'s change is the person's call, not a merge gtd redoes.
|
|
27272
|
+
*/
|
|
27273
|
+
const revertedPaths = (git, workspace, parent, tree, changed, treeRef) => gen(function* () {
|
|
27274
|
+
const head = yield* git.resolveRef("HEAD");
|
|
27275
|
+
const shipped = parseCommitMessage(yield* git.lastCommitMessage(parent)).tip;
|
|
27276
|
+
if (shipped !== void 0 && (yield* git.isAncestor(shipped, head))) return [];
|
|
27277
|
+
const fork = yield* git.mergeBase(parent, head);
|
|
27278
|
+
const foreign = isNone(fork) ? [...changed] : shippedPaths(withModes(workspace, commitTree(workspace, fork.value), fork.value), withModes(workspace, commitTree(workspace, parent), parent));
|
|
27279
|
+
const committed = withModes(workspace, commitTree(workspace, head), head);
|
|
27280
|
+
const moded = withModes(workspace, tree, treeRef);
|
|
27281
|
+
return foreign.filter((path) => changed.has(path) && moded.read(path) === committed.read(path) && moded.mode(path) === committed.mode(path));
|
|
27282
|
+
});
|
|
27283
|
+
/**
|
|
27284
|
+
* Whether landing a ship step with `tree` would ship, and onto what. The squash
|
|
27285
|
+
* replaces the user's branch's tree with `tree`, so it is blocked while that
|
|
27286
|
+
* branch carries a change `tree` would revert: one made since the last ship
|
|
27287
|
+
* (whose `Gtd-Tip` HEAD descends from), or since it forked from HEAD.
|
|
27288
|
+
*/
|
|
27289
|
+
const decideShip = (workspace, tree, treeRef) => gen(function* () {
|
|
27290
|
+
const git = yield* GitService;
|
|
27291
|
+
const target = yield* shipTarget(git);
|
|
27292
|
+
if (target.kind !== "ship") return target;
|
|
27293
|
+
const { branch, parent } = target;
|
|
27294
|
+
const changed = new Set(shippedPaths(withModes(workspace, commitTree(workspace, parent), parent), withModes(workspace, tree, treeRef)));
|
|
27295
|
+
const pending = parseCommitMessage(yield* git.lastCommitMessage(parent)).tip;
|
|
27296
|
+
if (changed.size === 0 && pending !== void 0 && pending === (yield* git.resolveRef("HEAD"))) return {
|
|
27297
|
+
kind: "recorded",
|
|
27298
|
+
hash: parent
|
|
27299
|
+
};
|
|
27300
|
+
const reverted = yield* revertedPaths(git, workspace, parent, tree, changed, treeRef);
|
|
27301
|
+
if (reverted.length > 0) return {
|
|
27302
|
+
kind: "blocked",
|
|
27303
|
+
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`
|
|
27304
|
+
};
|
|
27305
|
+
return changed.size === 0 ? {
|
|
27306
|
+
kind: "skip",
|
|
27307
|
+
note: `gtd: nothing to ship: ${branch} already has this tree`
|
|
27308
|
+
} : target;
|
|
27309
|
+
});
|
|
27310
|
+
const shipBlockedAt = (setup, pin, step, changes) => gen(function* () {
|
|
27311
|
+
if (pin !== void 0 || step.request.kind !== "ship" || changes.length > 0) return void 0;
|
|
27312
|
+
const head = yield* (yield* GitService).resolveRef("HEAD");
|
|
27313
|
+
const decision = yield* decideShip(setup.workspace, commitTree(setup.workspace, head), head);
|
|
27314
|
+
return decision.kind === "blocked" ? decision.message : void 0;
|
|
27315
|
+
});
|
|
27316
|
+
const shipSnapshot = (rest, message, verdicts, branch, approver) => gen(function* () {
|
|
27317
|
+
const tree = pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), void 0);
|
|
27318
|
+
const decision = yield* decideShip(rest.setup.workspace, tree, void 0);
|
|
27319
|
+
if (decision.kind === "blocked") return refusedSnapshot(rest, decision.message);
|
|
27320
|
+
const shipping = decision.kind === "ship";
|
|
27321
|
+
const decided = yield* decideLanding(rest, verdicts, branch, approver, decision.kind === "recorded" ? decision.hash : shipping ? SHIPPED_STAND_IN : void 0);
|
|
27322
|
+
if (decided.kind !== "commit") return snapshotOf(rest, decided);
|
|
27323
|
+
if (decision.kind === "recorded") {
|
|
27324
|
+
const recorded = {
|
|
27325
|
+
...decided,
|
|
27326
|
+
spec: {
|
|
27327
|
+
...decided.spec,
|
|
27328
|
+
shipped: decision.hash
|
|
27329
|
+
},
|
|
27330
|
+
recordedHistory: processHistoryRef(rest.run.startHash)
|
|
27331
|
+
};
|
|
27332
|
+
return snapshotOf(rest, yield* withBranchAndPush(recorded));
|
|
27333
|
+
}
|
|
27334
|
+
const landing = shipping ? {
|
|
27335
|
+
...decided,
|
|
27336
|
+
ship: {
|
|
27337
|
+
branch: decision.branch,
|
|
27338
|
+
parent: decision.parent,
|
|
27339
|
+
shipMessage: message,
|
|
27340
|
+
historyRef: processHistoryRef(rest.run.startHash)
|
|
27341
|
+
}
|
|
27342
|
+
} : {
|
|
27343
|
+
...decided,
|
|
27344
|
+
note: decision.note
|
|
27345
|
+
};
|
|
27346
|
+
return snapshotOf(rest, yield* withBranchAndPush(landing));
|
|
27347
|
+
});
|
|
26820
27348
|
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
26821
27349
|
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
26822
27350
|
const git = yield* GitService;
|
|
26823
|
-
const branch = yield* git
|
|
27351
|
+
const branch = yield* userBranch(git);
|
|
26824
27352
|
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
|
-
};
|
|
27353
|
+
const request = rest.step.request;
|
|
27354
|
+
if (rest.pin === void 0 && request.kind === "ship") return yield* shipSnapshot(rest, request.message, verdicts, branch, approver);
|
|
27355
|
+
const decided = yield* decideLanding(rest, verdicts, branch, approver);
|
|
27356
|
+
return snapshotOf(rest, yield* withBranchAndPush(decided));
|
|
26834
27357
|
});
|
|
26835
27358
|
const humanCommitsOf = (trace, entryCommit) => trace.filter((entry) => entry.actor === "human" && entry.hash !== entryCommit).map((entry) => ({
|
|
26836
27359
|
hash: entry.hash,
|
|
@@ -27957,6 +28480,32 @@ const runStartListCommand = (json, out) => gen(function* () {
|
|
|
27957
28480
|
out.write([...workflows.map((name) => `workflow ${name}\n`), ...doors.map(([n, d]) => `door ${doorSynopsis(n, d)} → ${d.workflow}\n`)].join(""));
|
|
27958
28481
|
});
|
|
27959
28482
|
/**
|
|
28483
|
+
* Back on `<b>` when the process ran on `gtd/<b>` and `<b>` moved only by its
|
|
28484
|
+
* ships (whose `Gtd-Tip` the tip descends from): the rewind stops at the last
|
|
28485
|
+
* ship, so the uncommitted changes are what it left unshipped.
|
|
28486
|
+
*/
|
|
28487
|
+
const abandonTarget = (current, tip, startParent) => gen(function* () {
|
|
28488
|
+
const git = yield* GitService;
|
|
28489
|
+
const userBranch = isSome(current) ? userBranchOf(current.value) : void 0;
|
|
28490
|
+
const userTip = userBranch === void 0 ? none() : yield* git.readRefOption(`refs/heads/${userBranch}`);
|
|
28491
|
+
if (userBranch === void 0 || isNone(userTip)) return {
|
|
28492
|
+
leaves: void 0,
|
|
28493
|
+
rewindTo: startParent
|
|
28494
|
+
};
|
|
28495
|
+
if (userTip.value === startParent) return {
|
|
28496
|
+
leaves: userBranch,
|
|
28497
|
+
rewindTo: startParent
|
|
28498
|
+
};
|
|
28499
|
+
const shipped = parseCommitMessage(yield* git.lastCommitMessage(userTip.value)).tip;
|
|
28500
|
+
return shipped !== void 0 && (yield* git.isAncestor(shipped, tip)) ? {
|
|
28501
|
+
leaves: userBranch,
|
|
28502
|
+
rewindTo: userTip.value
|
|
28503
|
+
} : {
|
|
28504
|
+
leaves: void 0,
|
|
28505
|
+
rewindTo: startParent
|
|
28506
|
+
};
|
|
28507
|
+
});
|
|
28508
|
+
/**
|
|
27960
28509
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
27961
28510
|
* `git reset --mixed` to the process's start commit leaves every turn
|
|
27962
28511
|
* commit's content in the working tree.
|
|
@@ -27971,7 +28520,8 @@ const runStartListCommand = (json, out) => gen(function* () {
|
|
|
27971
28520
|
*/
|
|
27972
28521
|
const runAbandonCommand = (json, out) => gen(function* () {
|
|
27973
28522
|
const git = yield* GitService;
|
|
27974
|
-
const
|
|
28523
|
+
const config = yield* (yield* ConfigService).load;
|
|
28524
|
+
const initial = config.workflow.initial;
|
|
27975
28525
|
const run = yield* currentRun;
|
|
27976
28526
|
if (run.trace.length === 0) {
|
|
27977
28527
|
const required = emitScripts([{
|
|
@@ -27983,6 +28533,10 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
27983
28533
|
const restState = run.trace[run.trace.length - 1].state;
|
|
27984
28534
|
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
28535
|
const tip = yield* git.resolveRef("HEAD");
|
|
28536
|
+
const historyRef = processHistoryRef(run.startHash);
|
|
28537
|
+
const current = yield* git.currentBranch();
|
|
28538
|
+
const { leaves, rewindTo } = yield* abandonTarget(current, tip, run.startParentHash);
|
|
28539
|
+
const remote = config.historyRemote;
|
|
27986
28540
|
const steps = [
|
|
27987
28541
|
{
|
|
27988
28542
|
kind: "gitWrite",
|
|
@@ -27990,21 +28544,60 @@ const runAbandonCommand = (json, out) => gen(function* () {
|
|
|
27990
28544
|
},
|
|
27991
28545
|
{
|
|
27992
28546
|
kind: "gitWrite",
|
|
27993
|
-
command:
|
|
28547
|
+
command: updateRef(historyRef, tip)
|
|
28548
|
+
},
|
|
28549
|
+
{
|
|
28550
|
+
kind: "gitWrite",
|
|
28551
|
+
command: mixedResetTo(rewindTo)
|
|
27994
28552
|
},
|
|
27995
28553
|
{
|
|
27996
28554
|
kind: "gitWrite",
|
|
27997
28555
|
command: unpinScript(PIN_PATH)
|
|
27998
28556
|
},
|
|
28557
|
+
...leaves !== void 0 ? [
|
|
28558
|
+
{
|
|
28559
|
+
kind: "gitWrite",
|
|
28560
|
+
command: switchTo(leaves)
|
|
28561
|
+
},
|
|
28562
|
+
{
|
|
28563
|
+
kind: "gitWrite",
|
|
28564
|
+
command: deleteBranch(getOrThrow$1(current))
|
|
28565
|
+
},
|
|
28566
|
+
{
|
|
28567
|
+
kind: "gitWrite",
|
|
28568
|
+
command: updateRef(RESUME_BRANCH_REF, tip)
|
|
28569
|
+
}
|
|
28570
|
+
] : [{
|
|
28571
|
+
kind: "gitWrite",
|
|
28572
|
+
command: deleteRef(RESUME_BRANCH_REF)
|
|
28573
|
+
}],
|
|
27999
28574
|
{
|
|
28000
28575
|
kind: "outcome",
|
|
28001
|
-
command: abandonedOutcome(restState,
|
|
28002
|
-
}
|
|
28576
|
+
command: abandonedOutcome(restState, rewindTo, initial)
|
|
28577
|
+
},
|
|
28578
|
+
...remote !== void 0 ? [{
|
|
28579
|
+
kind: "gitWrite",
|
|
28580
|
+
command: pushRef(remote, historyRef)
|
|
28581
|
+
}] : []
|
|
28003
28582
|
];
|
|
28004
28583
|
const required = emitScripts(steps).required;
|
|
28005
28584
|
yield* writeScript(json, out, combinedScript(required, ""));
|
|
28006
28585
|
});
|
|
28007
28586
|
/**
|
|
28587
|
+
* `gtd/<b>` when the abandon left it for `<b>` and HEAD is still on `<b>`. A
|
|
28588
|
+
* `gtd/<b>` made since is reset only when the retained tip contains it — the
|
|
28589
|
+
* `-C` switch would orphan anything else.
|
|
28590
|
+
*/
|
|
28591
|
+
const resumeBranch = (git, tip) => gen(function* () {
|
|
28592
|
+
if (isNone(yield* git.readRefOption("refs/worktree/gtd/history-branch"))) return void 0;
|
|
28593
|
+
const current = yield* git.currentBranch();
|
|
28594
|
+
if (isNone(current) || current.value.startsWith("gtd/")) return void 0;
|
|
28595
|
+
const branch = `${GTD_BRANCH_PREFIX}${current.value}`;
|
|
28596
|
+
const existing = yield* git.readRefOption(`refs/heads/${branch}`);
|
|
28597
|
+
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`));
|
|
28598
|
+
return branch;
|
|
28599
|
+
});
|
|
28600
|
+
/**
|
|
28008
28601
|
* `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
|
|
28009
28602
|
*
|
|
28010
28603
|
* Guarded by `restorability` so it never discards work it didn't create:
|
|
@@ -28018,18 +28611,24 @@ const runRestoreCommand = (json, out) => gen(function* () {
|
|
|
28018
28611
|
if (isNone(retained)) return yield* fail$4(/* @__PURE__ */ new Error("gtd restore: no retained history to restore."));
|
|
28019
28612
|
const tip = retained.value;
|
|
28020
28613
|
const headHash = yield* git.resolveRef("HEAD");
|
|
28021
|
-
const
|
|
28614
|
+
const shippedTip = parseCommitMessage(yield* git.lastCommitMessage(headHash)).tip;
|
|
28615
|
+
const check = shippedTip !== void 0 && (yield* git.isAncestor(shippedTip, tip)) ? { ok: true } : yield* restorability(git, headHash, tip);
|
|
28022
28616
|
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
28617
|
const after = yield* restAt(tip);
|
|
28618
|
+
const resumeOn = yield* resumeBranch(git, tip);
|
|
28024
28619
|
const steps = [
|
|
28025
28620
|
{
|
|
28026
28621
|
kind: "gitWrite",
|
|
28027
|
-
command: hardResetTo(tip)
|
|
28622
|
+
command: resumeOn === void 0 ? hardResetTo(tip) : switchBranchTo(resumeOn, tip)
|
|
28028
28623
|
},
|
|
28029
28624
|
{
|
|
28030
28625
|
kind: "gitWrite",
|
|
28031
28626
|
command: deleteRef(HISTORY_REF)
|
|
28032
28627
|
},
|
|
28628
|
+
{
|
|
28629
|
+
kind: "gitWrite",
|
|
28630
|
+
command: deleteRef(RESUME_BRANCH_REF)
|
|
28631
|
+
},
|
|
28033
28632
|
{
|
|
28034
28633
|
kind: "outcome",
|
|
28035
28634
|
command: restoredOutcome(tip, after.state)
|
|
@@ -28249,6 +28848,18 @@ const computeStatusChanges = (changes) => changes.map((change) => ({
|
|
|
28249
28848
|
}));
|
|
28250
28849
|
/** The step the pending change would land the process at. */
|
|
28251
28850
|
const computeNextMatch = (rest) => map(rest.changes.length === 0 ? succeed$1(void 0) : previewLanding(rest), (target) => target === void 0 ? null : { target });
|
|
28851
|
+
/** A validator retry's content is the correction; a session that cannot resume needs the original prompt too, and one that can hands a driver that loses it `fresh`. */
|
|
28852
|
+
const retryContent = (kind, rendered, resumes) => {
|
|
28853
|
+
if (kind !== "prompt" || rendered.original === void 0) return { rendered };
|
|
28854
|
+
const joined = `${rendered.original}\n\n${rendered.content}`;
|
|
28855
|
+
return resumes ? {
|
|
28856
|
+
rendered,
|
|
28857
|
+
fresh: joined
|
|
28858
|
+
} : { rendered: {
|
|
28859
|
+
...rendered,
|
|
28860
|
+
content: joined
|
|
28861
|
+
} };
|
|
28862
|
+
};
|
|
28252
28863
|
/** Everything one beat needs beyond the resolved rest itself, gathered once so plain/`--json` can never describe different rests for the same beat — built as `wire`'s `Demand` (what to do) and `BeatStatus` (what no driver branches on), then flattened by `beatDocument` into the one wire-shaped object plain/`--json` both render from. */
|
|
28253
28864
|
const gatherBeatDocument = (rest, rendered) => gen(function* () {
|
|
28254
28865
|
const kind = restBeatKind(rest);
|
|
@@ -28256,9 +28867,11 @@ const gatherBeatDocument = (rest, rendered) => gen(function* () {
|
|
|
28256
28867
|
const resolvedValidate = kind === "prompt" ? yield* resolveValidateScript(rest).pipe(catch_(() => succeed$1(void 0))) : void 0;
|
|
28257
28868
|
const validate = resolvedValidate !== void 0 && resolvedValidate.script.length > 0 ? resolvedValidate.script : void 0;
|
|
28258
28869
|
const log = yield* loopLogPath;
|
|
28870
|
+
const retry = retryContent(kind, rendered, session?.resume === true);
|
|
28259
28871
|
const demand = demandOf({
|
|
28260
|
-
rendered,
|
|
28872
|
+
rendered: retry.rendered,
|
|
28261
28873
|
kind,
|
|
28874
|
+
...retry.fresh !== void 0 ? { fresh: retry.fresh } : {},
|
|
28262
28875
|
...session !== void 0 ? { session: {
|
|
28263
28876
|
id: session.sessionId,
|
|
28264
28877
|
resume: session.resume
|
|
@@ -28744,7 +29357,7 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
|
|
|
28744
29357
|
},
|
|
28745
29358
|
group: "Start and stop",
|
|
28746
29359
|
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.
|
|
29360
|
+
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
29361
|
|
|
28749
29362
|
<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
29363
|
|
|
@@ -28758,7 +29371,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
28758
29371
|
arity: "none",
|
|
28759
29372
|
group: "Start and stop",
|
|
28760
29373
|
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
|
|
29374
|
+
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
29375
|
|
|
28763
29376
|
s=$(gtd abandon --json=script) && sh -c "$s"`
|
|
28764
29377
|
},
|
|
@@ -28768,7 +29381,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
28768
29381
|
arity: "none",
|
|
28769
29382
|
group: "Start and stop",
|
|
28770
29383
|
summary: "Bring back the last abandoned process",
|
|
28771
|
-
details: `Bring back the process the last \`gtd abandon\` stopped: the printed script
|
|
29384
|
+
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
29385
|
},
|
|
28773
29386
|
{
|
|
28774
29387
|
token: "repin",
|
|
@@ -28786,7 +29399,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
28786
29399
|
arity: "none",
|
|
28787
29400
|
group: "Inspect",
|
|
28788
29401
|
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.
|
|
29402
|
+
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
29403
|
|
|
28791
29404
|
Refuses when the workflow declares no summary template, or when the process has no commits.`
|
|
28792
29405
|
},
|
|
@@ -28926,7 +29539,7 @@ The fixed answerer reads --answers and leaves out the questions it does not cove
|
|
|
28926
29539
|
arity: { name: "file" },
|
|
28927
29540
|
group: "Driver plumbing",
|
|
28928
29541
|
summary: "Untick a review file's boxes",
|
|
28929
|
-
details: `Reset every ticked review box (- [x]) in <file> to - [ ], writing the file only when it changes; a missing file writes nothing. Reads no workflow and runs from any directory — the landing script at a review gate calls it
|
|
29542
|
+
details: `Reset every ticked review box (- [x]) in <file> to - [ ], writing the file only when it changes; a missing file writes nothing. Reads no workflow and runs from any directory — the landing script at a review gate calls it unless the next step is an agent turn, which keeps the ticks.`
|
|
28930
29543
|
},
|
|
28931
29544
|
{
|
|
28932
29545
|
token: "lsp",
|
|
@@ -29476,6 +30089,9 @@ function makeHash(message, parent, tree) {
|
|
|
29476
30089
|
const treeStr = [...tree.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join("\n");
|
|
29477
30090
|
return sha1(`${message}\n${parent ?? "null"}\n${treeStr}`);
|
|
29478
30091
|
}
|
|
30092
|
+
/** An executable file's content carries this prefix inside the twin, so every tree copy keeps the mode for free; only the read edges strip it. */
|
|
30093
|
+
const EXEC = "\0exec\0";
|
|
30094
|
+
const plain = (content) => content.startsWith(EXEC) ? content.slice(6) : content;
|
|
29479
30095
|
var InMemRepo = class InMemRepo {
|
|
29480
30096
|
testDouble = TEST_DOUBLE_SENTINEL;
|
|
29481
30097
|
commits = /* @__PURE__ */ new Map();
|
|
@@ -29589,6 +30205,71 @@ var InMemRepo = class InMemRepo {
|
|
|
29589
30205
|
this.worktree = new Map(tree);
|
|
29590
30206
|
this.index = new Map(tree);
|
|
29591
30207
|
}
|
|
30208
|
+
/** `git switch -C <name> [<start>]`: pending changes ride along only when the tip's tree is unchanged. */
|
|
30209
|
+
switchBranchTo(name, start) {
|
|
30210
|
+
const hash = start === void 0 ? this.head : this.resolveRef(start);
|
|
30211
|
+
if (hash === null) throw new Error(`invalid reference: ${start ?? "HEAD"}`);
|
|
30212
|
+
this.moveToBranch(name, hash, true);
|
|
30213
|
+
}
|
|
30214
|
+
/** `git switch <name>`: a pending change rides along when its path is the same in both trees. */
|
|
30215
|
+
switchTo(name) {
|
|
30216
|
+
const hash = this.branches.get(name);
|
|
30217
|
+
if (hash === void 0) throw new Error(`invalid reference: ${name}`);
|
|
30218
|
+
if (hash === this.head) return this.moveToBranch(name, hash, false);
|
|
30219
|
+
const from = this.headTree();
|
|
30220
|
+
const to = this.getCommit(hash)?.files ?? /* @__PURE__ */ new Map();
|
|
30221
|
+
const pending = [.../* @__PURE__ */ new Set([
|
|
30222
|
+
...from.keys(),
|
|
30223
|
+
...this.index.keys(),
|
|
30224
|
+
...this.worktree.keys()
|
|
30225
|
+
])].filter((path) => this.index.get(path) !== from.get(path) || this.worktree.get(path) !== from.get(path));
|
|
30226
|
+
if (pending.some((path) => to.get(path) !== from.get(path))) throw new Error(`cannot switch to '${name}': local changes would be overwritten`);
|
|
30227
|
+
const carry = (side) => {
|
|
30228
|
+
const out = new Map(to);
|
|
30229
|
+
for (const path of pending) {
|
|
30230
|
+
const content = side.get(path);
|
|
30231
|
+
if (content === void 0) out.delete(path);
|
|
30232
|
+
else out.set(path, content);
|
|
30233
|
+
}
|
|
30234
|
+
return out;
|
|
30235
|
+
};
|
|
30236
|
+
this.worktree = carry(this.worktree);
|
|
30237
|
+
this.index = carry(this.index);
|
|
30238
|
+
this.head = hash;
|
|
30239
|
+
this.currentBranch = name;
|
|
30240
|
+
}
|
|
30241
|
+
moveToBranch(name, hash, reset) {
|
|
30242
|
+
if (hash !== this.head) {
|
|
30243
|
+
if (this.statusPorcelain().trim() !== "") throw new Error(`cannot switch to '${name}': the working tree has uncommitted changes`);
|
|
30244
|
+
const tree = this.getCommit(hash)?.files ?? /* @__PURE__ */ new Map();
|
|
30245
|
+
this.head = hash;
|
|
30246
|
+
this.worktree = new Map(tree);
|
|
30247
|
+
this.index = new Map(tree);
|
|
30248
|
+
}
|
|
30249
|
+
if (reset || !this.branches.has(name)) this.branches.set(name, hash);
|
|
30250
|
+
this.currentBranch = name;
|
|
30251
|
+
}
|
|
30252
|
+
deleteBranch(name) {
|
|
30253
|
+
if (name === this.currentBranch) throw new Error(`cannot delete branch '${name}' checked out`);
|
|
30254
|
+
if (!this.branches.delete(name)) throw new Error(`branch '${name}' not found`);
|
|
30255
|
+
}
|
|
30256
|
+
remotes = /* @__PURE__ */ new Map();
|
|
30257
|
+
/** Test setup: a remote that accepts pushes. */
|
|
30258
|
+
addRemote(name) {
|
|
30259
|
+
this.remotes.set(name, /* @__PURE__ */ new Map());
|
|
30260
|
+
}
|
|
30261
|
+
/** What a remote holds after pushes: ref → hash. */
|
|
30262
|
+
remoteRefs(name) {
|
|
30263
|
+
return this.remotes.get(name) ?? /* @__PURE__ */ new Map();
|
|
30264
|
+
}
|
|
30265
|
+
/** `git push --force <remote> <ref>:<ref>`; returns git's complaint, or `undefined`. */
|
|
30266
|
+
pushRef(remote, ref) {
|
|
30267
|
+
const there = this.remotes.get(remote);
|
|
30268
|
+
if (there === void 0) return `fatal: '${remote}' does not appear to be a git repository`;
|
|
30269
|
+
const hash = this.resolveRef(ref);
|
|
30270
|
+
if (hash === null) return `error: src refspec ${ref} does not match any`;
|
|
30271
|
+
there.set(ref, hash);
|
|
30272
|
+
}
|
|
29592
30273
|
/** Refs under `prefix`, newest tip first; the commit sequence number stands in for the committer date. */
|
|
29593
30274
|
listRefs(prefix) {
|
|
29594
30275
|
return [...this.refs.entries()].filter(([ref]) => ref.startsWith(prefix)).map(([ref, hash]) => ({
|
|
@@ -29669,7 +30350,8 @@ var InMemRepo = class InMemRepo {
|
|
|
29669
30350
|
if (!hash) return null;
|
|
29670
30351
|
const c = this.getCommit(hash);
|
|
29671
30352
|
if (!c) return null;
|
|
29672
|
-
|
|
30353
|
+
const content = c.files.get(path);
|
|
30354
|
+
return content === void 0 ? null : plain(content);
|
|
29673
30355
|
}
|
|
29674
30356
|
/**
|
|
29675
30357
|
* `base` (default HEAD) mirrors the port's own optional base. Tree-vs-worktree
|
|
@@ -29693,7 +30375,18 @@ var InMemRepo = class InMemRepo {
|
|
|
29693
30375
|
}
|
|
29694
30376
|
/** The worktree content of `path`, or `undefined` when absent. A LIVE read — never a snapshot. */
|
|
29695
30377
|
readFile(path) {
|
|
29696
|
-
|
|
30378
|
+
const content = this.worktree.get(path);
|
|
30379
|
+
return content === void 0 ? void 0 : plain(content);
|
|
30380
|
+
}
|
|
30381
|
+
/** `chmod +x` / `-x` on a working-tree file. */
|
|
30382
|
+
setExecutable(path, on) {
|
|
30383
|
+
const content = this.readFile(path);
|
|
30384
|
+
if (content !== void 0) this.worktree.set(path, on ? EXEC + content : content);
|
|
30385
|
+
}
|
|
30386
|
+
/** Every path's file mode at `ref`, or in the working tree when `ref` is `undefined`. */
|
|
30387
|
+
modesAt(ref) {
|
|
30388
|
+
const tree = ref === void 0 ? this.worktree : this.treeAt(ref);
|
|
30389
|
+
return new Map([...tree].map(([path, c]) => [path, c.startsWith(EXEC) ? "100755" : "100644"]));
|
|
29697
30390
|
}
|
|
29698
30391
|
/** True when `path` is exactly a worktree file, or a directory prefix of one (any worktree key starts with `path/`). */
|
|
29699
30392
|
hasPath(path) {
|
|
@@ -29821,6 +30514,27 @@ var InMemRepo = class InMemRepo {
|
|
|
29821
30514
|
this.index = new Map(tree);
|
|
29822
30515
|
this.resetHard();
|
|
29823
30516
|
}
|
|
30517
|
+
/** Twin of `GitScript.shipCommit`: the staged tree, with `.gtd/` as `branch` has it, onto `branch`, then the landing commit naming it. */
|
|
30518
|
+
shipCommit(branch, parent, shipMessage, historyRef, message) {
|
|
30519
|
+
this.stageAll();
|
|
30520
|
+
if (this.head === null) return "fatal: Needed a single revision";
|
|
30521
|
+
if (this.signingFails) return "error: gpg failed to sign the data";
|
|
30522
|
+
const kept = [...this.commits.get(parent)?.files ?? []];
|
|
30523
|
+
const tree = new Map([...[...this.index].filter(([path]) => !path.startsWith(".gtd/")), ...kept.filter(([path]) => path.startsWith(".gtd/"))]);
|
|
30524
|
+
const shipped = `${shipMessage}\n\nGtd-History: ${historyRef}\nGtd-Tip: ${this.head}\n`;
|
|
30525
|
+
const hash = makeHash(shipped, parent, tree);
|
|
30526
|
+
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}`;
|
|
30527
|
+
this.commits.set(hash, {
|
|
30528
|
+
hash,
|
|
30529
|
+
message: shipped,
|
|
30530
|
+
files: tree,
|
|
30531
|
+
parent,
|
|
30532
|
+
seq: this.nextSeq++,
|
|
30533
|
+
signed: this.signingEnabled
|
|
30534
|
+
});
|
|
30535
|
+
this.branches.set(branch, hash);
|
|
30536
|
+
return this.commitAsIs(`${message}\nGtd-Shipped: ${hash}`);
|
|
30537
|
+
}
|
|
29824
30538
|
/** Discard every pending change, tracked or untracked: stage everything, then hard-reset (which drops the freshly-staged untracked paths too). */
|
|
29825
30539
|
discardPending() {
|
|
29826
30540
|
this.index = new Map(this.worktree);
|
|
@@ -29880,7 +30594,8 @@ var InMemRepo = class InMemRepo {
|
|
|
29880
30594
|
this.worktree = newWorktree;
|
|
29881
30595
|
}
|
|
29882
30596
|
writeFile(path, content) {
|
|
29883
|
-
this.worktree.
|
|
30597
|
+
const was = this.worktree.get(path);
|
|
30598
|
+
this.worktree.set(path, was?.startsWith(EXEC) ? EXEC + content : content);
|
|
29884
30599
|
}
|
|
29885
30600
|
deleteFile(path) {
|
|
29886
30601
|
this.worktree.delete(path);
|
|
@@ -30108,6 +30823,7 @@ const makeInMemoryWorkspaceOps = (repo, root) => {
|
|
|
30108
30823
|
},
|
|
30109
30824
|
treeSync: treeOf,
|
|
30110
30825
|
episodeTrees: (base, commits) => succeed$1(new Map([...base !== void 0 ? [base] : [], ...commits].map((h) => [h, treeOf(h)]))),
|
|
30826
|
+
modeSync: (ref) => repo.modesAt(ref),
|
|
30111
30827
|
worktreeSync: () => new Map(repo.pathsUnder("").filter((path) => !isGitDirKey(path)).map((path) => [path, repo.readFile(path)])),
|
|
30112
30828
|
atPath: (path) => readAt(toKey(path)),
|
|
30113
30829
|
writeAtPath: (path, content) => try_({
|
|
@@ -30610,6 +31326,18 @@ const recognizeKeepTurn = (repo, block) => {
|
|
|
30610
31326
|
status: 1
|
|
30611
31327
|
};
|
|
30612
31328
|
};
|
|
31329
|
+
const attempt = (apply) => {
|
|
31330
|
+
try {
|
|
31331
|
+
apply();
|
|
31332
|
+
return { kind: "applied" };
|
|
31333
|
+
} catch (e) {
|
|
31334
|
+
return {
|
|
31335
|
+
kind: "failed",
|
|
31336
|
+
error: `fatal: ${e instanceof Error ? e.message : String(e)}`,
|
|
31337
|
+
status: 128
|
|
31338
|
+
};
|
|
31339
|
+
}
|
|
31340
|
+
};
|
|
30613
31341
|
const recognizeGitBuilders = (repo, block) => {
|
|
30614
31342
|
if (block === discardPending()) {
|
|
30615
31343
|
repo.discardPending();
|
|
@@ -30687,6 +31415,58 @@ const recognizeGitBuilders = (repo, block) => {
|
|
|
30687
31415
|
}
|
|
30688
31416
|
return;
|
|
30689
31417
|
}
|
|
31418
|
+
if (block.startsWith("git switch -q -C ")) {
|
|
31419
|
+
const [branch, start] = extractQuotedTokens(block);
|
|
31420
|
+
if (branch !== void 0 && switchBranchTo(branch, start) === block) return attempt(() => repo.switchBranchTo(branch, start));
|
|
31421
|
+
return;
|
|
31422
|
+
}
|
|
31423
|
+
if (block.startsWith("if ! git switch -q ")) {
|
|
31424
|
+
const [branch, , note] = extractQuotedTokens(block);
|
|
31425
|
+
if (branch !== void 0 && switchBack(branch) === block) try {
|
|
31426
|
+
repo.switchTo(branch);
|
|
31427
|
+
return { kind: "applied" };
|
|
31428
|
+
} catch {
|
|
31429
|
+
return {
|
|
31430
|
+
kind: "noop",
|
|
31431
|
+
output: `${note}\n`
|
|
31432
|
+
};
|
|
31433
|
+
}
|
|
31434
|
+
return;
|
|
31435
|
+
}
|
|
31436
|
+
if (block.startsWith("git switch -q ")) {
|
|
31437
|
+
const [branch] = extractQuotedTokens(block);
|
|
31438
|
+
if (branch !== void 0 && switchTo(branch) === block) return attempt(() => repo.switchTo(branch));
|
|
31439
|
+
return;
|
|
31440
|
+
}
|
|
31441
|
+
if (block.startsWith("git branch -q -D ")) {
|
|
31442
|
+
const [branch] = extractQuotedTokens(block);
|
|
31443
|
+
if (branch !== void 0 && deleteBranch(branch) === block) return attempt(() => repo.deleteBranch(branch));
|
|
31444
|
+
return;
|
|
31445
|
+
}
|
|
31446
|
+
if (block.startsWith("if ! git push --force ")) {
|
|
31447
|
+
const [remote, refspec] = extractQuotedTokens(block);
|
|
31448
|
+
const ref = refspec?.split(":")[0];
|
|
31449
|
+
if (remote !== void 0 && ref !== void 0 && pushRef(remote, ref) === block) {
|
|
31450
|
+
const failure = repo.pushRef(remote, ref);
|
|
31451
|
+
if (failure === void 0) return { kind: "applied" };
|
|
31452
|
+
return {
|
|
31453
|
+
kind: "failed",
|
|
31454
|
+
error: `${failure}\n${extractQuotedTokens(block)[3]}`,
|
|
31455
|
+
status: 1
|
|
31456
|
+
};
|
|
31457
|
+
}
|
|
31458
|
+
return;
|
|
31459
|
+
}
|
|
31460
|
+
if (block.startsWith("gtd_ship_parent=")) {
|
|
31461
|
+
const [parent, branch, history, shipMessage, message] = extractQuotedTokens(block);
|
|
31462
|
+
if (parent === void 0 || branch === void 0 || history === void 0 || shipMessage === void 0 || message === void 0 || shipCommit(branch, parent, shipMessage, history, message) !== block) return;
|
|
31463
|
+
const failure = repo.shipCommit(branch, parent, shipMessage, history, message);
|
|
31464
|
+
return failure === void 0 ? { kind: "applied" } : {
|
|
31465
|
+
kind: "failed",
|
|
31466
|
+
error: failure,
|
|
31467
|
+
status: 1
|
|
31468
|
+
};
|
|
31469
|
+
}
|
|
30690
31470
|
if (block.startsWith("git update-ref -d ")) {
|
|
30691
31471
|
const [ref] = extractQuotedTokens(block);
|
|
30692
31472
|
if (ref !== void 0 && deleteRef(ref) === block) {
|
|
@@ -30793,6 +31573,31 @@ const recognizeModeCommand = (repo, commands, block) => {
|
|
|
30793
31573
|
} : scripted;
|
|
30794
31574
|
return recognizeScriptedCommand(repo, /* @__PURE__ */ new Map([[command, bound]]), command);
|
|
30795
31575
|
};
|
|
31576
|
+
const FAIL_REST_MIDDLE = "\n} 2>&1 )\" || gtd_run_status=$?\n";
|
|
31577
|
+
/** A `run(…, { onFail: "rest" })` body: the inner script's failure is recorded in the tree and the block itself succeeds. */
|
|
31578
|
+
const recognizeFailRest = (repo, commands, block) => {
|
|
31579
|
+
if (!block.startsWith("rm -f -- '") || !block.includes("gtd_run_out=")) return void 0;
|
|
31580
|
+
const [path] = extractQuotedTokens(block);
|
|
31581
|
+
const headerEnd = block.indexOf("gtd_run_out=\"$( {\n");
|
|
31582
|
+
const bodyEnd = block.indexOf(FAIL_REST_MIDDLE);
|
|
31583
|
+
if (path === void 0 || headerEnd === -1 || bodyEnd === -1) return void 0;
|
|
31584
|
+
const body = block.slice(headerEnd + 18, bodyEnd);
|
|
31585
|
+
if (failRestScript(body, path) !== block) return void 0;
|
|
31586
|
+
if (repo.readFile(path) !== void 0) repo.deleteFile(path);
|
|
31587
|
+
const result = applyEmittedScript(repo, commands, body);
|
|
31588
|
+
if (!result.ok && result.error?.startsWith(UNRECOGNIZED_PREFIX)) return {
|
|
31589
|
+
kind: "failed",
|
|
31590
|
+
error: `${result.output}${result.error}`
|
|
31591
|
+
};
|
|
31592
|
+
if (!result.ok) {
|
|
31593
|
+
const lines = result.output.replace(/\n+$/, "").split("\n");
|
|
31594
|
+
repo.writeFile(path, `exit ${result.status}\n${lines.slice(-50).join("\n")}\n`);
|
|
31595
|
+
}
|
|
31596
|
+
return {
|
|
31597
|
+
kind: "noop",
|
|
31598
|
+
output: result.output
|
|
31599
|
+
};
|
|
31600
|
+
};
|
|
30796
31601
|
const FAILURE_PROMPT_HEADER = "gtd_validate_status=0\ngtd_validate_out=\"$( {\n";
|
|
30797
31602
|
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
31603
|
const FAILURE_PROMPT_FOOTER = " \"$gtd_validate_out\"\n exit \"$gtd_validate_status\"\nfi";
|
|
@@ -30945,7 +31750,7 @@ const recognizeGtdCheck = (repo, block) => {
|
|
|
30945
31750
|
};
|
|
30946
31751
|
};
|
|
30947
31752
|
const GTD_UNCHECK_RE = /^gtd uncheck (.+)$/;
|
|
30948
|
-
/** `gtd uncheck <file>` — the review-gate reset prepended ahead of the human's commit,
|
|
31753
|
+
/** `gtd uncheck <file>` — the review-gate reset prepended ahead of the human's commit, absent when the next step is an agent turn. Re-runs the REAL `clearTicks`, writing back only on an actual change. */
|
|
30949
31754
|
const recognizeGtdUncheck = (repo, block) => {
|
|
30950
31755
|
const match = GTD_UNCHECK_RE.exec(block);
|
|
30951
31756
|
if (!match) return void 0;
|
|
@@ -31083,6 +31888,7 @@ const recognizersFor = (repo, commands, git) => [
|
|
|
31083
31888
|
(block) => git ? recognizeGitBuilders(repo, block) : void 0,
|
|
31084
31889
|
(block) => recognizeWriteLines(repo, block),
|
|
31085
31890
|
(block) => recognizePinScripts(repo, block),
|
|
31891
|
+
(block) => recognizeFailRest(repo, commands, block),
|
|
31086
31892
|
(block) => recognizeFailurePromptWrapper(repo, commands, block),
|
|
31087
31893
|
(block) => recognizePresentationSubshell(repo, commands, block, git),
|
|
31088
31894
|
(block) => recognizeModeContradictionSkipNotice(block),
|