@pmelab/gtd 24.1.0 → 24.3.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 +37 -10
- package/claude/hooks/access.ts +25 -0
- package/claude/hooks/assist.ts +135 -0
- package/claude/hooks/drive.ts +15 -7
- package/claude/hooks/judge.ts +27 -0
- package/claude/hooks/register.tsx +294 -135
- package/claude/hooks/tools.ts +121 -0
- package/claude/hooks/wording.ts +2 -35
- package/claude/types/index.d.ts +9 -0
- package/dist/chunks/{Schema-Dh-mkwkq.mjs → Schema-B5yK7QrT.mjs} +47 -1
- package/dist/chunks/{reserved-ejn-LVYO.mjs → reserved-DJwU5C4z.mjs} +1 -1
- package/dist/gtd.bundle.mjs +337 -55
- package/dist/testkit.mjs +477 -65
- package/dist/workflows/feature.mjs +1 -1
- package/dist/workflows/fix.mjs +1 -1
- package/dist/workflows/review.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +57 -0
- package/src/cli/Cli.ts +89 -9
- package/src/cli/History.test.ts +225 -0
- package/src/cli/History.ts +143 -0
- package/src/cli/Output.ts +68 -0
- package/src/cli/Program.test.ts +7 -1
- package/src/cli/Program.ts +23 -64
- package/src/edge/Edge.test.ts +256 -1
- package/src/edge/Edge.ts +217 -39
- package/src/edge/index.ts +1 -0
- package/src/emit/GitScript.test.ts +27 -0
- package/src/emit/GitScript.ts +18 -2
- package/src/emit/index.ts +1 -0
- package/src/platform/Git.ts +48 -10
- package/src/platform/RetainedHistory.test.ts +14 -0
- package/src/platform/RetainedHistory.ts +5 -0
- package/src/platform/index.ts +7 -1
- package/src/replay/Trailers.test.ts +32 -0
- package/src/replay/Trailers.ts +17 -0
- package/src/step/LandStep.ts +3 -1
- package/src/step/RepoSnapshot.ts +7 -1
- package/src/step/planStart.test.ts +6 -3
- package/src/step/planStep.test.ts +29 -1
- package/src/step/planStep.ts +8 -0
- package/src/testkit/EmittedScriptRecognizer.ts +1 -1
- package/src/testkit/FakeGitOperations.ts +6 -2
- package/src/testkit/GitTiers.ts +133 -2
- package/src/testkit/InMemRepo.ts +26 -6
package/dist/testkit.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { configProblems, configure, installContext, isFactoryWorkflow, protocolMismatch, t as flows_exports } from "./flows.mjs";
|
|
2
2
|
import { O as renderFormat, T as ACCESS_REFUSAL, _ as demandOf, a as landFields, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, i as judgeJson, l as renderLandPlain, m as statusOf, n as resolveSession, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches } from "./chunks/wire-DA7q2ikx.mjs";
|
|
3
3
|
import { C as headingSections, S as headingSectionBodies, a as unansweredQuestions, b as parseThreadsWithFindings, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-D-PvRC5W.mjs";
|
|
4
|
-
import { $ as makeSemaphore, $i as
|
|
4
|
+
import { $ as makeSemaphore, $i as toJSON, $n as mapBoth, $r as none$1, $t as provideMerge$1, A as catchTag, Ai as getOption, An as scopeTag, Ar as interrupt$2, At as sync$2, B as flatten$4, Bi as isSome, Bn as make$16, Br as tap$1, Bt as unsafeMakeSemaphore$1, C as all$1, Ci as unsafeFromArray, Cn as isInterruptedOnly, Cr as exitFailCause, Ct as scope, D as async, Di as Tag, Dn as ensuring$3, Dr as fiberId, Dt as serviceOption, E as asVoid$1, Ei as GenericTag, En as squash, Er as exitZipRight, Et as scopedWith$1, F as exit, Fi as filter$1, Fn as sequential, Fr as onInterrupt, Ft as tryPromise, G as forkScoped, Gi as isLeft, Gn as Class, Gr as withFiberRuntime, Gt as zipRight$3, H as forEach$2, Hi as match$4, Hn as set$1, Hr as uninterruptibleMask$1, Ht as withFiberRuntime$1, I as fail$5, Ii as flatMap$5, In as bind, Ir as scopeClose, It as try_, J as interrupt$4, Ji as right, Jn as die, Jr as empty$6, Jt as TaggedError$1, K as gen, Ki as left, Kn as spanTag, Kr as zipRight$1, Kt as zipWith, L as failCause$4, Li as fromNullable, Ln as bindTo, Lr as succeed$4, Lt as uninterruptible$1, M as dieMessage, Mi as empty$5, Mn as inheritAll$1, Mr as interruptFiber, Mt as tapErrorCause, N as either, Ni as fromIterable, Nn as interruptAllAs$1, Nr as interruptible$1, Nt as timeout, O as catchAll$1, Oi as add, On as forEachConcurrentDiscard, Or as fiberRefLocally, Ot as succeed$6, P as ensuring$2, Pi as splitAt, Pn as join$3, Pr as map$3, Pt as tryMap, Q as logWarning, Qi as format, Qn as map$5, Qr as make$19, Qt as provide$1, R as fiberIdWith, Ri as getOrElse, Rn as sleep, Rr as suspend$4, Rt as uninterruptibleMask, S as addFinalizer$1, Si as toArray, Sn as isInterrupted, Sr as exitFail, St as sandbox, T as as, Ti as unsafeHead, Tn as reduceWithContext, Tr as exitVoid, Tt as scoped$4, U as forkDaemon, Ui as none, Un as update$1, Ur as void_$2, Ut as zip$1, V as fnUntraced, Vi as map$8, Vn as modify$1, Vr as uninterruptible, Vt as when, W as forkIn, Wi as some, Wn as TypeId$10, Wr as whenEffect, Wt as zipLeft, X as intoDeferred, Xi as BaseProto, Xn as failCause$3, Xr as combineAll, Xt as fromEffect$5, Y as interruptible, Yi as pipeArguments, Yn as fail$4, Yr as combine, Yt as tagged, Z as isEffect, Zi as NodeInspectSymbol, Zn as isFailure, Zr as ids, Zt as mergeAll$2, _ as decodeSync, _i as prepend, _n as failureOrCause, _r as deferredMake, _t as repeat, a as Literal, aa as constVoid, ai as decode, an as forkIn$1, ar as as$1, at as matchCauseEffect, b as acquireRelease, bi as take$2, bn as interruptors, br as deferredUnsafeMake, bt as runPromiseExit$1, c as String$, ca as pipe, ci as appendAll, cn as addFinalizer, cr as catchAll, ct as onError, d as Union, di as filter, dn as extend, dr as deferredComplete, dt as orElseSucceed, ea as equals, ei as get$4, en as scoped$3, er as match$3, et as map$4, f as between, fi as fromIterable$1, fn as fork, fr as deferredFail, ft as promise, gi as of, gn as fail$3, gr as deferredIsDone, gt as raceWith, hi as map$7, hn as die$1, hr as deferredInterruptWith, ht as race, i as Defect, ia as constUndefined, ii as size$2, in as updateContext$1, ir as zipRight$2, it as matchCause, j as die$2, ji as omit, jn as _await$2, jr as interruptAsFiber, jt as tap, k as catchAllCause, ki as get$5, kn as scopeMake, kr as flatMap$3, kt as suspend$5, l as Struct, li as drop, ln as addFinalizerExit, lr as currentContext$1, lt as orDie, m as optional$1, mi as isEmpty$2, mn as NoSuchElementException, mr as deferredInterrupt, mt as provideService, n as Boolean$, na as constFalse, ni as set$2, nn as unsafeFork, nr as void_$3, nt as mapError, o as Never, oa as dual, oi as isFinite, on as unsafeMakeSemaphore, or as asVoid, ot as matchEffect, p as decodeUnknown, pi as isChunk, pn as make$18, pr as deferredFailCause, pt as provide$2, q as ignore, qi as match$2, qn as all, qr as DeferredTypeId$1, qt as spaced$1, r as Cause, ra as constTrue, ri as has, rn as unsafeRunPromiseExit, rr as zip$2, rt as match$5, s as Number$, sa as identity, si as append$1, sn as Scope, sr as asyncInterrupt, st as negate, t as Array$, ta as hasProperty, ti as make$17, tn as succeed$3, tr as succeed$5, tt as mapBoth$1, u as TaggedError, ui as empty$4, un as close, ur as deferredAwait, ut as orElse, v as encodeSync, vi as reduce, vn as flipCauseOption, vr as deferredSucceed, vt as retry, w as annotateLogs, wi as unsafeGet, wn as map$6, wr as exitSucceed, wt as scopeWith, x as acquireUseRelease$2, xi as takeRight, xn as isDieType, xr as exitAs, xt as runtime, y as _void, yi as reverse, yn as interrupt$3, yr as deferredUnsafeDone, yt as runPromise, z as flatMap$4, zi as isNone, zn as get$3, zr as sync$1, zt as unsafeMakeLatch } from "./chunks/Schema-B5yK7QrT.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import * as Crypto from "node:crypto";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import * as Path from "node:path";
|
|
9
|
-
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
9
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { PassThrough } from "node:stream";
|
|
11
11
|
import * as NFS from "node:fs";
|
|
12
12
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -5388,8 +5388,17 @@ const resolvedRefOrCorrupted = (ref, hash) => /^[0-9a-f]{40}$/.test(hash) ? succ
|
|
|
5388
5388
|
const makeGitImpl = (executor, root) => {
|
|
5389
5389
|
const exec = (...args) => run(root, ...args).pipe(provide$2(succeed$1(CommandExecutor, executor)));
|
|
5390
5390
|
return {
|
|
5391
|
-
lastCommitSubject: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%s", ref).pipe(map$4((s) => s.trim())),
|
|
5392
|
-
lastCommitMessage: () => exec("git", "log", "-1", "--pretty=%B").pipe(map$4((s) => s.trim())),
|
|
5391
|
+
lastCommitSubject: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%s", "--end-of-options", ref).pipe(map$4((s) => s.trim())),
|
|
5392
|
+
lastCommitMessage: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%B", "--end-of-options", ref).pipe(map$4((s) => s.trim())),
|
|
5393
|
+
currentBranch: () => exec("git", "symbolic-ref", "--quiet", "--short", "HEAD").pipe(map$4((s) => s.trim()), map$4((name) => some(name)), catchAll$1(() => succeed$6(none()))),
|
|
5394
|
+
listRefs: (prefix) => exec("git", "for-each-ref", "--sort=-committerdate", "--format=%(refname)%09%(objectname)%09%(committerdate:iso-strict)", prefix).pipe(map$4((out) => out.split("\n").filter((line) => line.length > 0).map((line) => {
|
|
5395
|
+
const [ref = "", hash = "", date = ""] = line.split(" ");
|
|
5396
|
+
return {
|
|
5397
|
+
ref,
|
|
5398
|
+
hash,
|
|
5399
|
+
date
|
|
5400
|
+
};
|
|
5401
|
+
}))),
|
|
5393
5402
|
hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll$1(() => succeed$6(false))),
|
|
5394
5403
|
readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
|
|
5395
5404
|
changedPaths: (base) => gen(function* () {
|
|
@@ -5409,7 +5418,7 @@ const makeGitImpl = (executor, root) => {
|
|
|
5409
5418
|
}
|
|
5410
5419
|
return all;
|
|
5411
5420
|
}),
|
|
5412
|
-
resolveRef: (ref) => exec("git", "rev-parse", "--verify", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
|
|
5421
|
+
resolveRef: (ref) => exec("git", "rev-parse", "--verify", "--end-of-options", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
|
|
5413
5422
|
readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll$1(() => succeed$6(none()))),
|
|
5414
5423
|
isAncestor: (a, b) => run(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$1(CommandExecutor, executor)), map$4(() => true), catchAll$1(() => succeed$6(false))),
|
|
5415
5424
|
mergeBase: (a, b) => exec("git", "merge-base", a, b).pipe(map$4((s) => some(s.trim())), catchAll$1(() => succeed$6(none()))),
|
|
@@ -6059,6 +6068,9 @@ const loopLogPath = gen(function* () {
|
|
|
6059
6068
|
//#region src/platform/RetainedHistory.ts
|
|
6060
6069
|
/** Per-worktree (`refs/worktree/gtd/*`) so linked worktrees sharing one `.git` don't clobber each other's retained history. */
|
|
6061
6070
|
const HISTORY_REF = "refs/worktree/gtd/history";
|
|
6071
|
+
/** One ref per finished process, kept past the branch's deletion; distinct from the per-worktree `HISTORY_REF`. */
|
|
6072
|
+
const PROCESS_HISTORY_PREFIX = "refs/gtd/history/";
|
|
6073
|
+
const processHistoryRef = (hash) => `${PROCESS_HISTORY_PREFIX}${hash}`;
|
|
6062
6074
|
const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
|
|
6063
6075
|
/**
|
|
6064
6076
|
* Whether resetting to `tipHash` is safe: HEAD must be an ancestor of
|
|
@@ -6855,6 +6867,12 @@ const TRAILER_READERS = {
|
|
|
6855
6867
|
"Gtd-Review-Base": (value, into) => {
|
|
6856
6868
|
into.reviewBase ??= value.split(/\s/)[0];
|
|
6857
6869
|
},
|
|
6870
|
+
"Gtd-Branch": (value, into) => {
|
|
6871
|
+
if (value !== "") into.branch ??= value;
|
|
6872
|
+
},
|
|
6873
|
+
"Gtd-Version": (value, into) => {
|
|
6874
|
+
if (value !== "") into.version ??= value;
|
|
6875
|
+
},
|
|
6858
6876
|
"Gtd-Format": (value, into) => {
|
|
6859
6877
|
if (FORMAT_RE.test(value)) into.format ??= Number(value);
|
|
6860
6878
|
},
|
|
@@ -6873,6 +6891,8 @@ const parseCommitMessage = (message) => {
|
|
|
6873
6891
|
step: void 0,
|
|
6874
6892
|
workflow: void 0,
|
|
6875
6893
|
reviewBase: void 0,
|
|
6894
|
+
branch: void 0,
|
|
6895
|
+
version: void 0,
|
|
6876
6896
|
judge: [],
|
|
6877
6897
|
vars: {},
|
|
6878
6898
|
config: {},
|
|
@@ -6898,6 +6918,8 @@ const formatCommitMessage = (spec) => {
|
|
|
6898
6918
|
for (const [path, hash] of sortedEntries(spec.config)) lines.push(`Gtd-Config: ${hash} ${encodePath(path)}`);
|
|
6899
6919
|
if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
|
|
6900
6920
|
for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`);
|
|
6921
|
+
if (spec.branch !== void 0) lines.push(`Gtd-Branch: ${spec.branch}`);
|
|
6922
|
+
if (spec.version !== void 0) lines.push(`Gtd-Version: ${spec.version}`);
|
|
6901
6923
|
lines.push(`Gtd-Format: 1`);
|
|
6902
6924
|
return `${formatSubject(spec.actor, spec.to, spec.from)}\n\n${lines.join("\n")}`;
|
|
6903
6925
|
};
|
|
@@ -8206,6 +8228,14 @@ const planStep = (snapshot, opts = {}) => {
|
|
|
8206
8228
|
message
|
|
8207
8229
|
}
|
|
8208
8230
|
},
|
|
8231
|
+
...landing.keepHistory !== void 0 ? [{
|
|
8232
|
+
kind: "gitWrite",
|
|
8233
|
+
write: {
|
|
8234
|
+
kind: "updateRef",
|
|
8235
|
+
ref: landing.keepHistory,
|
|
8236
|
+
target: "HEAD"
|
|
8237
|
+
}
|
|
8238
|
+
}] : [],
|
|
8209
8239
|
outcome
|
|
8210
8240
|
]
|
|
8211
8241
|
};
|
|
@@ -8513,25 +8543,38 @@ const currentRun = gen(function* () {
|
|
|
8513
8543
|
const config = yield* (yield* ConfigService).load;
|
|
8514
8544
|
return yield* computeProcessRun(git, config.workflow);
|
|
8515
8545
|
});
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
const git = yield* GitService;
|
|
8519
|
-
const def = (yield* (yield* ConfigService).load).workflow;
|
|
8520
|
-
const headEntry = (yield* git.subjectHistory(1, 0))[0];
|
|
8521
|
-
if (headEntry === void 0) {
|
|
8546
|
+
const runAtTip = (git, def, tip) => gen(function* () {
|
|
8547
|
+
if (tip === void 0) {
|
|
8522
8548
|
const history = yield* historyUpTo(git, def, void 0);
|
|
8523
8549
|
return yield* runOf(history, locateEpisode(def, history));
|
|
8524
8550
|
}
|
|
8525
|
-
if (!(parseCommitMessage(
|
|
8526
|
-
const history = yield* historyUpTo(git, def,
|
|
8551
|
+
if (!(parseCommitMessage(tip.subject).parsed?.to === def.initial)) {
|
|
8552
|
+
const history = yield* historyUpTo(git, def, tip.hash);
|
|
8527
8553
|
return yield* runOf(history, locateEpisode(def, history));
|
|
8528
8554
|
}
|
|
8529
|
-
const parentRef = `${
|
|
8555
|
+
const parentRef = `${tip.hash}~1`;
|
|
8530
8556
|
const before = yield* historyUpTo(git, def, parentRef);
|
|
8531
8557
|
const location = locateEpisode(def, before);
|
|
8532
|
-
let closing = yield* git.commitHistory(parentRef,
|
|
8533
|
-
if (closing.length === 0) closing = yield* git.commitHistory(void 0,
|
|
8534
|
-
return yield* runOf([...before, ...closing], { ...location },
|
|
8558
|
+
let closing = yield* git.commitHistory(parentRef, tip.hash);
|
|
8559
|
+
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
8560
|
+
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
8561
|
+
});
|
|
8562
|
+
/** The process HEAD closes or sits inside — `gtd summary`'s run. */
|
|
8563
|
+
const summaryRun = gen(function* () {
|
|
8564
|
+
const git = yield* GitService;
|
|
8565
|
+
const def = (yield* (yield* ConfigService).load).workflow;
|
|
8566
|
+
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
|
|
8567
|
+
});
|
|
8568
|
+
/** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
|
|
8569
|
+
const closedRunAt = (tip) => gen(function* () {
|
|
8570
|
+
const git = yield* GitService;
|
|
8571
|
+
const def = (yield* (yield* ConfigService).load).workflow;
|
|
8572
|
+
const hash = yield* git.resolveRef(tip);
|
|
8573
|
+
const subject = yield* git.lastCommitSubject(hash);
|
|
8574
|
+
return yield* runAtTip(git, def, {
|
|
8575
|
+
hash,
|
|
8576
|
+
subject
|
|
8577
|
+
});
|
|
8535
8578
|
});
|
|
8536
8579
|
const commitTree = (workspace, hash) => {
|
|
8537
8580
|
let entries;
|
|
@@ -8751,13 +8794,7 @@ const costByModel = (entries) => {
|
|
|
8751
8794
|
cost
|
|
8752
8795
|
})).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
|
|
8753
8796
|
};
|
|
8754
|
-
|
|
8755
|
-
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
8756
|
-
const git = yield* GitService;
|
|
8757
|
-
const config = yield* (yield* ConfigService).load;
|
|
8758
|
-
const workspace = yield* Workspace;
|
|
8759
|
-
const host = yield* Host;
|
|
8760
|
-
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
8797
|
+
const setupFor = (config, run, workspace, host, startOverrides = {}) => gen(function* () {
|
|
8761
8798
|
const resolved = yield* workflowOf(config, run);
|
|
8762
8799
|
const def = resolved.def;
|
|
8763
8800
|
const { vars, env } = settingsFor(resolved, run.pinnedVars, host.env, startOverrides);
|
|
@@ -8781,19 +8818,34 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
8781
8818
|
tree: commitTree(workspace, c.hash)
|
|
8782
8819
|
}));
|
|
8783
8820
|
let pendingMemo;
|
|
8784
|
-
|
|
8821
|
+
return {
|
|
8822
|
+
resolved,
|
|
8785
8823
|
def,
|
|
8786
|
-
run,
|
|
8787
8824
|
vars,
|
|
8788
8825
|
env,
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8826
|
+
setup: {
|
|
8827
|
+
def,
|
|
8828
|
+
run,
|
|
8829
|
+
vars,
|
|
8830
|
+
env,
|
|
8831
|
+
budget,
|
|
8832
|
+
skills,
|
|
8833
|
+
access,
|
|
8834
|
+
workspace,
|
|
8835
|
+
base,
|
|
8836
|
+
episode,
|
|
8837
|
+
pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
|
|
8838
|
+
}
|
|
8796
8839
|
};
|
|
8840
|
+
});
|
|
8841
|
+
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
8842
|
+
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
8843
|
+
const git = yield* GitService;
|
|
8844
|
+
const config = yield* (yield* ConfigService).load;
|
|
8845
|
+
const workspace = yield* Workspace;
|
|
8846
|
+
const host = yield* Host;
|
|
8847
|
+
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
8848
|
+
const { resolved, def, vars, env, setup } = yield* setupFor(config, run, workspace, host, startOverrides);
|
|
8797
8849
|
const outcome = yield* promise(() => replayFor(setup));
|
|
8798
8850
|
const error = replayError(outcome);
|
|
8799
8851
|
if (error !== void 0 || outcome.kind !== "rest") return yield* fail$5(error ?? /* @__PURE__ */ new Error("gtd: replay found no rest"));
|
|
@@ -8935,14 +8987,21 @@ const initialStepRefusal = (workflow, step, initial) => workflow !== void 0 && s
|
|
|
8935
8987
|
kind: "refusal",
|
|
8936
8988
|
message: `gtd: workflow "${workflow}" reaches step "${step}", the default workflow's first step — that is where a finished process waits, so a started process must never land there; rename the step`
|
|
8937
8989
|
} : void 0;
|
|
8938
|
-
const
|
|
8939
|
-
|
|
8990
|
+
const closingTrailers = (branch) => ({
|
|
8991
|
+
version: GTD_VERSION,
|
|
8992
|
+
...isSome(branch) ? { branch: branch.value } : {}
|
|
8993
|
+
});
|
|
8994
|
+
const isSettledNoop = (rest, to) => rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state;
|
|
8995
|
+
const landingTo = (rest, to, branch) => {
|
|
8996
|
+
if (isSettledNoop(rest, to)) return {
|
|
8940
8997
|
kind: "noop",
|
|
8941
8998
|
settled: true
|
|
8942
8999
|
};
|
|
8943
|
-
const
|
|
9000
|
+
const underway = !noProcessUnderway(rest);
|
|
9001
|
+
const starts = !underway && to !== rest.def.initial;
|
|
8944
9002
|
const refused = starts ? multilineRefusal(rest.vars) : void 0;
|
|
8945
9003
|
if (refused !== void 0) return refused;
|
|
9004
|
+
const finishes = underway && to === rest.def.initial;
|
|
8946
9005
|
return {
|
|
8947
9006
|
kind: "commit",
|
|
8948
9007
|
to,
|
|
@@ -8954,8 +9013,10 @@ const landingTo = (rest, to) => {
|
|
|
8954
9013
|
...starts ? {
|
|
8955
9014
|
vars: rest.vars,
|
|
8956
9015
|
config: rest.config
|
|
8957
|
-
} : {}
|
|
8958
|
-
|
|
9016
|
+
} : {},
|
|
9017
|
+
...finishes ? closingTrailers(branch) : {}
|
|
9018
|
+
},
|
|
9019
|
+
...finishes ? { keepHistory: processHistoryRef(rest.run.startHash) } : {}
|
|
8959
9020
|
};
|
|
8960
9021
|
};
|
|
8961
9022
|
/**
|
|
@@ -8963,7 +9024,7 @@ const landingTo = (rest, to) => {
|
|
|
8963
9024
|
* replay with the working tree as the pending turn. The target step is
|
|
8964
9025
|
* computed by replaying, never matched.
|
|
8965
9026
|
*/
|
|
8966
|
-
const decideLanding = (rest, verdicts) => gen(function* () {
|
|
9027
|
+
const decideLanding = (rest, verdicts, branch) => gen(function* () {
|
|
8967
9028
|
const early = cleanLanding(rest);
|
|
8968
9029
|
if (early !== void 0) return early;
|
|
8969
9030
|
if (rest.stepDef.kind === "prompt" && rest.stepDef.access?.write != null) {
|
|
@@ -8979,13 +9040,14 @@ const decideLanding = (rest, verdicts) => gen(function* () {
|
|
|
8979
9040
|
}));
|
|
8980
9041
|
const to = yield* landingTarget(outcome, rest);
|
|
8981
9042
|
if (typeof to !== "string") return to;
|
|
8982
|
-
return landingTo(rest, to);
|
|
9043
|
+
return landingTo(rest, to, branch);
|
|
8983
9044
|
});
|
|
8984
9045
|
/** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
|
|
8985
|
-
const previewLanding = (rest) => decideLanding(rest, void 0).pipe(map$4((landing) => landing.kind === "commit" ? landing.to : void 0), catchAll$1(() => succeed$6(void 0)));
|
|
9046
|
+
const previewLanding = (rest) => decideLanding(rest, void 0, none()).pipe(map$4((landing) => landing.kind === "commit" ? landing.to : void 0), catchAll$1(() => succeed$6(void 0)));
|
|
8986
9047
|
/** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
|
|
8987
9048
|
const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
8988
|
-
const
|
|
9049
|
+
const branch = yield* (yield* GitService).currentBranch();
|
|
9050
|
+
const landing = yield* decideLanding(rest, verdicts, branch);
|
|
8989
9051
|
return {
|
|
8990
9052
|
stepDef: rest.stepDef,
|
|
8991
9053
|
state: rest.state,
|
|
@@ -9016,6 +9078,66 @@ const summaryFor = (run) => gen(function* () {
|
|
|
9016
9078
|
...settingsFor(resolved, run.pinnedVars, host.env)
|
|
9017
9079
|
});
|
|
9018
9080
|
});
|
|
9081
|
+
/** Repository-relative paths of the config files today's config and the process's own record name; one outside the repository cannot be read at a ref, so it is left out. */
|
|
9082
|
+
const inRepoConfigPaths = (root, files, recorded) => {
|
|
9083
|
+
const paths = new Set(Object.keys(recorded));
|
|
9084
|
+
for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
|
|
9085
|
+
return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
|
|
9086
|
+
};
|
|
9087
|
+
const configRefusal = (tip, paths) => /* @__PURE__ */ new Error(`gtd history: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. A history ref replays under today's config, so it is unreadable after the config changes`);
|
|
9088
|
+
const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
|
|
9089
|
+
const differing = [];
|
|
9090
|
+
for (const path of inRepoConfigPaths(root, config.workflowFiles, run.pinnedConfig?.files ?? {})) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
|
|
9091
|
+
return differing;
|
|
9092
|
+
});
|
|
9093
|
+
const notClosedError = (hash, outcome) => outcome.kind === "rest" ? /* @__PURE__ */ new Error(`gtd history: ${hash.slice(0, 7)} does not close a process under today's config`) : replayError(outcome) ?? /* @__PURE__ */ new Error("gtd history: replay failed");
|
|
9094
|
+
const historySteps = (run) => {
|
|
9095
|
+
const messages = new Map(run.episode.commits.map((c) => [c.hash, c.message]));
|
|
9096
|
+
return run.trace.map((entry) => {
|
|
9097
|
+
const message = parseCommitMessage(messages.get(entry.hash) ?? "");
|
|
9098
|
+
return {
|
|
9099
|
+
hash: entry.hash,
|
|
9100
|
+
subject: (messages.get(entry.hash) ?? "").split("\n")[0] ?? "",
|
|
9101
|
+
actor: entry.actor,
|
|
9102
|
+
from: message.parsed?.from,
|
|
9103
|
+
to: entry.state,
|
|
9104
|
+
verdicts: message.judge
|
|
9105
|
+
};
|
|
9106
|
+
});
|
|
9107
|
+
};
|
|
9108
|
+
const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd: warning: this history was recorded by ${recorded === void 0 ? "an unknown gtd version" : `gtd ${recorded}`} and is replayed by gtd ${GTD_VERSION} — a workflow imported from @pmelab/gtd/workflows may have changed since`;
|
|
9109
|
+
/**
|
|
9110
|
+
* Replays the closed run at `tip` under today's config. The workflow is not
|
|
9111
|
+
* fingerprinted for a version change: one imported from `@pmelab/gtd/workflows`
|
|
9112
|
+
* moves with every upgrade, and the version warning is the only signal.
|
|
9113
|
+
*/
|
|
9114
|
+
const historyTrail = (tip) => gen(function* () {
|
|
9115
|
+
const git = yield* GitService;
|
|
9116
|
+
const config = yield* (yield* ConfigService).load;
|
|
9117
|
+
const workspace = yield* Workspace;
|
|
9118
|
+
const host = yield* Host;
|
|
9119
|
+
const hash = yield* git.resolveRef(tip);
|
|
9120
|
+
const run = yield* closedRunAt(hash);
|
|
9121
|
+
const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
|
|
9122
|
+
if (differing.length > 0) return yield* fail$5(configRefusal(hash, differing));
|
|
9123
|
+
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
9124
|
+
yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
|
|
9125
|
+
const outcome = yield* promise(() => replayFor(setup));
|
|
9126
|
+
if (outcome.kind !== "ended") return yield* fail$5(notClosedError(hash, outcome));
|
|
9127
|
+
const steps = historySteps(run);
|
|
9128
|
+
const recorded = parseCommitMessage(yield* git.lastCommitMessage(hash)).version;
|
|
9129
|
+
const versionWarning = versionWarningFor(recorded);
|
|
9130
|
+
return {
|
|
9131
|
+
tip: hash,
|
|
9132
|
+
steps,
|
|
9133
|
+
ended: outcome.via,
|
|
9134
|
+
version: {
|
|
9135
|
+
recorded,
|
|
9136
|
+
running: GTD_VERSION
|
|
9137
|
+
},
|
|
9138
|
+
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
9139
|
+
};
|
|
9140
|
+
});
|
|
9019
9141
|
//#endregion
|
|
9020
9142
|
//#region src/emit/GitScript.ts
|
|
9021
9143
|
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
@@ -9037,8 +9159,8 @@ const commitAllowEmpty = (message) => {
|
|
|
9037
9159
|
`fi`
|
|
9038
9160
|
].join("\n");
|
|
9039
9161
|
};
|
|
9040
|
-
/** `git add -A
|
|
9041
|
-
const commitAll = (message) => `git add -A
|
|
9162
|
+
/** `git add -A || exit 1` (an `&&` left side would not abort under `set -e`) then `commitAllowEmpty`. */
|
|
9163
|
+
const commitAll = (message) => `git add -A || exit 1\n${commitAllowEmpty(message)}`;
|
|
9042
9164
|
/** No current caller — see `Git.ts`'s `GitWriterOperations.commitAsIs` for why it's kept anyway. */
|
|
9043
9165
|
const commitAsIs = (message) => commitAllowEmpty(message);
|
|
9044
9166
|
/** No current caller — see `Git.ts`'s `GitWriterOperations.softResetTo` for why it's kept anyway. */
|
|
@@ -9056,6 +9178,17 @@ const discardPending = () => `git add -A && git reset --hard HEAD`;
|
|
|
9056
9178
|
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
9057
9179
|
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
9058
9180
|
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
9181
|
+
/**
|
|
9182
|
+
* No `+` on the refspec: a local ref the remote's tip does not descend from
|
|
9183
|
+
* fails the fetch instead of being overwritten (a fast-forward still applies).
|
|
9184
|
+
*/
|
|
9185
|
+
const fetchHistoryRefs = (remote) => [
|
|
9186
|
+
`set -eu`,
|
|
9187
|
+
`before=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
9188
|
+
`git fetch ${shellQuote(remote)} 'refs/gtd/history/*:refs/gtd/history/*'`,
|
|
9189
|
+
`after=$(git for-each-ref --format='x' 'refs/gtd/history/' | wc -l)`,
|
|
9190
|
+
`printf '%s history refs arrived\\n' "$((after - before))"`
|
|
9191
|
+
].join("\n");
|
|
9059
9192
|
/** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/testkit/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
|
|
9060
9193
|
const OUTCOME_MARKER$1 = "# gtd: outcome (print-only)";
|
|
9061
9194
|
const printfLine$1 = (fmt, args) => `${OUTCOME_MARKER$1}\nprintf ${shellQuote(fmt.replace(/\n/g, "\\n"))} ${args.join(" ")}`;
|
|
@@ -9073,6 +9206,7 @@ const renderOutcome = (outcome) => {
|
|
|
9073
9206
|
const renderGitWrite = (write) => {
|
|
9074
9207
|
switch (write.kind) {
|
|
9075
9208
|
case "commitAll": return commitAll(write.message);
|
|
9209
|
+
case "updateRef": return updateRef(write.ref, write.target);
|
|
9076
9210
|
}
|
|
9077
9211
|
};
|
|
9078
9212
|
/**
|
|
@@ -9440,7 +9574,7 @@ const validateScriptFor = (resolved, file) => gen(function* () {
|
|
|
9440
9574
|
*/
|
|
9441
9575
|
var ToolCommands = class extends Tag("ToolCommands")() {};
|
|
9442
9576
|
//#endregion
|
|
9443
|
-
//#region src/cli/
|
|
9577
|
+
//#region src/cli/Output.ts
|
|
9444
9578
|
/**
|
|
9445
9579
|
* A command-level failure that should exit like a CLI usage error (2), not a
|
|
9446
9580
|
* generic runtime failure (1): an unknown `--json=<path>` selector, or a
|
|
@@ -9450,12 +9584,92 @@ var ToolCommands = class extends Tag("ToolCommands")() {};
|
|
|
9450
9584
|
* about the resolved rest's own state.
|
|
9451
9585
|
*/
|
|
9452
9586
|
var SelectorUsageError = class extends Error {};
|
|
9453
|
-
/** The `--json=<path>` select branch, shared by
|
|
9587
|
+
/** The `--json=<path>` select branch, shared by the emitting commands so none drift on the unknown-selector message. `absent` writes nothing and lets the caller's success path continue. */
|
|
9454
9588
|
const writeSelection = (out, fields, path) => gen(function* () {
|
|
9455
9589
|
const selection = selectPath(fields, path);
|
|
9456
9590
|
if (selection.kind === "value") out.write(`${selection.text}\n`);
|
|
9457
9591
|
else if (selection.kind === "unknown") return yield* fail$5(new SelectorUsageError(`gtd: unknown --json selector "${selection.path}" — see \`gtd --help\``));
|
|
9458
9592
|
});
|
|
9593
|
+
//#endregion
|
|
9594
|
+
//#region src/cli/History.ts
|
|
9595
|
+
/** Newest tip first. */
|
|
9596
|
+
const historyEntries = gen(function* () {
|
|
9597
|
+
const git = yield* GitService;
|
|
9598
|
+
return yield* forEach$2((yield* git.listRefs(PROCESS_HISTORY_PREFIX)).filter(({ ref }) => ENTRY_HASH.test(ref.slice(17))), ({ ref, hash, date }) => gen(function* () {
|
|
9599
|
+
const entry = ref.slice(17);
|
|
9600
|
+
return {
|
|
9601
|
+
ref,
|
|
9602
|
+
entry,
|
|
9603
|
+
branch: parseCommitMessage(yield* git.lastCommitMessage(hash)).branch ?? null,
|
|
9604
|
+
tip: hash,
|
|
9605
|
+
date,
|
|
9606
|
+
subject: yield* git.lastCommitSubject(entry)
|
|
9607
|
+
};
|
|
9608
|
+
}));
|
|
9609
|
+
});
|
|
9610
|
+
const ENTRY_HASH = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
9611
|
+
const HEX = /^[0-9a-f]+$/i;
|
|
9612
|
+
/**
|
|
9613
|
+
* Lookup order: full ref; then entry hash or hex prefix (unique → it, several
|
|
9614
|
+
* → fail listing them); then branch name (newest tip first, as `entries` is
|
|
9615
|
+
* ordered). A hex-looking name that matches no entry still resolves as a
|
|
9616
|
+
* branch. No match → fail.
|
|
9617
|
+
*/
|
|
9618
|
+
const resolveHistoryRef = (arg, entries) => {
|
|
9619
|
+
const byRef = entries.find((e) => e.ref === arg);
|
|
9620
|
+
if (byRef !== void 0) return succeed$6(byRef);
|
|
9621
|
+
if (HEX.test(arg)) {
|
|
9622
|
+
const lower = arg.toLowerCase();
|
|
9623
|
+
const matches = entries.filter((e) => e.entry.toLowerCase().startsWith(lower));
|
|
9624
|
+
if (matches.length === 1) return succeed$6(matches[0]);
|
|
9625
|
+
if (matches.length > 1) return fail$5(/* @__PURE__ */ new Error(`gtd history: "${arg}" is ambiguous — it matches:\n${matches.map((e) => ` ${e.entry} ${e.branch ?? "-"}`).join("\n")}`));
|
|
9626
|
+
}
|
|
9627
|
+
const byBranch = entries.find((e) => e.branch === arg);
|
|
9628
|
+
if (byBranch !== void 0) return succeed$6(byBranch);
|
|
9629
|
+
return fail$5(/* @__PURE__ */ new Error(`gtd history: no history ref matches "${arg}"`));
|
|
9630
|
+
};
|
|
9631
|
+
const seven = (hash) => hash.slice(0, 7);
|
|
9632
|
+
/** Writes `document` per `json` (bare → whole, `=path` → one value); returns false for `off`. */
|
|
9633
|
+
const writeJson = (json, out, document) => gen(function* () {
|
|
9634
|
+
if (json.kind === "off") return false;
|
|
9635
|
+
if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
|
|
9636
|
+
else yield* writeSelection(out, document, json.path);
|
|
9637
|
+
return true;
|
|
9638
|
+
});
|
|
9639
|
+
const runHistoryListCommand = (json, out) => gen(function* () {
|
|
9640
|
+
const entries = yield* historyEntries;
|
|
9641
|
+
if (yield* writeJson(json, out, entries)) return;
|
|
9642
|
+
if (entries.length === 0) {
|
|
9643
|
+
out.write("no history refs; run gtd history fetch | sh to pull them\n");
|
|
9644
|
+
return;
|
|
9645
|
+
}
|
|
9646
|
+
out.write(entries.map((e) => `${seven(e.entry)} ${e.branch ?? "-"} ${seven(e.tip)} ${e.date} ${e.subject}\n`).join(""));
|
|
9647
|
+
});
|
|
9648
|
+
const runHistoryShowCommand = (ref, json, out) => gen(function* () {
|
|
9649
|
+
const entry = yield* resolveHistoryRef(ref, yield* historyEntries);
|
|
9650
|
+
const trail = yield* historyTrail(entry.tip);
|
|
9651
|
+
const document = {
|
|
9652
|
+
ref: entry.ref,
|
|
9653
|
+
entry: entry.entry,
|
|
9654
|
+
branch: entry.branch,
|
|
9655
|
+
...trail
|
|
9656
|
+
};
|
|
9657
|
+
if (yield* writeJson(json, out, document)) return;
|
|
9658
|
+
if (trail.versionWarning !== void 0) yield* (yield* Narrator).warn(trail.versionWarning);
|
|
9659
|
+
const lines = [`${entry.ref} (branch ${entry.branch ?? "-"}, tip ${seven(entry.tip)})`];
|
|
9660
|
+
for (const step of trail.steps) {
|
|
9661
|
+
lines.push(`${seven(step.hash)} ${step.subject}`);
|
|
9662
|
+
for (const v of step.verdicts) lines.push(` judge ${v.id} = ${v.answer} (p=${v.p})`);
|
|
9663
|
+
}
|
|
9664
|
+
lines.push(`ended (${trail.ended})`);
|
|
9665
|
+
out.write(`${lines.join("\n")}\n`);
|
|
9666
|
+
});
|
|
9667
|
+
/** Prints the script and never runs git: gtd's promise that it only decides and prints. */
|
|
9668
|
+
const runHistoryFetchCommand = (remote, out) => sync$2(() => {
|
|
9669
|
+
out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`);
|
|
9670
|
+
});
|
|
9671
|
+
//#endregion
|
|
9672
|
+
//#region src/cli/Program.ts
|
|
9459
9673
|
const selfGtd = () => {
|
|
9460
9674
|
const script = process.argv[1];
|
|
9461
9675
|
return script === void 0 ? ["gtd"] : [process.execPath, script];
|
|
@@ -10074,6 +10288,9 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
10074
10288
|
case "judge": return runJudgeCommand(json, out);
|
|
10075
10289
|
case "judgeAnswer": return runJudgeAnswerCommand(json, out);
|
|
10076
10290
|
case "judgeRun": return runJudgeRunCommand(command, out);
|
|
10291
|
+
case "historyList": return runHistoryListCommand(json, out);
|
|
10292
|
+
case "historyShow": return runHistoryShowCommand(command.ref, json, out);
|
|
10293
|
+
case "historyFetch": return runHistoryFetchCommand(command.remote, out);
|
|
10077
10294
|
}
|
|
10078
10295
|
};
|
|
10079
10296
|
/**
|
|
@@ -10112,13 +10329,13 @@ const FLAGS = [
|
|
|
10112
10329
|
name: "--json",
|
|
10113
10330
|
arity: "optional",
|
|
10114
10331
|
repeatable: false,
|
|
10115
|
-
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors",
|
|
10332
|
+
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow",
|
|
10116
10333
|
decode: ([raw]) => right(raw ?? ""),
|
|
10117
|
-
scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
10334
|
+
scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
10118
10335
|
valueHint: "<path>",
|
|
10119
10336
|
help: [
|
|
10120
|
-
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors
|
|
10121
|
-
"structured JSON. Bare --json prints the whole document;",
|
|
10337
|
+
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
|
|
10338
|
+
"list/gtd history show only) output structured JSON. Bare --json prints the whole document;",
|
|
10122
10339
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
10123
10340
|
"kind, content, session.id) prints just that value: a",
|
|
10124
10341
|
"scalar raw and unquoted, a boolean as true/false, a list",
|
|
@@ -10626,6 +10843,44 @@ const COMMAND_ROWS = [
|
|
|
10626
10843
|
"repository. jev and llm exit 1 when they cannot answer",
|
|
10627
10844
|
"every question, with nothing on stdout"
|
|
10628
10845
|
]
|
|
10846
|
+
},
|
|
10847
|
+
{
|
|
10848
|
+
token: "history list",
|
|
10849
|
+
kind: "historyList",
|
|
10850
|
+
arity: "none",
|
|
10851
|
+
details: [
|
|
10852
|
+
"List the finished processes kept behind refs/gtd/history/*,",
|
|
10853
|
+
"newest first: entry, branch, tip, date and subject per line.",
|
|
10854
|
+
"--json prints one array of { ref, entry, branch, tip, date,",
|
|
10855
|
+
"subject }. Writes nothing and never replays; with no refs it",
|
|
10856
|
+
"hints at `gtd history fetch | sh` and still exits 0"
|
|
10857
|
+
]
|
|
10858
|
+
},
|
|
10859
|
+
{
|
|
10860
|
+
token: "history show",
|
|
10861
|
+
kind: "historyShow",
|
|
10862
|
+
arity: { name: "ref" },
|
|
10863
|
+
details: [
|
|
10864
|
+
"Replay one kept process under today's config and print its",
|
|
10865
|
+
"steps and judge verdicts. <ref> is a full ref, an entry hash",
|
|
10866
|
+
"(or unique prefix) or a branch name. Refuses (exit 1) when a",
|
|
10867
|
+
"config file differs from its content at the ref's tip. A",
|
|
10868
|
+
"version mismatch only warns on stderr. --json prints the",
|
|
10869
|
+
"trail as one document; writes nothing"
|
|
10870
|
+
]
|
|
10871
|
+
},
|
|
10872
|
+
{
|
|
10873
|
+
token: "history fetch",
|
|
10874
|
+
kind: "historyFetch",
|
|
10875
|
+
arity: {
|
|
10876
|
+
name: "remote",
|
|
10877
|
+
optional: true
|
|
10878
|
+
},
|
|
10879
|
+
details: [
|
|
10880
|
+
"Print a script that fetches every refs/gtd/history/* from",
|
|
10881
|
+
"<remote> (default origin) and reports how many arrived — run",
|
|
10882
|
+
"it with `gtd history fetch | sh`. Never runs git itself"
|
|
10883
|
+
]
|
|
10629
10884
|
}
|
|
10630
10885
|
];
|
|
10631
10886
|
const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
@@ -10645,7 +10900,7 @@ const flagHeader = (row) => {
|
|
|
10645
10900
|
};
|
|
10646
10901
|
const renderHelp = () => {
|
|
10647
10902
|
const commandBlocks = COMMAND_ROWS.map((row) => {
|
|
10648
|
-
const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : `${row.token} <${row.arity.name}>`;
|
|
10903
|
+
const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : "optional" in row.arity ? `${row.token} [${row.arity.name}]` : `${row.token} <${row.arity.name}>`;
|
|
10649
10904
|
return renderBlock(header, row.details);
|
|
10650
10905
|
});
|
|
10651
10906
|
return `Usage: gtd [command] [options]\n\nCommands:\n${[
|
|
@@ -10756,6 +11011,7 @@ const conflictViolation = (present) => {
|
|
|
10756
11011
|
const arityError = (cmd, rest, arity) => {
|
|
10757
11012
|
if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
|
|
10758
11013
|
if ("names" in arity) return pairArityError(cmd, rest, arity.names);
|
|
11014
|
+
if ("optional" in arity) return rest.length > 1 ? `gtd ${cmd}: too many arguments — expected at most one ${arity.name}, got: ${rest.join(", ")}` : void 0;
|
|
10759
11015
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
|
|
10760
11016
|
if (!("variadic" in arity) && rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
|
|
10761
11017
|
};
|
|
@@ -10806,7 +11062,9 @@ const parseArgv = (argv) => {
|
|
|
10806
11062
|
kind: "output",
|
|
10807
11063
|
stdout: renderHelp()
|
|
10808
11064
|
};
|
|
10809
|
-
const
|
|
11065
|
+
const isHistory = first === "history";
|
|
11066
|
+
const row = first === void 0 ? void 0 : commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first);
|
|
11067
|
+
if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref> or fetch [remote]", jsonSeen);
|
|
10810
11068
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
|
|
10811
11069
|
const selectsStart = workflowPresent && first === void 0;
|
|
10812
11070
|
const judgeRun = row?.kind === "judge" && positionals[1] === "run";
|
|
@@ -10817,11 +11075,11 @@ const parseArgv = (argv) => {
|
|
|
10817
11075
|
if (first === void 0) return usagePlan("gtd: missing command — gtd decides and prints, a driver executes; run `gtd install`, or copy one from https://github.com/pmelab/gtd/blob/main/docs/driver.md's \"A complete minimal driver\" section and run that, or see usage above (`gtd --help`)", jsonSeen, renderHelp());
|
|
10818
11076
|
return usagePlan(`unknown command '${first}'`, jsonSeen);
|
|
10819
11077
|
}
|
|
10820
|
-
const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun ? 2 : 1);
|
|
11078
|
+
const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1);
|
|
10821
11079
|
if (kind === "start" && first === void 0) {
|
|
10822
11080
|
if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
|
|
10823
11081
|
} else {
|
|
10824
|
-
const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(first, restPositionals, row.arity);
|
|
11082
|
+
const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
|
|
10825
11083
|
if (arityMsg !== void 0) return usagePlan(arityMsg, jsonSeen);
|
|
10826
11084
|
}
|
|
10827
11085
|
const violation = scopeViolation(kind, present);
|
|
@@ -10910,6 +11168,24 @@ const parseArgv = (argv) => {
|
|
|
10910
11168
|
verbose
|
|
10911
11169
|
};
|
|
10912
11170
|
}
|
|
11171
|
+
if (kind === "historyShow") return {
|
|
11172
|
+
kind: "command",
|
|
11173
|
+
command: {
|
|
11174
|
+
kind: "historyShow",
|
|
11175
|
+
ref: restPositionals[0]
|
|
11176
|
+
},
|
|
11177
|
+
json,
|
|
11178
|
+
verbose
|
|
11179
|
+
};
|
|
11180
|
+
if (kind === "historyFetch") return {
|
|
11181
|
+
kind: "command",
|
|
11182
|
+
command: {
|
|
11183
|
+
kind: "historyFetch",
|
|
11184
|
+
remote: restPositionals[0] ?? "origin"
|
|
11185
|
+
},
|
|
11186
|
+
json,
|
|
11187
|
+
verbose
|
|
11188
|
+
};
|
|
10913
11189
|
if (kind === "uncheck") return {
|
|
10914
11190
|
kind: "command",
|
|
10915
11191
|
command: {
|
|
@@ -11010,6 +11286,7 @@ var InMemRepo = class InMemRepo {
|
|
|
11010
11286
|
currentBranch = "main";
|
|
11011
11287
|
worktree = /* @__PURE__ */ new Map();
|
|
11012
11288
|
index = /* @__PURE__ */ new Map();
|
|
11289
|
+
nextSeq = 1;
|
|
11013
11290
|
getCommit(hash) {
|
|
11014
11291
|
return this.commits.get(hash) ?? null;
|
|
11015
11292
|
}
|
|
@@ -11084,10 +11361,25 @@ var InMemRepo = class InMemRepo {
|
|
|
11084
11361
|
if (!c) return null;
|
|
11085
11362
|
return c.message.split("\n")[0] ?? null;
|
|
11086
11363
|
}
|
|
11087
|
-
lastCommitMessage() {
|
|
11088
|
-
const
|
|
11089
|
-
|
|
11090
|
-
return c.message;
|
|
11364
|
+
lastCommitMessage(ref) {
|
|
11365
|
+
const hash = ref === void 0 ? this.head : this.resolveRef(ref);
|
|
11366
|
+
const c = hash === null ? null : this.getCommit(hash);
|
|
11367
|
+
return c === null ? null : c.message;
|
|
11368
|
+
}
|
|
11369
|
+
branchName() {
|
|
11370
|
+
return this.currentBranch;
|
|
11371
|
+
}
|
|
11372
|
+
/** Refs under `prefix`, newest tip first; the commit sequence number stands in for the committer date. */
|
|
11373
|
+
listRefs(prefix) {
|
|
11374
|
+
return [...this.refs.entries()].filter(([ref]) => ref.startsWith(prefix)).map(([ref, hash]) => ({
|
|
11375
|
+
ref,
|
|
11376
|
+
hash,
|
|
11377
|
+
seq: this.getCommit(hash)?.seq ?? 0
|
|
11378
|
+
})).sort((a, b) => b.seq - a.seq).map(({ ref, hash, seq }) => ({
|
|
11379
|
+
ref,
|
|
11380
|
+
hash,
|
|
11381
|
+
date: (/* @__PURE__ */ new Date(seq * 1e3)).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
11382
|
+
}));
|
|
11091
11383
|
}
|
|
11092
11384
|
/**
|
|
11093
11385
|
* `head` (default `this.head`, mirroring production's `"HEAD"` default) is
|
|
@@ -11201,7 +11493,8 @@ var InMemRepo = class InMemRepo {
|
|
|
11201
11493
|
hash,
|
|
11202
11494
|
message,
|
|
11203
11495
|
files: new Map(tree),
|
|
11204
|
-
parent
|
|
11496
|
+
parent,
|
|
11497
|
+
seq: this.nextSeq++
|
|
11205
11498
|
};
|
|
11206
11499
|
this.commits.set(hash, commit);
|
|
11207
11500
|
this.head = hash;
|
|
@@ -11222,7 +11515,8 @@ var InMemRepo = class InMemRepo {
|
|
|
11222
11515
|
hash,
|
|
11223
11516
|
message,
|
|
11224
11517
|
files: new Map(tree),
|
|
11225
|
-
parent
|
|
11518
|
+
parent,
|
|
11519
|
+
seq: this.nextSeq++
|
|
11226
11520
|
};
|
|
11227
11521
|
this.commits.set(hash, commit);
|
|
11228
11522
|
this.head = hash;
|
|
@@ -11364,10 +11658,12 @@ const makeGitReaderOps = (repo, root) => ({
|
|
|
11364
11658
|
const subject = repo.lastCommitSubject(ref);
|
|
11365
11659
|
return subject !== null ? succeed$6(subject) : fail$5(/* @__PURE__ */ new Error("No commits"));
|
|
11366
11660
|
},
|
|
11367
|
-
lastCommitMessage: () => {
|
|
11368
|
-
const message = repo.lastCommitMessage();
|
|
11661
|
+
lastCommitMessage: (ref) => {
|
|
11662
|
+
const message = repo.lastCommitMessage(ref);
|
|
11369
11663
|
return message !== null ? succeed$6(message) : fail$5(/* @__PURE__ */ new Error("No commits"));
|
|
11370
11664
|
},
|
|
11665
|
+
currentBranch: () => succeed$6(some(repo.branchName())),
|
|
11666
|
+
listRefs: (prefix) => succeed$6(repo.listRefs(prefix)),
|
|
11371
11667
|
resolveRef: (ref) => {
|
|
11372
11668
|
const hash = repo.resolveRef(ref);
|
|
11373
11669
|
return hash !== null ? succeed$6(hash) : fail$5(/* @__PURE__ */ new Error(`Cannot resolve ref: ${ref}`));
|
|
@@ -11647,7 +11943,7 @@ const recognizeGitBuilders = (repo, block) => {
|
|
|
11647
11943
|
return { kind: "applied" };
|
|
11648
11944
|
}
|
|
11649
11945
|
const conditionLine = conditionStatement(block);
|
|
11650
|
-
if (block.startsWith("git add -A
|
|
11946
|
+
if (block.startsWith("git add -A || exit 1\n")) {
|
|
11651
11947
|
const [message] = extractQuotedTokens(conditionLine);
|
|
11652
11948
|
if (message !== void 0 && commitAll(message) === block) {
|
|
11653
11949
|
repo.commitAllWithPrefix(message);
|
|
@@ -12212,6 +12508,21 @@ const gitExecIn = (dir, ...args) => execSync(`git ${args.join(" ")}`, {
|
|
|
12212
12508
|
const makeLiveTier = (initialCommit = true) => {
|
|
12213
12509
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "gtd-git-tier-")));
|
|
12214
12510
|
const gitExec = (...args) => gitExecIn(root, ...args);
|
|
12511
|
+
let tick = 0;
|
|
12512
|
+
const commitEnv = () => ({
|
|
12513
|
+
...process.env,
|
|
12514
|
+
GIT_COMMITTER_DATE: new Date(Date.UTC(2024, 0, 1, 0, 0, ++tick)).toISOString()
|
|
12515
|
+
});
|
|
12516
|
+
const gitCommit = (message, allowEmpty = false) => execFileSync("git", [
|
|
12517
|
+
"commit",
|
|
12518
|
+
...allowEmpty ? ["--allow-empty"] : [],
|
|
12519
|
+
"-m",
|
|
12520
|
+
message
|
|
12521
|
+
], {
|
|
12522
|
+
cwd: root,
|
|
12523
|
+
env: commitEnv(),
|
|
12524
|
+
stdio: "pipe"
|
|
12525
|
+
});
|
|
12215
12526
|
gitExec("init");
|
|
12216
12527
|
gitExec(`config user.email "test@test.com"`);
|
|
12217
12528
|
gitExec(`config user.name "Test"`);
|
|
@@ -12219,7 +12530,7 @@ const makeLiveTier = (initialCommit = true) => {
|
|
|
12219
12530
|
if (initialCommit) {
|
|
12220
12531
|
writeFileSync(join(root, "readme.txt"), "hello");
|
|
12221
12532
|
gitExec("add", "-A");
|
|
12222
|
-
|
|
12533
|
+
gitCommit("init: first commit");
|
|
12223
12534
|
}
|
|
12224
12535
|
const provide = (eff) => runPromise(eff.pipe(provide$2(GitService.Live), provide$2(Host.layer({
|
|
12225
12536
|
root,
|
|
@@ -12250,7 +12561,7 @@ const makeLiveTier = (initialCommit = true) => {
|
|
|
12250
12561
|
writeFileSync(join(root, path), content);
|
|
12251
12562
|
}
|
|
12252
12563
|
gitExec("add", "-A");
|
|
12253
|
-
|
|
12564
|
+
gitCommit(message, true);
|
|
12254
12565
|
},
|
|
12255
12566
|
writeFile: (path, content) => {
|
|
12256
12567
|
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
@@ -12414,6 +12725,54 @@ const runGitServiceContract = (makeTier) => {
|
|
|
12414
12725
|
expect(await runGit(t, (g) => g.lastCommitMessage())).toBe("feat: subject only");
|
|
12415
12726
|
});
|
|
12416
12727
|
});
|
|
12728
|
+
describe("lastCommitMessage with a ref", () => {
|
|
12729
|
+
it("reads the given ref's full message, not HEAD's", async () => {
|
|
12730
|
+
t.seed.commit("feat: first\n\nGtd-Branch: one");
|
|
12731
|
+
t.seed.commit("feat: second", { "extra.txt": "extra" });
|
|
12732
|
+
expect(await runGit(t, (g) => g.lastCommitMessage("HEAD~1"))).toBe("feat: first\n\nGtd-Branch: one");
|
|
12733
|
+
});
|
|
12734
|
+
});
|
|
12735
|
+
describe("currentBranch", () => {
|
|
12736
|
+
it("is the checked-out branch's name", async () => {
|
|
12737
|
+
expect(isSome(await runGit(t, (g) => g.currentBranch()))).toBe(true);
|
|
12738
|
+
});
|
|
12739
|
+
it.skipIf(!capabilities.onDiskContent)("is none on a detached HEAD, never a failure", async () => {
|
|
12740
|
+
gitExecIn(t.root, "checkout", "--detach", "HEAD");
|
|
12741
|
+
expect(isNone(await runGit(t, (g) => g.currentBranch()))).toBe(true);
|
|
12742
|
+
});
|
|
12743
|
+
});
|
|
12744
|
+
describe("listRefs", () => {
|
|
12745
|
+
it("is [] when no ref lives under the prefix", async () => {
|
|
12746
|
+
expect(await runGit(t, (g) => g.listRefs("refs/gtd/history/"))).toEqual([]);
|
|
12747
|
+
});
|
|
12748
|
+
it("lists {ref, hash, date} for refs under the prefix only, newest tip first", async () => {
|
|
12749
|
+
const first = t.observe.resolveRef("HEAD");
|
|
12750
|
+
t.seed.commit("feat: second", { "second.txt": "2" });
|
|
12751
|
+
const second = t.observe.resolveRef("HEAD");
|
|
12752
|
+
t.seed.updateRef("refs/gtd/history/old", first);
|
|
12753
|
+
t.seed.updateRef("refs/gtd/history/new", second);
|
|
12754
|
+
t.seed.updateRef("refs/gtd/other/elsewhere", second);
|
|
12755
|
+
const refs = await runGit(t, (g) => g.listRefs("refs/gtd/history/"));
|
|
12756
|
+
expect(refs.map((r) => [r.ref, r.hash])).toEqual([["refs/gtd/history/new", second], ["refs/gtd/history/old", first]]);
|
|
12757
|
+
for (const r of refs) expect(r.date.length).toBeGreaterThan(0);
|
|
12758
|
+
});
|
|
12759
|
+
it("reports the date as ISO 8601 strict", async () => {
|
|
12760
|
+
t.seed.updateRef("refs/gtd/history/x", t.observe.resolveRef("HEAD"));
|
|
12761
|
+
const [entry] = await runGit(t, (g) => g.listRefs("refs/gtd/history/"));
|
|
12762
|
+
expect(entry?.date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/);
|
|
12763
|
+
});
|
|
12764
|
+
});
|
|
12765
|
+
describe("history reachable only from a retained ref", () => {
|
|
12766
|
+
it("commitHistory and readFileAtRef read it after HEAD moved off those commits", async () => {
|
|
12767
|
+
t.seed.commit("feat: second", { "kept.txt": "kept" });
|
|
12768
|
+
const tip = t.observe.resolveRef("HEAD");
|
|
12769
|
+
t.seed.updateRef("refs/gtd/history/tip", tip);
|
|
12770
|
+
t.seed.mixedReset("HEAD~1");
|
|
12771
|
+
expect(t.observe.resolveRef("HEAD")).not.toBe(tip);
|
|
12772
|
+
expect((await runGit(t, (g) => g.commitHistory(void 0, "refs/gtd/history/tip"))).map((c) => c.message)).toEqual(["init: first commit", "feat: second"]);
|
|
12773
|
+
expect(await runGit(t, (g) => g.readFileAtRef("refs/gtd/history/tip", "kept.txt"))).toBe("kept");
|
|
12774
|
+
});
|
|
12775
|
+
});
|
|
12417
12776
|
describe("hasCommits", () => {
|
|
12418
12777
|
it("returns false in an empty repo", async () => {
|
|
12419
12778
|
const empty = t.emptyRepo();
|
|
@@ -12974,6 +13333,11 @@ const runGitScriptContract = () => {
|
|
|
12974
13333
|
expect(t.observe.refExists("refs/gtd/marker")).toBe(true);
|
|
12975
13334
|
expect(t.observe.resolveRef("refs/gtd/marker")).toBe(head);
|
|
12976
13335
|
});
|
|
13336
|
+
it("updateRef with HEAD points the ref at the commit just made", () => {
|
|
13337
|
+
execScript(t, commitAll("feat: just made"));
|
|
13338
|
+
execScript(t, updateRef("refs/gtd/history/x", "HEAD"));
|
|
13339
|
+
expect(t.observe.resolveRef("refs/gtd/history/x")).toBe(t.observe.resolveRef("HEAD"));
|
|
13340
|
+
});
|
|
12977
13341
|
it("deleteRef removes an existing ref, and tolerates deleting it again", () => {
|
|
12978
13342
|
const head = t.observe.resolveRef("HEAD");
|
|
12979
13343
|
execScript(t, updateRef("refs/gtd/marker", head));
|
|
@@ -12986,6 +13350,54 @@ const runGitScriptContract = () => {
|
|
|
12986
13350
|
expect(t.observe.refExists("refs/gtd/never-created")).toBe(false);
|
|
12987
13351
|
});
|
|
12988
13352
|
});
|
|
13353
|
+
describe("fetchHistoryRefs", () => {
|
|
13354
|
+
it("pulls every refs/gtd/history/* from a bare remote and prints the count", () => {
|
|
13355
|
+
const bare = realpathSync(mkdtempSync(join(tmpdir(), "gtd-git-tier-bare-")));
|
|
13356
|
+
try {
|
|
13357
|
+
gitExecIn(bare, "init", "-q", "--bare");
|
|
13358
|
+
gitExecIn(t.root, "remote", "add", "origin", bare);
|
|
13359
|
+
const head = t.observe.resolveRef("HEAD");
|
|
13360
|
+
gitExecIn(t.root, "push", "-q", "origin", `${head}:refs/gtd/history/one`);
|
|
13361
|
+
gitExecIn(t.root, "push", "-q", "origin", `${head}:refs/gtd/history/two`);
|
|
13362
|
+
gitExecIn(t.root, "push", "-q", "origin", `${head}:refs/gtd/other/skip`);
|
|
13363
|
+
const out = execFileSync("bash", ["-c", fetchHistoryRefs("origin")], {
|
|
13364
|
+
cwd: t.root,
|
|
13365
|
+
encoding: "utf8"
|
|
13366
|
+
});
|
|
13367
|
+
expect(t.observe.refExists("refs/gtd/history/one")).toBe(true);
|
|
13368
|
+
expect(t.observe.refExists("refs/gtd/history/two")).toBe(true);
|
|
13369
|
+
expect(t.observe.refExists("refs/gtd/other/skip")).toBe(false);
|
|
13370
|
+
expect(out.trim()).toBe("2 history refs arrived");
|
|
13371
|
+
} finally {
|
|
13372
|
+
rmSync(bare, {
|
|
13373
|
+
recursive: true,
|
|
13374
|
+
force: true
|
|
13375
|
+
});
|
|
13376
|
+
}
|
|
13377
|
+
});
|
|
13378
|
+
it("fails loudly when a local history ref conflicts with the remote's", () => {
|
|
13379
|
+
const bare = realpathSync(mkdtempSync(join(tmpdir(), "gtd-git-tier-bare-")));
|
|
13380
|
+
try {
|
|
13381
|
+
gitExecIn(bare, "init", "-q", "--bare");
|
|
13382
|
+
gitExecIn(t.root, "remote", "add", "origin", bare);
|
|
13383
|
+
const first = t.observe.resolveRef("HEAD");
|
|
13384
|
+
t.seed.commit("feat: second", { "second.txt": "x" });
|
|
13385
|
+
const second = t.observe.resolveRef("HEAD");
|
|
13386
|
+
gitExecIn(t.root, "push", "-q", "origin", `${first}:refs/gtd/history/one`);
|
|
13387
|
+
t.seed.updateRef("refs/gtd/history/one", second);
|
|
13388
|
+
expect(() => execFileSync("bash", ["-c", fetchHistoryRefs("origin")], {
|
|
13389
|
+
cwd: t.root,
|
|
13390
|
+
stdio: "pipe"
|
|
13391
|
+
})).toThrow();
|
|
13392
|
+
expect(t.observe.resolveRef("refs/gtd/history/one")).toBe(second);
|
|
13393
|
+
} finally {
|
|
13394
|
+
rmSync(bare, {
|
|
13395
|
+
recursive: true,
|
|
13396
|
+
force: true
|
|
13397
|
+
});
|
|
13398
|
+
}
|
|
13399
|
+
});
|
|
13400
|
+
});
|
|
12989
13401
|
};
|
|
12990
13402
|
//#endregion
|
|
12991
13403
|
//#region src/testkit/StrictGitOperations.ts
|