@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/dist/testkit.mjs
CHANGED
|
@@ -6071,6 +6071,8 @@ const HISTORY_REF = "refs/worktree/gtd/history";
|
|
|
6071
6071
|
/** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
|
|
6072
6072
|
const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
|
|
6073
6073
|
const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
|
|
6074
|
+
/** 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. */
|
|
6075
|
+
const keptRef = (entry, step) => `refs/gtd/kept/${entry}/${step}`;
|
|
6074
6076
|
const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
|
|
6075
6077
|
/**
|
|
6076
6078
|
* Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
|
|
@@ -6085,6 +6087,26 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
|
|
|
6085
6087
|
};
|
|
6086
6088
|
});
|
|
6087
6089
|
//#endregion
|
|
6090
|
+
//#region src/flags/index.ts
|
|
6091
|
+
const FLAGS$1 = [];
|
|
6092
|
+
const BOOL = {
|
|
6093
|
+
"": false,
|
|
6094
|
+
"0": false,
|
|
6095
|
+
false: false,
|
|
6096
|
+
"1": true,
|
|
6097
|
+
true: true
|
|
6098
|
+
};
|
|
6099
|
+
const resolveFlags = (env, registry = FLAGS$1) => {
|
|
6100
|
+
const out = {};
|
|
6101
|
+
for (const { name } of registry) {
|
|
6102
|
+
const variable = `GTD_FLAG_${name.toUpperCase()}`;
|
|
6103
|
+
const key = (env[variable] ?? "").toLowerCase();
|
|
6104
|
+
if (!Object.hasOwn(BOOL, key)) throw new Error(`${variable} must be 1, 0, true or false`);
|
|
6105
|
+
out[name] = BOOL[key];
|
|
6106
|
+
}
|
|
6107
|
+
return out;
|
|
6108
|
+
};
|
|
6109
|
+
//#endregion
|
|
6088
6110
|
//#region src/install/Version.ts
|
|
6089
6111
|
const _require = createRequire(import.meta.url);
|
|
6090
6112
|
const findPackageJson = (fromUrl) => {
|
|
@@ -6750,6 +6772,22 @@ const accessShapeFault = (value) => {
|
|
|
6750
6772
|
const sameGlobs = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((g, i) => g === b[i]);
|
|
6751
6773
|
const sameAccess = (a, b) => sameGlobs(a?.read, b?.read) && sameGlobs(a?.write, b?.write);
|
|
6752
6774
|
//#endregion
|
|
6775
|
+
//#region src/replay/RefName.ts
|
|
6776
|
+
const FORBIDDEN = /[\u0000- \u007f~^:?*[\\]/;
|
|
6777
|
+
/** Why `name` cannot be (part of) a git ref name, or `undefined` when it can. */
|
|
6778
|
+
const refNameFault = (name) => {
|
|
6779
|
+
if (name === "") return "it is empty";
|
|
6780
|
+
if (FORBIDDEN.test(name)) return "it contains a space, control character or one of ~^:?*[\\";
|
|
6781
|
+
if (name.includes("..")) return "it contains \"..\"";
|
|
6782
|
+
if (name.includes("@{")) return "it contains \"@{\"";
|
|
6783
|
+
if (name.endsWith(".")) return "it ends with \".\"";
|
|
6784
|
+
for (const part of name.split("/")) {
|
|
6785
|
+
if (part === "") return "it has an empty \"/\" component";
|
|
6786
|
+
if (part.startsWith(".")) return "a component starts with \".\"";
|
|
6787
|
+
if (part.endsWith(".lock")) return "a component ends with \".lock\"";
|
|
6788
|
+
}
|
|
6789
|
+
};
|
|
6790
|
+
//#endregion
|
|
6753
6791
|
//#region src/replay/Tree.ts
|
|
6754
6792
|
const treeFromRecord = (files) => ({
|
|
6755
6793
|
paths: () => Object.keys(files).sort(),
|
|
@@ -6844,6 +6882,7 @@ const decodePath = (raw) => {
|
|
|
6844
6882
|
return typeof path === "string" && path !== "" && !CONTROL_CHARACTER$1.test(path) ? path : void 0;
|
|
6845
6883
|
};
|
|
6846
6884
|
const FORMAT_RE = /^[1-9][0-9]*$/;
|
|
6885
|
+
const HASH_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
6847
6886
|
const TRAILER_READERS = {
|
|
6848
6887
|
"Gtd-Step": (value, into) => {
|
|
6849
6888
|
into.step ??= parseStepId(value);
|
|
@@ -6873,6 +6912,9 @@ const TRAILER_READERS = {
|
|
|
6873
6912
|
"Gtd-Version": (value, into) => {
|
|
6874
6913
|
if (value !== "") into.version ??= value;
|
|
6875
6914
|
},
|
|
6915
|
+
"Gtd-Kept": (value, into) => {
|
|
6916
|
+
if (HASH_RE.test(value)) into.kept ??= value;
|
|
6917
|
+
},
|
|
6876
6918
|
"Gtd-Format": (value, into) => {
|
|
6877
6919
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
6878
6920
|
},
|
|
@@ -6893,6 +6935,7 @@ const parseCommitMessage = (message) => {
|
|
|
6893
6935
|
reviewBase: void 0,
|
|
6894
6936
|
branch: void 0,
|
|
6895
6937
|
version: void 0,
|
|
6938
|
+
kept: void 0,
|
|
6896
6939
|
judge: [],
|
|
6897
6940
|
vars: {},
|
|
6898
6941
|
config: {},
|
|
@@ -7007,7 +7050,8 @@ const KNOWN_OPTIONS = {
|
|
|
7007
7050
|
...STEERING_OPTIONS,
|
|
7008
7051
|
"model",
|
|
7009
7052
|
"system",
|
|
7010
|
-
"allowEmpty"
|
|
7053
|
+
"allowEmpty",
|
|
7054
|
+
"keep"
|
|
7011
7055
|
]),
|
|
7012
7056
|
human: /* @__PURE__ */ new Set([
|
|
7013
7057
|
...STEERING_OPTIONS,
|
|
@@ -7039,6 +7083,7 @@ const replay = async (input) => {
|
|
|
7039
7083
|
const scopeCallIds = [];
|
|
7040
7084
|
let scopeCallCount = 0;
|
|
7041
7085
|
const trace = [];
|
|
7086
|
+
const keptTips = /* @__PURE__ */ new Map();
|
|
7042
7087
|
let landed;
|
|
7043
7088
|
let outcome;
|
|
7044
7089
|
let signal;
|
|
@@ -7198,6 +7243,12 @@ const replay = async (input) => {
|
|
|
7198
7243
|
}
|
|
7199
7244
|
cursor++;
|
|
7200
7245
|
advance(commit);
|
|
7246
|
+
if (commit.parsed.kept !== void 0) if (commit.kept === void 0) keptTips.delete(step.name);
|
|
7247
|
+
else keptTips.set(step.name, {
|
|
7248
|
+
hash: commit.kept.hash,
|
|
7249
|
+
before: commit.tree,
|
|
7250
|
+
after: commit.kept.tree
|
|
7251
|
+
});
|
|
7201
7252
|
const to = commit.parsed.parsed?.to;
|
|
7202
7253
|
if (to !== void 0) expectedNext = {
|
|
7203
7254
|
name: to,
|
|
@@ -7205,6 +7256,21 @@ const replay = async (input) => {
|
|
|
7205
7256
|
};
|
|
7206
7257
|
return replyFor(step.request, commit.parsed.judge);
|
|
7207
7258
|
};
|
|
7259
|
+
const landPending = (step, pending) => {
|
|
7260
|
+
pendingUsed = true;
|
|
7261
|
+
landed = step;
|
|
7262
|
+
const keeping = step.request.kind === "agent" && step.request.options.keep === true;
|
|
7263
|
+
if (keeping) keptTips.set(step.name, {
|
|
7264
|
+
hash: "",
|
|
7265
|
+
before: position.tree,
|
|
7266
|
+
after: pending.tree
|
|
7267
|
+
});
|
|
7268
|
+
advance({
|
|
7269
|
+
hash: "",
|
|
7270
|
+
tree: keeping ? position.tree : pending.tree
|
|
7271
|
+
});
|
|
7272
|
+
return replyFor(step.request, pending.verdicts ?? []);
|
|
7273
|
+
};
|
|
7208
7274
|
const answer = (step) => {
|
|
7209
7275
|
while (cursor < commits.length && isAttemptAt(commits[cursor], step.name)) cursor++;
|
|
7210
7276
|
const commit = commits[cursor];
|
|
@@ -7215,17 +7281,18 @@ const replay = async (input) => {
|
|
|
7215
7281
|
trace,
|
|
7216
7282
|
landed
|
|
7217
7283
|
});
|
|
7218
|
-
|
|
7219
|
-
landed = step;
|
|
7220
|
-
advance({
|
|
7221
|
-
hash: "",
|
|
7222
|
-
tree: input.pending.tree
|
|
7223
|
-
});
|
|
7224
|
-
return replyFor(step.request, input.pending.verdicts ?? []);
|
|
7284
|
+
return landPending(step, input.pending);
|
|
7225
7285
|
};
|
|
7226
7286
|
const requestFault = (request) => {
|
|
7227
7287
|
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)`;
|
|
7228
|
-
|
|
7288
|
+
if (request.kind !== "agent") return void 0;
|
|
7289
|
+
const name = scoped(request.name);
|
|
7290
|
+
if (request.options.keep === true) {
|
|
7291
|
+
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`;
|
|
7292
|
+
const refFault = name.includes("/") ? "it contains \"/\"" : refNameFault(name);
|
|
7293
|
+
if (refFault !== void 0) return `gtd: step "${name}": keep: true files the turn under a git ref named after the step, but ${refFault}`;
|
|
7294
|
+
}
|
|
7295
|
+
return scopeAccessFault();
|
|
7229
7296
|
};
|
|
7230
7297
|
const handleStep = async (request) => {
|
|
7231
7298
|
if (outcome !== void 0) throw new Stop();
|
|
@@ -7308,6 +7375,13 @@ const replay = async (input) => {
|
|
|
7308
7375
|
faults: findings.filter((f) => f.message.startsWith(`Code thread at ${c.path}:${t.line + 1}: `)).map((f) => f.message)
|
|
7309
7376
|
}));
|
|
7310
7377
|
}),
|
|
7378
|
+
kept: (name) => {
|
|
7379
|
+
const tip = keptTips.get(scoped(name));
|
|
7380
|
+
return tip === void 0 ? void 0 : {
|
|
7381
|
+
hash: tip.hash,
|
|
7382
|
+
changes: changesBetween(tip.before, tip.after)
|
|
7383
|
+
};
|
|
7384
|
+
},
|
|
7311
7385
|
vars: input.vars,
|
|
7312
7386
|
env: input.env,
|
|
7313
7387
|
start: () => input.start,
|
|
@@ -7408,7 +7482,9 @@ const resolveVars = (workflowVars, entryVars, env) => {
|
|
|
7408
7482
|
...entryVars
|
|
7409
7483
|
};
|
|
7410
7484
|
for (const name of Object.keys(merged)) {
|
|
7411
|
-
const
|
|
7485
|
+
const key = PREFIX + name.toUpperCase();
|
|
7486
|
+
if (key.startsWith("GTD_FLAG_")) continue;
|
|
7487
|
+
const value = env[key];
|
|
7412
7488
|
if (value !== void 0) merged[name] = value;
|
|
7413
7489
|
}
|
|
7414
7490
|
return merged;
|
|
@@ -8271,7 +8347,11 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
8271
8347
|
...uncheckStep(snapshot),
|
|
8272
8348
|
{
|
|
8273
8349
|
kind: "gitWrite",
|
|
8274
|
-
write: {
|
|
8350
|
+
write: landing.keepTurn !== void 0 ? {
|
|
8351
|
+
kind: "keepTurn",
|
|
8352
|
+
ref: landing.keepTurn,
|
|
8353
|
+
message
|
|
8354
|
+
} : {
|
|
8275
8355
|
kind: "commitAll",
|
|
8276
8356
|
message
|
|
8277
8357
|
}
|
|
@@ -8746,6 +8826,7 @@ const promptDef = (common, request, skills, access) => ({
|
|
|
8746
8826
|
...optional("model", request.options.model),
|
|
8747
8827
|
...optional("system", request.options.system),
|
|
8748
8828
|
...optional("allowEmpty", request.options.allowEmpty),
|
|
8829
|
+
...optional("keep", request.options.keep),
|
|
8749
8830
|
...optional("skills", skills),
|
|
8750
8831
|
...optional("access", access)
|
|
8751
8832
|
});
|
|
@@ -8860,11 +8941,21 @@ const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(func
|
|
|
8860
8941
|
hash: run.episode.base,
|
|
8861
8942
|
tree: commitTree(workspace, run.episode.base)
|
|
8862
8943
|
};
|
|
8863
|
-
const
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8944
|
+
const git = yield* GitService;
|
|
8945
|
+
const episode = [];
|
|
8946
|
+
for (const c of run.episode.commits) {
|
|
8947
|
+
const tip = parseCommitMessage(c.message).kept;
|
|
8948
|
+
const exists = tip !== void 0 && isSome(yield* git.readRefOption(`${tip}^{commit}`));
|
|
8949
|
+
episode.push({
|
|
8950
|
+
hash: c.hash,
|
|
8951
|
+
message: c.message,
|
|
8952
|
+
tree: commitTree(workspace, c.hash),
|
|
8953
|
+
...exists ? { kept: {
|
|
8954
|
+
hash: tip,
|
|
8955
|
+
tree: commitTree(workspace, tip)
|
|
8956
|
+
} } : {}
|
|
8957
|
+
});
|
|
8958
|
+
}
|
|
8868
8959
|
let pendingMemo;
|
|
8869
8960
|
return {
|
|
8870
8961
|
resolved,
|
|
@@ -9040,6 +9131,7 @@ const closingTrailers = (branch) => ({
|
|
|
9040
9131
|
...isSome(branch) ? { branch: branch.value } : {}
|
|
9041
9132
|
});
|
|
9042
9133
|
const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
|
|
9134
|
+
const keptTurn = (rest) => rest.stepDef.keep === true && rest.changes.length > 0 ? { keepTurn: keptRef(rest.run.startHash, rest.state) } : {};
|
|
9043
9135
|
const landingTo = (rest, to, branch) => {
|
|
9044
9136
|
if (isSettledNoop(rest, to)) return {
|
|
9045
9137
|
kind: "noop",
|
|
@@ -9064,7 +9156,8 @@ const landingTo = (rest, to, branch) => {
|
|
|
9064
9156
|
} : {},
|
|
9065
9157
|
...finishes ? closingTrailers(branch) : {}
|
|
9066
9158
|
},
|
|
9067
|
-
...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {}
|
|
9159
|
+
...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {},
|
|
9160
|
+
...keptTurn(rest)
|
|
9068
9161
|
};
|
|
9069
9162
|
};
|
|
9070
9163
|
/**
|
|
@@ -9201,7 +9294,7 @@ const commitAllowEmpty = (message) => {
|
|
|
9201
9294
|
return [
|
|
9202
9295
|
`if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
|
|
9203
9296
|
` case "$out" in`,
|
|
9204
|
-
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} ;;`,
|
|
9297
|
+
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} || exit 1 ;;`,
|
|
9205
9298
|
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
9206
9299
|
` esac`,
|
|
9207
9300
|
`fi`
|
|
@@ -9223,6 +9316,36 @@ const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
|
|
|
9223
9316
|
* `GitWriterOperations.discardPending` for why it's kept anyway.
|
|
9224
9317
|
*/
|
|
9225
9318
|
const discardPending = () => `git add -A && git reset --hard HEAD`;
|
|
9319
|
+
/**
|
|
9320
|
+
* POSIX sh (runs under dash). The marker is a `commit-tree` over HEAD's own
|
|
9321
|
+
* tree, so no hook runs on it and its tree is the pre-turn tree by
|
|
9322
|
+
* construction; `commit-tree` ignores `commit.gpgSign`, hence the explicit
|
|
9323
|
+
* `-S`, read (and failing closed) before any object or ref is written; the
|
|
9324
|
+
* index is already staged by then, so an early exit leaves the turn staged.
|
|
9325
|
+
* The kept ref is deleted only after `reset -q` succeeds: it is then the
|
|
9326
|
+
* turn's only handle.
|
|
9327
|
+
* The branch moves before the final hard reset, so if that reset fails the
|
|
9328
|
+
* marker has landed and the turn is still in the working tree (kept ref
|
|
9329
|
+
* present); every other failure leaves the branch untouched.
|
|
9330
|
+
*/
|
|
9331
|
+
const keepTurn = (refPrefix, message) => {
|
|
9332
|
+
const keptRef = `${shellQuote(`${refPrefix}/`)}"$gtd_kept"`;
|
|
9333
|
+
return [
|
|
9334
|
+
`git add -A || exit 1`,
|
|
9335
|
+
`gtd_sign_cfg=$(git config --type=bool --default=false commit.gpgSign) || exit 1`,
|
|
9336
|
+
`if [ "$gtd_sign_cfg" = true ]; then gtd_sign=-S; else gtd_sign=--no-gpg-sign; fi`,
|
|
9337
|
+
`gtd_head=$(git rev-parse --verify HEAD) || exit 1`,
|
|
9338
|
+
`gtd_tree=$(git write-tree) || exit 1`,
|
|
9339
|
+
`gtd_kept=$(git commit-tree "$gtd_tree" -p "$gtd_head" -m ${shellQuote(message)}) || exit 1`,
|
|
9340
|
+
`git update-ref ${keptRef} "$gtd_kept" || exit 1`,
|
|
9341
|
+
`gtd_marker=$(printf '%s\\nGtd-Kept: %s' ${shellQuote(message)} "$gtd_kept")`,
|
|
9342
|
+
`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`,
|
|
9343
|
+
` git reset -q && git update-ref -d ${keptRef}`,
|
|
9344
|
+
` exit 1`,
|
|
9345
|
+
`fi`,
|
|
9346
|
+
`git reset --hard HEAD`
|
|
9347
|
+
].join("\n");
|
|
9348
|
+
};
|
|
9226
9349
|
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
9227
9350
|
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
9228
9351
|
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
@@ -9255,6 +9378,7 @@ const renderGitWrite = (write) => {
|
|
|
9255
9378
|
switch (write.kind) {
|
|
9256
9379
|
case "commitAll": return commitAll(write.message);
|
|
9257
9380
|
case "updateRef": return updateRef(write.ref, write.target);
|
|
9381
|
+
case "keepTurn": return keepTurn(write.ref, write.message);
|
|
9258
9382
|
}
|
|
9259
9383
|
};
|
|
9260
9384
|
/**
|
|
@@ -11303,7 +11427,10 @@ const runCli = (argv, io) => {
|
|
|
11303
11427
|
io.exit(2);
|
|
11304
11428
|
});
|
|
11305
11429
|
const out = bufferedArtifactOut(io);
|
|
11306
|
-
return
|
|
11430
|
+
return flatMap$4(Host, ({ env }) => try_({
|
|
11431
|
+
try: () => resolveFlags(env, io.flags),
|
|
11432
|
+
catch: (e) => new GtdError(e.message)
|
|
11433
|
+
})).pipe(flatMap$4(() => runCommand$1(plan.command, plan.json, out)), map$4(() => {
|
|
11307
11434
|
out.flush();
|
|
11308
11435
|
io.exit(0);
|
|
11309
11436
|
}), provide$2(io.layers(plan.verbose)), sandbox, catchAll$1((cause) => isInterruptedOnly(cause) ? failCause$4(cause) : report(io, plan.json.kind !== "off")(cause)));
|
|
@@ -11381,6 +11508,7 @@ var InMemRepo = class InMemRepo {
|
|
|
11381
11508
|
return this.head !== null;
|
|
11382
11509
|
}
|
|
11383
11510
|
resolveRef(ref) {
|
|
11511
|
+
if (ref.endsWith("^{commit}")) return this.resolveRef(ref.slice(0, -9));
|
|
11384
11512
|
if (/^[0-9a-f]{40}$/.test(ref)) return this.commits.has(ref) ? ref : null;
|
|
11385
11513
|
const tildeMatch = /^(HEAD|[0-9a-f]{40})(~(\d+)|\^)?$/.exec(ref);
|
|
11386
11514
|
if (tildeMatch) {
|
|
@@ -11553,8 +11681,12 @@ var InMemRepo = class InMemRepo {
|
|
|
11553
11681
|
stageAll() {
|
|
11554
11682
|
this.index = new Map(this.worktree);
|
|
11555
11683
|
}
|
|
11556
|
-
/**
|
|
11557
|
-
|
|
11684
|
+
/** Seeded failure modes (see `GitTierSeed`): a pre-commit hook printing this and exiting 1, and a signer that cannot write. */
|
|
11685
|
+
hookRejection = void 0;
|
|
11686
|
+
signingFails = false;
|
|
11687
|
+
/** Seeded: a pre-commit hook writing and staging this file. */
|
|
11688
|
+
hookStages = void 0;
|
|
11689
|
+
writeCommit(message) {
|
|
11558
11690
|
const tree = new Map(this.index);
|
|
11559
11691
|
const parent = this.head;
|
|
11560
11692
|
const hash = makeHash(message, parent, tree);
|
|
@@ -11569,27 +11701,59 @@ var InMemRepo = class InMemRepo {
|
|
|
11569
11701
|
this.head = hash;
|
|
11570
11702
|
this.branches.set(this.currentBranch, hash);
|
|
11571
11703
|
}
|
|
11572
|
-
/**
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11704
|
+
/**
|
|
11705
|
+
* Twin of the emitted `git commit --allow-empty` if/case retry: a hook
|
|
11706
|
+
* rejection printing "empty git commit" retries without hooks, any other
|
|
11707
|
+
* rejection fails; a failing signer fails either attempt. Commits whatever is
|
|
11708
|
+
* staged verbatim, like `git commit` after a soft reset.
|
|
11709
|
+
*/
|
|
11710
|
+
commitAsIs(message) {
|
|
11711
|
+
if (this.hookRejection !== void 0 && !this.hookRejection.includes("empty git commit")) return this.hookRejection;
|
|
11712
|
+
if (this.signingFails) return "error: gpg failed to sign the data";
|
|
11713
|
+
if (this.hookStages !== void 0) {
|
|
11714
|
+
this.worktree.set(this.hookStages.path, this.hookStages.content);
|
|
11715
|
+
this.index.set(this.hookStages.path, this.hookStages.content);
|
|
11716
|
+
}
|
|
11717
|
+
this.writeCommit(message);
|
|
11576
11718
|
}
|
|
11577
|
-
|
|
11719
|
+
/** Setup commit: stages everything, and throws if a failure is seeded, as the Live tier's hooks and signer would. */
|
|
11720
|
+
seedCommit(message) {
|
|
11721
|
+
if (this.hookRejection !== void 0 || this.hookStages !== void 0 || this.signingFails) throw new Error("seedCommit after a seeded commit failure");
|
|
11578
11722
|
this.index = new Map(this.worktree);
|
|
11723
|
+
this.writeCommit(message);
|
|
11724
|
+
}
|
|
11725
|
+
/** Twin of `GitScript.keepTurn`: no hook runs; `signingFails` fails the marker. */
|
|
11726
|
+
keepTurn(refPrefix, message) {
|
|
11727
|
+
this.stageAll();
|
|
11579
11728
|
const tree = new Map(this.index);
|
|
11580
|
-
const message = prefix;
|
|
11581
11729
|
const parent = this.head;
|
|
11582
11730
|
const hash = makeHash(message, parent, tree);
|
|
11583
|
-
|
|
11731
|
+
this.commits.set(hash, {
|
|
11584
11732
|
hash,
|
|
11585
11733
|
message,
|
|
11586
11734
|
files: new Map(tree),
|
|
11587
11735
|
parent,
|
|
11588
11736
|
seq: this.nextSeq++
|
|
11589
|
-
};
|
|
11590
|
-
|
|
11591
|
-
this.
|
|
11592
|
-
this.
|
|
11737
|
+
});
|
|
11738
|
+
const ref = `${refPrefix}/${hash}`;
|
|
11739
|
+
this.refs.set(ref, hash);
|
|
11740
|
+
this.index = new Map(this.headTree());
|
|
11741
|
+
if (this.signingFails) {
|
|
11742
|
+
this.refs.delete(ref);
|
|
11743
|
+
return "error: gpg failed to sign the data";
|
|
11744
|
+
}
|
|
11745
|
+
this.writeCommit(`${message}\nGtd-Kept: ${hash}`);
|
|
11746
|
+
this.index = new Map(tree);
|
|
11747
|
+
this.resetHard();
|
|
11748
|
+
}
|
|
11749
|
+
/** Discard every pending change, tracked or untracked: stage everything, then hard-reset (which drops the freshly-staged untracked paths too). */
|
|
11750
|
+
discardPending() {
|
|
11751
|
+
this.index = new Map(this.worktree);
|
|
11752
|
+
this.resetHard();
|
|
11753
|
+
}
|
|
11754
|
+
commitAllWithPrefix(prefix) {
|
|
11755
|
+
this.index = new Map(this.worktree);
|
|
11756
|
+
return this.commitAsIs(prefix);
|
|
11593
11757
|
}
|
|
11594
11758
|
static EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
11595
11759
|
softResetTo(ref) {
|
|
@@ -11742,6 +11906,7 @@ const makeGitReaderOps = (repo, root) => ({
|
|
|
11742
11906
|
return content !== null ? succeed$6(content) : fail$5(/* @__PURE__ */ new Error(`path not found at ${ref}: ${path}`));
|
|
11743
11907
|
},
|
|
11744
11908
|
readRefOption: (ref) => {
|
|
11909
|
+
if (/^[0-9a-f]{40}$/.test(ref)) return succeed$6(some(ref));
|
|
11745
11910
|
const hash = repo.resolveRef(ref);
|
|
11746
11911
|
return succeed$6(hash !== null ? some(hash) : none());
|
|
11747
11912
|
},
|
|
@@ -11764,10 +11929,13 @@ const makeGitReaderOps = (repo, root) => ({
|
|
|
11764
11929
|
subjectHistory: (pageSize, skip, head) => succeed$6(repo.subjectHistory(pageSize, skip, head)),
|
|
11765
11930
|
changedPaths: (base) => succeed$6(repo.changedPathsWorktree(base))
|
|
11766
11931
|
});
|
|
11932
|
+
const failOn = (output) => {
|
|
11933
|
+
if (output !== void 0) throw new Error(output);
|
|
11934
|
+
};
|
|
11767
11935
|
const makeGitWriterOps = (repo) => ({
|
|
11768
|
-
commitAllWithPrefix: (prefix) => tryCatch(() => repo.commitAllWithPrefix(prefix)),
|
|
11936
|
+
commitAllWithPrefix: (prefix) => tryCatch(() => failOn(repo.commitAllWithPrefix(prefix))),
|
|
11769
11937
|
softResetTo: (ref) => tryCatch(() => repo.softResetTo(ref)),
|
|
11770
|
-
commitAsIs: (message) => tryCatch(() => repo.commitAsIs(message)),
|
|
11938
|
+
commitAsIs: (message) => tryCatch(() => failOn(repo.commitAsIs(message))),
|
|
11771
11939
|
discardPending: () => tryCatch(() => repo.discardPending()),
|
|
11772
11940
|
updateRef: (ref, hash) => tryCatch(() => repo.updateRef(ref, hash)),
|
|
11773
11941
|
deleteRef: (ref) => tryCatch(() => repo.deleteRef(ref)),
|
|
@@ -12270,25 +12438,51 @@ const conditionStatement = (block) => {
|
|
|
12270
12438
|
const end = block.indexOf(CONDITION_TERMINATOR, start);
|
|
12271
12439
|
return end === -1 ? block.slice(start) : block.slice(start, end);
|
|
12272
12440
|
};
|
|
12441
|
+
/** `keepTurn`: an invalid ref name fails before anything moves, as `git update-ref` does. */
|
|
12442
|
+
const recognizeKeepTurn = (repo, block) => {
|
|
12443
|
+
const [message, refDir] = extractQuotedTokens(block);
|
|
12444
|
+
const ref = refDir?.slice(0, -1);
|
|
12445
|
+
if (message === void 0 || ref === void 0 || keepTurn(ref, message) !== block) return;
|
|
12446
|
+
if (refNameFault(ref) !== void 0) return {
|
|
12447
|
+
kind: "failed",
|
|
12448
|
+
error: `fatal: invalid ref format: ${ref}`,
|
|
12449
|
+
status: 128
|
|
12450
|
+
};
|
|
12451
|
+
const failure = repo.keepTurn(ref, message);
|
|
12452
|
+
return failure === void 0 ? { kind: "applied" } : {
|
|
12453
|
+
kind: "failed",
|
|
12454
|
+
error: failure,
|
|
12455
|
+
status: 1
|
|
12456
|
+
};
|
|
12457
|
+
};
|
|
12273
12458
|
const recognizeGitBuilders = (repo, block) => {
|
|
12274
12459
|
if (block === discardPending()) {
|
|
12275
12460
|
repo.discardPending();
|
|
12276
12461
|
return { kind: "applied" };
|
|
12277
12462
|
}
|
|
12278
12463
|
const conditionLine = conditionStatement(block);
|
|
12464
|
+
if (block.startsWith("git add -A || exit 1\ngtd_sign_cfg=")) return recognizeKeepTurn(repo, block);
|
|
12279
12465
|
if (block.startsWith("git add -A || exit 1\n")) {
|
|
12280
12466
|
const [message] = extractQuotedTokens(conditionLine);
|
|
12281
12467
|
if (message !== void 0 && commitAll(message) === block) {
|
|
12282
|
-
repo.commitAllWithPrefix(message);
|
|
12283
|
-
return { kind: "applied" }
|
|
12468
|
+
const failure = repo.commitAllWithPrefix(message);
|
|
12469
|
+
return failure === void 0 ? { kind: "applied" } : {
|
|
12470
|
+
kind: "failed",
|
|
12471
|
+
error: failure,
|
|
12472
|
+
status: 1
|
|
12473
|
+
};
|
|
12284
12474
|
}
|
|
12285
12475
|
return;
|
|
12286
12476
|
}
|
|
12287
12477
|
if (block.startsWith("if ! out=$(git commit --allow-empty -m ")) {
|
|
12288
12478
|
const [message] = extractQuotedTokens(conditionLine);
|
|
12289
12479
|
if (message !== void 0 && commitAsIs(message) === block) {
|
|
12290
|
-
repo.commitAsIs(message);
|
|
12291
|
-
return { kind: "applied" }
|
|
12480
|
+
const failure = repo.commitAsIs(message);
|
|
12481
|
+
return failure === void 0 ? { kind: "applied" } : {
|
|
12482
|
+
kind: "failed",
|
|
12483
|
+
error: failure,
|
|
12484
|
+
status: 1
|
|
12485
|
+
};
|
|
12292
12486
|
}
|
|
12293
12487
|
return;
|
|
12294
12488
|
}
|
|
@@ -12825,7 +13019,7 @@ const testRepo = (options) => {
|
|
|
12825
13019
|
const id = `module-${++registered}`;
|
|
12826
13020
|
repo.writeFile("gtd.config.ts", typeof workflow === "string" ? workflow : moduleSource(id, workflow));
|
|
12827
13021
|
for (const [path, content] of Object.entries(options.files ?? {})) repo.writeFile(path, content);
|
|
12828
|
-
repo.
|
|
13022
|
+
repo.seedCommit("chore: initial commit");
|
|
12829
13023
|
const gtd = async (...args) => {
|
|
12830
13024
|
const { io, result } = makeCapturingCliIo(repo, env);
|
|
12831
13025
|
if (typeof workflow !== "string") registry().set(id, workflow);
|
|
@@ -12893,7 +13087,7 @@ const testRepo = (options) => {
|
|
|
12893
13087
|
read: (path) => repo.readFile(path),
|
|
12894
13088
|
write: (path, content) => repo.writeFile(path, content),
|
|
12895
13089
|
remove: (path) => repo.deleteFile(path),
|
|
12896
|
-
commit: (message) => repo.
|
|
13090
|
+
commit: (message) => repo.seedCommit(message),
|
|
12897
13091
|
show: (ref, path) => repo.fileAtRef(ref, path) ?? void 0,
|
|
12898
13092
|
runScript: (script) => {
|
|
12899
13093
|
const flow = applyFlowScript(repo, /* @__PURE__ */ new Map(), script);
|
|
@@ -12933,6 +13127,14 @@ const testRepo = (options) => {
|
|
|
12933
13127
|
};
|
|
12934
13128
|
//#endregion
|
|
12935
13129
|
//#region src/testkit/GitTiers.ts
|
|
13130
|
+
const writeRejectingHook = (hooksDir, output) => {
|
|
13131
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
13132
|
+
writeFileSync(join(hooksDir, "pre-commit"), `#!/bin/sh\ncat >&2 <<'GTD_HOOK_EOF'\n${output}\nGTD_HOOK_EOF\nexit 1\n`, { mode: 493 });
|
|
13133
|
+
};
|
|
13134
|
+
const writeStagingHook = (hooksDir, path, content) => {
|
|
13135
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
13136
|
+
writeFileSync(join(hooksDir, "pre-commit"), `#!/bin/sh\nprintf '%s' ${shellQuote(content)} > ${shellQuote(path)} && git add ${shellQuote(path)}\nexit 0\n`, { mode: 493 });
|
|
13137
|
+
};
|
|
12936
13138
|
/** No-op — these tests assert on git/config behavior, not narration. */
|
|
12937
13139
|
const noopNarratorLayer = Narrator.layer(() => {}, false);
|
|
12938
13140
|
const gitExecIn = (dir, ...args) => execSync(`git ${args.join(" ")}`, {
|
|
@@ -12982,7 +13184,6 @@ const makeLiveTier = (initialCommit = true) => {
|
|
|
12982
13184
|
root,
|
|
12983
13185
|
capabilities: {
|
|
12984
13186
|
onDiskContent: true,
|
|
12985
|
-
commitHooks: true,
|
|
12986
13187
|
linkedWorktrees: true,
|
|
12987
13188
|
directoryChainConfig: true,
|
|
12988
13189
|
remotes: true
|
|
@@ -13010,7 +13211,22 @@ const makeLiveTier = (initialCommit = true) => {
|
|
|
13010
13211
|
},
|
|
13011
13212
|
stageAll: () => gitExec("add", "-A"),
|
|
13012
13213
|
updateRef: (ref, hash) => gitExec("update-ref", ref, hash),
|
|
13013
|
-
mixedReset: (ref) => gitExec("reset", "--mixed", ref)
|
|
13214
|
+
mixedReset: (ref) => gitExec("reset", "--mixed", ref),
|
|
13215
|
+
rejectingHook: (output) => {
|
|
13216
|
+
const hooks = join(root, ".git/hooks");
|
|
13217
|
+
gitExec("config", "core.hooksPath", hooks);
|
|
13218
|
+
writeRejectingHook(hooks, output);
|
|
13219
|
+
},
|
|
13220
|
+
stagingHook: (path, content) => {
|
|
13221
|
+
const hooks = join(root, ".git/hooks");
|
|
13222
|
+
gitExec("config", "core.hooksPath", hooks);
|
|
13223
|
+
writeStagingHook(hooks, path, content);
|
|
13224
|
+
},
|
|
13225
|
+
failingSigner: () => {
|
|
13226
|
+
gitExec("config", "commit.gpgsign", "true");
|
|
13227
|
+
gitExec("config", "gpg.format", "openpgp");
|
|
13228
|
+
gitExec("config", "gpg.program", "false");
|
|
13229
|
+
}
|
|
13014
13230
|
},
|
|
13015
13231
|
observe: {
|
|
13016
13232
|
resolveRef: (ref) => gitExec("rev-parse", ref),
|
|
@@ -13076,7 +13292,7 @@ const makeInMemTier = (initialCommit = true) => {
|
|
|
13076
13292
|
const repo = new InMemRepo();
|
|
13077
13293
|
if (initialCommit) {
|
|
13078
13294
|
repo.writeFile("readme.txt", "hello");
|
|
13079
|
-
repo.
|
|
13295
|
+
repo.seedCommit("init: first commit");
|
|
13080
13296
|
}
|
|
13081
13297
|
const gitLayer = gitTestLayer(repo, IN_MEM_ROOT);
|
|
13082
13298
|
const provide = (eff) => runPromise(eff.pipe(provide$2(gitLayer), provide$2(noopNarratorLayer)));
|
|
@@ -13086,7 +13302,6 @@ const makeInMemTier = (initialCommit = true) => {
|
|
|
13086
13302
|
root: IN_MEM_ROOT,
|
|
13087
13303
|
capabilities: {
|
|
13088
13304
|
onDiskContent: false,
|
|
13089
|
-
commitHooks: false,
|
|
13090
13305
|
linkedWorktrees: false,
|
|
13091
13306
|
directoryChainConfig: true,
|
|
13092
13307
|
remotes: false
|
|
@@ -13097,17 +13312,29 @@ const makeInMemTier = (initialCommit = true) => {
|
|
|
13097
13312
|
seed: {
|
|
13098
13313
|
commit: (message, files = { "file.txt": message }) => {
|
|
13099
13314
|
for (const [path, content] of Object.entries(files)) repo.writeFile(path, content);
|
|
13100
|
-
repo.
|
|
13315
|
+
repo.seedCommit(message);
|
|
13101
13316
|
},
|
|
13102
13317
|
writeFile: (path, content) => repo.writeFile(path, content),
|
|
13103
13318
|
deleteFile: (path) => repo.deleteFile(path),
|
|
13104
13319
|
commitDeletion: (path, message) => {
|
|
13105
13320
|
repo.deleteFile(path);
|
|
13106
|
-
repo.
|
|
13321
|
+
repo.seedCommit(message);
|
|
13107
13322
|
},
|
|
13108
13323
|
stageAll: () => repo.stageAll(),
|
|
13109
13324
|
updateRef: (ref, hash) => repo.updateRef(ref, hash),
|
|
13110
|
-
mixedReset: (ref) => repo.mixedResetTo(ref)
|
|
13325
|
+
mixedReset: (ref) => repo.mixedResetTo(ref),
|
|
13326
|
+
rejectingHook: (output) => {
|
|
13327
|
+
repo.hookRejection = output;
|
|
13328
|
+
},
|
|
13329
|
+
stagingHook: (path, content) => {
|
|
13330
|
+
repo.hookStages = {
|
|
13331
|
+
path,
|
|
13332
|
+
content
|
|
13333
|
+
};
|
|
13334
|
+
},
|
|
13335
|
+
failingSigner: () => {
|
|
13336
|
+
repo.signingFails = true;
|
|
13337
|
+
}
|
|
13111
13338
|
},
|
|
13112
13339
|
observe: {
|
|
13113
13340
|
resolveRef: (ref) => {
|
|
@@ -13438,16 +13665,6 @@ const runGitServiceContract = (makeTier) => {
|
|
|
13438
13665
|
const history = await runGit(t, (g) => g.commitHistory());
|
|
13439
13666
|
expect(history[history.length - 1]?.message).toBe("gtd: grilled");
|
|
13440
13667
|
});
|
|
13441
|
-
it.skipIf(!capabilities.commitHooks)("retries with --no-verify when a hook blocks the empty commit", async () => {
|
|
13442
|
-
const hookPath = join(t.root, ".git/hooks/pre-commit");
|
|
13443
|
-
writeFileSync(hookPath, `#!/bin/sh\necho "lint-staged prevented an empty git commit." >&2\nexit 1\n`);
|
|
13444
|
-
execSync(`chmod +x "${hookPath}"`);
|
|
13445
|
-
const headBefore = t.observe.resolveRef("HEAD");
|
|
13446
|
-
await runGit(t, (g) => g.commitAllWithPrefix("gtd: grilled"));
|
|
13447
|
-
expect(t.observe.resolveRef("HEAD")).not.toBe(headBefore);
|
|
13448
|
-
const history = await runGit(t, (g) => g.commitHistory());
|
|
13449
|
-
expect(history[history.length - 1]?.message).toBe("gtd: grilled");
|
|
13450
|
-
});
|
|
13451
13668
|
});
|
|
13452
13669
|
describe("commitAsIs", () => {
|
|
13453
13670
|
it("commits exactly what's staged, ignoring later worktree writes (the squash mechanic)", async () => {
|
|
@@ -13466,6 +13683,29 @@ const runGitServiceContract = (makeTier) => {
|
|
|
13466
13683
|
expect(t.observe.resolveRef("HEAD")).not.toBe(headBefore);
|
|
13467
13684
|
});
|
|
13468
13685
|
});
|
|
13686
|
+
describe.each([["commitAllWithPrefix", (g) => g.commitAllWithPrefix("gtd: building")], ["commitAsIs", (g) => g.commitAsIs("gtd: building")]])("%s seeded commit failures", (_name, commit) => {
|
|
13687
|
+
it("retries with --no-verify when a hook blocks the empty commit", async () => {
|
|
13688
|
+
t.seed.rejectingHook("lint-staged prevented an empty git commit.");
|
|
13689
|
+
const headBefore = t.observe.resolveRef("HEAD");
|
|
13690
|
+
await runGit(t, commit);
|
|
13691
|
+
expect(t.observe.resolveRef("HEAD")).not.toBe(headBefore);
|
|
13692
|
+
const history = await runGit(t, (g) => g.commitHistory());
|
|
13693
|
+
expect(history[history.length - 1]?.message).toBe("gtd: building");
|
|
13694
|
+
});
|
|
13695
|
+
it("fails on a hook rejection that is not the empty-commit one, leaving HEAD unchanged", async () => {
|
|
13696
|
+
t.seed.rejectingHook("lint error");
|
|
13697
|
+
const headBefore = t.observe.resolveRef("HEAD");
|
|
13698
|
+
expect(isFailure(await runGitExit(t, commit))).toBe(true);
|
|
13699
|
+
expect(t.observe.resolveRef("HEAD")).toBe(headBefore);
|
|
13700
|
+
});
|
|
13701
|
+
it("fails both attempts when the signer cannot write, leaving HEAD unchanged", async () => {
|
|
13702
|
+
t.seed.rejectingHook("rejected: empty git commit");
|
|
13703
|
+
t.seed.failingSigner();
|
|
13704
|
+
const headBefore = t.observe.resolveRef("HEAD");
|
|
13705
|
+
expect(isFailure(await runGitExit(t, commit))).toBe(true);
|
|
13706
|
+
expect(t.observe.resolveRef("HEAD")).toBe(headBefore);
|
|
13707
|
+
});
|
|
13708
|
+
});
|
|
13469
13709
|
describe("softResetTo", () => {
|
|
13470
13710
|
it("moves HEAD back but keeps worktree changes from second commit", async () => {
|
|
13471
13711
|
const firstHash = t.observe.resolveRef("HEAD");
|
|
@@ -13513,6 +13753,20 @@ const runGitServiceContract = (makeTier) => {
|
|
|
13513
13753
|
it("readRefOption is None for a ref that doesn't exist — never a failure", async () => {
|
|
13514
13754
|
expect((await runGit(t, (g) => g.readRefOption("refs/gtd/nonexistent")))._tag).toBe("None");
|
|
13515
13755
|
});
|
|
13756
|
+
it("readRefOption of <hash>^{commit} is Some for an existing commit", async () => {
|
|
13757
|
+
const head = t.observe.resolveRef("HEAD");
|
|
13758
|
+
const resolved = await runGit(t, (g) => g.readRefOption(`${head}^{commit}`));
|
|
13759
|
+
expect(resolved._tag).toBe("Some");
|
|
13760
|
+
if (resolved._tag === "Some") expect(resolved.value).toBe(head);
|
|
13761
|
+
});
|
|
13762
|
+
it("readRefOption of <hash>^{commit} is None for a 40-hex hash with no object", async () => {
|
|
13763
|
+
const missing = "0123456789abcdef0123456789abcdef01234567";
|
|
13764
|
+
expect((await runGit(t, (g) => g.readRefOption(`${missing}^{commit}`)))._tag).toBe("None");
|
|
13765
|
+
});
|
|
13766
|
+
it("readRefOption of a bare 40-hex hash with no object is Some", async () => {
|
|
13767
|
+
const missing = "0123456789abcdef0123456789abcdef01234567";
|
|
13768
|
+
expect((await runGit(t, (g) => g.readRefOption(missing)))._tag).toBe("Some");
|
|
13769
|
+
});
|
|
13516
13770
|
it("deleteRef removes a ref idempotently (deleting twice is a no-op)", async () => {
|
|
13517
13771
|
const head = t.observe.resolveRef("HEAD");
|
|
13518
13772
|
await runGit(t, (g) => g.updateRef("refs/gtd/marker", head));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"src/cli/",
|
|
70
70
|
"src/edge/",
|
|
71
71
|
"src/emit/",
|
|
72
|
+
"src/flags/",
|
|
72
73
|
"src/install/",
|
|
73
74
|
"src/platform/",
|
|
74
75
|
"src/replay/",
|