@pmelab/gtd 25.0.0 → 26.1.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 +7 -0
- package/dist/flows.mjs +15 -1
- package/dist/gtd.bundle.mjs +161 -20
- package/dist/testkit.mjs +311 -57
- package/package.json +2 -1
- package/src/cli/Cli.test.ts +28 -3
- package/src/cli/Cli.ts +12 -1
- package/src/cli/History.test.ts +2 -2
- package/src/cli/Program.test.ts +10 -12
- package/src/edge/Edge.test.ts +113 -16
- package/src/edge/Edge.ts +22 -6
- package/src/emit/GitScript.test.ts +65 -2
- package/src/emit/GitScript.ts +34 -1
- package/src/emit/index.ts +1 -0
- package/src/flags/index.test.ts +67 -0
- package/src/flags/index.ts +38 -0
- package/src/flows/runtime.test.ts +33 -0
- package/src/flows/runtime.ts +31 -0
- package/src/platform/RetainedHistory.test.ts +9 -0
- package/src/platform/RetainedHistory.ts +3 -0
- package/src/platform/Workspace.test.ts +1 -1
- package/src/platform/index.ts +1 -0
- package/src/replay/RefName.test.ts +40 -0
- package/src/replay/RefName.ts +19 -0
- package/src/replay/Replay.test.ts +193 -2
- package/src/replay/Replay.ts +51 -6
- package/src/replay/Trailers.test.ts +26 -0
- package/src/replay/Trailers.ts +8 -0
- package/src/replay/index.ts +1 -0
- package/src/step/LandStep.ts +1 -0
- package/src/step/RepoSnapshot.ts +2 -0
- package/src/step/planStart.test.ts +5 -5
- package/src/step/planStep.test.ts +53 -0
- package/src/step/planStep.ts +7 -1
- package/src/testkit/EmittedScriptRecognizer.test.ts +57 -29
- package/src/testkit/EmittedScriptRecognizer.ts +26 -4
- package/src/testkit/EmittedScripts.test.ts +182 -0
- package/src/testkit/FakeGitOperations.ts +8 -2
- package/src/testkit/GitTiers.ts +103 -24
- package/src/testkit/InMemRepo.ts +57 -12
- package/src/testkit/TestRepo.ts +2 -2
- package/src/workflow/Workflow.ts +1 -0
- package/src/workflow/discovery.test.ts +1 -1
- package/src/workflow/vars.test.ts +16 -0
- package/src/workflow/vars.ts +4 -1
- package/src/workflows/shared/text.fixture.ts +1 -0
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ Or run without installing (prefix every `gtd` below with `npx`) — see
|
|
|
28
28
|
for the settings most projects tune. A process setting's value is committed to
|
|
29
29
|
Git history; keep secrets in an environment setting (see
|
|
30
30
|
[Settings](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#settings)).
|
|
31
|
+
`GTD_FLAG_<NAME>` variables toggle experimental features — see
|
|
32
|
+
[Feature flags](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#feature-flags).
|
|
31
33
|
|
|
32
34
|
Also install all three skill sources the bundled workflow names in its prompts —
|
|
33
35
|
none is optional. See
|
|
@@ -574,6 +576,11 @@ git config --add remote.origin.fetch 'refs/gtd/history/*:refs/gtd/history/*'
|
|
|
574
576
|
The second line makes every plain `git fetch` include them. It has no leading
|
|
575
577
|
`+`, so a conflicting local ref fails instead of being overwritten.
|
|
576
578
|
|
|
579
|
+
A step declared with `keep: true` files its turn under `refs/gtd/kept/` instead
|
|
580
|
+
of on the branch (see [Kept tips](docs/configuration.md#kept-tips)). Those refs
|
|
581
|
+
are not process history: `gtd history` does not list them, and the
|
|
582
|
+
`refs/gtd/history/*` refspecs above neither push nor fetch them.
|
|
583
|
+
|
|
577
584
|
## Entrypoints
|
|
578
585
|
|
|
579
586
|
gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
|
package/dist/flows.mjs
CHANGED
|
@@ -109,6 +109,19 @@ const changesSince = (hash, pattern) => {
|
|
|
109
109
|
const all = context.changesSince(hash);
|
|
110
110
|
return toChanges(pattern === void 0 ? all : all.filter((c) => context.matches(c.path, pattern)));
|
|
111
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* The latest kept tip of step `name` (scoped from here, same as `agent()`) in
|
|
114
|
+
* this episode, or `undefined` when none.
|
|
115
|
+
*/
|
|
116
|
+
const kept = (name) => {
|
|
117
|
+
const context = ctx();
|
|
118
|
+
if (typeof context.kept !== "function") throw new Error("gtd: kept() needs a newer gtd — upgrade the gtd that runs it");
|
|
119
|
+
const tip = context.kept(name);
|
|
120
|
+
return tip === void 0 ? void 0 : {
|
|
121
|
+
hash: tip.hash,
|
|
122
|
+
changes: toChanges(tip.changes)
|
|
123
|
+
};
|
|
124
|
+
};
|
|
112
125
|
/** The commit the process stands on at this point of the flow. */
|
|
113
126
|
const head = () => ctx().head();
|
|
114
127
|
/** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
|
|
@@ -502,6 +515,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
502
515
|
installContext: () => installContext,
|
|
503
516
|
isFactoryWorkflow: () => isFactoryWorkflow,
|
|
504
517
|
judge: () => judge,
|
|
518
|
+
kept: () => kept,
|
|
505
519
|
moveScript: () => moveScript,
|
|
506
520
|
numeric: () => numeric,
|
|
507
521
|
openQuestions: () => openQuestions,
|
|
@@ -530,4 +544,4 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
530
544
|
wrote: () => wrote
|
|
531
545
|
});
|
|
532
546
|
//#endregion
|
|
533
|
-
export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
|
|
547
|
+
export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, kept, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, showScript, skillsFor, start, flows_exports as t, threads, vars, wrote };
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -20943,6 +20943,8 @@ const HISTORY_REF = "refs/worktree/gtd/history";
|
|
|
20943
20943
|
/** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
|
|
20944
20944
|
const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
|
|
20945
20945
|
const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
|
|
20946
|
+
/** Directory of a step's turns set aside by `keep: true`, each at `<this>/<kept hash>`; own namespace, so history list/show/fetch never see it. */
|
|
20947
|
+
const keptRef = (entry, step) => `refs/gtd/kept/${entry}/${step}`;
|
|
20946
20948
|
const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
|
|
20947
20949
|
/**
|
|
20948
20950
|
* Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
|
|
@@ -20957,6 +20959,26 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
|
|
|
20957
20959
|
};
|
|
20958
20960
|
});
|
|
20959
20961
|
//#endregion
|
|
20962
|
+
//#region src/flags/index.ts
|
|
20963
|
+
const FLAGS$1 = [];
|
|
20964
|
+
const BOOL = {
|
|
20965
|
+
"": false,
|
|
20966
|
+
"0": false,
|
|
20967
|
+
false: false,
|
|
20968
|
+
"1": true,
|
|
20969
|
+
true: true
|
|
20970
|
+
};
|
|
20971
|
+
const resolveFlags = (env, registry = FLAGS$1) => {
|
|
20972
|
+
const out = {};
|
|
20973
|
+
for (const { name } of registry) {
|
|
20974
|
+
const variable = `GTD_FLAG_${name.toUpperCase()}`;
|
|
20975
|
+
const key = (env[variable] ?? "").toLowerCase();
|
|
20976
|
+
if (!Object.hasOwn(BOOL, key)) throw new Error(`${variable} must be 1, 0, true or false`);
|
|
20977
|
+
out[name] = BOOL[key];
|
|
20978
|
+
}
|
|
20979
|
+
return out;
|
|
20980
|
+
};
|
|
20981
|
+
//#endregion
|
|
20960
20982
|
//#region src/install/Version.ts
|
|
20961
20983
|
const _require = createRequire(import.meta.url);
|
|
20962
20984
|
const findPackageJson = (fromUrl) => {
|
|
@@ -21695,6 +21717,19 @@ const changesSince = (hash, pattern) => {
|
|
|
21695
21717
|
const all = context.changesSince(hash);
|
|
21696
21718
|
return toChanges(pattern === void 0 ? all : all.filter((c) => context.matches(c.path, pattern)));
|
|
21697
21719
|
};
|
|
21720
|
+
/**
|
|
21721
|
+
* The latest kept tip of step `name` (scoped from here, same as `agent()`) in
|
|
21722
|
+
* this episode, or `undefined` when none.
|
|
21723
|
+
*/
|
|
21724
|
+
const kept = (name) => {
|
|
21725
|
+
const context = ctx();
|
|
21726
|
+
if (typeof context.kept !== "function") throw new Error("gtd: kept() needs a newer gtd — upgrade the gtd that runs it");
|
|
21727
|
+
const tip = context.kept(name);
|
|
21728
|
+
return tip === void 0 ? void 0 : {
|
|
21729
|
+
hash: tip.hash,
|
|
21730
|
+
changes: toChanges(tip.changes)
|
|
21731
|
+
};
|
|
21732
|
+
};
|
|
21698
21733
|
/** The commit the process stands on at this point of the flow. */
|
|
21699
21734
|
const head = () => ctx().head();
|
|
21700
21735
|
/** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
|
|
@@ -22088,6 +22123,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
22088
22123
|
installContext: () => installContext,
|
|
22089
22124
|
isFactoryWorkflow: () => isFactoryWorkflow,
|
|
22090
22125
|
judge: () => judge,
|
|
22126
|
+
kept: () => kept,
|
|
22091
22127
|
moveScript: () => moveScript,
|
|
22092
22128
|
numeric: () => numeric,
|
|
22093
22129
|
openQuestions: () => openQuestions,
|
|
@@ -37471,8 +37507,24 @@ var init_wire = __esmMin((() => {
|
|
|
37471
37507
|
init_Glob();
|
|
37472
37508
|
}));
|
|
37473
37509
|
//#endregion
|
|
37474
|
-
//#region src/replay/
|
|
37510
|
+
//#region src/replay/RefName.ts
|
|
37475
37511
|
init_wire();
|
|
37512
|
+
const FORBIDDEN = /[\u0000- \u007f~^:?*[\\]/;
|
|
37513
|
+
/** Why `name` cannot be (part of) a git ref name, or `undefined` when it can. */
|
|
37514
|
+
const refNameFault = (name) => {
|
|
37515
|
+
if (name === "") return "it is empty";
|
|
37516
|
+
if (FORBIDDEN.test(name)) return "it contains a space, control character or one of ~^:?*[\\";
|
|
37517
|
+
if (name.includes("..")) return "it contains \"..\"";
|
|
37518
|
+
if (name.includes("@{")) return "it contains \"@{\"";
|
|
37519
|
+
if (name.endsWith(".")) return "it ends with \".\"";
|
|
37520
|
+
for (const part of name.split("/")) {
|
|
37521
|
+
if (part === "") return "it has an empty \"/\" component";
|
|
37522
|
+
if (part.startsWith(".")) return "a component starts with \".\"";
|
|
37523
|
+
if (part.endsWith(".lock")) return "a component ends with \".lock\"";
|
|
37524
|
+
}
|
|
37525
|
+
};
|
|
37526
|
+
//#endregion
|
|
37527
|
+
//#region src/replay/Tree.ts
|
|
37476
37528
|
const treeFromRecord = (files) => ({
|
|
37477
37529
|
paths: () => Object.keys(files).sort(),
|
|
37478
37530
|
read: (path) => Object.hasOwn(files, path) ? files[path] : void 0
|
|
@@ -37566,6 +37618,7 @@ const decodePath = (raw) => {
|
|
|
37566
37618
|
return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
|
|
37567
37619
|
};
|
|
37568
37620
|
const FORMAT_RE = /^[1-9][0-9]*$/;
|
|
37621
|
+
const HASH_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
37569
37622
|
const TRAILER_READERS = {
|
|
37570
37623
|
"Gtd-Step": (value, into) => {
|
|
37571
37624
|
into.step ??= parseStepId(value);
|
|
@@ -37595,6 +37648,9 @@ const TRAILER_READERS = {
|
|
|
37595
37648
|
"Gtd-Version": (value, into) => {
|
|
37596
37649
|
if (value !== "") into.version ??= value;
|
|
37597
37650
|
},
|
|
37651
|
+
"Gtd-Kept": (value, into) => {
|
|
37652
|
+
if (HASH_RE.test(value)) into.kept ??= value;
|
|
37653
|
+
},
|
|
37598
37654
|
"Gtd-Format": (value, into) => {
|
|
37599
37655
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
37600
37656
|
},
|
|
@@ -37615,6 +37671,7 @@ const parseCommitMessage = (message) => {
|
|
|
37615
37671
|
reviewBase: void 0,
|
|
37616
37672
|
branch: void 0,
|
|
37617
37673
|
version: void 0,
|
|
37674
|
+
kept: void 0,
|
|
37618
37675
|
judge: [],
|
|
37619
37676
|
vars: {},
|
|
37620
37677
|
config: {},
|
|
@@ -37729,7 +37786,8 @@ const KNOWN_OPTIONS = {
|
|
|
37729
37786
|
...STEERING_OPTIONS,
|
|
37730
37787
|
"model",
|
|
37731
37788
|
"system",
|
|
37732
|
-
"allowEmpty"
|
|
37789
|
+
"allowEmpty",
|
|
37790
|
+
"keep"
|
|
37733
37791
|
]),
|
|
37734
37792
|
human: /* @__PURE__ */ new Set([
|
|
37735
37793
|
...STEERING_OPTIONS,
|
|
@@ -37761,6 +37819,7 @@ const replay = async (input) => {
|
|
|
37761
37819
|
const scopeCallIds = [];
|
|
37762
37820
|
let scopeCallCount = 0;
|
|
37763
37821
|
const trace = [];
|
|
37822
|
+
const keptTips = /* @__PURE__ */ new Map();
|
|
37764
37823
|
let landed;
|
|
37765
37824
|
let outcome;
|
|
37766
37825
|
let signal;
|
|
@@ -37920,6 +37979,12 @@ const replay = async (input) => {
|
|
|
37920
37979
|
}
|
|
37921
37980
|
cursor++;
|
|
37922
37981
|
advance(commit);
|
|
37982
|
+
if (commit.parsed.kept !== void 0) if (commit.kept === void 0) keptTips.delete(step.name);
|
|
37983
|
+
else keptTips.set(step.name, {
|
|
37984
|
+
hash: commit.kept.hash,
|
|
37985
|
+
before: commit.tree,
|
|
37986
|
+
after: commit.kept.tree
|
|
37987
|
+
});
|
|
37923
37988
|
const to = commit.parsed.parsed?.to;
|
|
37924
37989
|
if (to !== void 0) expectedNext = {
|
|
37925
37990
|
name: to,
|
|
@@ -37927,6 +37992,21 @@ const replay = async (input) => {
|
|
|
37927
37992
|
};
|
|
37928
37993
|
return replyFor(step.request, commit.parsed.judge);
|
|
37929
37994
|
};
|
|
37995
|
+
const landPending = (step, pending) => {
|
|
37996
|
+
pendingUsed = true;
|
|
37997
|
+
landed = step;
|
|
37998
|
+
const keeping = step.request.kind === "agent" && step.request.options.keep === true;
|
|
37999
|
+
if (keeping) keptTips.set(step.name, {
|
|
38000
|
+
hash: "",
|
|
38001
|
+
before: position.tree,
|
|
38002
|
+
after: pending.tree
|
|
38003
|
+
});
|
|
38004
|
+
advance({
|
|
38005
|
+
hash: "",
|
|
38006
|
+
tree: keeping ? position.tree : pending.tree
|
|
38007
|
+
});
|
|
38008
|
+
return replyFor(step.request, pending.verdicts ?? []);
|
|
38009
|
+
};
|
|
37930
38010
|
const answer = (step) => {
|
|
37931
38011
|
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) cursor++;
|
|
37932
38012
|
const commit = commits[cursor];
|
|
@@ -37937,17 +38017,18 @@ const replay = async (input) => {
|
|
|
37937
38017
|
trace,
|
|
37938
38018
|
landed
|
|
37939
38019
|
});
|
|
37940
|
-
|
|
37941
|
-
landed = step;
|
|
37942
|
-
advance({
|
|
37943
|
-
hash: "",
|
|
37944
|
-
tree: input.pending.tree
|
|
37945
|
-
});
|
|
37946
|
-
return replyFor(step.request, input.pending.verdicts ?? []);
|
|
38020
|
+
return landPending(step, input.pending);
|
|
37947
38021
|
};
|
|
37948
38022
|
const requestFault = (request) => {
|
|
37949
38023
|
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)`;
|
|
37950
|
-
|
|
38024
|
+
if (request.kind !== "agent") return void 0;
|
|
38025
|
+
const name = scoped(request.name);
|
|
38026
|
+
if (request.options.keep === true) {
|
|
38027
|
+
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`;
|
|
38028
|
+
const refFault = name.includes("/") ? "it contains \"/\"" : refNameFault(name);
|
|
38029
|
+
if (refFault !== void 0) return `gtd: step "${name}": keep: true files the turn under a git ref named after the step, but ${refFault}`;
|
|
38030
|
+
}
|
|
38031
|
+
return scopeAccessFault();
|
|
37951
38032
|
};
|
|
37952
38033
|
const handleStep = async (request) => {
|
|
37953
38034
|
if (outcome !== void 0) throw new Stop();
|
|
@@ -38030,6 +38111,13 @@ const replay = async (input) => {
|
|
|
38030
38111
|
faults: findings.filter((f) => f.message.startsWith(`Code thread at ${c.path}:${t.line + 1}: `)).map((f) => f.message)
|
|
38031
38112
|
}));
|
|
38032
38113
|
}),
|
|
38114
|
+
kept: (name) => {
|
|
38115
|
+
const tip = keptTips.get(scoped(name));
|
|
38116
|
+
return tip === void 0 ? void 0 : {
|
|
38117
|
+
hash: tip.hash,
|
|
38118
|
+
changes: changesBetween(tip.before, tip.after)
|
|
38119
|
+
};
|
|
38120
|
+
},
|
|
38033
38121
|
vars: input.vars,
|
|
38034
38122
|
env: input.env,
|
|
38035
38123
|
start: () => input.start,
|
|
@@ -38131,7 +38219,9 @@ const resolveVars = (workflowVars, entryVars, env) => {
|
|
|
38131
38219
|
...entryVars
|
|
38132
38220
|
};
|
|
38133
38221
|
for (const name of Object.keys(merged)) {
|
|
38134
|
-
const
|
|
38222
|
+
const key = PREFIX + name.toUpperCase();
|
|
38223
|
+
if (key.startsWith("GTD_FLAG_")) continue;
|
|
38224
|
+
const value = env[key];
|
|
38135
38225
|
if (value !== void 0) merged[name] = value;
|
|
38136
38226
|
}
|
|
38137
38227
|
return merged;
|
|
@@ -38997,7 +39087,11 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
38997
39087
|
...uncheckStep(snapshot),
|
|
38998
39088
|
{
|
|
38999
39089
|
kind: "gitWrite",
|
|
39000
|
-
write: {
|
|
39090
|
+
write: landing.keepTurn !== void 0 ? {
|
|
39091
|
+
kind: "keepTurn",
|
|
39092
|
+
ref: landing.keepTurn,
|
|
39093
|
+
message
|
|
39094
|
+
} : {
|
|
39001
39095
|
kind: "commitAll",
|
|
39002
39096
|
message
|
|
39003
39097
|
}
|
|
@@ -39477,6 +39571,7 @@ const promptDef = (common, request, skills, access) => ({
|
|
|
39477
39571
|
...optional$1("model", request.options.model),
|
|
39478
39572
|
...optional$1("system", request.options.system),
|
|
39479
39573
|
...optional$1("allowEmpty", request.options.allowEmpty),
|
|
39574
|
+
...optional$1("keep", request.options.keep),
|
|
39480
39575
|
...optional$1("skills", skills),
|
|
39481
39576
|
...optional$1("access", access)
|
|
39482
39577
|
});
|
|
@@ -39591,11 +39686,21 @@ const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(func
|
|
|
39591
39686
|
hash: run.episode.base,
|
|
39592
39687
|
tree: commitTree(workspace, run.episode.base)
|
|
39593
39688
|
};
|
|
39594
|
-
const
|
|
39595
|
-
|
|
39596
|
-
|
|
39597
|
-
|
|
39598
|
-
|
|
39689
|
+
const git = yield* GitService;
|
|
39690
|
+
const episode = [];
|
|
39691
|
+
for (const c of run.episode.commits) {
|
|
39692
|
+
const tip = parseCommitMessage(c.message).kept;
|
|
39693
|
+
const exists = tip !== void 0 && isSome(yield* git.readRefOption(`${tip}^{commit}`));
|
|
39694
|
+
episode.push({
|
|
39695
|
+
hash: c.hash,
|
|
39696
|
+
message: c.message,
|
|
39697
|
+
tree: commitTree(workspace, c.hash),
|
|
39698
|
+
...exists ? { kept: {
|
|
39699
|
+
hash: tip,
|
|
39700
|
+
tree: commitTree(workspace, tip)
|
|
39701
|
+
} } : {}
|
|
39702
|
+
});
|
|
39703
|
+
}
|
|
39599
39704
|
let pendingMemo;
|
|
39600
39705
|
return {
|
|
39601
39706
|
resolved,
|
|
@@ -39771,6 +39876,7 @@ const closingTrailers = (branch) => ({
|
|
|
39771
39876
|
...isSome(branch) ? { branch: branch.value } : {}
|
|
39772
39877
|
});
|
|
39773
39878
|
const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
|
|
39879
|
+
const keptTurn = (rest) => rest.stepDef.keep === true && rest.changes.length > 0 ? { keepTurn: keptRef(rest.run.startHash, rest.state) } : {};
|
|
39774
39880
|
const landingTo = (rest, to, branch) => {
|
|
39775
39881
|
if (isSettledNoop(rest, to)) return {
|
|
39776
39882
|
kind: "noop",
|
|
@@ -39795,7 +39901,8 @@ const landingTo = (rest, to, branch) => {
|
|
|
39795
39901
|
} : {},
|
|
39796
39902
|
...finishes ? closingTrailers(branch) : {}
|
|
39797
39903
|
},
|
|
39798
|
-
...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {}
|
|
39904
|
+
...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {},
|
|
39905
|
+
...keptTurn(rest)
|
|
39799
39906
|
};
|
|
39800
39907
|
};
|
|
39801
39908
|
/**
|
|
@@ -39932,7 +40039,7 @@ const commitAllowEmpty = (message) => {
|
|
|
39932
40039
|
return [
|
|
39933
40040
|
`if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
|
|
39934
40041
|
` case "$out" in`,
|
|
39935
|
-
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} ;;`,
|
|
40042
|
+
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} || exit 1 ;;`,
|
|
39936
40043
|
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
39937
40044
|
` esac`,
|
|
39938
40045
|
`fi`
|
|
@@ -39942,6 +40049,36 @@ const commitAllowEmpty = (message) => {
|
|
|
39942
40049
|
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
39943
40050
|
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote$1(ref)}`;
|
|
39944
40051
|
const hardResetTo = (ref) => `git reset --hard ${shellQuote$1(ref)}`;
|
|
40052
|
+
/**
|
|
40053
|
+
* POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
|
|
40054
|
+
* tree, so no hook runs on it and its tree is the pre-turn tree by
|
|
40055
|
+
* construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
|
|
40056
|
+
* `-S`, read (and failing closed) before any object or ref is written; the
|
|
40057
|
+
* index is already staged by then, so an early exit leaves the turn staged.
|
|
40058
|
+
* The kept ref is deleted only after `reset -q` succeeds: it is then the
|
|
40059
|
+
* turn's only handle.
|
|
40060
|
+
* The branch moves before the final hard reset, so if that reset fails the
|
|
40061
|
+
* marker has landed and the turn is still in the working tree (kept ref
|
|
40062
|
+
* present); every other failure leaves the branch untouched.
|
|
40063
|
+
*/
|
|
40064
|
+
const keepTurn = (refPrefix, message) => {
|
|
40065
|
+
const keptRef = `${shellQuote$1(`${refPrefix}/`)}"$gtd_kept"`;
|
|
40066
|
+
return [
|
|
40067
|
+
`git add -A || exit 1`,
|
|
40068
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
40069
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
40070
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
40071
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
40072
|
+
`gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote$1(message)}) || exit 1`,
|
|
40073
|
+
`git update-ref ${keptRef} "$gtd_kept" || exit 1`,
|
|
40074
|
+
`gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote$1(message)} "$gtd_kept")`,
|
|
40075
|
+
`if ! gtd_tip=$(git commit-tree "$gtd_sign" "$gtd_head^{tree}" -p "$gtd_head" -m "$gtd_marker") || ! git update-ref HEAD "$gtd_tip" "$gtd_head"; then`,
|
|
40076
|
+
` git reset -q && git update-ref -d ${keptRef}`,
|
|
40077
|
+
` exit 1`,
|
|
40078
|
+
`fi`,
|
|
40079
|
+
`git reset --hard HEAD`
|
|
40080
|
+
].join("\n");
|
|
40081
|
+
};
|
|
39945
40082
|
const updateRef = (ref, hash) => `git update-ref ${shellQuote$1(ref)} ${shellQuote$1(hash)}`;
|
|
39946
40083
|
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
39947
40084
|
const deleteRef = (ref) => `git update-ref -d ${shellQuote$1(ref)}`;
|
|
@@ -39974,6 +40111,7 @@ const renderGitWrite = (write) => {
|
|
|
39974
40111
|
switch (write.kind) {
|
|
39975
40112
|
case "commitAll": return commitAll(write.message);
|
|
39976
40113
|
case "updateRef": return updateRef(write.ref, write.target);
|
|
40114
|
+
case "keepTurn": return keepTurn(write.ref, write.message);
|
|
39977
40115
|
}
|
|
39978
40116
|
};
|
|
39979
40117
|
/**
|
|
@@ -42051,7 +42189,10 @@ const runCli = (argv, io) => {
|
|
|
42051
42189
|
io.exit(2);
|
|
42052
42190
|
});
|
|
42053
42191
|
const out = bufferedArtifactOut(io);
|
|
42054
|
-
return
|
|
42192
|
+
return flatMap$4(Host, ({ env }) => try_({
|
|
42193
|
+
try: () => resolveFlags(env, io.flags),
|
|
42194
|
+
catch: (e) => new GtdError(e.message)
|
|
42195
|
+
})).pipe(flatMap$4(() => runCommand(plan.command, plan.json, out)), map$4(() => {
|
|
42055
42196
|
out.flush();
|
|
42056
42197
|
io.exit(0);
|
|
42057
42198
|
}), provide$1(io.layers(plan.verbose)), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json.kind !== "off")(cause)));
|